From 5c72fad8c7048757cebde05d5360df5519369e25 Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Sat, 14 Mar 2026 23:05:15 +0000 Subject: [PATCH 1/9] refactor: split metamodel folder Signed-off-by: Ertugrul Karademir --- Cargo.lock | 9 + Cargo.toml | 19 +- concerto-core/Cargo.toml | 17 + {src => concerto-core/src}/error.rs | 0 {src => concerto-core/src}/introspect/mod.rs | 0 {src => concerto-core/src}/lib.rs | 16 +- .../src}/metamodel_validation.rs | 34 +- {src => concerto-core/src}/model_file.rs | 16 +- {src => concerto-core/src}/model_manager.rs | 52 +- {src => concerto-core/src}/property_type.rs | 0 {src => concerto-core/src}/traits.rs | 102 +- {src => concerto-core/src}/util.rs | 0 {src => concerto-core/src}/validation.rs | 0 .../tests}/conformance_tests.rs | 35 +- .../tests}/declaration_tests.rs | 2 +- {tests => concerto-core/tests}/enum_tests.rs | 29 +- {tests => concerto-core/tests}/map_tests.rs | 6 +- .../tests}/namespace_tests.rs | 4 +- .../tests}/scalar_tests.rs | 13 +- concerto-metamodel/Cargo.toml | 8 + concerto-metamodel/src/lib.rs | 7 + concerto-metamodel/src/metamodel/concerto.rs | 41 + .../src/metamodel/concerto_1_0_0.rs | 55 + .../src/metamodel/concerto_decorator_1_0_0.rs | 20 + .../src/metamodel/concerto_metamodel_1_0_0.rs | 1058 ++++++++++ .../src}/metamodel/mod.rs | 0 concerto-metamodel/src/metamodel/utils.rs | 245 +++ src/metamodel/concerto.rs | 56 - src/metamodel/concerto_1_0_0.rs | 70 - src/metamodel/concerto_decorator_1_0_0.rs | 27 - src/metamodel/concerto_metamodel_1_0_0.rs | 1734 ----------------- src/metamodel/utils.rs | 40 - 32 files changed, 1651 insertions(+), 2064 deletions(-) create mode 100644 concerto-core/Cargo.toml rename {src => concerto-core/src}/error.rs (100%) rename {src => concerto-core/src}/introspect/mod.rs (100%) rename {src => concerto-core/src}/lib.rs (79%) rename {src => concerto-core/src}/metamodel_validation.rs (88%) rename {src => concerto-core/src}/model_file.rs (96%) rename {src => concerto-core/src}/model_manager.rs (88%) rename {src => concerto-core/src}/property_type.rs (100%) rename {src => concerto-core/src}/traits.rs (91%) rename {src => concerto-core/src}/util.rs (100%) rename {src => concerto-core/src}/validation.rs (100%) rename {tests => concerto-core/tests}/conformance_tests.rs (92%) rename {tests => concerto-core/tests}/declaration_tests.rs (96%) rename {tests => concerto-core/tests}/enum_tests.rs (86%) rename {tests => concerto-core/tests}/map_tests.rs (97%) rename {tests => concerto-core/tests}/namespace_tests.rs (97%) rename {tests => concerto-core/tests}/scalar_tests.rs (92%) create mode 100644 concerto-metamodel/Cargo.toml create mode 100644 concerto-metamodel/src/lib.rs create mode 100644 concerto-metamodel/src/metamodel/concerto.rs create mode 100644 concerto-metamodel/src/metamodel/concerto_1_0_0.rs create mode 100644 concerto-metamodel/src/metamodel/concerto_decorator_1_0_0.rs create mode 100644 concerto-metamodel/src/metamodel/concerto_metamodel_1_0_0.rs rename {src => concerto-metamodel/src}/metamodel/mod.rs (100%) create mode 100644 concerto-metamodel/src/metamodel/utils.rs delete mode 100644 src/metamodel/concerto.rs delete mode 100644 src/metamodel/concerto_1_0_0.rs delete mode 100644 src/metamodel/concerto_decorator_1_0_0.rs delete mode 100644 src/metamodel/concerto_metamodel_1_0_0.rs delete mode 100644 src/metamodel/utils.rs diff --git a/Cargo.lock b/Cargo.lock index cc18abc..fcc5e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,11 +67,20 @@ dependencies = [ "windows-link", ] +[[package]] +name = "concerto-metamodel" +version = "3.12.8" +dependencies = [ + "chrono", + "serde", +] + [[package]] name = "concerto_core" version = "0.1.0" dependencies = [ "chrono", + "concerto-metamodel", "lazy_static", "log", "regex", diff --git a/Cargo.toml b/Cargo.toml index c60e67b..c3feb10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,3 @@ -[package] -name = "concerto_core" -version = "0.1.0" -edition = "2021" -authors = ["Accord Project"] -description = "Rust implementation of Concerto Core for structural and semantic metamodel validation" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "1.0" -regex = "1.10" -semver = "1.0" -log = "0.4" -lazy_static = "1.4" -chrono = "0.4" +[workspace] +resolver = "3" +members = ["concerto-metamodel", "concerto-core"] diff --git a/concerto-core/Cargo.toml b/concerto-core/Cargo.toml new file mode 100644 index 0000000..41786fe --- /dev/null +++ b/concerto-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "concerto_core" +version = "0.1.0" +edition = "2021" +authors = ["Accord Project"] +description = "Rust implementation of Concerto Core for structural and semantic metamodel validation" + +[dependencies] +concerto-metamodel = { path = "../concerto-metamodel" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +regex = "1.10" +semver = "1.0" +log = "0.4" +lazy_static = "1.4" +chrono = "0.4" diff --git a/src/error.rs b/concerto-core/src/error.rs similarity index 100% rename from src/error.rs rename to concerto-core/src/error.rs diff --git a/src/introspect/mod.rs b/concerto-core/src/introspect/mod.rs similarity index 100% rename from src/introspect/mod.rs rename to concerto-core/src/introspect/mod.rs diff --git a/src/lib.rs b/concerto-core/src/lib.rs similarity index 79% rename from src/lib.rs rename to concerto-core/src/lib.rs index 87c96be..e3575de 100644 --- a/src/lib.rs +++ b/concerto-core/src/lib.rs @@ -1,15 +1,14 @@ // Main library file for concerto_core // Define modules -pub mod model_file; -pub mod model_manager; pub mod error; -pub mod validation; pub mod introspect; -pub mod util; -pub mod metamodel; pub mod metamodel_validation; +pub mod model_file; +pub mod model_manager; pub mod traits; +pub mod util; +pub mod validation; // Re-export the main components pub use model_file::ModelFile; @@ -17,13 +16,12 @@ pub use model_manager::ModelManager; pub use traits::*; // Metamodel types exports -pub use metamodel::concerto_metamodel_1_0_0::{ - Model, Declaration, Import, - Property, Decorator, TypeIdentifier +pub use concerto_metamodel::concerto_metamodel_1_0_0::{ + Declaration, Decorator, Import, Model, Property, TypeIdentifier, }; -pub use validation::Validate; pub use error::ConcertoError; +pub use validation::Validate; /// Version of the Concerto core library pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/metamodel_validation.rs b/concerto-core/src/metamodel_validation.rs similarity index 88% rename from src/metamodel_validation.rs rename to concerto-core/src/metamodel_validation.rs index 43e36cc..478d373 100644 --- a/src/metamodel_validation.rs +++ b/concerto-core/src/metamodel_validation.rs @@ -1,6 +1,7 @@ +use concerto_metamodel::concerto_metamodel_1_0_0::*; + use crate::error::ConcertoError; use crate::validation::Validate; -use crate::metamodel::concerto_metamodel_1_0_0::*; use regex::Regex; @@ -19,7 +20,9 @@ impl Validate for Model { fn validate(&self) -> Result<(), ConcertoError> { // Validate namespace if self.namespace.is_empty() { - return Err(ConcertoError::ValidationError("Namespace cannot be empty".to_string())); + return Err(ConcertoError::ValidationError( + "Namespace cannot be empty".to_string(), + )); } // Validate declarations if any @@ -29,9 +32,10 @@ impl Validate for Model { for decl in declarations { // Check if this declaration name has already been seen if !declaration_names.insert(&decl.name) { - return Err(ConcertoError::ValidationError( - format!("Duplicate declaration name: {}", decl.name) - )); + return Err(ConcertoError::ValidationError(format!( + "Duplicate declaration name: {}", + decl.name + ))); } // Validate the declaration itself @@ -71,7 +75,10 @@ impl DeclarationValidator for Declaration { crate::traits::CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { crate::traits::CommonDeclarationValidator::validate_decorators(decorators) } } @@ -80,7 +87,9 @@ impl Validate for Decorator { fn validate(&self) -> Result<(), ConcertoError> { // Validate the name of the decorator if self.name.is_empty() { - return Err(ConcertoError::ValidationError("Decorator name cannot be empty".to_string())); + return Err(ConcertoError::ValidationError( + "Decorator name cannot be empty".to_string(), + )); } // Arguments validation would go here if needed @@ -109,9 +118,10 @@ impl Validate for ConceptDeclaration { for property in &self.properties { // Check if this property name has already been seen if !property_names.insert(&property.name) { - return Err(ConcertoError::ValidationError( - format!("Duplicate property name: {} in concept {}", property.name, self.name) - )); + return Err(ConcertoError::ValidationError(format!( + "Duplicate property name: {} in concept {}", + property.name, self.name + ))); } // Validate the property itself @@ -244,7 +254,9 @@ impl Validate for Import { fn validate(&self) -> Result<(), ConcertoError> { // Validate namespace if self.namespace.is_empty() { - return Err(ConcertoError::ValidationError("Import namespace cannot be empty".to_string())); + return Err(ConcertoError::ValidationError( + "Import namespace cannot be empty".to_string(), + )); } Ok(()) diff --git a/src/model_file.rs b/concerto-core/src/model_file.rs similarity index 96% rename from src/model_file.rs rename to concerto-core/src/model_file.rs index b5626b1..8207073 100644 --- a/src/model_file.rs +++ b/concerto-core/src/model_file.rs @@ -1,8 +1,9 @@ -use std::path::Path; use std::fs; +use std::path::Path; + +use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Import, Model, Property}; use crate::error::ConcertoError; -use crate::metamodel::concerto_metamodel_1_0_0::{Model, Import, Declaration, Property}; use crate::validation::Validate; /// Represents a Concerto model file @@ -19,7 +20,11 @@ impl ModelFile { /// Creates a new model file pub fn new(model: Model, content: String, file_name: String) -> Self { - ModelFile { model, content, file_name } + ModelFile { + model, + content, + file_name, + } } /// Creates a new model file from a namespace and optional version. @@ -44,8 +49,6 @@ impl ModelFile { self.model.namespace.clone() } - - /// Loads a model file from a string pub fn from_string(content: String) -> Result { // Note: This is a placeholder - in a real implementation, @@ -53,7 +56,8 @@ impl ModelFile { // For now, we'll just return an error as this functionality would be // provided by the JavaScript parser Err(ConcertoError::ParseError( - "Parsing from string is not implemented in Rust yet. Use the JavaScript parser.".to_string() + "Parsing from string is not implemented in Rust yet. Use the JavaScript parser." + .to_string(), )) } diff --git a/src/model_manager.rs b/concerto-core/src/model_manager.rs similarity index 88% rename from src/model_manager.rs rename to concerto-core/src/model_manager.rs index 9929191..51493e8 100644 --- a/src/model_manager.rs +++ b/concerto-core/src/model_manager.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; +use concerto_metamodel::concerto_metamodel_1_0_0::*; + use crate::error::ConcertoError; use crate::model_file::ModelFile; -use crate::validation::Validate; -use crate::metamodel::concerto_metamodel_1_0_0::*; use crate::traits::*; +use crate::validation::Validate; /// Manages models and provides validation /// Maps from JavaScript ModelManager class but using metamodel types @@ -32,7 +33,8 @@ impl ModelManager { model_file.validate()?; // Add to our collection - self.models.insert(model_file.model.namespace.clone(), model_file); + self.models + .insert(model_file.model.namespace.clone(), model_file); Ok(()) } @@ -107,9 +109,10 @@ impl ModelManager { while let Some(next_super) = inheritance_map.get(current) { // If we've seen this class before, we have a cycle if !visited.insert(current) { - return Err(ConcertoError::ValidationError( - format!("Circular inheritance detected for class {}", class_name) - )); + return Err(ConcertoError::ValidationError(format!( + "Circular inheritance detected for class {}", + class_name + ))); } current = next_super; @@ -121,8 +124,6 @@ impl ModelManager { Ok(()) } - - /// Validates all references between models fn validate_references(&self) -> Result<(), ConcertoError> { // Iterate through all model files @@ -156,7 +157,10 @@ impl ModelManager { } /// Helper method to treat a declaration as a concept declaration - fn as_concept_declaration<'a>(&self, decl: &'a Declaration) -> Option<&'a dyn ConceptDeclarationBase> { + fn as_concept_declaration<'a>( + &self, + decl: &'a Declaration, + ) -> Option<&'a dyn ConceptDeclarationBase> { // Check class name to determine type match decl._class.as_str() { "concerto.metamodel@1.0.0.ConceptDeclaration" => { @@ -164,12 +168,12 @@ impl ModelManager { // This would require unsafe code or a different approach in real code // For now, this is just a placeholder for the concept None - }, + } "concerto.metamodel@1.0.0.AssetDeclaration" => None, "concerto.metamodel@1.0.0.ParticipantDeclaration" => None, "concerto.metamodel@1.0.0.TransactionDeclaration" => None, "concerto.metamodel@1.0.0.EventDeclaration" => None, - _ => None + _ => None, } } @@ -197,13 +201,17 @@ impl ModelManager { } /// Validates that a referenced type exists in the model - pub fn validate_type_exists(&self, type_id: &crate::metamodel::concerto_metamodel_1_0_0::TypeIdentifier) -> Result<(), ConcertoError> { + pub fn validate_type_exists( + &self, + type_id: &concerto_metamodel::concerto_metamodel_1_0_0::TypeIdentifier, + ) -> Result<(), ConcertoError> { let namespace = match &type_id.namespace { Some(ns) => ns, None => { - return Err(ConcertoError::ValidationError( - format!("Type {} is missing namespace", type_id.name) - )); + return Err(ConcertoError::ValidationError(format!( + "Type {} is missing namespace", + type_id.name + ))); } }; @@ -211,9 +219,10 @@ impl ModelManager { let model_file = match self.get_model_file(namespace) { Some(mf) => mf, None => { - return Err(ConcertoError::ValidationError( - format!("Could not find namespace {}", namespace) - )); + return Err(ConcertoError::ValidationError(format!( + "Could not find namespace {}", + namespace + ))); } }; @@ -227,9 +236,10 @@ impl ModelManager { } } - Err(ConcertoError::ValidationError( - format!("Could not find type {}.{}", namespace, type_id.name) - )) + Err(ConcertoError::ValidationError(format!( + "Could not find type {}.{}", + namespace, type_id.name + ))) } /// Validates a property type diff --git a/src/property_type.rs b/concerto-core/src/property_type.rs similarity index 100% rename from src/property_type.rs rename to concerto-core/src/property_type.rs diff --git a/src/traits.rs b/concerto-core/src/traits.rs similarity index 91% rename from src/traits.rs rename to concerto-core/src/traits.rs index 1b4a7e7..cb585ea 100644 --- a/src/traits.rs +++ b/concerto-core/src/traits.rs @@ -1,6 +1,7 @@ -use crate::{ConcertoError, ModelManager, metamodel_validation::is_valid_identifier}; -use crate::metamodel::concerto_metamodel_1_0_0::*; +use concerto_metamodel::concerto_metamodel_1_0_0::*; + use crate::validation::Validate; +use crate::{metamodel_validation::is_valid_identifier, ConcertoError, ModelManager}; /// Base trait for declaration types pub trait DeclarationBase { @@ -27,7 +28,6 @@ impl DeclarationBase for Declaration { fn get_location(&self) -> Option<&Range> { self.location.as_ref() } - } impl DeclarationBase for AssetDeclaration { @@ -454,7 +454,7 @@ pub trait PropertyValidator { impl PropertyValidator for RelationshipProperty { fn validate(&self, model_manager: &ModelManager) -> Result<(), ConcertoError> { - model_manager.validate_type_exists(&self.type_) + model_manager.validate_type_exists(&self.type_) } } @@ -514,7 +514,8 @@ pub trait DeclarationValidator { fn validate_name(&self, name: &str) -> Result<(), ConcertoError>; /// Validates decorators if present - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError>; + fn validate_decorators(&self, decorators: &Option>) + -> Result<(), ConcertoError>; } /// Common validator implementation that can be reused across declaration types @@ -559,9 +560,10 @@ impl CommonDeclarationValidator { // Check for duplicate property names if !property_names.insert(&property.name) { - return Err(ConcertoError::ValidationError( - format!("Duplicate property: {}", property.name) - )); + return Err(ConcertoError::ValidationError(format!( + "Duplicate property: {}", + property.name + ))); } } @@ -587,7 +589,10 @@ impl DeclarationValidator for ConceptDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -605,7 +610,10 @@ impl DeclarationValidator for AssetDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -622,7 +630,10 @@ impl DeclarationValidator for ParticipantDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -639,7 +650,10 @@ impl DeclarationValidator for TransactionDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -656,7 +670,10 @@ impl DeclarationValidator for EventDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -671,16 +688,18 @@ impl DeclarationValidator for EnumDeclaration { for property in &self.properties { // Validate each enum property if !CommonDeclarationValidator::validate_identifier(&property.name).is_ok() { - return Err(ConcertoError::ValidationError( - format!("'{}' is not a valid enum property name", property.name) - )); + return Err(ConcertoError::ValidationError(format!( + "'{}' is not a valid enum property name", + property.name + ))); } // Check for duplicate enum values if !enum_values.insert(&property.name) { - return Err(ConcertoError::ValidationError( - format!("Duplicate enum value: {}", property.name) - )); + return Err(ConcertoError::ValidationError(format!( + "Duplicate enum value: {}", + property.name + ))); } // Validate decorators on enum properties @@ -697,7 +716,10 @@ impl DeclarationValidator for EnumDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -708,9 +730,11 @@ impl DeclarationValidator for MapDeclaration { // Validate key type - must be StringMapKeyType or DateTimeMapKeyType // We need to check the _class field of the key type - if !self.key._class.contains("StringMapKeyType") && !self.key._class.contains("DateTimeMapKeyType") { + if !self.key._class.contains("StringMapKeyType") + && !self.key._class.contains("DateTimeMapKeyType") + { return Err(ConcertoError::ValidationError( - "Invalid map key type. Map keys must be String or DateTime".to_string() + "Invalid map key type. Map keys must be String or DateTime".to_string(), )); } @@ -726,7 +750,10 @@ impl DeclarationValidator for MapDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -774,7 +801,10 @@ impl DeclarationValidator for ScalarDeclaration { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -885,11 +915,12 @@ impl DeclarationValidator for StringScalar { // StringRegexValidator has a direct pattern field (not Option) // Try to compile the regex to validate it match regex::Regex::new(&validator.pattern) { - Ok(_) => {}, // Regex is valid + Ok(_) => {} // Regex is valid Err(_) => { - return Err(ConcertoError::ValidationError( - format!("Invalid regex pattern in string validator: {}", validator.pattern) - )); + return Err(ConcertoError::ValidationError(format!( + "Invalid regex pattern in string validator: {}", + validator.pattern + ))); } } } @@ -901,7 +932,10 @@ impl DeclarationValidator for StringScalar { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -955,7 +989,10 @@ impl DeclarationValidator for BooleanScalar { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } @@ -970,7 +1007,10 @@ impl DeclarationValidator for DateTimeScalar { CommonDeclarationValidator::validate_identifier(name) } - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { + fn validate_decorators( + &self, + decorators: &Option>, + ) -> Result<(), ConcertoError> { CommonDeclarationValidator::validate_decorators(decorators) } } diff --git a/src/util.rs b/concerto-core/src/util.rs similarity index 100% rename from src/util.rs rename to concerto-core/src/util.rs diff --git a/src/validation.rs b/concerto-core/src/validation.rs similarity index 100% rename from src/validation.rs rename to concerto-core/src/validation.rs diff --git a/tests/conformance_tests.rs b/concerto-core/tests/conformance_tests.rs similarity index 92% rename from tests/conformance_tests.rs rename to concerto-core/tests/conformance_tests.rs index 7d0de33..88f1042 100644 --- a/tests/conformance_tests.rs +++ b/concerto-core/tests/conformance_tests.rs @@ -1,11 +1,11 @@ //! Integration tests for validation rules use concerto_core::model_file::ModelFile; -use concerto_core::metamodel::concerto_metamodel_1_0_0::{ - ConceptDeclaration, Declaration, Property, TypeIdentifier -}; -use concerto_core::validation::Validate; use concerto_core::model_manager::ModelManager; +use concerto_core::validation::Validate; +use concerto_metamodel::concerto_metamodel_1_0_0::{ + ConceptDeclaration, Declaration, Property, TypeIdentifier, +}; #[test] fn test_conformance_system_property_name() { @@ -19,16 +19,14 @@ fn test_conformance_system_property_name() { _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), name: "Person".to_string(), super_type: None, - properties: vec![ - Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "$class".to_string(), // System-reserved name - is_array: false, - is_optional: false, - decorators: None, - location: None, - } - ], + properties: vec![Property { + _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), + name: "$class".to_string(), // System-reserved name + is_array: false, + is_optional: false, + decorators: None, + location: None, + }], decorators: None, is_abstract: false, identified: None, @@ -50,8 +48,6 @@ fn test_conformance_system_property_name() { let result = model_file.validate(); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("Invalid")); - - } #[test] @@ -215,7 +211,7 @@ fn test_conformance_property_duplicate_names() { is_optional: false, decorators: None, location: None, - } + }, ], decorators: None, is_abstract: false, @@ -226,7 +222,10 @@ fn test_conformance_property_duplicate_names() { // First, directly validate the ConceptDeclaration which should fail due to duplicate properties let concept_result = concept_decl.validate(); assert!(concept_result.is_err()); - assert!(concept_result.unwrap_err().to_string().contains("Duplicate property")); + assert!(concept_result + .unwrap_err() + .to_string() + .contains("Duplicate property")); // The ModelFile test is not needed since we've already verified the validation works directly // If needed in a real implementation, we would need to ensure ModelFile validation diff --git a/tests/declaration_tests.rs b/concerto-core/tests/declaration_tests.rs similarity index 96% rename from tests/declaration_tests.rs rename to concerto-core/tests/declaration_tests.rs index af80a0a..27f57ef 100644 --- a/tests/declaration_tests.rs +++ b/concerto-core/tests/declaration_tests.rs @@ -1,7 +1,7 @@ //! Basic tests for declaration validation -use concerto_core::metamodel::concerto_metamodel_1_0_0::{ConceptDeclaration, Property, Declaration}; use concerto_core::validation::Validate; +use concerto_metamodel::concerto_metamodel_1_0_0::{ConceptDeclaration, Declaration, Property}; #[test] fn test_declaration_validation() { diff --git a/tests/enum_tests.rs b/concerto-core/tests/enum_tests.rs similarity index 86% rename from tests/enum_tests.rs rename to concerto-core/tests/enum_tests.rs index b4ebb49..383fd9c 100644 --- a/tests/enum_tests.rs +++ b/concerto-core/tests/enum_tests.rs @@ -1,9 +1,11 @@ //! Tests for enum validation -use concerto_core::*; -use concerto_core::metamodel::concerto_metamodel_1_0_0::{EnumDeclaration, EnumProperty, Declaration, Decorator}; -use concerto_core::validation::Validate; use concerto_core::traits::DeclarationBase; +use concerto_core::validation::Validate; +use concerto_core::*; +use concerto_metamodel::concerto_metamodel_1_0_0::{ + Declaration, Decorator, EnumDeclaration, EnumProperty, +}; // Helper function to create an EnumProperty fn create_enum_property(name: &str) -> EnumProperty { @@ -17,9 +19,10 @@ fn create_enum_property(name: &str) -> EnumProperty { // Helper function to create an EnumDeclaration fn create_enum_declaration(name: &str, values: Vec<&str>) -> EnumDeclaration { - let properties = values.iter() - .map(|v| create_enum_property(v)) - .collect::>(); + let properties = values + .iter() + .map(|v| create_enum_property(v)) + .collect::>(); EnumDeclaration { _class: "concerto.metamodel@1.0.0.EnumDeclaration".to_string(), @@ -101,14 +104,12 @@ fn test_enum_with_invalid_property_name() { let enum_decl = EnumDeclaration { _class: "concerto.metamodel@1.0.0.EnumDeclaration".to_string(), name: "InvalidEnum".to_string(), - properties: vec![ - EnumProperty { - _class: "concerto.metamodel@1.0.0.EnumProperty".to_string(), - name: "123INVALID".to_string(), // Invalid name (starts with number) - decorators: None, - location: None, - } - ], + properties: vec![EnumProperty { + _class: "concerto.metamodel@1.0.0.EnumProperty".to_string(), + name: "123INVALID".to_string(), // Invalid name (starts with number) + decorators: None, + location: None, + }], decorators: None, location: None, }; diff --git a/tests/map_tests.rs b/concerto-core/tests/map_tests.rs similarity index 97% rename from tests/map_tests.rs rename to concerto-core/tests/map_tests.rs index 755fe0c..2d07428 100644 --- a/tests/map_tests.rs +++ b/concerto-core/tests/map_tests.rs @@ -1,9 +1,9 @@ //! Tests for map validation -use concerto_core::metamodel::concerto_metamodel_1_0_0::{ - MapDeclaration, Declaration, MapKeyType, MapValueType -}; use concerto_core::validation::Validate; +use concerto_metamodel::concerto_metamodel_1_0_0::{ + Declaration, MapDeclaration, MapKeyType, MapValueType, +}; #[test] fn test_map_with_valid_string_key() { diff --git a/tests/namespace_tests.rs b/concerto-core/tests/namespace_tests.rs similarity index 97% rename from tests/namespace_tests.rs rename to concerto-core/tests/namespace_tests.rs index 75fec03..894b0e7 100644 --- a/tests/namespace_tests.rs +++ b/concerto-core/tests/namespace_tests.rs @@ -1,9 +1,9 @@ //! Tests for namespace and import validation -use concerto_core::*; use concerto_core::model_file::ModelFile; -use concerto_core::metamodel::concerto_metamodel_1_0_0::Import; use concerto_core::validation::Validate; +use concerto_core::*; +use concerto_metamodel::concerto_metamodel_1_0_0::Import; #[test] fn test_valid_namespace_format() { diff --git a/tests/scalar_tests.rs b/concerto-core/tests/scalar_tests.rs similarity index 92% rename from tests/scalar_tests.rs rename to concerto-core/tests/scalar_tests.rs index e667f97..a97f62c 100644 --- a/tests/scalar_tests.rs +++ b/concerto-core/tests/scalar_tests.rs @@ -1,10 +1,10 @@ //! Tests for scalar validation -use concerto_core::metamodel::concerto_metamodel_1_0_0::{ - Declaration, IntegerScalar, StringScalar, - IntegerDomainValidator, StringRegexValidator, StringLengthValidator -}; use concerto_core::validation::Validate; +use concerto_metamodel::concerto_metamodel_1_0_0::{ + Declaration, IntegerDomainValidator, IntegerScalar, StringLengthValidator, + StringRegexValidator, StringScalar, +}; #[test] fn test_scalar_with_valid_number_bounds() { @@ -114,5 +114,8 @@ fn test_scalar_with_invalid_name() { // Should fail validation with message about invalid identifier let result = declaration.validate(); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("not a valid identifier")); + assert!(result + .unwrap_err() + .to_string() + .contains("not a valid identifier")); } diff --git a/concerto-metamodel/Cargo.toml b/concerto-metamodel/Cargo.toml new file mode 100644 index 0000000..e2ec0e3 --- /dev/null +++ b/concerto-metamodel/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "concerto-metamodel" +version = "3.12.8" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +chrono = "0.4" diff --git a/concerto-metamodel/src/lib.rs b/concerto-metamodel/src/lib.rs new file mode 100644 index 0000000..0ff26df --- /dev/null +++ b/concerto-metamodel/src/lib.rs @@ -0,0 +1,7 @@ +mod metamodel; + +pub use metamodel::concerto; +pub use metamodel::concerto_1_0_0; +pub use metamodel::concerto_decorator_1_0_0; +pub use metamodel::concerto_metamodel_1_0_0; +pub use metamodel::utils; diff --git a/concerto-metamodel/src/metamodel/concerto.rs b/concerto-metamodel/src/metamodel/concerto.rs new file mode 100644 index 0000000..f78cb74 --- /dev/null +++ b/concerto-metamodel/src/metamodel/concerto.rs @@ -0,0 +1,41 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::concerto_decorator_1_0_0::*; +use super::utils::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Concept { + #[serde(rename = "$class")] + pub _class: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Asset { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "$identifier")] + pub _identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Participant { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "$identifier")] + pub _identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + #[serde(rename = "$class")] + pub _class: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + #[serde(rename = "$class")] + pub _class: String, +} diff --git a/concerto-metamodel/src/metamodel/concerto_1_0_0.rs b/concerto-metamodel/src/metamodel/concerto_1_0_0.rs new file mode 100644 index 0000000..3c1b384 --- /dev/null +++ b/concerto-metamodel/src/metamodel/concerto_1_0_0.rs @@ -0,0 +1,55 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::concerto_decorator_1_0_0::*; +use super::utils::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Concept { + #[serde(rename = "$class")] + pub _class: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Asset { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "$identifier")] + pub _identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Participant { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "$identifier")] + pub _identifier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + #[serde(rename = "$class")] + pub _class: String, + + #[serde( + rename = "$timestamp", + serialize_with = "serialize_datetime", + deserialize_with = "deserialize_datetime" + )] + pub _timestamp: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Event { + #[serde(rename = "$class")] + pub _class: String, + + #[serde( + rename = "$timestamp", + serialize_with = "serialize_datetime", + deserialize_with = "deserialize_datetime" + )] + pub _timestamp: DateTime, +} diff --git a/concerto-metamodel/src/metamodel/concerto_decorator_1_0_0.rs b/concerto-metamodel/src/metamodel/concerto_decorator_1_0_0.rs new file mode 100644 index 0000000..db68b31 --- /dev/null +++ b/concerto-metamodel/src/metamodel/concerto_decorator_1_0_0.rs @@ -0,0 +1,20 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::concerto_1_0_0::*; +use super::utils::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decorator { + #[serde(rename = "$class")] + pub _class: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DotNetNamespace { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "namespace")] + pub namespace: String, +} diff --git a/concerto-metamodel/src/metamodel/concerto_metamodel_1_0_0.rs b/concerto-metamodel/src/metamodel/concerto_metamodel_1_0_0.rs new file mode 100644 index 0000000..c759d74 --- /dev/null +++ b/concerto-metamodel/src/metamodel/concerto_metamodel_1_0_0.rs @@ -0,0 +1,1058 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::concerto_1_0_0::*; +use super::utils::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Position { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "line")] + pub line: i32, + + #[serde(rename = "column")] + pub column: i32, + + #[serde(rename = "offset")] + pub offset: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Range { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "start")] + pub start: Position, + + #[serde(rename = "end")] + pub end: Position, + + #[serde(rename = "source", skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TypeIdentifier { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "resolvedName", skip_serializing_if = "Option::is_none")] + pub resolved_name: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecoratorLiteral { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecoratorString { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "value")] + pub value: String, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecoratorNumber { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "value")] + pub value: f64, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecoratorBoolean { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "value")] + pub value: bool, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecoratorTypeReference { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decorator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "arguments", skip_serializing_if = "Option::is_none")] + pub arguments: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Identified { + #[serde(rename = "$class")] + pub _class: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdentifiedBy { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Declaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MapKeyType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MapDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "key")] + pub key: MapKeyType, + + #[serde(rename = "value")] + pub value: MapValueType, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringMapKeyType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateTimeMapKeyType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObjectMapKeyType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BooleanMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateTimeMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegerMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LongMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DoubleMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObjectMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelationshipMapValueType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnumDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnumProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConceptDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "isAbstract")] + pub is_abstract: bool, + + #[serde(rename = "identified", skip_serializing_if = "Option::is_none")] + pub identified: Option, + + #[serde(rename = "superType", skip_serializing_if = "Option::is_none")] + pub super_type: Option, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "isAbstract")] + pub is_abstract: bool, + + #[serde(rename = "identified", skip_serializing_if = "Option::is_none")] + pub identified: Option, + + #[serde(rename = "superType", skip_serializing_if = "Option::is_none")] + pub super_type: Option, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ParticipantDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "isAbstract")] + pub is_abstract: bool, + + #[serde(rename = "identified", skip_serializing_if = "Option::is_none")] + pub identified: Option, + + #[serde(rename = "superType", skip_serializing_if = "Option::is_none")] + pub super_type: Option, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "isAbstract")] + pub is_abstract: bool, + + #[serde(rename = "identified", skip_serializing_if = "Option::is_none")] + pub identified: Option, + + #[serde(rename = "superType", skip_serializing_if = "Option::is_none")] + pub super_type: Option, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "isAbstract")] + pub is_abstract: bool, + + #[serde(rename = "identified", skip_serializing_if = "Option::is_none")] + pub identified: Option, + + #[serde(rename = "superType", skip_serializing_if = "Option::is_none")] + pub super_type: Option, + + #[serde(rename = "properties")] + pub properties: Vec, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Property { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelationshipProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ObjectProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "type")] + pub type_: TypeIdentifier, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BooleanProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateTimeProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "lengthValidator", skip_serializing_if = "Option::is_none")] + pub length_validator: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringRegexValidator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "pattern")] + pub pattern: String, + + #[serde(rename = "flags")] + pub flags: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringLengthValidator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")] + pub min_length: Option, + + #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")] + pub max_length: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DoubleProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DoubleDomainValidator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "lower", skip_serializing_if = "Option::is_none")] + pub lower: Option, + + #[serde(rename = "upper", skip_serializing_if = "Option::is_none")] + pub upper: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegerProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegerDomainValidator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "lower", skip_serializing_if = "Option::is_none")] + pub lower: Option, + + #[serde(rename = "upper", skip_serializing_if = "Option::is_none")] + pub upper: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LongProperty { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "isArray")] + pub is_array: bool, + + #[serde(rename = "isOptional")] + pub is_optional: bool, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LongDomainValidator { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "lower", skip_serializing_if = "Option::is_none")] + pub lower: Option, + + #[serde(rename = "upper", skip_serializing_if = "Option::is_none")] + pub upper: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AliasedType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "aliasedName")] + pub aliased_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Import { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "namespace")] + pub namespace: String, + + #[serde(rename = "uri", skip_serializing_if = "Option::is_none")] + pub uri: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportAll { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "namespace")] + pub namespace: String, + + #[serde(rename = "uri", skip_serializing_if = "Option::is_none")] + pub uri: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportType { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "namespace")] + pub namespace: String, + + #[serde(rename = "uri", skip_serializing_if = "Option::is_none")] + pub uri: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportTypes { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "types")] + pub types: Vec, + + #[serde(rename = "aliasedTypes", skip_serializing_if = "Option::is_none")] + pub aliased_types: Option>, + + #[serde(rename = "namespace")] + pub namespace: String, + + #[serde(rename = "uri", skip_serializing_if = "Option::is_none")] + pub uri: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Model { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "namespace")] + pub namespace: String, + + #[serde(rename = "sourceUri", skip_serializing_if = "Option::is_none")] + pub source_uri: Option, + + #[serde(rename = "concertoVersion", skip_serializing_if = "Option::is_none")] + pub concerto_version: Option, + + #[serde(rename = "imports", skip_serializing_if = "Option::is_none")] + pub imports: Option>, + + #[serde(rename = "declarations", skip_serializing_if = "Option::is_none")] + pub declarations: Option>, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Models { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "models")] + pub models: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScalarDeclaration { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BooleanScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegerScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LongScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DoubleScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StringScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "validator", skip_serializing_if = "Option::is_none")] + pub validator: Option, + + #[serde(rename = "lengthValidator", skip_serializing_if = "Option::is_none")] + pub length_validator: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateTimeScalar { + #[serde(rename = "$class")] + pub _class: String, + + #[serde(rename = "defaultValue", skip_serializing_if = "Option::is_none")] + pub default_value: Option, + + #[serde(rename = "namespace", skip_serializing_if = "Option::is_none")] + pub namespace: Option, + + #[serde(rename = "name")] + pub name: String, + + #[serde(rename = "decorators", skip_serializing_if = "Option::is_none")] + pub decorators: Option>, + + #[serde(rename = "location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} diff --git a/src/metamodel/mod.rs b/concerto-metamodel/src/metamodel/mod.rs similarity index 100% rename from src/metamodel/mod.rs rename to concerto-metamodel/src/metamodel/mod.rs diff --git a/concerto-metamodel/src/metamodel/utils.rs b/concerto-metamodel/src/metamodel/utils.rs new file mode 100644 index 0000000..63ba893 --- /dev/null +++ b/concerto-metamodel/src/metamodel/utils.rs @@ -0,0 +1,245 @@ +use chrono::{ DateTime, Utc }; +use serde::{ Deserialize, Serialize, Deserializer, Serializer }; + +pub fn serialize_datetime_option(datetime: &Option>, serializer: S) -> Result +where + S: Serializer, +{ + match datetime { + Some(dt) => { + serialize_datetime(&dt, serializer) + }, + _ => unreachable!(), + } +} + +pub fn deserialize_datetime_option<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + match deserialize_datetime(deserializer) { + Ok(result)=>Ok(Some(result)), + Err(error) => Err(error), + } +} + +pub fn deserialize_datetime<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let datetime_str = String::deserialize(deserializer)?; + DateTime::parse_from_str(&datetime_str, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom) +} + +pub fn serialize_datetime(datetime: &chrono::DateTime, serializer: S) -> Result +where + S: Serializer, +{ + let datetime_str = datetime.format("%+").to_string(); + serializer.serialize_str(&datetime_str) +} + +pub fn serialize_datetime_array(datetime_array: &Vec>, serializer: S) -> Result +where + S: Serializer, +{ + let datetime_strings: Vec = datetime_array + .iter() + .map(|dt| dt.format("%+").to_string()) + .collect(); + datetime_strings.serialize(serializer) +} + +pub fn deserialize_datetime_array<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let datetime_strings = Vec::::deserialize(deserializer)?; + datetime_strings + .iter() + .map(|s| DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)) + .collect() +} + +pub fn serialize_datetime_array_option(datetime_array: &Option>>, serializer: S) -> Result +where + S: Serializer, +{ + match datetime_array { + Some(arr) => { + serialize_datetime_array(&arr, serializer) + }, + None => serializer.serialize_none(), + } +} + +pub fn deserialize_datetime_array_option<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + match Option::>::deserialize(deserializer)? { + Some(datetime_strings) => { + let result: Result, _> = datetime_strings + .iter() + .map(|s| DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)) + .collect(); + result.map(Some) + }, + None => Ok(None), + } +} + +pub fn serialize_hashmap_datetime_key(hashmap: &std::collections::HashMap, String>, serializer: S) -> Result +where + S: Serializer, +{ + let string_map: std::collections::HashMap = hashmap + .iter() + .map(|(k, v)| (k.format("%+").to_string(), v.clone())) + .collect(); + string_map.serialize(serializer) +} + +pub fn deserialize_hashmap_datetime_key<'de, D>(deserializer: D) -> Result, String>, D::Error> +where + D: Deserializer<'de>, +{ + let string_map = std::collections::HashMap::::deserialize(deserializer)?; + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_key = DateTime::parse_from_str(&k, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(datetime_key, v); + } + Ok(result) +} + +pub fn serialize_hashmap_datetime_value(hashmap: &std::collections::HashMap>, serializer: S) -> Result +where + S: Serializer, +{ + let string_map: std::collections::HashMap = hashmap + .iter() + .map(|(k, v)| (k.clone(), v.format("%+").to_string())) + .collect(); + string_map.serialize(serializer) +} + +pub fn deserialize_hashmap_datetime_value<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let string_map = std::collections::HashMap::::deserialize(deserializer)?; + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_value = DateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(k, datetime_value); + } + Ok(result) +} + +pub fn serialize_hashmap_datetime_both(hashmap: &std::collections::HashMap, chrono::DateTime>, serializer: S) -> Result +where + S: Serializer, +{ + let string_map: std::collections::HashMap = hashmap + .iter() + .map(|(k, v)| (k.format("%+").to_string(), v.format("%+").to_string())) + .collect(); + string_map.serialize(serializer) +} + +pub fn deserialize_hashmap_datetime_both<'de, D>(deserializer: D) -> Result, chrono::DateTime>, D::Error> +where + D: Deserializer<'de>, +{ + let string_map = std::collections::HashMap::::deserialize(deserializer)?; + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_key = DateTime::parse_from_str(&k, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + let datetime_value = DateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(datetime_key, datetime_value); + } + Ok(result) +} + +pub fn serialize_hashmap_datetime_key_option(hashmap: &Option, String>>, serializer: S) -> Result +where + S: Serializer, +{ + match hashmap { + Some(map) => serialize_hashmap_datetime_key(map, serializer), + None => serializer.serialize_none(), + } +} + +pub fn deserialize_hashmap_datetime_key_option<'de, D>(deserializer: D) -> Result, String>>, D::Error> +where + D: Deserializer<'de>, +{ + match Option::>::deserialize(deserializer)? { + Some(string_map) => { + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_key = DateTime::parse_from_str(&k, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(datetime_key, v); + } + Ok(Some(result)) + }, + None => Ok(None), + } +} + +pub fn serialize_hashmap_datetime_value_option(hashmap: &Option>>, serializer: S) -> Result +where + S: Serializer, +{ + match hashmap { + Some(map) => serialize_hashmap_datetime_value(map, serializer), + None => serializer.serialize_none(), + } +} + +pub fn deserialize_hashmap_datetime_value_option<'de, D>(deserializer: D) -> Result>>, D::Error> +where + D: Deserializer<'de>, +{ + match Option::>::deserialize(deserializer)? { + Some(string_map) => { + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_value = DateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(k, datetime_value); + } + Ok(Some(result)) + }, + None => Ok(None), + } +} + +pub fn serialize_hashmap_datetime_both_option(hashmap: &Option, chrono::DateTime>>, serializer: S) -> Result +where + S: Serializer, +{ + match hashmap { + Some(map) => serialize_hashmap_datetime_both(map, serializer), + None => serializer.serialize_none(), + } +} + +pub fn deserialize_hashmap_datetime_both_option<'de, D>(deserializer: D) -> Result, chrono::DateTime>>, D::Error> +where + D: Deserializer<'de>, +{ + match Option::>::deserialize(deserializer)? { + Some(string_map) => { + let mut result = std::collections::HashMap::new(); + for (k, v) in string_map { + let datetime_key = DateTime::parse_from_str(&k, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + let datetime_value = DateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S%.3f%Z").map(|dt| dt.with_timezone(&Utc)).map_err(serde::de::Error::custom)?; + result.insert(datetime_key, datetime_value); + } + Ok(Some(result)) + }, + None => Ok(None), + } +} diff --git a/src/metamodel/concerto.rs b/src/metamodel/concerto.rs deleted file mode 100644 index 9bb1890..0000000 --- a/src/metamodel/concerto.rs +++ /dev/null @@ -1,56 +0,0 @@ -use serde::{ Deserialize, Serialize }; -use chrono::{ DateTime, TimeZone, Utc }; - -use crate::metamodel::concerto_decorator_1_0_0::*; -use crate::metamodel::utils::*; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Concept { - #[serde( - rename = "$class", - )] - pub _class: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Asset { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$identifier", - )] - pub _identifier: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Participant { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$identifier", - )] - pub _identifier: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Transaction { - #[serde( - rename = "$class", - )] - pub _class: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Event { - #[serde( - rename = "$class", - )] - pub _class: String, -} - diff --git a/src/metamodel/concerto_1_0_0.rs b/src/metamodel/concerto_1_0_0.rs deleted file mode 100644 index 521de04..0000000 --- a/src/metamodel/concerto_1_0_0.rs +++ /dev/null @@ -1,70 +0,0 @@ -use serde::{ Deserialize, Serialize }; -use chrono::{ DateTime, TimeZone, Utc }; - -use crate::metamodel::concerto_decorator_1_0_0::*; -use crate::metamodel::utils::*; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Concept { - #[serde( - rename = "$class", - )] - pub _class: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Asset { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$identifier", - )] - pub _identifier: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Participant { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$identifier", - )] - pub _identifier: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Transaction { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$timestamp", - serialize_with = "serialize_datetime", - deserialize_with = "deserialize_datetime", - )] - pub _timestamp: DateTime, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Event { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "$timestamp", - serialize_with = "serialize_datetime", - deserialize_with = "deserialize_datetime", - )] - pub _timestamp: DateTime, -} - diff --git a/src/metamodel/concerto_decorator_1_0_0.rs b/src/metamodel/concerto_decorator_1_0_0.rs deleted file mode 100644 index eaba31e..0000000 --- a/src/metamodel/concerto_decorator_1_0_0.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::{ Deserialize, Serialize }; -use chrono::{ DateTime, TimeZone, Utc }; - -use crate::metamodel::concerto_1_0_0::*; -use crate::metamodel::utils::*; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Decorator { - #[serde( - rename = "$class", - )] - pub _class: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DotNetNamespace { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "namespace", - )] - pub namespace: String, -} - diff --git a/src/metamodel/concerto_metamodel_1_0_0.rs b/src/metamodel/concerto_metamodel_1_0_0.rs deleted file mode 100644 index 5177002..0000000 --- a/src/metamodel/concerto_metamodel_1_0_0.rs +++ /dev/null @@ -1,1734 +0,0 @@ -use serde::{ Deserialize, Serialize }; -use chrono::{ DateTime, TimeZone, Utc }; - -use crate::metamodel::concerto_1_0_0::*; -use crate::metamodel::utils::*; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Position { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "line", - )] - pub line: i32, - - #[serde( - rename = "column", - )] - pub column: i32, - - #[serde( - rename = "offset", - )] - pub offset: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Range { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "start", - )] - pub start: Position, - - #[serde( - rename = "end", - )] - pub end: Position, - - #[serde( - rename = "source", - skip_serializing_if = "Option::is_none", - )] - pub source: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TypeIdentifier { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "namespace", - skip_serializing_if = "Option::is_none", - )] - pub namespace: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DecoratorLiteral { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DecoratorString { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "value", - )] - pub value: String, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DecoratorNumber { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "value", - )] - pub value: f64, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DecoratorBoolean { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "value", - )] - pub value: bool, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DecoratorTypeReference { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Decorator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "arguments", - skip_serializing_if = "Option::is_none", - )] - pub arguments: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Identified { - #[serde( - rename = "$class", - )] - pub _class: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IdentifiedBy { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Declaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MapKeyType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MapDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "key", - )] - pub key: MapKeyType, - - #[serde( - rename = "value", - )] - pub value: MapValueType, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringMapKeyType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DateTimeMapKeyType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ObjectMapKeyType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BooleanMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DateTimeMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IntegerMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LongMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DoubleMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ObjectMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct RelationshipMapValueType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EnumDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EnumProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ConceptDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "isAbstract", - )] - pub is_abstract: bool, - - #[serde( - rename = "identified", - skip_serializing_if = "Option::is_none", - )] - pub identified: Option, - - #[serde( - rename = "superType", - skip_serializing_if = "Option::is_none", - )] - pub super_type: Option, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AssetDeclaration{ - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "isAbstract", - )] - pub is_abstract: bool, - - #[serde( - rename = "identified", - skip_serializing_if = "Option::is_none", - )] - pub identified: Option, - - #[serde( - rename = "superType", - skip_serializing_if = "Option::is_none", - )] - pub super_type: Option, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ParticipantDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "isAbstract", - )] - pub is_abstract: bool, - - #[serde( - rename = "identified", - skip_serializing_if = "Option::is_none", - )] - pub identified: Option, - - #[serde( - rename = "superType", - skip_serializing_if = "Option::is_none", - )] - pub super_type: Option, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TransactionDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "isAbstract", - )] - pub is_abstract: bool, - - #[serde( - rename = "identified", - skip_serializing_if = "Option::is_none", - )] - pub identified: Option, - - #[serde( - rename = "superType", - skip_serializing_if = "Option::is_none", - )] - pub super_type: Option, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EventDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "isAbstract", - )] - pub is_abstract: bool, - - #[serde( - rename = "identified", - skip_serializing_if = "Option::is_none", - )] - pub identified: Option, - - #[serde( - rename = "superType", - skip_serializing_if = "Option::is_none", - )] - pub super_type: Option, - - #[serde( - rename = "properties", - )] - pub properties: Vec, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Property { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct RelationshipProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ObjectProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "type", - )] - pub type_: TypeIdentifier, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BooleanProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DateTimeProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "lengthValidator", - skip_serializing_if = "Option::is_none", - )] - pub length_validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringRegexValidator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "pattern", - )] - pub pattern: String, - - #[serde( - rename = "flags", - )] - pub flags: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringLengthValidator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "minLength", - skip_serializing_if = "Option::is_none", - )] - pub min_length: Option, - - #[serde( - rename = "maxLength", - skip_serializing_if = "Option::is_none", - )] - pub max_length: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DoubleProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DoubleDomainValidator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "lower", - skip_serializing_if = "Option::is_none", - )] - pub lower: Option, - - #[serde( - rename = "upper", - skip_serializing_if = "Option::is_none", - )] - pub upper: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IntegerProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IntegerDomainValidator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "lower", - skip_serializing_if = "Option::is_none", - )] - pub lower: Option, - - #[serde( - rename = "upper", - skip_serializing_if = "Option::is_none", - )] - pub upper: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LongProperty { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "isArray", - )] - pub is_array: bool, - - #[serde( - rename = "isOptional", - )] - pub is_optional: bool, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LongDomainValidator { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "lower", - skip_serializing_if = "Option::is_none", - )] - pub lower: Option, - - #[serde( - rename = "upper", - skip_serializing_if = "Option::is_none", - )] - pub upper: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct AliasedType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "aliasedName", - )] - pub aliased_name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Import { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "namespace", - )] - pub namespace: String, - - #[serde( - rename = "uri", - skip_serializing_if = "Option::is_none", - )] - pub uri: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ImportAll { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "namespace", - )] - pub namespace: String, - - #[serde( - rename = "uri", - skip_serializing_if = "Option::is_none", - )] - pub uri: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ImportType { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "namespace", - )] - pub namespace: String, - - #[serde( - rename = "uri", - skip_serializing_if = "Option::is_none", - )] - pub uri: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ImportTypes { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "types", - )] - pub types: Vec, - - #[serde( - rename = "aliasedTypes", - skip_serializing_if = "Option::is_none", - )] - pub aliased_types: Option>, - - #[serde( - rename = "namespace", - )] - pub namespace: String, - - #[serde( - rename = "uri", - skip_serializing_if = "Option::is_none", - )] - pub uri: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Model { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "namespace", - )] - pub namespace: String, - - #[serde( - rename = "sourceUri", - skip_serializing_if = "Option::is_none", - )] - pub source_uri: Option, - - #[serde( - rename = "concertoVersion", - skip_serializing_if = "Option::is_none", - )] - pub concerto_version: Option, - - #[serde( - rename = "imports", - skip_serializing_if = "Option::is_none", - )] - pub imports: Option>, - - #[serde( - rename = "declarations", - skip_serializing_if = "Option::is_none", - )] - pub declarations: Option>, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Models { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "models", - )] - pub models: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ScalarDeclaration { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct BooleanScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct IntegerScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LongScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DoubleScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StringScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "validator", - skip_serializing_if = "Option::is_none", - )] - pub validator: Option, - - #[serde( - rename = "lengthValidator", - skip_serializing_if = "Option::is_none", - )] - pub length_validator: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DateTimeScalar { - #[serde( - rename = "$class", - )] - pub _class: String, - - #[serde( - rename = "defaultValue", - skip_serializing_if = "Option::is_none", - )] - pub default_value: Option, - - #[serde( - rename = "name", - )] - pub name: String, - - #[serde( - rename = "decorators", - skip_serializing_if = "Option::is_none", - )] - pub decorators: Option>, - - #[serde( - rename = "location", - skip_serializing_if = "Option::is_none", - )] - pub location: Option, -} - diff --git a/src/metamodel/utils.rs b/src/metamodel/utils.rs deleted file mode 100644 index 00e8c11..0000000 --- a/src/metamodel/utils.rs +++ /dev/null @@ -1,40 +0,0 @@ -use chrono::{ DateTime, TimeZone, Utc }; -use serde::{ Deserialize, Serialize, Deserializer, Serializer }; - -pub fn serialize_datetime_option(datetime: &Option>, serializer: S) -> Result -where - S: Serializer, -{ - match datetime { - Some(dt) => { - serialize_datetime(&dt, serializer) - }, - _ => unreachable!(), - } -} - -pub fn deserialize_datetime_option<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - match deserialize_datetime(deserializer) { - Ok(result)=>Ok(Some(result)), - Err(error) => Err(error), - } -} - -pub fn deserialize_datetime<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let datetime_str = String::deserialize(deserializer)?; - Utc.datetime_from_str(&datetime_str, "%Y-%m-%dT%H:%M:%S%.3f%Z").map_err(serde::de::Error::custom) -} - -pub fn serialize_datetime(datetime: &chrono::DateTime, serializer: S) -> Result -where - S: Serializer, -{ - let datetime_str = datetime.format("%+").to_string(); - serializer.serialize_str(&datetime_str) -} From f8af3bf99fee32d8b6020c108691d068214097d0 Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Sun, 15 Mar 2026 22:01:18 +0000 Subject: [PATCH 2/9] refactor(*): begin fixing types Signed-off-by: Ertugrul Karademir --- .vscode/extensions.json | 5 + .vscode/settings.json | 3 + README.md | 2 - TODO | 6 + concerto-core/src/error.rs | 7 + concerto-core/src/introspect/mod.rs | 40 +---- concerto-core/src/introspect/model_file.rs | 30 ++++ concerto-core/src/lib.rs | 70 ++++++++- concerto-core/src/model_file.rs | 175 --------------------- concerto-core/src/model_manager.rs | 14 +- concerto-core/src/property_type.rs | 0 concerto-core/src/types.rs | 4 + concerto-core/src/util.rs | 17 -- 13 files changed, 130 insertions(+), 243 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 TODO create mode 100644 concerto-core/src/introspect/model_file.rs delete mode 100644 concerto-core/src/model_file.rs delete mode 100644 concerto-core/src/property_type.rs create mode 100644 concerto-core/src/types.rs delete mode 100644 concerto-core/src/util.rs diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4dfb273 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cac0e10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/README.md b/README.md index 289a880..e950d47 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,6 @@ The project is organized as follows: - `model_manager.rs`: Manages model collections and cross-model validations - `error.rs`: Error types for the library - `validation.rs`: Validation traits and implementations - - `introspect/mod.rs`: Introspection capabilities - - `util.rs`: Utility functions ## Testing diff --git a/TODO b/TODO new file mode 100644 index 0000000..d258d92 --- /dev/null +++ b/TODO @@ -0,0 +1,6 @@ +- Implement Ast model manager +- Model manager should have the base model manager API +- Implement Sumtypes for declarations +- Decorated trait +- Validate trait +- Traits should only have traits each introspect should implement traits diff --git a/concerto-core/src/error.rs b/concerto-core/src/error.rs index e3ad37a..94f581e 100644 --- a/concerto-core/src/error.rs +++ b/concerto-core/src/error.rs @@ -26,4 +26,11 @@ pub enum ConcertoError { /// Generic error #[error("Error: {0}")] GenericError(String), + + /// Ast Error + #[error("Ast not found")] + AstError, + + #[error("JSON parsing error: {0}")] + JsonError(#[from] serde_json::Error), } diff --git a/concerto-core/src/introspect/mod.rs b/concerto-core/src/introspect/mod.rs index c4692b1..219d350 100644 --- a/concerto-core/src/introspect/mod.rs +++ b/concerto-core/src/introspect/mod.rs @@ -1,39 +1,3 @@ -/// Provides introspection capabilities for Concerto models -/// Maps from various introspect-related JavaScript classes -pub mod introspect { - use crate::error::ConcertoError; +pub mod model_file; - /// Checks if a type name is valid in Concerto - pub fn is_valid_type_name(name: &str) -> bool { - if name.is_empty() { - return false; - } - - // Check if name starts with a letter and contains only alphanumeric or underscore - name.chars().next().map_or(false, |c| c.is_ascii_alphabetic()) && - name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') - } - - /// Checks if a namespace is valid in Concerto - pub fn is_valid_namespace(namespace: &str) -> bool { - if namespace.is_empty() { - return false; - } - - // Check if namespace follows dot notation pattern - namespace.split('.').all(|part| is_valid_type_name(part)) - } - - /// Gets the fully qualified name for a declaration - pub fn get_fully_qualified_name(namespace: &str, name: &str) -> Result { - if !is_valid_namespace(namespace) { - return Err(ConcertoError::ValidationError(format!("Invalid namespace: {}", namespace))); - } - - if !is_valid_type_name(name) { - return Err(ConcertoError::ValidationError(format!("Invalid type name: {}", name))); - } - - Ok(format!("{}.{}", namespace, name)) - } -} +pub use model_file::ModelFile; diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs new file mode 100644 index 0000000..3e3ca7e --- /dev/null +++ b/concerto-core/src/introspect/model_file.rs @@ -0,0 +1,30 @@ +use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Model}; + +use crate::{types::Ast, ConcertoError}; + +#[derive(Debug)] +pub struct ModelFile { + ast: Ast, +} + +impl ModelFile { + pub fn new(ast_json: &str) -> Result { + let parsed: Model = serde_json::from_str(ast_json)?; + let ast = Ast(parsed); + Ok(ModelFile { ast }) + } +} + +impl ModelFile { + pub fn get_namespace(&self) -> String { + return self.ast.0.namespace.clone(); + } + + pub fn get_declarations(&self) -> Option> { + return self.ast.0.declarations.clone(); + } + + pub fn validate(&self) -> Result<(), ConcertoError> { + unimplemented!() + } +} diff --git a/concerto-core/src/lib.rs b/concerto-core/src/lib.rs index e3575de..3a5c750 100644 --- a/concerto-core/src/lib.rs +++ b/concerto-core/src/lib.rs @@ -4,14 +4,12 @@ pub mod error; pub mod introspect; pub mod metamodel_validation; -pub mod model_file; pub mod model_manager; pub mod traits; -pub mod util; +pub mod types; pub mod validation; // Re-export the main components -pub use model_file::ModelFile; pub use model_manager::ModelManager; pub use traits::*; @@ -25,3 +23,69 @@ pub use validation::Validate; /// Version of the Concerto core library pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Provides introspection capabilities for Concerto models +/// Maps from various introspect-related JavaScript classes +pub mod introspection { + use crate::error::ConcertoError; + + /// Checks if a type name is valid in Concerto + pub fn is_valid_type_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + + // Check if name starts with a letter and contains only alphanumeric or underscore + name.chars() + .next() + .map_or(false, |c| c.is_ascii_alphabetic()) + && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + } + + /// Checks if a namespace is valid in Concerto + pub fn is_valid_namespace(namespace: &str) -> bool { + if namespace.is_empty() { + return false; + } + + // Check if namespace follows dot notation pattern + namespace.split('.').all(|part| is_valid_type_name(part)) + } + + /// Gets the fully qualified name for a declaration + pub fn get_fully_qualified_name(namespace: &str, name: &str) -> Result { + if !is_valid_namespace(namespace) { + return Err(ConcertoError::ValidationError(format!( + "Invalid namespace: {}", + namespace + ))); + } + + if !is_valid_type_name(name) { + return Err(ConcertoError::ValidationError(format!( + "Invalid type name: {}", + name + ))); + } + + Ok(format!("{}.{}", namespace, name)) + } +} + +/// Utility functions for Concerto +pub mod utility { + use semver::Version; + + /// Checks if a string is a valid semver version + pub fn is_valid_semantic_version(version: &str) -> bool { + Version::parse(version).is_ok() + } + + /// Compares two semver versions + pub fn compare_versions(version1: &str, version2: &str) -> Result { + let v1 = Version::parse(version1).map_err(|e| e.to_string())?; + let v2 = Version::parse(version2).map_err(|e| e.to_string())?; + + Ok(v1.cmp(&v2)) + } +} diff --git a/concerto-core/src/model_file.rs b/concerto-core/src/model_file.rs deleted file mode 100644 index 8207073..0000000 --- a/concerto-core/src/model_file.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::fs; -use std::path::Path; - -use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Import, Model, Property}; - -use crate::error::ConcertoError; -use crate::validation::Validate; - -/// Represents a Concerto model file -/// Using the metamodel structures -#[derive(Debug, Clone)] -pub struct ModelFile { - /// The metamodel representation - pub model: Model, - pub content: String, - pub file_name: String, -} - -impl ModelFile { - /// Creates a new model file - - pub fn new(model: Model, content: String, file_name: String) -> Self { - ModelFile { - model, - content, - file_name, - } - } - - /// Creates a new model file from a namespace and optional version. - /// Convenience constructor for tests and simple use cases. - pub fn from_namespace(namespace: String, version: Option) -> Self { - let model = Model { - _class: "concerto.metamodel@1.0.0.Model".to_string(), - namespace, - source_uri: None, - concerto_version: version, - imports: None, - declarations: None, - decorators: None, - }; - ModelFile { - model, - content: String::new(), - file_name: String::new(), - } - } - pub fn get_name(&self) -> String { - self.model.namespace.clone() - } - - /// Loads a model file from a string - pub fn from_string(content: String) -> Result { - // Note: This is a placeholder - in a real implementation, - // you would parse the string into a Model structure. - // For now, we'll just return an error as this functionality would be - // provided by the JavaScript parser - Err(ConcertoError::ParseError( - "Parsing from string is not implemented in Rust yet. Use the JavaScript parser." - .to_string(), - )) - } - - /// Loads a model file from a file path - pub fn from_file>(path: P) -> Result { - let content = fs::read_to_string(&path) - .map_err(|e| ConcertoError::ParseError(format!("Failed to read file: {}", e)))?; - - Self::from_string(content) - } - - /// Gets the namespace for this model file - pub fn get_namespace(&self) -> &str { - &self.model.namespace - } - - /// Gets the version for this model file - pub fn get_version(&self) -> Option<&String> { - self.model.concerto_version.as_ref() - } - - /// Gets the imports for this model file - pub fn get_imports(&self) -> Vec<&Import> { - match &self.model.imports { - Some(imports) => imports.iter().collect(), - None => Vec::new(), - } - } - - /// Gets the declarations for this model file - pub fn get_declarations(&self) -> Vec<&Declaration> { - match &self.model.declarations { - Some(declarations) => declarations.iter().collect(), - None => Vec::new(), - } - } - - /// Adds an import to this model file - pub fn add_import(&mut self, import: Import) { - if self.model.imports.is_none() { - self.model.imports = Some(Vec::new()); - } - - if let Some(imports) = &mut self.model.imports { - imports.push(import); - } - } - - /// Adds a declaration to this model file - pub fn add_declaration(&mut self, declaration: Declaration) { - if self.model.declarations.is_none() { - self.model.declarations = Some(Vec::new()); - } - - if let Some(declarations) = &mut self.model.declarations { - declarations.push(declaration); - } - } - - /// Helper function to find properties for a declaration - /// In a real implementation, this would use proper type information and downcasting - /// This is a simplified approach just for the test case - pub fn find_properties_for_declaration(&self, declaration: &Declaration) -> Vec { - // For the test case, we have the properties separately in the test file - // We're looking specifically for the test where a property name is "$class" - - let mut properties = Vec::new(); - - // Special case for our test - if declaration.name == "Person" && declaration._class.contains("ConceptDeclaration") { - // In test_conformance_system_property_name, we add a property with name "$class" - properties.push(Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "$class".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }); - } - - properties - } -} - -impl Validate for ModelFile { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate the model structure first - self.model.validate()?; - - // Additional validation for system property names in concept declarations - if let Some(declarations) = &self.model.declarations { - for declaration in declarations { - // For each declaration, check if it's a ConceptDeclaration by its class name - if declaration._class.contains("ConceptDeclaration") { - // In a real implementation, this would use proper downcasting - // For now, we'll manually check any conceptual properties - - // Look for the actual ConceptDeclaration in our test case - // This is a simplified approach just for the test - for prop in self.find_properties_for_declaration(declaration).iter() { - // Validate each property - if prop.name.starts_with('$') { - return Err(ConcertoError::ValidationError( - format!("Invalid field name '{}'. Property names starting with $ are reserved for system use", prop.name) - )); - } - } - } - } - } - - Ok(()) - } -} diff --git a/concerto-core/src/model_manager.rs b/concerto-core/src/model_manager.rs index 51493e8..b9503ff 100644 --- a/concerto-core/src/model_manager.rs +++ b/concerto-core/src/model_manager.rs @@ -3,9 +3,8 @@ use std::collections::HashMap; use concerto_metamodel::concerto_metamodel_1_0_0::*; use crate::error::ConcertoError; -use crate::model_file::ModelFile; +use crate::introspect::ModelFile; use crate::traits::*; -use crate::validation::Validate; /// Manages models and provides validation /// Maps from JavaScript ModelManager class but using metamodel types @@ -33,8 +32,7 @@ impl ModelManager { model_file.validate()?; // Add to our collection - self.models - .insert(model_file.model.namespace.clone(), model_file); + self.models.insert(model_file.get_namespace(), model_file); Ok(()) } @@ -73,10 +71,10 @@ impl ModelManager { // Check each namespace for circular inheritance for model_file in self.models.values() { // Get the namespace name - let namespace = &model_file.model.namespace; + let namespace = &model_file.get_namespace(); // Get all declarations in this namespace - if let Some(declarations) = &model_file.model.declarations { + if let Some(declarations) = &model_file.get_declarations() { // Create a map of class name to superclass name let mut inheritance_map = std::collections::HashMap::new(); @@ -129,7 +127,7 @@ impl ModelManager { // Iterate through all model files for model_file in self.models.values() { // Get declarations for this model file - if let Some(declarations) = &model_file.model.declarations { + if let Some(declarations) = &model_file.get_declarations() { // Check each declaration - using traits to handle different types for declaration in declarations { // Check for concept declarations that might have super types @@ -227,7 +225,7 @@ impl ModelManager { }; // Check if type exists in this namespace using the DeclarationBase trait - if let Some(declarations) = &model_file.model.declarations { + if let Some(declarations) = &model_file.get_declarations() { for decl in declarations { // Use the DeclarationBase trait to get name regardless of declaration type if decl.name == type_id.name { diff --git a/concerto-core/src/property_type.rs b/concerto-core/src/property_type.rs deleted file mode 100644 index e69de29..0000000 diff --git a/concerto-core/src/types.rs b/concerto-core/src/types.rs new file mode 100644 index 0000000..d6660e6 --- /dev/null +++ b/concerto-core/src/types.rs @@ -0,0 +1,4 @@ +use concerto_metamodel::concerto_metamodel_1_0_0::Model; + +#[derive(Debug)] +pub struct Ast(pub Model); diff --git a/concerto-core/src/util.rs b/concerto-core/src/util.rs deleted file mode 100644 index 2e4d9b3..0000000 --- a/concerto-core/src/util.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Utility functions for Concerto -pub mod util { - use semver::Version; - - /// Checks if a string is a valid semver version - pub fn is_valid_semantic_version(version: &str) -> bool { - Version::parse(version).is_ok() - } - - /// Compares two semver versions - pub fn compare_versions(version1: &str, version2: &str) -> Result { - let v1 = Version::parse(version1).map_err(|e| e.to_string())?; - let v2 = Version::parse(version2).map_err(|e| e.to_string())?; - - Ok(v1.cmp(&v2)) - } -} From 8cb00732076c42e37e94fe8cc40aaeadf1e2c8db Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Tue, 17 Mar 2026 12:36:41 +0000 Subject: [PATCH 3/9] feat: basic model file Signed-off-by: Ertugrul Karademir --- TODO | 4 + .../examples/assets/concerto_models/basic.cto | 5 + .../assets/concerto_models/basic.json | 51 ++++ concerto-core/src/error.rs | 3 + concerto-core/src/introspect/model_file.rs | 52 +++- concerto-core/tests/conformance_tests.rs | 233 ------------------ concerto-core/tests/declaration_tests.rs | 101 -------- concerto-core/tests/enum_tests.rs | 129 ---------- concerto-core/tests/map_tests.rs | 141 ----------- concerto-core/tests/namespace_tests.rs | 102 -------- concerto-core/tests/scalar_tests.rs | 121 --------- 11 files changed, 112 insertions(+), 830 deletions(-) create mode 100644 concerto-core/examples/assets/concerto_models/basic.cto create mode 100644 concerto-core/examples/assets/concerto_models/basic.json delete mode 100644 concerto-core/tests/conformance_tests.rs delete mode 100644 concerto-core/tests/declaration_tests.rs delete mode 100644 concerto-core/tests/enum_tests.rs delete mode 100644 concerto-core/tests/map_tests.rs delete mode 100644 concerto-core/tests/namespace_tests.rs delete mode 100644 concerto-core/tests/scalar_tests.rs diff --git a/TODO b/TODO index d258d92..ab702cf 100644 --- a/TODO +++ b/TODO @@ -4,3 +4,7 @@ - Decorated trait - Validate trait - Traits should only have traits each introspect should implement traits + +Visitor pattern + +- All introspect classes implements visitor pattern. Maybe add a visitor trait. diff --git a/concerto-core/examples/assets/concerto_models/basic.cto b/concerto-core/examples/assets/concerto_models/basic.cto new file mode 100644 index 0000000..e1ba17d --- /dev/null +++ b/concerto-core/examples/assets/concerto_models/basic.cto @@ -0,0 +1,5 @@ +namespace com.example.foo@1.0.0 + +concept Foo { + o String bar +} diff --git a/concerto-core/examples/assets/concerto_models/basic.json b/concerto-core/examples/assets/concerto_models/basic.json new file mode 100644 index 0000000..20ff1a8 --- /dev/null +++ b/concerto-core/examples/assets/concerto_models/basic.json @@ -0,0 +1,51 @@ +{ + "$class": "concerto.metamodel@1.0.0.Model", + "decorators": [], + "namespace": "com.example.foo@1.0.0", + "imports": [], + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Foo", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "bar", + "isArray": false, + "isOptional": false, + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 51, + "line": 4, + "column": 5, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 64, + "line": 5, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ], + "location": { + "$class": "concerto.metamodel@1.0.0.Range", + "start": { + "offset": 33, + "line": 3, + "column": 1, + "$class": "concerto.metamodel@1.0.0.Position" + }, + "end": { + "offset": 65, + "line": 5, + "column": 2, + "$class": "concerto.metamodel@1.0.0.Position" + } + } + } + ] +} \ No newline at end of file diff --git a/concerto-core/src/error.rs b/concerto-core/src/error.rs index 94f581e..4e0852b 100644 --- a/concerto-core/src/error.rs +++ b/concerto-core/src/error.rs @@ -33,4 +33,7 @@ pub enum ConcertoError { #[error("JSON parsing error: {0}")] JsonError(#[from] serde_json::Error), + + #[error("Invalid namespace: {0}")] + InvalidNS(String), } diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index 3e3ca7e..a8fb3b5 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -5,26 +5,72 @@ use crate::{types::Ast, ConcertoError}; #[derive(Debug)] pub struct ModelFile { ast: Ast, + pub namespace: String, + pub version: String, } impl ModelFile { pub fn new(ast_json: &str) -> Result { let parsed: Model = serde_json::from_str(ast_json)?; let ast = Ast(parsed); - Ok(ModelFile { ast }) + ast.into() } } impl ModelFile { pub fn get_namespace(&self) -> String { - return self.ast.0.namespace.clone(); + self.namespace.clone() } pub fn get_declarations(&self) -> Option> { - return self.ast.0.declarations.clone(); + self.ast.0.declarations.clone() } pub fn validate(&self) -> Result<(), ConcertoError> { unimplemented!() } } + +impl From for Result { + fn from(value: Ast) -> Self { + let (namespace, version) = parse_namespace_and_version(&value)?; + Ok(ModelFile { + ast: value, + namespace: namespace, + version: version, + }) + } +} + +fn parse_namespace_and_version(ast: &Ast) -> Result<(String, String), ConcertoError> { + let x = ast.0.namespace.as_str(); + let parts: Vec<&str> = x.split('@').collect(); + // For now, assume all NS will have namespace@version + if parts.len() != 2 { + Err(ConcertoError::InvalidNS(x.to_string())) + } else { + Ok((parts[0].to_string(), parts[1].to_string())) + } +} + +mod test { + use crate::types::Ast; + use concerto_metamodel::concerto_metamodel_1_0_0::Model; + + fn make_ast() -> Ast { + let basic_ast = include_str!("../../examples/assets/concerto_models/basic.json"); + let parsed: Model = serde_json::from_str(basic_ast).expect("Cannot parse basic.json"); + Ast(parsed) + } + + #[test] + fn test_parse_ns() { + let ast = make_ast(); + if let Ok((ns, version)) = super::parse_namespace_and_version(&ast) { + assert_eq!(ns, "com.example.foo".to_string(), "Can parse namespace"); + assert_eq!(version, "1.0.0".to_string(), "Can parse version"); + } else { + assert!(false); + } + } +} diff --git a/concerto-core/tests/conformance_tests.rs b/concerto-core/tests/conformance_tests.rs deleted file mode 100644 index 88f1042..0000000 --- a/concerto-core/tests/conformance_tests.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Integration tests for validation rules - -use concerto_core::model_file::ModelFile; -use concerto_core::model_manager::ModelManager; -use concerto_core::validation::Validate; -use concerto_metamodel::concerto_metamodel_1_0_0::{ - ConceptDeclaration, Declaration, Property, TypeIdentifier, -}; - -#[test] -fn test_conformance_system_property_name() { - // Test: Property name uses system-reserved name - - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Create a concept declaration with a property that has a reserved name - let concept_decl = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: None, - properties: vec![Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "$class".to_string(), // System-reserved name - is_array: false, - is_optional: false, - decorators: None, - location: None, - }], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Convert ConceptDeclaration to Declaration - let declaration = Declaration { - _class: concept_decl._class.clone(), - name: concept_decl.name.clone(), - decorators: concept_decl.decorators.clone(), - location: concept_decl.location.clone(), - }; - - // Add the declaration to the model file - model_file.add_declaration(declaration); - - // Should fail validation with message about invalid field name - let result = model_file.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Invalid")); -} - -#[test] -fn test_conformance_duplicate_declaration() { - // Test: Duplicate declaration names in the same model - - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // First declaration - let concept_decl1 = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: None, - properties: vec![], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Second declaration with same name - let concept_decl2 = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: None, - properties: vec![], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Convert ConceptDeclarations to Declarations - let declaration1 = Declaration { - _class: concept_decl1._class.clone(), - name: concept_decl1.name.clone(), - decorators: concept_decl1.decorators.clone(), - location: concept_decl1.location.clone(), - }; - - let declaration2 = Declaration { - _class: concept_decl2._class.clone(), - name: concept_decl2.name.clone(), - decorators: concept_decl2.decorators.clone(), - location: concept_decl2.location.clone(), - }; - - // Add both declarations to the model file - model_file.add_declaration(declaration1); - model_file.add_declaration(declaration2); - - // Should fail validation with message about duplicate class name - let result = model_file.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Duplicate")); -} - -#[test] -#[ignore] -fn test_conformance_circular_inheritance() { - // Test: Circular inheritance detection - - // Create a model manager - let mut model_manager = ModelManager::new(true); - - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Person extends Employee - let concept_decl1 = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: Some(TypeIdentifier { - _class: "concerto.metamodel@1.0.0.TypeIdentifier".to_string(), - name: "Employee".to_string(), - namespace: Some("org.example".to_string()), - }), - properties: vec![], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Employee extends Person (circular) - let concept_decl2 = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Employee".to_string(), - super_type: Some(TypeIdentifier { - _class: "concerto.metamodel@1.0.0.TypeIdentifier".to_string(), - name: "Person".to_string(), - namespace: Some("org.example".to_string()), - }), - properties: vec![], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Convert ConceptDeclarations to Declarations - let declaration1 = Declaration { - _class: concept_decl1._class.clone(), - name: concept_decl1.name.clone(), - decorators: concept_decl1.decorators.clone(), - location: concept_decl1.location.clone(), - }; - - let declaration2 = Declaration { - _class: concept_decl2._class.clone(), - name: concept_decl2.name.clone(), - decorators: concept_decl2.decorators.clone(), - location: concept_decl2.location.clone(), - }; - - // Add both declarations to the model file - model_file.add_declaration(declaration1); - model_file.add_declaration(declaration2); - - // Add the model file to the model manager - let result = model_manager.add_model_file(model_file); - assert!(result.is_ok()); - - // Should fail validation with message about circular inheritance - let result = model_manager.validate_models(); - assert!(result.is_err()); - - // Get the error message - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains("circular") || error_message.contains("Circular")); -} - -#[test] -fn test_conformance_property_duplicate_names() { - // Test: Duplicate property names in a declaration - - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Create a concept with duplicate property names - let concept_decl = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: None, - properties: vec![ - // First property - Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "name".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }, - // Duplicate property with same name - Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "name".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }, - ], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // First, directly validate the ConceptDeclaration which should fail due to duplicate properties - let concept_result = concept_decl.validate(); - assert!(concept_result.is_err()); - assert!(concept_result - .unwrap_err() - .to_string() - .contains("Duplicate property")); - - // The ModelFile test is not needed since we've already verified the validation works directly - // If needed in a real implementation, we would need to ensure ModelFile validation - // properly traverses and validates the full ConceptDeclaration structure -} diff --git a/concerto-core/tests/declaration_tests.rs b/concerto-core/tests/declaration_tests.rs deleted file mode 100644 index 27f57ef..0000000 --- a/concerto-core/tests/declaration_tests.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Basic tests for declaration validation - -use concerto_core::validation::Validate; -use concerto_metamodel::concerto_metamodel_1_0_0::{ConceptDeclaration, Declaration, Property}; - -#[test] -fn test_declaration_validation() { - // Create a valid declaration - let concept_decl = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "Person".to_string(), - super_type: None, - properties: vec![ - Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "firstName".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }, - Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "lastName".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }, - ], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Convert ConceptDeclaration to Declaration - let declaration = Declaration { - _class: concept_decl._class.clone(), - name: concept_decl.name.clone(), - decorators: concept_decl.decorators.clone(), - location: concept_decl.location.clone(), - }; - - // Validate the declaration - let result = declaration.validate(); - assert!(result.is_ok()); - - // Test invalid declaration (empty name) - let invalid_concept_decl = ConceptDeclaration { - _class: "concerto.metamodel@1.0.0.ConceptDeclaration".to_string(), - name: "".to_string(), - super_type: None, - properties: vec![], - decorators: None, - is_abstract: false, - identified: None, - location: None, - }; - - // Convert to Declaration - let invalid_declaration = Declaration { - _class: invalid_concept_decl._class.clone(), - name: invalid_concept_decl.name.clone(), - decorators: invalid_concept_decl.decorators.clone(), - location: invalid_concept_decl.location.clone(), - }; - - let result = invalid_declaration.validate(); - assert!(result.is_err()); -} - -#[test] -fn test_property_validation() { - // Create a valid property - let property = Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "firstName".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }; - - // Validate the property - let result = property.validate(); - assert!(result.is_ok()); - - // Test invalid property (empty name) - let invalid_property = Property { - _class: "concerto.metamodel@1.0.0.StringProperty".to_string(), - name: "".to_string(), - is_array: false, - is_optional: false, - decorators: None, - location: None, - }; - - let result = invalid_property.validate(); - assert!(result.is_err()); -} diff --git a/concerto-core/tests/enum_tests.rs b/concerto-core/tests/enum_tests.rs deleted file mode 100644 index 383fd9c..0000000 --- a/concerto-core/tests/enum_tests.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Tests for enum validation - -use concerto_core::traits::DeclarationBase; -use concerto_core::validation::Validate; -use concerto_core::*; -use concerto_metamodel::concerto_metamodel_1_0_0::{ - Declaration, Decorator, EnumDeclaration, EnumProperty, -}; - -// Helper function to create an EnumProperty -fn create_enum_property(name: &str) -> EnumProperty { - EnumProperty { - _class: "concerto.metamodel@1.0.0.EnumProperty".to_string(), - name: name.to_string(), - decorators: None, - location: None, - } -} - -// Helper function to create an EnumDeclaration -fn create_enum_declaration(name: &str, values: Vec<&str>) -> EnumDeclaration { - let properties = values - .iter() - .map(|v| create_enum_property(v)) - .collect::>(); - - EnumDeclaration { - _class: "concerto.metamodel@1.0.0.EnumDeclaration".to_string(), - name: name.to_string(), - properties: properties, - decorators: None, - location: None, - } -} - -#[test] -#[ignore] -fn test_enum_with_duplicate_values() { - // Create an enum with duplicate values - let enum_decl = create_enum_declaration("Color", vec!["RED", "GREEN", "RED"]); - - // Convert to Declaration - let declaration = Declaration { - _class: enum_decl._class.clone(), - name: enum_decl.name.clone(), - decorators: enum_decl.decorators.clone(), - location: enum_decl.location.clone(), - }; - - // Should fail validation with "Duplicate enum value" - let result = declaration.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Duplicate enum")); -} - -#[test] -fn test_enum_with_valid_values() { - // Create an enum with valid values - let enum_decl = create_enum_declaration("Color", vec!["RED", "GREEN", "BLUE"]); - - // Convert to Declaration - let declaration = Declaration { - _class: enum_decl._class.clone(), - name: enum_decl.name.clone(), - decorators: enum_decl.decorators.clone(), - location: enum_decl.location.clone(), - }; - - // Should pass validation - let result = declaration.validate(); - assert!(result.is_ok()); -} - -#[test] -#[ignore] -fn test_enum_with_empty_values() { - // Create an enum with no values - let enum_decl = EnumDeclaration { - _class: "concerto.metamodel@1.0.0.EnumDeclaration".to_string(), - name: "EmptyEnum".to_string(), - properties: vec![], - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: enum_decl._class.clone(), - name: enum_decl.name.clone(), - decorators: enum_decl.decorators.clone(), - location: enum_decl.location.clone(), - }; - - // Should fail validation with a message about needing at least one value - // Note: The exact error message may differ from the original - let result = declaration.validate(); - assert!(result.is_err()); -} - -#[test] -#[ignore] -fn test_enum_with_invalid_property_name() { - // Create an enum with an invalid property name - let enum_decl = EnumDeclaration { - _class: "concerto.metamodel@1.0.0.EnumDeclaration".to_string(), - name: "InvalidEnum".to_string(), - properties: vec![EnumProperty { - _class: "concerto.metamodel@1.0.0.EnumProperty".to_string(), - name: "123INVALID".to_string(), // Invalid name (starts with number) - decorators: None, - location: None, - }], - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: enum_decl._class.clone(), - name: enum_decl.name.clone(), - decorators: enum_decl.decorators.clone(), - location: enum_decl.location.clone(), - }; - - // Should fail validation with message about invalid identifier - let result = declaration.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("not a valid")); -} diff --git a/concerto-core/tests/map_tests.rs b/concerto-core/tests/map_tests.rs deleted file mode 100644 index 2d07428..0000000 --- a/concerto-core/tests/map_tests.rs +++ /dev/null @@ -1,141 +0,0 @@ -//! Tests for map validation - -use concerto_core::validation::Validate; -use concerto_metamodel::concerto_metamodel_1_0_0::{ - Declaration, MapDeclaration, MapKeyType, MapValueType, -}; - -#[test] -fn test_map_with_valid_string_key() { - // Create a map with String key type - let map_decl = MapDeclaration { - _class: "concerto.metamodel@1.0.0.MapDeclaration".to_string(), - name: "StringKeyMap".to_string(), - key: MapKeyType { - _class: "concerto.metamodel@1.0.0.StringMapKeyType".to_string(), - decorators: None, - location: None, - }, - value: MapValueType { - _class: "concerto.metamodel@1.0.0.IntegerMapValueType".to_string(), - decorators: None, - location: None, - }, - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: map_decl._class.clone(), - name: map_decl.name.clone(), - decorators: map_decl.decorators.clone(), - location: map_decl.location.clone(), - }; - - // Should pass validation - let result = declaration.validate(); - assert!(result.is_ok()); -} - -#[test] -fn test_map_with_datetime_key() { - // Create a map with DateTime key type - let map_decl = MapDeclaration { - _class: "concerto.metamodel@1.0.0.MapDeclaration".to_string(), - name: "DateTimeKeyMap".to_string(), - key: MapKeyType { - _class: "concerto.metamodel@1.0.0.DateTimeMapKeyType".to_string(), - decorators: None, - location: None, - }, - value: MapValueType { - _class: "concerto.metamodel@1.0.0.StringMapValueType".to_string(), - decorators: None, - location: None, - }, - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: map_decl._class.clone(), - name: map_decl.name.clone(), - decorators: map_decl.decorators.clone(), - location: map_decl.location.clone(), - }; - - // Should pass validation - let result = declaration.validate(); - assert!(result.is_ok()); -} - -#[test] -fn test_map_with_missing_key() { - // Create a map with invalid name (empty) - let map_decl = MapDeclaration { - _class: "concerto.metamodel@1.0.0.MapDeclaration".to_string(), - name: "".to_string(), // Empty name should fail validation - key: MapKeyType { - _class: "concerto.metamodel@1.0.0.StringMapKeyType".to_string(), - decorators: None, - location: None, - }, - value: MapValueType { - _class: "concerto.metamodel@1.0.0.StringMapValueType".to_string(), - decorators: None, - location: None, - }, - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: map_decl._class.clone(), - name: map_decl.name.clone(), - decorators: map_decl.decorators.clone(), - location: map_decl.location.clone(), - }; - - // Should fail validation due to empty name - let result = declaration.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("name")); -} - -#[test] -#[ignore] -fn test_map_with_missing_value() { - // Create a map with invalid class name - let map_decl = MapDeclaration { - _class: "".to_string(), // Empty class name should fail validation - name: "MissingValueTypeMap".to_string(), - key: MapKeyType { - _class: "concerto.metamodel@1.0.0.StringMapKeyType".to_string(), - decorators: None, - location: None, - }, - value: MapValueType { - _class: "concerto.metamodel@1.0.0.StringMapValueType".to_string(), - decorators: None, - location: None, - }, - decorators: None, - location: None, - }; - - // Convert to Declaration - let declaration = Declaration { - _class: map_decl._class.clone(), - name: map_decl.name.clone(), - decorators: map_decl.decorators.clone(), - location: map_decl.location.clone(), - }; - - // Should fail validation due to empty class name - let result = declaration.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("class")); -} diff --git a/concerto-core/tests/namespace_tests.rs b/concerto-core/tests/namespace_tests.rs deleted file mode 100644 index 894b0e7..0000000 --- a/concerto-core/tests/namespace_tests.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Tests for namespace and import validation - -use concerto_core::model_file::ModelFile; -use concerto_core::validation::Validate; -use concerto_core::*; -use concerto_metamodel::concerto_metamodel_1_0_0::Import; - -#[test] -fn test_valid_namespace_format() { - // Create a model file with a valid namespace format - let model_file = ModelFile::from_namespace("org.example.model".to_string(), Some("1.0.0".to_string())); - - // Should pass validation - let result = model_file.validate(); - assert!(result.is_ok()); -} - -#[test] -fn test_empty_namespace() { - // Create a model file with an empty namespace - let model_file = ModelFile::from_namespace("".to_string(), Some("1.0.0".to_string())); - - // Should fail validation with message about empty namespace - let result = model_file.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("cannot be empty")); -} - -#[test] -#[ignore] -fn test_duplicate_namespace_imports() { - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Add duplicate imports - let import1 = Import { - _class: "concerto.metamodel@1.0.0.Import".to_string(), - namespace: "org.external.types".to_string(), - uri: None, - }; - - let import2 = Import { - _class: "concerto.metamodel@1.0.0.Import".to_string(), - namespace: "org.external.types".to_string(), - uri: None, - }; - - model_file.add_import(import1); - model_file.add_import(import2); - - // Should fail validation with message about duplicate imports - // Note: This test assumes the validation code checks for duplicate imports. - // If it doesn't, you might need to add this validation or adapt the test. - let result = model_file.validate(); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Duplicate import")); -} - -#[test] -fn test_valid_imports() { - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Add valid imports with different namespaces - let import1 = Import { - _class: "concerto.metamodel@1.0.0.Import".to_string(), - namespace: "org.external.types1".to_string(), - uri: None, - }; - - let import2 = Import { - _class: "concerto.metamodel@1.0.0.Import".to_string(), - namespace: "org.external.types2".to_string(), - uri: None, - }; - - model_file.add_import(import1); - model_file.add_import(import2); - - // Should pass validation - let result = model_file.validate(); - assert!(result.is_ok()); -} - -#[test] -fn test_import_with_version() { - // Create a model file - let mut model_file = ModelFile::from_namespace("org.example".to_string(), Some("1.0.0".to_string())); - - // Add import with version - let import = Import { - _class: "concerto.metamodel@1.0.0.Import".to_string(), - namespace: "org.external.types".to_string(), - uri: None, - }; - - model_file.add_import(import); - - // Should pass validation - let result = model_file.validate(); - assert!(result.is_ok()); -} diff --git a/concerto-core/tests/scalar_tests.rs b/concerto-core/tests/scalar_tests.rs deleted file mode 100644 index a97f62c..0000000 --- a/concerto-core/tests/scalar_tests.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Tests for scalar validation - -use concerto_core::validation::Validate; -use concerto_metamodel::concerto_metamodel_1_0_0::{ - Declaration, IntegerDomainValidator, IntegerScalar, StringLengthValidator, - StringRegexValidator, StringScalar, -}; - -#[test] -fn test_scalar_with_valid_number_bounds() { - // Create a scalar with valid number bounds - let scalar_decl = IntegerScalar { - _class: "concerto.metamodel@1.0.0.IntegerScalar".to_string(), - name: "Percentage".to_string(), - validator: Some(IntegerDomainValidator { - _class: "concerto.metamodel@1.0.0.IntegerDomainValidator".to_string(), - lower: Some(0), - upper: Some(100), - }), - decorators: None, - location: None, - default_value: None, - }; - - // Should pass validation - let result = scalar_decl.validate(); - assert!(result.is_ok()); -} - -#[test] -#[ignore] -fn test_scalar_with_invalid_number_bounds() { - // Create a scalar with lower > upper - let scalar_decl = IntegerScalar { - _class: "concerto.metamodel@1.0.0.IntegerScalar".to_string(), - name: "InvalidRange".to_string(), - validator: Some(IntegerDomainValidator { - _class: "concerto.metamodel@1.0.0.IntegerDomainValidator".to_string(), - lower: Some(100), - upper: Some(0), - }), - decorators: None, - location: None, - default_value: None, - }; - - // Create a declaration - let declaration = Declaration { - _class: scalar_decl._class.clone(), - name: scalar_decl.name.clone(), - decorators: scalar_decl.decorators.clone(), - location: scalar_decl.location.clone(), - }; - - // Should fail validation with message about bounds - // Note: The exact validation message may differ based on implementation - let result = declaration.validate(); - assert!(result.is_err()); - // The error message should contain something about bounds - assert!(result.unwrap_err().to_string().contains("bound")); -} - -#[test] -fn test_scalar_with_string_validator() { - // Create a scalar with string validator - let scalar_decl = StringScalar { - _class: "concerto.metamodel@1.0.0.StringScalar".to_string(), - name: "Email".to_string(), - validator: Some(StringRegexValidator { - _class: "concerto.metamodel@1.0.0.StringRegexValidator".to_string(), - pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$".to_string(), - flags: "".to_string(), - }), - decorators: None, - location: None, - default_value: None, - length_validator: None, - }; - - // Create a declaration - let declaration = Declaration { - _class: scalar_decl._class.clone(), - name: scalar_decl.name.clone(), - decorators: scalar_decl.decorators.clone(), - location: scalar_decl.location.clone(), - }; - - // Should pass validation - let result = declaration.validate(); - assert!(result.is_ok()); -} - -#[test] -fn test_scalar_with_invalid_name() { - // Create a scalar with invalid name - let scalar_decl = StringScalar { - _class: "concerto.metamodel@1.0.0.StringScalar".to_string(), - name: "123Invalid".to_string(), - validator: None, - decorators: None, - location: None, - default_value: None, - length_validator: None, - }; - - // Create a declaration - let declaration = Declaration { - _class: scalar_decl._class.clone(), - name: scalar_decl.name.clone(), - decorators: scalar_decl.decorators.clone(), - location: scalar_decl.location.clone(), - }; - - // Should fail validation with message about invalid identifier - let result = declaration.validate(); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("not a valid identifier")); -} From 13f6d20a8ec7ec06b0c3ea4a99dd6e3217f26c18 Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Wed, 18 Mar 2026 00:31:24 +0000 Subject: [PATCH 4/9] refactor: better align with JS RT Signed-off-by: Ertugrul Karademir --- Cargo.lock | 7 --- concerto-core/Cargo.toml | 1 - concerto-core/src/introspect/model_file.rs | 50 ++------------- concerto-core/src/lib.rs | 3 +- concerto-core/src/metamodel_validation.rs | 16 +---- concerto-core/src/model_manager.rs | 5 +- concerto-core/src/traits.rs | 10 +-- concerto-core/src/util.rs | 71 ++++++++++++++++++++++ concerto-core/src/validation.rs | 8 --- 9 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 concerto-core/src/util.rs delete mode 100644 concerto-core/src/validation.rs diff --git a/Cargo.lock b/Cargo.lock index fcc5e9a..909d787 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,7 +81,6 @@ version = "0.1.0" dependencies = [ "chrono", "concerto-metamodel", - "lazy_static", "log", "regex", "semver", @@ -136,12 +135,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.174" diff --git a/concerto-core/Cargo.toml b/concerto-core/Cargo.toml index 41786fe..7b1bedd 100644 --- a/concerto-core/Cargo.toml +++ b/concerto-core/Cargo.toml @@ -13,5 +13,4 @@ thiserror = "1.0" regex = "1.10" semver = "1.0" log = "0.4" -lazy_static = "1.4" chrono = "0.4" diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index a8fb3b5..1d53d4a 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -4,9 +4,7 @@ use crate::{types::Ast, ConcertoError}; #[derive(Debug)] pub struct ModelFile { - ast: Ast, - pub namespace: String, - pub version: String, + inner: Ast, } impl ModelFile { @@ -18,12 +16,12 @@ impl ModelFile { } impl ModelFile { - pub fn get_namespace(&self) -> String { - self.namespace.clone() + pub fn get_namespace(&self) -> &str { + &self.inner.0.namespace } pub fn get_declarations(&self) -> Option> { - self.ast.0.declarations.clone() + self.inner.0.declarations.clone() } pub fn validate(&self) -> Result<(), ConcertoError> { @@ -33,44 +31,6 @@ impl ModelFile { impl From for Result { fn from(value: Ast) -> Self { - let (namespace, version) = parse_namespace_and_version(&value)?; - Ok(ModelFile { - ast: value, - namespace: namespace, - version: version, - }) - } -} - -fn parse_namespace_and_version(ast: &Ast) -> Result<(String, String), ConcertoError> { - let x = ast.0.namespace.as_str(); - let parts: Vec<&str> = x.split('@').collect(); - // For now, assume all NS will have namespace@version - if parts.len() != 2 { - Err(ConcertoError::InvalidNS(x.to_string())) - } else { - Ok((parts[0].to_string(), parts[1].to_string())) - } -} - -mod test { - use crate::types::Ast; - use concerto_metamodel::concerto_metamodel_1_0_0::Model; - - fn make_ast() -> Ast { - let basic_ast = include_str!("../../examples/assets/concerto_models/basic.json"); - let parsed: Model = serde_json::from_str(basic_ast).expect("Cannot parse basic.json"); - Ast(parsed) - } - - #[test] - fn test_parse_ns() { - let ast = make_ast(); - if let Ok((ns, version)) = super::parse_namespace_and_version(&ast) { - assert_eq!(ns, "com.example.foo".to_string(), "Can parse namespace"); - assert_eq!(version, "1.0.0".to_string(), "Can parse version"); - } else { - assert!(false); - } + Ok(ModelFile { inner: value }) } } diff --git a/concerto-core/src/lib.rs b/concerto-core/src/lib.rs index 3a5c750..a39352e 100644 --- a/concerto-core/src/lib.rs +++ b/concerto-core/src/lib.rs @@ -7,7 +7,7 @@ pub mod metamodel_validation; pub mod model_manager; pub mod traits; pub mod types; -pub mod validation; +pub mod util; // Re-export the main components pub use model_manager::ModelManager; @@ -19,7 +19,6 @@ pub use concerto_metamodel::concerto_metamodel_1_0_0::{ }; pub use error::ConcertoError; -pub use validation::Validate; /// Version of the Concerto core library pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/concerto-core/src/metamodel_validation.rs b/concerto-core/src/metamodel_validation.rs index 478d373..8b4c130 100644 --- a/concerto-core/src/metamodel_validation.rs +++ b/concerto-core/src/metamodel_validation.rs @@ -1,20 +1,7 @@ use concerto_metamodel::concerto_metamodel_1_0_0::*; use crate::error::ConcertoError; -use crate::validation::Validate; - -use regex::Regex; - -/// Checks if a string is a valid identifier name in Concerto -/// Identifiers must start with a letter and contain only letters, numbers, or underscores -pub fn is_valid_identifier(name: &str) -> bool { - lazy_static::lazy_static! { - // TODO use the full regex from the Concerto spec - static ref IDENTIFIER_REGEX: Regex = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*$").unwrap(); - } - - IDENTIFIER_REGEX.is_match(name) -} +use crate::traits::Validate; impl Validate for Model { fn validate(&self) -> Result<(), ConcertoError> { @@ -100,6 +87,7 @@ impl Validate for Decorator { // Use DeclarationValidator trait for implementing Validate use crate::traits::DeclarationValidator; +use crate::util::is_valid_identifier; impl Validate for AssetDeclaration { fn validate(&self) -> Result<(), ConcertoError> { diff --git a/concerto-core/src/model_manager.rs b/concerto-core/src/model_manager.rs index b9503ff..14a66ad 100644 --- a/concerto-core/src/model_manager.rs +++ b/concerto-core/src/model_manager.rs @@ -29,10 +29,11 @@ impl ModelManager { /// Adds a model file to the model manager pub fn add_model_file(&mut self, model_file: ModelFile) -> Result<(), ConcertoError> { // Basic validation of the model file (without cross-model validation) - model_file.validate()?; + // model_file.validate()?; // Add to our collection - self.models.insert(model_file.get_namespace(), model_file); + self.models + .insert(model_file.get_namespace().to_string(), model_file); Ok(()) } diff --git a/concerto-core/src/traits.rs b/concerto-core/src/traits.rs index cb585ea..d1930c0 100644 --- a/concerto-core/src/traits.rs +++ b/concerto-core/src/traits.rs @@ -1,7 +1,11 @@ use concerto_metamodel::concerto_metamodel_1_0_0::*; -use crate::validation::Validate; -use crate::{metamodel_validation::is_valid_identifier, ConcertoError, ModelManager}; +use crate::{util::is_valid_identifier, ConcertoError, ModelManager}; + +pub trait Validate { + /// Validates the component + fn validate(&self) -> Result<(), ConcertoError>; +} /// Base trait for declaration types pub trait DeclarationBase { @@ -525,7 +529,6 @@ impl CommonDeclarationValidator { /// Helper function to validate a Concerto identifier pub fn validate_identifier(name: &str) -> Result<(), ConcertoError> { // This should use the same validation logic as in metamodel_validation.rs - use crate::metamodel_validation::is_valid_identifier; if !is_valid_identifier(name) { return Err(ConcertoError::ValidationError( @@ -540,7 +543,6 @@ impl CommonDeclarationValidator { if let Some(decs) = decorators { for decorator in decs { // Use the Validate trait for each decorator - use crate::validation::Validate; decorator.validate()?; } } diff --git a/concerto-core/src/util.rs b/concerto-core/src/util.rs new file mode 100644 index 0000000..e9bc3f1 --- /dev/null +++ b/concerto-core/src/util.rs @@ -0,0 +1,71 @@ +use regex::Regex; +use std::sync::LazyLock; + +use crate::{types::Ast, ConcertoError}; + +/// Parses a potentially versioned namespace into +/// its name and version parts. The version of the namespace +/// (if present) is parsed using semver.parse. +pub fn parse_namespace_and_version(ast: &Ast) -> Result<(String, String), ConcertoError> { + let x = ast.0.namespace.as_str(); + let parts: Vec<&str> = x.split('@').collect(); + // For now, assume all NS will have to have namespace@version + if parts.len() != 2 { + Err(ConcertoError::InvalidNS(x.to_string())) + } else { + Ok((parts[0].to_string(), parts[1].to_string())) + } +} + +const ID_REGEX: &str = r"^(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\u{005C}u[0-9A-Fa-f]{4})(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\u{005C}u[0-9A-Fa-f]{4}|\p{Mn}|\p{Mc}|\p{Nd}|\p{Pc}|\u{200C}|\u{200D})*$"; + +static IDENTIFIER_REGEX: LazyLock = + LazyLock::new(|| Regex::new(ID_REGEX).expect("Invalid ID regex")); + +/// Checks if a string is a valid identifier name in Concerto +/// Identifiers must start with a letter and contain only letters, numbers, or underscores +pub fn is_valid_identifier(name: &str) -> bool { + IDENTIFIER_REGEX.is_match(name) +} + +#[cfg(test)] +mod test { + use crate::types::Ast; + use concerto_metamodel::concerto_metamodel_1_0_0::Model; + + #[allow(unused)] + fn make_ast() -> Ast { + let basic_ast = include_str!("../examples/assets/concerto_models/basic.json"); + let parsed: Model = serde_json::from_str(basic_ast).expect("Cannot parse basic.json"); + Ast(parsed) + } + + #[test] + fn test_parse_ns() { + let ast = make_ast(); + if let Ok((ns, version)) = super::parse_namespace_and_version(&ast) { + assert_eq!(ns, "com.example.foo".to_string(), "Can parse namespace"); + assert_eq!(version, "1.0.0".to_string(), "Can parse version"); + } else { + assert!(false); + } + } + + #[test] + fn test_valid_identifiers() { + assert!(super::is_valid_identifier("simple_var")); + assert!(super::is_valid_identifier("$dollar")); + assert!(super::is_valid_identifier("π_ratio")); + assert!(super::is_valid_identifier("变量")); + assert!(super::is_valid_identifier(r"a\u0061")); // Literal backslash-u-hex + } + + #[test] + fn test_invalid_identifiers() { + assert!(!super::is_valid_identifier("1stVariable")); // Starts with digit + assert!(!super::is_valid_identifier("var-name")); // Contains hyphen + assert!(!super::is_valid_identifier("no space")); // Contains space + assert!(!super::is_valid_identifier("heavy+metal")); // Contains plus sign + assert!(!super::is_valid_identifier("🚀_star")); // Contains emoji + } +} diff --git a/concerto-core/src/validation.rs b/concerto-core/src/validation.rs deleted file mode 100644 index 9c70e84..0000000 --- a/concerto-core/src/validation.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::error::ConcertoError; - -/// A trait for validating Concerto components -pub trait Validate { - /// Validates the component - /// Returns Ok(()) if valid, or an error if invalid - fn validate(&self) -> Result<(), ConcertoError>; -} From c573a327be90b3ce182290fba5b24165202bc32f Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Wed, 18 Mar 2026 01:36:41 +0000 Subject: [PATCH 5/9] refactor: add from_ast trait Signed-off-by: Ertugrul Karademir --- concerto-core/src/introspect/decorator.rs | 31 ++++++++++++++++++++++ concerto-core/src/introspect/mod.rs | 2 ++ concerto-core/src/introspect/model_file.rs | 19 ++++++++----- concerto-core/src/traits.rs | 16 +++++++++++ concerto-core/src/types.rs | 7 +++-- concerto-core/src/util.rs | 10 +++---- 6 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 concerto-core/src/introspect/decorator.rs diff --git a/concerto-core/src/introspect/decorator.rs b/concerto-core/src/introspect/decorator.rs new file mode 100644 index 0000000..cda11b2 --- /dev/null +++ b/concerto-core/src/introspect/decorator.rs @@ -0,0 +1,31 @@ +use crate::{types::DecoratorAst, ConcertoError, FromAst}; +use concerto_metamodel::concerto_metamodel_1_0_0::Decorator as CDecorator; + +pub struct Decorator { + inner: DecoratorAst, +} + +// Builder methods +impl Decorator { + pub fn new(ast_json: &str) -> Result { + let parsed: CDecorator = serde_json::from_str(ast_json)?; + Ok(Decorator::from_ast(parsed)) + } +} + +// Public methods +impl Decorator { + pub fn get_name(&self) -> &str { + &self.inner.0.name + } +} + +impl FromAst for Decorator { + type ConcertoType = CDecorator; + + fn from_ast(concerto_type: Self::ConcertoType) -> Self { + Decorator { + inner: DecoratorAst(concerto_type), + } + } +} diff --git a/concerto-core/src/introspect/mod.rs b/concerto-core/src/introspect/mod.rs index 219d350..a585815 100644 --- a/concerto-core/src/introspect/mod.rs +++ b/concerto-core/src/introspect/mod.rs @@ -1,3 +1,5 @@ +pub mod decorator; pub mod model_file; +pub use decorator::Decorator; pub use model_file::ModelFile; diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index 1d53d4a..37129c2 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -1,20 +1,21 @@ use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Model}; -use crate::{types::Ast, ConcertoError}; +use crate::{types::ModelAst, ConcertoError, FromAst}; #[derive(Debug)] pub struct ModelFile { - inner: Ast, + inner: ModelAst, } +// Builder methods impl ModelFile { pub fn new(ast_json: &str) -> Result { let parsed: Model = serde_json::from_str(ast_json)?; - let ast = Ast(parsed); - ast.into() + Ok(ModelFile::from_ast(parsed)) } } +// Public methods impl ModelFile { pub fn get_namespace(&self) -> &str { &self.inner.0.namespace @@ -29,8 +30,12 @@ impl ModelFile { } } -impl From for Result { - fn from(value: Ast) -> Self { - Ok(ModelFile { inner: value }) +impl FromAst for ModelFile { + type ConcertoType = Model; + + fn from_ast(concerto_type: Self::ConcertoType) -> Self { + ModelFile { + inner: ModelAst(concerto_type), + } } } diff --git a/concerto-core/src/traits.rs b/concerto-core/src/traits.rs index d1930c0..59c92d3 100644 --- a/concerto-core/src/traits.rs +++ b/concerto-core/src/traits.rs @@ -2,6 +2,22 @@ use concerto_metamodel::concerto_metamodel_1_0_0::*; use crate::{util::is_valid_identifier, ConcertoError, ModelManager}; +pub trait FromAst: Sized { + /// Concerto core wrapper type around Concerto metamodel + type ConcertoType; + + /// Implement the method that returnes Introspect structure from `Self::Ast` + fn from_ast(concerto_type: Self::ConcertoType) -> Self; + + fn from_json(json: &str) -> Result + where + Self::ConcertoType: for<'de> serde::Deserialize<'de>, + { + let parsed: Self::ConcertoType = serde_json::from_str(json)?; + Ok(Self::from_ast(parsed)) + } +} + pub trait Validate { /// Validates the component fn validate(&self) -> Result<(), ConcertoError>; diff --git a/concerto-core/src/types.rs b/concerto-core/src/types.rs index d6660e6..d61f218 100644 --- a/concerto-core/src/types.rs +++ b/concerto-core/src/types.rs @@ -1,4 +1,7 @@ -use concerto_metamodel::concerto_metamodel_1_0_0::Model; +use concerto_metamodel::concerto_metamodel_1_0_0::{Decorator, Model}; #[derive(Debug)] -pub struct Ast(pub Model); +pub struct ModelAst(pub Model); + +#[derive(Debug)] +pub struct DecoratorAst(pub Decorator); diff --git a/concerto-core/src/util.rs b/concerto-core/src/util.rs index e9bc3f1..0fb5b46 100644 --- a/concerto-core/src/util.rs +++ b/concerto-core/src/util.rs @@ -1,12 +1,12 @@ use regex::Regex; use std::sync::LazyLock; -use crate::{types::Ast, ConcertoError}; +use crate::{types::ModelAst, ConcertoError}; /// Parses a potentially versioned namespace into /// its name and version parts. The version of the namespace /// (if present) is parsed using semver.parse. -pub fn parse_namespace_and_version(ast: &Ast) -> Result<(String, String), ConcertoError> { +pub fn parse_namespace_and_version(ast: &ModelAst) -> Result<(String, String), ConcertoError> { let x = ast.0.namespace.as_str(); let parts: Vec<&str> = x.split('@').collect(); // For now, assume all NS will have to have namespace@version @@ -30,14 +30,14 @@ pub fn is_valid_identifier(name: &str) -> bool { #[cfg(test)] mod test { - use crate::types::Ast; + use crate::types::ModelAst; use concerto_metamodel::concerto_metamodel_1_0_0::Model; #[allow(unused)] - fn make_ast() -> Ast { + fn make_ast() -> ModelAst { let basic_ast = include_str!("../examples/assets/concerto_models/basic.json"); let parsed: Model = serde_json::from_str(basic_ast).expect("Cannot parse basic.json"); - Ast(parsed) + ModelAst(parsed) } #[test] From d2b34f6d6dbf27b525834e93d0513a47403bd73c Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Wed, 18 Mar 2026 02:25:46 +0000 Subject: [PATCH 6/9] refactor: split out derive macros and from_ast Signed-off-by: Ertugrul Karademir --- .vscode/extensions.json | 3 +- Cargo.lock | 17 ++++++-- Cargo.toml | 2 +- concerto-core/Cargo.toml | 1 + concerto-core/src/introspect/decorator.rs | 15 ++----- concerto-core/src/introspect/model_file.rs | 18 +++----- concerto-core/src/traits.rs | 2 +- concerto-macros/Cargo.toml | 11 +++++ concerto-macros/src/lib.rs | 50 ++++++++++++++++++++++ 9 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 concerto-macros/Cargo.toml create mode 100644 concerto-macros/src/lib.rs diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4dfb273..7f27eba 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ - "rust-lang.rust-analyzer" + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml" ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 909d787..92ff2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,14 @@ dependencies = [ "windows-link", ] +[[package]] +name = "concerto-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "concerto-metamodel" version = "3.12.8" @@ -80,6 +88,7 @@ name = "concerto_core" version = "0.1.0" dependencies = [ "chrono", + "concerto-macros", "concerto-metamodel", "log", "regex", @@ -179,9 +188,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -273,9 +282,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c3feb10..055e35c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "3" -members = ["concerto-metamodel", "concerto-core"] +members = ["concerto-metamodel", "concerto-core", "concerto-macros"] diff --git a/concerto-core/Cargo.toml b/concerto-core/Cargo.toml index 7b1bedd..f57bda3 100644 --- a/concerto-core/Cargo.toml +++ b/concerto-core/Cargo.toml @@ -7,6 +7,7 @@ description = "Rust implementation of Concerto Core for structural and semantic [dependencies] concerto-metamodel = { path = "../concerto-metamodel" } +concerto-macros = { path = "../concerto-macros" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" diff --git a/concerto-core/src/introspect/decorator.rs b/concerto-core/src/introspect/decorator.rs index cda11b2..6044906 100644 --- a/concerto-core/src/introspect/decorator.rs +++ b/concerto-core/src/introspect/decorator.rs @@ -1,6 +1,8 @@ use crate::{types::DecoratorAst, ConcertoError, FromAst}; +use concerto_macros::FromAst; use concerto_metamodel::concerto_metamodel_1_0_0::Decorator as CDecorator; +#[derive(FromAst)] pub struct Decorator { inner: DecoratorAst, } @@ -8,8 +10,7 @@ pub struct Decorator { // Builder methods impl Decorator { pub fn new(ast_json: &str) -> Result { - let parsed: CDecorator = serde_json::from_str(ast_json)?; - Ok(Decorator::from_ast(parsed)) + Decorator::from_json(ast_json) } } @@ -19,13 +20,3 @@ impl Decorator { &self.inner.0.name } } - -impl FromAst for Decorator { - type ConcertoType = CDecorator; - - fn from_ast(concerto_type: Self::ConcertoType) -> Self { - Decorator { - inner: DecoratorAst(concerto_type), - } - } -} diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index 37129c2..8aa388f 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -1,8 +1,11 @@ +use concerto_macros::FromAst; use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Model}; use crate::{types::ModelAst, ConcertoError, FromAst}; -#[derive(Debug)] +#[derive(Debug, FromAst)] +#[concerto_ast_type(Model)] +#[wrapper(ModelAst)] pub struct ModelFile { inner: ModelAst, } @@ -10,8 +13,7 @@ pub struct ModelFile { // Builder methods impl ModelFile { pub fn new(ast_json: &str) -> Result { - let parsed: Model = serde_json::from_str(ast_json)?; - Ok(ModelFile::from_ast(parsed)) + ModelFile::from_json(ast_json) } } @@ -29,13 +31,3 @@ impl ModelFile { unimplemented!() } } - -impl FromAst for ModelFile { - type ConcertoType = Model; - - fn from_ast(concerto_type: Self::ConcertoType) -> Self { - ModelFile { - inner: ModelAst(concerto_type), - } - } -} diff --git a/concerto-core/src/traits.rs b/concerto-core/src/traits.rs index 59c92d3..45ea316 100644 --- a/concerto-core/src/traits.rs +++ b/concerto-core/src/traits.rs @@ -18,7 +18,7 @@ pub trait FromAst: Sized { } } -pub trait Validate { +pub trait Validate: Sized { /// Validates the component fn validate(&self) -> Result<(), ConcertoError>; } diff --git a/concerto-macros/Cargo.toml b/concerto-macros/Cargo.toml new file mode 100644 index 0000000..a0b6281 --- /dev/null +++ b/concerto-macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "concerto-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0.117" +quote = "1.0.45" diff --git a/concerto-macros/src/lib.rs b/concerto-macros/src/lib.rs new file mode 100644 index 0000000..9dd5801 --- /dev/null +++ b/concerto-macros/src/lib.rs @@ -0,0 +1,50 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, Ident, parse_macro_input}; + +#[proc_macro_derive(FromAst, attributes(concerto_ast_type, wrapper))] +pub fn derive_from_ast(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let concerto_ast_ident = get_concerto_ast_type(&input); + let wrapper_ident = get_wrapper_type(&input); + let name = &input.ident; + + let expanded = quote! { + impl FromAst for #name { + type ConcertoType = #concerto_ast_ident; + + fn from_ast(concerto_type: Self::ConcertoType) -> Self { + Self { + inner: #wrapper_ident(concerto_type), + } + } + } + }; + TokenStream::from(expanded) +} + +fn get_concerto_ast_type(input: &DeriveInput) -> Ident { + for attr in &input.attrs { + if attr.path().is_ident("concerto_ast_type") { + let nested_ident: Ident = attr.parse_args().expect("Expected a Concerto Type"); + return nested_ident; + } + } + + // Fallback to `CConcertoType`, e.g. `CDecorator` as a convention + let fallback_name = format!("C{}", input.ident); + Ident::new(&fallback_name, input.ident.span()) +} + +fn get_wrapper_type(input: &DeriveInput) -> Ident { + for attr in &input.attrs { + if attr.path().is_ident("wrapper") { + let nested_ident: Ident = attr.parse_args().expect("Expected a Wrapper Type"); + return nested_ident; + } + } + + // Fallback to `CConcertoType`, e.g. `CDecorator` as a convention + let fallback_name = format!("{}Ast", input.ident); + Ident::new(&fallback_name, input.ident.span()) +} From 32f0b0d91ffb2ab83d9bf5f07cbf652e00a7c91c Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Wed, 18 Mar 2026 02:38:11 +0000 Subject: [PATCH 7/9] refactor: validate for model file Signed-off-by: Ertugrul Karademir --- concerto-core/src/introspect/model_file.rs | 7 +++++-- concerto-core/src/model_manager.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index 8aa388f..b7dea1f 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -1,7 +1,7 @@ use concerto_macros::FromAst; use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Model}; -use crate::{types::ModelAst, ConcertoError, FromAst}; +use crate::{types::ModelAst, ConcertoError, FromAst, Validate}; #[derive(Debug, FromAst)] #[concerto_ast_type(Model)] @@ -26,8 +26,11 @@ impl ModelFile { pub fn get_declarations(&self) -> Option> { self.inner.0.declarations.clone() } +} - pub fn validate(&self) -> Result<(), ConcertoError> { +// Impls +impl Validate for ModelFile { + fn validate(&self) -> Result<(), ConcertoError> { unimplemented!() } } diff --git a/concerto-core/src/model_manager.rs b/concerto-core/src/model_manager.rs index 14a66ad..94014e6 100644 --- a/concerto-core/src/model_manager.rs +++ b/concerto-core/src/model_manager.rs @@ -29,7 +29,7 @@ impl ModelManager { /// Adds a model file to the model manager pub fn add_model_file(&mut self, model_file: ModelFile) -> Result<(), ConcertoError> { // Basic validation of the model file (without cross-model validation) - // model_file.validate()?; + model_file.validate()?; // Add to our collection self.models From d797972dc85af422242c40e145bc8ebb95fe8167 Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Sun, 22 Mar 2026 14:37:19 +0000 Subject: [PATCH 8/9] feat(*): results after 1-shot using claude opus 4.6 Signed-off-by: Ertugrul Karademir --- .gitignore | 2 +- AGENTS.md | 22 + Cargo.lock | 42 +- README.md | 79 +- TODO | 10 - concerto-core/Cargo.toml | 19 +- .../examples/assets/concerto_models/basic.cto | 5 - .../assets/concerto_models/basic.json | 51 - concerto-core/src/error.rs | 56 +- concerto-core/src/introspect/declarations.rs | 593 +++++++++ concerto-core/src/introspect/decorator.rs | 31 +- concerto-core/src/introspect/imports.rs | 87 ++ concerto-core/src/introspect/map_types.rs | 74 ++ concerto-core/src/introspect/mod.rs | 9 +- concerto-core/src/introspect/model_file.rs | 323 ++++- concerto-core/src/introspect/properties.rs | 178 +++ concerto-core/src/introspect/traits.rs | 57 + concerto-core/src/introspect/validation.rs | 380 ++++++ concerto-core/src/lib.rs | 98 +- concerto-core/src/metamodel_validation.rs | 252 ---- concerto-core/src/model_manager.rs | 422 +++---- concerto-core/src/model_util.rs | 242 ++++ concerto-core/src/rootmodel.rs | 120 ++ concerto-core/src/traits.rs | 1077 ----------------- concerto-core/src/types.rs | 7 - concerto-core/src/util.rs | 71 -- concerto-core/src/validation/mod.rs | 1 + .../src/validation/object_validator.rs | 368 ++++++ concerto-macros/Cargo.toml | 6 +- concerto-macros/src/lib.rs | 263 +++- concerto-metamodel/Cargo.toml | 4 + 31 files changed, 2968 insertions(+), 1981 deletions(-) create mode 100644 AGENTS.md delete mode 100644 TODO delete mode 100644 concerto-core/examples/assets/concerto_models/basic.cto delete mode 100644 concerto-core/examples/assets/concerto_models/basic.json create mode 100644 concerto-core/src/introspect/declarations.rs create mode 100644 concerto-core/src/introspect/imports.rs create mode 100644 concerto-core/src/introspect/map_types.rs create mode 100644 concerto-core/src/introspect/properties.rs create mode 100644 concerto-core/src/introspect/traits.rs create mode 100644 concerto-core/src/introspect/validation.rs delete mode 100644 concerto-core/src/metamodel_validation.rs create mode 100644 concerto-core/src/model_util.rs create mode 100644 concerto-core/src/rootmodel.rs delete mode 100644 concerto-core/src/traits.rs delete mode 100644 concerto-core/src/types.rs delete mode 100644 concerto-core/src/util.rs create mode 100644 concerto-core/src/validation/mod.rs create mode 100644 concerto-core/src/validation/object_validator.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..b60de5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/target +**/target diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a95f34c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# Documenting code + +- Refer to [Concerto Specification documentation](https://concerto.accordproject.org/docs/category/specification) when figuring out anything related to `concerto-core`, especially anything related to instrospection. +- Add Rust-Docs using above documentation when possible. + +# Adding and modifying code + +- Always use the most idiomatic approach suitable for Rust language. +- If needed split the code into different crates. If a crate does not exist, ask the operator for help. +- Add a derive macro for repeated `impl`s of a trait, into `cocnerto-macros` crate. +- Types under `concerto-core` should only refer to the types from `concerto-metamodel` using new-type pattern. +- Prefer sum-type over complicated traits. +- Remove any code that is not being used (don't add extra code, just because, that might be useful in the future.) +- Add unit tests for new functions and methods. + +# Testing + +- Run `cargo build` and `cargo test` to verify any change. + +# PR instructions + +- Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style for PR titles. diff --git a/Cargo.lock b/Cargo.lock index 92ff2d5..b8260fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,19 @@ dependencies = [ "windows-link", ] +[[package]] +name = "concerto-core" +version = "0.1.0" +dependencies = [ + "chrono", + "concerto-macros", + "concerto-metamodel", + "regex", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "concerto-macros" version = "0.1.0" @@ -83,21 +96,6 @@ dependencies = [ "serde", ] -[[package]] -name = "concerto_core" -version = "0.1.0" -dependencies = [ - "chrono", - "concerto-macros", - "concerto-metamodel", - "log", - "regex", - "semver", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -236,12 +234,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - [[package]] name = "serde" version = "1.0.219" @@ -293,18 +285,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index e950d47..7321472 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,22 @@ # Concerto Rust -A prototype Rust implementation of the Concerto modeling language, focusing on structural and semantic metamodel validation. +Rust implementation of the Concerto modeling language, focusing on structural and semantic metamodel validation. ## Overview This project is a partial Rust implementation of the [Concerto](https://github.com/accordproject/concerto) modeling language, which was originally developed in JavaScript. This Rust version focuses specifically on the structural and semantic validation aspects of the metamodel. -## Features - -The implementation includes: - -- Model file parsing and validation -- Declaration types (Asset, Concept, Enum, Scalar, Map) -- Property type validation -- Import and namespace validation -- Semantic validation based on the Concerto Conformance rules - ## Project Structure -The project is organized as follows: - -- `src/` - - `lib.rs`: Main entry point exporting public modules - - `declaration.rs`: Core declarations for data modeling - - `model_file.rs`: Represents model files with namespace and imports management - - `model_manager.rs`: Manages model collections and cross-model validations - - `error.rs`: Error types for the library - - `validation.rs`: Validation traits and implementations +The project is organized into different crates under one workspace: -## Testing - -The test suite includes: - -- `declaration_tests.rs`: Tests for declaration validation -- `conformance_tests.rs`: Tests for conformance with the specification -- `enum_tests.rs`: Tests for enum declarations and validation -- `scalar_tests.rs`: Tests for scalar declarations and validation -- `map_tests.rs`: Tests for map declarations and validation -- `namespace_tests.rs`: Tests for namespace validation +- [`concerto-core`](/concerto-core/) is the core implementation with introspection and validation. +- [`concerto-macros`](/concerto-macros/) has the derive macros used by the `cocnerto-core` crate. +- [`concerto-metamodel`](/concerto-metamodel/) crate exposes the generated Rust types from the main [`concerto-metamodel`](https://github.com/accordproject/concerto-metamodel) package. ## Usage -```rust -use concerto_core::{ - ModelFile, - Declaration, - ModelManager, - validation::Validate, -}; - -// Create a model file -let model_file = ModelFile { - namespace: "org.example".to_string(), - imports: vec![], - declarations: vec![ - // Add your declarations here - ], -}; - -// Validate the model file -match model_file.validate() { - Ok(_) => println!("Model file is valid"), - Err(e) => println!("Validation error: {}", e), -}; - -// Create a model manager -let mut model_manager = ModelManager::new(); -model_manager.add_model_file(model_file).unwrap(); - -// Validate the entire model -match model_manager.validate() { - Ok(_) => println!("Model is valid"), - Err(e) => println!("Validation error: {}", e), -}; -``` - -## Status - -This is a partial implementation focusing on the validation aspects of Concerto. It includes: - -- Basic model file structure and validation -- Declaration validation -- Cross-model validation -- Import and namespace validation -- Support for Enums, Scalars, and Maps +TBD ## License diff --git a/TODO b/TODO deleted file mode 100644 index ab702cf..0000000 --- a/TODO +++ /dev/null @@ -1,10 +0,0 @@ -- Implement Ast model manager -- Model manager should have the base model manager API -- Implement Sumtypes for declarations -- Decorated trait -- Validate trait -- Traits should only have traits each introspect should implement traits - -Visitor pattern - -- All introspect classes implements visitor pattern. Maybe add a visitor trait. diff --git a/concerto-core/Cargo.toml b/concerto-core/Cargo.toml index f57bda3..85d67bc 100644 --- a/concerto-core/Cargo.toml +++ b/concerto-core/Cargo.toml @@ -1,17 +1,18 @@ [package] -name = "concerto_core" +name = "concerto-core" version = "0.1.0" -edition = "2021" -authors = ["Accord Project"] -description = "Rust implementation of Concerto Core for structural and semantic metamodel validation" +edition = "2024" +authors = ["Accord Project "] +description = "Core implementation of Concerto Modelling Language in Rust" +license = "Apache-2.0" +license-file = "../LICENSE" +homepage = "https://concerto.accordproject.org/" [dependencies] concerto-metamodel = { path = "../concerto-metamodel" } concerto-macros = { path = "../concerto-macros" } +thiserror = "2" +regex = "1" +chrono = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -thiserror = "1.0" -regex = "1.10" -semver = "1.0" -log = "0.4" -chrono = "0.4" diff --git a/concerto-core/examples/assets/concerto_models/basic.cto b/concerto-core/examples/assets/concerto_models/basic.cto deleted file mode 100644 index e1ba17d..0000000 --- a/concerto-core/examples/assets/concerto_models/basic.cto +++ /dev/null @@ -1,5 +0,0 @@ -namespace com.example.foo@1.0.0 - -concept Foo { - o String bar -} diff --git a/concerto-core/examples/assets/concerto_models/basic.json b/concerto-core/examples/assets/concerto_models/basic.json deleted file mode 100644 index 20ff1a8..0000000 --- a/concerto-core/examples/assets/concerto_models/basic.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$class": "concerto.metamodel@1.0.0.Model", - "decorators": [], - "namespace": "com.example.foo@1.0.0", - "imports": [], - "declarations": [ - { - "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", - "name": "Foo", - "isAbstract": false, - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.StringProperty", - "name": "bar", - "isArray": false, - "isOptional": false, - "location": { - "$class": "concerto.metamodel@1.0.0.Range", - "start": { - "offset": 51, - "line": 4, - "column": 5, - "$class": "concerto.metamodel@1.0.0.Position" - }, - "end": { - "offset": 64, - "line": 5, - "column": 1, - "$class": "concerto.metamodel@1.0.0.Position" - } - } - } - ], - "location": { - "$class": "concerto.metamodel@1.0.0.Range", - "start": { - "offset": 33, - "line": 3, - "column": 1, - "$class": "concerto.metamodel@1.0.0.Position" - }, - "end": { - "offset": 65, - "line": 5, - "column": 2, - "$class": "concerto.metamodel@1.0.0.Position" - } - } - } - ] -} \ No newline at end of file diff --git a/concerto-core/src/error.rs b/concerto-core/src/error.rs index 4e0852b..9008078 100644 --- a/concerto-core/src/error.rs +++ b/concerto-core/src/error.rs @@ -1,39 +1,23 @@ -use thiserror::Error; +use concerto_metamodel::concerto_metamodel_1_0_0::Range; -/// Error types for Concerto operations -#[derive(Error, Debug)] +/// Errors produced by concerto-core. +#[derive(Debug, thiserror::Error)] pub enum ConcertoError { - /// Error during parsing - #[error("Parse error: {0}")] - ParseError(String), - - /// Error during validation - #[error("Validation error: {0}")] - ValidationError(String), - - /// Declaration not found - #[error("Declaration not found: {0}")] - DeclarationNotFound(String), - - /// Namespace not found - #[error("Namespace not found: {0}")] - NamespaceNotFound(String), - - /// I/O error - #[error("I/O error: {0}")] - IoError(String), - - /// Generic error - #[error("Error: {0}")] - GenericError(String), - - /// Ast Error - #[error("Ast not found")] - AstError, - - #[error("JSON parsing error: {0}")] - JsonError(#[from] serde_json::Error), - - #[error("Invalid namespace: {0}")] - InvalidNS(String), + /// The model contains an illegal construct. + #[error("{message}")] + IllegalModel { + message: String, + file_name: Option, + location: Option, + }, + + /// A referenced type could not be found. + #[error("Type not found: {type_name}")] + TypeNotFound { type_name: String }, + + /// A value failed validation against the model. + #[error("Validation error on {component}: {message}")] + Validation { message: String, component: String }, } + +pub type Result = std::result::Result; diff --git a/concerto-core/src/introspect/declarations.rs b/concerto-core/src/introspect/declarations.rs new file mode 100644 index 0000000..0d73c9b --- /dev/null +++ b/concerto-core/src/introspect/declarations.rs @@ -0,0 +1,593 @@ +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; + +use crate::error::{ConcertoError, Result}; +use crate::model_util; + +use super::properties::PropertyDecl; +use super::traits::{Decorated, HasProperties, Identifiable, Named}; + +use concerto_macros::{Decorated, HasProperties, Identifiable, Named}; + +// =================================================================== +// Flattened class-declaration structs +// =================================================================== + +/// A concept declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named, HasProperties, Identifiable)] +pub struct ConceptDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) is_abstract: bool, + pub(crate) identified: Option, + pub(crate) super_type: Option, + pub(crate) properties: Vec, +} + +/// An asset declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named, HasProperties, Identifiable)] +pub struct AssetDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) is_abstract: bool, + pub(crate) identified: Option, + pub(crate) super_type: Option, + pub(crate) properties: Vec, +} + +/// A participant declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named, HasProperties, Identifiable)] +pub struct ParticipantDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) is_abstract: bool, + pub(crate) identified: Option, + pub(crate) super_type: Option, + pub(crate) properties: Vec, +} + +/// A transaction declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named, HasProperties, Identifiable)] +pub struct TransactionDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) is_abstract: bool, + pub(crate) identified: Option, + pub(crate) super_type: Option, + pub(crate) properties: Vec, +} + +/// An event declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named, HasProperties, Identifiable)] +pub struct EventDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) is_abstract: bool, + pub(crate) identified: Option, + pub(crate) super_type: Option, + pub(crate) properties: Vec, +} + +/// An enum declaration with fields extracted from the metamodel AST. +#[derive(Debug, Clone, Decorated, Named)] +pub struct EnumDeclaration { + pub(crate) name: String, + pub(crate) decorators: Option>, + pub(crate) location: Option, + pub(crate) properties: Vec, +} + +impl HasProperties for EnumDeclaration { + fn own_properties(&self) -> &[PropertyDecl] { + &self.properties + } + fn super_type(&self) -> Option<&mm::TypeIdentifier> { + None + } + fn is_abstract(&self) -> bool { + false + } +} + +impl Identifiable for EnumDeclaration { + fn is_identified(&self) -> bool { + false + } + fn is_system_identified(&self) -> bool { + false + } + fn is_explicitly_identified(&self) -> bool { + false + } + fn identifier_field_name(&self) -> Option<&str> { + None + } +} + +/// Wraps `mm::MapDeclaration`. +#[derive(Debug, Clone, Decorated, Named)] +pub struct MapDeclaration(pub(crate) mm::MapDeclaration); + +// =================================================================== +// Constructors from metamodel types +// =================================================================== + +macro_rules! impl_class_decl_new { + ($rust_ty:ident, $mm_ty:ty) => { + impl $rust_ty { + pub(crate) fn new(mm: $mm_ty, properties: Vec) -> Self { + Self { + name: mm.name, + decorators: mm.decorators, + location: mm.location, + is_abstract: mm.is_abstract, + identified: mm.identified, + super_type: mm.super_type, + properties, + } + } + } + }; +} + +impl_class_decl_new!(ConceptDeclaration, mm::ConceptDeclaration); +impl_class_decl_new!(AssetDeclaration, mm::AssetDeclaration); +impl_class_decl_new!(ParticipantDeclaration, mm::ParticipantDeclaration); +impl_class_decl_new!(TransactionDeclaration, mm::TransactionDeclaration); +impl_class_decl_new!(EventDeclaration, mm::EventDeclaration); + +impl EnumDeclaration { + pub(crate) fn new(mm: mm::EnumDeclaration, properties: Vec) -> Self { + Self { + name: mm.name, + decorators: mm.decorators, + location: mm.location, + properties, + } + } +} + +// =================================================================== +// ScalarDeclKind — sum over scalar sub-types +// =================================================================== + +#[derive(Debug, Clone)] +pub enum ScalarDeclKind { + Boolean(mm::BooleanScalar), + Integer(mm::IntegerScalar), + Long(mm::LongScalar), + Double(mm::DoubleScalar), + String(mm::StringScalar), + DateTime(mm::DateTimeScalar), +} + +/// Wraps one of the six metamodel scalar types. +#[derive(Debug, Clone)] +pub struct ScalarDeclaration(pub(crate) ScalarDeclKind); + +// =================================================================== +// ClassDeclaration — sum type replacing JS inheritance subtree +// =================================================================== + +#[derive(Debug, Clone)] +pub enum ClassDeclaration { + Concept(ConceptDeclaration), + Asset(AssetDeclaration), + Participant(ParticipantDeclaration), + Transaction(TransactionDeclaration), + Event(EventDeclaration), + Enum(EnumDeclaration), +} + +// =================================================================== +// Declaration — top-level sum type +// =================================================================== + +#[derive(Debug, Clone)] +pub enum Declaration { + Class(ClassDeclaration), + Scalar(ScalarDeclaration), + Map(MapDeclaration), +} + +// =================================================================== +// TryFrom — construct from AST JSON +// =================================================================== + +fn parse_properties_from_json(value: &serde_json::Value) -> Result> { + match value.get("properties") { + Some(serde_json::Value::Array(arr)) => { + arr.iter().map(|v| PropertyDecl::try_from(v.clone())).collect() + } + _ => Ok(vec![]), + } +} + +macro_rules! impl_try_from_for_class_decl { + ($rust_ty:ident, $mm_ty:ty, $variant:ident, $label:expr) => { + impl TryFrom for $rust_ty { + type Error = ConcertoError; + fn try_from(value: serde_json::Value) -> Result { + let inner: $mm_ty = + serde_json::from_value(value.clone()).map_err(|e| ConcertoError::IllegalModel { + message: format!(concat!("Invalid ", $label, ": {}"), e), + file_name: None, + location: None, + })?; + let properties = parse_properties_from_json(&value)?; + Ok(Self::new(inner, properties)) + } + } + }; +} + +impl_try_from_for_class_decl!(ConceptDeclaration, mm::ConceptDeclaration, Concept, "ConceptDeclaration"); +impl_try_from_for_class_decl!(AssetDeclaration, mm::AssetDeclaration, Asset, "AssetDeclaration"); +impl_try_from_for_class_decl!(ParticipantDeclaration, mm::ParticipantDeclaration, Participant, "ParticipantDeclaration"); +impl_try_from_for_class_decl!(TransactionDeclaration, mm::TransactionDeclaration, Transaction, "TransactionDeclaration"); +impl_try_from_for_class_decl!(EventDeclaration, mm::EventDeclaration, Event, "EventDeclaration"); +impl_try_from_for_class_decl!(EnumDeclaration, mm::EnumDeclaration, Enum, "EnumDeclaration"); + +impl TryFrom for MapDeclaration { + type Error = ConcertoError; + fn try_from(value: serde_json::Value) -> Result { + let inner: mm::MapDeclaration = + serde_json::from_value(value).map_err(|e| ConcertoError::IllegalModel { + message: format!("Invalid MapDeclaration: {e}"), + file_name: None, + location: None, + })?; + Ok(MapDeclaration(inner)) + } +} + +impl TryFrom for ScalarDeclaration { + type Error = ConcertoError; + fn try_from(value: serde_json::Value) -> Result { + let class = value + .get("$class") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let kind = model_util::get_short_name(&class).to_string(); + + let mk_err = |e: serde_json::Error, k: &str| ConcertoError::IllegalModel { + message: format!("Invalid {k}: {e}"), + file_name: None, + location: None, + }; + + let scalar_kind = match kind.as_str() { + "BooleanScalar" => ScalarDeclKind::Boolean(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + "IntegerScalar" => ScalarDeclKind::Integer(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + "LongScalar" => ScalarDeclKind::Long(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + "DoubleScalar" => ScalarDeclKind::Double(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + "StringScalar" => ScalarDeclKind::String(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + "DateTimeScalar" => ScalarDeclKind::DateTime(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?), + _ => { + return Err(ConcertoError::IllegalModel { + message: format!("Unknown scalar type: {class}"), + file_name: None, + location: None, + }); + } + }; + + Ok(ScalarDeclaration(scalar_kind)) + } +} + +impl TryFrom for Declaration { + type Error = ConcertoError; + fn try_from(value: serde_json::Value) -> Result { + let class = value + .get("$class") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let kind = model_util::get_short_name(&class).to_string(); + + match kind.as_str() { + "ConceptDeclaration" => Ok(Declaration::Class(ClassDeclaration::Concept( + ConceptDeclaration::try_from(value)?, + ))), + "AssetDeclaration" => Ok(Declaration::Class(ClassDeclaration::Asset( + AssetDeclaration::try_from(value)?, + ))), + "ParticipantDeclaration" => Ok(Declaration::Class(ClassDeclaration::Participant( + ParticipantDeclaration::try_from(value)?, + ))), + "TransactionDeclaration" => Ok(Declaration::Class(ClassDeclaration::Transaction( + TransactionDeclaration::try_from(value)?, + ))), + "EventDeclaration" => Ok(Declaration::Class(ClassDeclaration::Event( + EventDeclaration::try_from(value)?, + ))), + "EnumDeclaration" => Ok(Declaration::Class(ClassDeclaration::Enum( + EnumDeclaration::try_from(value)?, + ))), + "MapDeclaration" => Ok(Declaration::Map(MapDeclaration::try_from(value)?)), + s if s.ends_with("Scalar") => { + Ok(Declaration::Scalar(ScalarDeclaration::try_from(value)?)) + } + _ => Err(ConcertoError::IllegalModel { + message: format!("Unknown declaration type: {class}"), + file_name: None, + location: None, + }), + } + } +} + +// =================================================================== +// Decorated / Named for ScalarDeclaration +// =================================================================== + +impl Decorated for ScalarDeclaration { + fn decorators(&self) -> &[mm::Decorator] { + match &self.0 { + ScalarDeclKind::Boolean(s) => s.decorators.as_deref().unwrap_or(&[]), + ScalarDeclKind::Integer(s) => s.decorators.as_deref().unwrap_or(&[]), + ScalarDeclKind::Long(s) => s.decorators.as_deref().unwrap_or(&[]), + ScalarDeclKind::Double(s) => s.decorators.as_deref().unwrap_or(&[]), + ScalarDeclKind::String(s) => s.decorators.as_deref().unwrap_or(&[]), + ScalarDeclKind::DateTime(s) => s.decorators.as_deref().unwrap_or(&[]), + } + } +} + +impl Named for ScalarDeclaration { + fn name(&self) -> &str { + match &self.0 { + ScalarDeclKind::Boolean(s) => &s.name, + ScalarDeclKind::Integer(s) => &s.name, + ScalarDeclKind::Long(s) => &s.name, + ScalarDeclKind::Double(s) => &s.name, + ScalarDeclKind::String(s) => &s.name, + ScalarDeclKind::DateTime(s) => &s.name, + } + } + fn location(&self) -> Option<&mm::Range> { + match &self.0 { + ScalarDeclKind::Boolean(s) => s.location.as_ref(), + ScalarDeclKind::Integer(s) => s.location.as_ref(), + ScalarDeclKind::Long(s) => s.location.as_ref(), + ScalarDeclKind::Double(s) => s.location.as_ref(), + ScalarDeclKind::String(s) => s.location.as_ref(), + ScalarDeclKind::DateTime(s) => s.location.as_ref(), + } + } +} + +impl ScalarDeclaration { + /// Returns the primitive type that this scalar wraps. + pub fn scalar_type(&self) -> &str { + match &self.0 { + ScalarDeclKind::Boolean(_) => "Boolean", + ScalarDeclKind::Integer(_) => "Integer", + ScalarDeclKind::Long(_) => "Long", + ScalarDeclKind::Double(_) => "Double", + ScalarDeclKind::String(_) => "String", + ScalarDeclKind::DateTime(_) => "DateTime", + } + } + + pub fn kind(&self) -> &ScalarDeclKind { + &self.0 + } +} + +// =================================================================== +// Dispatch on ClassDeclaration sum type +// =================================================================== + +impl Decorated for ClassDeclaration { + fn decorators(&self) -> &[mm::Decorator] { + match self { + Self::Concept(d) => d.decorators(), + Self::Asset(d) => d.decorators(), + Self::Participant(d) => d.decorators(), + Self::Transaction(d) => d.decorators(), + Self::Event(d) => d.decorators(), + Self::Enum(d) => d.decorators(), + } + } +} + +impl Named for ClassDeclaration { + fn name(&self) -> &str { + match self { + Self::Concept(d) => d.name(), + Self::Asset(d) => d.name(), + Self::Participant(d) => d.name(), + Self::Transaction(d) => d.name(), + Self::Event(d) => d.name(), + Self::Enum(d) => d.name(), + } + } + fn location(&self) -> Option<&mm::Range> { + match self { + Self::Concept(d) => d.location(), + Self::Asset(d) => d.location(), + Self::Participant(d) => d.location(), + Self::Transaction(d) => d.location(), + Self::Event(d) => d.location(), + Self::Enum(d) => d.location(), + } + } +} + +impl HasProperties for ClassDeclaration { + fn own_properties(&self) -> &[PropertyDecl] { + match self { + Self::Concept(d) => d.own_properties(), + Self::Asset(d) => d.own_properties(), + Self::Participant(d) => d.own_properties(), + Self::Transaction(d) => d.own_properties(), + Self::Event(d) => d.own_properties(), + Self::Enum(d) => d.own_properties(), + } + } + fn super_type(&self) -> Option<&mm::TypeIdentifier> { + match self { + Self::Concept(d) => d.super_type(), + Self::Asset(d) => d.super_type(), + Self::Participant(d) => d.super_type(), + Self::Transaction(d) => d.super_type(), + Self::Event(d) => d.super_type(), + Self::Enum(d) => d.super_type(), + } + } + fn is_abstract(&self) -> bool { + match self { + Self::Concept(d) => d.is_abstract(), + Self::Asset(d) => d.is_abstract(), + Self::Participant(d) => d.is_abstract(), + Self::Transaction(d) => d.is_abstract(), + Self::Event(d) => d.is_abstract(), + Self::Enum(d) => d.is_abstract(), + } + } +} + +impl Identifiable for ClassDeclaration { + fn is_identified(&self) -> bool { + match self { + Self::Concept(d) => d.is_identified(), + Self::Asset(d) => d.is_identified(), + Self::Participant(d) => d.is_identified(), + Self::Transaction(d) => d.is_identified(), + Self::Event(d) => d.is_identified(), + Self::Enum(d) => d.is_identified(), + } + } + fn is_system_identified(&self) -> bool { + match self { + Self::Concept(d) => d.is_system_identified(), + Self::Asset(d) => d.is_system_identified(), + Self::Participant(d) => d.is_system_identified(), + Self::Transaction(d) => d.is_system_identified(), + Self::Event(d) => d.is_system_identified(), + Self::Enum(d) => d.is_system_identified(), + } + } + fn is_explicitly_identified(&self) -> bool { + match self { + Self::Concept(d) => d.is_explicitly_identified(), + Self::Asset(d) => d.is_explicitly_identified(), + Self::Participant(d) => d.is_explicitly_identified(), + Self::Transaction(d) => d.is_explicitly_identified(), + Self::Event(d) => d.is_explicitly_identified(), + Self::Enum(d) => d.is_explicitly_identified(), + } + } + fn identifier_field_name(&self) -> Option<&str> { + match self { + Self::Concept(d) => d.identifier_field_name(), + Self::Asset(d) => d.identifier_field_name(), + Self::Participant(d) => d.identifier_field_name(), + Self::Transaction(d) => d.identifier_field_name(), + Self::Event(d) => d.identifier_field_name(), + Self::Enum(d) => d.identifier_field_name(), + } + } +} + +impl ClassDeclaration { + /// Returns the declaration kind string, matching JS `declarationKind()`. + pub fn declaration_kind(&self) -> &'static str { + match self { + Self::Concept(_) => "ConceptDeclaration", + Self::Asset(_) => "AssetDeclaration", + Self::Participant(_) => "ParticipantDeclaration", + Self::Transaction(_) => "TransactionDeclaration", + Self::Event(_) => "EventDeclaration", + Self::Enum(_) => "EnumDeclaration", + } + } + + pub fn is_concept(&self) -> bool { + matches!(self, Self::Concept(_)) + } + pub fn is_asset(&self) -> bool { + matches!(self, Self::Asset(_)) + } + pub fn is_participant(&self) -> bool { + matches!(self, Self::Participant(_)) + } + pub fn is_transaction(&self) -> bool { + matches!(self, Self::Transaction(_)) + } + pub fn is_event(&self) -> bool { + matches!(self, Self::Event(_)) + } + pub fn is_enum(&self) -> bool { + matches!(self, Self::Enum(_)) + } +} + +// =================================================================== +// Dispatch on Declaration sum type +// =================================================================== + +impl Decorated for Declaration { + fn decorators(&self) -> &[mm::Decorator] { + match self { + Self::Class(d) => d.decorators(), + Self::Scalar(d) => d.decorators(), + Self::Map(d) => d.decorators(), + } + } +} + +impl Named for Declaration { + fn name(&self) -> &str { + match self { + Self::Class(d) => d.name(), + Self::Scalar(d) => d.name(), + Self::Map(d) => d.name(), + } + } + fn location(&self) -> Option<&mm::Range> { + match self { + Self::Class(d) => d.location(), + Self::Scalar(d) => d.location(), + Self::Map(d) => d.location(), + } + } +} + +impl Declaration { + pub fn is_class(&self) -> bool { + matches!(self, Self::Class(_)) + } + pub fn is_scalar(&self) -> bool { + matches!(self, Self::Scalar(_)) + } + pub fn is_map(&self) -> bool { + matches!(self, Self::Map(_)) + } + + pub fn as_class(&self) -> Option<&ClassDeclaration> { + match self { + Self::Class(c) => Some(c), + _ => None, + } + } + pub fn as_scalar(&self) -> Option<&ScalarDeclaration> { + match self { + Self::Scalar(s) => Some(s), + _ => None, + } + } + pub fn as_map(&self) -> Option<&MapDeclaration> { + match self { + Self::Map(m) => Some(m), + _ => None, + } + } +} diff --git a/concerto-core/src/introspect/decorator.rs b/concerto-core/src/introspect/decorator.rs index 6044906..ff9027e 100644 --- a/concerto-core/src/introspect/decorator.rs +++ b/concerto-core/src/introspect/decorator.rs @@ -1,22 +1,25 @@ -use crate::{types::DecoratorAst, ConcertoError, FromAst}; -use concerto_macros::FromAst; -use concerto_metamodel::concerto_metamodel_1_0_0::Decorator as CDecorator; +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; -#[derive(FromAst)] -pub struct Decorator { - inner: DecoratorAst, -} +/// [Decorator] encapsulates a decorator (annotation) on a Class, Property, or a Model. +#[derive(Debug, Clone)] +pub struct Decorator(pub(crate) mm::Decorator); -// Builder methods impl Decorator { - pub fn new(ast_json: &str) -> Result { - Decorator::from_json(ast_json) + pub fn name(&self) -> &str { + &self.0.name + } + + pub fn arguments(&self) -> &[mm::DecoratorLiteral] { + self.0.arguments.as_deref().unwrap_or(&[]) + } + + pub fn location(&self) -> Option<&mm::Range> { + self.0.location.as_ref() } } -// Public methods -impl Decorator { - pub fn get_name(&self) -> &str { - &self.inner.0.name +impl From for Decorator { + fn from(d: mm::Decorator) -> Self { + Self(d) } } diff --git a/concerto-core/src/introspect/imports.rs b/concerto-core/src/introspect/imports.rs new file mode 100644 index 0000000..a1b5176 --- /dev/null +++ b/concerto-core/src/introspect/imports.rs @@ -0,0 +1,87 @@ +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; + +/// Sum type for all import types in Concerto. +/// See [Concerto documentation on imports](https://concerto.accordproject.org/docs/design/specification/model-imports). +#[derive(Debug, Clone)] +pub enum ImportDecl { + /// Corresponds to `import com.example.test.*`. + /// This is only allowed in non-strict mode. + All(mm::ImportAll), + /// Corresponds to importing a single type from + /// a namespace. `import com.example.test@1.0.0.Foo` + Type(mm::ImportType), + /// Corresponds to importing several types from + /// a namespace. `import com.example.test@1.0.0.{Foo, Bar}` + Types(mm::ImportTypes), +} + +impl ImportDecl { + pub fn namespace(&self) -> &str { + match self { + Self::All(i) => &i.namespace, + Self::Type(i) => &i.namespace, + Self::Types(i) => &i.namespace, + } + } + + pub fn uri(&self) -> Option<&str> { + match self { + Self::All(i) => i.uri.as_deref(), + Self::Type(i) => i.uri.as_deref(), + Self::Types(i) => i.uri.as_deref(), + } + } + + /// Returns the fully-qualified names introduced by this import. + pub fn fully_qualified_names(&self) -> Vec { + match self { + Self::All(_) => { + // ImportAll doesn't enumerate specific types; + // resolution requires the model manager. + vec![] + } + Self::Type(i) => { + vec![crate::model_util::fully_qualified_name( + &i.namespace, + &i.name, + )] + } + Self::Types(i) => i + .types + .iter() + .map(|t| crate::model_util::fully_qualified_name(&i.namespace, t)) + .collect(), + } + } + +} + +/// Construct an [`ImportDecl`] from AST JSON by inspecting `$class`. +impl TryFrom for ImportDecl { + type Error = crate::error::ConcertoError; + + fn try_from(value: serde_json::Value) -> crate::error::Result { + let class = value + .get("$class") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let kind = crate::model_util::get_short_name(&class).to_string(); + let mk_err = |e: serde_json::Error, k: &str| crate::error::ConcertoError::IllegalModel { + message: format!("Invalid {k}: {e}"), + file_name: None, + location: None, + }; + + match kind.as_str() { + "ImportAll" => Ok(Self::All(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "ImportType" => Ok(Self::Type(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "ImportTypes" => Ok(Self::Types(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + _ => Err(crate::error::ConcertoError::IllegalModel { + message: format!("Unknown import type: {class}"), + file_name: None, + location: None, + }), + } + } +} diff --git a/concerto-core/src/introspect/map_types.rs b/concerto-core/src/introspect/map_types.rs new file mode 100644 index 0000000..18a9287 --- /dev/null +++ b/concerto-core/src/introspect/map_types.rs @@ -0,0 +1,74 @@ +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; + +// =================================================================== +// MapKeyTypeDecl +// =================================================================== + +#[derive(Debug, Clone)] +pub enum MapKeyTypeDecl { + String(mm::StringMapKeyType), + DateTime(mm::DateTimeMapKeyType), + Object(mm::ObjectMapKeyType), +} + +impl MapKeyTypeDecl { + pub fn decorators(&self) -> &[mm::Decorator] { + match self { + Self::String(k) => k.decorators.as_deref().unwrap_or(&[]), + Self::DateTime(k) => k.decorators.as_deref().unwrap_or(&[]), + Self::Object(k) => k.decorators.as_deref().unwrap_or(&[]), + } + } + + pub fn type_name(&self) -> &str { + match self { + Self::String(_) => "String", + Self::DateTime(_) => "DateTime", + Self::Object(k) => &k.type_.name, + } + } +} + +// =================================================================== +// MapValueTypeDecl +// =================================================================== + +#[derive(Debug, Clone)] +pub enum MapValueTypeDecl { + Boolean(mm::BooleanMapValueType), + DateTime(mm::DateTimeMapValueType), + String(mm::StringMapValueType), + Integer(mm::IntegerMapValueType), + Long(mm::LongMapValueType), + Double(mm::DoubleMapValueType), + Object(mm::ObjectMapValueType), + Relationship(mm::RelationshipMapValueType), +} + +impl MapValueTypeDecl { + pub fn decorators(&self) -> &[mm::Decorator] { + match self { + Self::Boolean(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::DateTime(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::String(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::Integer(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::Long(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::Double(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::Object(v) => v.decorators.as_deref().unwrap_or(&[]), + Self::Relationship(v) => v.decorators.as_deref().unwrap_or(&[]), + } + } + + pub fn type_name(&self) -> &str { + match self { + Self::Boolean(_) => "Boolean", + Self::DateTime(_) => "DateTime", + Self::String(_) => "String", + Self::Integer(_) => "Integer", + Self::Long(_) => "Long", + Self::Double(_) => "Double", + Self::Object(v) => &v.type_.name, + Self::Relationship(v) => &v.type_.name, + } + } +} diff --git a/concerto-core/src/introspect/mod.rs b/concerto-core/src/introspect/mod.rs index a585815..f6239de 100644 --- a/concerto-core/src/introspect/mod.rs +++ b/concerto-core/src/introspect/mod.rs @@ -1,5 +1,8 @@ +pub mod declarations; pub mod decorator; +pub mod imports; +pub mod map_types; pub mod model_file; - -pub use decorator::Decorator; -pub use model_file::ModelFile; +pub mod properties; +pub mod traits; +pub mod validation; diff --git a/concerto-core/src/introspect/model_file.rs b/concerto-core/src/introspect/model_file.rs index b7dea1f..621be9b 100644 --- a/concerto-core/src/introspect/model_file.rs +++ b/concerto-core/src/introspect/model_file.rs @@ -1,36 +1,317 @@ -use concerto_macros::FromAst; -use concerto_metamodel::concerto_metamodel_1_0_0::{Declaration, Model}; +use std::collections::HashMap; -use crate::{types::ModelAst, ConcertoError, FromAst, Validate}; +use crate::error::{ConcertoError, Result}; +use crate::model_util; -#[derive(Debug, FromAst)] -#[concerto_ast_type(Model)] -#[wrapper(ModelAst)] +use super::declarations::*; +use super::imports::ImportDecl; +use super::traits::Named; + +/// A parsed model file with resolved declarations and imports. +#[derive(Debug, Clone)] pub struct ModelFile { - inner: ModelAst, + namespace: String, + version: Option, + declarations: Vec, + imports: Vec, + /// Maps short (local) type name → index into `declarations`. + local_types: HashMap, + file_name: Option, + is_external: bool, } -// Builder methods impl ModelFile { - pub fn new(ast_json: &str) -> Result { - ModelFile::from_json(ast_json) + /// Build a [`ModelFile`] by deserializing the raw JSON AST of a + /// `concerto.metamodel@1.0.0.Model`. + /// + /// This delegates to `Declaration::try_from` which inspects the `$class` + /// discriminator on each declaration and property to construct the correct + /// sum-type variant. + pub fn from_json(json: &serde_json::Value, file_name: Option) -> Result { + let namespace = json + .get("namespace") + .and_then(|v| v.as_str()) + .ok_or_else(|| ConcertoError::IllegalModel { + message: "Model missing 'namespace'".into(), + file_name: file_name.clone(), + location: None, + })? + .to_string(); + + let parsed_ns = model_util::parse_namespace(&namespace)?; + + let is_external = json + .get("sourceUri") + .and_then(|v| v.as_str()) + .is_some(); + + // --- imports ------------------------------------------------------- + let imports = match json.get("imports") { + Some(serde_json::Value::Array(arr)) => arr + .iter() + .map(|v| ImportDecl::try_from(v.clone())) + .collect::>>()?, + _ => vec![], + }; + + // --- declarations -------------------------------------------------- + let raw_decls = match json.get("declarations") { + Some(serde_json::Value::Array(arr)) => arr, + _ => return Ok(Self::new_empty(namespace, parsed_ns.version, file_name, is_external, imports)), + }; + + let mut declarations = Vec::with_capacity(raw_decls.len()); + let mut local_types = HashMap::new(); + + for (idx, raw) in raw_decls.iter().enumerate() { + let decl = Declaration::try_from(raw.clone()).map_err(|e| match e { + ConcertoError::IllegalModel { message, .. } => ConcertoError::IllegalModel { + message, + file_name: file_name.clone(), + location: None, + }, + other => other, + })?; + local_types.insert(decl.name().to_string(), idx); + declarations.push(decl); + } + + Ok(Self { + namespace, + version: parsed_ns.version, + declarations, + imports, + local_types, + file_name, + is_external, + }) } -} -// Public methods -impl ModelFile { - pub fn get_namespace(&self) -> &str { - &self.inner.0.namespace + fn new_empty( + namespace: String, + version: Option, + file_name: Option, + is_external: bool, + imports: Vec, + ) -> Self { + Self { + namespace, + version, + declarations: vec![], + imports, + local_types: HashMap::new(), + file_name, + is_external, + } + } + + // --------------------------------------------------------------- + // Accessors + // --------------------------------------------------------------- + + pub fn namespace(&self) -> &str { + &self.namespace + } + + pub fn version(&self) -> Option<&str> { + self.version.as_deref() } - pub fn get_declarations(&self) -> Option> { - self.inner.0.declarations.clone() + pub fn file_name(&self) -> Option<&str> { + self.file_name.as_deref() + } + + pub fn is_external(&self) -> bool { + self.is_external + } + + pub fn all_declarations(&self) -> &[Declaration] { + &self.declarations + } + + pub fn imports(&self) -> &[ImportDecl] { + &self.imports + } + + pub fn get_local_type(&self, name: &str) -> Option<&Declaration> { + self.local_types.get(name).map(|&idx| &self.declarations[idx]) + } + + /// Resolve a short type name to its fully-qualified name, checking + /// local types first, then imports. + pub fn fully_qualified_type_name(&self, short_name: &str) -> Result { + // 1. Primitive? + if model_util::is_primitive_type(short_name) { + return Ok(short_name.to_string()); + } + + // 2. Local type? + if self.local_types.contains_key(short_name) { + return Ok(model_util::fully_qualified_name(&self.namespace, short_name)); + } + + // 3. Imported type? + for imp in &self.imports { + match imp { + ImportDecl::Type(t) if t.name == short_name => { + return Ok(model_util::fully_qualified_name(&t.namespace, &t.name)); + } + ImportDecl::Types(t) => { + if t.types.iter().any(|n| n == short_name) { + return Ok(model_util::fully_qualified_name(&t.namespace, short_name)); + } + if let Some(aliased) = &t.aliased_types { + if let Some(at) = aliased.iter().find(|a| a.name == short_name) { + return Ok(model_util::fully_qualified_name( + &t.namespace, + &at.aliased_name, + )); + } + } + } + ImportDecl::All(_) => { + // Cannot resolve without the model manager — keep searching + } + _ => {} + } + } + + Err(ConcertoError::TypeNotFound { + type_name: short_name.to_string(), + }) + } + + /// Returns `true` when this is the `concerto@1.0.0` system model. + pub fn is_system_model_file(&self) -> bool { + self.namespace.starts_with("concerto@") || self.namespace == "concerto" + } + + // --------------------------------------------------------------- + // Filtered declaration accessors + // --------------------------------------------------------------- + + pub fn class_declarations(&self) -> Vec<&ClassDeclaration> { + self.declarations + .iter() + .filter_map(|d| d.as_class()) + .collect() + } + + pub fn concept_declarations(&self) -> Vec<&ConceptDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Concept(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn asset_declarations(&self) -> Vec<&AssetDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Asset(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn participant_declarations(&self) -> Vec<&ParticipantDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Participant(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn transaction_declarations(&self) -> Vec<&TransactionDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Transaction(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn event_declarations(&self) -> Vec<&EventDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Event(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn enum_declarations(&self) -> Vec<&EnumDeclaration> { + self.class_declarations() + .into_iter() + .filter_map(|c| match c { + ClassDeclaration::Enum(d) => Some(d), + _ => None, + }) + .collect() + } + + pub fn scalar_declarations(&self) -> Vec<&ScalarDeclaration> { + self.declarations + .iter() + .filter_map(|d| d.as_scalar()) + .collect() + } + + pub fn map_declarations(&self) -> Vec<&MapDeclaration> { + self.declarations + .iter() + .filter_map(|d| d.as_map()) + .collect() } } -// Impls -impl Validate for ModelFile { - fn validate(&self) -> Result<(), ConcertoError> { - unimplemented!() +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_root_model() { + let json = crate::rootmodel::root_model_ast(); + let mf = ModelFile::from_json(&json, Some("rootmodel".into())).unwrap(); + + assert_eq!(mf.namespace(), "concerto@1.0.0"); + assert_eq!(mf.version(), Some("1.0.0")); + assert!(mf.is_system_model_file()); + + // Root model has 5 declarations: Concept, Asset, Participant, Transaction, Event + assert_eq!(mf.all_declarations().len(), 5); + + // All are concept declarations in the root model + assert_eq!(mf.concept_declarations().len(), 5); + + // Check specific types exist + assert!(mf.get_local_type("Concept").is_some()); + assert!(mf.get_local_type("Asset").is_some()); + assert!(mf.get_local_type("Participant").is_some()); + assert!(mf.get_local_type("Transaction").is_some()); + assert!(mf.get_local_type("Event").is_some()); + } + + #[test] + fn test_fully_qualified_type_name() { + let json = crate::rootmodel::root_model_ast(); + let mf = ModelFile::from_json(&json, None).unwrap(); + + assert_eq!( + mf.fully_qualified_type_name("Concept").unwrap(), + "concerto@1.0.0.Concept" + ); + assert_eq!( + mf.fully_qualified_type_name("String").unwrap(), + "String" + ); + assert!(mf.fully_qualified_type_name("DoesNotExist").is_err()); } } diff --git a/concerto-core/src/introspect/properties.rs b/concerto-core/src/introspect/properties.rs new file mode 100644 index 0000000..69e2d1c --- /dev/null +++ b/concerto-core/src/introspect/properties.rs @@ -0,0 +1,178 @@ +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; + +// =================================================================== +// PropertyDecl — sum type replacing JS Property inheritance hierarchy +// =================================================================== + +#[derive(Debug, Clone)] +pub enum PropertyDecl { + Boolean(mm::BooleanProperty), + String(mm::StringProperty), + Integer(mm::IntegerProperty), + Long(mm::LongProperty), + Double(mm::DoubleProperty), + DateTime(mm::DateTimeProperty), + Object(mm::ObjectProperty), + Relationship(mm::RelationshipProperty), + Enum(mm::EnumProperty), +} + +/// Common accessors shared by all property variants. +impl PropertyDecl { + pub fn name(&self) -> &str { + match self { + Self::Boolean(p) => &p.name, + Self::String(p) => &p.name, + Self::Integer(p) => &p.name, + Self::Long(p) => &p.name, + Self::Double(p) => &p.name, + Self::DateTime(p) => &p.name, + Self::Object(p) => &p.name, + Self::Relationship(p) => &p.name, + Self::Enum(p) => &p.name, + } + } + + pub fn is_array(&self) -> bool { + match self { + Self::Boolean(p) => p.is_array, + Self::String(p) => p.is_array, + Self::Integer(p) => p.is_array, + Self::Long(p) => p.is_array, + Self::Double(p) => p.is_array, + Self::DateTime(p) => p.is_array, + Self::Object(p) => p.is_array, + Self::Relationship(p) => p.is_array, + Self::Enum(_) => false, + } + } + + pub fn is_optional(&self) -> bool { + match self { + Self::Boolean(p) => p.is_optional, + Self::String(p) => p.is_optional, + Self::Integer(p) => p.is_optional, + Self::Long(p) => p.is_optional, + Self::Double(p) => p.is_optional, + Self::DateTime(p) => p.is_optional, + Self::Object(p) => p.is_optional, + Self::Relationship(p) => p.is_optional, + Self::Enum(_) => false, + } + } + + /// Returns `true` when the property holds a primitive Concerto type + /// (Boolean, String, Integer, Long, Double, DateTime). + pub fn is_primitive(&self) -> bool { + matches!( + self, + Self::Boolean(_) + | Self::String(_) + | Self::Integer(_) + | Self::Long(_) + | Self::Double(_) + | Self::DateTime(_) + ) + } + + /// Returns `true` when this property is a relationship reference. + pub fn is_relationship(&self) -> bool { + matches!(self, Self::Relationship(_)) + } + + /// Returns `true` when this is an enum value member. + pub fn is_enum_value(&self) -> bool { + matches!(self, Self::Enum(_)) + } + + /// For `Object` and `Relationship` properties, returns the referenced + /// type identifier. For primitives this returns `None`. + pub fn type_identifier(&self) -> Option<&mm::TypeIdentifier> { + match self { + Self::Object(p) => Some(&p.type_), + Self::Relationship(p) => Some(&p.type_), + _ => None, + } + } + + /// The primitive type name for primitive properties, or the type + /// reference name for Object/Relationship. `None` only for Enum values. + pub fn type_name(&self) -> Option<&str> { + match self { + Self::Boolean(_) => Some("Boolean"), + Self::String(_) => Some("String"), + Self::Integer(_) => Some("Integer"), + Self::Long(_) => Some("Long"), + Self::Double(_) => Some("Double"), + Self::DateTime(_) => Some("DateTime"), + Self::Object(p) => Some(&p.type_.name), + Self::Relationship(p) => Some(&p.type_.name), + Self::Enum(_) => None, + } + } + + pub fn decorators(&self) -> &[mm::Decorator] { + match self { + Self::Boolean(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::String(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Integer(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Long(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Double(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::DateTime(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Object(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Relationship(p) => p.decorators.as_deref().unwrap_or(&[]), + Self::Enum(p) => p.decorators.as_deref().unwrap_or(&[]), + } + } + + pub fn location(&self) -> Option<&mm::Range> { + match self { + Self::Boolean(p) => p.location.as_ref(), + Self::String(p) => p.location.as_ref(), + Self::Integer(p) => p.location.as_ref(), + Self::Long(p) => p.location.as_ref(), + Self::Double(p) => p.location.as_ref(), + Self::DateTime(p) => p.location.as_ref(), + Self::Object(p) => p.location.as_ref(), + Self::Relationship(p) => p.location.as_ref(), + Self::Enum(p) => p.location.as_ref(), + } + } +} + +/// Construct a [`PropertyDecl`] from AST JSON by inspecting `$class`. +impl TryFrom for PropertyDecl { + type Error = crate::error::ConcertoError; + + fn try_from(value: serde_json::Value) -> crate::error::Result { + let class = value + .get("$class") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let kind = crate::model_util::get_short_name(&class).to_string(); + let mk_err = |e: serde_json::Error, k: &str| crate::error::ConcertoError::IllegalModel { + message: format!("Invalid {k}: {e}"), + file_name: None, + location: None, + }; + + match kind.as_str() { + "BooleanProperty" => Ok(Self::Boolean(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "StringProperty" => Ok(Self::String(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "IntegerProperty" => Ok(Self::Integer(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "LongProperty" => Ok(Self::Long(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "DoubleProperty" => Ok(Self::Double(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "DateTimeProperty" => Ok(Self::DateTime(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "ObjectProperty" => Ok(Self::Object(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "RelationshipProperty" => Ok(Self::Relationship(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + "EnumProperty" => Ok(Self::Enum(serde_json::from_value(value).map_err(|e| mk_err(e, &kind))?)), + _ => Err(crate::error::ConcertoError::IllegalModel { + message: format!("Unknown property type: {class}"), + file_name: None, + location: None, + }), + } + } +} diff --git a/concerto-core/src/introspect/traits.rs b/concerto-core/src/introspect/traits.rs new file mode 100644 index 0000000..68b314e --- /dev/null +++ b/concerto-core/src/introspect/traits.rs @@ -0,0 +1,57 @@ +use concerto_metamodel::concerto_metamodel_1_0_0::{Decorator, Range, TypeIdentifier}; + +use super::properties::PropertyDecl; + +// --------------------------------------------------------------------------- +// Decorated +// --------------------------------------------------------------------------- + +/// Mirrors the JS `Decorated` base class — provides access to decorators. +pub trait Decorated { + fn decorators(&self) -> &[Decorator]; + fn decorator(&self, name: &str) -> Option<&Decorator> { + self.decorators().iter().find(|d| d.name == name) + } +} + +// --------------------------------------------------------------------------- +// Named +// --------------------------------------------------------------------------- + +/// Something that has a name and an optional source location. +pub trait Named: Decorated { + fn name(&self) -> &str; + fn location(&self) -> Option<&Range>; +} + +// --------------------------------------------------------------------------- +// HasProperties +// --------------------------------------------------------------------------- + +/// A declaration that owns properties and may extend a super-type. +pub trait HasProperties: Named { + fn own_properties(&self) -> &[PropertyDecl]; + fn super_type(&self) -> Option<&TypeIdentifier>; + fn is_abstract(&self) -> bool; +} + +// --------------------------------------------------------------------------- +// Identifiable +// --------------------------------------------------------------------------- + +/// A declaration that may carry an identity field. +pub trait Identifiable: HasProperties { + fn is_identified(&self) -> bool; + fn is_system_identified(&self) -> bool; + fn is_explicitly_identified(&self) -> bool; + fn identifier_field_name(&self) -> Option<&str>; +} + +// --------------------------------------------------------------------------- +// Validate +// --------------------------------------------------------------------------- + +/// Types that can be validated against a [`ValidationContext`]. +pub trait Validate { + fn validate(&self, ctx: &super::validation::ValidationContext<'_>) -> crate::error::Result<()>; +} diff --git a/concerto-core/src/introspect/validation.rs b/concerto-core/src/introspect/validation.rs new file mode 100644 index 0000000..63c80ad --- /dev/null +++ b/concerto-core/src/introspect/validation.rs @@ -0,0 +1,380 @@ +use crate::error::{ConcertoError, Result}; +use crate::introspect::declarations::{ClassDeclaration, Declaration}; +use crate::introspect::model_file::ModelFile; +use crate::introspect::traits::{HasProperties, Identifiable, Named}; +use crate::model_util; + +use std::collections::{HashMap, HashSet}; + +// =================================================================== +// ValidationContext +// =================================================================== + +/// Provides type resolution context during model validation. +pub struct ValidationContext<'a> { + model_files: &'a HashMap, + current_namespace: &'a str, +} + +impl<'a> ValidationContext<'a> { + pub fn new(model_files: &'a HashMap, current_namespace: &'a str) -> Self { + Self { + model_files, + current_namespace, + } + } + + /// Resolve a fully-qualified type name to its declaration. + pub fn get_type(&self, fqn: &str) -> Option<&'a Declaration> { + let ns = model_util::get_namespace(fqn); + let short = model_util::get_short_name(fqn); + self.model_files + .get(ns) + .and_then(|mf| mf.get_local_type(short)) + } + + pub fn current_model_file(&self) -> Option<&'a ModelFile> { + self.model_files.get(self.current_namespace) + } +} + +// =================================================================== +// Model Validation +// =================================================================== + +/// Validate all model files in the given map. +pub fn validate_model_files(model_files: &HashMap) -> Result<()> { + for (ns, mf) in model_files { + let ctx = ValidationContext::new(model_files, ns); + validate_model_file(mf, &ctx)?; + } + Ok(()) +} + +/// Validate a single model file. +fn validate_model_file(mf: &ModelFile, ctx: &ValidationContext<'_>) -> Result<()> { + for decl in mf.all_declarations() { + match decl { + Declaration::Class(cd) => validate_class_declaration(cd, mf, ctx)?, + Declaration::Scalar(sd) => validate_scalar_declaration(sd, mf)?, + Declaration::Map(md) => validate_map_declaration(md, mf)?, + } + } + Ok(()) +} + +// ------------------------------------------------------------------- +// ClassDeclaration validation +// ------------------------------------------------------------------- + +fn validate_class_declaration( + cd: &ClassDeclaration, + mf: &ModelFile, + ctx: &ValidationContext<'_>, +) -> Result<()> { + // 1. Validate super-type + if let Some(super_type_id) = cd.super_type() { + let super_fqn = resolve_type_identifier(super_type_id, mf)?; + + // Super type must exist + let super_decl = ctx.get_type(&super_fqn).ok_or_else(|| { + ConcertoError::IllegalModel { + message: format!( + "Super type '{}' not found for '{}'", + super_fqn, + cd.name() + ), + file_name: mf.file_name().map(String::from), + location: None, + } + })?; + + // Super type must be a class declaration + if super_decl.as_class().is_none() { + return Err(ConcertoError::IllegalModel { + message: format!( + "Super type '{}' of '{}' is not a class declaration", + super_fqn, + cd.name() + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + + // Check for circular inheritance + check_circular_inheritance(cd, mf, ctx)?; + } + + // 2. Validate identifier field (for identified declarations) + if cd.is_identified() && !cd.is_enum() { + validate_identifier_field(cd, mf, ctx)?; + } + + // 3. Check for duplicate property names + check_duplicate_properties(cd, mf, ctx)?; + + // 4. Validate each property + for prop in cd.own_properties() { + validate_property(prop, mf, ctx)?; + } + + Ok(()) +} + +fn resolve_type_identifier( + type_id: &concerto_metamodel::concerto_metamodel_1_0_0::TypeIdentifier, + mf: &ModelFile, +) -> Result { + // If already resolved, use that + if let Some(ref resolved) = type_id.resolved_name { + return Ok(resolved.clone()); + } + // If namespace is present, build FQN directly + if let Some(ref ns) = type_id.namespace { + return Ok(model_util::fully_qualified_name(ns, &type_id.name)); + } + // Otherwise resolve through the model file + mf.fully_qualified_type_name(&type_id.name) +} + +fn check_circular_inheritance( + cd: &ClassDeclaration, + mf: &ModelFile, + ctx: &ValidationContext<'_>, +) -> Result<()> { + let mut visited = HashSet::new(); + let start_fqn = model_util::fully_qualified_name(mf.namespace(), cd.name()); + visited.insert(start_fqn.clone()); + + let mut current: Option<&ClassDeclaration> = Some(cd); + while let Some(decl) = current { + if let Some(super_type_id) = decl.super_type() { + let super_fqn = resolve_type_identifier(super_type_id, mf) + .or_else(|_| { + // Try resolving from model files directly + if let Some(ref ns) = super_type_id.namespace { + Ok(model_util::fully_qualified_name(ns, &super_type_id.name)) + } else { + Ok(model_util::fully_qualified_name( + mf.namespace(), + &super_type_id.name, + )) + } + })?; + + if !visited.insert(super_fqn.clone()) { + return Err(ConcertoError::IllegalModel { + message: format!( + "Circular inheritance detected for '{}'", + start_fqn + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + + current = ctx + .get_type(&super_fqn) + .and_then(|d| d.as_class()); + } else { + current = None; + } + } + + Ok(()) +} + +fn validate_identifier_field( + cd: &ClassDeclaration, + mf: &ModelFile, + ctx: &ValidationContext<'_>, +) -> Result<()> { + // System-identified declarations don't need an explicit field + if cd.is_system_identified() { + return Ok(()); + } + + // For explicitly-identified declarations, find the identifier field + // Walk properties including inherited ones + if let Some(id_field_name) = cd.identifier_field_name() { + let prop = find_property_in_chain(cd, id_field_name, mf, ctx); + if prop.is_none() { + return Err(ConcertoError::IllegalModel { + message: format!( + "Identifier field '{}' not found on '{}'", + id_field_name, + cd.name() + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + } + + Ok(()) +} + +/// Find a property by name, walking the super-type chain. +fn find_property_in_chain<'a>( + cd: &'a ClassDeclaration, + name: &str, + mf: &'a ModelFile, + ctx: &'a ValidationContext<'_>, +) -> Option<&'a crate::introspect::properties::PropertyDecl> { + // Check own properties first + if let Some(p) = cd.own_properties().iter().find(|p| p.name() == name) { + return Some(p); + } + + // Walk super-type chain + if let Some(super_type_id) = cd.super_type() { + let super_fqn = resolve_type_identifier(super_type_id, mf).ok()?; + let super_cd = ctx.get_type(&super_fqn)?.as_class()?; + return find_property_in_chain(super_cd, name, mf, ctx); + } + + None +} + +fn check_duplicate_properties( + cd: &ClassDeclaration, + mf: &ModelFile, + ctx: &ValidationContext<'_>, +) -> Result<()> { + let mut seen = HashSet::new(); + + // Collect all property names including inherited + collect_property_names(cd, mf, ctx, &mut seen)?; + + Ok(()) +} + +fn collect_property_names( + cd: &ClassDeclaration, + mf: &ModelFile, + ctx: &ValidationContext<'_>, + seen: &mut HashSet, +) -> Result<()> { + // First collect inherited property names + if let Some(super_type_id) = cd.super_type() { + if let Ok(super_fqn) = resolve_type_identifier(super_type_id, mf) { + if let Some(super_cd) = ctx.get_type(&super_fqn).and_then(|d| d.as_class()) { + collect_property_names(super_cd, mf, ctx, seen)?; + } + } + } + + // Then check own properties for duplicates + for prop in cd.own_properties() { + if !seen.insert(prop.name().to_string()) { + return Err(ConcertoError::IllegalModel { + message: format!( + "Duplicate property name '{}' in '{}'", + prop.name(), + cd.name() + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + } + + Ok(()) +} + +// ------------------------------------------------------------------- +// Property validation +// ------------------------------------------------------------------- + +fn validate_property( + prop: &crate::introspect::properties::PropertyDecl, + mf: &ModelFile, + ctx: &ValidationContext<'_>, +) -> Result<()> { + use crate::introspect::properties::PropertyDecl; + + match prop { + PropertyDecl::Object(p) => { + let fqn = resolve_type_identifier(&p.type_, mf)?; + if ctx.get_type(&fqn).is_none() && !model_util::is_primitive_type(&p.type_.name) { + return Err(ConcertoError::IllegalModel { + message: format!( + "Type '{}' not found for property '{}'", + fqn, p.name + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + } + PropertyDecl::Relationship(p) => { + let fqn = resolve_type_identifier(&p.type_, mf)?; + // Relationship type must not be primitive + if model_util::is_primitive_type(&p.type_.name) { + return Err(ConcertoError::IllegalModel { + message: format!( + "Relationship '{}' cannot have primitive type '{}'", + p.name, p.type_.name + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + // Relationship target must exist + let target = ctx.get_type(&fqn).ok_or_else(|| ConcertoError::IllegalModel { + message: format!( + "Relationship target type '{}' not found for '{}'", + fqn, p.name + ), + file_name: mf.file_name().map(String::from), + location: None, + })?; + // Relationship target must be identified + if let Some(class_decl) = target.as_class() { + if !class_decl.is_identified() { + return Err(ConcertoError::IllegalModel { + message: format!( + "Relationship '{}' must point to an identified type, but '{}' is not identified", + p.name, fqn + ), + file_name: mf.file_name().map(String::from), + location: None, + }); + } + } + } + // Primitive properties don't need type resolution + _ => {} + } + + Ok(()) +} + +// ------------------------------------------------------------------- +// Scalar validation +// ------------------------------------------------------------------- + +fn validate_scalar_declaration( + _sd: &crate::introspect::declarations::ScalarDeclaration, + _mf: &ModelFile, +) -> Result<()> { + // Scalar validation: check that default value passes validator. + // This is a simplified version — full validator execution is done + // at runtime. + Ok(()) +} + +// ------------------------------------------------------------------- +// Map validation +// ------------------------------------------------------------------- + +fn validate_map_declaration( + _md: &crate::introspect::declarations::MapDeclaration, + _mf: &ModelFile, +) -> Result<()> { + // Map key must be String, DateTime, or a scalar thereof. + // Map value must not be a Map. + // Simplified — full checking deferred to runtime validation. + Ok(()) +} diff --git a/concerto-core/src/lib.rs b/concerto-core/src/lib.rs index a39352e..1c54f05 100644 --- a/concerto-core/src/lib.rs +++ b/concerto-core/src/lib.rs @@ -1,90 +1,14 @@ -// Main library file for concerto_core - -// Define modules +//! # Concerto +//! +//! Concerto is a lightweight data modeling (schema) language and runtime for business concepts. +//! +//! Refer to the [language documentation](https://concerto.accordproject.org/) for more information. +//! +//! This crate is the "core" implementation of Concerto in Rust language. +//! pub mod error; pub mod introspect; -pub mod metamodel_validation; pub mod model_manager; -pub mod traits; -pub mod types; -pub mod util; - -// Re-export the main components -pub use model_manager::ModelManager; -pub use traits::*; - -// Metamodel types exports -pub use concerto_metamodel::concerto_metamodel_1_0_0::{ - Declaration, Decorator, Import, Model, Property, TypeIdentifier, -}; - -pub use error::ConcertoError; - -/// Version of the Concerto core library -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// Provides introspection capabilities for Concerto models -/// Maps from various introspect-related JavaScript classes -pub mod introspection { - use crate::error::ConcertoError; - - /// Checks if a type name is valid in Concerto - pub fn is_valid_type_name(name: &str) -> bool { - if name.is_empty() { - return false; - } - - // Check if name starts with a letter and contains only alphanumeric or underscore - name.chars() - .next() - .map_or(false, |c| c.is_ascii_alphabetic()) - && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') - } - - /// Checks if a namespace is valid in Concerto - pub fn is_valid_namespace(namespace: &str) -> bool { - if namespace.is_empty() { - return false; - } - - // Check if namespace follows dot notation pattern - namespace.split('.').all(|part| is_valid_type_name(part)) - } - - /// Gets the fully qualified name for a declaration - pub fn get_fully_qualified_name(namespace: &str, name: &str) -> Result { - if !is_valid_namespace(namespace) { - return Err(ConcertoError::ValidationError(format!( - "Invalid namespace: {}", - namespace - ))); - } - - if !is_valid_type_name(name) { - return Err(ConcertoError::ValidationError(format!( - "Invalid type name: {}", - name - ))); - } - - Ok(format!("{}.{}", namespace, name)) - } -} - -/// Utility functions for Concerto -pub mod utility { - use semver::Version; - - /// Checks if a string is a valid semver version - pub fn is_valid_semantic_version(version: &str) -> bool { - Version::parse(version).is_ok() - } - - /// Compares two semver versions - pub fn compare_versions(version1: &str, version2: &str) -> Result { - let v1 = Version::parse(version1).map_err(|e| e.to_string())?; - let v2 = Version::parse(version2).map_err(|e| e.to_string())?; - - Ok(v1.cmp(&v2)) - } -} +pub mod model_util; +pub mod rootmodel; +pub mod validation; diff --git a/concerto-core/src/metamodel_validation.rs b/concerto-core/src/metamodel_validation.rs deleted file mode 100644 index 8b4c130..0000000 --- a/concerto-core/src/metamodel_validation.rs +++ /dev/null @@ -1,252 +0,0 @@ -use concerto_metamodel::concerto_metamodel_1_0_0::*; - -use crate::error::ConcertoError; -use crate::traits::Validate; - -impl Validate for Model { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate namespace - if self.namespace.is_empty() { - return Err(ConcertoError::ValidationError( - "Namespace cannot be empty".to_string(), - )); - } - - // Validate declarations if any - if let Some(declarations) = &self.declarations { - // Check for duplicate declaration names - let mut declaration_names = std::collections::HashSet::new(); - for decl in declarations { - // Check if this declaration name has already been seen - if !declaration_names.insert(&decl.name) { - return Err(ConcertoError::ValidationError(format!( - "Duplicate declaration name: {}", - decl.name - ))); - } - - // Validate the declaration itself - decl.validate()?; - } - } - - // Validate decorators if any - if let Some(decorators) = &self.decorators { - for decorator in decorators { - decorator.validate()?; - } - } - - Ok(()) - } -} - -impl Validate for Declaration { - fn validate(&self) -> Result<(), ConcertoError> { - // Basic validation of a declaration using the common validator - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } -} - -// Implement DeclarationValidator for the Declaration struct -impl DeclarationValidator for Declaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - crate::traits::CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - crate::traits::CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl Validate for Decorator { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate the name of the decorator - if self.name.is_empty() { - return Err(ConcertoError::ValidationError( - "Decorator name cannot be empty".to_string(), - )); - } - - // Arguments validation would go here if needed - - Ok(()) - } -} - -// Use DeclarationValidator trait for implementing Validate -use crate::traits::DeclarationValidator; -use crate::util::is_valid_identifier; - -impl Validate for AssetDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - // We can use our new trait for validation - self.validate_declaration() - } -} - -impl Validate for ConceptDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - // Call the common declaration validation - self.validate_declaration()?; - - // Check for duplicate property names - let mut property_names = std::collections::HashSet::new(); - for property in &self.properties { - // Check if this property name has already been seen - if !property_names.insert(&property.name) { - return Err(ConcertoError::ValidationError(format!( - "Duplicate property name: {} in concept {}", - property.name, self.name - ))); - } - - // Validate the property itself - property.validate()?; - } - - Ok(()) - } -} - -impl Validate for ParticipantDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - self.validate_declaration() - } -} - -impl Validate for TransactionDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - self.validate_declaration() - } -} - -impl Validate for EventDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - self.validate_declaration() - } -} - -impl Validate for EnumDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - // Use our new trait for validation - self.validate_declaration() - } -} - -impl Validate for EnumProperty { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate enum property name - if !is_valid_identifier(&self.name) { - return Err(ConcertoError::ValidationError(format!("'{}' is not a valid enum property name. Identifiers must start with a letter and can contain only letters, numbers, or underscores", self.name))); - } - - // Validate decorators if present - if let Some(decs) = &self.decorators { - for decorator in decs { - decorator.validate()?; - } - } - - Ok(()) - } -} - -impl Validate for MapKeyType { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate decorators if present - if let Some(decorators) = &self.decorators { - for decorator in decorators { - decorator.validate()?; - } - } - - // Basic validation - other checks happen in MapDeclaration::validate_declaration - Ok(()) - } -} - -// Add validation for MapValueType -impl Validate for MapValueType { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate decorators if present - if let Some(decorators) = &self.decorators { - for decorator in decorators { - decorator.validate()?; - } - } - - // Each value type might have additional specific validation requirements - // but for now this is sufficient - - Ok(()) - } -} - -// Fix the MapDeclaration validate method to use our new trait -impl Validate for MapDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - // Use our new trait for validation - self.validate_declaration() - } -} - -impl Validate for ScalarDeclaration { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for Property { - fn validate(&self) -> Result<(), ConcertoError> { - // For the standalone Validate trait, we'll do basic validation - // The PropertyValidator trait is for validation within a model context - - // Validate property name - if !is_valid_identifier(&self.name) { - return Err(ConcertoError::ValidationError(format!("'{}' is not a valid property name. Identifiers must start with a letter and can contain only letters, numbers, or underscores", self.name))); - } - - // TODO check for other reserved names - - // Check for system-reserved property names - if self.name.starts_with('$') { - return Err(ConcertoError::ValidationError(format!("Invalid field name '{}'. Property names starting with $ are reserved for system use", self.name))); - } - - // Validate decorators if present - if let Some(decs) = &self.decorators { - for decorator in decs { - decorator.validate()?; - } - } - - Ok(()) - } -} - -// Import validation -impl Validate for Import { - fn validate(&self) -> Result<(), ConcertoError> { - // Validate namespace - if self.namespace.is_empty() { - return Err(ConcertoError::ValidationError( - "Import namespace cannot be empty".to_string(), - )); - } - - Ok(()) - } -} diff --git a/concerto-core/src/model_manager.rs b/concerto-core/src/model_manager.rs index 94014e6..0aaa883 100644 --- a/concerto-core/src/model_manager.rs +++ b/concerto-core/src/model_manager.rs @@ -1,264 +1,268 @@ -use std::collections::HashMap; - -use concerto_metamodel::concerto_metamodel_1_0_0::*; +use crate::error::{ConcertoError, Result}; +use crate::introspect::declarations::{ClassDeclaration, Declaration}; +use crate::introspect::model_file::ModelFile; +use crate::introspect::validation; +use crate::model_util; -use crate::error::ConcertoError; -use crate::introspect::ModelFile; -use crate::traits::*; +use std::collections::HashMap; -/// Manages models and provides validation -/// Maps from JavaScript ModelManager class but using metamodel types -#[derive(Debug, Default)] -pub struct ModelManager { - /// The model files managed by this instance - pub models: HashMap, +// =================================================================== +// ModelManagerOptions +// =================================================================== - /// Whether strict validation is enabled +/// Configuration for [`ModelManager`]. +#[derive(Debug, Clone)] +pub struct ModelManagerOptions { + /// Require versioned namespaces. pub strict: bool, + /// Enable the Map type feature. + pub enable_map_type: bool, } -impl ModelManager { - /// Creates a new model manager - pub fn new(strict: bool) -> Self { - ModelManager { - models: HashMap::new(), - strict, +impl Default for ModelManagerOptions { + fn default() -> Self { + Self { + strict: false, + enable_map_type: true, } } +} - /// Adds a model file to the model manager - pub fn add_model_file(&mut self, model_file: ModelFile) -> Result<(), ConcertoError> { - // Basic validation of the model file (without cross-model validation) - model_file.validate()?; - - // Add to our collection - self.models - .insert(model_file.get_namespace().to_string(), model_file); +// =================================================================== +// ModelManager +// =================================================================== - Ok(()) - } +/// Manages a set of Concerto model files, providing type resolution +/// and validation across namespaces. +#[derive(Debug)] +pub struct ModelManager { + model_files: HashMap, + options: ModelManagerOptions, +} - /// Gets a model file by namespace - pub fn get_model_file(&self, namespace: &str) -> Option<&ModelFile> { - self.models.get(namespace) +impl ModelManager { + /// Creates a new [`ModelManager`] with the built-in root model loaded. + pub fn new(options: ModelManagerOptions) -> Result { + let mut mgr = Self { + model_files: HashMap::new(), + options, + }; + mgr.add_root_model()?; + Ok(mgr) } - /// Gets all model files - pub fn get_model_files(&self) -> Vec<&ModelFile> { - self.models.values().collect() + fn add_root_model(&mut self) -> Result<()> { + let json = crate::rootmodel::root_model_ast(); + let mf = ModelFile::from_json(&json, Some("rootmodel".into()))?; + self.model_files.insert(mf.namespace().to_string(), mf); + Ok(()) } - /// Validates all models - pub fn validate_models(&self) -> Result<(), ConcertoError> { - // First, validate each model file individually - for model_file in self.models.values() { - model_file.validate()?; + /// Add a model from its JSON AST representation. + pub fn add_model( + &mut self, + json: &serde_json::Value, + file_name: Option, + disable_validation: bool, + ) -> Result<()> { + let mf = ModelFile::from_json(json, file_name)?; + let ns = mf.namespace().to_string(); + + if self.model_files.contains_key(&ns) { + return Err(ConcertoError::IllegalModel { + message: format!("Duplicate namespace: {ns}"), + file_name: mf.file_name().map(String::from), + location: None, + }); } - // Then perform cross-model validation - self.validate_references()?; + self.model_files.insert(ns, mf); - // Validate no circular inheritance - self.validate_no_circular_inheritance()?; + if !disable_validation { + self.validate_model_files()?; + } Ok(()) } - /// Validates that there is no circular inheritance in the model - fn validate_no_circular_inheritance(&self) -> Result<(), ConcertoError> { - // This implementation specifically handles the test case in conformance_tests.rs - // In a full implementation, we would need proper type information and safe casting - - // Check each namespace for circular inheritance - for model_file in self.models.values() { - // Get the namespace name - let namespace = &model_file.get_namespace(); - - // Get all declarations in this namespace - if let Some(declarations) = &model_file.get_declarations() { - // Create a map of class name to superclass name - let mut inheritance_map = std::collections::HashMap::new(); - - // First pass: build the inheritance map - for decl in declarations { - // We only care about declarations with possible inheritance - if decl._class.contains("ConceptDeclaration") { - // For the test case, we're looking for "Person" and "Employee" with circular references - // In a real implementation, we'd need to safely cast to ConceptDeclaration to get super_type - - // For our test case, we know Person extends Employee and Employee extends Person - if decl.name == "Person" { - inheritance_map.insert("Person", "Employee"); - } else if decl.name == "Employee" { - inheritance_map.insert("Employee", "Person"); - } - } + /// Add multiple models at once. On error, all additions are rolled back. + pub fn add_models( + &mut self, + models: &[serde_json::Value], + disable_validation: bool, + ) -> Result<()> { + let mut added_namespaces = Vec::new(); + + for json in models { + let mf = ModelFile::from_json(json, None)?; + let ns = mf.namespace().to_string(); + + if self.model_files.contains_key(&ns) { + // Rollback + for ns in &added_namespaces { + self.model_files.remove(ns); } + return Err(ConcertoError::IllegalModel { + message: format!("Duplicate namespace: {ns}"), + file_name: mf.file_name().map(String::from), + location: None, + }); + } - // Second pass: check for cycles in the inheritance map - for (class_name, super_name) in &inheritance_map { - // Track visited classes to detect cycles - let mut visited = std::collections::HashSet::new(); - visited.insert(*class_name); - - // Start at the superclass - let mut current = *super_name; - - // Follow the inheritance chain - while let Some(next_super) = inheritance_map.get(current) { - // If we've seen this class before, we have a cycle - if !visited.insert(current) { - return Err(ConcertoError::ValidationError(format!( - "Circular inheritance detected for class {}", - class_name - ))); - } + self.model_files.insert(ns.clone(), mf); + added_namespaces.push(ns); + } - current = next_super; - } + if !disable_validation { + if let Err(e) = self.validate_model_files() { + // Rollback + for ns in &added_namespaces { + self.model_files.remove(ns); } + return Err(e); } } Ok(()) } - /// Validates all references between models - fn validate_references(&self) -> Result<(), ConcertoError> { - // Iterate through all model files - for model_file in self.models.values() { - // Get declarations for this model file - if let Some(declarations) = &model_file.get_declarations() { - // Check each declaration - using traits to handle different types - for declaration in declarations { - // Check for concept declarations that might have super types - if let Some(concept) = self.as_concept_declaration(declaration) { - // Validate super type if present - if let Some(super_type) = concept.get_super_type() { - self.validate_type_exists(super_type)?; - } + /// Validate all loaded model files. + pub fn validate_model_files(&self) -> Result<()> { + validation::validate_model_files(&self.model_files) + } - // Validate property types - for property in concept.get_properties() { - self.validate_property(property)?; - } - } + /// Resolve a fully-qualified type name to its declaration. + pub fn get_type(&self, fqn: &str) -> Result<&Declaration> { + let ns = model_util::get_namespace(fqn); + let short = model_util::get_short_name(fqn); - // Check for map declarations - if let Some(map_decl) = self.as_map_declaration(declaration) { - self.validate_map_value_type(&map_decl.value)?; - } - } + let mf = self.model_files.get(ns).ok_or_else(|| { + ConcertoError::TypeNotFound { + type_name: fqn.to_string(), } - } + })?; - Ok(()) + mf.get_local_type(short).ok_or_else(|| { + ConcertoError::TypeNotFound { + type_name: fqn.to_string(), + } + }) } - /// Helper method to treat a declaration as a concept declaration - fn as_concept_declaration<'a>( - &self, - decl: &'a Declaration, - ) -> Option<&'a dyn ConceptDeclarationBase> { - // Check class name to determine type - match decl._class.as_str() { - "concerto.metamodel@1.0.0.ConceptDeclaration" => { - // We need to cast the declaration to a ConceptDeclaration - // This would require unsafe code or a different approach in real code - // For now, this is just a placeholder for the concept - None - } - "concerto.metamodel@1.0.0.AssetDeclaration" => None, - "concerto.metamodel@1.0.0.ParticipantDeclaration" => None, - "concerto.metamodel@1.0.0.TransactionDeclaration" => None, - "concerto.metamodel@1.0.0.EventDeclaration" => None, - _ => None, - } + /// Look up a model file by namespace. + pub fn get_model_file(&self, namespace: &str) -> Option<&ModelFile> { + self.model_files.get(namespace) } - /// Helper method to treat a declaration as a map declaration - fn as_map_declaration<'a>(&self, decl: &'a Declaration) -> Option<&'a MapDeclaration> { - if decl._class == "concerto.metamodel@1.0.0.MapDeclaration" { - // This would require casting in real code - None - } else { - None - } + /// Returns all loaded model files (excluding system namespaces). + pub fn get_model_files(&self) -> Vec<&ModelFile> { + self.model_files + .values() + .filter(|mf| !mf.is_system_model_file()) + .collect() } - /// Validates a property - fn validate_property(&self, property: &Property) -> Result<(), ConcertoError> { - // Use our PropertyValidator trait - use crate::traits::PropertyValidator; - PropertyValidator::validate(property, self) + /// Returns all loaded model files including system namespaces. + pub fn get_all_model_files(&self) -> Vec<&ModelFile> { + self.model_files.values().collect() } - /// Validates a map value type - fn validate_map_value_type(&self, value_type: &MapValueType) -> Result<(), ConcertoError> { - // Implementation would depend on the MapValueType structure - Ok(()) + pub fn options(&self) -> &ModelManagerOptions { + &self.options } - /// Validates that a referenced type exists in the model - pub fn validate_type_exists( - &self, - type_id: &concerto_metamodel::concerto_metamodel_1_0_0::TypeIdentifier, - ) -> Result<(), ConcertoError> { - let namespace = match &type_id.namespace { - Some(ns) => ns, - None => { - return Err(ConcertoError::ValidationError(format!( - "Type {} is missing namespace", - type_id.name - ))); - } - }; + /// Returns all class declarations across all model files. + pub fn class_declarations(&self) -> Vec<&ClassDeclaration> { + self.model_files + .values() + .flat_map(|mf| mf.class_declarations()) + .collect() + } - // Find the model file for this namespace - let model_file = match self.get_model_file(namespace) { - Some(mf) => mf, - None => { - return Err(ConcertoError::ValidationError(format!( - "Could not find namespace {}", - namespace - ))); - } - }; + /// Look up a class declaration by FQN. + pub fn get_class_declaration(&self, fqn: &str) -> Result<&ClassDeclaration> { + let decl = self.get_type(fqn)?; + decl.as_class().ok_or_else(|| ConcertoError::TypeNotFound { + type_name: fqn.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; - // Check if type exists in this namespace using the DeclarationBase trait - if let Some(declarations) = &model_file.get_declarations() { - for decl in declarations { - // Use the DeclarationBase trait to get name regardless of declaration type - if decl.name == type_id.name { - return Ok(()); + #[test] + fn test_model_manager_creates_with_root_model() { + let mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); + assert!(mgr.get_model_file("concerto@1.0.0").is_some()); + assert!(mgr.get_type("concerto@1.0.0.Concept").is_ok()); + assert!(mgr.get_type("concerto@1.0.0.Asset").is_ok()); + } + + #[test] + fn test_model_manager_add_model() { + let mut mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); + + let model_json = serde_json::json!({ + "$class": "concerto.metamodel@1.0.0.Model", + "namespace": "org.example@1.0.0", + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Person", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + } + ] } - } - } + ] + }); - Err(ConcertoError::ValidationError(format!( - "Could not find type {}.{}", - namespace, type_id.name - ))) + mgr.add_model(&model_json, None, false).unwrap(); + assert!(mgr.get_type("org.example@1.0.0.Person").is_ok()); } - /// Validates a property type - fn validate_property_type(&self, property: &Property) -> Result<(), ConcertoError> { - // Using the PropertyValidator trait to validate based on property type - // In a real implementation, you'd check the _class field and cast to the appropriate type - // For demonstration purposes, we'll just return Ok - - // Example of how it might work: - // if property._class == "concerto.metamodel@1.0.0.RelationshipProperty" { - // let relationship = property as &RelationshipProperty; - // if let Some(type_id) = &relationship.type_reference { - // self.validate_type_exists(type_id)?; - // } else { - // return Err(ConcertoError::ValidationError( - // "Relationship type is missing type reference".to_string() - // )); - // } - // } + #[test] + fn test_duplicate_namespace_rejected() { + let mut mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); - Ok(()) + let model_json = serde_json::json!({ + "$class": "concerto.metamodel@1.0.0.Model", + "namespace": "org.example@1.0.0", + "declarations": [] + }); + + mgr.add_model(&model_json, None, true).unwrap(); + let result = mgr.add_model(&model_json, None, true); + assert!(result.is_err()); + } + + #[test] + fn test_batch_add_with_rollback() { + let mut mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); + + let models = vec![ + serde_json::json!({ + "$class": "concerto.metamodel@1.0.0.Model", + "namespace": "org.a@1.0.0", + "declarations": [] + }), + serde_json::json!({ + "$class": "concerto.metamodel@1.0.0.Model", + "namespace": "org.a@1.0.0", + "declarations": [] + }), + ]; + + let result = mgr.add_models(&models, true); + assert!(result.is_err()); + // First namespace should have been rolled back + assert!(mgr.get_model_file("org.a@1.0.0").is_none()); } } diff --git a/concerto-core/src/model_util.rs b/concerto-core/src/model_util.rs new file mode 100644 index 0000000..d878a6c --- /dev/null +++ b/concerto-core/src/model_util.rs @@ -0,0 +1,242 @@ +use std::sync::LazyLock; + +use regex::Regex; + +// Unicode-aware identifier regex, mirrors the JS ID_REGEX. +// Rust's `regex` crate supports Unicode categories with \p{…}. +static ID_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r"^[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}$_][\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}$_\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*$", + ) + .expect("ID_REGEX is valid") +}); + +const PRIMITIVE_TYPES: &[&str] = &["Boolean", "String", "DateTime", "Double", "Integer", "Long"]; + +const RESERVED_PROPERTIES: &[&str] = &[ + "$class", + "$identifier", + "$timestamp", + // private / internal + "$classDeclaration", + "$namespace", + "$type", + "$modelManager", + "$validator", + "$identifierFieldName", + "$imports", + "$superTypes", + "$id", +]; + +const PRIVATE_RESERVED_PROPERTIES: &[&str] = &[ + "$classDeclaration", + "$namespace", + "$type", + "$modelManager", + "$validator", + "$identifierFieldName", + "$imports", + "$superTypes", + "$id", +]; + +/// Returns everything after the last dot of the fully-qualified name. +/// +/// ``` +/// # use concerto_core::model_util::get_short_name; +/// assert_eq!(get_short_name("org.example.Asset"), "Asset"); +/// assert_eq!(get_short_name("Asset"), "Asset"); +/// ``` +pub fn get_short_name(fqn: &str) -> &str { + match fqn.rfind('.') { + Some(idx) => &fqn[idx + 1..], + None => fqn, + } +} + +/// Returns the namespace portion of a fully-qualified name (everything +/// before the last dot). Returns an empty string if there is no dot. +/// +/// ``` +/// # use concerto_core::model_util::get_namespace; +/// assert_eq!(get_namespace("org.example.Asset"), "org.example"); +/// assert_eq!(get_namespace("Asset"), ""); +/// ``` +pub fn get_namespace(fqn: &str) -> &str { + match fqn.rfind('.') { + Some(idx) => &fqn[..idx], + None => "", + } +} + +/// Builds a fully-qualified name from a namespace and a short type name. +/// +/// ``` +/// # use concerto_core::model_util::fully_qualified_name; +/// assert_eq!(fully_qualified_name("org.example", "Asset"), "org.example.Asset"); +/// assert_eq!(fully_qualified_name("", "Asset"), "Asset"); +/// ``` +pub fn fully_qualified_name(namespace: &str, type_name: &str) -> String { + if namespace.is_empty() { + type_name.to_string() + } else { + format!("{namespace}.{type_name}") + } +} + +/// Result of parsing a potentially-versioned namespace string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedNamespace { + /// The namespace name without the version. + pub name: String, + /// The version string (e.g. `"1.0.0"`), if present. + pub version: Option, +} + +/// Parses a namespace that may contain a `@version` suffix. +/// +/// ``` +/// # use concerto_core::model_util::parse_namespace; +/// let ns = parse_namespace("org.example@1.0.0").unwrap(); +/// assert_eq!(ns.name, "org.example"); +/// assert_eq!(ns.version.as_deref(), Some("1.0.0")); +/// +/// let ns = parse_namespace("org.example").unwrap(); +/// assert_eq!(ns.name, "org.example"); +/// assert_eq!(ns.version, None); +/// ``` +pub fn parse_namespace(ns: &str) -> crate::error::Result { + let parts: Vec<&str> = ns.split('@').collect(); + match parts.len() { + 1 => Ok(ParsedNamespace { + name: parts[0].to_string(), + version: None, + }), + 2 => Ok(ParsedNamespace { + name: parts[0].to_string(), + version: Some(parts[1].to_string()), + }), + _ => Err(crate::error::ConcertoError::IllegalModel { + message: format!("Invalid namespace {ns}"), + file_name: None, + location: None, + }), + } +} + +/// Returns `true` if `type_name` is one of the Concerto primitive types. +pub fn is_primitive_type(type_name: &str) -> bool { + PRIMITIVE_TYPES.contains(&type_name) +} + +/// Returns `true` if `name` is a reserved system property (e.g. `$class`). +pub fn is_system_property(name: &str) -> bool { + RESERVED_PROPERTIES.contains(&name) +} + +/// Returns `true` if `name` is a private (internal-only) system property. +pub fn is_private_system_property(name: &str) -> bool { + PRIVATE_RESERVED_PROPERTIES.contains(&name) +} + +/// Returns `true` if `name` is a valid Concerto identifier. +pub fn is_valid_identifier(name: &str) -> bool { + ID_REGEX.is_match(name) +} + +/// Strips the `@version` part from the namespace portion of a +/// fully-qualified type name. Primitive types are returned unchanged. +pub fn remove_namespace_version(fqn: &str) -> String { + if is_primitive_type(fqn) { + return fqn.to_string(); + } + let ns = get_namespace(fqn); + let parsed = match parse_namespace(ns) { + Ok(p) => p, + Err(_) => return fqn.to_string(), + }; + let type_name = get_short_name(fqn); + fully_qualified_name(&parsed.name, type_name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_short_name() { + assert_eq!(get_short_name("org.example@1.0.0.Asset"), "Asset"); + assert_eq!(get_short_name("Asset"), "Asset"); + assert_eq!(get_short_name("a.b.c.D"), "D"); + } + + #[test] + fn test_get_namespace() { + assert_eq!(get_namespace("org.example.Asset"), "org.example"); + assert_eq!(get_namespace("Asset"), ""); + } + + #[test] + fn test_fully_qualified_name() { + assert_eq!( + fully_qualified_name("org.example", "Asset"), + "org.example.Asset" + ); + assert_eq!(fully_qualified_name("", "Asset"), "Asset"); + } + + #[test] + fn test_parse_namespace() { + let ns = parse_namespace("org.example@1.0.0").unwrap(); + assert_eq!(ns.name, "org.example"); + assert_eq!(ns.version.as_deref(), Some("1.0.0")); + + let ns = parse_namespace("org.example").unwrap(); + assert_eq!(ns.name, "org.example"); + assert!(ns.version.is_none()); + + assert!(parse_namespace("a@b@c").is_err()); + } + + #[test] + fn test_is_primitive_type() { + assert!(is_primitive_type("String")); + assert!(is_primitive_type("Boolean")); + assert!(is_primitive_type("DateTime")); + assert!(is_primitive_type("Double")); + assert!(is_primitive_type("Integer")); + assert!(is_primitive_type("Long")); + assert!(!is_primitive_type("Concept")); + } + + #[test] + fn test_is_system_property() { + assert!(is_system_property("$class")); + assert!(is_system_property("$identifier")); + assert!(!is_system_property("name")); + } + + #[test] + fn test_is_valid_identifier() { + assert!(is_valid_identifier("foo")); + assert!(is_valid_identifier("_bar")); + assert!(is_valid_identifier("$baz")); + assert!(is_valid_identifier("MyType")); + assert!(!is_valid_identifier("123abc")); + assert!(!is_valid_identifier("")); + } + + #[test] + fn test_remove_namespace_version() { + assert_eq!( + remove_namespace_version("org.example@1.0.0.Asset"), + "org.example.Asset" + ); + assert_eq!(remove_namespace_version("String"), "String"); + assert_eq!( + remove_namespace_version("org.example.Asset"), + "org.example.Asset" + ); + } +} diff --git a/concerto-core/src/rootmodel.rs b/concerto-core/src/rootmodel.rs new file mode 100644 index 0000000..4fe8b06 --- /dev/null +++ b/concerto-core/src/rootmodel.rs @@ -0,0 +1,120 @@ +//! Programmatic construction of the Concerto root model. +//! +//! The root model defines the five abstract base types in the `concerto@1.0.0` +//! namespace: `Concept`, `Asset`, `Participant`, `Transaction`, and `Event`. +//! +//! Rather than embedding a static `rootmodel.json` file that could drift from +//! the metamodel, this module builds the equivalent AST JSON from code. + +use concerto_metamodel::concerto_metamodel_1_0_0 as mm; + +const MM: &str = "concerto.metamodel@1.0.0"; + +fn class(suffix: &str) -> String { + format!("{MM}.{suffix}") +} + +fn concept_declaration(name: &str, identified: Option) -> mm::ConceptDeclaration { + mm::ConceptDeclaration { + _class: class("ConceptDeclaration"), + name: name.to_string(), + is_abstract: true, + identified, + super_type: None, + properties: vec![], + decorators: None, + location: None, + } +} + +/// Returns the root model AST as a `serde_json::Value`. +/// +/// This is equivalent to the `rootmodel.json` file in the JavaScript +/// implementation and defines the five abstract base types. +pub fn root_model_ast() -> serde_json::Value { + let identified = mm::Identified { + _class: class("Identified"), + }; + + let import = mm::ImportType { + _class: class("ImportType"), + name: "DotNetNamespace".to_string(), + namespace: "concerto.decorator@1.0.0".to_string(), + uri: None, + }; + + let declarations = vec![ + concept_declaration("Concept", None), + concept_declaration("Asset", Some(identified.clone())), + concept_declaration("Participant", Some(identified)), + concept_declaration("Transaction", None), + concept_declaration("Event", None), + ]; + + // Serialize each declaration individually so they keep their full + // ConceptDeclaration fields ($class, isAbstract, properties, etc.) + // rather than being truncated to the base Declaration struct. + let decl_values: Vec = declarations + .iter() + .map(|d| serde_json::to_value(d).expect("root model declaration serialization")) + .collect(); + + let import_value = serde_json::to_value(&import).expect("root model import serialization"); + + // The mm::DecoratorLiteral struct doesn't carry the `value` field + // (it's on the subtype DecoratorString), so we build manually. + let decorator_value = serde_json::json!({ + "$class": format!("{MM}.Decorator"), + "name": "DotNetNamespace", + "arguments": [ + { + "$class": format!("{MM}.DecoratorString"), + "value": "AccordProject.Concerto" + } + ] + }); + + serde_json::json!({ + "$class": format!("{MM}.Model"), + "namespace": "concerto@1.0.0", + "imports": [import_value], + "declarations": decl_values, + "decorators": [decorator_value] + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn root_model_has_five_declarations() { + let ast = root_model_ast(); + let decls = ast["declarations"].as_array().unwrap(); + assert_eq!(decls.len(), 5); + assert_eq!(decls[0]["name"], "Concept"); + assert_eq!(decls[1]["name"], "Asset"); + assert_eq!(decls[2]["name"], "Participant"); + assert_eq!(decls[3]["name"], "Transaction"); + assert_eq!(decls[4]["name"], "Event"); + } + + #[test] + fn root_model_namespace() { + let ast = root_model_ast(); + assert_eq!(ast["namespace"], "concerto@1.0.0"); + } + + #[test] + fn asset_and_participant_are_identified() { + let ast = root_model_ast(); + let decls = ast["declarations"].as_array().unwrap(); + // Asset (index 1) and Participant (index 2) should have identified + assert!(decls[1].get("identified").is_some()); + assert!(decls[2].get("identified").is_some()); + // Concept, Transaction, Event should not + assert!(decls[0].get("identified").is_none()); + assert!(decls[3].get("identified").is_none()); + assert!(decls[4].get("identified").is_none()); + } +} diff --git a/concerto-core/src/traits.rs b/concerto-core/src/traits.rs deleted file mode 100644 index 45ea316..0000000 --- a/concerto-core/src/traits.rs +++ /dev/null @@ -1,1077 +0,0 @@ -use concerto_metamodel::concerto_metamodel_1_0_0::*; - -use crate::{util::is_valid_identifier, ConcertoError, ModelManager}; - -pub trait FromAst: Sized { - /// Concerto core wrapper type around Concerto metamodel - type ConcertoType; - - /// Implement the method that returnes Introspect structure from `Self::Ast` - fn from_ast(concerto_type: Self::ConcertoType) -> Self; - - fn from_json(json: &str) -> Result - where - Self::ConcertoType: for<'de> serde::Deserialize<'de>, - { - let parsed: Self::ConcertoType = serde_json::from_str(json)?; - Ok(Self::from_ast(parsed)) - } -} - -pub trait Validate: Sized { - /// Validates the component - fn validate(&self) -> Result<(), ConcertoError>; -} - -/// Base trait for declaration types -pub trait DeclarationBase { - /// Get the name of the declaration - fn get_name(&self) -> &str; - - /// Get the decorators if any - fn get_decorators(&self) -> Option<&Vec>; - - /// Get the location if any - fn get_location(&self) -> Option<&Range>; -} - -// Implement for all declaration types -impl DeclarationBase for Declaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for AssetDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for ConceptDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for EnumDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for ParticipantDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for TransactionDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for EventDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for MapDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -/// Trait for concept-based declarations (Asset, Participant, Transaction, Event all inherit from Concept) -pub trait ConceptDeclarationBase: DeclarationBase { - /// Get whether the concept is abstract - fn is_abstract(&self) -> bool; - - /// Get the super type if any - fn get_super_type(&self) -> Option<&TypeIdentifier>; - - /// Get the properties - fn get_properties(&self) -> &Vec; - - /// Get the identified information if any - fn get_identified(&self) -> Option<&Identified>; -} - -impl ConceptDeclarationBase for ConceptDeclaration { - fn is_abstract(&self) -> bool { - self.is_abstract - } - - fn get_super_type(&self) -> Option<&TypeIdentifier> { - self.super_type.as_ref() - } - - fn get_properties(&self) -> &Vec { - &self.properties - } - - fn get_identified(&self) -> Option<&Identified> { - self.identified.as_ref() - } -} - -impl ConceptDeclarationBase for AssetDeclaration { - fn is_abstract(&self) -> bool { - self.is_abstract - } - - fn get_super_type(&self) -> Option<&TypeIdentifier> { - self.super_type.as_ref() - } - - fn get_properties(&self) -> &Vec { - &self.properties - } - - fn get_identified(&self) -> Option<&Identified> { - self.identified.as_ref() - } -} - -impl ConceptDeclarationBase for ParticipantDeclaration { - fn is_abstract(&self) -> bool { - self.is_abstract - } - - fn get_super_type(&self) -> Option<&TypeIdentifier> { - self.super_type.as_ref() - } - - fn get_properties(&self) -> &Vec { - &self.properties - } - - fn get_identified(&self) -> Option<&Identified> { - self.identified.as_ref() - } -} - -impl ConceptDeclarationBase for TransactionDeclaration { - fn is_abstract(&self) -> bool { - self.is_abstract - } - - fn get_super_type(&self) -> Option<&TypeIdentifier> { - self.super_type.as_ref() - } - - fn get_properties(&self) -> &Vec { - &self.properties - } - - fn get_identified(&self) -> Option<&Identified> { - self.identified.as_ref() - } -} - -impl ConceptDeclarationBase for EventDeclaration { - fn is_abstract(&self) -> bool { - self.is_abstract - } - - fn get_super_type(&self) -> Option<&TypeIdentifier> { - self.super_type.as_ref() - } - - fn get_properties(&self) -> &Vec { - &self.properties - } - - fn get_identified(&self) -> Option<&Identified> { - self.identified.as_ref() - } -} - -/// Base trait for property types -pub trait PropertyBase { - /// Get the name of the property - fn get_name(&self) -> &str; - - /// Check if property is an array - fn is_array(&self) -> bool; - - /// Check if property is optional - fn is_optional(&self) -> bool; - - /// Get the decorators if any - fn get_decorators(&self) -> Option<&Vec>; - - /// Get the location if any - fn get_location(&self) -> Option<&Range>; -} - -// Implement for all property types -impl PropertyBase for RelationshipProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for ObjectProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for BooleanProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for DateTimeProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for StringProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for DoubleProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for IntegerProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl PropertyBase for LongProperty { - fn get_name(&self) -> &str { - &self.name - } - - fn is_array(&self) -> bool { - self.is_array - } - - fn is_optional(&self) -> bool { - self.is_optional - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -/// Trait for property types that have a type reference -pub trait TypeReferenceProperty: PropertyBase { - /// Get the type reference - fn get_type_reference(&self) -> &TypeIdentifier; -} - -impl TypeReferenceProperty for ObjectProperty { - fn get_type_reference(&self) -> &TypeIdentifier { - &self.type_ - } -} - -impl TypeReferenceProperty for RelationshipProperty { - fn get_type_reference(&self) -> &TypeIdentifier { - &self.type_ - } -} - -/// Trait for property validation -pub trait PropertyValidator { - /// Validate the property against a model manager - fn validate(&self, model_manager: &ModelManager) -> Result<(), ConcertoError>; -} - -impl PropertyValidator for RelationshipProperty { - fn validate(&self, model_manager: &ModelManager) -> Result<(), ConcertoError> { - model_manager.validate_type_exists(&self.type_) - } -} - -impl PropertyValidator for ObjectProperty { - fn validate(&self, model_manager: &ModelManager) -> Result<(), ConcertoError> { - model_manager.validate_type_exists(&self.type_) - } -} - -// Implementation for base Property -impl PropertyValidator for Property { - fn validate(&self, _model_manager: &ModelManager) -> Result<(), ConcertoError> { - // Validate property name - if !is_valid_identifier(&self.name) { - return Err(ConcertoError::ValidationError(format!( - "'{}' is not a valid property name. Identifiers must start with a letter and can contain only letters, numbers, or underscores", - self.name - ))); - } - - // Validate decorators if present - if let Some(decs) = &self.decorators { - for decorator in decs { - decorator.validate()?; - } - } - - Ok(()) - } -} - -// Default implementation for primitive types that don't need validation -macro_rules! impl_property_validator_noop { - ($t:ty) => { - impl PropertyValidator for $t { - fn validate(&self, _model_manager: &ModelManager) -> Result<(), ConcertoError> { - // No validation needed for primitive types - Ok(()) - } - } - }; -} - -impl_property_validator_noop!(StringProperty); -impl_property_validator_noop!(BooleanProperty); -impl_property_validator_noop!(DateTimeProperty); -impl_property_validator_noop!(DoubleProperty); -impl_property_validator_noop!(IntegerProperty); -impl_property_validator_noop!(LongProperty); - -/// Trait for declaration validation -pub trait DeclarationValidator { - /// Validates the declaration structure and rules - fn validate_declaration(&self) -> Result<(), ConcertoError>; - - /// Validates that a declaration has a valid name according to Concerto rules - fn validate_name(&self, name: &str) -> Result<(), ConcertoError>; - - /// Validates decorators if present - fn validate_decorators(&self, decorators: &Option>) - -> Result<(), ConcertoError>; -} - -/// Common validator implementation that can be reused across declaration types -pub struct CommonDeclarationValidator; - -impl CommonDeclarationValidator { - /// Helper function to validate a Concerto identifier - pub fn validate_identifier(name: &str) -> Result<(), ConcertoError> { - // This should use the same validation logic as in metamodel_validation.rs - - if !is_valid_identifier(name) { - return Err(ConcertoError::ValidationError( - format!("'{}' is not a valid identifier name. Identifiers must start with a letter and can contain only letters, numbers, or underscores", name) - )); - } - Ok(()) - } - - /// Helper function to validate decorators - pub fn validate_decorators(decorators: &Option>) -> Result<(), ConcertoError> { - if let Some(decs) = decorators { - for decorator in decs { - // Use the Validate trait for each decorator - decorator.validate()?; - } - } - Ok(()) - } - - /// Helper function to validate properties and check for duplicates - pub fn validate_properties(properties: &[Property]) -> Result<(), ConcertoError> { - use std::collections::HashSet; - let mut property_names = HashSet::new(); - - for property in properties { - // Use the Validate trait for each property - // This does the basic property validation without model context - // For model context validation, we'd use PropertyValidator - Validate::validate(property)?; - - // Check for duplicate property names - if !property_names.insert(&property.name) { - return Err(ConcertoError::ValidationError(format!( - "Duplicate property: {}", - property.name - ))); - } - } - - Ok(()) - } -} - -// Implement the DeclarationValidator for ConceptDeclaration and its child types -impl DeclarationValidator for ConceptDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - - // Validate properties if present - CommonDeclarationValidator::validate_properties(&self.properties)?; - - // Validate decorators - self.validate_decorators(&self.decorators)?; - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -// We can implement for the other declaration types similarly -impl DeclarationValidator for AssetDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - CommonDeclarationValidator::validate_properties(&self.properties)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for ParticipantDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - CommonDeclarationValidator::validate_properties(&self.properties)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for TransactionDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - CommonDeclarationValidator::validate_properties(&self.properties)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for EventDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - CommonDeclarationValidator::validate_properties(&self.properties)?; - self.validate_decorators(&self.decorators)?; - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for EnumDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - - // Validate enum properties (values) - use std::collections::HashSet; - let mut enum_values = HashSet::new(); - for property in &self.properties { - // Validate each enum property - if !CommonDeclarationValidator::validate_identifier(&property.name).is_ok() { - return Err(ConcertoError::ValidationError(format!( - "'{}' is not a valid enum property name", - property.name - ))); - } - - // Check for duplicate enum values - if !enum_values.insert(&property.name) { - return Err(ConcertoError::ValidationError(format!( - "Duplicate enum value: {}", - property.name - ))); - } - - // Validate decorators on enum properties - CommonDeclarationValidator::validate_decorators(&property.decorators)?; - } - - // Validate decorators - self.validate_decorators(&self.decorators)?; - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for MapDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - - // Validate key type - must be StringMapKeyType or DateTimeMapKeyType - // We need to check the _class field of the key type - if !self.key._class.contains("StringMapKeyType") - && !self.key._class.contains("DateTimeMapKeyType") - { - return Err(ConcertoError::ValidationError( - "Invalid map key type. Map keys must be String or DateTime".to_string(), - )); - } - - // Value type is always present by the struct definition - - // Validate decorators - self.validate_decorators(&self.decorators)?; - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -/// Trait for scalar declarations -pub trait ScalarDeclarationBase: DeclarationBase { - /// Get the default value type if any - fn get_default_value(&self) -> Option<&str>; -} - -// Implementation for ScalarDeclaration -impl DeclarationBase for ScalarDeclaration { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -/// Trait for scalar types with validators -pub trait ValidatedScalar { - /// Check if the scalar has a validator - fn has_validator(&self) -> bool; -} - -// Add implementation for ScalarDeclaration -impl DeclarationValidator for ScalarDeclaration { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators)?; - - // Basic validation for scalar declaration - // More specific validation would happen in specialized scalar implementations - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -// Implementations for specialized scalar types -impl DeclarationBase for StringScalar { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl ScalarDeclarationBase for StringScalar { - fn get_default_value(&self) -> Option<&str> { - self.default_value.as_deref() - } -} - -impl ValidatedScalar for StringScalar { - fn has_validator(&self) -> bool { - self.validator.is_some() - } -} - -impl DeclarationBase for IntegerScalar { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl ValidatedScalar for IntegerScalar { - fn has_validator(&self) -> bool { - self.validator.is_some() - } -} - -impl DeclarationBase for DoubleScalar { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl ValidatedScalar for DoubleScalar { - fn has_validator(&self) -> bool { - self.validator.is_some() - } -} - -impl DeclarationBase for BooleanScalar { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -impl DeclarationBase for DateTimeScalar { - fn get_name(&self) -> &str { - &self.name - } - - fn get_decorators(&self) -> Option<&Vec> { - self.decorators.as_ref() - } - - fn get_location(&self) -> Option<&Range> { - self.location.as_ref() - } -} - -// Implement DeclarationValidator for the specific scalar types -impl DeclarationValidator for StringScalar { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators)?; - - // Validate string regex validator if present - if let Some(validator) = &self.validator { - // StringRegexValidator has a direct pattern field (not Option) - // Try to compile the regex to validate it - match regex::Regex::new(&validator.pattern) { - Ok(_) => {} // Regex is valid - Err(_) => { - return Err(ConcertoError::ValidationError(format!( - "Invalid regex pattern in string validator: {}", - validator.pattern - ))); - } - } - } - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -// Implement for numeric scalar types with validators -macro_rules! impl_numeric_scalar_validator { - ($scalar_type:ty) => { - impl DeclarationValidator for $scalar_type { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators)?; - - // Validate numeric domain validator if present - if let Some(validator) = &self.validator { - // For numeric domain validators with lower/upper bounds - // Both lower and upper are actual values, not Option types - if validator.lower > validator.upper { - return Err(ConcertoError::ValidationError( - format!("Invalid range in validator: lower bound ({:?}) must be less than or equal to upper bound ({:?})", - validator.lower, validator.upper) - )); - } - } - - Ok(()) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators(&self, decorators: &Option>) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } - } - }; -} - -impl_numeric_scalar_validator!(IntegerScalar); -impl_numeric_scalar_validator!(DoubleScalar); -impl_numeric_scalar_validator!(LongScalar); - -// Simple validation for other scalar types -impl DeclarationValidator for BooleanScalar { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -impl DeclarationValidator for DateTimeScalar { - fn validate_declaration(&self) -> Result<(), ConcertoError> { - self.validate_name(&self.name)?; - self.validate_decorators(&self.decorators) - } - - fn validate_name(&self, name: &str) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_identifier(name) - } - - fn validate_decorators( - &self, - decorators: &Option>, - ) -> Result<(), ConcertoError> { - CommonDeclarationValidator::validate_decorators(decorators) - } -} - -// Add Validate implementations for all scalar types -impl Validate for StringScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for IntegerScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for DoubleScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for LongScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for BooleanScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} - -impl Validate for DateTimeScalar { - fn validate(&self) -> Result<(), ConcertoError> { - // Use the DeclarationValidator trait we implemented - self.validate_declaration() - } -} diff --git a/concerto-core/src/types.rs b/concerto-core/src/types.rs deleted file mode 100644 index d61f218..0000000 --- a/concerto-core/src/types.rs +++ /dev/null @@ -1,7 +0,0 @@ -use concerto_metamodel::concerto_metamodel_1_0_0::{Decorator, Model}; - -#[derive(Debug)] -pub struct ModelAst(pub Model); - -#[derive(Debug)] -pub struct DecoratorAst(pub Decorator); diff --git a/concerto-core/src/util.rs b/concerto-core/src/util.rs deleted file mode 100644 index 0fb5b46..0000000 --- a/concerto-core/src/util.rs +++ /dev/null @@ -1,71 +0,0 @@ -use regex::Regex; -use std::sync::LazyLock; - -use crate::{types::ModelAst, ConcertoError}; - -/// Parses a potentially versioned namespace into -/// its name and version parts. The version of the namespace -/// (if present) is parsed using semver.parse. -pub fn parse_namespace_and_version(ast: &ModelAst) -> Result<(String, String), ConcertoError> { - let x = ast.0.namespace.as_str(); - let parts: Vec<&str> = x.split('@').collect(); - // For now, assume all NS will have to have namespace@version - if parts.len() != 2 { - Err(ConcertoError::InvalidNS(x.to_string())) - } else { - Ok((parts[0].to_string(), parts[1].to_string())) - } -} - -const ID_REGEX: &str = r"^(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\u{005C}u[0-9A-Fa-f]{4})(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\u{005C}u[0-9A-Fa-f]{4}|\p{Mn}|\p{Mc}|\p{Nd}|\p{Pc}|\u{200C}|\u{200D})*$"; - -static IDENTIFIER_REGEX: LazyLock = - LazyLock::new(|| Regex::new(ID_REGEX).expect("Invalid ID regex")); - -/// Checks if a string is a valid identifier name in Concerto -/// Identifiers must start with a letter and contain only letters, numbers, or underscores -pub fn is_valid_identifier(name: &str) -> bool { - IDENTIFIER_REGEX.is_match(name) -} - -#[cfg(test)] -mod test { - use crate::types::ModelAst; - use concerto_metamodel::concerto_metamodel_1_0_0::Model; - - #[allow(unused)] - fn make_ast() -> ModelAst { - let basic_ast = include_str!("../examples/assets/concerto_models/basic.json"); - let parsed: Model = serde_json::from_str(basic_ast).expect("Cannot parse basic.json"); - ModelAst(parsed) - } - - #[test] - fn test_parse_ns() { - let ast = make_ast(); - if let Ok((ns, version)) = super::parse_namespace_and_version(&ast) { - assert_eq!(ns, "com.example.foo".to_string(), "Can parse namespace"); - assert_eq!(version, "1.0.0".to_string(), "Can parse version"); - } else { - assert!(false); - } - } - - #[test] - fn test_valid_identifiers() { - assert!(super::is_valid_identifier("simple_var")); - assert!(super::is_valid_identifier("$dollar")); - assert!(super::is_valid_identifier("π_ratio")); - assert!(super::is_valid_identifier("变量")); - assert!(super::is_valid_identifier(r"a\u0061")); // Literal backslash-u-hex - } - - #[test] - fn test_invalid_identifiers() { - assert!(!super::is_valid_identifier("1stVariable")); // Starts with digit - assert!(!super::is_valid_identifier("var-name")); // Contains hyphen - assert!(!super::is_valid_identifier("no space")); // Contains space - assert!(!super::is_valid_identifier("heavy+metal")); // Contains plus sign - assert!(!super::is_valid_identifier("🚀_star")); // Contains emoji - } -} diff --git a/concerto-core/src/validation/mod.rs b/concerto-core/src/validation/mod.rs new file mode 100644 index 0000000..9e078df --- /dev/null +++ b/concerto-core/src/validation/mod.rs @@ -0,0 +1 @@ +pub mod object_validator; diff --git a/concerto-core/src/validation/object_validator.rs b/concerto-core/src/validation/object_validator.rs new file mode 100644 index 0000000..0502765 --- /dev/null +++ b/concerto-core/src/validation/object_validator.rs @@ -0,0 +1,368 @@ +use serde_json::Value; + +use crate::error::{ConcertoError, Result}; +use crate::introspect::declarations::ClassDeclaration; +use crate::introspect::properties::PropertyDecl; +use crate::introspect::traits::HasProperties; +use crate::model_manager::ModelManager; +use crate::model_util; + +/// Validates a JSON object (`serde_json::Value`) against a loaded Concerto +/// model, mirroring the JS `ObjectValidator`. +pub struct ObjectValidator<'a> { + model_manager: &'a ModelManager, +} + +impl<'a> ObjectValidator<'a> { + pub fn new(model_manager: &'a ModelManager) -> Self { + Self { model_manager } + } + + /// Validate a JSON object. The object must have a `$class` field. + pub fn validate(&self, obj: &Value) -> Result<()> { + let class = obj + .get("$class") + .and_then(|v| v.as_str()) + .ok_or_else(|| ConcertoError::Validation { + message: "Object missing '$class' field".into(), + component: "$class".into(), + })?; + + let decl = self.model_manager.get_type(class)?; + let class_decl = decl.as_class().ok_or_else(|| ConcertoError::Validation { + message: format!("Type '{class}' is not a class declaration"), + component: class.to_string(), + })?; + + // Cannot instantiate abstract types + if class_decl.is_abstract() { + return Err(ConcertoError::Validation { + message: format!("Cannot validate instance of abstract type '{class}'"), + component: class.to_string(), + }); + } + + let obj_map = obj.as_object().ok_or_else(|| ConcertoError::Validation { + message: "Expected a JSON object".into(), + component: class.to_string(), + })?; + + // Collect all properties including inherited ones + let all_props = self.collect_all_properties(class_decl)?; + + // Check for undeclared fields + for key in obj_map.keys() { + if model_util::is_system_property(key) { + continue; + } + if !all_props.iter().any(|p| p.name() == key.as_str()) { + return Err(ConcertoError::Validation { + message: format!( + "Unexpected property '{key}' on instance of '{class}'" + ), + component: class.to_string(), + }); + } + } + + // Check all required fields are present and validate types + for prop in &all_props { + let value = obj_map.get(prop.name()); + + if !prop.is_optional() && value.is_none() { + return Err(ConcertoError::Validation { + message: format!( + "Missing required property '{}' on instance of '{class}'", + prop.name() + ), + component: class.to_string(), + }); + } + + if let Some(val) = value { + self.validate_property_value(prop, val, class)?; + } + } + + Ok(()) + } + + fn collect_all_properties( + &self, + cd: &'a ClassDeclaration, + ) -> Result> { + let mut props: Vec<&'a PropertyDecl> = Vec::new(); + + // Collect from super-type chain first (inherited properties) + if let Some(super_type_id) = cd.super_type() { + let super_fqn = if let Some(ref ns) = super_type_id.namespace { + model_util::fully_qualified_name(ns, &super_type_id.name) + } else if let Some(ref resolved) = super_type_id.resolved_name { + resolved.clone() + } else { + super_type_id.name.clone() + }; + + if let Ok(super_decl) = self.model_manager.get_type(&super_fqn) { + if let Some(super_cd) = super_decl.as_class() { + let inherited = self.collect_all_properties(super_cd)?; + props.extend(inherited); + } + } + } + + // Add own properties + props.extend(cd.own_properties().iter()); + + Ok(props) + } + + fn validate_property_value( + &self, + prop: &PropertyDecl, + value: &Value, + parent_class: &str, + ) -> Result<()> { + if prop.is_array() { + let arr = value.as_array().ok_or_else(|| ConcertoError::Validation { + message: format!( + "Property '{}' should be an array on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + })?; + for item in arr { + self.validate_single_value(prop, item, parent_class)?; + } + } else { + self.validate_single_value(prop, value, parent_class)?; + } + Ok(()) + } + + fn validate_single_value( + &self, + prop: &PropertyDecl, + value: &Value, + parent_class: &str, + ) -> Result<()> { + match prop { + PropertyDecl::Boolean(_) => { + if !value.is_boolean() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be Boolean on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::String(_) => { + if !value.is_string() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be String on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::Integer(_) | PropertyDecl::Long(_) => { + if !value.is_i64() && !value.is_u64() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be an integer on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::Double(_) => { + if !value.is_number() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be a number on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::DateTime(_) => { + // DateTime is serialized as an ISO string + if !value.is_string() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be a DateTime string on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::Object(_) => { + // For complex object properties, recursively validate + if value.is_object() { + self.validate(value)?; + } else if !value.is_null() { + return Err(ConcertoError::Validation { + message: format!( + "Property '{}' should be an object on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::Relationship(_) => { + // Relationships are serialized as URI strings + if !value.is_string() { + return Err(ConcertoError::Validation { + message: format!( + "Relationship '{}' should be a string URI on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + PropertyDecl::Enum(_) => { + // Enum values in an object context would be strings + if !value.is_string() { + return Err(ConcertoError::Validation { + message: format!( + "Enum property '{}' should be a string on '{parent_class}'", + prop.name() + ), + component: parent_class.to_string(), + }); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model_manager::{ModelManager, ModelManagerOptions}; + + fn make_manager_with_person() -> ModelManager { + let mut mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); + let model_json = serde_json::json!({ + "$class": "concerto.metamodel@1.0.0.Model", + "namespace": "org.example@1.0.0", + "declarations": [ + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Person", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.StringProperty", + "name": "name", + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.IntegerProperty", + "name": "age", + "isArray": false, + "isOptional": true + } + ] + } + ] + }); + mgr.add_model(&model_json, None, false).unwrap(); + mgr + } + + #[test] + fn test_validate_valid_object() { + let mgr = make_manager_with_person(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "$class": "org.example@1.0.0.Person", + "name": "Alice", + "age": 30 + }); + + assert!(validator.validate(&obj).is_ok()); + } + + #[test] + fn test_validate_missing_required_field() { + let mgr = make_manager_with_person(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "$class": "org.example@1.0.0.Person", + "age": 30 + }); + + let err = validator.validate(&obj).unwrap_err(); + assert!(matches!(err, ConcertoError::Validation { .. })); + } + + #[test] + fn test_validate_undeclared_field() { + let mgr = make_manager_with_person(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "$class": "org.example@1.0.0.Person", + "name": "Alice", + "unknownField": true + }); + + let err = validator.validate(&obj).unwrap_err(); + assert!(matches!(err, ConcertoError::Validation { .. })); + } + + #[test] + fn test_validate_wrong_type() { + let mgr = make_manager_with_person(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "$class": "org.example@1.0.0.Person", + "name": 123, + "age": 30 + }); + + let err = validator.validate(&obj).unwrap_err(); + assert!(matches!(err, ConcertoError::Validation { .. })); + } + + #[test] + fn test_validate_missing_class() { + let mgr = make_manager_with_person(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "name": "Alice" + }); + + let err = validator.validate(&obj).unwrap_err(); + assert!(matches!(err, ConcertoError::Validation { .. })); + } + + #[test] + fn test_validate_abstract_rejected() { + let mgr = ModelManager::new(ModelManagerOptions::default()).unwrap(); + let validator = ObjectValidator::new(&mgr); + + let obj = serde_json::json!({ + "$class": "concerto@1.0.0.Concept" + }); + + let err = validator.validate(&obj).unwrap_err(); + assert!(matches!(err, ConcertoError::Validation { .. })); + } +} diff --git a/concerto-macros/Cargo.toml b/concerto-macros/Cargo.toml index a0b6281..b0326e9 100644 --- a/concerto-macros/Cargo.toml +++ b/concerto-macros/Cargo.toml @@ -2,10 +2,14 @@ name = "concerto-macros" version = "0.1.0" edition = "2024" +authors = ["Accord Project "] +description = "Macro definitions used by the `concerto-core` crate." +license = "Apache-2.0" +license-file = "../LICENSE" [lib] proc-macro = true [dependencies] -syn = "2.0.117" +syn = { version = "2.0.117", features = ["full"] } quote = "1.0.45" diff --git a/concerto-macros/src/lib.rs b/concerto-macros/src/lib.rs index 9dd5801..757420a 100644 --- a/concerto-macros/src/lib.rs +++ b/concerto-macros/src/lib.rs @@ -1,50 +1,255 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{DeriveInput, Ident, parse_macro_input}; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; -#[proc_macro_derive(FromAst, attributes(concerto_ast_type, wrapper))] -pub fn derive_from_ast(input: TokenStream) -> TokenStream { +/// Derive macro for the `Decorated` trait. +/// +/// Expects the struct to have a field named `decorators` of type +/// `Option>`, or a newtype wrapping a struct that does. +/// +/// For newtypes (single unnamed field), it delegates to `.0.decorators`. +/// For named structs, it reads the `decorators` field directly. +#[proc_macro_derive(Decorated)] +pub fn derive_decorated(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - let concerto_ast_ident = get_concerto_ast_type(&input); - let wrapper_ident = get_wrapper_type(&input); let name = &input.ident; + let generics = &input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = match &input.data { + Data::Struct(data) => match &data.fields { + // Newtype: struct Foo(Inner) + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + quote! { + fn decorators(&self) -> &[concerto_metamodel::concerto_metamodel_1_0_0::Decorator] { + self.0.decorators.as_deref().unwrap_or(&[]) + } + } + } + // Named struct with `decorators` field + Fields::Named(_) => { + quote! { + fn decorators(&self) -> &[concerto_metamodel::concerto_metamodel_1_0_0::Decorator] { + self.decorators.as_deref().unwrap_or(&[]) + } + } + } + _ => { + return syn::Error::new_spanned( + &input, + "Decorated can only be derived for named structs or newtypes", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(&input, "Decorated can only be derived for structs") + .to_compile_error() + .into(); + } + }; let expanded = quote! { - impl FromAst for #name { - type ConcertoType = #concerto_ast_ident; + impl #impl_generics crate::introspect::traits::Decorated for #name #ty_generics #where_clause { + #body + } + }; - fn from_ast(concerto_type: Self::ConcertoType) -> Self { - Self { - inner: #wrapper_ident(concerto_type), + expanded.into() +} + +/// Derive macro for the `Named` trait. +/// +/// Expects the struct to have `name: String` and `location: Option` fields, +/// or be a newtype wrapping such a struct. +#[proc_macro_derive(Named)] +pub fn derive_named(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let generics = &input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + quote! { + fn name(&self) -> &str { + &self.0.name + } + fn location(&self) -> Option<&concerto_metamodel::concerto_metamodel_1_0_0::Range> { + self.0.location.as_ref() + } + } + } + Fields::Named(_) => { + quote! { + fn name(&self) -> &str { + &self.name + } + fn location(&self) -> Option<&concerto_metamodel::concerto_metamodel_1_0_0::Range> { + self.location.as_ref() + } } } + _ => { + return syn::Error::new_spanned( + &input, + "Named can only be derived for named structs or newtypes", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(&input, "Named can only be derived for structs") + .to_compile_error() + .into(); } }; - TokenStream::from(expanded) + + let expanded = quote! { + impl #impl_generics crate::introspect::traits::Named for #name #ty_generics #where_clause { + #body + } + }; + + expanded.into() } -fn get_concerto_ast_type(input: &DeriveInput) -> Ident { - for attr in &input.attrs { - if attr.path().is_ident("concerto_ast_type") { - let nested_ident: Ident = attr.parse_args().expect("Expected a Concerto Type"); - return nested_ident; +/// Derive macro for the `HasProperties` trait. +/// +/// Expects the struct to have `properties: Vec`, +/// `super_type: Option`, and `is_abstract: bool` fields, +/// or be a newtype wrapping such a struct. +#[proc_macro_derive(HasProperties)] +pub fn derive_has_properties(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let generics = &input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + quote! { + fn own_properties(&self) -> &[crate::introspect::properties::PropertyDecl] { + &self.0.properties + } + fn super_type(&self) -> Option<&concerto_metamodel::concerto_metamodel_1_0_0::TypeIdentifier> { + self.0.super_type.as_ref() + } + fn is_abstract(&self) -> bool { + self.0.is_abstract + } + } + } + Fields::Named(_) => { + quote! { + fn own_properties(&self) -> &[crate::introspect::properties::PropertyDecl] { + &self.properties + } + fn super_type(&self) -> Option<&concerto_metamodel::concerto_metamodel_1_0_0::TypeIdentifier> { + self.super_type.as_ref() + } + fn is_abstract(&self) -> bool { + self.is_abstract + } + } + } + _ => { + return syn::Error::new_spanned( + &input, + "HasProperties can only be derived for named structs or newtypes", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned( + &input, + "HasProperties can only be derived for structs", + ) + .to_compile_error() + .into(); + } + }; + + let expanded = quote! { + impl #impl_generics crate::introspect::traits::HasProperties for #name #ty_generics #where_clause { + #body } - } + }; - // Fallback to `CConcertoType`, e.g. `CDecorator` as a convention - let fallback_name = format!("C{}", input.ident); - Ident::new(&fallback_name, input.ident.span()) + expanded.into() } -fn get_wrapper_type(input: &DeriveInput) -> Ident { - for attr in &input.attrs { - if attr.path().is_ident("wrapper") { - let nested_ident: Ident = attr.parse_args().expect("Expected a Wrapper Type"); - return nested_ident; +/// Derive macro for the `Identifiable` trait. +/// +/// Expects the struct to have an `identified: Option` field, +/// or be a newtype wrapping a struct that does. +/// +/// The `$class` discriminator on the `Identified` value determines whether +/// the identification is system-provided or explicit: +/// - `concerto.metamodel@1.0.0.Identified` → system identified +/// - `concerto.metamodel@1.0.0.IdentifiedBy` → explicitly identified +#[proc_macro_derive(Identifiable)] +pub fn derive_identifiable(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let generics = &input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let identified_expr = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + quote! { &self.0.identified } + } + Fields::Named(_) => { + quote! { &self.identified } + } + _ => { + return syn::Error::new_spanned( + &input, + "Identifiable can only be derived for named structs or newtypes", + ) + .to_compile_error() + .into(); + } + }, + _ => { + return syn::Error::new_spanned(&input, "Identifiable can only be derived for structs") + .to_compile_error() + .into(); } - } + }; + + let expanded = quote! { + impl #impl_generics crate::introspect::traits::Identifiable for #name #ty_generics #where_clause { + fn is_identified(&self) -> bool { + (#identified_expr).is_some() + } + fn is_system_identified(&self) -> bool { + match #identified_expr { + Some(id) => !id._class.contains("IdentifiedBy"), + None => false, + } + } + fn is_explicitly_identified(&self) -> bool { + match #identified_expr { + Some(id) => id._class.contains("IdentifiedBy"), + None => false, + } + } + fn identifier_field_name(&self) -> Option<&str> { + match #identified_expr { + Some(id) if !id._class.contains("IdentifiedBy") => Some("$identifier"), + _ => None, + } + } + } + }; - // Fallback to `CConcertoType`, e.g. `CDecorator` as a convention - let fallback_name = format!("{}Ast", input.ident); - Ident::new(&fallback_name, input.ident.span()) + expanded.into() } diff --git a/concerto-metamodel/Cargo.toml b/concerto-metamodel/Cargo.toml index e2ec0e3..ba26d65 100644 --- a/concerto-metamodel/Cargo.toml +++ b/concerto-metamodel/Cargo.toml @@ -2,6 +2,10 @@ name = "concerto-metamodel" version = "3.12.8" edition = "2024" +authors = ["Accord Project "] +description = "Concerto Metamodel types generated from the `concerto-metamodel` package." +license = "Apache-2.0" +license-file = "../LICENSE" [dependencies] serde = { version = "1.0", features = ["derive"] } From 332a0c24f89dcc51edd7d9d7b6ee50b07adb554a Mon Sep 17 00:00:00 2001 From: Ertugrul Karademir Date: Sun, 22 Mar 2026 14:47:29 +0000 Subject: [PATCH 9/9] fix: typo in AGENTS Signed-off-by: Ertugrul Karademir --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index a95f34c..9371277 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ - Always use the most idiomatic approach suitable for Rust language. - If needed split the code into different crates. If a crate does not exist, ask the operator for help. -- Add a derive macro for repeated `impl`s of a trait, into `cocnerto-macros` crate. +- Add a derive macro for repeated `impl`s of a trait, into `concerto-macros` crate. - Types under `concerto-core` should only refer to the types from `concerto-metamodel` using new-type pattern. - Prefer sum-type over complicated traits. - Remove any code that is not being used (don't add extra code, just because, that might be useful in the future.)