Skip to content

Commit 477122e

Browse files
committed
return dedicated error types, iri checks, xsd:integer and xsd:float fixes
1 parent 82c52dc commit 477122e

4 files changed

Lines changed: 111 additions & 82 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ categories = ["command-line-utilities", "encoding", "parser-implementations", "s
1414
clap = { version = "4.6", features = ["derive"] }
1515
oxrdf = "0.3"
1616
serde_json = "1.0"
17+
thiserror = "2"
1718

1819
[dev-dependencies]
1920
oxrdfio = "0.2"

src/lib.rs

Lines changed: 103 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,34 @@
1818
//! - Allows specifying a custom RDF namespace for generated predicates and objects.
1919
//! - Outputs the RDF data to a specified file or prints it to the console.
2020
21-
use clap::Error;
2221
use oxrdf::vocab::xsd;
23-
use oxrdf::{BlankNode, Graph, Literal, NamedNodeRef, TripleRef};
22+
use oxrdf::{BlankNode, Graph, IriParseError, Literal, NamedNodeRef, TripleRef};
2423

2524
use serde_json::{Deserializer, Value};
2625
use std::collections::VecDeque;
2726
use std::fs::{File, OpenOptions};
2827
use std::io::{BufReader, Write};
28+
use thiserror::Error;
29+
30+
/// Errors that can occur while converting JSON to RDF.
31+
#[derive(Debug, Error)]
32+
pub enum Json2RdfError {
33+
/// Failure opening, reading, or writing a file.
34+
#[error("I/O error: {0}")]
35+
Io(#[from] std::io::Error),
36+
37+
/// Failure parsing the input JSON.
38+
#[error("JSON parse error: {0}")]
39+
Json(#[from] serde_json::Error),
40+
41+
/// A JSON key produced a string that is not a valid IRI.
42+
#[error("invalid IRI {iri:?} generated from JSON key: {source}")]
43+
InvalidIri {
44+
iri: String,
45+
#[source]
46+
source: IriParseError,
47+
},
48+
}
2949

3050
/// Converts JSON data to RDF format.
3151
///
@@ -38,24 +58,32 @@ use std::io::{BufReader, Write};
3858
/// - `namespace`: Optional custom namespace for RDF predicates.
3959
/// - `output_file`: Optional output file path for writing RDF data.
4060
///
61+
/// # Errors
62+
/// Returns [`Json2RdfError`] if the input file cannot be read, the JSON cannot be parsed,
63+
/// the output file cannot be written, or a JSON key produces an invalid IRI.
64+
///
4165
/// # Example
4266
/// ```rust
4367
/// use json2rdf::json_to_rdf;
4468
///
45-
/// json_to_rdf(&"tests/airplane.json".to_string(), &Some("http://example.com/ns#".to_string()), &Some("output.nt".to_string()));
69+
/// json_to_rdf(
70+
/// &"tests/airplane.json".to_string(),
71+
/// &Some("http://example.com/ns#".to_string()),
72+
/// &Some("output.nt".to_string()),
73+
/// ).expect("conversion failed");
4674
/// ```
4775
pub fn json_to_rdf(
4876
file_path: &String,
4977
namespace: &Option<String>,
5078
output_file: &Option<String>,
51-
) -> Result<Option<Graph>, Error> {
79+
) -> Result<Option<Graph>, Json2RdfError> {
5280
let rdf_namespace: String = if namespace.is_some() {
5381
namespace.clone().unwrap()
5482
} else {
5583
"https://decisym.ai/json2rdf/model".to_owned()
5684
};
5785

58-
let file = File::open(file_path).unwrap();
86+
let file = File::open(file_path)?;
5987
let reader = BufReader::new(file);
6088
let stream = Deserializer::from_reader(reader).into_iter::<Value>();
6189

@@ -65,10 +93,11 @@ pub fn json_to_rdf(
6593
let mut property: Option<String> = None;
6694

6795
for value in stream {
96+
let value = value?;
6897
match value {
69-
Ok(Value::Object(obj)) => {
98+
Value::Object(obj) => {
7099
let subject = BlankNode::default(); // Create a new blank node
71-
subject_stack.push_back(subject.clone());
100+
subject_stack.push_back(subject);
72101

73102
for (key, val) in obj {
74103
property = Some(format!("{}/{}", rdf_namespace, key));
@@ -78,33 +107,30 @@ pub fn json_to_rdf(
78107
val,
79108
&mut graph,
80109
&rdf_namespace,
81-
);
110+
)?;
82111
}
83112

84113
subject_stack.pop_back();
85114
}
86-
Ok(Value::Array(arr)) => {
115+
Value::Array(arr) => {
87116
for val in arr {
88117
process_value(
89118
&mut subject_stack,
90119
&property,
91120
val,
92121
&mut graph,
93-
&rdf_namespace.clone(),
94-
);
122+
&rdf_namespace,
123+
)?;
95124
}
96125
}
97-
Ok(other) => {
126+
other => {
98127
process_value(
99128
&mut subject_stack,
100129
&property,
101130
other,
102131
&mut graph,
103-
&rdf_namespace.clone(),
104-
);
105-
}
106-
Err(e) => {
107-
eprintln!("Error parsing JSON: {}", e);
132+
&rdf_namespace,
133+
)?;
108134
}
109135
}
110136
}
@@ -113,10 +139,9 @@ pub fn json_to_rdf(
113139
let mut file = OpenOptions::new()
114140
.create(true)
115141
.append(true)
116-
.open(output_path)
117-
.expect("Error opening file");
142+
.open(output_path)?;
118143

119-
writeln!(file, "{}", graph).expect("Error writing json2rdf data to file");
144+
writeln!(file, "{}", graph)?;
120145
Ok(None)
121146
} else {
122147
Ok(Some(graph))
@@ -147,77 +172,77 @@ pub fn json_to_rdf(
147172
/// - **Array**: Iterates over elements and processes each as a separate value.
148173
/// - **String**: Converts to `xsd:string` literal.
149174
/// - **Boolean**: Converts to `xsd:boolean` literal.
150-
/// - **Number**: Converts to `xsd:int` or `xsd:float` literal based on value type.
175+
/// - **Number**: Converts to `xsd:integer` for whole numbers, `xsd:double` for floating-point values.
151176
fn process_value(
152177
subject_stack: &mut VecDeque<BlankNode>,
153178
property: &Option<String>,
154179
value: Value,
155180
graph: &mut Graph,
156181
namespace: &String,
157-
) {
182+
) -> Result<(), Json2RdfError> {
158183
let ns = if namespace.ends_with("/") {
159184
namespace
160185
} else {
161186
&([namespace, "/"].join(""))
162187
};
163188

164-
if let Some(last_subject) = subject_stack.clone().back() {
165-
if let Some(prop) = property {
166-
match value {
167-
Value::Bool(b) => {
168-
graph.insert(TripleRef::new(
169-
subject_stack.back().unwrap(),
170-
NamedNodeRef::new(prop.as_str()).unwrap(),
171-
&Literal::new_typed_literal(b.to_string(), xsd::BOOLEAN),
172-
));
173-
}
174-
Value::Number(num) => {
175-
if num.as_i64().is_some() {
176-
graph.insert(TripleRef::new(
177-
subject_stack.back().unwrap(),
178-
NamedNodeRef::new(prop.as_str()).unwrap(),
179-
&Literal::new_typed_literal(num.to_string(), xsd::INT),
180-
));
181-
} else if num.as_f64().is_some() {
182-
graph.insert(TripleRef::new(
183-
subject_stack.back().unwrap(),
184-
NamedNodeRef::new(prop.as_str()).unwrap(),
185-
&Literal::new_typed_literal(num.to_string(), xsd::FLOAT),
186-
));
187-
}
188-
}
189-
Value::String(s) => {
190-
graph.insert(TripleRef::new(
191-
subject_stack.back().unwrap(),
192-
NamedNodeRef::new(prop.as_str()).unwrap(),
193-
&Literal::new_typed_literal(s, xsd::STRING),
194-
));
195-
}
196-
Value::Null => {
197-
//println!("Null value");
198-
}
199-
Value::Object(obj) => {
200-
let subject = BlankNode::default();
201-
subject_stack.push_back(subject);
202-
203-
graph.insert(TripleRef::new(
204-
last_subject,
205-
NamedNodeRef::new(prop.as_str()).unwrap(),
206-
subject_stack.back().unwrap(),
207-
));
208-
209-
for (key, val) in obj {
210-
let nested_property: Option<String> = Some(format!("{}{}", ns, key));
211-
process_value(subject_stack, &nested_property, val, graph, ns);
212-
}
213-
subject_stack.pop_back();
214-
}
215-
Value::Array(arr) => {
216-
for val in arr {
217-
process_value(subject_stack, property, val, graph, ns);
218-
}
219-
}
189+
let Some(last_subject) = subject_stack.back().cloned() else {
190+
return Ok(());
191+
};
192+
let Some(prop) = property else {
193+
return Ok(());
194+
};
195+
196+
let predicate =
197+
NamedNodeRef::new(prop.as_str()).map_err(|source| Json2RdfError::InvalidIri {
198+
iri: prop.clone(),
199+
source,
200+
})?;
201+
202+
match value {
203+
Value::Bool(b) => {
204+
graph.insert(TripleRef::new(
205+
&last_subject,
206+
predicate,
207+
&Literal::new_typed_literal(b.to_string(), xsd::BOOLEAN),
208+
));
209+
}
210+
Value::Number(num) => {
211+
let datatype = if num.is_i64() || num.is_u64() {
212+
xsd::INTEGER
213+
} else {
214+
xsd::DOUBLE
215+
};
216+
graph.insert(TripleRef::new(
217+
&last_subject,
218+
predicate,
219+
&Literal::new_typed_literal(num.to_string(), datatype),
220+
));
221+
}
222+
Value::String(s) => {
223+
graph.insert(TripleRef::new(
224+
&last_subject,
225+
predicate,
226+
&Literal::new_typed_literal(s, xsd::STRING),
227+
));
228+
}
229+
Value::Null => {}
230+
Value::Object(obj) => {
231+
let new_subject = BlankNode::default();
232+
graph.insert(TripleRef::new(&last_subject, predicate, &new_subject));
233+
subject_stack.push_back(new_subject);
234+
235+
for (key, val) in obj {
236+
let nested_property: Option<String> = Some(format!("{}{}", ns, key));
237+
process_value(subject_stack, &nested_property, val, graph, ns)?;
238+
}
239+
subject_stack.pop_back();
240+
}
241+
Value::Array(arr) => {
242+
for val in arr {
243+
process_value(subject_stack, property, val, graph, ns)?;
220244
}
221245
}
222246
}
247+
Ok(())
223248
}

src/main.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,12 @@ fn main() {
7979
namespace,
8080
json_files,
8181
output_file,
82-
}) => match json_to_rdf(json_files, namespace, output_file) {
83-
Ok(_) => {}
84-
Err(e) => eprintln!("Error writing: {}", e),
85-
},
82+
}) => {
83+
if let Err(e) = json_to_rdf(json_files, namespace, output_file) {
84+
eprintln!("json2rdf: {}", e);
85+
std::process::exit(1);
86+
}
87+
}
8688
None => {}
8789
}
8890
}

0 commit comments

Comments
 (0)