mirror of
https://github.com/instructkr/claw-code.git
synced 2026-10-14 00:00:19 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fb5c171b | ||
|
|
4c2a09c868 |
+360
-240
@@ -49,10 +49,10 @@ impl JsonValue {
|
||||
.join(",");
|
||||
format!("[{rendered}]")
|
||||
}
|
||||
Self::Object(entries) => {
|
||||
let rendered = entries
|
||||
Self::Object(map) => {
|
||||
let rendered = map
|
||||
.iter()
|
||||
.map(|(key, value)| format!("{}:{}", render_string(key), value.render()))
|
||||
.map(|(k, v)| format!("{}:{}", render_string(k), v.render()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
format!("{{{rendered}}}")
|
||||
@@ -60,45 +60,10 @@ impl JsonValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(source: &str) -> Result<Self, JsonError> {
|
||||
let mut parser = Parser::new(source);
|
||||
let value = parser.parse_value()?;
|
||||
parser.skip_whitespace();
|
||||
if parser.is_eof() {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(JsonError::new("unexpected trailing content"))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_object(&self) -> Option<&BTreeMap<String, JsonValue>> {
|
||||
match self {
|
||||
Self::Object(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_array(&self) -> Option<&[JsonValue]> {
|
||||
match self {
|
||||
Self::Array(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::String(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::Bool(value) => Some(*value),
|
||||
Self::Bool(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -106,249 +71,370 @@ impl JsonValue {
|
||||
#[must_use]
|
||||
pub fn as_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
Self::Number(value) => Some(*value),
|
||||
Self::Number(n) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_string(value: &str) -> String {
|
||||
let mut rendered = String::with_capacity(value.len() + 2);
|
||||
rendered.push('"');
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'"' => rendered.push_str("\\\""),
|
||||
'\\' => rendered.push_str("\\\\"),
|
||||
'\n' => rendered.push_str("\\n"),
|
||||
'\r' => rendered.push_str("\\r"),
|
||||
'\t' => rendered.push_str("\\t"),
|
||||
'\u{08}' => rendered.push_str("\\b"),
|
||||
'\u{0C}' => rendered.push_str("\\f"),
|
||||
control if control.is_control() => push_unicode_escape(&mut rendered, control),
|
||||
plain => rendered.push(plain),
|
||||
}
|
||||
}
|
||||
rendered.push('"');
|
||||
rendered
|
||||
}
|
||||
|
||||
fn push_unicode_escape(rendered: &mut String, control: char) {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
|
||||
rendered.push_str("\\u");
|
||||
let value = u32::from(control);
|
||||
for shift in [12_u32, 8, 4, 0] {
|
||||
let nibble = ((value >> shift) & 0xF) as usize;
|
||||
rendered.push(char::from(HEX[nibble]));
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
chars: Vec<char>,
|
||||
index: usize,
|
||||
_source: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn new(source: &'a str) -> Self {
|
||||
Self {
|
||||
chars: source.chars().collect(),
|
||||
index: 0,
|
||||
_source: source,
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::String(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(&mut self) -> Result<JsonValue, JsonError> {
|
||||
self.skip_whitespace();
|
||||
match self.peek() {
|
||||
Some('n') => self.parse_literal("null", JsonValue::Null),
|
||||
Some('t') => self.parse_literal("true", JsonValue::Bool(true)),
|
||||
Some('f') => self.parse_literal("false", JsonValue::Bool(false)),
|
||||
Some('"') => self.parse_string().map(JsonValue::String),
|
||||
Some('[') => self.parse_array(),
|
||||
Some('{') => self.parse_object(),
|
||||
Some('-' | '0'..='9') => self.parse_number().map(JsonValue::Number),
|
||||
Some(other) => Err(JsonError::new(format!("unexpected character: {other}"))),
|
||||
None => Err(JsonError::new("unexpected end of input")),
|
||||
#[must_use]
|
||||
pub fn as_array(&self) -> Option<&[JsonValue]> {
|
||||
match self {
|
||||
Self::Array(a) => Some(a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_literal(&mut self, expected: &str, value: JsonValue) -> Result<JsonValue, JsonError> {
|
||||
for expected_char in expected.chars() {
|
||||
if self.next() != Some(expected_char) {
|
||||
return Err(JsonError::new(format!(
|
||||
"invalid literal: expected {expected}"
|
||||
)));
|
||||
}
|
||||
#[must_use]
|
||||
pub fn as_object(&self) -> Option<&BTreeMap<String, JsonValue>> {
|
||||
match self {
|
||||
Self::Object(o) => Some(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Result<Self, JsonError> {
|
||||
let mut chars = input.trim().chars().peekable();
|
||||
let value = parse_value(&mut chars)?;
|
||||
if chars.peek().is_some() {
|
||||
return Err(JsonError::new("trailing content"));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> Result<String, JsonError> {
|
||||
self.expect('"')?;
|
||||
let mut value = String::new();
|
||||
while let Some(ch) = self.next() {
|
||||
match ch {
|
||||
'"' => return Ok(value),
|
||||
'\\' => value.push(self.parse_escape()?),
|
||||
plain => value.push(plain),
|
||||
}
|
||||
}
|
||||
Err(JsonError::new("unterminated string"))
|
||||
fn parse_value(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
match chars.peek() {
|
||||
Some('n') => parse_null(chars),
|
||||
Some('t') | Some('f') => parse_bool(chars),
|
||||
Some('"') => parse_string(chars).map(JsonValue::String),
|
||||
Some('[') => parse_array(chars),
|
||||
Some('{') => parse_object(chars),
|
||||
Some('-') | Some('0'..='9') => parse_number(chars),
|
||||
Some(c) => Err(JsonError::new(format!("unexpected char: {c}"))),
|
||||
None => Err(JsonError::new("empty input")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_null(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
if consume_literal(chars, "null") {
|
||||
Ok(JsonValue::Null)
|
||||
} else {
|
||||
Err(JsonError::new("invalid null"))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
if consume_literal(chars, "true") {
|
||||
return Ok(JsonValue::Bool(true));
|
||||
}
|
||||
if consume_literal(chars, "false") {
|
||||
return Ok(JsonValue::Bool(false));
|
||||
}
|
||||
Err(JsonError::new("invalid bool"))
|
||||
}
|
||||
|
||||
fn parse_string(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<String, JsonError> {
|
||||
if chars.next() != Some('"') {
|
||||
return Err(JsonError::new("expected opening quote"));
|
||||
}
|
||||
|
||||
fn parse_escape(&mut self) -> Result<char, JsonError> {
|
||||
match self.next() {
|
||||
Some('"') => Ok('"'),
|
||||
Some('\\') => Ok('\\'),
|
||||
Some('/') => Ok('/'),
|
||||
Some('b') => Ok('\u{08}'),
|
||||
Some('f') => Ok('\u{0C}'),
|
||||
Some('n') => Ok('\n'),
|
||||
Some('r') => Ok('\r'),
|
||||
Some('t') => Ok('\t'),
|
||||
Some('u') => self.parse_unicode_escape(),
|
||||
Some(other) => Err(JsonError::new(format!("invalid escape sequence: {other}"))),
|
||||
None => Err(JsonError::new("unexpected end of input in escape sequence")),
|
||||
let mut result = String::new();
|
||||
loop {
|
||||
match chars.next() {
|
||||
None => return Err(JsonError::new("unterminated string")),
|
||||
Some('"') => return Ok(result),
|
||||
Some('\\') => match chars.next() {
|
||||
Some('"') => result.push('"'),
|
||||
Some('\\') => result.push('\\'),
|
||||
Some('/') => result.push('/'),
|
||||
Some('b') => result.push('\u{08}'),
|
||||
Some('f') => result.push('\u{0C}'),
|
||||
Some('n') => result.push('\n'),
|
||||
Some('r') => result.push('\r'),
|
||||
Some('t') => result.push('\t'),
|
||||
Some('u') => {
|
||||
let mut hex = String::new();
|
||||
for _ in 0..4 {
|
||||
match chars.next() {
|
||||
Some(c @ '0'..='9') | Some(c @ 'a'..='f') | Some(c @ 'A'..='F') => {
|
||||
hex.push(c)
|
||||
}
|
||||
_ => return Err(JsonError::new("invalid unicode escape")),
|
||||
}
|
||||
}
|
||||
let code = u32::from_str_radix(&hex, 16)
|
||||
.map_err(|_| JsonError::new("invalid hex code"))?;
|
||||
if (0xD800..=0xDFFF).contains(&code) {
|
||||
return Err(JsonError::new("surrogate not allowed"));
|
||||
}
|
||||
result.push(
|
||||
char::from_u32(code).ok_or_else(|| JsonError::new("invalid codepoint"))?,
|
||||
);
|
||||
}
|
||||
_ => return Err(JsonError::new("invalid escape")),
|
||||
},
|
||||
Some(c) => result.push(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unicode_escape(&mut self) -> Result<char, JsonError> {
|
||||
let mut value = 0_u32;
|
||||
for _ in 0..4 {
|
||||
let Some(ch) = self.next() else {
|
||||
return Err(JsonError::new("unexpected end of input in unicode escape"));
|
||||
};
|
||||
value = (value << 4)
|
||||
| ch.to_digit(16)
|
||||
.ok_or_else(|| JsonError::new("invalid unicode escape"))?;
|
||||
}
|
||||
char::from_u32(value).ok_or_else(|| JsonError::new("invalid unicode scalar value"))
|
||||
fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
let mut num_str = String::new();
|
||||
|
||||
if chars.peek() == Some(&'-') {
|
||||
num_str.push(chars.next().unwrap());
|
||||
}
|
||||
|
||||
fn parse_array(&mut self) -> Result<JsonValue, JsonError> {
|
||||
self.expect('[')?;
|
||||
let mut values = Vec::new();
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.try_consume(']') {
|
||||
break;
|
||||
}
|
||||
values.push(self.parse_value()?);
|
||||
self.skip_whitespace();
|
||||
if self.try_consume(']') {
|
||||
break;
|
||||
}
|
||||
self.expect(',')?;
|
||||
}
|
||||
Ok(JsonValue::Array(values))
|
||||
}
|
||||
|
||||
fn parse_object(&mut self) -> Result<JsonValue, JsonError> {
|
||||
self.expect('{')?;
|
||||
let mut entries = BTreeMap::new();
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.try_consume('}') {
|
||||
break;
|
||||
}
|
||||
let key = self.parse_string()?;
|
||||
self.skip_whitespace();
|
||||
self.expect(':')?;
|
||||
let value = self.parse_value()?;
|
||||
entries.insert(key, value);
|
||||
self.skip_whitespace();
|
||||
if self.try_consume('}') {
|
||||
break;
|
||||
}
|
||||
self.expect(',')?;
|
||||
}
|
||||
Ok(JsonValue::Object(entries))
|
||||
}
|
||||
|
||||
fn parse_number(&mut self) -> Result<i64, JsonError> {
|
||||
let mut value = String::new();
|
||||
if self.try_consume('-') {
|
||||
value.push('-');
|
||||
}
|
||||
|
||||
while let Some(ch @ '0'..='9') = self.peek() {
|
||||
value.push(ch);
|
||||
self.index += 1;
|
||||
}
|
||||
|
||||
if value.is_empty() || value == "-" {
|
||||
return Err(JsonError::new("invalid number"));
|
||||
}
|
||||
|
||||
value
|
||||
.parse::<i64>()
|
||||
.map_err(|_| JsonError::new("number out of range"))
|
||||
}
|
||||
|
||||
fn expect(&mut self, expected: char) -> Result<(), JsonError> {
|
||||
match self.next() {
|
||||
Some(actual) if actual == expected => Ok(()),
|
||||
Some(actual) => Err(JsonError::new(format!(
|
||||
"expected '{expected}', found '{actual}'"
|
||||
))),
|
||||
None => Err(JsonError::new(format!(
|
||||
"expected '{expected}', found end of input"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_consume(&mut self, expected: char) -> bool {
|
||||
if self.peek() == Some(expected) {
|
||||
self.index += 1;
|
||||
true
|
||||
while let Some(&c) = chars.peek() {
|
||||
if c.is_ascii_digit() {
|
||||
num_str.push(chars.next().unwrap());
|
||||
} else {
|
||||
false
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while matches!(self.peek(), Some(' ' | '\n' | '\r' | '\t')) {
|
||||
self.index += 1;
|
||||
if num_str.is_empty() || num_str == "-" {
|
||||
return Err(JsonError::new("invalid number"));
|
||||
}
|
||||
|
||||
num_str
|
||||
.parse::<i64>()
|
||||
.map(JsonValue::Number)
|
||||
.map_err(|_| JsonError::new("number out of range"))
|
||||
}
|
||||
|
||||
fn parse_array(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
if chars.next() != Some('[') {
|
||||
return Err(JsonError::new("expected ["));
|
||||
}
|
||||
|
||||
let mut values = Vec::new();
|
||||
|
||||
skip_whitespace(chars);
|
||||
if chars.peek() == Some(&']') {
|
||||
chars.next();
|
||||
return Ok(JsonValue::Array(values));
|
||||
}
|
||||
|
||||
loop {
|
||||
skip_whitespace(chars);
|
||||
values.push(parse_value(chars)?);
|
||||
skip_whitespace(chars);
|
||||
|
||||
match chars.peek() {
|
||||
Some(',') => {
|
||||
chars.next();
|
||||
}
|
||||
Some(']') => {
|
||||
chars.next();
|
||||
return Ok(JsonValue::Array(values));
|
||||
}
|
||||
_ => return Err(JsonError::new("expected , or ]")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<char> {
|
||||
self.chars.get(self.index).copied()
|
||||
fn parse_object(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<JsonValue, JsonError> {
|
||||
if chars.next() != Some('{') {
|
||||
return Err(JsonError::new("expected {"));
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<char> {
|
||||
let ch = self.peek()?;
|
||||
self.index += 1;
|
||||
Some(ch)
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
skip_whitespace(chars);
|
||||
if chars.peek() == Some(&'}') {
|
||||
chars.next();
|
||||
return Ok(JsonValue::Object(map));
|
||||
}
|
||||
|
||||
fn is_eof(&self) -> bool {
|
||||
self.index >= self.chars.len()
|
||||
loop {
|
||||
skip_whitespace(chars);
|
||||
|
||||
if chars.peek() != Some(&'"') {
|
||||
return Err(JsonError::new("expected key string"));
|
||||
}
|
||||
let key = parse_string(chars)?;
|
||||
|
||||
skip_whitespace(chars);
|
||||
if chars.next() != Some(':') {
|
||||
return Err(JsonError::new("expected :"));
|
||||
}
|
||||
|
||||
skip_whitespace(chars);
|
||||
let value = parse_value(chars)?;
|
||||
map.insert(key, value);
|
||||
|
||||
skip_whitespace(chars);
|
||||
match chars.peek() {
|
||||
Some(',') => {
|
||||
chars.next();
|
||||
}
|
||||
Some('}') => {
|
||||
chars.next();
|
||||
return Ok(JsonValue::Object(map));
|
||||
}
|
||||
_ => return Err(JsonError::new("expected , or }")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_literal(chars: &mut std::iter::Peekable<std::str::Chars>, literal: &str) -> bool {
|
||||
let remaining: String = chars.clone().collect();
|
||||
if remaining.starts_with(literal) {
|
||||
for _ in literal.chars() {
|
||||
chars.next();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_whitespace(chars: &mut std::iter::Peekable<std::str::Chars>) {
|
||||
while let Some(&c) = chars.peek() {
|
||||
if c.is_whitespace() {
|
||||
chars.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_string(s: &str) -> String {
|
||||
let mut result = String::from('"');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => result.push_str("\\\""),
|
||||
'\\' => result.push_str("\\\\"),
|
||||
'\n' => result.push_str("\\n"),
|
||||
'\r' => result.push_str("\\r"),
|
||||
'\t' => result.push_str("\\t"),
|
||||
'\u{08}' => result.push_str("\\b"),
|
||||
'\u{0C}' => result.push_str("\\f"),
|
||||
c if c.is_control() => {
|
||||
let code = c as u32;
|
||||
result.push_str(&format!("\\u{code:04x}"));
|
||||
}
|
||||
c => result.push(c),
|
||||
}
|
||||
}
|
||||
result.push('"');
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{render_string, JsonValue};
|
||||
use std::collections::BTreeMap;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renders_and_parses_json_values() {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert("flag".to_string(), JsonValue::Bool(true));
|
||||
object.insert(
|
||||
"items".to_string(),
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::Number(4),
|
||||
JsonValue::String("ok".to_string()),
|
||||
]),
|
||||
fn renders_primitive_values() {
|
||||
assert_eq!(JsonValue::Null.render(), "null");
|
||||
assert_eq!(JsonValue::Bool(true).render(), "true");
|
||||
assert_eq!(JsonValue::Bool(false).render(), "false");
|
||||
assert_eq!(JsonValue::Number(42).render(), "42");
|
||||
assert_eq!(JsonValue::Number(-7).render(), "-7");
|
||||
assert_eq!(JsonValue::String("hi".to_string()).render(), "\"hi\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_arrays_and_objects_with_sorted_keys() {
|
||||
assert_eq!(JsonValue::Array(Vec::new()).render(), "[]");
|
||||
assert_eq!(
|
||||
JsonValue::Array(vec![JsonValue::Number(1), JsonValue::Number(2)]).render(),
|
||||
"[1,2]"
|
||||
);
|
||||
assert_eq!(JsonValue::Object(BTreeMap::new()).render(), "{}");
|
||||
|
||||
let rendered = JsonValue::Object(object).render();
|
||||
let parsed = JsonValue::parse(&rendered).expect("json should parse");
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert("b".to_string(), JsonValue::Bool(false));
|
||||
object.insert("a".to_string(), JsonValue::Number(1));
|
||||
// A BTreeMap iterates keys in sorted order, so object rendering is
|
||||
// deterministic regardless of insertion order.
|
||||
assert_eq!(JsonValue::Object(object).render(), "{\"a\":1,\"b\":false}");
|
||||
}
|
||||
|
||||
assert_eq!(parsed.as_object().expect("object").len(), 2);
|
||||
#[test]
|
||||
fn render_string_escapes_quotes_backslashes_and_other_controls() {
|
||||
// Backslash and double-quote are backslash-escaped.
|
||||
assert_eq!(render_string("\"\\"), "\"\\\"\\\\\"");
|
||||
// Carriage return has a dedicated short escape.
|
||||
assert_eq!(render_string("\r"), "\"\\r\"");
|
||||
// Other control characters fall back to the \uXXXX form (U+0001 here).
|
||||
assert_eq!(render_string("\u{1}"), "\"\\u0001\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_primitive_values() {
|
||||
assert_eq!(
|
||||
JsonValue::parse("null").expect("null parses"),
|
||||
JsonValue::Null
|
||||
);
|
||||
assert_eq!(
|
||||
JsonValue::parse("true").expect("true parses"),
|
||||
JsonValue::Bool(true)
|
||||
);
|
||||
assert_eq!(
|
||||
JsonValue::parse("false").expect("false parses"),
|
||||
JsonValue::Bool(false)
|
||||
);
|
||||
assert_eq!(
|
||||
JsonValue::parse("\"hello\"").expect("string parses"),
|
||||
JsonValue::String("hello".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_signed_integers_and_ignores_surrounding_whitespace() {
|
||||
assert_eq!(
|
||||
JsonValue::parse("-42").expect("negative parses").as_i64(),
|
||||
Some(-42)
|
||||
);
|
||||
assert_eq!(
|
||||
JsonValue::parse(" 123 ").expect("padded parses").as_i64(),
|
||||
Some(123)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_nested_arrays_and_objects() {
|
||||
let value =
|
||||
JsonValue::parse("{\"nums\": [1, 2], \"flag\": true}").expect("nested json parses");
|
||||
let object = value.as_object().expect("top level is an object");
|
||||
|
||||
assert_eq!(object.len(), 2);
|
||||
|
||||
let nums = object
|
||||
.get("nums")
|
||||
.and_then(JsonValue::as_array)
|
||||
.expect("nums is an array");
|
||||
assert_eq!(nums.len(), 2);
|
||||
assert_eq!(nums[0].as_i64(), Some(1));
|
||||
|
||||
assert_eq!(object.get("flag").and_then(JsonValue::as_bool), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_rendered_values_through_the_parser() {
|
||||
let mut object = BTreeMap::new();
|
||||
object.insert("name".to_string(), JsonValue::String("clawi".to_string()));
|
||||
object.insert("count".to_string(), JsonValue::Number(-3));
|
||||
object.insert(
|
||||
"tags".to_string(),
|
||||
JsonValue::Array(vec![JsonValue::Bool(true), JsonValue::Null]),
|
||||
);
|
||||
let original = JsonValue::Object(object);
|
||||
|
||||
let reparsed = JsonValue::parse(&original.render()).expect("rendered json should re-parse");
|
||||
|
||||
assert_eq!(reparsed, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -433,6 +519,7 @@ mod tests {
|
||||
object.insert("a\nb".to_string(), JsonValue::String("c\td".to_string()));
|
||||
let original = JsonValue::Object(object);
|
||||
let reparsed = JsonValue::parse(&original.render()).expect("escaped key re-parses");
|
||||
|
||||
assert_eq!(reparsed, original);
|
||||
}
|
||||
|
||||
@@ -471,4 +558,37 @@ mod tests {
|
||||
"i64::MIN - 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_malformed_input() {
|
||||
assert!(JsonValue::parse("").is_err(), "empty input");
|
||||
assert!(JsonValue::parse("nul").is_err(), "truncated literal");
|
||||
assert!(JsonValue::parse("true false").is_err(), "trailing content");
|
||||
assert!(
|
||||
JsonValue::parse("\"unterminated").is_err(),
|
||||
"unterminated string"
|
||||
);
|
||||
assert!(JsonValue::parse("[1, 2").is_err(), "unterminated array");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_integers_outside_the_i64_range() {
|
||||
// Far beyond i64::MAX (~9.2e18): the parser must reject rather than
|
||||
// silently truncate the value.
|
||||
assert!(JsonValue::parse("9999999999999999999999999").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accessors_return_none_for_mismatched_variants() {
|
||||
assert_eq!(JsonValue::Null.as_bool(), None);
|
||||
assert_eq!(JsonValue::Bool(true).as_i64(), None);
|
||||
assert!(JsonValue::Number(1).as_str().is_none());
|
||||
assert!(JsonValue::Number(1).as_array().is_none());
|
||||
assert!(JsonValue::String("x".to_string()).as_object().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_error_displays_its_message() {
|
||||
assert_eq!(JsonError::new("boom").to_string(), "boom");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user