Skip to main content

maskforge_core/frontend/
mod.rs

1//! Converts JSON Schema text into validated schema IR.
2
3mod ast;
4mod json_schema;
5pub(crate) mod regex;
6mod resources;
7
8pub use json_schema::{schema_to_ir, schema_to_ir_timed};
9pub(crate) use json_schema::{JSON_STRING_BODY, JSON_STRING_CODEPOINT};
10pub use regex::regex_to_ir;
11#[cfg(all(test, feature = "alloc-probe"))]
12pub(crate) use resources::discover_memory_for_probe;
13pub use resources::{SchemaRegistry, SchemaResourceLimits};
14
15use crate::error::CompileError;
16use crate::ir::{CompileOptions, SchemaIR};
17
18#[doc(hidden)]
19#[cfg(feature = "bench-internals")]
20#[derive(Clone, Copy, Debug)]
21pub struct SchemaSetProfile {
22    pub registry_insert_ns: u64,
23    pub document_parse_ns: u64,
24    pub resource_uri_anchor_index_ns: u64,
25    pub resource_canonicalize_ns: u64,
26    pub reference_resolution_ns: u64,
27    pub graph_freeze_ns: u64,
28    pub ir_lowering_and_construction_validation_ns: u64,
29    pub ir_revalidation_ns: u64,
30    pub retained_graph_bytes: usize,
31    pub peak_resource_build_bytes: usize,
32}
33
34pub fn schema_to_ir_with_external_refs(
35    text: &str,
36    options: CompileOptions,
37    resolver: &rustc_hash::FxHashMap<String, String>,
38) -> Result<SchemaIR, CompileError> {
39    let limits = SchemaResourceLimits::default();
40    let mut registry = SchemaRegistry::new(limits);
41    for (uri, schema) in resolver {
42        registry.insert(uri, std::sync::Arc::<str>::from(schema.as_str()))?;
43    }
44    json_schema::schema_set_to_ir(text, None, options, limits, registry)
45}
46
47pub fn schema_to_ir_with_resources(
48    text: &str,
49    retrieval_uri: Option<&str>,
50    options: CompileOptions,
51    limits: SchemaResourceLimits,
52    registry: SchemaRegistry,
53) -> Result<SchemaIR, CompileError> {
54    json_schema::schema_set_to_ir(text, retrieval_uri, options, limits, registry)
55}
56
57#[doc(hidden)]
58#[cfg(feature = "bench-internals")]
59pub fn schema_to_ir_with_resources_profiled(
60    text: &str,
61    retrieval_uri: Option<&str>,
62    options: CompileOptions,
63    limits: SchemaResourceLimits,
64    registry: SchemaRegistry,
65) -> Result<(SchemaIR, SchemaSetProfile), CompileError> {
66    json_schema::schema_set_to_ir_profiled(text, retrieval_uri, options, limits, registry)
67}
68
69#[cfg(test)]
70mod resource_lowering_tests {
71    use super::*;
72
73    fn compile(root: &str, retrieval: Option<&str>, resources: &[(&str, &str)]) -> SchemaIR {
74        let limits = SchemaResourceLimits::default();
75        let mut registry = SchemaRegistry::new(limits);
76        for (uri, schema) in resources {
77            registry.insert(uri, *schema).expect("resource");
78        }
79        schema_to_ir_with_resources(root, retrieval, CompileOptions::default(), limits, registry)
80            .expect("schema set")
81    }
82
83    #[test]
84    fn external_static_root_pointer_anchor_and_transitive_refs_lower_once() {
85        let ir = compile(
86            r##"{
87                "type":"object",
88                "properties":{
89                    "a":{"$ref":"defs.json#/$defs/value"},
90                    "b":{"$ref":"defs.json#value"}
91                },
92                "required":["a","b"],
93                "additionalProperties":false
94            }"##,
95            Some("https://example.com/root.json"),
96            &[(
97                "https://example.com/defs.json",
98                r##"{"$defs":{"value":{"$anchor":"value","type":"integer","minimum":5}}}"##,
99            )],
100        );
101        assert_eq!(ir.resources().len(), 2);
102        assert!(crate::structured::try_accepts(
103            std::sync::Arc::new(ir.clone()),
104            br#"{"a":5,"b":6}"#
105        )
106        .expect("accept"));
107        assert!(
108            !crate::structured::try_accepts(std::sync::Arc::new(ir), br#"{"a":4,"b":6}"#)
109                .expect("reject")
110        );
111    }
112
113    #[test]
114    fn productive_external_mutual_recursion_is_not_a_resolver_cycle() {
115        let ir = compile(
116            r##"{"$ref":"a.json"}"##,
117            Some("https://example.com/root.json"),
118            &[
119                (
120                    "https://example.com/a.json",
121                    r##"{"type":"object","properties":{"b":{"$ref":"b.json"}},"additionalProperties":false}"##,
122                ),
123                (
124                    "https://example.com/b.json",
125                    r##"{"type":"object","properties":{"a":{"$ref":"a.json"}},"additionalProperties":false}"##,
126                ),
127            ],
128        );
129        assert!(
130            crate::structured::try_accepts(std::sync::Arc::new(ir), br#"{"b":{"a":{}}}"#)
131                .expect("accept")
132        );
133    }
134
135    #[test]
136    fn nested_id_changes_the_active_reference_base() {
137        let ir = compile(
138            r##"{
139                "$defs":{"nested":{"$id":"folder/child.json","$ref":"value.json"}},
140                "$ref":"folder/child.json"
141            }"##,
142            Some("https://example.com/root.json"),
143            &[(
144                "https://example.com/folder/value.json",
145                r##"{"type":"string"}"##,
146            )],
147        );
148        assert!(
149            crate::structured::try_accepts(std::sync::Arc::new(ir), br#""ok""#).expect("accept")
150        );
151    }
152
153    #[test]
154    fn registry_insertion_order_does_not_change_the_frozen_hash() {
155        fn build(reverse: bool) -> [u8; 32] {
156            let limits = SchemaResourceLimits::default();
157            let mut registry = SchemaRegistry::new(limits);
158            let resources = [
159                (
160                    "https://example.com/a",
161                    r##"{"$anchor":"a","type":"string"}"##,
162                ),
163                (
164                    "https://example.com/b",
165                    r##"{"$anchor":"b","type":"integer"}"##,
166                ),
167            ];
168            for index in if reverse { [1, 0] } else { [0, 1] } {
169                registry
170                    .insert(resources[index].0, resources[index].1)
171                    .expect("resource");
172            }
173            schema_to_ir_with_resources(
174                r##"{"allOf":[{"$ref":"https://example.com/a#a"},{"$ref":"https://example.com/b#b"}]}"##,
175                Some("https://example.com/root"),
176                CompileOptions::default(),
177                limits,
178                registry,
179            )
180            .expect("schema set")
181            .canonical_hash()
182        }
183        assert_eq!(build(false), build(true));
184    }
185
186    #[test]
187    fn anonymous_root_resolves_empty_pointer_and_plain_name_fragments() {
188        let ir = compile(
189            r##"{
190                "$defs":{"value":{"$anchor":"value","type":"integer"}},
191                "allOf":[{"$ref":"#/$defs/value"},{"$ref":"#value"}]
192            }"##,
193            None,
194            &[],
195        );
196        assert!(crate::structured::try_accepts(std::sync::Arc::new(ir.clone()), b"7").unwrap());
197        assert!(!crate::structured::try_accepts(std::sync::Arc::new(ir), br#""x""#).unwrap());
198    }
199}