diff options
author | Chia-You Chen <hortune@google.com> | 2023-03-20 06:35:59 +0000 |
---|---|---|
committer | Chia-You Chen <hortune@google.com> | 2023-03-20 06:37:22 +0000 |
commit | 8510b006a6a6473e19ee115c373e70595dbcfa5f (patch) | |
tree | bd849b13c38135fd3569ab230fce549c43883ca4 | |
parent | b6416d5eaa20b40ae73d74d3c820641ae1a7e2e5 (diff) | |
download | tinyjson-main-16k.tar.gz |
Import tinyjson crate.main-16k
Import this due to the dependency from rules_rust.
Bug: 272102666
Change-Id: Ifbc4d61a7302701738e837e1036d7a6ab157ad24
-rw-r--r-- | .gitattributes | 2 | ||||
-rw-r--r-- | .github/workflows/ci.yaml | 51 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | .rustfmt.toml | 1 | ||||
-rw-r--r-- | Cargo.toml | 44 | ||||
-rw-r--r-- | Cargo.toml.orig | 30 | ||||
l--------- | LICENSE | 1 | ||||
-rw-r--r-- | LICENSE.txt | 21 | ||||
-rw-r--r-- | METADATA | 32 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | OWNERS | 1 | ||||
-rw-r--r-- | README.md | 93 | ||||
-rw-r--r-- | examples/README.md | 8 | ||||
-rw-r--r-- | src/generator.rs | 330 | ||||
-rw-r--r-- | src/json_value.rs | 796 | ||||
-rw-r--r-- | src/lib.rs | 90 | ||||
-rw-r--r-- | src/parser.rs | 470 | ||||
-rw-r--r-- | tests/assets/JSON-Schema-Test-Suite/README.md | 3 | ||||
-rw-r--r-- | tests/assets/JSONTestSuite/README.md | 3 | ||||
-rw-r--r-- | tests/assets/jsonorg/README.md | 5 |
20 files changed, 1984 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..47d64b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/bench/assets -text +/bench/assets linguist-generated diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..e6711f3 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,51 @@ +name: CI +on: [push, pull_request] +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + unit-test: + name: unit tests + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - name: Run tests + run: cargo test --color always --all + linter: + name: clippy and rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - run: cargo fmt --all -- --color always --check + - run: cargo clippy --color always --all -- -D warnings + examples: + name: examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - name: Run parse example + run: | + ret="$(echo '{"hello": "world"}' | cargo run --example parse)" + if [[ "$ret" != 'Parsed: Object({"hello": String("world")})' ]]; then + echo "Error: '$ret'" 2>&1 + exit 1 + fi + - name: Run minify example + run: | + ret="$(echo ' { "hello" : "world" } ' | cargo run --example minify)" + if [[ "$ret" != '{"hello":"world"}' ]]; then + echo "Error: '$ret'" 2>&1 + exit 1 + fi + - name: Run json_value example + run: cargo run --example json_value diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59767ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +/src/main.rs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..32a9786 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2018" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48067d3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,44 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "tinyjson" +version = "2.5.1" +authors = ["rhysd <lin90162@yahoo.co.jp>"] +include = [ + "Cargo.toml", + "README.md", + "LICENSE.txt", + "src/*.rs", +] +description = "Tiny simple JSON parser/generator" +homepage = "https://github.com/rhysd/tinyjson" +readme = "README.md" +keywords = [ + "json", + "parser", + "generator", +] +categories = [ + "encoding", + "parsing", +] +license = "MIT" +repository = "https://github.com/rhysd/tinyjson" + +[dependencies] + +[dev-dependencies.walkdir] +version = "2" + +[badges.maintenance] +status = "passively-maintained" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..dfbea0a --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,30 @@ +[package] +name = "tinyjson" +version = "2.5.1" +edition = "2018" +authors = ["rhysd <lin90162@yahoo.co.jp>"] +description = "Tiny simple JSON parser/generator" +license = "MIT" +readme = "README.md" +homepage = "https://github.com/rhysd/tinyjson" +repository = "https://github.com/rhysd/tinyjson" +keywords = ["json", "parser", "generator"] +categories = ["encoding", "parsing"] + +include = [ + "Cargo.toml", + "README.md", + "LICENSE.txt", + "src/*.rs", +] + +[badges] +maintenance = { status = "passively-maintained" } + +[dependencies] + +[dev-dependencies] +walkdir = "2" + +[workspace] +members = ["bench"] @@ -0,0 +1 @@ +LICENSE.txt
\ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..108178e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +the MIT License + +Copyright (c) 2016 rhysd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..15f8c3d --- /dev/null +++ b/METADATA @@ -0,0 +1,32 @@ +name: "tinyjson" +description: + "tinyjson is a library to parse/generate JSON format document. " + " " + "Goals of this library are " + " " + "Simplicity: This library uses standard containers like Vec or HashMap as " + "its internal representation and exposes it to users. Users can operate " + "JSON values via the standard APIs. And it keeps this crate as small as " + "possible. " + "Explicit: This library does not hide memory allocation from users. You " + "need to allocate memory like Vec, String, HashMap by yourself. It is good " + "for readers of your source code to show where memory allocations happen. " + "And you can have control of how memory is allocated (e.g. allocating " + "memory in advance with with_capacity method). " + "No dependencies: This library is built on top of only standard libraries. " + "No unsafe code: This library is built with Safe Rust. " + "Well tested: This library is tested with famous test suites: " + "JSON checker in json.org " + "JSONTestSuite " + "JSON-Schema-Test-Suite " + "" + +third_party { + url { + type: ARCHIVE + value: "https://crates.io/crates/tinyjson" + } + version: "v2.5.1" + last_upgrade_date { year: 2023 month: 2 day: 13 } + license_type: NOTICE +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT @@ -0,0 +1 @@ +include platform/prebuilts/rust:master:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5a2e99 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +tinyjson +======== +[![version](https://img.shields.io/crates/v/tinyjson.svg)](https://crates.io/crates/tinyjson) +[![CI](https://github.com/rhysd/tinyjson/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/rhysd/tinyjson/actions) + +[tinyjson](https://crates.io/crates/tinyjson) is a library to parse/generate JSON format document. + +Goals of this library are + +- **Simplicity**: This library uses standard containers like `Vec` or `HashMap` as its internal representation + and exposes it to users. Users can operate JSON values via the standard APIs. And it keeps this crate as small + as possible. +- **Explicit**: This library does not hide memory allocation from users. You need to allocate memory like `Vec`, + `String`, `HashMap` by yourself. It is good for readers of your source code to show where memory allocations + happen. And you can have control of how memory is allocated (e.g. allocating memory in advance with + `with_capacity` method). +- **No dependencies**: This library is built on top of only standard libraries. +- **No unsafe code**: This library is built with Safe Rust. +- **Well tested**: This library is tested with famous test suites: + - [JSON checker in json.org](http://www.json.org/JSON_checker/) + - [JSONTestSuite](https://github.com/nst/JSONTestSuite) + - [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) + +[Documentation](https://docs.rs/tinyjson/latest/tinyjson) + +## Requirements + +Rust stable toolchain. + +## Installation + +Add this crate to `dependencies` section of your `Cargo.toml` + +```toml +[dependencies] +tinyjson = "2" +``` + +## Example + +```rust +use tinyjson::JsonValue; +use std::collections::HashMap; +use std::convert::TryInto; + +let s = r#" + { + "bool": true, + "arr": [1, null, "test"], + "nested": { + "blah": false, + "blahblah": 3.14 + }, + "unicode": "\u2764" + } +"#; + +// Parse from strings +let parsed: JsonValue = s.parse().unwrap(); + +// Access to inner value represented with standard containers +let object: &HashMap<_, _> = parsed.get().unwrap(); +println!("Parsed HashMap: {:?}", object); + +// Generate JSON string +println!("{}", parsed.stringify().unwrap()); +// Generate formatted JSON string with indent +println!("{}", parsed.format().unwrap()); + +// Convert to inner value represented with standard containers +let object: HashMap<_, _> = parsed.try_into().unwrap(); +println!("Converted into HashMap: {:?}", object); + +// Create JSON values from standard containers +let mut m = HashMap::new(); +m.insert("foo".to_string(), true.into()); +let mut v = JsonValue::from(m); + +// Access with `Index` and `IndexMut` operators quickly +println!("{:?}", v["foo"]); +v["foo"] = JsonValue::from("hello".to_string()); +println!("{:?}", v["foo"]); +``` + +See [the document](https://docs.rs/tinyjson/latest/tinyjson) to know all APIs. + +## Repository + +https://github.com/rhysd/tinyjson + +## License + +[the MIT License](LICENSE.txt) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..abd0360 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +Working examples are put in this directory. They can be run with `cargo run --example`. + +```sh +echo '{"hello": "world"}' | cargo run --example parse +echo '["foo", 42, null ]' | cargo run --example minify +cargo run --example json_value +``` + diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..079f98f --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,330 @@ +use crate::JsonValue; +use std::collections::HashMap; +use std::fmt; +use std::io::{self, Write}; + +/// Serialization error. This error only happens when some write error happens on writing the serialized byte sequence +/// to the given `io::Write` object. +#[derive(Debug)] +pub struct JsonGenerateError { + msg: String, +} + +impl JsonGenerateError { + pub fn message(&self) -> &str { + self.msg.as_str() + } +} + +impl fmt::Display for JsonGenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Generate error: {}", &self.msg) + } +} + +impl std::error::Error for JsonGenerateError {} + +/// Convenient type alias for serialization results. +pub type JsonGenerateResult = Result<String, JsonGenerateError>; + +/// JSON serializer for `JsonValue`. +/// +/// Basically you don't need to use this struct directly since `JsonValue::stringify` or `JsonValue::format` methods are +/// using this internally. +/// +/// ``` +/// use tinyjson::{JsonGenerator, JsonValue}; +/// +/// let v = JsonValue::from("hello, world".to_string()); +/// let mut buf = vec![]; +/// let mut gen = JsonGenerator::new(&mut buf); +/// gen.generate(&v).unwrap(); +/// +/// assert_eq!(String::from_utf8(buf).unwrap(), "\"hello, world\""); +/// ``` +pub struct JsonGenerator<'indent, W: Write> { + out: W, + indent: Option<&'indent str>, +} + +impl<'indent, W: Write> JsonGenerator<'indent, W> { + /// Create a new `JsonGenerator` object. The serialized byte sequence will be written to the given `io::Write` + /// object. + pub fn new(out: W) -> Self { + Self { out, indent: None } + } + + /// Set indent string. This will be used by [`JsonGenerator::generate`]. + /// ``` + /// use tinyjson::{JsonGenerator, JsonValue}; + /// + /// let v = JsonValue::from(vec![1.0.into(), 2.0.into(), 3.0.into()]); + /// let mut buf = vec![]; + /// let mut gen = JsonGenerator::new(&mut buf).indent(" "); + /// gen.generate(&v).unwrap(); + /// + /// assert_eq!(String::from_utf8(buf).unwrap(), + /// "[ + /// 1, + /// 2, + /// 3 + /// ]"); + /// ``` + pub fn indent(mut self, indent: &'indent str) -> Self { + self.indent = Some(indent); + self + } + + fn quote(&mut self, s: &str) -> io::Result<()> { + const B: u8 = b'b'; // \x08 + const T: u8 = b't'; // \x09 + const N: u8 = b'n'; // \x0a + const F: u8 = b'f'; // \x0c + const R: u8 = b'r'; // \x0d + const Q: u8 = b'"'; // \x22 + const S: u8 = b'\\'; // \x5c + const U: u8 = 1; // non-printable + + #[rustfmt::skip] + const ESCAPE_TABLE: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + U, U, U, U, U, U, U, U, B, T, N, U, F, R, U, U, // 0 + U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, // 1 + 0, 0, Q, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, S, 0, 0, 0, // 5 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 7 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F + ]; + + self.out.write_all(b"\"")?; + let mut start = 0; + for (i, c) in s.char_indices() { + let u = c as usize; + if u < 256 { + let esc = ESCAPE_TABLE[u]; + if esc == 0 { + continue; + } + if start != i { + self.out.write_all(s[start..i].as_bytes())?; + } + if esc == U { + write!(self.out, "\\u{:04x}", u)?; + } else { + self.out.write_all(&[b'\\', esc])?; + } + start = i + 1; + } + } + if start != s.len() { + self.out.write_all(s[start..].as_bytes())?; + } + self.out.write_all(b"\"") + } + + fn number(&mut self, f: f64) -> io::Result<()> { + if f.is_infinite() { + Err(io::Error::new( + io::ErrorKind::Other, + "JSON cannot represent inf", + )) + } else if f.is_nan() { + Err(io::Error::new( + io::ErrorKind::Other, + "JSON cannot represent NaN", + )) + } else { + write!(self.out, "{}", f) + } + } + + fn encode_array(&mut self, array: &[JsonValue]) -> io::Result<()> { + self.out.write_all(b"[")?; + let mut first = true; + for elem in array.iter() { + if first { + first = false; + } else { + self.out.write_all(b",")?; + } + self.encode(elem)?; + } + self.out.write_all(b"]") + } + + fn encode_object(&mut self, m: &HashMap<String, JsonValue>) -> io::Result<()> { + self.out.write_all(b"{")?; + let mut first = true; + for (k, v) in m { + if first { + first = false; + } else { + self.out.write_all(b",")?; + } + self.quote(k)?; + self.out.write_all(b":")?; + self.encode(v)?; + } + self.out.write_all(b"}") + } + + fn encode(&mut self, value: &JsonValue) -> io::Result<()> { + match value { + JsonValue::Number(n) => self.number(*n), + JsonValue::Boolean(b) => write!(self.out, "{}", *b), + JsonValue::String(s) => self.quote(s), + JsonValue::Null => self.out.write_all(b"null"), + JsonValue::Array(a) => self.encode_array(a), + JsonValue::Object(o) => self.encode_object(o), + } + } + + fn write_indent(&mut self, indent: &str, level: usize) -> io::Result<()> { + for _ in 0..level { + self.out.write_all(indent.as_bytes())?; + } + Ok(()) + } + + fn format_array(&mut self, array: &[JsonValue], indent: &str, level: usize) -> io::Result<()> { + if array.is_empty() { + return self.out.write_all(b"[]"); + } + + self.out.write_all(b"[\n")?; + let mut first = true; + for elem in array.iter() { + if first { + first = false; + } else { + self.out.write_all(b",\n")?; + } + self.write_indent(indent, level + 1)?; + self.format(elem, indent, level + 1)?; + } + self.out.write_all(b"\n")?; + self.write_indent(indent, level)?; + self.out.write_all(b"]") + } + + fn format_object( + &mut self, + m: &HashMap<String, JsonValue>, + indent: &str, + level: usize, + ) -> io::Result<()> { + if m.is_empty() { + return self.out.write_all(b"{}"); + } + + self.out.write_all(b"{\n")?; + let mut first = true; + for (k, v) in m { + if first { + first = false; + } else { + self.out.write_all(b",\n")?; + } + self.write_indent(indent, level + 1)?; + self.quote(k)?; + self.out.write_all(b": ")?; + self.format(v, indent, level + 1)?; + } + self.out.write_all(b"\n")?; + self.write_indent(indent, level)?; + self.out.write_all(b"}") + } + + fn format(&mut self, value: &JsonValue, indent: &str, level: usize) -> io::Result<()> { + match value { + JsonValue::Number(n) => self.number(*n), + JsonValue::Boolean(b) => write!(self.out, "{}", *b), + JsonValue::String(s) => self.quote(s), + JsonValue::Null => self.out.write_all(b"null"), + JsonValue::Array(a) => self.format_array(a, indent, level), + JsonValue::Object(o) => self.format_object(o, indent, level), + } + } + + /// Serialize the `JsonValue` into UTF-8 byte sequence. The result will be written to the `io::Write` object passed + /// at [`JsonGenerator::new`]. + /// This method serializes the value without indentation by default. But after setting an indent string via + /// [`JsonGenerator::indent`], this method will use the indent for elements of array and object. + /// + /// ``` + /// use tinyjson::{JsonGenerator, JsonValue}; + /// + /// let v = JsonValue::from(vec![1.0.into(), 2.0.into(), 3.0.into()]); + /// + /// let mut buf = vec![]; + /// let mut gen = JsonGenerator::new(&mut buf); + /// gen.generate(&v).unwrap(); + /// assert_eq!(String::from_utf8(buf).unwrap(), "[1,2,3]"); + /// + /// let mut buf = vec![]; + /// let mut gen = JsonGenerator::new(&mut buf).indent(" "); // with 2-spaces indent + /// gen.generate(&v).unwrap(); + /// + /// assert_eq!(String::from_utf8(buf).unwrap(), + /// "[ + /// 1, + /// 2, + /// 3 + /// ]"); + /// ``` + pub fn generate(&mut self, value: &JsonValue) -> io::Result<()> { + if let Some(indent) = &self.indent { + self.format(value, indent, 0) + } else { + self.encode(value) + } + } +} + +/// Serialize the given `JsonValue` value to `String` without indentation. This method is almost identical to +/// `JsonValue::stringify` but exists for a historical reason. +/// +/// ``` +/// use tinyjson::JsonValue; +/// +/// let v = JsonValue::from(vec![1.0.into(), 2.0.into(), 3.0.into()]); +/// let s = tinyjson::stringify(&v).unwrap(); +/// assert_eq!(s, "[1,2,3]"); +/// ``` +pub fn stringify(value: &JsonValue) -> JsonGenerateResult { + let mut to = Vec::new(); + let mut gen = JsonGenerator::new(&mut to); + gen.generate(value).map_err(|err| JsonGenerateError { + msg: format!("{}", err), + })?; + Ok(String::from_utf8(to).unwrap()) +} + +/// Serialize the given `JsonValue` value to `String` with 2-spaces indentation. This method is almost identical to +/// `JsonValue::format` but exists for a historical reason. +/// +/// ``` +/// use tinyjson::JsonValue; +/// +/// let v = JsonValue::from(vec![1.0.into(), 2.0.into(), 3.0.into()]); +/// let s = tinyjson::format(&v).unwrap(); +/// assert_eq!(s, "[\n 1,\n 2,\n 3\n]"); +/// ``` +pub fn format(value: &JsonValue) -> JsonGenerateResult { + let mut to = Vec::new(); + let mut gen = JsonGenerator::new(&mut to).indent(" "); + gen.generate(value).map_err(|err| JsonGenerateError { + msg: format!("{}", err), + })?; + Ok(String::from_utf8(to).unwrap()) +} diff --git a/src/json_value.rs b/src/json_value.rs new file mode 100644 index 0000000..d92e309 --- /dev/null +++ b/src/json_value.rs @@ -0,0 +1,796 @@ +use crate::generator::{format, stringify, JsonGenerateResult, JsonGenerator}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt; +use std::io; +use std::ops::{Index, IndexMut}; + +const NULL: () = (); + +/// Enum to represent one JSON value. Each variant represents corresponding JSON types. +/// ``` +/// use tinyjson::JsonValue; +/// use std::convert::TryInto; +/// +/// // Convert from raw values using `From` trait +/// let value = JsonValue::from("this is string".to_string()); +/// +/// // Get reference to inner value +/// let maybe_number: Option<&f64> = value.get(); +/// assert!(maybe_number.is_none()); +/// let maybe_string: Option<&String> = value.get(); +/// assert!(maybe_string.is_some()); +/// +/// // Check type of JSON value +/// assert!(matches!(value, JsonValue::String(_))); +/// assert!(value.is_string()); +/// +/// // Convert into raw values using `TryInto` trait +/// let original_value: String = value.try_into().unwrap(); +/// ``` +#[derive(Debug, Clone, PartialEq)] +pub enum JsonValue { + /// Number type value. + Number(f64), + /// Boolean type value. + Boolean(bool), + /// String type value. + String(String), + /// Null type value. + Null, + /// Array type value. + Array(Vec<JsonValue>), + /// Object type value. + Object(HashMap<String, JsonValue>), +} + +/// Trait to access to inner value of `JsonValue` as reference. +/// +/// This is used by several APIs like [`JsonValue::get`] to represent any inner values of [`JsonValue`]. +pub trait InnerAsRef { + fn json_value_as(v: &JsonValue) -> Option<&Self>; +} + +macro_rules! impl_inner_ref { + ($to:ty, $pat:pat => $val:expr) => { + impl InnerAsRef for $to { + fn json_value_as(v: &JsonValue) -> Option<&$to> { + use JsonValue::*; + match v { + $pat => Some($val), + _ => None, + } + } + } + }; +} + +impl_inner_ref!(f64, Number(n) => n); +impl_inner_ref!(bool, Boolean(b) => b); +impl_inner_ref!(String, String(s) => s); +impl_inner_ref!((), Null => &NULL); +impl_inner_ref!(Vec<JsonValue>, Array(a) => a); +impl_inner_ref!(HashMap<String, JsonValue>, Object(h) => h); + +/// Trait to access to inner value of `JsonValue` as mutable reference. +/// +/// This is a mutable version of [`InnerAsRef`]. +pub trait InnerAsRefMut { + fn json_value_as_mut(v: &mut JsonValue) -> Option<&mut Self>; +} + +macro_rules! impl_inner_ref_mut { + ($to:ty, $pat:pat => $val:expr) => { + impl InnerAsRefMut for $to { + fn json_value_as_mut(v: &mut JsonValue) -> Option<&mut $to> { + use JsonValue::*; + match v { + $pat => Some($val), + _ => None, + } + } + } + }; +} + +impl_inner_ref_mut!(f64, Number(n) => n); +impl_inner_ref_mut!(bool, Boolean(b) => b); +impl_inner_ref_mut!(String, String(s) => s); +impl_inner_ref_mut!(Vec<JsonValue>, Array(a) => a); +impl_inner_ref_mut!(HashMap<String, JsonValue>, Object(h) => h); + +// Note: matches! is available from Rust 1.42 +macro_rules! is_xxx { + ( + $(#[$meta:meta])* + $name:ident, + $variant:pat, + ) => { + $(#[$meta])* + pub fn $name(&self) -> bool { + match self { + $variant => true, + _ => false, + } + } + }; +} + +impl JsonValue { + /// Get immutable reference to the inner value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let value: JsonValue = "[1, 2, 3]".parse().unwrap(); + /// let vec: &Vec<_> = value.get().unwrap(); + /// assert_eq!(vec[0], JsonValue::from(1.0)); + /// + /// // Try to convert with incorrect type + /// assert!(value.get::<f64>().is_none()); + /// ``` + pub fn get<T: InnerAsRef>(&self) -> Option<&T> { + T::json_value_as(self) + } + + /// Get mutable reference to the inner value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let mut value: JsonValue = "[1, 2, 3]".parse().unwrap(); + /// let vec: &mut Vec<_> = value.get_mut().unwrap(); + /// vec[0] = JsonValue::from(false); + /// assert_eq!(value.stringify().unwrap(), "[false,2,3]"); + /// + /// // Try to convert with incorrect type + /// assert!(value.get_mut::<f64>().is_none()); + /// ``` + pub fn get_mut<T: InnerAsRefMut>(&mut self) -> Option<&mut T> { + T::json_value_as_mut(self) + } + + is_xxx!( + /// Check if the inner value is a boolean. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(true); + /// assert!(v.is_bool()); + /// let v = JsonValue::from(1.0); + /// assert!(!v.is_bool()); + /// ``` + is_bool, + JsonValue::Boolean(_), + ); + is_xxx!( + /// Check if the inner value is a number. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(1.0); + /// assert!(v.is_number()); + /// let v = JsonValue::from(false); + /// assert!(!v.is_number()); + /// ``` + is_number, + JsonValue::Number(_), + ); + is_xxx!( + /// Check if the inner value is a string. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from("foo".to_string()); + /// assert!(v.is_string()); + /// let v = JsonValue::from(1.0); + /// assert!(!v.is_string()); + /// ``` + is_string, + JsonValue::String(_), + ); + is_xxx!( + /// Check if the inner value is null. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(()); // () is inner representation of null value + /// assert!(v.is_null()); + /// let v = JsonValue::from(false); + /// assert!(!v.is_null()); + /// ``` + is_null, + JsonValue::Null, + ); + is_xxx!( + /// Check if the inner value is an array. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(vec![]); + /// assert!(v.is_array()); + /// let v = JsonValue::from(1.0); + /// assert!(!v.is_array()); + /// ``` + is_array, + JsonValue::Array(_), + ); + is_xxx!( + /// Check if the inner value is an object. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::collections::HashMap; + /// + /// let v = JsonValue::from(HashMap::new()); + /// assert!(v.is_object()); + /// let v = JsonValue::from(vec![]); + /// assert!(!v.is_object()); + /// ``` + is_object, + JsonValue::Object(_), + ); + + /// Convert this JSON value to `String` value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(vec![1.0.into(), true.into(), "str".to_string().into()]); + /// let s = v.stringify().unwrap(); + /// assert_eq!(&s, "[1,true,\"str\"]"); + /// ``` + pub fn stringify(&self) -> JsonGenerateResult { + stringify(self) + } + + /// Write this JSON value to the given `io::Write` object as UTF-8 byte sequence. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::io::Write; + /// + /// let v = JsonValue::from(vec![1.0.into(), true.into(), "str".to_string().into()]); + /// let mut bytes = vec![]; + /// v.write_to(&mut bytes).unwrap(); + /// assert_eq!(&String::from_utf8(bytes).unwrap(), "[1,true,\"str\"]"); + /// ``` + pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { + JsonGenerator::new(w).generate(self) + } + + /// Convert this JSON value to `String` value with 2-spaces indentation. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(vec![1.0.into(), true.into(), "str".to_string().into()]); + /// let s = v.format().unwrap(); + /// assert_eq!(&s, + /// "[ + /// 1, + /// true, + /// \"str\" + /// ]"); + /// ``` + pub fn format(&self) -> JsonGenerateResult { + format(self) + } + + /// Write this JSON value to the given `io::Write` object as UTF-8 byte sequence with 2-spaces indentation. + /// + /// ``` + /// use tinyjson::JsonValue; + /// + /// let v = JsonValue::from(vec![1.0.into(), true.into(), "str".to_string().into()]); + /// let mut bytes = vec![]; + /// v.format_to(&mut bytes).unwrap(); + /// assert_eq!(&String::from_utf8(bytes).unwrap(), + /// "[ + /// 1, + /// true, + /// \"str\" + /// ]"); + /// ``` + pub fn format_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { + JsonGenerator::new(w).indent(" ").generate(self) + } +} + +/// Access to value of the key of object. +/// +/// ``` +/// use tinyjson::JsonValue; +/// use std::collections::HashMap; +/// +/// let mut m = HashMap::new(); +/// m.insert("foo".to_string(), 1.0.into()); +/// let v = JsonValue::from(m); +/// let i = &v["foo"]; +/// assert_eq!(i, &JsonValue::Number(1.0)); +/// ``` +/// +/// This will panic when the given `JsonValue` value is not an object +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// let v = JsonValue::from(vec![]); +/// let _ = &v["foo"]; // Panic +/// ``` +/// +/// or when the key does not exist in the object. +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// # use std::collections::HashMap; +/// let v = JsonValue::from(HashMap::new()); +/// let _ = &v["foo"]; // Panic +/// ``` +/// +/// Using this operator, you can access the nested elements quickly +/// +/// ``` +/// # use tinyjson::JsonValue; +/// let mut json: JsonValue = r#" +/// { +/// "foo": { +/// "bar": [ +/// { "target": 42 } +/// ] +/// } +/// } +/// "#.parse().unwrap(); +/// +/// // Access with index operator +/// let target_value: f64 = *json["foo"]["bar"][0]["target"].get().unwrap(); +/// assert_eq!(target_value, 42.0); +/// ``` + +impl<'a> Index<&'a str> for JsonValue { + type Output = JsonValue; + + fn index(&self, key: &'a str) -> &Self::Output { + let obj = match self { + JsonValue::Object(o) => o, + _ => panic!( + "Attempted to access to an object with key '{}' but actually it was {:?}", + key, self + ), + }; + + match obj.get(key) { + Some(json) => json, + None => panic!("Key '{}' was not found in {:?}", key, self), + } + } +} + +/// Access to value of the index of array. +/// +/// ``` +/// use tinyjson::JsonValue; +/// +/// let v = JsonValue::from(vec![1.0.into(), true.into()]); +/// let b = &v[1]; +/// assert_eq!(b, &JsonValue::Boolean(true)); +/// ``` +/// +/// This will panic when the given `JsonValue` value is not an array +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// use std::collections::HashMap; +/// let v = JsonValue::from(HashMap::new()); +/// let _ = &v[0]; // Panic +/// ``` +/// +/// or when the index is out of bounds. +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// let v = JsonValue::from(vec![]); +/// let _ = &v[0]; // Panic +/// ``` +impl Index<usize> for JsonValue { + type Output = JsonValue; + + fn index(&self, index: usize) -> &'_ Self::Output { + let array = match self { + JsonValue::Array(a) => a, + _ => panic!( + "Attempted to access to an array with index {} but actually the value was {:?}", + index, self, + ), + }; + &array[index] + } +} + +/// Access to value of the key of mutable object. +/// +/// ``` +/// use tinyjson::JsonValue; +/// use std::collections::HashMap; +/// +/// let mut m = HashMap::new(); +/// m.insert("foo".to_string(), 1.0.into()); +/// let mut v = JsonValue::from(m); +/// v["foo"] = JsonValue::Number(3.14); +/// assert_eq!(v["foo"], JsonValue::Number(3.14)); +/// ``` +/// +/// This will panic when the given `JsonValue` value is not an object +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// let mut v = JsonValue::from(vec![]); +/// let _ = &mut v["foo"]; // Panic +/// ``` +/// +/// or when the key does not exist in the object. +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// # use std::collections::HashMap; +/// let mut v = JsonValue::from(HashMap::new()); +/// let _ = &mut v["foo"]; // Panic +/// ``` +/// +/// Using this operator, you can modify the nested elements quickly +/// +/// ``` +/// # use tinyjson::JsonValue; +/// let mut json: JsonValue = r#" +/// { +/// "foo": { +/// "bar": [ +/// { "target": 42 } +/// ] +/// } +/// } +/// "#.parse().unwrap(); +/// +/// // Modify with index operator +/// json["foo"]["bar"][0]["target"] = JsonValue::Boolean(false); +/// assert_eq!(json["foo"]["bar"][0]["target"], JsonValue::Boolean(false)); +/// ``` +impl<'a> IndexMut<&'a str> for JsonValue { + fn index_mut(&mut self, key: &'a str) -> &mut Self::Output { + let obj = match self { + JsonValue::Object(o) => o, + _ => panic!( + "Attempted to access to an object with key '{}' but actually it was {:?}", + key, self + ), + }; + + if let Some(json) = obj.get_mut(key) { + json + } else { + panic!("Key '{}' was not found in object", key) + } + } +} + +/// Access to value of the index of mutable array. +/// +/// ``` +/// use tinyjson::JsonValue; +/// +/// let mut v = JsonValue::from(vec![1.0.into(), true.into()]); +/// let b = &mut v[1]; +/// assert_eq!(b, &JsonValue::Boolean(true)); +/// ``` +/// +/// This will panic when the given `JsonValue` value is not an array +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// use std::collections::HashMap; +/// let mut v = JsonValue::from(HashMap::new()); +/// let _ = &mut v[0]; // Panic +/// ``` +/// +/// or when the index is out of bounds. +/// +/// ```should_panic +/// # use tinyjson::JsonValue; +/// let mut v = JsonValue::from(vec![]); +/// let _ = &mut v[0]; // Panic +/// ``` +impl IndexMut<usize> for JsonValue { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + let array = match self { + JsonValue::Array(a) => a, + _ => panic!( + "Attempted to access to an array with index {} but actually the value was {:?}", + index, self, + ), + }; + + &mut array[index] + } +} + +macro_rules! impl_from { + ( + $(#[$meta:meta])* + $v:ident: $t:ty => $e:expr + ) => { + $(#[$meta])* + impl From<$t> for JsonValue { + fn from($v: $t) -> JsonValue { + use JsonValue::*; + $e + } + } + }; +} + +impl_from!( + /// Convert `f64` value into `JsonValue`. + /// + /// ``` + /// use tinyjson::JsonValue; + /// let v = JsonValue::from(1.0); + /// assert!(v.is_number()); + /// ``` + n: f64 => Number(n) +); +impl_from!( + /// Convert `bool` value into `JsonValue`. + /// + /// ``` + /// use tinyjson::JsonValue; + /// let v = JsonValue::from(true); + /// assert!(v.is_bool()); + /// ``` + b: bool => Boolean(b) +); +impl_from!( + /// Convert `bool` value into `JsonValue`. Note that `&str` is not available. Explicitly allocate `String` object + /// and pass it. + /// + /// ``` + /// use tinyjson::JsonValue; + /// let v = JsonValue::from("foo".to_string()); + /// assert!(v.is_string()); + /// ``` + s: String => String(s) +); +impl_from!( + /// Convert `()` into `JsonValue`. `()` is an inner representation of null JSON value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// let v = JsonValue::from(()); + /// assert!(v.is_null()); + /// ``` + _x: () => Null +); +impl_from!( + /// Convert `Vec` value into `JsonValue`. + /// + /// ``` + /// use tinyjson::JsonValue; + /// let v = JsonValue::from(vec![1.0.into(), true.into()]); + /// assert!(v.is_array()); + /// ``` + a: Vec<JsonValue> => Array(a) +); +impl_from!( + /// Convert `HashMap` value into `JsonValue`. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::collections::HashMap; + /// let mut m = HashMap::new(); + /// m.insert("foo".to_string(), 1.0.into()); + /// let v = JsonValue::from(m); + /// assert!(v.is_object()); + /// ``` + o: HashMap<String, JsonValue> => Object(o) +); + +/// Error caused when trying to convert `JsonValue` into some wrong type value. +/// +/// ``` +/// use tinyjson::{JsonValue, UnexpectedValue}; +/// use std::convert::TryFrom; +/// +/// let error = String::try_from(JsonValue::from(1.0)).unwrap_err(); +/// assert!(matches!(error, UnexpectedValue{..})); +/// ``` +#[derive(Debug)] +pub struct UnexpectedValue { + value: JsonValue, + expected: &'static str, +} + +impl UnexpectedValue { + /// Get reference to the value which failed to be converted. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let error = String::try_from(JsonValue::from(1.0)).unwrap_err(); + /// assert_eq!(error.value(), &JsonValue::Number(1.0)); + /// ``` + pub fn value(&self) -> &JsonValue { + &self.value + } +} + +impl fmt::Display for UnexpectedValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Unexpected JSON value: {:?}. Expected {} value", + self.value, self.expected + ) + } +} + +impl std::error::Error for UnexpectedValue {} + +/// Convert this error into the value which failed to be converted. +/// +/// ``` +/// use tinyjson::JsonValue; +/// use std::convert::TryFrom; +/// +/// let error = String::try_from(JsonValue::from(1.0)).unwrap_err(); +/// assert_eq!(JsonValue::from(error), JsonValue::Number(1.0)); +/// ``` +impl From<UnexpectedValue> for JsonValue { + fn from(err: UnexpectedValue) -> Self { + err.value + } +} + +macro_rules! impl_try_from { + ( + $(#[$meta:meta])* + $pat:pat => $val:expr, + $ty:ty, + ) => { + $(#[$meta])* + impl TryFrom<JsonValue> for $ty { + type Error = UnexpectedValue; + + fn try_from(v: JsonValue) -> Result<Self, UnexpectedValue> { + match v { + $pat => Ok($val), + v => Err(UnexpectedValue { + value: v, + expected: stringify!($ty), + }), + } + } + } + }; +} + +impl_try_from!( + /// Try to convert the `JsonValue` value into `f64`. `UnexpectedValue` error happens when trying to convert an + /// incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let v = JsonValue::from(1.0); + /// let r = f64::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(true); + /// let r = f64::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::Number(n) => n, + f64, +); +impl_try_from!( + /// Try to convert the `JsonValue` value into `bool`. `UnexpectedValue` error happens when trying to convert an + /// incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let v = JsonValue::from(true); + /// let r = bool::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(1.0); + /// let r = bool::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::Boolean(b) => b, + bool, +); +impl_try_from!( + /// Try to convert the `JsonValue` value into `String`. `UnexpectedValue` error happens when trying to convert an + /// incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let v = JsonValue::from("foo".to_string()); + /// let r = String::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(1.0); + /// let r = String::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::String(s) => s, + String, +); +impl_try_from!( + /// Try to convert the `JsonValue` value into `()`. Note that `()` is an inner representation of null JSON value. + /// `UnexpectedValue` error happens when trying to convert an incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let v = JsonValue::from(()); + /// let r = <()>::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(1.0); + /// let r = <()>::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::Null => (), + (), +); +impl_try_from!( + /// Try to convert the `JsonValue` value into `Vec<JsonValue>`. `UnexpectedValue` error happens when trying to + /// convert an incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// + /// let v = JsonValue::from(vec![true.into()]); + /// let r = <Vec<_>>::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(1.0); + /// let r = <Vec<_>>::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::Array(a) => a, + Vec<JsonValue>, +); +impl_try_from!( + /// Try to convert the `JsonValue` value into `HashMap<String, JsonValue>`. `UnexpectedValue` error happens when + /// trying to convert an incorrect type value. + /// + /// ``` + /// use tinyjson::JsonValue; + /// use std::convert::TryFrom; + /// use std::collections::HashMap; + /// + /// let mut m = HashMap::new(); + /// m.insert("foo".to_string(), 42.0.into()); + /// let v = JsonValue::from(m); + /// let r = <HashMap<_, _>>::try_from(v); + /// assert!(r.is_ok()); + /// + /// let v = JsonValue::from(1.0); + /// let r = <HashMap<_, _>>::try_from(v); + /// assert!(r.is_err()); + /// ``` + JsonValue::Object(o) => o, + HashMap<String, JsonValue>, +); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..39ca04d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,90 @@ +//! [tinyjson](https://crates.io/crates/tinyjson) is a library to parse/generate JSON format document. +//! +//! Goals of this library are +//! +//! - **Simplicity**: This library uses standard containers like `Vec` or `HashMap` as its internal representation +//! and exposes it to users. Users can operate JSON values via the standard APIs. And it keeps this crate as small +//! as possible. +//! - **Explicit**: This library does not hide memory allocation from users. You need to allocate memory like `Vec`, +//! `String`, `HashMap` by yourself. It is good for readers of your source code to show where memory allocations +//! happen. And you can have control of how memory is allocated (e.g. allocating memory in advance with +//! `with_capacity` method). +//! - **No dependencies**: This library is built on top of only standard libraries. +//! - **No unsafe code**: This library is built with Safe Rust. +//! - **Well tested**: This library is tested with famous test suites: +//! - [JSON checker in json.org](http://www.json.org/JSON_checker/) +//! - [JSONTestSuite](https://github.com/nst/JSONTestSuite) +//! - [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) +//! +//! Example: +//! +//! ``` +//! use tinyjson::JsonValue; +//! use std::collections::HashMap; +//! use std::convert::TryInto; +//! +//! let s = r#" +//! { +//! "bool": true, +//! "arr": [1, null, "test"], +//! "nested": { +//! "blah": false, +//! "blahblah": 3.14 +//! }, +//! "unicode": "\u2764" +//! } +//! "#; +//! +//! // Parse from strings +//! let parsed: JsonValue = s.parse().unwrap(); +//! +//! // Access to inner value represented with standard containers +//! let object: &HashMap<_, _> = parsed.get().unwrap(); +//! println!("Parsed HashMap: {:?}", object); +//! +//! // Generate JSON string +//! println!("{}", parsed.stringify().unwrap()); +//! // Generate formatted JSON string with indent +//! println!("{}", parsed.format().unwrap()); +//! +//! // Convert to inner value represented with standard containers +//! let object: HashMap<_, _> = parsed.try_into().unwrap(); +//! println!("Converted into HashMap: {:?}", object); +//! +//! // Create JSON values from standard containers +//! let mut m = HashMap::new(); +//! m.insert("foo".to_string(), true.into()); +//! let mut v = JsonValue::from(m); +//! +//! // Access with `Index` and `IndexMut` operators quickly +//! println!("{:?}", v["foo"]); +//! v["foo"] = JsonValue::from("hello".to_string()); +//! println!("{:?}", v["foo"]); +//! ``` +//! +//! Any JSON value is represented with [`JsonValue`] enum. +//! +//! Each JSON types are mapped to Rust types as follows: +//! +//! | JSON | Rust | +//! |---------|------------------------------| +//! | Number | `f64` | +//! | Boolean | `bool` | +//! | String | `String` | +//! | Null | `()` | +//! | Array | `Vec<JsonValue>` | +//! | Object | `HashMap<String, JsonValue>` | + +// This library is built with Safe Rust +#![forbid(unsafe_code)] +// Suppress warning which prefers `matches!` macro to `match` statement since the macro was +// introduced in recent Rust 1.42. This library should support older Rust. +#![allow(clippy::match_like_matches_macro)] + +mod generator; +mod json_value; +mod parser; + +pub use generator::*; +pub use json_value::{InnerAsRef, InnerAsRefMut, JsonValue, UnexpectedValue}; +pub use parser::*; diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..9eac64e --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,470 @@ +use std::char; +use std::collections::HashMap; +use std::fmt; +use std::iter::Peekable; +use std::str::FromStr; + +use crate::JsonValue; + +/// Parse error. +/// +/// ``` +/// use tinyjson::{JsonParser, JsonParseError}; +/// let error = JsonParser::new("[1, 2, 3".chars()).parse().unwrap_err(); +/// assert!(matches!(error, JsonParseError{..})); +/// ``` +#[derive(Debug)] +pub struct JsonParseError { + msg: String, + line: usize, + col: usize, +} + +impl JsonParseError { + fn new(msg: String, line: usize, col: usize) -> JsonParseError { + JsonParseError { msg, line, col } + } + + /// Get the line numbr where the parse error happened. This value is 1-based. + /// + /// ``` + /// use tinyjson::{JsonParser, JsonParseError}; + /// let error = JsonParser::new("[1, 2, 3".chars()).parse().unwrap_err(); + /// assert_eq!(error.line(), 1); + /// ``` + pub fn line(&self) -> usize { + self.line + } + + /// Get the column numbr where the parse error happened. This value is 1-based. + /// + /// ``` + /// use tinyjson::{JsonParser, JsonParseError}; + /// let error = JsonParser::new("[1, 2, 3".chars()).parse().unwrap_err(); + /// assert_eq!(error.column(), 8); + /// ``` + pub fn column(&self) -> usize { + self.col + } +} + +impl fmt::Display for JsonParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Parse error at line:{}, col:{}: {}", + self.line, self.col, &self.msg, + ) + } +} + +impl std::error::Error for JsonParseError {} + +/// Convenient type alias for parse results. +pub type JsonParseResult = Result<JsonValue, JsonParseError>; + +// Note: char::is_ascii_whitespace is not available because some characters are not defined as +// whitespace character in JSON spec. For example, U+000C FORM FEED is whitespace in Rust but +// it isn't in JSON. +fn is_whitespace(c: char) -> bool { + match c { + '\u{0020}' | '\u{000a}' | '\u{000d}' | '\u{0009}' => true, + _ => false, + } +} + +/// JSON parser to parse UTF-8 string into `JsonValue` value. +/// +/// Basically you don't need to use this struct directly thanks to `FromStr` trait implementation. +/// +/// ``` +/// use tinyjson::{JsonParser, JsonValue}; +/// +/// let mut parser = JsonParser::new("[1, 2, 3]".chars()); +/// let array = parser.parse().unwrap(); +/// +/// // Equivalent to the above code using `FromStr` +/// let array: JsonValue = "[1, 2, 3]".parse().unwrap(); +/// ``` +pub struct JsonParser<I> +where + I: Iterator<Item = char>, +{ + chars: Peekable<I>, + line: usize, + col: usize, +} + +impl<I: Iterator<Item = char>> JsonParser<I> { + /// Create a new parser instance from an iterator which iterates characters. The iterator is usually built from + /// `str::chars` for parsing `str` or `String` values. + pub fn new(it: I) -> Self { + JsonParser { + chars: it.peekable(), + line: 1, + col: 0, + } + } + + fn err<T>(&self, msg: String) -> Result<T, JsonParseError> { + Err(JsonParseError::new(msg, self.line, self.col)) + } + + fn unexpected_eof(&self) -> Result<char, JsonParseError> { + Err(JsonParseError::new( + String::from("Unexpected EOF"), + self.line, + self.col, + )) + } + + fn next_pos(&mut self, c: char) { + if c == '\n' { + self.col = 0; + self.line += 1; + } else { + self.col += 1; + } + } + + fn peek(&mut self) -> Result<char, JsonParseError> { + while let Some(c) = self.chars.peek().copied() { + if !is_whitespace(c) { + return Ok(c); + } + self.next_pos(c); + self.chars.next().unwrap(); + } + self.unexpected_eof() + } + + fn next(&mut self) -> Option<char> { + while let Some(c) = self.chars.next() { + self.next_pos(c); + if !is_whitespace(c) { + return Some(c); + } + } + None + } + + fn consume(&mut self) -> Result<char, JsonParseError> { + if let Some(c) = self.next() { + Ok(c) + } else { + self.unexpected_eof() + } + } + + fn consume_no_skip(&mut self) -> Result<char, JsonParseError> { + if let Some(c) = self.chars.next() { + self.next_pos(c); + Ok(c) + } else { + self.unexpected_eof() + } + } + + fn parse_object(&mut self) -> JsonParseResult { + if self.consume()? != '{' { + return self.err(String::from("Object must starts with '{'")); + } + + if self.peek()? == '}' { + self.consume().unwrap(); + return Ok(JsonValue::Object(HashMap::new())); + } + + let mut m = HashMap::new(); + loop { + let key = match self.parse_any()? { + JsonValue::String(s) => s, + v => return self.err(format!("Key of object must be string but found {:?}", v)), + }; + + let c = self.consume()?; + if c != ':' { + return self.err(format!( + "':' is expected after key of object but actually found '{}'", + c + )); + } + + m.insert(key, self.parse_any()?); + + match self.consume()? { + ',' => {} + '}' => return Ok(JsonValue::Object(m)), + c => { + return self.err(format!( + "',' or '}}' is expected for object but actually found '{}'", + c.escape_debug(), + )) + } + } + } + } + + fn parse_array(&mut self) -> JsonParseResult { + if self.consume()? != '[' { + return self.err(String::from("Array must starts with '['")); + } + + if self.peek()? == ']' { + self.consume().unwrap(); + return Ok(JsonValue::Array(vec![])); + } + + let mut v = vec![self.parse_any()?]; + loop { + match self.consume()? { + ',' => {} + ']' => return Ok(JsonValue::Array(v)), + c => { + return self.err(format!( + "',' or ']' is expected for array but actually found '{}'", + c + )) + } + } + + v.push(self.parse_any()?); // Next element + } + } + + fn push_utf16(&self, s: &mut String, utf16: &mut Vec<u16>) -> Result<(), JsonParseError> { + if utf16.is_empty() { + return Ok(()); + } + + match String::from_utf16(utf16) { + Ok(utf8) => s.push_str(&utf8), + Err(err) => return self.err(format!("Invalid UTF-16 sequence {:?}: {}", &utf16, err)), + } + utf16.clear(); + Ok(()) + } + + fn parse_string(&mut self) -> JsonParseResult { + if self.consume()? != '"' { + return self.err(String::from("String must starts with double quote")); + } + + let mut utf16 = Vec::new(); // Buffer for parsing \uXXXX UTF-16 characters + let mut s = String::new(); + loop { + let c = match self.consume_no_skip()? { + '\\' => match self.consume_no_skip()? { + '\\' => '\\', + '/' => '/', + '"' => '"', + 'b' => '\u{0008}', + 'f' => '\u{000c}', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'u' => { + let mut u = 0u16; + for _ in 0..4 { + let c = self.consume()?; + if let Some(h) = c.to_digit(16) { + u = u * 0x10 + h as u16; + } else { + return self.err(format!("Unicode character must be \\uXXXX (X is hex character) format but found character '{}'", c)); + } + } + utf16.push(u); + // Additional \uXXXX character may follow. UTF-16 characters must be converted + // into UTF-8 string as sequence because surrogate pairs must be considered + // like "\uDBFF\uDFFF". + continue; + } + c => return self.err(format!("'\\{}' is invalid escaped character", c)), + }, + '"' => { + self.push_utf16(&mut s, &mut utf16)?; + return Ok(JsonValue::String(s)); + } + // Note: c.is_control() is not available here because JSON accepts 0x7f (DEL) in + // string literals but 0x7f is control character. + // Rough spec of JSON says string literal cannot contain control characters. But it + // can actually contain 0x7f. + c if (c as u32) < 0x20 => { + return self.err(format!( + "String cannot contain control character {}", + c.escape_debug(), + )); + } + c => c, + }; + + self.push_utf16(&mut s, &mut utf16)?; + + s.push(c); + } + } + + fn parse_constant(&mut self, s: &'static str) -> Option<JsonParseError> { + for c in s.chars() { + match self.consume_no_skip() { + Ok(x) if x != c => { + return Some(JsonParseError::new( + format!("Unexpected character '{}' while parsing '{}'", c, s), + self.line, + self.col, + )); + } + Ok(_) => {} + Err(e) => return Some(e), + } + } + None + } + + fn parse_null(&mut self) -> JsonParseResult { + match self.parse_constant("null") { + Some(err) => Err(err), + None => Ok(JsonValue::Null), + } + } + + fn parse_true(&mut self) -> JsonParseResult { + match self.parse_constant("true") { + Some(err) => Err(err), + None => Ok(JsonValue::Boolean(true)), + } + } + + fn parse_false(&mut self) -> JsonParseResult { + match self.parse_constant("false") { + Some(err) => Err(err), + None => Ok(JsonValue::Boolean(false)), + } + } + + fn parse_number(&mut self) -> JsonParseResult { + let neg = if self.peek()? == '-' { + self.consume_no_skip().unwrap(); + true + } else { + false + }; + + let mut s = String::new(); + let mut saw_dot = false; + let mut saw_exp = false; + + while let Some(d) = self.chars.peek() { + match d { + '0'..='9' => s.push(*d), + '.' => { + saw_dot = true; + break; + } + 'e' | 'E' => { + saw_exp = true; + break; + } + _ => break, + } + self.consume_no_skip().unwrap(); + } + + if s.is_empty() { + return self.err("Integer part must not be empty in number literal".to_string()); + } + + if s.starts_with('0') && s.len() > 1 { + return self + .err("Integer part of number must not start with 0 except for '0'".to_string()); + } + + if saw_dot { + s.push(self.consume_no_skip().unwrap()); // eat '.' + while let Some(d) = self.chars.peek() { + match d { + '0'..='9' => s.push(*d), + 'e' | 'E' => { + saw_exp = true; + break; + } + _ => break, + } + self.consume_no_skip().unwrap(); + } + if s.ends_with('.') { + return self.err("Fraction part of number must not be empty".to_string()); + } + } + + if saw_exp { + s.push(self.consume_no_skip().unwrap()); // eat 'e' or 'E' + if let Some('+') | Some('-') = self.chars.peek() { + s.push(self.consume_no_skip().unwrap()); + } + + let mut saw_digit = false; + while let Some(d) = self.chars.peek() { + match d { + '0'..='9' => s.push(*d), + _ => break, + } + saw_digit = true; + self.consume_no_skip().unwrap(); + } + + if !saw_digit { + return self.err("Exponent part must not be empty in number literal".to_string()); + } + } + + match s.parse::<f64>() { + Ok(n) => Ok(JsonValue::Number(if neg { -n } else { n })), + Err(err) => self.err(format!("Invalid number literal '{}': {}", s, err)), + } + } + + fn parse_any(&mut self) -> JsonParseResult { + match self.peek()? { + '0'..='9' | '-' => self.parse_number(), + '"' => self.parse_string(), + '[' => self.parse_array(), + '{' => self.parse_object(), + 't' => self.parse_true(), + 'f' => self.parse_false(), + 'n' => self.parse_null(), + c => self.err(format!("Invalid character: {}", c.escape_debug())), + } + } + + /// Run the parser to parse one JSON value. + pub fn parse(&mut self) -> JsonParseResult { + let v = self.parse_any()?; + + if let Some(c) = self.next() { + return self.err(format!( + "Expected EOF but got character '{}'", + c.escape_debug(), + )); + } + + Ok(v) + } +} + +/// Parse given `str` object into `JsonValue` value. This is recommended way to parse strings into JSON value with +/// this library. +/// +/// ``` +/// use tinyjson::JsonValue; +/// +/// let array: JsonValue = "[1, 2, 3]".parse().unwrap(); +/// assert!(array.is_array()); +/// ``` +impl FromStr for JsonValue { + type Err = JsonParseError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + JsonParser::new(s.chars()).parse() + } +} diff --git a/tests/assets/JSON-Schema-Test-Suite/README.md b/tests/assets/JSON-Schema-Test-Suite/README.md new file mode 100644 index 0000000..4674bdc --- /dev/null +++ b/tests/assets/JSON-Schema-Test-Suite/README.md @@ -0,0 +1,3 @@ +Imported from https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/846dd7abddd2347803372cd7212e5fb0cc74379e + +See [LICENSE](./LICENSE) for files in this directory. diff --git a/tests/assets/JSONTestSuite/README.md b/tests/assets/JSONTestSuite/README.md new file mode 100644 index 0000000..db92c38 --- /dev/null +++ b/tests/assets/JSONTestSuite/README.md @@ -0,0 +1,3 @@ +Imported from https://github.com/nst/JSONTestSuite/tree/9f23c68b521dd700e8c99151d6dc1c5c52a0246e + +See [LICENSE](./LICENSE) for license for files in this directory. diff --git a/tests/assets/jsonorg/README.md b/tests/assets/jsonorg/README.md new file mode 100644 index 0000000..dace048 --- /dev/null +++ b/tests/assets/jsonorg/README.md @@ -0,0 +1,5 @@ +These assets are imported from json.org. + +http://www.json.org/JSON_checker/ + + |