diff --git a/Cargo.toml b/Cargo.toml index a62e34f..5a5f0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,7 @@ repository = "https://github.com/kaleidawave/simple-json-parser" [lib] path = "lib.rs" + +[lints.clippy] +pedantic = "deny" + diff --git a/examples/package_json.rs b/examples/package_json.rs index c4260bf..bfa0934 100644 --- a/examples/package_json.rs +++ b/examples/package_json.rs @@ -1,4 +1,4 @@ -use simple_json_parser::parse; +use simple_json_parser::{parse, JSONParseError}; fn main() { let content = r#"{ @@ -83,7 +83,7 @@ fn main() { let result = parse(content, |keys, value| eprintln!("{keys:?} -> {value:?}")); - if let Err((idx, err)) = result { - eprintln!("{err:?} @ {idx}") + if let Err(JSONParseError { at, reason }) = result { + eprintln!("{reason:?} @ {at}"); } } diff --git a/examples/to_object.rs b/examples/to_object.rs new file mode 100644 index 0000000..1c45949 --- /dev/null +++ b/examples/to_object.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use simple_json_parser::{parse, JSONKey, RootJSONValue}; + +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).ok_or("Expected first argument")?; + let content = std::fs::read_to_string(path)?; + + pub type Object = HashMap; + + #[derive(Debug)] + pub enum Value { + Object(Object), + String(String), + Number(String), + Boolean(bool), + Null, + } + + impl Value { + pub fn new_empty_object() -> Self { + Self::Object(HashMap::new()) + } + + pub fn set<'a>(&'a mut self, keys: &'a [JSONKey<'a>], value: RootJSONValue<'a>) { + if let Value::Object(ref mut obj) = self { + if let [last] = keys { + let name = match last { + JSONKey::Slice(s) => (*s).to_string(), + JSONKey::Index(i) => i.to_string(), + }; + let value = match value { + RootJSONValue::String(s) => Value::String(s.to_string()), + RootJSONValue::Number(n) => Value::Number(n.to_string()), + RootJSONValue::True => Value::Boolean(true), + RootJSONValue::False => Value::Boolean(false), + RootJSONValue::Null => Value::Null, + }; + let existing = obj.insert(name, value); + debug_assert!(existing.is_none()); + } else if let [first, others @ ..] = keys { + let name = match first { + JSONKey::Slice(s) => (*s).to_string(), + JSONKey::Index(i) => i.to_string(), + }; + obj.entry(name) + .or_insert_with(Value::new_empty_object) + .set(others, value); + } else { + unreachable!("empty keys") + } + } else { + unreachable!() + } + } + } + + let mut root = Value::new_empty_object(); + + parse(&content, |keys, value| root.set(keys, value))?; + + eprintln!("Object:\n{root:#?}"); + Ok(()) +} diff --git a/examples/to_object_unsafe.rs b/examples/to_object_unsafe.rs new file mode 100644 index 0000000..eb5dfc7 --- /dev/null +++ b/examples/to_object_unsafe.rs @@ -0,0 +1,76 @@ +//! Uses iterations rather than recursion to build a object map. Unfortunately at the cost of using `unsafe` + +use std::collections::HashMap; + +use simple_json_parser::{parse, JSONKey, RootJSONValue}; + +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).ok_or("Expected first argument")?; + let content = std::fs::read_to_string(path)?; + + pub type Object = HashMap; + + #[derive(Debug)] + pub enum Value { + Object(Object), + String(String), + Number(String), + Boolean(bool), + Null, + } + + impl Value { + pub fn new_empty_object() -> Self { + Self::Object(HashMap::new()) + } + } + + let mut root = Object::new(); + + let _res = parse(&content, |keys, value| { + let [path @ .., end] = keys else { + unreachable!("empty key change") + }; + let pointer = &mut root; + + let mut to_add_to: *mut Object = pointer; + + for key in path { + let name = match key { + JSONKey::Slice(s) => (*s).to_string(), + JSONKey::Index(i) => i.to_string(), + }; + if let Some(Value::Object(ref mut obj)) = + unsafe { (to_add_to.as_mut().unwrap()).get_mut(&name) } + { + to_add_to = obj; + } else { + let value = unsafe { + (to_add_to.as_mut().unwrap()) + .entry(name) + .or_insert_with(Value::new_empty_object) + }; + if let Value::Object(ref mut obj) = value { + to_add_to = obj; + } + } + } + let name = match end { + JSONKey::Slice(s) => (*s).to_string(), + JSONKey::Index(i) => i.to_string(), + }; + let value = match value { + RootJSONValue::String(s) => Value::String(s.to_string()), + RootJSONValue::Number(n) => Value::Number(n.to_string()), + RootJSONValue::True => Value::Boolean(true), + RootJSONValue::False => Value::Boolean(false), + RootJSONValue::Null => Value::Null, + }; + unsafe { + (to_add_to.as_mut().unwrap()).insert(name, value); + } + }); + + eprintln!("Object:\n{root:#?}"); + Ok(()) +} diff --git a/lib.rs b/lib.rs index 60e6ac8..bf89d87 100644 --- a/lib.rs +++ b/lib.rs @@ -14,7 +14,7 @@ pub enum RootJSONValue<'a> { } #[derive(Debug)] -pub enum JSONParseError { +pub enum JSONParseErrorReason { ExpectedColon, ExpectedEndOfValue, ExpectedBracket, @@ -22,12 +22,44 @@ pub enum JSONParseError { ExpectedValue, } +#[derive(Debug)] +pub struct JSONParseError { + pub at: usize, + pub reason: JSONParseErrorReason, +} + +impl std::error::Error for JSONParseError {} + +impl std::fmt::Display for JSONParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_fmt(format_args!( + "JSONParseError: {:?} at {:?}", + self.reason, self.at + )) + } +} + +/// If you want to return early (not parse the whole input) use [`parse_with_exit_signal`] +/// +/// # Errors +/// Returns an error if it tries to parse invalid JSON input pub fn parse<'a>( on: &'a str, - cb: impl for<'b> Fn(&'b [JSONKey<'a>], RootJSONValue<'a>), -) -> Result<(), (usize, JSONParseError)> { - let chars = on.char_indices(); + mut cb: impl for<'b> FnMut(&'b [JSONKey<'a>], RootJSONValue<'a>), +) -> Result<(), JSONParseError> { + parse_with_exit_signal(on, |k, v| { + cb(k, v); + false + }) +} +/// # Errors +/// Returns an error if it tries to parse invalid JSON input +#[allow(clippy::too_many_lines)] +pub fn parse_with_exit_signal<'a>( + on: &'a str, + mut cb: impl for<'b> FnMut(&'b [JSONKey<'a>], RootJSONValue<'a>) -> bool, +) -> Result<(), JSONParseError> { enum State { InKey { escaped: bool, @@ -39,6 +71,7 @@ pub fn parse<'a>( start: usize, multiline: bool, last_was_asterisk: bool, + hash: bool, }, ExpectingValue, None, @@ -55,6 +88,8 @@ pub fn parse<'a>( EndOfValue, } + let chars = on.char_indices(); + let mut key_chain = Vec::new(); let mut state = State::None; @@ -64,7 +99,7 @@ pub fn parse<'a>( char: char, state: &mut State, key_chain: &mut Vec>, - ) -> Result<(), (usize, JSONParseError)> { + ) -> Result<(), JSONParseError> { if char == ',' { if let Some(JSONKey::Index(i)) = key_chain.last_mut() { *i += 1; @@ -78,7 +113,10 @@ pub fn parse<'a>( } else if let (']', Some(JSONKey::Index(..))) = (char, key_chain.last()) { key_chain.pop(); } else if !char.is_whitespace() { - return Err((idx, JSONParseError::ExpectedEndOfValue)); + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedEndOfValue, + }); } Ok(()) } @@ -101,7 +139,10 @@ pub fn parse<'a>( } => { if !*escaped && char == '"' { state = State::EndOfValue; - cb(&key_chain, RootJSONValue::String(&on[start..idx])); + let res = cb(&key_chain, RootJSONValue::String(&on[start..idx])); + if res { + return Ok(()); + } } else { *escaped = char == '\\'; } @@ -110,7 +151,10 @@ pub fn parse<'a>( if char == ':' { state = State::ExpectingValue; } else if !char.is_whitespace() { - return Err((idx, JSONParseError::ExpectedColon)); + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedColon, + }); } } State::EndOfValue => { @@ -119,25 +163,26 @@ pub fn parse<'a>( State::Comment { ref mut last_was_asterisk, ref mut multiline, + hash, start, } => { if char == '\n' && !*multiline { if let Some(JSONKey::Index(..)) = key_chain.last() { - state = State::ExpectingValue + state = State::ExpectingValue; } else { - state = State::InObject + state = State::InObject; } - } else if char == '*' && start + 1 == idx { - *multiline = true + } else if char == '*' && start + 1 == idx && !hash { + *multiline = true; } else if *multiline { if *last_was_asterisk && char == '/' { if let Some(JSONKey::Index(..)) = key_chain.last() { - state = State::ExpectingValue + state = State::ExpectingValue; } else { - state = State::InObject + state = State::InObject; } } else { - *last_was_asterisk = char == '*' + *last_was_asterisk = char == '*'; } } } @@ -152,19 +197,20 @@ pub fn parse<'a>( start: idx + '"'.len_utf8(), escaped: false, }, - '/' => State::Comment { + c @ ('/' | '#') => State::Comment { last_was_asterisk: false, start: idx, multiline: false, + hash: c == '#', }, '0'..='9' | '-' => State::NumberValue { start: idx }, 't' | 'f' | 'n' => State::TrueFalseNull { start: idx }, - char => { - if !char.is_whitespace() { - return Err((idx, JSONParseError::ExpectedValue)); - } else { - state - } + char if char.is_whitespace() => state, + _ => { + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedValue, + }) } } } @@ -176,9 +222,9 @@ pub fn parse<'a>( }; } else if char == '}' { if let Some(JSONKey::Index(..)) = key_chain.last() { - state = State::ExpectingValue + state = State::ExpectingValue; } else { - state = State::InObject + state = State::InObject; } } } @@ -186,13 +232,19 @@ pub fn parse<'a>( if char == '{' { state = State::InObject; } else if !char.is_whitespace() { - return Err((idx, JSONParseError::ExpectedBracket)); + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedBracket, + }); } } State::NumberValue { start } => { // TODO actual number handing if matches!(char, '\n' | ' ' | '}' | ',' | ']') { - cb(&key_chain, RootJSONValue::Number(&on[start..idx])); + let res = cb(&key_chain, RootJSONValue::Number(&on[start..idx])); + if res { + return Ok(()); + } end_of_value(idx, char, &mut state, &mut key_chain)?; } } @@ -203,21 +255,35 @@ pub fn parse<'a>( } else if diff == 4 { match &on[start..=idx] { "true" => { - cb(&key_chain, RootJSONValue::True); + let res = cb(&key_chain, RootJSONValue::True); + if res { + return Ok(()); + } state = State::EndOfValue; } "null" => { - cb(&key_chain, RootJSONValue::Null); + let res = cb(&key_chain, RootJSONValue::Null); + if res { + return Ok(()); + } state = State::EndOfValue; } "fals" => {} - _ => return Err((idx, JSONParseError::ExpectedTrueFalseNull)), + _ => { + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedTrueFalseNull, + }) + } } } else if let "false" = &on[start..=idx] { cb(&key_chain, RootJSONValue::False); state = State::EndOfValue; } else { - return Err((idx, JSONParseError::ExpectedTrueFalseNull)); + return Err(JSONParseError { + at: idx, + reason: JSONParseErrorReason::ExpectedTrueFalseNull, + }); } } }