Skip to content

Commit c8187e0

Browse files
authored
feat(typia): align lenient parse recovery with TS parity (#336)
## Summary - align Rust `typia` lenient JSON EOF recovery with upstream TS behavior - add upstream parse parity test coverage (66 mapped parse cases) - expand runtime `validate`/`stringify` boundary tests - sync typia contracts/docs with parity baseline and explicit exclusions ## Details - update `crates/typia/src/lenient_json.rs` to recover truncated EOF inputs without parser-token errors - keep syntax/token violations and depth guard as failures - add `crates/typia/tests/llm_json_parse_parity.rs` with table-driven parity checks - update `crates/typia/tests/llm_data_runtime.rs` expectations for recovery semantics - update docs: - `docs/crates-typia-core-foundation.md` - `docs/project-typia.md` - `crates/typia/README.md` ## Explicit parity exclusions - JS `undefined` expectations - non-finite numbers (`Infinity`, `-Infinity`) - lone-surrogate code-unit expectations ## Validation - `cargo test -p typia` - `cargo test`
1 parent 264aff2 commit c8187e0

6 files changed

Lines changed: 515 additions & 34 deletions

File tree

crates/typia/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ Supported recovery behaviors:
4848
- unicode escapes (including surrogate-pair decoding)
4949
- depth guard (`MAX_DEPTH = 512`)
5050

51+
Current parse parity baseline for lenient behavior:
52+
53+
- `samchon/typia` `master` parse suite at commit
54+
`29a02742661d476ce5ef5414fe32acc7e97c0e6c`
55+
56+
Important parity exclusions:
57+
58+
- JS `undefined`-dependent expectations
59+
- non-finite numbers (`Infinity`, `-Infinity`)
60+
- lone-surrogate code-unit expectations
61+
62+
Additional EOF recovery contract:
63+
64+
- incomplete/truncated but structurally recoverable inputs are accepted by the
65+
lenient parser (for example: unclosed object/array/string, key-only EOF, and
66+
key-colon EOF), while token/syntax violations still surface failures.
67+
5168
## Local Validation
5269

5370
Run from repository root:

crates/typia/src/lenient_json.rs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,6 @@ impl<'a> LenientJsonParser<'a> {
234234

235235
self.skip_whitespace();
236236
if self.pos >= self.chars.len() {
237-
self.errors.push(LlmJsonParseError {
238-
path: path.to_owned(),
239-
expected: "':'".to_owned(),
240-
description: "end of input".to_owned(),
241-
});
242237
self.depth -= 1;
243238
return Some(Value::Object(result));
244239
}
@@ -256,11 +251,6 @@ impl<'a> LenientJsonParser<'a> {
256251

257252
self.skip_whitespace();
258253
if self.pos >= self.chars.len() {
259-
self.errors.push(LlmJsonParseError {
260-
path: format!("{path}.{key}"),
261-
expected: "JSON value".to_owned(),
262-
description: "end of input".to_owned(),
263-
});
264254
self.depth -= 1;
265255
return Some(Value::Object(result));
266256
}
@@ -275,11 +265,6 @@ impl<'a> LenientJsonParser<'a> {
275265
}
276266
}
277267

278-
self.errors.push(LlmJsonParseError {
279-
path: path.to_owned(),
280-
expected: "'}'".to_owned(),
281-
description: "end of input".to_owned(),
282-
});
283268
self.depth -= 1;
284269
Some(Value::Object(result))
285270
}
@@ -332,16 +317,11 @@ impl<'a> LenientJsonParser<'a> {
332317
}
333318
}
334319

335-
self.errors.push(LlmJsonParseError {
336-
path: path.to_owned(),
337-
expected: "']'".to_owned(),
338-
description: "end of input".to_owned(),
339-
});
340320
self.depth -= 1;
341321
Some(Value::Array(result))
342322
}
343323

344-
fn parse_string(&mut self, path: &str) -> String {
324+
fn parse_string(&mut self, _path: &str) -> String {
345325
self.pos += 1;
346326
let mut result = String::new();
347327
let mut escaped = false;
@@ -417,11 +397,6 @@ impl<'a> LenientJsonParser<'a> {
417397
self.pos += 1;
418398
}
419399

420-
self.errors.push(LlmJsonParseError {
421-
path: path.to_owned(),
422-
expected: "closing quote".to_owned(),
423-
description: "end of input".to_owned(),
424-
});
425400
result
426401
}
427402

crates/typia/tests/llm_data_runtime.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,20 @@ fn parse_unicode_surrogate_pair() {
125125
}
126126

127127
#[test]
128-
fn parse_incomplete_json_reports_failure_with_partial_data() {
128+
fn parse_incomplete_json_recovers_to_success() {
129129
let result = User::parse(r#"{"id":1,"name":"alice""#);
130130

131131
match result {
132-
LlmJsonParseResult::Failure { data, errors, .. } => {
133-
assert!(data.is_some(), "expected partial data");
134-
assert!(!errors.is_empty(), "expected parser errors");
135-
assert!(
136-
errors.iter().any(|error| error.expected.contains("'}'")),
137-
"expected missing object terminator error"
132+
LlmJsonParseResult::Success { data } => {
133+
assert_eq!(
134+
data,
135+
User {
136+
id: 1,
137+
name: "alice".to_owned(),
138+
}
138139
);
139140
}
140-
other => panic!("expected failure, got {other:?}"),
141+
other => panic!("expected success, got {other:?}"),
141142
}
142143
}
143144

@@ -213,3 +214,45 @@ fn validate_and_stringify_use_serde() {
213214
let encoded = validated.stringify().expect("stringify should succeed");
214215
assert_eq!(encoded, r#"{"id":42,"name":"eve"}"#);
215216
}
217+
218+
#[test]
219+
fn validate_reports_missing_required_field() {
220+
let value = typia::serde_json::json!({
221+
"id": 7
222+
});
223+
224+
let error = User::validate(value).expect_err("validation should fail");
225+
assert!(
226+
error.to_string().contains("missing field"),
227+
"expected missing field error"
228+
);
229+
}
230+
231+
#[test]
232+
fn validate_reports_type_mismatch() {
233+
let value = typia::serde_json::json!({
234+
"id": "not-a-number",
235+
"name": "eve"
236+
});
237+
238+
let error = User::validate(value).expect_err("validation should fail");
239+
assert!(
240+
error.to_string().contains("invalid type"),
241+
"expected invalid type error"
242+
);
243+
}
244+
245+
#[test]
246+
fn stringify_roundtrip_through_validate() {
247+
let user = User {
248+
id: 9,
249+
name: "frank".to_owned(),
250+
};
251+
252+
let encoded = user.stringify().expect("stringify should succeed");
253+
let decoded: typia::serde_json::Value =
254+
typia::serde_json::from_str(&encoded).expect("must be valid JSON");
255+
256+
let validated = User::validate(decoded).expect("validation should succeed");
257+
assert_eq!(validated, user);
258+
}

0 commit comments

Comments
 (0)