···11+MIT License
22+33+Copyright (c) 2025 Slices Network
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy of
66+this software and associated documentation files (the "Software"), to deal in
77+the Software without restriction, including without limitation the rights to
88+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
99+the Software, and to permit persons to whom the Software is furnished to do so,
1010+subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
1717+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
1818+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
1919+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
2020+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
···99// WASM-specific code - only compiled when the wasm feature is enabled
1010#[cfg(feature = "wasm")]
1111mod wasm_bindings {
1212- use wasm_bindgen::prelude::*;
1313- use serde_json::Value;
1412 use super::*;
1313+ use serde_json::Value;
1414+ use wasm_bindgen::prelude::*;
15151616 // When the `console_error_panic_hook` feature is enabled, we can call the
1717 // `set_panic_hook` function at least once during initialization, and then
···4040 let lexicons: Vec<Value> = serde_json::from_str(lexicons_json)
4141 .map_err(|e| JsValue::from_str(&format!("Failed to parse lexicons JSON: {}", e)))?;
42424343- let validator = LexiconValidator::new(lexicons)
4444- .map_err(|e| JsValue::from_str(&format!("Failed to create validator: {}", e)))?;
4343+ let validator =
4444+ LexiconValidator::new(lexicons).map_err(|e| JsValue::from_str(&e.to_string()))?;
45454646 Ok(WasmLexiconValidator { inner: validator })
4747 }
···55555656 self.inner
5757 .validate_record(collection, &record)
5858- .map_err(|e| JsValue::from_str(&format!("Validation failed: {}", e)))
5858+ .map_err(|e| JsValue::from_str(&e.to_string()))
5959 }
60606161 /// Validate that all cross-lexicon references can be resolved
···6363 pub fn validate_lexicon_set_completeness(&self) -> Result<(), JsValue> {
6464 self.inner
6565 .validate_lexicon_set_completeness()
6666- .map_err(|e| JsValue::from_str(&format!("Lexicon set validation failed: {}", e)))
6666+ .map_err(|e| JsValue::from_str(&e.to_string()))
6767 }
6868 }
6969···8383 .map_err(|e| JsValue::from_str(&format!("String format validation failed: {}", e)))
8484 }
85858686- // Utility function to check if a string is a valid lexicon ID
8686+ // Utility function to check if a string is a valid lexicon ID (uses shared implementation)
8787 #[wasm_bindgen]
8888 pub fn is_valid_nsid(nsid: &str) -> bool {
8989- use regex::Regex;
9090- let nsid_regex = Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$").unwrap();
9191- nsid_regex.is_match(nsid)
8989+ crate::is_valid_nsid(nsid)
9290 }
9391}
9492···9997// Non-WASM utility functions that are always available
10098pub fn is_valid_nsid(nsid: &str) -> bool {
10199 use regex::Regex;
102102- let nsid_regex = Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$").unwrap();
100100+ // NSID must have at least one dot (require reverse-domain-name format)
101101+ let nsid_regex = Regex::new(
102102+ r"^[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$",
103103+ )
104104+ .unwrap();
103105 nsid_regex.is_match(nsid)
104106}
105107106108#[cfg(test)]
107107-mod tests;109109+mod tests;
+573
packages/lexicon-rs/src/tests.rs
···294294 }),
295295 json!({
296296 "lexicon": 1,
297297+ "id": "com.example.stringGraphemes",
298298+ "defs": {
299299+ "main": {
300300+ "type": "record",
301301+ "record": {
302302+ "type": "object",
303303+ "properties": {
304304+ "emoji": {
305305+ "type": "string",
306306+ "minGraphemes": 1,
307307+ "maxGraphemes": 3
308308+ }
309309+ }
310310+ }
311311+ }
312312+ }
313313+ }),
314314+ json!({
315315+ "lexicon": 1,
316316+ "id": "com.example.blobConstraints",
317317+ "defs": {
318318+ "main": {
319319+ "type": "record",
320320+ "record": {
321321+ "type": "object",
322322+ "properties": {
323323+ "image": {
324324+ "type": "blob",
325325+ "accept": ["image/png", "image/jpeg"],
326326+ "maxSize": 1000000
327327+ }
328328+ }
329329+ }
330330+ }
331331+ }
332332+ }),
333333+ json!({
334334+ "lexicon": 1,
297335 "id": "com.example.nsid",
298336 "defs": {
299337 "main": {
···10101048 // Test with complete lexicon set (should pass)
10111049 let complete_lexicons = vec![
10121050 serde_json::json!({
10511051+ "lexicon": 1,
10131052 "id": "com.example.base",
10141053 "defs": {
10151054 "user": {
···10211060 }
10221061 }),
10231062 serde_json::json!({
10631063+ "lexicon": 1,
10241064 "id": "com.example.posts",
10251065 "defs": {
10261066 "main": {
···1046108610471087 // Test with missing lexicon reference (should fail)
10481088 let incomplete_lexicons = vec![serde_json::json!({
10891089+ "lexicon": 1,
10491090 "id": "com.example.posts",
10501091 "defs": {
10511092 "main": {
···11761217 // Note: Token validation happens at the union level, not at the token definition level
11771218 // The union validation should catch invalid references, but for now tokens just accept anything
11781219 // This is consistent with how tokens work - they're type discriminators, not value validators
12201220+}
12211221+12221222+#[test]
12231223+fn test_invalid_types_validation() {
12241224+ // Test the invalid lexicon from the user's example
12251225+ let invalid_lexicon = json!({
12261226+ "lexicon": 1,
12271227+ "id": "com.recordcollector.album",
12281228+ "defs": {
12291229+ "main": {
12301230+ "type": "hey", // Invalid type
12311231+ "description": "A vinyl album record",
12321232+ "record": {
12331233+ "type": "object",
12341234+ "properties": {
12351235+ "title": {
12361236+ "type": "whoa", // Invalid type
12371237+ "description": "Album title"
12381238+ },
12391239+ "artist": {
12401240+ "type": "string",
12411241+ "description": "Artist or band name"
12421242+ }
12431243+ }
12441244+ }
12451245+ }
12461246+ }
12471247+ });
12481248+12491249+ // Validation should fail during lexicon loading due to invalid types
12501250+ let result = LexiconValidator::new(vec![invalid_lexicon]);
12511251+ assert!(result.is_err(), "Validator should reject lexicon with invalid types");
12521252+12531253+ let error_msg = result.unwrap_err().to_string();
12541254+ assert!(
12551255+ error_msg.contains("unknown type 'hey'") || error_msg.contains("unknown type 'whoa'"),
12561256+ "Error should mention the invalid types: {}",
12571257+ error_msg
12581258+ );
12591259+12601260+ // Test another invalid lexicon with invalid property type
12611261+ let invalid_property_type = json!({
12621262+ "lexicon": 1,
12631263+ "id": "com.example.invalid",
12641264+ "defs": {
12651265+ "main": {
12661266+ "type": "record",
12671267+ "record": {
12681268+ "type": "object",
12691269+ "properties": {
12701270+ "badField": {
12711271+ "type": "invalidType", // Invalid type
12721272+ "description": "A field with invalid type"
12731273+ }
12741274+ }
12751275+ }
12761276+ }
12771277+ }
12781278+ });
12791279+12801280+ let result2 = LexiconValidator::new(vec![invalid_property_type]);
12811281+ assert!(result2.is_err(), "Validator should reject lexicon with invalid property types");
12821282+12831283+ let error_msg2 = result2.unwrap_err().to_string();
12841284+ assert!(
12851285+ error_msg2.contains("unknown type 'invalidType'"),
12861286+ "Error should mention the invalid property type: {}",
12871287+ error_msg2
12881288+ );
12891289+}
12901290+12911291+#[test]
12921292+fn test_lexicon_document_structure_validation() {
12931293+ // Test missing lexicon version field
12941294+ let missing_version = json!({
12951295+ "id": "com.example.test",
12961296+ "defs": {
12971297+ "main": {
12981298+ "type": "string"
12991299+ }
13001300+ }
13011301+ });
13021302+13031303+ let result = LexiconValidator::new(vec![missing_version]);
13041304+ assert!(result.is_err(), "Should reject lexicon without version field");
13051305+ assert!(result.unwrap_err().to_string().contains("Missing or invalid lexicon version"));
13061306+13071307+ // Test invalid lexicon version
13081308+ let invalid_version = json!({
13091309+ "lexicon": 2,
13101310+ "id": "com.example.test",
13111311+ "defs": {
13121312+ "main": {
13131313+ "type": "string"
13141314+ }
13151315+ }
13161316+ });
13171317+13181318+ let result = LexiconValidator::new(vec![invalid_version]);
13191319+ assert!(result.is_err(), "Should reject unsupported lexicon version");
13201320+ assert!(result.unwrap_err().to_string().contains("Unsupported lexicon version: 2"));
13211321+13221322+ // Test invalid lexicon ID format
13231323+ let invalid_id = json!({
13241324+ "lexicon": 1,
13251325+ "id": "123invalid.nsid", // Starts with number, invalid NSID
13261326+ "defs": {
13271327+ "main": {
13281328+ "type": "string"
13291329+ }
13301330+ }
13311331+ });
13321332+13331333+ let result = LexiconValidator::new(vec![invalid_id]);
13341334+ assert!(result.is_err(), "Should reject invalid NSID format");
13351335+ assert!(result.unwrap_err().to_string().contains("Invalid lexicon ID format"));
13361336+13371337+ // Test valid lexicon document
13381338+ let valid_lexicon = json!({
13391339+ "lexicon": 1,
13401340+ "id": "com.example.test",
13411341+ "defs": {
13421342+ "main": {
13431343+ "type": "string"
13441344+ }
13451345+ }
13461346+ });
13471347+13481348+ let result = LexiconValidator::new(vec![valid_lexicon]);
13491349+ assert!(result.is_ok(), "Should accept valid lexicon document");
13501350+}
13511351+13521352+#[test]
13531353+fn test_strict_field_validation() {
13541354+ // Test rejection of unknown top-level fields
13551355+ let unknown_fields = json!({
13561356+ "lexicon": 1,
13571357+ "id": "com.example.test",
13581358+ "defs": {
13591359+ "main": {
13601360+ "type": "string"
13611361+ }
13621362+ },
13631363+ "maaain": "some random value",
13641364+ "randomStuff": 42
13651365+ });
13661366+13671367+ let result = LexiconValidator::new(vec![unknown_fields]);
13681368+ assert!(result.is_err(), "Should reject lexicon with unknown fields");
13691369+ let error_msg = result.unwrap_err().to_string();
13701370+ assert!(
13711371+ error_msg.contains("Unrecognized key(s) in lexicon document") &&
13721372+ (error_msg.contains("maaain") || error_msg.contains("randomStuff")),
13731373+ "Error should mention the unknown fields: {}",
13741374+ error_msg
13751375+ );
13761376+13771377+ // Test that optional fields are allowed
13781378+ let with_optional_fields = json!({
13791379+ "lexicon": 1,
13801380+ "id": "com.example.test",
13811381+ "revision": 123,
13821382+ "description": "A test lexicon",
13831383+ "defs": {
13841384+ "main": {
13851385+ "type": "string"
13861386+ }
13871387+ }
13881388+ });
13891389+13901390+ let result = LexiconValidator::new(vec![with_optional_fields]);
13911391+ assert!(result.is_ok(), "Should accept lexicon with optional fields");
13921392+}
13931393+13941394+#[test]
13951395+fn test_nsid_validation_directly() {
13961396+ // Test that our NSID validation now requires dots
13971397+ assert!(crate::is_valid_nsid("com.example.test"), "Should accept valid NSID with dots");
13981398+ assert!(!crate::is_valid_nsid("invalid-nsid"), "Should reject NSID without dots");
13991399+ assert!(!crate::is_valid_nsid("123invalid.nsid"), "Should reject NSID starting with number");
14001400+}
14011401+14021402+#[test]
14031403+fn test_multiple_validation_errors() {
14041404+ // Test lexicon with multiple invalid types in different definitions
14051405+ let multiple_errors_lexicon = json!({
14061406+ "lexicon": 1,
14071407+ "id": "com.example.test",
14081408+ "defs": {
14091409+ "post": {
14101410+ "type": "banana", // Invalid type
14111411+ "description": "A post"
14121412+ },
14131413+ "comment": {
14141414+ "type": "pizza", // Invalid type
14151415+ "description": "A comment"
14161416+ },
14171417+ "user": {
14181418+ "type": "record", // Valid type
14191419+ "record": {
14201420+ "type": "object",
14211421+ "properties": {
14221422+ "name": {
14231423+ "type": "string"
14241424+ },
14251425+ "score": {
14261426+ "type": "magical", // Invalid type in property
14271427+ "description": "User score"
14281428+ }
14291429+ }
14301430+ }
14311431+ }
14321432+ }
14331433+ });
14341434+14351435+ let result = LexiconValidator::new(vec![multiple_errors_lexicon]);
14361436+ assert!(result.is_err(), "Should reject lexicon with multiple invalid types");
14371437+14381438+ let error_msg = result.unwrap_err().to_string();
14391439+14401440+ // Should contain all the invalid types in the error message
14411441+ assert!(error_msg.contains("banana"), "Should mention 'banana' type error");
14421442+ assert!(error_msg.contains("pizza"), "Should mention 'pizza' type error");
14431443+ assert!(error_msg.contains("magical"), "Should mention 'magical' type error in property");
14441444+ assert!(error_msg.contains("Multiple validation errors"), "Should indicate multiple errors");
14451445+14461446+ println!("Multiple errors captured: {}", error_msg);
14471447+}
14481448+14491449+#[test]
14501450+fn test_multiple_property_errors() {
14511451+ // Test lexicon with multiple invalid property types within a single record
14521452+ let property_errors_lexicon = json!({
14531453+ "lexicon": 1,
14541454+ "id": "com.example.props",
14551455+ "defs": {
14561456+ "main": {
14571457+ "type": "record",
14581458+ "record": {
14591459+ "type": "object",
14601460+ "properties": {
14611461+ "title": {
14621462+ "type": "whoa", // Invalid type
14631463+ "description": "Title"
14641464+ },
14651465+ "author": {
14661466+ "type": "hey", // Invalid type
14671467+ "description": "Author"
14681468+ },
14691469+ "content": {
14701470+ "type": "string" // Valid type
14711471+ }
14721472+ }
14731473+ }
14741474+ }
14751475+ }
14761476+ });
14771477+14781478+ let result = LexiconValidator::new(vec![property_errors_lexicon]);
14791479+ assert!(result.is_err(), "Should reject lexicon with multiple invalid property types");
14801480+14811481+ let error_msg = result.unwrap_err().to_string();
14821482+14831483+ // Should contain both property type errors
14841484+ assert!(error_msg.contains("whoa"), "Should mention 'whoa' property type error");
14851485+ assert!(error_msg.contains("hey"), "Should mention 'hey' property type error");
14861486+ assert!(error_msg.contains("Multiple validation errors"), "Should indicate multiple errors");
14871487+14881488+ println!("Multiple property errors captured: {}", error_msg);
14891489+}
14901490+14911491+#[test]
14921492+fn test_grapheme_validation() {
14931493+ let validator = LexiconValidator::new(get_test_lexicons()).unwrap();
14941494+14951495+ // Valid emoji within grapheme limit
14961496+ let valid_emoji = json!({ "emoji": "👨👩👧👦" }); // 1 grapheme (family emoji)
14971497+ assert!(validator.validate_record("com.example.stringGraphemes", &valid_emoji).is_ok());
14981498+14991499+ // Too many graphemes
15001500+ let too_many_emoji = json!({ "emoji": "🎉🎊🎈🎁" }); // 4 graphemes > max 3
15011501+ assert!(validator.validate_record("com.example.stringGraphemes", &too_many_emoji).is_err());
15021502+15031503+ // Edge case: exactly at limit
15041504+ let at_limit = json!({ "emoji": "👍👎👌" }); // exactly 3 graphemes
15051505+ assert!(validator.validate_record("com.example.stringGraphemes", &at_limit).is_ok());
15061506+}
15071507+15081508+#[test]
15091509+fn test_blob_validation() {
15101510+ let validator = LexiconValidator::new(get_test_lexicons()).unwrap();
15111511+15121512+ // Valid blob with accepted MIME type and size
15131513+ let valid_blob = json!({
15141514+ "image": {
15151515+ "$type": "blob",
15161516+ "ref": { "$link": "bafytest123" },
15171517+ "mimeType": "image/png",
15181518+ "size": 500000
15191519+ }
15201520+ });
15211521+ assert!(validator.validate_record("com.example.blobConstraints", &valid_blob).is_ok());
15221522+15231523+ // Invalid MIME type
15241524+ let invalid_mime = json!({
15251525+ "image": {
15261526+ "$type": "blob",
15271527+ "ref": { "$link": "bafytest123" },
15281528+ "mimeType": "image/gif", // not in accept list
15291529+ "size": 500000
15301530+ }
15311531+ });
15321532+ assert!(validator.validate_record("com.example.blobConstraints", &invalid_mime).is_err());
15331533+15341534+ // Size too large
15351535+ let too_large = json!({
15361536+ "image": {
15371537+ "$type": "blob",
15381538+ "ref": { "$link": "bafytest123" },
15391539+ "mimeType": "image/png",
15401540+ "size": 2000000 // exceeds maxSize
15411541+ }
15421542+ });
15431543+ assert!(validator.validate_record("com.example.blobConstraints", &too_large).is_err());
15441544+}
15451545+15461546+#[test]
15471547+fn test_structural_validation() {
15481548+ // Test AT Protocol structural validation rules
15491549+15501550+ // Test array without items field
15511551+ let array_no_items = json!({
15521552+ "lexicon": 1,
15531553+ "id": "com.example.bad",
15541554+ "defs": {
15551555+ "main": {
15561556+ "type": "array"
15571557+ // Missing required "items" field
15581558+ }
15591559+ }
15601560+ });
15611561+ let result = LexiconValidator::new(vec![array_no_items]);
15621562+ assert!(result.is_err());
15631563+ assert!(result.unwrap_err().to_string().contains("must have 'items' field"));
15641564+15651565+ // Test union without refs field
15661566+ let union_no_refs = json!({
15671567+ "lexicon": 1,
15681568+ "id": "com.example.bad",
15691569+ "defs": {
15701570+ "main": {
15711571+ "type": "union"
15721572+ // Missing required "refs" field
15731573+ }
15741574+ }
15751575+ });
15761576+ let result = LexiconValidator::new(vec![union_no_refs]);
15771577+ assert!(result.is_err());
15781578+ assert!(result.unwrap_err().to_string().contains("must have 'refs' field"));
15791579+15801580+ // Test ref without ref field
15811581+ let ref_no_ref = json!({
15821582+ "lexicon": 1,
15831583+ "id": "com.example.bad",
15841584+ "defs": {
15851585+ "main": {
15861586+ "type": "ref"
15871587+ // Missing required "ref" field
15881588+ }
15891589+ }
15901590+ });
15911591+ let result = LexiconValidator::new(vec![ref_no_ref]);
15921592+ assert!(result.is_err());
15931593+ assert!(result.unwrap_err().to_string().contains("must have 'ref' field"));
15941594+15951595+ // Test object with required field not in properties
15961596+ let object_bad_required = json!({
15971597+ "lexicon": 1,
15981598+ "id": "com.example.bad",
15991599+ "defs": {
16001600+ "main": {
16011601+ "type": "object",
16021602+ "required": ["name", "missing"],
16031603+ "properties": {
16041604+ "name": { "type": "string" }
16051605+ // "missing" field not in properties
16061606+ }
16071607+ }
16081608+ }
16091609+ });
16101610+ let result = LexiconValidator::new(vec![object_bad_required]);
16111611+ assert!(result.is_err());
16121612+ assert!(result.unwrap_err().to_string().contains("required field 'missing' not found in properties"));
16131613+16141614+ // Test valid structural definitions
16151615+ let valid_structural = json!({
16161616+ "lexicon": 1,
16171617+ "id": "com.example.good",
16181618+ "defs": {
16191619+ "myArray": {
16201620+ "type": "array",
16211621+ "items": { "type": "string" }
16221622+ },
16231623+ "myUnion": {
16241624+ "type": "union",
16251625+ "refs": ["#myArray", "#myObject"]
16261626+ },
16271627+ "myRef": {
16281628+ "type": "ref",
16291629+ "ref": "#myObject"
16301630+ },
16311631+ "myObject": {
16321632+ "type": "object",
16331633+ "required": ["name"],
16341634+ "properties": {
16351635+ "name": { "type": "string" },
16361636+ "optional": { "type": "integer" }
16371637+ }
16381638+ }
16391639+ }
16401640+ });
16411641+ let result = LexiconValidator::new(vec![valid_structural]);
16421642+ assert!(result.is_ok(), "Valid structural definitions should pass");
16431643+}
16441644+16451645+#[test]
16461646+fn test_default_validation() {
16471647+ // Test that invalid defaults are caught during lexicon compilation
16481648+16491649+ // String default violating minLength
16501650+ let string_minlength_violation = json!({
16511651+ "lexicon": 1,
16521652+ "id": "com.example.stringMinLengthDefault",
16531653+ "defs": {
16541654+ "main": {
16551655+ "type": "record",
16561656+ "record": {
16571657+ "type": "object",
16581658+ "properties": {
16591659+ "name": {
16601660+ "type": "string",
16611661+ "minLength": 5,
16621662+ "default": "hi"
16631663+ }
16641664+ }
16651665+ }
16661666+ }
16671667+ }
16681668+ });
16691669+ let result = LexiconValidator::new(vec![string_minlength_violation]);
16701670+ assert!(result.is_err());
16711671+ assert!(result.unwrap_err().to_string().contains("default value 'hi' length 2 is less than minimum 5"));
16721672+16731673+ // String default violating enum
16741674+ let string_enum_violation = json!({
16751675+ "lexicon": 1,
16761676+ "id": "com.example.stringEnumDefault",
16771677+ "defs": {
16781678+ "main": {
16791679+ "type": "record",
16801680+ "record": {
16811681+ "type": "object",
16821682+ "properties": {
16831683+ "color": {
16841684+ "type": "string",
16851685+ "enum": ["red", "blue", "green"],
16861686+ "default": "yellow"
16871687+ }
16881688+ }
16891689+ }
16901690+ }
16911691+ }
16921692+ });
16931693+ let result = LexiconValidator::new(vec![string_enum_violation]);
16941694+ assert!(result.is_err());
16951695+ assert!(result.unwrap_err().to_string().contains("default value 'yellow' must be one of (red|blue|green)"));
16961696+16971697+ // Integer default violating minimum
16981698+ let integer_minimum_violation = json!({
16991699+ "lexicon": 1,
17001700+ "id": "com.example.integerMinimumDefault",
17011701+ "defs": {
17021702+ "main": {
17031703+ "type": "record",
17041704+ "record": {
17051705+ "type": "object",
17061706+ "properties": {
17071707+ "age": {
17081708+ "type": "integer",
17091709+ "minimum": 10,
17101710+ "default": 5
17111711+ }
17121712+ }
17131713+ }
17141714+ }
17151715+ }
17161716+ });
17171717+ let result = LexiconValidator::new(vec![integer_minimum_violation]);
17181718+ assert!(result.is_err());
17191719+ assert!(result.unwrap_err().to_string().contains("default value 5 is less than minimum 10"));
17201720+17211721+ // Boolean default violating const
17221722+ let boolean_const_violation = json!({
17231723+ "lexicon": 1,
17241724+ "id": "com.example.booleanConstDefault",
17251725+ "defs": {
17261726+ "main": {
17271727+ "type": "record",
17281728+ "record": {
17291729+ "type": "object",
17301730+ "properties": {
17311731+ "enabled": {
17321732+ "type": "boolean",
17331733+ "const": true,
17341734+ "default": false
17351735+ }
17361736+ }
17371737+ }
17381738+ }
17391739+ }
17401740+ });
17411741+ let result = LexiconValidator::new(vec![boolean_const_violation]);
17421742+ assert!(result.is_err());
17431743+ assert!(result.unwrap_err().to_string().contains("default value false does not match const value true"));
17441744+17451745+ // Test that valid defaults pass - the existing kitchen sink includes com.example.validDefaults
17461746+ let lexicons = get_test_lexicons();
17471747+ let result = LexiconValidator::new(lexicons);
17481748+ match result {
17491749+ Ok(_) => {}, // Success
17501750+ Err(e) => panic!("Kitchen sink with valid defaults should pass validation, but got error: {}", e),
17511751+ }
11791752}
+196
packages/lexicon-rs/src/types.rs
···4949 }
5050}
51515252+/// String constraints for lexicon string types
5353+#[derive(Debug, Clone, Default)]
5454+pub struct StringConstraints {
5555+ pub min_length: Option<u32>,
5656+ pub max_length: Option<u32>,
5757+ pub min_graphemes: Option<u32>,
5858+ pub max_graphemes: Option<u32>,
5959+ pub enum_values: Option<Vec<String>>,
6060+ pub const_value: Option<String>,
6161+ pub default: Option<String>,
6262+ pub format: Option<StringFormat>,
6363+ pub known_values: Option<Vec<String>>,
6464+}
6565+6666+/// Integer constraints for lexicon integer types
6767+#[derive(Debug, Clone, Default)]
6868+pub struct IntegerConstraints {
6969+ pub minimum: Option<i64>,
7070+ pub maximum: Option<i64>,
7171+ pub enum_values: Option<Vec<i64>>,
7272+ pub const_value: Option<i64>,
7373+ pub default: Option<i64>,
7474+}
7575+7676+/// Array constraints for lexicon array types
7777+#[derive(Debug, Clone, Default)]
7878+pub struct ArrayConstraints {
7979+ pub min_length: Option<u32>,
8080+ pub max_length: Option<u32>,
8181+}
8282+8383+/// Boolean constraints for lexicon boolean types
8484+#[derive(Debug, Clone, Default)]
8585+pub struct BooleanConstraints {
8686+ pub const_value: Option<bool>,
8787+ pub default: Option<bool>,
8888+}
8989+9090+/// Bytes constraints for lexicon bytes types
9191+#[derive(Debug, Clone, Default)]
9292+pub struct BytesConstraints {
9393+ pub min_length: Option<u32>,
9494+ pub max_length: Option<u32>,
9595+}
9696+9797+/// Blob constraints for lexicon blob types
9898+#[derive(Debug, Clone, Default)]
9999+pub struct BlobConstraints {
100100+ pub accept: Option<Vec<String>>,
101101+ pub max_size: Option<u64>,
102102+}
103103+52104/// Validation context to track the current path in the object being validated
53105#[derive(Debug, Clone)]
54106pub struct ValidationContext {
55107 pub path: Vec<String>,
108108+}
109109+110110+impl StringConstraints {
111111+ pub fn from_json(value: &Value) -> Self {
112112+ let mut constraints = Self::default();
113113+114114+ if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) {
115115+ constraints.min_length = Some(min_len as u32);
116116+ }
117117+ if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) {
118118+ constraints.max_length = Some(max_len as u32);
119119+ }
120120+ if let Some(min_graphemes) = value.get("minGraphemes").and_then(|v| v.as_u64()) {
121121+ constraints.min_graphemes = Some(min_graphemes as u32);
122122+ }
123123+ if let Some(max_graphemes) = value.get("maxGraphemes").and_then(|v| v.as_u64()) {
124124+ constraints.max_graphemes = Some(max_graphemes as u32);
125125+ }
126126+ if let Some(format_str) = value.get("format").and_then(|v| v.as_str()) {
127127+ constraints.format = StringFormat::from_str(format_str);
128128+ }
129129+ if let Some(const_val) = value.get("const").and_then(|v| v.as_str()) {
130130+ constraints.const_value = Some(const_val.to_string());
131131+ }
132132+ if let Some(default_val) = value.get("default").and_then(|v| v.as_str()) {
133133+ constraints.default = Some(default_val.to_string());
134134+ }
135135+ if let Some(enum_array) = value.get("enum").and_then(|v| v.as_array()) {
136136+ let enum_values: Vec<String> = enum_array
137137+ .iter()
138138+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
139139+ .collect();
140140+ if !enum_values.is_empty() {
141141+ constraints.enum_values = Some(enum_values);
142142+ }
143143+ }
144144+ if let Some(known_array) = value.get("knownValues").and_then(|v| v.as_array()) {
145145+ let known_values: Vec<String> = known_array
146146+ .iter()
147147+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
148148+ .collect();
149149+ if !known_values.is_empty() {
150150+ constraints.known_values = Some(known_values);
151151+ }
152152+ }
153153+154154+ constraints
155155+ }
156156+}
157157+158158+impl IntegerConstraints {
159159+ pub fn from_json(value: &Value) -> Self {
160160+ let mut constraints = Self::default();
161161+162162+ if let Some(min) = value.get("minimum").and_then(|v| v.as_i64()) {
163163+ constraints.minimum = Some(min);
164164+ }
165165+ if let Some(max) = value.get("maximum").and_then(|v| v.as_i64()) {
166166+ constraints.maximum = Some(max);
167167+ }
168168+ if let Some(const_val) = value.get("const").and_then(|v| v.as_i64()) {
169169+ constraints.const_value = Some(const_val);
170170+ }
171171+ if let Some(default_val) = value.get("default").and_then(|v| v.as_i64()) {
172172+ constraints.default = Some(default_val);
173173+ }
174174+ if let Some(enum_array) = value.get("enum").and_then(|v| v.as_array()) {
175175+ let enum_values: Vec<i64> = enum_array
176176+ .iter()
177177+ .filter_map(|v| v.as_i64())
178178+ .collect();
179179+ if !enum_values.is_empty() {
180180+ constraints.enum_values = Some(enum_values);
181181+ }
182182+ }
183183+184184+ constraints
185185+ }
186186+}
187187+188188+impl ArrayConstraints {
189189+ pub fn from_json(value: &Value) -> Self {
190190+ let mut constraints = Self::default();
191191+192192+ if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) {
193193+ constraints.min_length = Some(min_len as u32);
194194+ }
195195+ if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) {
196196+ constraints.max_length = Some(max_len as u32);
197197+ }
198198+199199+ constraints
200200+ }
201201+}
202202+203203+impl BooleanConstraints {
204204+ pub fn from_json(value: &Value) -> Self {
205205+ let mut constraints = Self::default();
206206+207207+ if let Some(const_val) = value.get("const").and_then(|v| v.as_bool()) {
208208+ constraints.const_value = Some(const_val);
209209+ }
210210+ if let Some(default_val) = value.get("default").and_then(|v| v.as_bool()) {
211211+ constraints.default = Some(default_val);
212212+ }
213213+214214+ constraints
215215+ }
216216+}
217217+218218+impl BytesConstraints {
219219+ pub fn from_json(value: &Value) -> Self {
220220+ let mut constraints = Self::default();
221221+222222+ if let Some(min_len) = value.get("minLength").and_then(|v| v.as_u64()) {
223223+ constraints.min_length = Some(min_len as u32);
224224+ }
225225+ if let Some(max_len) = value.get("maxLength").and_then(|v| v.as_u64()) {
226226+ constraints.max_length = Some(max_len as u32);
227227+ }
228228+229229+ constraints
230230+ }
231231+}
232232+233233+impl BlobConstraints {
234234+ pub fn from_json(value: &Value) -> Self {
235235+ let mut constraints = Self::default();
236236+237237+ if let Some(accept_array) = value.get("accept").and_then(|v| v.as_array()) {
238238+ let accept_values: Vec<String> = accept_array
239239+ .iter()
240240+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
241241+ .collect();
242242+ if !accept_values.is_empty() {
243243+ constraints.accept = Some(accept_values);
244244+ }
245245+ }
246246+ if let Some(max_size) = value.get("maxSize").and_then(|v| v.as_u64()) {
247247+ constraints.max_size = Some(max_size);
248248+ }
249249+250250+ constraints
251251+ }
56252}
5725358254impl ValidationContext {
+1216-215
packages/lexicon-rs/src/validator.rs
···22use regex::Regex;
33use serde_json::Value;
44use std::collections::HashMap;
55+use unicode_segmentation::UnicodeSegmentation;
5667use super::errors::ValidationError;
77-use super::types::{LexiconDoc, StringFormat, ValidationContext};
88+use super::types::{
99+ ArrayConstraints, BlobConstraints, BooleanConstraints, BytesConstraints, IntegerConstraints,
1010+ LexiconDoc, StringConstraints, StringFormat, ValidationContext
1111+};
81299-#[derive(Clone)]
1313+#[derive(Clone, Debug)]
1014pub struct LexiconValidator {
1115 lexicons: HashMap<String, LexiconDoc>,
1216}
···1721 let mut lexicon_map = HashMap::new();
18221923 for lexicon_value in lexicons {
2424+ // Validate that only allowed top-level fields are present
2525+ if let Some(obj) = lexicon_value.as_object() {
2626+ let allowed_fields = ["lexicon", "id", "defs", "revision", "description"];
2727+ let mut unknown_fields = Vec::new();
2828+2929+ for key in obj.keys() {
3030+ if !allowed_fields.contains(&key.as_str()) {
3131+ unknown_fields.push(key.clone());
3232+ }
3333+ }
3434+3535+ if !unknown_fields.is_empty() {
3636+ return Err(ValidationError::InvalidSchema(format!(
3737+ "Unrecognized key(s) in lexicon document: {}",
3838+ unknown_fields.join(", ")
3939+ )));
4040+ }
4141+ }
4242+4343+ // Validate lexicon version field
4444+ let lexicon_version = lexicon_value["lexicon"]
4545+ .as_u64()
4646+ .ok_or_else(|| ValidationError::InvalidSchema("Missing or invalid lexicon version field".to_string()))?;
4747+4848+ if lexicon_version != 1 {
4949+ return Err(ValidationError::InvalidSchema(format!(
5050+ "Unsupported lexicon version: {}. Only version 1 is supported.",
5151+ lexicon_version
5252+ )));
5353+ }
5454+2055 let id = lexicon_value["id"]
2156 .as_str()
2257 .ok_or_else(|| ValidationError::InvalidSchema("Missing lexicon id".to_string()))?
2358 .to_string();
24596060+ // Validate lexicon ID format (NSID)
6161+ if !crate::is_valid_nsid(&id) {
6262+ return Err(ValidationError::InvalidSchema(format!(
6363+ "Invalid lexicon ID format: '{}'. Must be a valid NSID.",
6464+ id
6565+ )));
6666+ }
6767+2568 let defs = lexicon_value["defs"].clone();
2669 if defs.is_null() {
2770 return Err(ValidationError::InvalidSchema(format!(
···3073 )));
3174 }
32753333- lexicon_map.insert(id.clone(), LexiconDoc { id, defs });
7676+ let lexicon_doc = LexiconDoc { id: id.clone(), defs };
7777+7878+ // Validate the lexicon definitions immediately upon loading
7979+ Self::validate_lexicon_definitions_static(&lexicon_doc.defs)?;
8080+8181+ lexicon_map.insert(id, lexicon_doc);
3482 }
35833684 Ok(Self {
···103151 Ok(())
104152 }
105153154154+ /// Static version of lexicon definitions validation for use during loading
155155+ fn validate_lexicon_definitions_static(definitions: &Value) -> Result<(), ValidationError> {
156156+ let definitions_obj = definitions.as_object().ok_or_else(|| {
157157+ ValidationError::InvalidSchema("Lexicon definitions must be a JSON object".to_string())
158158+ })?;
159159+160160+ let mut errors = Vec::new();
161161+162162+ // Each key should be a valid definition name, each value should be a valid definition
163163+ for (def_name, def_value) in definitions_obj {
164164+ // Validate definition name (should be camelCase identifier - letters and numbers only)
165165+ if def_name.is_empty() || !def_name.chars().all(|c| c.is_ascii_alphanumeric()) {
166166+ errors.push(format!(
167167+ "Invalid definition name '{}': must be camelCase (letters and numbers only)",
168168+ def_name
169169+ ));
170170+ continue;
171171+ }
172172+173173+ // Validate definition structure
174174+ let def_type = match def_value.get("type").and_then(|v| v.as_str()) {
175175+ Some(t) => t,
176176+ None => {
177177+ errors.push(format!(
178178+ "Definition '{}' missing required 'type' field",
179179+ def_name
180180+ ));
181181+ continue;
182182+ }
183183+ };
184184+185185+ // Validate based on definition type
186186+ match def_type {
187187+ "record" => {
188188+ if let Err(e) = Self::validate_record_definition_static(def_name, def_value) {
189189+ errors.push(e.to_string());
190190+ }
191191+ }
192192+ "object" => {
193193+ if let Err(e) = Self::validate_object_definition_static(def_name, def_value) {
194194+ errors.push(e.to_string());
195195+ }
196196+ }
197197+ "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes"
198198+ | "cid-link" | "unknown" | "token" | "null" => {
199199+ // Basic types are valid, could add more specific validation here
200200+ if let Err(e) = Self::validate_type_definition_static(def_name, def_value, def_type) {
201201+ errors.push(e.to_string());
202202+ }
203203+ }
204204+ _ => {
205205+ errors.push(format!(
206206+ "Definition '{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null",
207207+ def_name, def_type
208208+ ));
209209+ }
210210+ }
211211+ }
212212+213213+ if errors.is_empty() {
214214+ Ok(())
215215+ } else {
216216+ Err(ValidationError::MultipleErrors { errors })
217217+ }
218218+ }
219219+106220 /// Validate that a JSON value contains valid lexicon definitions
107221 fn validate_lexicon_definitions(&self, definitions: &Value) -> Result<(), ValidationError> {
108222 let definitions_obj = definitions.as_object().ok_or_else(|| {
···135249 "record" => self.validate_record_definition(def_name, def_value)?,
136250 "object" => self.validate_object_definition(def_name, def_value)?,
137251 "string" | "integer" | "boolean" | "array" | "union" | "ref" | "blob" | "bytes"
138138- | "cid-link" | "unknown" | "token" => {
252252+ | "cid-link" | "unknown" | "token" | "null" => {
139253 // Basic types are valid, could add more specific validation here
254254+ self.validate_type_definition(def_name, def_value, def_type)?;
140255 }
141256 _ => {
142257 return Err(ValidationError::InvalidSchema(format!(
143143- "Definition '{}' has unknown type '{}'",
258258+ "Definition '{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null",
144259 def_name, def_type
145260 )));
146261 }
···180295 def_name
181296 )));
182297 }
298298+299299+ // Validate each property's type
300300+ for (prop_name, prop_def) in properties.as_object().unwrap() {
301301+ self.validate_property_definition(def_name, prop_name, prop_def)?;
302302+ }
183303 }
184304185305 Ok(())
186306 }
187307308308+ /// Static version of record definition validation
309309+ fn validate_record_definition_static(
310310+ def_name: &str,
311311+ def_value: &Value,
312312+ ) -> Result<(), ValidationError> {
313313+ let mut errors = Vec::new();
314314+315315+ // Validate allowed fields for record definition
316316+ let allowed_record_fields = [
317317+ "type", "record", "description", "key"
318318+ ];
319319+ Self::validate_allowed_fields(def_name, def_value, &allowed_record_fields, "Record", &mut errors);
320320+321321+ // Record definitions should have a "record" field
322322+ let record_def = match def_value.get("record") {
323323+ Some(r) => r,
324324+ None => {
325325+ return Err(ValidationError::InvalidSchema(format!(
326326+ "Record definition '{}' missing 'record' field",
327327+ def_name
328328+ )));
329329+ }
330330+ };
331331+332332+ // The record field should be an object type
333333+ if record_def.get("type").and_then(|v| v.as_str()) != Some("object") {
334334+ errors.push(format!(
335335+ "Record definition '{}' record field must be type 'object'",
336336+ def_name
337337+ ));
338338+ }
339339+340340+ // Validate allowed fields for the nested record object
341341+ let allowed_object_fields = [
342342+ "type", "properties", "required", "nullable", "description"
343343+ ];
344344+ Self::validate_allowed_fields(&format!("{}.record", def_name), record_def, &allowed_object_fields, "Object", &mut errors);
345345+346346+ // Validate properties if they exist
347347+ if let Some(properties) = record_def.get("properties") {
348348+ if !properties.is_object() {
349349+ errors.push(format!(
350350+ "Record definition '{}' properties must be an object",
351351+ def_name
352352+ ));
353353+ } else {
354354+ // Validate each property's type
355355+ for (prop_name, prop_def) in properties.as_object().unwrap() {
356356+ Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors);
357357+ }
358358+ }
359359+ }
360360+361361+ // Validate required field if it exists
362362+ if let Some(required) = record_def.get("required") {
363363+ if !required.is_array() {
364364+ errors.push(format!(
365365+ "Record definition '{}' required field must be an array",
366366+ def_name
367367+ ));
368368+ } else {
369369+ // Check that all required fields exist in properties
370370+ if let Some(properties) = record_def.get("properties").and_then(|p| p.as_object()) {
371371+ for req_field in required.as_array().unwrap() {
372372+ if let Some(field_name) = req_field.as_str() {
373373+ if !properties.contains_key(field_name) {
374374+ errors.push(format!(
375375+ "Record definition '{}' required field '{}' not found in properties",
376376+ def_name, field_name
377377+ ));
378378+ }
379379+ }
380380+ }
381381+ }
382382+ }
383383+ }
384384+385385+ if errors.is_empty() {
386386+ Ok(())
387387+ } else {
388388+ Err(ValidationError::MultipleErrors { errors })
389389+ }
390390+ }
391391+392392+ /// Static version of object definition validation
393393+ fn validate_object_definition_static(
394394+ def_name: &str,
395395+ def_value: &Value,
396396+ ) -> Result<(), ValidationError> {
397397+ let mut errors = Vec::new();
398398+399399+ // Validate allowed fields
400400+ let allowed_object_fields = [
401401+ "type", "properties", "required", "nullable", "description"
402402+ ];
403403+ Self::validate_allowed_fields(def_name, def_value, &allowed_object_fields, "Object", &mut errors);
404404+405405+ // Object definitions should have properties
406406+ if let Some(properties) = def_value.get("properties") {
407407+ if !properties.is_object() {
408408+ errors.push(format!(
409409+ "Object definition '{}' properties must be an object",
410410+ def_name
411411+ ));
412412+ } else {
413413+ // Validate each property's type
414414+ for (prop_name, prop_def) in properties.as_object().unwrap() {
415415+ Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors);
416416+ }
417417+ }
418418+ }
419419+420420+ // Validate required field if it exists
421421+ if let Some(required) = def_value.get("required") {
422422+ if !required.is_array() {
423423+ errors.push(format!(
424424+ "Object definition '{}' required field must be an array",
425425+ def_name
426426+ ));
427427+ } else {
428428+ // Check that all required fields exist in properties
429429+ if let Some(properties) = def_value.get("properties").and_then(|p| p.as_object()) {
430430+ for req_field in required.as_array().unwrap() {
431431+ if let Some(field_name) = req_field.as_str() {
432432+ if !properties.contains_key(field_name) {
433433+ errors.push(format!(
434434+ "Object definition '{}' required field '{}' not found in properties",
435435+ def_name, field_name
436436+ ));
437437+ }
438438+ }
439439+ }
440440+ }
441441+ }
442442+ }
443443+444444+ // Validate nullable field if present
445445+ if let Some(nullable) = def_value.get("nullable") {
446446+ if !nullable.is_array() {
447447+ errors.push(format!(
448448+ "Object definition '{}' nullable field must be an array",
449449+ def_name
450450+ ));
451451+ }
452452+ }
453453+454454+ if errors.is_empty() {
455455+ Ok(())
456456+ } else {
457457+ Err(ValidationError::MultipleErrors { errors })
458458+ }
459459+ }
460460+461461+ /// Validate that only allowed fields are present in a definition
462462+ fn validate_allowed_fields(
463463+ def_name: &str,
464464+ def_value: &Value,
465465+ allowed_fields: &[&str],
466466+ def_type: &str,
467467+ errors: &mut Vec<String>,
468468+ ) {
469469+ if let Some(obj) = def_value.as_object() {
470470+ let mut unknown_fields = Vec::new();
471471+472472+ for key in obj.keys() {
473473+ if !allowed_fields.contains(&key.as_str()) {
474474+ unknown_fields.push(key.clone());
475475+ }
476476+ }
477477+478478+ if !unknown_fields.is_empty() {
479479+ errors.push(format!(
480480+ "{} definition '{}' has unknown field(s): {}. Allowed fields are: {}",
481481+ def_type,
482482+ def_name,
483483+ unknown_fields.join(", "),
484484+ allowed_fields.join(", ")
485485+ ));
486486+ }
487487+ }
488488+ }
489489+490490+ /// Static version of type definition validation with AT Protocol structural rules
491491+ fn validate_type_definition_static(
492492+ def_name: &str,
493493+ def_value: &Value,
494494+ type_name: &str,
495495+ ) -> Result<(), ValidationError> {
496496+ let mut errors = Vec::new();
497497+498498+ // Apply AT Protocol structural validation rules
499499+ match type_name {
500500+ "object" => {
501501+ let allowed_object_fields = [
502502+ "type", "properties", "required", "nullable", "description"
503503+ ];
504504+ Self::validate_allowed_fields(def_name, def_value, &allowed_object_fields, "Object", &mut errors);
505505+506506+ // Object type should have properties field
507507+ if let Some(properties) = def_value.get("properties") {
508508+ if !properties.is_object() {
509509+ errors.push(format!(
510510+ "Object definition '{}' properties must be an object",
511511+ def_name
512512+ ));
513513+ } else {
514514+ // Validate each property's type
515515+ for (prop_name, prop_def) in properties.as_object().unwrap() {
516516+ Self::validate_property_definition_static(def_name, prop_name, prop_def, &mut errors);
517517+ }
518518+ }
519519+ }
520520+521521+ // Validate required field if present
522522+ if let Some(required) = def_value.get("required") {
523523+ if !required.is_array() {
524524+ errors.push(format!(
525525+ "Object definition '{}' required field must be an array",
526526+ def_name
527527+ ));
528528+ } else {
529529+ // Check that all required fields exist in properties
530530+ if let Some(properties) = def_value.get("properties").and_then(|p| p.as_object()) {
531531+ for req_field in required.as_array().unwrap() {
532532+ if let Some(field_name) = req_field.as_str() {
533533+ if !properties.contains_key(field_name) {
534534+ errors.push(format!(
535535+ "Object definition '{}' required field '{}' not found in properties",
536536+ def_name, field_name
537537+ ));
538538+ }
539539+ }
540540+ }
541541+ }
542542+ }
543543+ }
544544+545545+ // Validate nullable field if present
546546+ if let Some(nullable) = def_value.get("nullable") {
547547+ if !nullable.is_array() {
548548+ errors.push(format!(
549549+ "Object definition '{}' nullable field must be an array",
550550+ def_name
551551+ ));
552552+ }
553553+ }
554554+ },
555555+ "array" => {
556556+ // Array type must have items field
557557+ if def_value.get("items").is_none() {
558558+ errors.push(format!(
559559+ "Array definition '{}' must have 'items' field",
560560+ def_name
561561+ ));
562562+ } else {
563563+ let items = &def_value["items"];
564564+ // Validate that items has a type
565565+ if !items.get("type").and_then(|t| t.as_str()).is_some() &&
566566+ !items.get("ref").and_then(|r| r.as_str()).is_some() {
567567+ errors.push(format!(
568568+ "Array definition '{}' items must have 'type' or 'ref' field",
569569+ def_name
570570+ ));
571571+ }
572572+ }
573573+ },
574574+ "union" => {
575575+ // Union type must have refs field
576576+ if def_value.get("refs").is_none() {
577577+ errors.push(format!(
578578+ "Union definition '{}' must have 'refs' field",
579579+ def_name
580580+ ));
581581+ } else {
582582+ let refs = &def_value["refs"];
583583+ if !refs.is_array() {
584584+ errors.push(format!(
585585+ "Union definition '{}' refs field must be an array",
586586+ def_name
587587+ ));
588588+ } else if refs.as_array().unwrap().is_empty() {
589589+ errors.push(format!(
590590+ "Union definition '{}' refs field must not be empty",
591591+ def_name
592592+ ));
593593+ }
594594+ }
595595+ },
596596+ "ref" => {
597597+ // Reference type must have ref field
598598+ if def_value.get("ref").is_none() {
599599+ errors.push(format!(
600600+ "Reference definition '{}' must have 'ref' field",
601601+ def_name
602602+ ));
603603+ } else {
604604+ let ref_val = &def_value["ref"];
605605+ if !ref_val.is_string() {
606606+ errors.push(format!(
607607+ "Reference definition '{}' ref field must be a string",
608608+ def_name
609609+ ));
610610+ }
611611+ }
612612+ },
613613+ "blob" => {
614614+ // Blob type constraints are validated at runtime, not at definition time
615615+ // But we can validate accept and maxSize field types if present
616616+ if let Some(accept) = def_value.get("accept") {
617617+ if !accept.is_array() {
618618+ errors.push(format!(
619619+ "Blob definition '{}' accept field must be an array",
620620+ def_name
621621+ ));
622622+ }
623623+ }
624624+ if let Some(max_size) = def_value.get("maxSize") {
625625+ if !max_size.is_number() {
626626+ errors.push(format!(
627627+ "Blob definition '{}' maxSize field must be a number",
628628+ def_name
629629+ ));
630630+ }
631631+ }
632632+ },
633633+ "string" => {
634634+ // Validate string constraint field types
635635+ if let Some(min_len) = def_value.get("minLength") {
636636+ if !min_len.is_number() {
637637+ errors.push(format!(
638638+ "String definition '{}' minLength must be a number",
639639+ def_name
640640+ ));
641641+ }
642642+ }
643643+ if let Some(max_len) = def_value.get("maxLength") {
644644+ if !max_len.is_number() {
645645+ errors.push(format!(
646646+ "String definition '{}' maxLength must be a number",
647647+ def_name
648648+ ));
649649+ }
650650+ }
651651+ if let Some(enum_val) = def_value.get("enum") {
652652+ if !enum_val.is_array() {
653653+ errors.push(format!(
654654+ "String definition '{}' enum field must be an array",
655655+ def_name
656656+ ));
657657+ }
658658+ }
659659+ if let Some(format) = def_value.get("format") {
660660+ if !format.is_string() {
661661+ errors.push(format!(
662662+ "String definition '{}' format field must be a string",
663663+ def_name
664664+ ));
665665+ } else if StringFormat::from_str(format.as_str().unwrap()).is_none() {
666666+ errors.push(format!(
667667+ "String definition '{}' has unknown format '{}'",
668668+ def_name, format.as_str().unwrap()
669669+ ));
670670+ }
671671+ }
672672+ },
673673+ "integer" => {
674674+ // Validate integer constraint field types
675675+ if let Some(min) = def_value.get("minimum") {
676676+ if !min.is_number() {
677677+ errors.push(format!(
678678+ "Integer definition '{}' minimum must be a number",
679679+ def_name
680680+ ));
681681+ }
682682+ }
683683+ if let Some(max) = def_value.get("maximum") {
684684+ if !max.is_number() {
685685+ errors.push(format!(
686686+ "Integer definition '{}' maximum must be a number",
687687+ def_name
688688+ ));
689689+ }
690690+ }
691691+ if let Some(enum_val) = def_value.get("enum") {
692692+ if !enum_val.is_array() {
693693+ errors.push(format!(
694694+ "Integer definition '{}' enum field must be an array",
695695+ def_name
696696+ ));
697697+ }
698698+ }
699699+ },
700700+ "bytes" => {
701701+ // Validate bytes constraint field types
702702+ if let Some(min_len) = def_value.get("minLength") {
703703+ if !min_len.is_number() {
704704+ errors.push(format!(
705705+ "Bytes definition '{}' minLength must be a number",
706706+ def_name
707707+ ));
708708+ }
709709+ }
710710+ if let Some(max_len) = def_value.get("maxLength") {
711711+ if !max_len.is_number() {
712712+ errors.push(format!(
713713+ "Bytes definition '{}' maxLength must be a number",
714714+ def_name
715715+ ));
716716+ }
717717+ }
718718+ },
719719+ // token, null, unknown, cid-link, boolean don't require special structural validation
720720+ _ => {}
721721+ }
722722+723723+ if errors.is_empty() {
724724+ Ok(())
725725+ } else {
726726+ Err(ValidationError::MultipleErrors { errors })
727727+ }
728728+ }
729729+730730+ /// Static version of property definition validation
731731+ fn validate_property_definition_static(
732732+ parent_name: &str,
733733+ prop_name: &str,
734734+ prop_def: &Value,
735735+ errors: &mut Vec<String>,
736736+ ) {
737737+ if let Some(prop_type) = prop_def.get("type").and_then(|v| v.as_str()) {
738738+ // Check if the property type is valid and validate allowed fields for each type
739739+ match prop_type {
740740+ "string" => {
741741+ let allowed_fields = [
742742+ "type", "description", "default", "const", "enum", "format",
743743+ "minLength", "maxLength", "minGraphemes", "maxGraphemes", "knownValues"
744744+ ];
745745+ Self::validate_allowed_fields(
746746+ &format!("{}.{}", parent_name, prop_name),
747747+ prop_def,
748748+ &allowed_fields,
749749+ "String property",
750750+ errors
751751+ );
752752+753753+ // Validate default value against constraints
754754+ if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_str()) {
755755+ Self::validate_string_default(parent_name, prop_name, default_val, prop_def, errors);
756756+ }
757757+ }
758758+ "integer" => {
759759+ let allowed_fields = [
760760+ "type", "description", "default", "const", "enum",
761761+ "minimum", "maximum"
762762+ ];
763763+ Self::validate_allowed_fields(
764764+ &format!("{}.{}", parent_name, prop_name),
765765+ prop_def,
766766+ &allowed_fields,
767767+ "Integer property",
768768+ errors
769769+ );
770770+771771+ // Validate default value against constraints
772772+ if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_i64()) {
773773+ Self::validate_integer_default(parent_name, prop_name, default_val, prop_def, errors);
774774+ }
775775+ }
776776+ "boolean" => {
777777+ let allowed_fields = ["type", "description", "default", "const"];
778778+ Self::validate_allowed_fields(
779779+ &format!("{}.{}", parent_name, prop_name),
780780+ prop_def,
781781+ &allowed_fields,
782782+ "Boolean property",
783783+ errors
784784+ );
785785+786786+ // Validate default value against constraints
787787+ if let Some(default_val) = prop_def.get("default").and_then(|v| v.as_bool()) {
788788+ Self::validate_boolean_default(parent_name, prop_name, default_val, prop_def, errors);
789789+ }
790790+ }
791791+ "array" => {
792792+ let allowed_fields = [
793793+ "type", "description", "items", "minLength", "maxLength"
794794+ ];
795795+ Self::validate_allowed_fields(
796796+ &format!("{}.{}", parent_name, prop_name),
797797+ prop_def,
798798+ &allowed_fields,
799799+ "Array property",
800800+ errors
801801+ );
802802+803803+ // Validate items if present
804804+ if let Some(items) = prop_def.get("items") {
805805+ let items_path = format!("{}.items", prop_name);
806806+ Self::validate_property_definition_static(parent_name, &items_path, items, errors);
807807+ }
808808+ }
809809+ "object" => {
810810+ let allowed_fields = [
811811+ "type", "description", "properties", "required", "nullable"
812812+ ];
813813+ Self::validate_allowed_fields(
814814+ &format!("{}.{}", parent_name, prop_name),
815815+ prop_def,
816816+ &allowed_fields,
817817+ "Object property",
818818+ errors
819819+ );
820820+821821+ // Recursively validate nested properties
822822+ if let Some(nested_props) = prop_def.get("properties") {
823823+ if !nested_props.is_object() {
824824+ errors.push(format!(
825825+ "Property '{}.{}' properties must be an object",
826826+ parent_name, prop_name
827827+ ));
828828+ } else {
829829+ for (nested_name, nested_def) in nested_props.as_object().unwrap() {
830830+ let nested_path = format!("{}.{}", prop_name, nested_name);
831831+ Self::validate_property_definition_static(parent_name, &nested_path, nested_def, errors);
832832+ }
833833+ }
834834+ }
835835+ }
836836+ "ref" => {
837837+ let allowed_fields = ["type", "description", "ref"];
838838+ Self::validate_allowed_fields(
839839+ &format!("{}.{}", parent_name, prop_name),
840840+ prop_def,
841841+ &allowed_fields,
842842+ "Ref property",
843843+ errors
844844+ );
845845+ }
846846+ "union" => {
847847+ let allowed_fields = ["type", "description", "refs", "closed"];
848848+ Self::validate_allowed_fields(
849849+ &format!("{}.{}", parent_name, prop_name),
850850+ prop_def,
851851+ &allowed_fields,
852852+ "Union property",
853853+ errors
854854+ );
855855+ }
856856+ "blob" => {
857857+ let allowed_fields = ["type", "description", "accept", "maxSize"];
858858+ Self::validate_allowed_fields(
859859+ &format!("{}.{}", parent_name, prop_name),
860860+ prop_def,
861861+ &allowed_fields,
862862+ "Blob property",
863863+ errors
864864+ );
865865+ }
866866+ "bytes" => {
867867+ let allowed_fields = ["type", "description", "minLength", "maxLength"];
868868+ Self::validate_allowed_fields(
869869+ &format!("{}.{}", parent_name, prop_name),
870870+ prop_def,
871871+ &allowed_fields,
872872+ "Bytes property",
873873+ errors
874874+ );
875875+ }
876876+ "record" | "cid-link" | "unknown" | "token" | "null" => {
877877+ let allowed_fields = ["type", "description"];
878878+ Self::validate_allowed_fields(
879879+ &format!("{}.{}", parent_name, prop_name),
880880+ prop_def,
881881+ &allowed_fields,
882882+ &format!("{} property", prop_type),
883883+ errors
884884+ );
885885+ }
886886+ _ => {
887887+ errors.push(format!(
888888+ "Property '{}.{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null",
889889+ parent_name, prop_name, prop_type
890890+ ));
891891+ }
892892+ }
893893+ } else {
894894+ errors.push(format!(
895895+ "Property '{}.{}' missing required 'type' field",
896896+ parent_name, prop_name
897897+ ));
898898+ }
899899+ }
900900+188901 /// Validate an object definition structure
189902 fn validate_object_definition(
190903 &self,
···199912 def_name
200913 )));
201914 }
915915+916916+ // Validate each property's type
917917+ for (prop_name, prop_def) in properties.as_object().unwrap() {
918918+ self.validate_property_definition(def_name, prop_name, prop_def)?;
919919+ }
202920 }
203921204922 // Validate required field if it exists
···208926 "Object definition '{}' required field must be an array",
209927 def_name
210928 )));
929929+ }
930930+ }
931931+932932+ Ok(())
933933+ }
934934+935935+ /// Validate a type definition (for basic types)
936936+ fn validate_type_definition(
937937+ &self,
938938+ def_name: &str,
939939+ def_value: &Value,
940940+ type_name: &str,
941941+ ) -> Result<(), ValidationError> {
942942+ // For basic types, validate any nested properties if they exist
943943+ if let Some(properties) = def_value.get("properties") {
944944+ if !properties.is_object() {
945945+ return Err(ValidationError::InvalidSchema(format!(
946946+ "{} definition '{}' properties must be an object",
947947+ type_name, def_name
948948+ )));
949949+ }
950950+951951+ // Validate each property's type
952952+ for (prop_name, prop_def) in properties.as_object().unwrap() {
953953+ self.validate_property_definition(def_name, prop_name, prop_def)?;
954954+ }
955955+ }
956956+957957+ Ok(())
958958+ }
959959+960960+ /// Validate a property definition's type
961961+ fn validate_property_definition(
962962+ &self,
963963+ parent_name: &str,
964964+ prop_name: &str,
965965+ prop_def: &Value,
966966+ ) -> Result<(), ValidationError> {
967967+ if let Some(prop_type) = prop_def.get("type").and_then(|v| v.as_str()) {
968968+ // Check if the property type is valid
969969+ match prop_type {
970970+ "record" | "object" | "string" | "integer" | "boolean" | "array" | "union"
971971+ | "ref" | "blob" | "bytes" | "cid-link" | "unknown" | "token" | "null" => {
972972+ // Valid type, recursively validate if it has properties
973973+ if prop_type == "object" {
974974+ if let Some(nested_props) = prop_def.get("properties") {
975975+ if !nested_props.is_object() {
976976+ return Err(ValidationError::InvalidSchema(format!(
977977+ "Property '{}.{}' properties must be an object",
978978+ parent_name, prop_name
979979+ )));
980980+ }
981981+982982+ for (nested_name, nested_def) in nested_props.as_object().unwrap() {
983983+ let nested_path = format!("{}.{}", prop_name, nested_name);
984984+ self.validate_property_definition(parent_name, &nested_path, nested_def)?;
985985+ }
986986+ }
987987+ }
988988+ }
989989+ _ => {
990990+ return Err(ValidationError::InvalidSchema(format!(
991991+ "Property '{}.{}' has unknown type '{}'. Valid types are: record, object, string, integer, boolean, array, union, ref, blob, bytes, cid-link, unknown, token, null",
992992+ parent_name, prop_name, prop_type
993993+ )));
994994+ }
211995 }
212996 }
213997···4391223 actual: format!("{:?}", value),
4401224 })?;
4411225442442- // Check min/max length (in UTF-8 bytes)
443443- if let Some(min_length) = schema["minLength"].as_u64() {
444444- if (string_val.len() as u64) < min_length {
445445- return Err(ValidationError::StringValidationFailed {
446446- path: ctx.path_string(),
447447- message: format!(
448448- "String length {} is less than minimum {}",
449449- string_val.len(),
450450- min_length
451451- ),
452452- });
453453- }
454454- }
12261226+ // Parse constraints from schema
12271227+ let constraints = StringConstraints::from_json(schema);
4551228456456- if let Some(max_length) = schema["maxLength"].as_u64() {
457457- if (string_val.len() as u64) > max_length {
458458- return Err(ValidationError::StringValidationFailed {
459459- path: ctx.path_string(),
460460- message: format!(
461461- "String length {} exceeds maximum {}",
462462- string_val.len(),
463463- max_length
464464- ),
465465- });
466466- }
467467- }
468468-469469- // Check min/max graphemes (simplified grapheme counting)
470470- if let Some(min_graphemes) = schema["minGraphemes"].as_u64() {
471471- let grapheme_count = self.count_graphemes(string_val);
472472- if (grapheme_count as u64) < min_graphemes {
473473- return Err(ValidationError::StringValidationFailed {
474474- path: ctx.path_string(),
475475- message: format!(
476476- "String has {} graphemes, less than minimum {}",
477477- grapheme_count, min_graphemes
478478- ),
479479- });
480480- }
481481- }
482482-483483- if let Some(max_graphemes) = schema["maxGraphemes"].as_u64() {
484484- let grapheme_count = self.count_graphemes(string_val);
485485- if (grapheme_count as u64) > max_graphemes {
486486- return Err(ValidationError::StringValidationFailed {
487487- path: ctx.path_string(),
488488- message: format!(
489489- "String has {} graphemes, exceeds maximum {}",
490490- grapheme_count, max_graphemes
491491- ),
492492- });
493493- }
494494- }
495495-496496- // Check const value
497497- if let Some(const_val) = schema["const"].as_str() {
498498- if string_val != const_val {
499499- return Err(ValidationError::TypeMismatch {
500500- path: ctx.path_string(),
501501- expected: format!("\"{}\"", const_val),
502502- actual: format!("\"{}\"", string_val),
503503- });
504504- }
505505- }
506506-507507- // Check enum values
508508- if let Some(enum_values) = schema["enum"].as_array() {
509509- let valid = enum_values.iter().any(|v| v.as_str() == Some(string_val));
510510- if !valid {
511511- return Err(ValidationError::EnumValidationFailed {
512512- path: ctx.path_string(),
513513- });
514514- }
515515- }
516516-517517- // Check format
518518- if let Some(format_str) = schema["format"].as_str() {
519519- if let Some(format) = StringFormat::from_str(format_str) {
520520- self.validate_string_format(string_val, format, ctx)?
521521- }
522522- }
12291229+ // Validate all string constraints
12301230+ self.validate_string_constraints(string_val, &constraints, ctx)?;
52312315241232 Ok(())
5251233 }
···6941402 actual: format!("{:?}", value),
6951403 })?;
6961404697697- if let Some(minimum) = schema["minimum"].as_i64() {
698698- if int_val < minimum {
699699- return Err(ValidationError::IntegerValidationFailed {
700700- path: ctx.path_string(),
701701- message: format!("Value {} is less than minimum {}", int_val, minimum),
702702- });
703703- }
704704- }
705705-706706- if let Some(maximum) = schema["maximum"].as_i64() {
707707- if int_val > maximum {
708708- return Err(ValidationError::IntegerValidationFailed {
709709- path: ctx.path_string(),
710710- message: format!("Value {} exceeds maximum {}", int_val, maximum),
711711- });
712712- }
713713- }
14051405+ // Parse constraints from schema
14061406+ let constraints = IntegerConstraints::from_json(schema);
7141407715715- // Check const value
716716- if let Some(const_val) = schema["const"].as_i64() {
717717- if int_val != const_val {
718718- return Err(ValidationError::TypeMismatch {
719719- path: ctx.path_string(),
720720- expected: format!("{}", const_val),
721721- actual: format!("{}", int_val),
722722- });
723723- }
724724- }
725725-726726- if let Some(enum_values) = schema["enum"].as_array() {
727727- let valid = enum_values.iter().any(|v| v.as_i64() == Some(int_val));
728728- if !valid {
729729- return Err(ValidationError::EnumValidationFailed {
730730- path: ctx.path_string(),
731731- });
732732- }
733733- }
14081408+ // Validate all integer constraints
14091409+ self.validate_integer_constraints(int_val, &constraints, ctx)?;
73414107351411 Ok(())
7361412 }
···7421418 schema: &Value,
7431419 ctx: &ValidationContext,
7441420 ) -> Result<(), ValidationError> {
745745- value
14211421+ let bool_val = value
7461422 .as_bool()
7471423 .ok_or_else(|| ValidationError::TypeMismatch {
7481424 path: ctx.path_string(),
···7501426 actual: format!("{:?}", value),
7511427 })?;
7521428753753- // Check const value if specified
754754- if let Some(const_val) = schema["const"].as_bool() {
755755- if value.as_bool() != Some(const_val) {
756756- return Err(ValidationError::TypeMismatch {
757757- path: ctx.path_string(),
758758- expected: format!("{}", const_val),
759759- actual: format!("{:?}", value),
760760- });
761761- }
762762- }
14291429+ // Parse constraints from schema
14301430+ let constraints = BooleanConstraints::from_json(schema);
14311431+14321432+ // Validate all boolean constraints
14331433+ self.validate_boolean_constraints(bool_val, &constraints, ctx)?;
76314347641435 Ok(())
7651436 }
···7791450 actual: format!("{:?}", value),
7801451 })?;
7811452782782- // Check min/max length
783783- if let Some(min_length) = schema["minLength"].as_u64() {
784784- if (array.len() as u64) < min_length {
785785- return Err(ValidationError::ArrayValidationFailed {
786786- path: ctx.path_string(),
787787- message: format!(
788788- "Array length {} is less than minimum {}",
789789- array.len(),
790790- min_length
791791- ),
792792- });
793793- }
794794- }
14531453+ // Parse constraints from schema
14541454+ let constraints = ArrayConstraints::from_json(schema);
7951455796796- if let Some(max_length) = schema["maxLength"].as_u64() {
797797- if (array.len() as u64) > max_length {
798798- return Err(ValidationError::ArrayValidationFailed {
799799- path: ctx.path_string(),
800800- message: format!(
801801- "Array length {} exceeds maximum {}",
802802- array.len(),
803803- max_length
804804- ),
805805- });
806806- }
807807- }
14561456+ // Validate array constraints
14571457+ self.validate_array_constraints(array, &constraints, ctx)?;
80814588091459 // Validate items
8101460 if let Some(items_schema) = schema.get("items") {
···9281578 fn validate_blob(
9291579 &self,
9301580 value: &Value,
931931- _schema: &Value,
15811581+ schema: &Value,
9321582 ctx: &ValidationContext,
9331583 ) -> Result<(), ValidationError> {
9341584 let obj = value
···9661616 }
96716179681618 // Check other required fields
969969- if !obj.contains_key("mimeType") {
970970- return Err(ValidationError::RequiredFieldMissing {
16191619+ let mime_type = obj.get("mimeType").and_then(|v| v.as_str()).ok_or_else(|| {
16201620+ ValidationError::RequiredFieldMissing {
9711621 path: ctx.with_field("mimeType").path_string(),
972972- });
973973- }
16221622+ }
16231623+ })?;
9741624975975- if !obj.contains_key("size") {
976976- return Err(ValidationError::RequiredFieldMissing {
16251625+ let size = obj.get("size").and_then(|v| v.as_u64()).ok_or_else(|| {
16261626+ ValidationError::RequiredFieldMissing {
9771627 path: ctx.with_field("size").path_string(),
978978- });
979979- }
16281628+ }
16291629+ })?;
16301630+16311631+ // Parse constraints from schema
16321632+ let constraints = BlobConstraints::from_json(schema);
16331633+16341634+ // Validate blob constraints
16351635+ self.validate_blob_constraints(mime_type, size, &constraints, ctx)?;
98016369811637 Ok(())
9821638 }
···10091665 });
10101666 }
1011166710121012- // Check length constraints (bytes length, not string length)
16681668+ // Calculate decoded byte length
10131669 let decoded_len =
10141670 (string_val.len() * 3 / 4) - string_val.chars().filter(|&c| c == '=').count();
1015167110161016- if let Some(min_length) = schema["minLength"].as_u64() {
10171017- if (decoded_len as u64) < min_length {
10181018- return Err(ValidationError::StringValidationFailed {
10191019- path: ctx.path_string(),
10201020- message: format!(
10211021- "Bytes length {} is less than minimum {}",
10221022- decoded_len, min_length
10231023- ),
10241024- });
10251025- }
10261026- }
16721672+ // Parse constraints from schema
16731673+ let constraints = BytesConstraints::from_json(schema);
1027167410281028- if let Some(max_length) = schema["maxLength"].as_u64() {
10291029- if (decoded_len as u64) > max_length {
10301030- return Err(ValidationError::StringValidationFailed {
10311031- path: ctx.path_string(),
10321032- message: format!(
10331033- "Bytes length {} exceeds maximum {}",
10341034- decoded_len, max_length
10351035- ),
10361036- });
10371037- }
10381038- }
16751675+ // Validate bytes constraints
16761676+ self.validate_bytes_constraints(decoded_len, &constraints, ctx)?;
1039167710401678 Ok(())
10411679 }
···10941732 Ok(())
10951733 }
1096173410971097- /// Simplified grapheme counting (approximation)
10981098- /// This is a basic implementation - for full compliance with TS version,
10991099- /// would need proper Unicode grapheme cluster segmentation
17351735+ /// Count grapheme clusters properly using unicode-segmentation
11001736 fn count_graphemes(&self, s: &str) -> usize {
11011101- // Very simplified approach: count Unicode scalar values, with basic combining character handling
11021102- let mut count = 0;
11031103- let mut chars = s.chars().peekable();
17371737+ s.graphemes(true).count()
17381738+ }
17391739+17401740+ /// Validate string constraints (length, graphemes, enum, const, default, format)
17411741+ fn validate_string_constraints(
17421742+ &self,
17431743+ value: &str,
17441744+ constraints: &StringConstraints,
17451745+ ctx: &ValidationContext,
17461746+ ) -> Result<(), ValidationError> {
17471747+ // Check const constraint
17481748+ if let Some(const_value) = &constraints.const_value {
17491749+ if value != const_value {
17501750+ return Err(ValidationError::StringValidationFailed {
17511751+ path: ctx.path_string(),
17521752+ message: format!("Value '{}' does not match const value '{}'", value, const_value),
17531753+ });
17541754+ }
17551755+ }
17561756+17571757+ // Check enum constraint
17581758+ if let Some(enum_values) = &constraints.enum_values {
17591759+ if !enum_values.contains(&value.to_string()) {
17601760+ return Err(ValidationError::StringValidationFailed {
17611761+ path: ctx.path_string(),
17621762+ message: format!("Value '{}' must be one of ({})", value, enum_values.join("|")),
17631763+ });
17641764+ }
17651765+ }
17661766+17671767+ // Check length constraints (UTF-8 byte length)
17681768+ let byte_len = value.len() as u32;
17691769+ if let Some(min_len) = constraints.min_length {
17701770+ if byte_len < min_len {
17711771+ return Err(ValidationError::StringValidationFailed {
17721772+ path: ctx.path_string(),
17731773+ message: format!("String length {} is less than minimum {}", byte_len, min_len),
17741774+ });
17751775+ }
17761776+ }
17771777+ if let Some(max_len) = constraints.max_length {
17781778+ if byte_len > max_len {
17791779+ return Err(ValidationError::StringValidationFailed {
17801780+ path: ctx.path_string(),
17811781+ message: format!("String length {} exceeds maximum {}", byte_len, max_len),
17821782+ });
17831783+ }
17841784+ }
17851785+17861786+ // Check grapheme constraints (optimized like AT Protocol)
17871787+ if constraints.min_graphemes.is_some() || constraints.max_graphemes.is_some() {
17881788+ let char_len = value.chars().count() as u32;
17891789+17901790+ if let Some(max_graphemes) = constraints.max_graphemes {
17911791+ // If char count is within limit, skip expensive grapheme calculation
17921792+ if char_len > max_graphemes {
17931793+ let grapheme_count = self.count_graphemes(value) as u32;
17941794+ if grapheme_count > max_graphemes {
17951795+ return Err(ValidationError::StringValidationFailed {
17961796+ path: ctx.path_string(),
17971797+ message: format!("String grapheme count {} exceeds maximum {}", grapheme_count, max_graphemes),
17981798+ });
17991799+ }
18001800+ }
18011801+ }
18021802+18031803+ if let Some(min_graphemes) = constraints.min_graphemes {
18041804+ // For minimum, we need to count accurately
18051805+ let grapheme_count = self.count_graphemes(value) as u32;
18061806+ if grapheme_count < min_graphemes {
18071807+ return Err(ValidationError::StringValidationFailed {
18081808+ path: ctx.path_string(),
18091809+ message: format!("String grapheme count {} is less than minimum {}", grapheme_count, min_graphemes),
18101810+ });
18111811+ }
18121812+ }
18131813+ }
18141814+18151815+ // Check format constraint
18161816+ if let Some(format) = &constraints.format {
18171817+ self.validate_string_format(value, format.clone(), ctx)?;
18181818+ }
18191819+18201820+ Ok(())
18211821+ }
18221822+18231823+ /// Validate integer constraints (minimum, maximum, enum, const, default)
18241824+ fn validate_integer_constraints(
18251825+ &self,
18261826+ value: i64,
18271827+ constraints: &IntegerConstraints,
18281828+ ctx: &ValidationContext,
18291829+ ) -> Result<(), ValidationError> {
18301830+ // Check const constraint
18311831+ if let Some(const_value) = constraints.const_value {
18321832+ if value != const_value {
18331833+ return Err(ValidationError::IntegerValidationFailed {
18341834+ path: ctx.path_string(),
18351835+ message: format!("Value {} does not match const value {}", value, const_value),
18361836+ });
18371837+ }
18381838+ }
18391839+18401840+ // Check enum constraint
18411841+ if let Some(enum_values) = &constraints.enum_values {
18421842+ if !enum_values.contains(&value) {
18431843+ let enum_str: Vec<String> = enum_values.iter().map(|v| v.to_string()).collect();
18441844+ return Err(ValidationError::IntegerValidationFailed {
18451845+ path: ctx.path_string(),
18461846+ message: format!("Value {} must be one of ({})", value, enum_str.join("|")),
18471847+ });
18481848+ }
18491849+ }
18501850+18511851+ // Check range constraints
18521852+ if let Some(minimum) = constraints.minimum {
18531853+ if value < minimum {
18541854+ return Err(ValidationError::IntegerValidationFailed {
18551855+ path: ctx.path_string(),
18561856+ message: format!("Value {} is less than minimum {}", value, minimum),
18571857+ });
18581858+ }
18591859+ }
18601860+ if let Some(maximum) = constraints.maximum {
18611861+ if value > maximum {
18621862+ return Err(ValidationError::IntegerValidationFailed {
18631863+ path: ctx.path_string(),
18641864+ message: format!("Value {} exceeds maximum {}", value, maximum),
18651865+ });
18661866+ }
18671867+ }
18681868+18691869+ Ok(())
18701870+ }
18711871+18721872+ /// Validate array constraints (minLength, maxLength)
18731873+ fn validate_array_constraints(
18741874+ &self,
18751875+ array: &[Value],
18761876+ constraints: &ArrayConstraints,
18771877+ ctx: &ValidationContext,
18781878+ ) -> Result<(), ValidationError> {
18791879+ let len = array.len() as u32;
18801880+18811881+ if let Some(min_len) = constraints.min_length {
18821882+ if len < min_len {
18831883+ return Err(ValidationError::ArrayValidationFailed {
18841884+ path: ctx.path_string(),
18851885+ message: format!("Array length {} is less than minimum {}", len, min_len),
18861886+ });
18871887+ }
18881888+ }
18891889+ if let Some(max_len) = constraints.max_length {
18901890+ if len > max_len {
18911891+ return Err(ValidationError::ArrayValidationFailed {
18921892+ path: ctx.path_string(),
18931893+ message: format!("Array length {} exceeds maximum {}", len, max_len),
18941894+ });
18951895+ }
18961896+ }
18971897+18981898+ Ok(())
18991899+ }
19001900+19011901+ /// Validate boolean constraints (const, default)
19021902+ fn validate_boolean_constraints(
19031903+ &self,
19041904+ value: bool,
19051905+ constraints: &BooleanConstraints,
19061906+ ctx: &ValidationContext,
19071907+ ) -> Result<(), ValidationError> {
19081908+ // Check const constraint
19091909+ if let Some(const_value) = constraints.const_value {
19101910+ if value != const_value {
19111911+ return Err(ValidationError::TypeMismatch {
19121912+ path: ctx.path_string(),
19131913+ expected: format!("boolean const value {}", const_value),
19141914+ actual: value.to_string(),
19151915+ });
19161916+ }
19171917+ }
19181918+19191919+ Ok(())
19201920+ }
19211921+19221922+ /// Validate bytes constraints (minLength, maxLength for decoded bytes)
19231923+ fn validate_bytes_constraints(
19241924+ &self,
19251925+ decoded_len: usize,
19261926+ constraints: &BytesConstraints,
19271927+ ctx: &ValidationContext,
19281928+ ) -> Result<(), ValidationError> {
19291929+ let len = decoded_len as u32;
19301930+19311931+ if let Some(min_len) = constraints.min_length {
19321932+ if len < min_len {
19331933+ return Err(ValidationError::StringValidationFailed {
19341934+ path: ctx.path_string(),
19351935+ message: format!("Bytes length {} is less than minimum {}", len, min_len),
19361936+ });
19371937+ }
19381938+ }
19391939+ if let Some(max_len) = constraints.max_length {
19401940+ if len > max_len {
19411941+ return Err(ValidationError::StringValidationFailed {
19421942+ path: ctx.path_string(),
19431943+ message: format!("Bytes length {} exceeds maximum {}", len, max_len),
19441944+ });
19451945+ }
19461946+ }
19471947+19481948+ Ok(())
19491949+ }
19501950+19511951+ /// Validate blob constraints (accept mime types, maxSize)
19521952+ fn validate_blob_constraints(
19531953+ &self,
19541954+ mime_type: &str,
19551955+ size: u64,
19561956+ constraints: &BlobConstraints,
19571957+ ctx: &ValidationContext,
19581958+ ) -> Result<(), ValidationError> {
19591959+ // Check accepted mime types
19601960+ if let Some(accept_types) = &constraints.accept {
19611961+ if !accept_types.contains(&mime_type.to_string()) {
19621962+ return Err(ValidationError::StringValidationFailed {
19631963+ path: ctx.path_string(),
19641964+ message: format!("MIME type '{}' not in accepted types: {}", mime_type, accept_types.join(", ")),
19651965+ });
19661966+ }
19671967+ }
19681968+19691969+ // Check max size
19701970+ if let Some(max_size) = constraints.max_size {
19711971+ if size > max_size {
19721972+ return Err(ValidationError::StringValidationFailed {
19731973+ path: ctx.path_string(),
19741974+ message: format!("Blob size {} exceeds maximum {}", size, max_size),
19751975+ });
19761976+ }
19771977+ }
19781978+19791979+ Ok(())
19801980+ }
19811981+19821982+ /// Validate that a string default value satisfies the field's constraints
19831983+ fn validate_string_default(
19841984+ parent_name: &str,
19851985+ prop_name: &str,
19861986+ default_val: &str,
19871987+ prop_def: &Value,
19881988+ errors: &mut Vec<String>,
19891989+ ) {
19901990+ let field_path = format!("{}.{}", parent_name, prop_name);
19911991+19921992+ // Check const constraint
19931993+ if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_str()) {
19941994+ if default_val != const_val {
19951995+ errors.push(format!(
19961996+ "String property '{}' default value '{}' does not match const value '{}'",
19971997+ field_path, default_val, const_val
19981998+ ));
19991999+ }
20002000+ }
20012001+20022002+ // Check enum constraint
20032003+ if let Some(enum_array) = prop_def.get("enum").and_then(|v| v.as_array()) {
20042004+ let enum_values: Vec<String> = enum_array
20052005+ .iter()
20062006+ .filter_map(|v| v.as_str())
20072007+ .map(|s| s.to_string())
20082008+ .collect();
20092009+ if !enum_values.contains(&default_val.to_string()) {
20102010+ errors.push(format!(
20112011+ "String property '{}' default value '{}' must be one of ({})",
20122012+ field_path, default_val, enum_values.join("|")
20132013+ ));
20142014+ }
20152015+ }
1104201611051105- while let Some(_ch) = chars.next() {
11061106- count += 1;
20172017+ // Check length constraints
20182018+ let byte_len = default_val.len() as u32;
20192019+ if let Some(min_len) = prop_def.get("minLength").and_then(|v| v.as_u64()) {
20202020+ if byte_len < min_len as u32 {
20212021+ errors.push(format!(
20222022+ "String property '{}' default value '{}' length {} is less than minimum {}",
20232023+ field_path, default_val, byte_len, min_len
20242024+ ));
20252025+ }
20262026+ }
20272027+ if let Some(max_len) = prop_def.get("maxLength").and_then(|v| v.as_u64()) {
20282028+ if byte_len > max_len as u32 {
20292029+ errors.push(format!(
20302030+ "String property '{}' default value '{}' length {} exceeds maximum {}",
20312031+ field_path, default_val, byte_len, max_len
20322032+ ));
20332033+ }
20342034+ }
1107203511081108- // Skip combining characters that follow this base character
11091109- while let Some(&next_ch) = chars.peek() {
11101110- if self.is_combining_character(next_ch) {
11111111- chars.next(); // consume the combining character
11121112- } else {
11131113- break;
20362036+ // Check grapheme constraints
20372037+ if prop_def.get("minGraphemes").is_some() || prop_def.get("maxGraphemes").is_some() {
20382038+ let char_len = default_val.chars().count() as u32;
20392039+ if let Some(min_graphemes) = prop_def.get("minGraphemes").and_then(|v| v.as_u64()) {
20402040+ if char_len < min_graphemes as u32 {
20412041+ errors.push(format!(
20422042+ "String property '{}' default value '{}' grapheme length {} is less than minimum {}",
20432043+ field_path, default_val, char_len, min_graphemes
20442044+ ));
11142045 }
11152046 }
20472047+ if let Some(max_graphemes) = prop_def.get("maxGraphemes").and_then(|v| v.as_u64()) {
20482048+ if char_len > max_graphemes as u32 {
20492049+ errors.push(format!(
20502050+ "String property '{}' default value '{}' grapheme length {} exceeds maximum {}",
20512051+ field_path, default_val, char_len, max_graphemes
20522052+ ));
20532053+ }
20542054+ }
20552055+ }
20562056+ }
20572057+20582058+ /// Validate that an integer default value satisfies the field's constraints
20592059+ fn validate_integer_default(
20602060+ parent_name: &str,
20612061+ prop_name: &str,
20622062+ default_val: i64,
20632063+ prop_def: &Value,
20642064+ errors: &mut Vec<String>,
20652065+ ) {
20662066+ let field_path = format!("{}.{}", parent_name, prop_name);
20672067+20682068+ // Check const constraint
20692069+ if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_i64()) {
20702070+ if default_val != const_val {
20712071+ errors.push(format!(
20722072+ "Integer property '{}' default value {} does not match const value {}",
20732073+ field_path, default_val, const_val
20742074+ ));
20752075+ }
11162076 }
1117207711181118- count
20782078+ // Check enum constraint
20792079+ if let Some(enum_array) = prop_def.get("enum").and_then(|v| v.as_array()) {
20802080+ let enum_values: Vec<i64> = enum_array
20812081+ .iter()
20822082+ .filter_map(|v| v.as_i64())
20832083+ .collect();
20842084+ if !enum_values.contains(&default_val) {
20852085+ errors.push(format!(
20862086+ "Integer property '{}' default value {} must be one of ({})",
20872087+ field_path, default_val,
20882088+ enum_values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join("|")
20892089+ ));
20902090+ }
20912091+ }
20922092+20932093+ // Check minimum constraint
20942094+ if let Some(minimum) = prop_def.get("minimum").and_then(|v| v.as_i64()) {
20952095+ if default_val < minimum {
20962096+ errors.push(format!(
20972097+ "Integer property '{}' default value {} is less than minimum {}",
20982098+ field_path, default_val, minimum
20992099+ ));
21002100+ }
21012101+ }
21022102+21032103+ // Check maximum constraint
21042104+ if let Some(maximum) = prop_def.get("maximum").and_then(|v| v.as_i64()) {
21052105+ if default_val > maximum {
21062106+ errors.push(format!(
21072107+ "Integer property '{}' default value {} exceeds maximum {}",
21082108+ field_path, default_val, maximum
21092109+ ));
21102110+ }
21112111+ }
11192112 }
1120211311211121- /// Check if a character is a combining character (very simplified)
11221122- fn is_combining_character(&self, ch: char) -> bool {
11231123- // This is a simplified check for common combining marks
11241124- // In a full implementation, would need proper Unicode category checking
11251125- matches!(ch as u32,
11261126- 0x0300..=0x036F | // Combining Diacritical Marks
11271127- 0x1AB0..=0x1AFF | // Combining Diacritical Marks Extended
11281128- 0x1DC0..=0x1DFF | // Combining Diacritical Marks Supplement
11291129- 0x20D0..=0x20FF | // Combining Diacritical Marks for Symbols
11301130- 0xFE20..=0xFE2F // Combining Half Marks
11311131- )
21142114+ /// Validate that a boolean default value satisfies the field's constraints
21152115+ fn validate_boolean_default(
21162116+ parent_name: &str,
21172117+ prop_name: &str,
21182118+ default_val: bool,
21192119+ prop_def: &Value,
21202120+ errors: &mut Vec<String>,
21212121+ ) {
21222122+ let field_path = format!("{}.{}", parent_name, prop_name);
21232123+21242124+ // Check const constraint
21252125+ if let Some(const_val) = prop_def.get("const").and_then(|v| v.as_bool()) {
21262126+ if default_val != const_val {
21272127+ errors.push(format!(
21282128+ "Boolean property '{}' default value {} does not match const value {}",
21292129+ field_path, default_val, const_val
21302130+ ));
21312131+ }
21322132+ }
11322133 }
11332134}