From e26504f7d221d2d5fc178896df3d3a140028da03 Mon Sep 17 00:00:00 2001 From: Jozsef Damokos Date: Thu, 4 Jul 2024 09:44:26 +0300 Subject: [PATCH 1/2] Added schema validation --- profiles/product_with_manufacturer.yaml | 8 + src/config.rs | 1 + src/data/mod.rs | 2 + src/data/validate.rs | 365 ++++++++++++++++++++++++ src/main.rs | 7 +- 5 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 src/data/validate.rs diff --git a/profiles/product_with_manufacturer.yaml b/profiles/product_with_manufacturer.yaml index 8b2a111..c42f370 100644 --- a/profiles/product_with_manufacturer.yaml +++ b/profiles/product_with_manufacturer.yaml @@ -15,24 +15,32 @@ sort: mappings: - file_column: "id" entity_path: "id" + field_type: "uuid" - file_column: "product number" entity_path: "productNumber" + field_type: "string" - file_column: "default name" entity_path: "name" + field_type: "string" - file_column: "default price net" key: "default_price_net" - file_column: "default price gross" key: "default_price_gross" - file_column: "stock" entity_path: "stock" + field_type: "integer" - file_column: "tax id" entity_path: "taxId" + field_type: "uuid" - file_column: "manufacturer id" entity_path: "manufacturer?.id" + field_type: "uuid" - file_column: "manufacturer name" entity_path: "manufacturer?.name" + field_type: "string" - file_column: "manufacturer website" entity_path: "manufacturer?.link" + field_type: "string" serialize_script: | let default_currency = get_default("CURRENCY"); diff --git a/src/config.rs b/src/config.rs index 0d73ca9..b3b69b9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,6 +45,7 @@ impl Mapping { pub struct EntityPathMapping { pub file_column: String, pub entity_path: String, + pub field_type: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/data/mod.rs b/src/data/mod.rs index 9ca38ca..85a029b 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,8 +1,10 @@ mod export; mod import; mod transform; +mod validate; pub use export::export; pub use import::import; pub use transform::prepare_scripting_environment; pub use transform::ScriptingEnvironment; +pub use validate::validate_paths_for_entity; diff --git a/src/data/validate.rs b/src/data/validate.rs new file mode 100644 index 0000000..10345b3 --- /dev/null +++ b/src/data/validate.rs @@ -0,0 +1,365 @@ +use crate::config::Mapping; + +/// Validate paths for entity +pub fn validate_paths_for_entity( + entity: &str, + mappings: &Vec, + api_schema: &serde_json::Map, +) -> anyhow::Result<()> { + // if entity name is not set in api_schema throw an exception + if !api_schema.contains_key(entity) { + anyhow::bail!("Entity {} not found in API schema", entity); + } + + for entry in mappings { + let path_mapping = match entry { + Mapping::ByPath(path_mapping) => path_mapping, + Mapping::ByScript(_) => continue, + }; + + let path = path_mapping.entity_path.split('.').collect::>(); + + // if path starts with ? its optional + if path[0].ends_with('?') { + continue; + } + + // check if first path element is set in the entity object in schema + if !api_schema[entity]["properties"][path[0]].is_object() { + anyhow::bail!("Entity {} does not have a field {}", entity, path[0]); + } + + let root_path = api_schema[entity]["properties"][path[0]].as_object().unwrap(); + + // if there is only one path element + if path.len() == 1 { + let field_type = root_path["type"].as_str().unwrap(); + + // check if type matches the type defined in the schema + if field_type != path_mapping.field_type { + anyhow::bail!("Type {} does not match schema type {} for {} in {}", path_mapping.field_type, field_type, path[0], entity); + } + } else { + // if its multiple parts it should be an association + if root_path["type"].as_str().unwrap() != "association" { + anyhow::bail!("Field {} in {} is not an association", path[0], entity); + } + + let entity_name = root_path["entity"].as_str().unwrap(); + let path = path[1..].join("."); + + // create a new mapping with the new path + let mapping = Mapping::ByPath(crate::config::EntityPathMapping { + file_column: path_mapping.file_column.clone(), + entity_path: path, + field_type: path_mapping.field_type.clone(), + }); + + // validate the new mapping + validate_paths_for_entity(entity_name, &vec![mapping], api_schema)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + #[test] + fn validate_non_existent_entity() { + let entity = "nonexistent"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + field_type: "uuid".to_string(), + })]; + let api_schema = json!({ + "product": { + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Entity nonexistent not found in API schema"))); + } + + #[test] + fn validate_non_existent_simple_path() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + field_type: "uuid".to_string(), + })]; + let api_schema = json!({ + "product": { + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Entity product does not have a field manufacturerId"))); + } + + #[test] + fn validate_optional_simple_path() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + field_type: "uuid".to_string(), + })]; + let api_schema = json!({ + "product": { + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Entity product does not have a field manufacturerId"))); + } + + #[test] + fn validate_existing_simple_path_but_type_mismatch() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "manufacturerId": { + "type": "uuid" + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for manufacturerId in product"))); + } + + #[test] + fn validate_existing_simple_path() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + field_type: "uuid".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "manufacturerId": { + "type": "uuid" + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_ok()); + } + + #[test] + fn validate_non_existent_association() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer.name".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "manufacturer": { + "type": "string", + } + } + }, + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Field manufacturer in product is not an association"))); + } + + #[test] + fn validate_existing_association() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer.name".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "manufacturer": { + "type": "association", + "entity": "product_manufacturer" + } + } + }, + "product_manufacturer": { + "entity": "product_manufacturer", + "properties": { + "name": { + "type": "string" + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_ok()); + } + + #[test] + fn validate_existing_association_but_type_mismatch() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturer.id".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "manufacturer": { + "type": "association", + "entity": "product_manufacturer" + } + } + }, + "product_manufacturer": { + "entity": "product_manufacturer", + "properties": { + "id": { + "type": "uuid" + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for id in product_manufacturer"))); + } + + #[test] + fn validate_optional_association() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer?.name".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": {} + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_ok()); + } + + #[test] + fn validate_valid_nested_association() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "tax country".to_string(), + entity_path: "tax.country.name".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "tax": { + "type": "association", + "entity": "tax" + } + } + }, + "tax": { + "entity": "tax", + "properties": { + "country": { + "type": "association", + "entity": "country" + } + } + }, + "country": { + "entity": "country", + "properties": { + "name": { + "type": "string", + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_ok()); + } + + #[test] + fn validate_invalid_nested_association() { + let entity = "product"; + let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { + file_column: "tax country".to_string(), + entity_path: "tax.country.id".to_string(), + field_type: "string".to_string(), + })]; + let api_schema = json!({ + "product": { + "entity": "product", + "properties": { + "tax": { + "type": "association", + "entity": "tax" + } + } + }, + "tax": { + "entity": "tax", + "properties": { + "country": { + "type": "association", + "entity": "country" + } + } + }, + "country": { + "entity": "country", + "properties": { + "id": { + "type": "uuid", + } + } + } + }); + + let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + + assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for id in country"))); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3b1eea8..7f0d51a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use crate::api::SwClient; use crate::config::{Credentials, Mapping, Schema}; use crate::data::{export, import, prepare_scripting_environment, ScriptingEnvironment}; +use crate::data::validate_paths_for_entity; use anyhow::Context; use clap::{Parser, Subcommand}; use std::collections::HashSet; @@ -169,9 +170,11 @@ async fn create_context( .context("No .credentials.toml found. Call command auth first.")?; let credentials: Credentials = toml::from_str(&serialized_credentials)?; let sw_client = SwClient::new(credentials, in_flight_limit).await?; - // ToDo: lookup entities.json definitions - // ToDo: further schema verification + let api_schema = sw_client.entity_schema().await; + let entity = &schema.entity; + + let _ = validate_paths_for_entity(entity, &schema.mappings, &api_schema?); // ToDo: create lookup table for languages + currencies? From 394c3033578a24dc8b4fa697fdbec7617a4c0eeb Mon Sep 17 00:00:00 2001 From: Jozsef Damokos Date: Thu, 4 Jul 2024 14:30:30 +0300 Subject: [PATCH 2/2] Updated validation based on feedback --- profiles/product_with_manufacturer.yaml | 8 - src/config.rs | 1 - src/data/validate.rs | 312 +++++++++++------------- src/main.rs | 4 +- 4 files changed, 144 insertions(+), 181 deletions(-) diff --git a/profiles/product_with_manufacturer.yaml b/profiles/product_with_manufacturer.yaml index c42f370..8b2a111 100644 --- a/profiles/product_with_manufacturer.yaml +++ b/profiles/product_with_manufacturer.yaml @@ -15,32 +15,24 @@ sort: mappings: - file_column: "id" entity_path: "id" - field_type: "uuid" - file_column: "product number" entity_path: "productNumber" - field_type: "string" - file_column: "default name" entity_path: "name" - field_type: "string" - file_column: "default price net" key: "default_price_net" - file_column: "default price gross" key: "default_price_gross" - file_column: "stock" entity_path: "stock" - field_type: "integer" - file_column: "tax id" entity_path: "taxId" - field_type: "uuid" - file_column: "manufacturer id" entity_path: "manufacturer?.id" - field_type: "uuid" - file_column: "manufacturer name" entity_path: "manufacturer?.name" - field_type: "string" - file_column: "manufacturer website" entity_path: "manufacturer?.link" - field_type: "string" serialize_script: | let default_currency = get_default("CURRENCY"); diff --git a/src/config.rs b/src/config.rs index b3b69b9..0d73ca9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,7 +45,6 @@ impl Mapping { pub struct EntityPathMapping { pub file_column: String, pub entity_path: String, - pub field_type: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/data/validate.rs b/src/data/validate.rs index 10345b3..cbfcfcb 100644 --- a/src/data/validate.rs +++ b/src/data/validate.rs @@ -18,46 +18,41 @@ pub fn validate_paths_for_entity( }; let path = path_mapping.entity_path.split('.').collect::>(); + let root_path = path[0]; + + // if path ends with ? remove it + let root_path = root_path.trim_end_matches('?'); + + let Some(root_property) = api_schema + .get(entity) + .and_then(|x| x.get("properties")) + .and_then(|x| x.get(root_path)) + .and_then(|x| x.as_object()) + else { + anyhow::bail!("Entity {} does not have a field {}", entity, root_path); + }; - // if path starts with ? its optional - if path[0].ends_with('?') { + // if path has only one part it should be a simple field + if path.len() == 1 { continue; } - // check if first path element is set in the entity object in schema - if !api_schema[entity]["properties"][path[0]].is_object() { - anyhow::bail!("Entity {} does not have a field {}", entity, path[0]); + // if its multiple parts it should be an association + if root_property["type"].as_str().unwrap() != "association" { + anyhow::bail!("Field {} in {} is not an association", root_path, entity); } - let root_path = api_schema[entity]["properties"][path[0]].as_object().unwrap(); - - // if there is only one path element - if path.len() == 1 { - let field_type = root_path["type"].as_str().unwrap(); + let entity_name = root_property["entity"].as_str().unwrap(); + let path = path[1..].join("."); - // check if type matches the type defined in the schema - if field_type != path_mapping.field_type { - anyhow::bail!("Type {} does not match schema type {} for {} in {}", path_mapping.field_type, field_type, path[0], entity); - } - } else { - // if its multiple parts it should be an association - if root_path["type"].as_str().unwrap() != "association" { - anyhow::bail!("Field {} in {} is not an association", path[0], entity); - } - - let entity_name = root_path["entity"].as_str().unwrap(); - let path = path[1..].join("."); - - // create a new mapping with the new path - let mapping = Mapping::ByPath(crate::config::EntityPathMapping { - file_column: path_mapping.file_column.clone(), - entity_path: path, - field_type: path_mapping.field_type.clone(), - }); + // create a new mapping with the new path + let mapping = Mapping::ByPath(crate::config::EntityPathMapping { + file_column: path_mapping.file_column.clone(), + entity_path: path, + }); - // validate the new mapping - validate_paths_for_entity(entity_name, &vec![mapping], api_schema)?; - } + // validate the new mapping + validate_paths_for_entity(entity_name, &vec![mapping], api_schema)?; } Ok(()) @@ -70,89 +65,62 @@ mod tests { #[test] fn validate_non_existent_entity() { let entity = "nonexistent"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturerId".to_string(), - field_type: "uuid".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + }, + )]; let api_schema = json!({ "product": { } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); - assert!(result.is_err_and(|x| x.to_string().contains("Entity nonexistent not found in API schema"))); + assert!(result.is_err_and(|x| x + .to_string() + .contains("Entity nonexistent not found in API schema"))); } #[test] fn validate_non_existent_simple_path() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturerId".to_string(), - field_type: "uuid".to_string(), - })]; - let api_schema = json!({ - "product": { - } - }); - - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); - - assert!(result.is_err_and(|x| x.to_string().contains("Entity product does not have a field manufacturerId"))); - } - - #[test] - fn validate_optional_simple_path() { - let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturerId".to_string(), - field_type: "uuid".to_string(), - })]; - let api_schema = json!({ - "product": { - } - }); - - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); - - assert!(result.is_err_and(|x| x.to_string().contains("Entity product does not have a field manufacturerId"))); - } - - #[test] - fn validate_existing_simple_path_but_type_mismatch() { - let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturerId".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + }, + )]; let api_schema = json!({ "product": { - "entity": "product", - "properties": { - "manufacturerId": { - "type": "uuid" - } - } } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); - assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for manufacturerId in product"))); + assert!(result.is_err_and(|x| x + .to_string() + .contains("Entity product does not have a field manufacturerId"))); } #[test] fn validate_existing_simple_path() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturerId".to_string(), - field_type: "uuid".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer id".to_string(), + entity_path: "manufacturerId".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", @@ -164,7 +132,11 @@ mod tests { } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); assert!(result.is_ok()); } @@ -172,11 +144,12 @@ mod tests { #[test] fn validate_non_existent_association() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer name".to_string(), - entity_path: "manufacturer.name".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer.name".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", @@ -188,19 +161,26 @@ mod tests { }, }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); - assert!(result.is_err_and(|x| x.to_string().contains("Field manufacturer in product is not an association"))); + assert!(result.is_err_and(|x| x + .to_string() + .contains("Field manufacturer in product is not an association"))); } #[test] fn validate_existing_association() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer name".to_string(), - entity_path: "manufacturer.name".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer.name".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", @@ -221,19 +201,24 @@ mod tests { } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); assert!(result.is_ok()); } #[test] - fn validate_existing_association_but_type_mismatch() { + fn validate_valid_optional_value() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer id".to_string(), - entity_path: "manufacturer.id".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer?.name".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", @@ -247,88 +232,71 @@ mod tests { "product_manufacturer": { "entity": "product_manufacturer", "properties": { - "id": { - "type": "uuid" + "name": { + "type": "string" } } } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); - - assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for id in product_manufacturer"))); - } - - #[test] - fn validate_optional_association() { - let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "manufacturer name".to_string(), - entity_path: "manufacturer?.name".to_string(), - field_type: "string".to_string(), - })]; - let api_schema = json!({ - "product": { - "entity": "product", - "properties": {} - } - }); - - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); assert!(result.is_ok()); } #[test] - fn validate_valid_nested_association() { + fn validate_invalid_optional_value() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "tax country".to_string(), - entity_path: "tax.country.name".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "manufacturer name".to_string(), + entity_path: "manufacturer?.name".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", "properties": { - "tax": { - "type": "association", - "entity": "tax" - } - } - }, - "tax": { - "entity": "tax", - "properties": { - "country": { + "manufacturer": { "type": "association", - "entity": "country" + "entity": "product_manufacturer" } } }, - "country": { - "entity": "country", + "product_manufacturer": { + "entity": "product_manufacturer", "properties": { - "name": { - "type": "string", + "id": { + "type": "uuid" } } } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); - assert!(result.is_ok()); + assert!(result.is_err_and(|x| x + .to_string() + .contains("Entity product_manufacturer does not have a field name"))); } #[test] - fn validate_invalid_nested_association() { + fn validate_valid_nested_association() { let entity = "product"; - let mapping = vec![crate::config::Mapping::ByPath(crate::config::EntityPathMapping { - file_column: "tax country".to_string(), - entity_path: "tax.country.id".to_string(), - field_type: "string".to_string(), - })]; + let mapping = vec![crate::config::Mapping::ByPath( + crate::config::EntityPathMapping { + file_column: "tax country".to_string(), + entity_path: "tax.country.name".to_string(), + }, + )]; let api_schema = json!({ "product": { "entity": "product", @@ -351,15 +319,19 @@ mod tests { "country": { "entity": "country", "properties": { - "id": { - "type": "uuid", + "name": { + "type": "string", } } } }); - let result = crate::data::validate::validate_paths_for_entity(entity, &mapping, api_schema.as_object().unwrap()); + let result = crate::data::validate::validate_paths_for_entity( + entity, + &mapping, + api_schema.as_object().unwrap(), + ); - assert!(result.is_err_and(|x| x.to_string().contains("Type string does not match schema type uuid for id in country"))); + assert!(result.is_ok()); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 7f0d51a..6faabaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use crate::api::SwClient; use crate::config::{Credentials, Mapping, Schema}; -use crate::data::{export, import, prepare_scripting_environment, ScriptingEnvironment}; use crate::data::validate_paths_for_entity; +use crate::data::{export, import, prepare_scripting_environment, ScriptingEnvironment}; use anyhow::Context; use clap::{Parser, Subcommand}; use std::collections::HashSet; @@ -174,7 +174,7 @@ async fn create_context( let api_schema = sw_client.entity_schema().await; let entity = &schema.entity; - let _ = validate_paths_for_entity(entity, &schema.mappings, &api_schema?); + validate_paths_for_entity(entity, &schema.mappings, &api_schema?)?; // ToDo: create lookup table for languages + currencies?