diff --git a/v3/engine/src/execute/model_tracking.rs b/v3/engine/src/execute/model_tracking.rs index 03f9517db27..c274695c48e 100644 --- a/v3/engine/src/execute/model_tracking.rs +++ b/v3/engine/src/execute/model_tracking.rs @@ -158,219 +158,243 @@ pub fn count_command(command: Qualified, all_usage_counts: &mut Usa } } -#[test] -fn test_extend_usage_count() { - let model_count1 = ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - count: 1, - }; - let model_count2 = ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())), - count: 5, - }; - let model_count3 = ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model3".to_string())), - count: 2, - }; - let command_count1 = CommandCount { - command: Qualified::new("subgraph".to_string(), CommandName("command1".to_string())), - count: 2, - }; - let command_count2 = CommandCount { - command: Qualified::new("subgraph".to_string(), CommandName("command2".to_string())), - count: 1, - }; - let command_count3 = CommandCount { - command: Qualified::new("subgraph".to_string(), CommandName("command3".to_string())), - count: 3, - }; - let usage_counts = UsagesCounts { - models_used: vec![model_count1, model_count2.clone()], - commands_used: vec![command_count1, command_count2.clone()], - }; - let mut aggregator = UsagesCounts { - models_used: vec![model_count2, model_count3], - commands_used: vec![command_count2, command_count3], - }; - extend_usage_count(usage_counts, &mut aggregator); - let expected = UsagesCounts { - models_used: vec![ - ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())), - count: 10, - }, - ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model3".to_string())), - count: 2, - }, - ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - count: 1, - }, - ], - commands_used: vec![ - CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command2".to_string()), - ), - count: 2, - }, - CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command3".to_string()), - ), - count: 3, - }, - CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()), - ), - count: 2, - }, - ], - }; - assert_eq!(aggregator, expected); -} +#[cfg(test)] +mod tests { + use open_dds::{commands::CommandName, identifier, models::ModelName}; -#[test] -fn test_counter_functions() { - let mut aggregator = UsagesCounts::new(); - count_command( - Qualified::new("subgraph".to_string(), CommandName("command1".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { - models_used: Vec::new(), - commands_used: vec![CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()) - ), - count: 1, - }] - } - ); - count_command( - Qualified::new("subgraph".to_string(), CommandName("command1".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { - models_used: Vec::new(), - commands_used: vec![CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()) - ), - count: 2, - }] - } - ); - count_model( - Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { - models_used: vec![ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - count: 1, - }], - commands_used: vec![CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()) - ), - count: 2, - }] - } - ); - count_model( - Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { - models_used: vec![ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - count: 2, - }], - commands_used: vec![CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()) - ), - count: 2, - }] - } - ); - count_model( - Qualified::new("subgraph".to_string(), ModelName("model2".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { + use crate::{ + execute::model_tracking::{ + count_command, count_model, extend_usage_count, CommandCount, ModelCount, UsagesCounts, + }, + metadata::resolved::subgraph::Qualified, + }; + + #[test] + fn test_extend_usage_count() { + let model_count1 = ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), + count: 1, + }; + let model_count2 = ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))), + count: 5, + }; + let model_count3 = ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model3"))), + count: 2, + }; + let command_count1 = CommandCount { + command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))), + count: 2, + }; + let command_count2 = CommandCount { + command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command2"))), + count: 1, + }; + let command_count3 = CommandCount { + command: Qualified::new("subgraph".to_string(), CommandName(identifier!("command3"))), + count: 3, + }; + let usage_counts = UsagesCounts { + models_used: vec![model_count1, model_count2.clone()], + commands_used: vec![command_count1, command_count2.clone()], + }; + let mut aggregator = UsagesCounts { + models_used: vec![model_count2, model_count3], + commands_used: vec![command_count2, command_count3], + }; + extend_usage_count(usage_counts, &mut aggregator); + let expected = UsagesCounts { models_used: vec![ ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))), + count: 10, + }, + ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model3"))), count: 2, }, ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())), + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), count: 1, - } - ], - commands_used: vec![CommandCount { - command: Qualified::new( - "subgraph".to_string(), - CommandName("command1".to_string()) - ), - count: 2, - }] - } - ); - count_command( - Qualified::new("subgraph".to_string(), CommandName("command2".to_string())), - &mut aggregator, - ); - assert_eq!( - aggregator, - UsagesCounts { - models_used: vec![ - ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model1".to_string())), - count: 2, }, - ModelCount { - model: Qualified::new("subgraph".to_string(), ModelName("model2".to_string())), - count: 1, - } ], commands_used: vec![ CommandCount { command: Qualified::new( "subgraph".to_string(), - CommandName("command1".to_string()) + CommandName(identifier!("command2")), ), count: 2, }, CommandCount { command: Qualified::new( "subgraph".to_string(), - CommandName("command2".to_string()) + CommandName(identifier!("command3")), + ), + count: 3, + }, + CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")), + ), + count: 2, + }, + ], + }; + assert_eq!(aggregator, expected); + } + + #[test] + fn test_counter_functions() { + let mut aggregator = UsagesCounts::new(); + count_command( + Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: Vec::new(), + commands_used: vec![CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) ), count: 1, - } - ] - } - ); + }] + } + ); + count_command( + Qualified::new("subgraph".to_string(), CommandName(identifier!("command1"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: Vec::new(), + commands_used: vec![CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) + ), + count: 2, + }] + } + ); + count_model( + Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: vec![ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), + count: 1, + }], + commands_used: vec![CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) + ), + count: 2, + }] + } + ); + count_model( + Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: vec![ModelCount { + model: Qualified::new("subgraph".to_string(), ModelName(identifier!("model1"))), + count: 2, + }], + commands_used: vec![CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) + ), + count: 2, + }] + } + ); + count_model( + Qualified::new("subgraph".to_string(), ModelName(identifier!("model2"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: vec![ + ModelCount { + model: Qualified::new( + "subgraph".to_string(), + ModelName(identifier!("model1")) + ), + count: 2, + }, + ModelCount { + model: Qualified::new( + "subgraph".to_string(), + ModelName(identifier!("model2")) + ), + count: 1, + } + ], + commands_used: vec![CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) + ), + count: 2, + }] + } + ); + count_command( + Qualified::new("subgraph".to_string(), CommandName(identifier!("command2"))), + &mut aggregator, + ); + assert_eq!( + aggregator, + UsagesCounts { + models_used: vec![ + ModelCount { + model: Qualified::new( + "subgraph".to_string(), + ModelName(identifier!("model1")) + ), + count: 2, + }, + ModelCount { + model: Qualified::new( + "subgraph".to_string(), + ModelName(identifier!("model2")) + ), + count: 1, + } + ], + commands_used: vec![ + CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command1")) + ), + count: 2, + }, + CommandCount { + command: Qualified::new( + "subgraph".to_string(), + CommandName(identifier!("command2")) + ), + count: 1, + } + ] + } + ); + } } diff --git a/v3/engine/src/metadata/resolved/types.rs b/v3/engine/src/metadata/resolved/types.rs index 2dea32ce863..b4eb346a2bc 100644 --- a/v3/engine/src/metadata/resolved/types.rs +++ b/v3/engine/src/metadata/resolved/types.rs @@ -9,6 +9,7 @@ use indexmap::IndexMap; use lang_graphql::ast::common as ast; use ndc_client as ndc; use open_dds::data_connector::DataConnectorName; +use open_dds::identifier; use open_dds::models::EnableAllOrSpecific; use open_dds::permissions::{Role, TypeOutputPermission, TypePermissionsV1}; use open_dds::types::{ @@ -184,7 +185,7 @@ pub fn resolve_object_type( if !global_id_fields.is_empty() { // Throw error if the object type has a field called id" and has global fields configured. // Because, when the global id fields are configured, the `id` field will be auto-generated. - if resolved_fields.contains_key(&FieldName("id".into())) { + if resolved_fields.contains_key(&FieldName(identifier!("id"))) { return Err(Error::IdFieldConflictingGlobalId { type_name: qualified_type_name.clone(), }); @@ -297,7 +298,7 @@ pub fn resolve_data_connector_type_mapping( } else { // If no mapping is defined for a field, implicitly create a mapping // with the same column name as the field. - field_name.0.to_owned() + field_name.to_string() }; let source_column = get_column(ndc_object_type, field_name, &resolved_field_mapping_column)?; diff --git a/v3/engine/tests/validate_metadata_artifacts/metadata_with_field_path_to_argument_relationship_mapping.json b/v3/engine/tests/validate_metadata_artifacts/metadata_with_field_path_to_argument_relationship_mapping.json index 0584ab87912..ef532ad0cb9 100644 --- a/v3/engine/tests/validate_metadata_artifacts/metadata_with_field_path_to_argument_relationship_mapping.json +++ b/v3/engine/tests/validate_metadata_artifacts/metadata_with_field_path_to_argument_relationship_mapping.json @@ -367,7 +367,7 @@ }, "target": { "argument": { - "argumentName": "dummy-argument" + "argumentName": "dummy_argument" } } } diff --git a/v3/open-dds/metadata.jsonschema b/v3/open-dds/metadata.jsonschema index a640a086230..2e923df088e 100644 --- a/v3/open-dds/metadata.jsonschema +++ b/v3/open-dds/metadata.jsonschema @@ -463,7 +463,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/DataConnectorName", "title": "DataConnectorName", "description": "The name of a data connector.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "DataConnectorUrlV1": { "$id": "https://hasura.io/jsonschemas/metadata/DataConnectorUrlV1", @@ -684,7 +685,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/CustomTypeName", "title": "CustomTypeName", "description": "The name of a user-defined type.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "FieldDefinition": { "$id": "https://hasura.io/jsonschemas/metadata/FieldDefinition", @@ -726,7 +728,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/FieldName", "title": "FieldName", "description": "The name of a field in a user-defined object type.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "TypeReference": { "$id": "https://hasura.io/jsonschemas/metadata/TypeReference", @@ -1307,7 +1310,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/ModelName", "title": "ModelName", "description": "The name of data model.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "ArgumentDefinition": { "$id": "https://hasura.io/jsonschemas/metadata/ArgumentDefinition", @@ -1337,7 +1341,8 @@ "ArgumentName": { "$id": "https://hasura.io/jsonschemas/metadata/ArgumentName", "title": "ArgumentName", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "ModelSource": { "$id": "https://hasura.io/jsonschemas/metadata/ModelSource", @@ -1664,7 +1669,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/CommandName", "title": "CommandName", "description": "The name of a command.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "CommandSource": { "$id": "https://hasura.io/jsonschemas/metadata/CommandSource", @@ -1886,7 +1892,8 @@ "$id": "https://hasura.io/jsonschemas/metadata/RelationshipName", "title": "RelationshipName", "description": "The name of the GraphQL relationship field.", - "type": "string" + "type": "string", + "pattern": "^[_a-zA-Z][_a-zA-Z0-9]*$" }, "RelationshipTarget": { "$id": "https://hasura.io/jsonschemas/metadata/RelationshipTarget", diff --git a/v3/open-dds/src/arguments.rs b/v3/open-dds/src/arguments.rs index e8efc908727..d8df2d0dd13 100644 --- a/v3/open-dds/src/arguments.rs +++ b/v3/open-dds/src/arguments.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference}; +use crate::{identifier::Identifier, impl_JsonSchema_with_OpenDd_for, types::TypeReference}; #[derive( Serialize, @@ -15,7 +15,7 @@ use crate::{impl_JsonSchema_with_OpenDd_for, types::TypeReference}; derive_more::Display, opendds_derive::OpenDd, )] -pub struct ArgumentName(pub String); +pub struct ArgumentName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(ArgumentName); diff --git a/v3/open-dds/src/commands.rs b/v3/open-dds/src/commands.rs index 61688d65aef..e373ae7f216 100644 --- a/v3/open-dds/src/commands.rs +++ b/v3/open-dds/src/commands.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ arguments::{ArgumentDefinition, ArgumentName}, data_connector::DataConnectorName, + identifier::Identifier, impl_JsonSchema_with_OpenDd_for, types::{GraphQlFieldName, TypeReference}, }; @@ -22,7 +23,7 @@ use crate::{ derive_more::Display, opendds_derive::OpenDd, )] -pub struct CommandName(pub String); +pub struct CommandName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(CommandName); diff --git a/v3/open-dds/src/data_connector.rs b/v3/open-dds/src/data_connector.rs index 6ccb28c8476..802e685f543 100644 --- a/v3/open-dds/src/data_connector.rs +++ b/v3/open-dds/src/data_connector.rs @@ -6,7 +6,7 @@ mod v1; pub use v1::{DataConnectorLinkV1, DataConnectorUrlV1 as DataConnectorUrl, ReadWriteUrls}; -use crate::impl_OpenDd_default_for; +use crate::{identifier::Identifier, impl_OpenDd_default_for}; /// The name of a data connector. #[derive( @@ -21,7 +21,7 @@ use crate::impl_OpenDd_default_for; derive_more::Display, opendds_derive::OpenDd, )] -pub struct DataConnectorName(pub String); +pub struct DataConnectorName(pub Identifier); #[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)] #[serde(tag = "version", content = "definition")] diff --git a/v3/open-dds/src/identifier.rs b/v3/open-dds/src/identifier.rs new file mode 100644 index 00000000000..2c64761fee0 --- /dev/null +++ b/v3/open-dds/src/identifier.rs @@ -0,0 +1,103 @@ +use schemars::schema::{Schema::Object as SchemaObjectVariant, SchemaObject, StringValidation}; +use serde::{de::Error, Deserialize, Serialize}; +use std::ops::Deref; + +use crate::{ + impl_JsonSchema_with_OpenDd_for, + traits::{OpenDd, OpenDdDeserializeError}, +}; + +// Macro to produce a validated identifier using a string literal that crashes +// if the literal is invalid. Does not work for non-literal strings to avoid +// use on user supplied input. +#[macro_export] +macro_rules! identifier { + ($name:literal) => { + open_dds::identifier::Identifier::new($name.to_string()).unwrap() + }; +} + +/// Type capturing an identifier used within the metadata. The wrapped String +/// is guaranteed to be a valid identifier, i.e. +/// - starts with an alphabet or underscore +/// - all characters are either alphanumeric or underscore +#[derive(Serialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Display)] +pub struct Identifier(pub String); + +impl Identifier { + pub fn new(string: String) -> Result { + if let Some(c) = string.chars().next() { + if !c.is_ascii_alphabetic() && c != '_' { + return Err("must start with an alphabet or underscore"); + } + } else { + return Err("cannot be an empty string"); + } + if !string + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_') + { + return Err("must contain only alphanumeric characters or underscore"); + } + Ok(Identifier(string)) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl Deref for Identifier { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl OpenDd for Identifier { + fn deserialize(json: serde_json::Value) -> Result { + let string: String = + serde_json::from_value(json).map_err(|error| OpenDdDeserializeError { + error, + path: Default::default(), + })?; + Identifier::new(string).map_err(|e| OpenDdDeserializeError { + error: serde_json::Error::custom(e), + path: Default::default(), + }) + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + SchemaObjectVariant(SchemaObject { + instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( + schemars::schema::InstanceType::String, + ))), + string: Some(Box::new(StringValidation { + pattern: Some("^[_a-zA-Z][_a-zA-Z0-9]*$".to_string()), + ..Default::default() + })), + ..Default::default() + }) + } + + fn _schema_name() -> String { + "Identifier".to_string() + } + + fn _schema_is_referenceable() -> bool { + // This is a tiny leaf schema so just make it non-referenceable to avoid a layer of + // indirection in the overall JSONSchema. + false + } +} + +impl_JsonSchema_with_OpenDd_for!(Identifier); + +impl<'de> Deserialize<'de> for Identifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + OpenDd::deserialize(serde_json::Value::deserialize(deserializer)?).map_err(D::Error::custom) + } +} diff --git a/v3/open-dds/src/lib.rs b/v3/open-dds/src/lib.rs index a33723e7f99..05222c14d02 100644 --- a/v3/open-dds/src/lib.rs +++ b/v3/open-dds/src/lib.rs @@ -9,6 +9,7 @@ pub mod commands; pub mod data_connector; pub mod flags; pub mod graphql_config; +pub mod identifier; pub mod models; pub mod permissions; pub mod relationships; diff --git a/v3/open-dds/src/models.rs b/v3/open-dds/src/models.rs index 10678710049..a6fbe5c7f91 100644 --- a/v3/open-dds/src/models.rs +++ b/v3/open-dds/src/models.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ arguments::{ArgumentDefinition, ArgumentName}, data_connector::DataConnectorName, + identifier::Identifier, impl_JsonSchema_with_OpenDd_for, traits::{OpenDd, OpenDdDeserializeError}, types::{CustomTypeName, FieldName, GraphQlFieldName, GraphQlTypeName}, @@ -23,7 +24,7 @@ use crate::{ derive_more::Display, opendds_derive::OpenDd, )] -pub struct ModelName(pub String); +pub struct ModelName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(ModelName); diff --git a/v3/open-dds/src/relationships.rs b/v3/open-dds/src/relationships.rs index 19afa775aac..762c2f893fa 100644 --- a/v3/open-dds/src/relationships.rs +++ b/v3/open-dds/src/relationships.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ arguments::ArgumentName, commands::CommandName, + identifier::Identifier, impl_JsonSchema_with_OpenDd_for, models::ModelName, permissions::ValueExpression, @@ -22,7 +23,7 @@ use crate::{ Hash, opendds_derive::OpenDd, )] -pub struct RelationshipName(pub String); +pub struct RelationshipName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(RelationshipName); diff --git a/v3/open-dds/src/types.rs b/v3/open-dds/src/types.rs index b4c2accc84b..7a9f4f8ea72 100644 --- a/v3/open-dds/src/types.rs +++ b/v3/open-dds/src/types.rs @@ -9,8 +9,8 @@ use serde::{ }; use crate::{ - data_connector::DataConnectorName, impl_JsonSchema_with_OpenDd_for, impl_OpenDd_default_for, - models::EnableAllOrSpecific, + data_connector::DataConnectorName, identifier::Identifier, impl_JsonSchema_with_OpenDd_for, + impl_OpenDd_default_for, models::EnableAllOrSpecific, }; #[derive( @@ -46,25 +46,25 @@ pub enum TypeName { Ord, opendds_derive::OpenDd, )] -pub struct CustomTypeName(pub String); +pub struct CustomTypeName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(CustomTypeName); impl CustomTypeName { fn new(s: String) -> Result { - // First character should be alphabetic or underscore - let first_char_valid = - matches!(s.chars().next(), Some(c) if c.is_ascii_alphabetic() || c == '_'); - // All characters should be alphanumeric or underscore - let all_chars_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + let identifier = Identifier::new(s)?; // Should not be an inbuilt type - let not_an_inbuilt_type = - InbuiltType::deserialize(StrDeserializer::::new(s.as_str())) - .is_err(); - if first_char_valid && all_chars_valid && not_an_inbuilt_type { - Ok(CustomTypeName(s)) + if InbuiltType::deserialize(StrDeserializer::::new( + identifier.0.as_str(), + )) + .is_ok() + { + Err(format!( + "custom types cannot have the same name as an inbuilt type: {}", + identifier.0 + )) } else { - Err(format!("invalid custom type name: {s}")) + Ok(CustomTypeName(identifier)) } } } @@ -392,7 +392,7 @@ pub struct ColumnFieldMapping { Ord, opendds_derive::OpenDd, )] -pub struct FieldName(pub String); +pub struct FieldName(pub Identifier); impl_JsonSchema_with_OpenDd_for!(FieldName);