Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Commit

Permalink
Merge pull request #16 from jozsefdamokos/schema-validation
Browse files Browse the repository at this point in the history
Added schema validation
  • Loading branch information
MalteJanz committed Jul 4, 2024
2 parents 8b49ee9 + 394c303 commit 179d0f5
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/data/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
337 changes: 337 additions & 0 deletions src/data/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
use crate::config::Mapping;

/// Validate paths for entity
pub fn validate_paths_for_entity(
entity: &str,
mappings: &Vec<Mapping>,
api_schema: &serde_json::Map<String, serde_json::Value>,
) -> 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::<Vec<_>>();
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 has only one part it should be a simple field
if path.len() == 1 {
continue;
}

// 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 entity_name = root_property["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,
});

// 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(),
},
)];
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(),
},
)];
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() {
let entity = "product";
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(),
);

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(),
},
)];
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(),
},
)];
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_valid_optional_value() {
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(),
},
)];
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_invalid_optional_value() {
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(),
},
)];
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("Entity product_manufacturer does not have a field name")));
}

#[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(),
},
)];
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());
}
}
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::api::SwClient;
use crate::config::{Credentials, Mapping, Schema};
use crate::data::validate_paths_for_entity;
use crate::data::{export, import, prepare_scripting_environment, ScriptingEnvironment};
use anyhow::Context;
use clap::{Parser, Subcommand};
Expand Down Expand Up @@ -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;

validate_paths_for_entity(entity, &schema.mappings, &api_schema?)?;

// ToDo: create lookup table for languages + currencies?

Expand Down

0 comments on commit 179d0f5

Please sign in to comment.