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

Commit

Permalink
Implemented entity_path resolution for import
Browse files Browse the repository at this point in the history
  • Loading branch information
MalteJanz committed Jun 29, 2024
1 parent e158630 commit dfcec5a
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# NEXT-RELEASE
- "To-One-Association" values are now imported correctly

# v0.3.0
- Added `associations` entry for schema (used on export only)
- Implemented proper `entity_path` resolution with optional chaining `?.` for export
Expand Down
51 changes: 51 additions & 0 deletions profiles/product_with_manufacturer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Profile for products together with their manufacturers
# Manufacturers can be modified and imported again
entity: product

filter:
# export main products (parentId = NULL) only
- type: "equals"
field: "parentId"
value: null

sort:
- field: "manufacturerId"
order: "DESC"

mappings:
- file_column: "id"
entity_path: "id"
- file_column: "product number"
entity_path: "productNumber"
- file_column: "default name"
entity_path: "name"
- file_column: "default price net"
key: "default_price_net"
- file_column: "default price gross"
key: "default_price_gross"
- file_column: "stock"
entity_path: "stock"
- file_column: "tax id"
entity_path: "taxId"
- file_column: "manufacturer id"
entity_path: "manufacturer?.id"
- file_column: "manufacturer name"
entity_path: "manufacturer?.name"
- file_column: "manufacturer website"
entity_path: "manufacturer?.link"

serialize_script: |
// ToDo: add convenience function to lookup default currencyId
let price = entity.price.find(|p| p.currencyId == "b7d2554b0ce847cd82f3ac9bd1c0dfca");
row.default_price_net = price.net;
row.default_price_gross = price.gross;
deserialize_script: |
entity.price = [
#{
net: row.default_price_net,
gross: row.default_price_gross,
linked: true,
currencyId: "b7d2554b0ce847cd82f3ac9bd1c0dfca",
}
];
144 changes: 139 additions & 5 deletions src/data/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn deserialize_row(
.context("failed to get column of row")?;
let raw_value_lowercase = raw_value.to_lowercase();

let json_value = if raw_value_lowercase == "null" {
let json_value = if raw_value_lowercase == "null" || raw_value.trim().is_empty() {
serde_json::Value::Null
} else if raw_value_lowercase == "true" {
serde_json::Value::Bool(true)
Expand All @@ -88,8 +88,7 @@ pub fn deserialize_row(
serde_json::Value::String(raw_value.to_owned())
};

// ToDo: insert path must be considered and create child json objects (maps)
entity.insert(path_mapping.entity_path.clone(), json_value);
entity.insert_by_path(&path_mapping.entity_path, json_value);
}
Mapping::ByScript(_script_mapping) => {
// nothing to do here, the script already executed beforehand
Expand Down Expand Up @@ -294,11 +293,35 @@ impl EntityPath for Entity {
panic!("empty entity_path encountered");
}

let mut tokens = path.split('.').map(|t| t.trim_end_matches('?'));
let mut tokens = path.split('.').map(|t| t.trim_end_matches('?')).peekable();

let first_token = tokens.next().expect("has a value because non empty");
let pointer = self.entry(first_token).or_insert_with(|| {
if tokens.peek().is_none() {
value.clone()
} else {
let child = Entity::with_capacity(1);
serde_json::Value::Object(child)
}
});
if tokens.peek().is_none() {
*pointer = value;
return;
}

let mut pointer = pointer.as_object_mut().expect("insert_by_path lead to non object");
while let Some(token) = tokens.next() {
if tokens.peek().is_none() {
// simply insert the value
pointer.insert(token.to_string(), value);
return;
}

todo!("implement me and write tests")
pointer = pointer.entry(token).or_insert_with(|| {
let child = Entity::with_capacity(1);
serde_json::Value::Object(child)
}).as_object_mut().expect("insert_by_path lead to non object");
}
}
}

Expand Down Expand Up @@ -345,4 +368,115 @@ mod tests {
assert_eq!(entity.get_by_path("child.hello"), Some(&Value::Null));
assert_eq!(entity.get_by_path("child.hello?.bar"), Some(&Value::Null));
}

#[test]
fn test_insert_by_path() {
let entity = json!({
"fiz": "buz"
});
let mut entity = match entity {
Value::Object(map) => map,
_ => unreachable!(),
};

entity.insert_by_path("child.bar", json!("hello"));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": "buz",
"child": {
"bar": "hello",
},
}));

entity.insert_by_path("another.nested.child.value", json!(42));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": "buz",
"child": {
"bar": "hello",
},
"another": {
"nested": {
"child": {
"value": 42,
},
},
},
}));

entity.insert_by_path("fiz", json!(42));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": 42,
"child": {
"bar": "hello",
},
"another": {
"nested": {
"child": {
"value": 42,
},
},
},
}));

entity.insert_by_path("child.bar", json!("buz"));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": 42,
"child": {
"bar": "buz",
},
"another": {
"nested": {
"child": {
"value": 42,
},
},
},
}));

entity.insert_by_path("child.hello", json!("world"));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": 42,
"child": {
"bar": "buz",
"hello": "world",
},
"another": {
"nested": {
"child": {
"value": 42,
},
},
},
}));

entity.insert_by_path("another.nested.sibling", json!({"type": "cousin"}));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": 42,
"child": {
"bar": "buz",
"hello": "world",
},
"another": {
"nested": {
"child": {
"value": 42,
},
"sibling": {
"type": "cousin",
},
},
},
}));

entity.insert_by_path("another.nested", json!("replaced"));
assert_eq!(Value::Object(entity.clone()), json!({
"fiz": 42,
"child": {
"bar": "buz",
"hello": "world",
},
"another": {
"nested": "replaced"
},
}));
}
}

0 comments on commit dfcec5a

Please sign in to comment.