From 3798dcb334c99001ed298763de8b3db2d48c4b4a Mon Sep 17 00:00:00 2001 From: Marcin Olichwiruk <21108638+olichwiruk@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:54:14 +0100 Subject: [PATCH] feat: implement AttributeFraming overlay --- semantics/oca-bundle/src/build.rs | 54 +++++++++++ semantics/oca-bundle/src/state/oca.rs | 62 +++++++++++++ .../state/oca/overlay/attribute_framing.rs | 29 +++++- .../oca-file/src/ocafile/instructions/add.rs | 6 ++ .../src/ocafile/instructions/helpers.rs | 84 ++++++++++++++++- semantics/oca-file/src/ocafile/mod.rs | 92 +++++++++++++++++++ tests/build_from_ocafile.rs | 27 +++++- 7 files changed, 348 insertions(+), 6 deletions(-) diff --git a/semantics/oca-bundle/src/build.rs b/semantics/oca-bundle/src/build.rs index 76fe885..eaa7aba 100644 --- a/semantics/oca-bundle/src/build.rs +++ b/semantics/oca-bundle/src/build.rs @@ -1,3 +1,4 @@ +use crate::state::oca::overlay::attribute_framing::{FramingScope, Framings}; use crate::state::oca::overlay::cardinality::Cardinalitys; use crate::state::oca::overlay::character_encoding::CharacterEncodings; use crate::state::oca::overlay::conditional::Conditionals; @@ -471,6 +472,59 @@ pub fn apply_command(base: Option, op: ast::Command) -> Result { + let mut frame_id = None; + let mut frame_meta = HashMap::new(); + if let Some(ref properties) = content.properties { + for prop in properties { + if let (prop_name, ast::NestedValue::Value(prop_value)) = prop { + if prop_name.eq("id") { + frame_id = Some(prop_value.clone()); + } else { + frame_meta.insert(format!("frame_{}", prop_name), prop_value.clone()); + } + } + } + } + if frame_id.is_none() { + errors.push("Undefined frame id".to_string()); + } + + if let Some(ref attributes) = content.attributes { + for (attr_name, attr_framing_value) in attributes { + let mut attribute = oca + .attributes + .get(attr_name) + .ok_or_else(|| { + errors.push(format!("Undefined attribute: {attr_name}")); + errors.clone() + })? + .clone(); + if let ast::NestedValue::Object(attr_framing) = attr_framing_value { + let mut framing = HashMap::new(); + for (framing_key, framing_value) in attr_framing { + if let ast::NestedValue::Object(framing_value) = framing_value { + if let Some(ast::NestedValue::Value(predicate_id)) = framing_value.get("predicate_id") { + if let Some(ast::NestedValue::Value(framing_justification)) = framing_value.get("framing_justification") { + let framing_scope = FramingScope { + predicate_id: predicate_id.to_string(), + framing_justification: framing_justification.to_string(), + frame_meta: frame_meta.clone(), + }; + framing.insert( + framing_key.clone(), + framing_scope + ); + } + } + } + } + attribute.set_framing(frame_id.clone().unwrap(), framing.clone()); + } + oca.add_attribute(attribute); + } + } + } _ => (), } } diff --git a/semantics/oca-bundle/src/state/oca.rs b/semantics/oca-bundle/src/state/oca.rs index 5de713b..a458408 100644 --- a/semantics/oca-bundle/src/state/oca.rs +++ b/semantics/oca-bundle/src/state/oca.rs @@ -11,6 +11,7 @@ use crate::state::oca::overlay::label::Labels; use crate::state::oca::overlay::meta::Metas; use crate::state::oca::overlay::unit::Units; use indexmap::IndexMap; +use overlay::attribute_framing::Framings; use said::derivation::HashFunctionCode; use said::sad::{SerializationFormats, SAD}; use said::version::SerializationInfo; @@ -294,6 +295,26 @@ impl OCABox { } } } + + if let Some(framings) = &attribute.framings { + for frame_id in framings.keys() { + let mut framing_ov = overlays.iter_mut().find(|x| { + match x.as_any().downcast_ref::() { + Some(o) => o.metadata.get("frame_id") == Some(frame_id), + None => false, + } + }); + + if framing_ov.is_none() { + overlays + .push(Box::new(overlay::AttributeFraming::new(frame_id.clone()))); + framing_ov = overlays.last_mut(); + } + if let Some(ov) = framing_ov { + ov.add(attribute); + } + } + } } overlays @@ -503,6 +524,15 @@ impl<'de> Deserialize<'de> for DynOverlay { })?, )); } + OverlayType::AttributeFraming => { + return Ok(Box::new( + de_overlay + .deserialize_into::() + .map_err(|e| { + serde::de::Error::custom(format!("AttributeFraming overlay: {e}")) + })?, + )); + } _ => { return Err(serde::de::Error::custom(format!( "Overlay type not supported: {:?}", @@ -541,6 +571,7 @@ where OverlayType::Label, OverlayType::Information, OverlayType::Link, + OverlayType::AttributeFraming, ]; let mut overlays_map: BTreeMap = BTreeMap::new(); @@ -836,6 +867,29 @@ impl From for OCABox { } } + let framing_overlays = oca_bundle + .overlays + .iter() + .filter_map(|x| x.as_any().downcast_ref::()) + .collect::>(); + for overlay in framing_overlays { + let frame_id = overlay.metadata.get("frame_id").unwrap(); + let mut frame_meta = overlay.metadata.clone(); + frame_meta.remove("frame_id"); + + for (attr_name, attr_framing) in overlay.attribute_framing.iter() { + let framing = attr_framing.clone().iter_mut().map(|(f, scope)| { + scope.frame_meta = frame_meta.clone(); + (f.clone(), scope.clone()) + }).collect::>(); + + attributes + .get_mut(attr_name) + .unwrap() + .set_framing(frame_id.to_string(), framing); + } + } + for (_, attribute) in attributes { oca_box.add_attribute(attribute); } @@ -1250,6 +1304,7 @@ impl AttributeLayoutValues { mod tests { use maplit::hashmap; use oca_ast_semantics::ast::{NestedAttrType, RefValue}; + use overlay::attribute_framing::{FramingScope, Framings}; use said::SelfAddressingIdentifier; use super::*; @@ -1315,6 +1370,13 @@ mod tests { let mut attr = Attribute::new("ref".to_string()); let said = SelfAddressingIdentifier::default(); attr.set_attribute_type(NestedAttrType::Reference(RefValue::Said(said))); + let mut framing = HashMap::new(); + framing.insert("url".to_string(), FramingScope { + predicate_id: "skos:exactMatch".to_string(), + framing_justification: "semapv:ManualMappingCuration".to_string(), + frame_meta: HashMap::new() + }); + attr.set_framing("frame_id".to_string(), framing); oca.add_attribute(attr); let oca_bundle = oca.generate_bundle(); diff --git a/semantics/oca-bundle/src/state/oca/overlay/attribute_framing.rs b/semantics/oca-bundle/src/state/oca/overlay/attribute_framing.rs index 93892a7..731dc85 100644 --- a/semantics/oca-bundle/src/state/oca/overlay/attribute_framing.rs +++ b/semantics/oca-bundle/src/state/oca/overlay/attribute_framing.rs @@ -13,6 +13,8 @@ pub type Framing = HashMap; pub struct FramingScope { pub predicate_id: String, pub framing_justification: String, + #[serde(skip)] + pub frame_meta: HashMap, } pub trait Framings { @@ -38,6 +40,23 @@ impl Framings for Attribute { } } +pub fn serialize_metadata( + metadata: &HashMap, + s: S, +) -> Result +where + S: Serializer, +{ + use std::collections::BTreeMap; + + let mut ser = s.serialize_map(Some(metadata.len()))?; + let sorted_metadata: BTreeMap<_, _> = metadata.iter().collect(); + for (k, v) in sorted_metadata { + ser.serialize_entry(k, v)?; + } + ser.end() +} + pub fn serialize_framing( attributes: &HashMap, s: S, @@ -64,7 +83,7 @@ pub struct AttributeFramingOverlay { capture_base: Option, #[serde(rename = "type")] overlay_type: OverlayType, - #[serde(rename = "framing_metadata")] + #[serde(rename = "framing_metadata", serialize_with = "serialize_metadata")] pub metadata: HashMap, #[serde(serialize_with = "serialize_framing")] pub attribute_framing: HashMap, @@ -100,6 +119,12 @@ impl Overlay for AttributeFramingOverlay { if let Some(value) = framing.get(id) { self.attribute_framing .insert(attribute.name.clone(), value.clone()); + + for framing_scope in value.values() { + for (k, v) in framing_scope.frame_meta.iter() { + self.metadata.insert(k.clone(), v.clone()); + } + } } } } @@ -134,6 +159,7 @@ mod tests { predicate_id: "skos:exactMatch".to_string(), framing_justification: "semapv:ManualMappingCuration" .to_string(), + frame_meta: HashMap::new(), }, ); let mut loc2 = HashMap::new(); @@ -143,6 +169,7 @@ mod tests { predicate_id: "skos:exactMatch".to_string(), framing_justification: "semapv:ManualMappingCuration" .to_string(), + frame_meta: HashMap::new(), }, ); let attr = cascade! { diff --git a/semantics/oca-file/src/ocafile/instructions/add.rs b/semantics/oca-file/src/ocafile/instructions/add.rs index 4dcdc3a..78ca464 100644 --- a/semantics/oca-file/src/ocafile/instructions/add.rs +++ b/semantics/oca-file/src/ocafile/instructions/add.rs @@ -137,6 +137,12 @@ impl AddInstruction { helpers::extract_content(object), )); } + Rule::attribute_framing => { + object_kind = Some(ObjectKind::Overlay( + OverlayType::AttributeFraming, + helpers::extract_content(object), + )); + } Rule::flagged_attrs => { object_kind = Some(ObjectKind::CaptureBase(CaptureContent { properties: None, diff --git a/semantics/oca-file/src/ocafile/instructions/helpers.rs b/semantics/oca-file/src/ocafile/instructions/helpers.rs index f31e580..a3af27c 100644 --- a/semantics/oca-file/src/ocafile/instructions/helpers.rs +++ b/semantics/oca-file/src/ocafile/instructions/helpers.rs @@ -89,12 +89,14 @@ pub fn extract_attribute_key_pairs(attr_pair: Pair) -> Option<(String, NestedVal debug!("Extracting the attribute from: {:?}", attr_pair); for item in attr_pair.into_inner() { match item.as_rule() { - Rule::attr_key => { + Rule::attr_key | + Rule::framing_metadata_key => { key = item.as_str().to_string(); debug!("Extracting attribute key {:?}", key); } Rule::key_value | - Rule::unit_value => { + Rule::unit_value | + Rule::framing_metadata_value => { if let Some(nested_item) = item.clone().into_inner().next() { match nested_item.as_rule() { Rule::string => { @@ -284,6 +286,9 @@ pub fn extract_attribute_key_pairs(attr_pair: Pair) -> Option<(String, NestedVal } } } + Rule::json_object => { + value = extract_json_object(item); + } _ => { panic!("Invalid attribute in {:?}", item.as_rule()); } @@ -292,6 +297,66 @@ pub fn extract_attribute_key_pairs(attr_pair: Pair) -> Option<(String, NestedVal Some((key, value)) } +pub fn extract_json_object(object: Pair) -> NestedValue { + let mut json_object = IndexMap::new(); + for item in object.into_inner() { + match item.as_rule() { + Rule::json_pair => { + let mut key = String::new(); + let mut value = NestedValue::Value(String::new()); + for el in item.clone().into_inner() { + match el.as_rule() { + Rule::json_key => { + key = el.clone() + .into_inner() + .last() + .unwrap() + .into_inner() + .last() + .unwrap() + .as_span() + .as_str() + .to_lowercase(); + } + Rule::json_value => { + if let Some(nested_item) = el.clone().into_inner().next() { + match nested_item.as_rule() { + Rule::string => { + value = NestedValue::Value( + nested_item + .clone() + .into_inner() + .last() + .unwrap() + .as_span() + .as_str() + .to_string(), + ); + } + Rule::json_object => { + value = extract_json_object(nested_item); + } + _ => { + panic!("Invalid json value in {:?}", nested_item.as_rule()); + } + } + } + } + _ => { + panic!("Invalid json pair in {:?}", el.as_rule()); + } + } + } + json_object.insert(key, value); + } + _ => { + panic!("Invalid json object in {:?}", item.as_rule()); + } + } + } + NestedValue::Object(json_object) +} + pub fn extract_attributes_key_paris(object: Pair) -> Option> { let mut attributes: IndexMap = IndexMap::new(); @@ -302,7 +367,8 @@ pub fn extract_attributes_key_paris(object: Pair) -> Option { + Rule::unit_attr_key_pairs | + Rule::attr_framing_key_pairs => { for attr in attr.into_inner() { debug!("Parsing attribute {:?}", attr); if let Some((key, value)) = extract_attribute_key_pairs(attr) { @@ -375,6 +441,18 @@ pub fn extract_properites_key_pairs(object: Pair) -> Option { + debug!("Parsing framing metadata: {:?}", attr.as_str()); + for prop in attr.into_inner() { + debug!("Parsing property {:?}", prop); + if let Some((key, value)) = extract_attribute_key_pairs(prop) { + debug!("Parsed property: {:?} = {:?}", key, value); + properties.insert(key, value); + } else { + debug!("Skipping property"); + } + } + } _ => { debug!( "Unexpected token: Invalid attribute in instruction {:?}", diff --git a/semantics/oca-file/src/ocafile/mod.rs b/semantics/oca-file/src/ocafile/mod.rs index f014916..16f8283 100644 --- a/semantics/oca-file/src/ocafile/mod.rs +++ b/semantics/oca-file/src/ocafile/mod.rs @@ -413,6 +413,58 @@ pub fn generate_from_ast(ast: &OCAAst) -> String { } }; } + ast::OverlayType::AttributeFraming => { + line.push_str("ATTR_FRAMING \\\n"); + if let Some(content) = command.object_kind.overlay_content() { + if let Some(ref properties) = content.properties { + for (prop_name, prop_value) in properties { + let key = prop_name.replace("frame_", ""); + if let ast::NestedValue::Value(value) = prop_value { + line.push_str(format!(" {}=\"{}\" \\\n", key, value).as_str()); + } + } + } + if let Some(ref attributes) = content.attributes { + line.push_str(" ATTRS \\"); + attributes.iter().for_each(|(key, value)| { + if let ast::NestedValue::Object(object) = value { + let mut frames_str = "".to_string(); + for (f_key, f_value) in object.iter() { + let mut frame_str = "\n ".to_string(); + frame_str.push_str( + format!( + "\"{}\": {{", + f_key + ).as_str() + ); + + if let ast::NestedValue::Object(frame) = f_value { + frame.iter().for_each(|(frame_key, frame_value)| { + if let ast::NestedValue::Value(frame_value) = frame_value { + frame_str.push_str( + format!( + "\n \"{}\": \"{}\",", + frame_key, + frame_value + ).as_str() + ); + } + + }); + } + + frame_str.push_str("\n },"); + + frames_str.push_str(frame_str.as_str()); + } + line.push_str( + format!("\n {}={{{}\n }}", key, frames_str).as_str(), + ); + } + }); + } + }; + } _ => { line.push_str( format!("{} ", o_type.to_string().to_case(Case::UpperSnake)) @@ -519,6 +571,24 @@ ADD CARDINALITY ATTRS list="1-2" ADD ENTRY_CODE ATTRS list="entry_code_said" el=["o1", "o2", "o3"] ADD ENTRY en ATTRS list="entry_said" el={"o1": "o1_label", "o2": "o2_label", "o3": "o3_label"} ADD FLAGGED_ATTRIBUTES name age +ADD ATTR_FRAMING \ + id=SNOMEDCT \ + label="Systematized Nomenclature of Medicine Clinical Terms" \ + location="https://bioportal.bioontology.org/ontologies/SNOMEDCT" \ + version=2023AA \ + ATTRS \ + name = { + "http://purl.bioontology.org/ontology/SNOMEDCT/703503000": { + "Predicate_id": "skos:exactMatch", + "Framing_justification": "semapv:ManualMappingCuration" + } + } + age = { + "http://purl.bioontology.org/ontology/SNOMEDCT/397669002": { + "Predicate_id": "skos:exactMatch", + "Framing_justification": "semapv:ManualMappingCuration" + } + } "#; let oca_ast = parse_from_string(unparsed_file.to_string()).unwrap(); assert_eq!(oca_ast.meta.get("version").unwrap(), "0.0.1"); @@ -551,6 +621,28 @@ ADD CONDITION ATTRS radio="${age} > 18" ADD ENTRY_CODE ATTRS list={"g1": ["el1"], "g2": ["el2", "el3"]} ADD ENTRY pl ATTRS list={"el1": "element1", "el2": "element2", "el3": "element3", "g1": "grupa1", "g2": "grupa2"} ADD LINK refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu ATTRS name="n" +ADD ATTR_FRAMING \ + id="SNOMEDCT" \ + label="Systematized Nomenclature of Medicine Clinical Terms" \ + location="https://bioportal.bioontology.org/ontologies/SNOMEDCT" \ + version="2023AA" \ + ATTRS \ + name={ + "http://purl.bioontology.org/ontology/snomedct/703503000": { + "predicate_id": "skos:exactMatch", + "framing_justification": "semapv:ManualMappingCuration", + }, + "http://purl.bioontology.org/ontology/snomedct/703503001": { + "predicate_id": "skos:exactMatch", + "framing_justification": "semapv:ManualMappingCuration", + }, + } + age={ + "http://purl.bioontology.org/ontology/snomedct/397669002": { + "predicate_id": "skos:exactMatch", + "framing_justification": "semapv:ManualMappingCuration", + }, + } "#; let oca_ast = parse_from_string(unparsed_file.to_string()).unwrap(); diff --git a/tests/build_from_ocafile.rs b/tests/build_from_ocafile.rs index c7e92fd..22fcb00 100644 --- a/tests/build_from_ocafile.rs +++ b/tests/build_from_ocafile.rs @@ -20,6 +20,29 @@ ADD CHARACTER_ENCODING ATTRS d=utf-8 i=utf-8 passed=utf-8 ADD CONFORMANCE ATTRS d=M i=M passed=M ADD LABEL en ATTRS d="Schema digest" i="Credential Issuee" passed="Passed" ADD INFORMATION en ATTRS d="Schema digest" i="Credential Issuee" passed="Enables or disables passing" + +ADD ATTR_FRAMING \ + id=SNOMEDCT \ + label="Systematized Nomenclature of Medicine Clinical Terms" \ + location="https://bioportal.bioontology.org/ontologies/SNOMEDCT" \ + version=2023AA \ + ATTRS \ + d = { + "http://purl.bioontology.org/ontology/SNOMEDCT/703503000": { + "Predicate_id": "skos:exactMatch", + "Framing_justification": "semapv:ManualMappingCuration" + }, + "http://purl.bioontology.org/ontology/SNOMEDCT/703503001": { + "Predicate_id": "skos:exactMatch", + "Framing_justification": "semapv:ManualMappingCuration" + } + } + i = { + "http://purl.bioontology.org/ontology/SNOMEDCT/397669002": { + "Predicate_id": "skos:exactMatch", + "Framing_justification": "semapv:ManualMappingCuration" + } + } "#.to_string(); let mut facade = Facade::new(Box::new(db), Box::new(db_cache), cache_storage_config); @@ -27,7 +50,7 @@ ADD INFORMATION en ATTRS d="Schema digest" i="Credential Issuee" passed="Enables assert_eq!( result.said.clone().unwrap().to_string(), - "EKHBds6myKVIsQuT7Zr23M8Xk_gwq-2SaDRUprvqOXxa" + "EHP1RKZeYhIO7zTb9JJDfsNeTLaOp84GE9oDaEj9XlFk" ); let code = HashFunctionCode::Blake3_256; @@ -36,7 +59,7 @@ ADD INFORMATION en ATTRS d="Schema digest" i="Credential Issuee" passed="Enables let oca_bundle_version = String::from_utf8( oca_bundle_encoded[6..23].to_vec() ).unwrap(); - assert_eq!(oca_bundle_version, "OCAS11JSON000646_"); + assert_eq!(oca_bundle_version, "OCAS11JSON0009ac_"); let search_result = facade.search_oca_bundle(None, "Ent".to_string(), 10, 1); assert_eq!(search_result.metadata.total, 1);