diff --git a/fixtures/merger.json b/fixtures/merger.json index be02081..f035ff0 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -719,6 +719,24 @@ { "name": "attachment", "schemas": [ + { + "$id": "type_metadata", + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + { + "$id": "other_metadata", + "type": "object", + "properties": { + "other": { + "type": "string" + } + } + }, { "$id": "attachment", "$ref": "entity", @@ -729,9 +747,11 @@ "type": "string" } }, - "metadata": { - "type": "object", - "additionalProperties": true + "type_metadata": { + "$ref": "type_metadata" + }, + "other_metadata": { + "$ref": "other_metadata" } } } @@ -744,7 +764,8 @@ "id", "type", "flags", - "metadata", + "type_metadata", + "other_metadata", "created_at", "created_by", "modified_at", @@ -756,7 +777,8 @@ "id", "type", "flags", - "metadata" + "type_metadata", + "other_metadata" ], "entity": [ "id", @@ -772,7 +794,8 @@ "id": "uuid", "type": "text", "flags": "_text", - "metadata": "jsonb", + "type_metadata": "jsonb", + "other_metadata": "jsonb", "created_at": "timestamptz", "created_by": "uuid", "modified_at": "timestamptz", @@ -2260,7 +2283,7 @@ } }, { - "description": "Insert attachment displaying side-by-side array literal and jsonb formatting translations", + "description": "Attachment with text[] and jsonb metadata structures", "action": "merge", "data": { "type": "attachment", @@ -2268,9 +2291,11 @@ "urgent", "reviewed" ], - "metadata": { - "size": 1024, - "source": "upload" + "other_metadata": { + "other": "hello" + }, + "type_metadata": { + "type": "type_metadata" } }, "expect": { @@ -2298,14 +2323,16 @@ "INSERT INTO agreego.\"attachment\" (", " \"flags\",", " \"id\",", - " \"metadata\",", - " \"type\"", + " \"other_metadata\",", + " \"type\",", + " \"type_metadata\"", ")", "VALUES (", " '{\"urgent\",\"reviewed\"}',", " '{{uuid:attachment_id}}',", - " '{\"size\":1024,\"source\":\"upload\"}',", - " 'attachment'", + " '{\"other\":\"hello\"}',", + " 'attachment',", + " '{\"type\":\"type_metadata\"}'", ")" ], [ @@ -2322,8 +2349,9 @@ " NULL,", " '{", " \"flags\":[\"urgent\",\"reviewed\"],", - " \"metadata\":{\"size\":1024,\"source\":\"upload\"},", - " \"type\":\"attachment\"", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", " }',", " '{{uuid:attachment_id}}',", " '{{uuid}}',", @@ -2339,15 +2367,17 @@ " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", " \"flags\":[\"urgent\",\"reviewed\"],", " \"id\":\"{{uuid:attachment_id}}\",", - " \"metadata\":{\"size\":1024,\"source\":\"upload\"},", " \"modified_at\":\"{{timestamp}}\",", " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", - " \"type\":\"attachment\"", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", " },", " \"new\":{", " \"flags\":[\"urgent\",\"reviewed\"],", - " \"metadata\":{\"size\":1024,\"source\":\"upload\"},", - " \"type\":\"attachment\"", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", " }", " }')" ] diff --git a/m1.json b/m1.json new file mode 100644 index 0000000..35438a9 --- /dev/null +++ b/m1.json @@ -0,0 +1,102 @@ +{ + "description": "Attachment with both type_metadata and other_metadata ad-hoc structures", + "action": "merge", + "data": { + "type": "attachment", + "flags": [ + "urgent", + "reviewed" + ], + "other_metadata": { + "other": "hello" + }, + "type_metadata": { + "type": "type_metadata" + } + }, + "expect": { + "success": true, + "sql": [ + [ + "INSERT INTO agreego.\"entity\" (", + " \"created_at\",", + " \"created_by\",", + " \"id\",", + " \"modified_at\",", + " \"modified_by\",", + " \"type\"", + ")", + "VALUES (", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " '{{uuid:attachment_id}}',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000',", + " 'attachment'", + ")" + ], + [ + "INSERT INTO agreego.\"attachment\" (", + " \"flags\",", + " \"id\",", + " \"other_metadata\",", + " \"type\",", + " \"type_metadata\"", + ")", + "VALUES (", + " '{\"urgent\",\"reviewed\"}',", + " '{{uuid:attachment_id}}',", + " '{\"other\":\"hello\"}',", + " 'attachment',", + " '{\"type\":\"type_metadata\"}'", + ")" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " entity_id,", + " id,", + " kind,", + " modified_at,", + " modified_by", + ")", + "VALUES (", + " NULL,", + " '{", + " \"flags\":[\"urgent\",\"reviewed\"],", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", + " }',", + " '{{uuid:attachment_id}}',", + " '{{uuid}}',", + " 'create',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "SELECT pg_notify('entity', '{", + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"flags\":[\"urgent\",\"reviewed\"],", + " \"id\":\"{{uuid:attachment_id}}\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", + " },", + " \"new\":{", + " \"flags\":[\"urgent\",\"reviewed\"],", + " \"other_metadata\":{\"other\":\"hello\"},", + " \"type\":\"attachment\",", + " \"type_metadata\":{\"type\":\"type_metadata\"}", + " }", + " }')" + ] + ] + } +} diff --git a/src/validator/rules/object.rs b/src/validator/rules/object.rs index 4975fe4..86d64d2 100644 --- a/src/validator/rules/object.rs +++ b/src/validator/rules/object.rs @@ -13,28 +13,40 @@ impl<'a> ValidationContext<'a> { ) -> Result { let current = self.instance; if let Some(obj) = current.as_object() { - // Entity Bound Implicit Type Validation - if let Some(lookup_key) = self.schema.id.as_ref().or(self.schema.r#ref.as_ref()) { - let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string(); - if let Some(type_def) = self.db.types.get(&base_type_name) - && let Some(type_val) = obj.get("type") + // Entity implicit type validation + // Use the specific schema id or ref as a fallback + if let Some(identifier) = self.schema.id.as_ref().or(self.schema.r#ref.as_ref()) { + // Kick in if the data object has a type field + if let Some(type_val) = obj.get("type") && let Some(type_str) = type_val.as_str() { - if type_def.variations.contains(type_str) { - // Ensure it passes strict mode - result.evaluated_keys.insert("type".to_string()); + // Get the string or the final segment as the base + let base = identifier.split('.').next_back().unwrap_or("").to_string(); + // Check if the base is a global type name + if let Some(type_def) = self.db.types.get(&base) { + // Ensure the instance type is a variation of the global type + if type_def.variations.contains(type_str) { + // Ensure it passes strict mode + result.evaluated_keys.insert("type".to_string()); + } else { + result.errors.push(ValidationError { + code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors + message: format!( + "Type '{}' is not a valid descendant for this entity bound schema", + type_str + ), + path: format!("{}/type", self.path), + }); + } } else { - result.errors.push(ValidationError { - code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors - message: format!( - "Type '{}' is not a valid descendant for this entity bound schema", - type_str - ), - path: format!("{}/type", self.path), - }); + // Ad-Hoc schemas natively use strict schema discriminator strings instead of variation inheritance + if type_str == identifier { + result.evaluated_keys.insert("type".to_string()); + } } } } + if let Some(min) = self.schema.min_properties && (obj.len() as f64) < min { @@ -44,6 +56,7 @@ impl<'a> ValidationContext<'a> { path: self.path.to_string(), }); } + if let Some(max) = self.schema.max_properties && (obj.len() as f64) > max { @@ -53,6 +66,7 @@ impl<'a> ValidationContext<'a> { path: self.path.to_string(), }); } + if let Some(ref req) = self.schema.required { for field in req { if !obj.contains_key(field) { @@ -114,10 +128,14 @@ impl<'a> ValidationContext<'a> { // Entity Bound Implicit Type Interception if key == "type" - && let Some(lookup_key) = sub_schema.id.as_ref().or(sub_schema.r#ref.as_ref()) + && let Some(schema_bound) = sub_schema.id.as_ref().or(sub_schema.r#ref.as_ref()) { - let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string(); - if let Some(type_def) = self.db.types.get(&base_type_name) + let physical_type_name = schema_bound + .split('.') + .next_back() + .unwrap_or("") + .to_string(); + if let Some(type_def) = self.db.types.get(&physical_type_name) && let Some(instance_type) = child_instance.as_str() && type_def.variations.contains(instance_type) {