Skip to main content

oca_bundle/state/
oca_bundle.rs

1use indexmap::IndexMap;
2use log::info;
3use overlay::{Overlay, OverlayModel};
4use overlay_file::overlay_registry::{OverlayLocalRegistry, OverlayRegistry};
5pub use said::derivation::{HashFunction, HashFunctionCode};
6pub use said::error;
7pub use said::{ProtocolVersion, SelfAddressingIdentifier, make_me_sad};
8use serde::ser::Error;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use thiserror::Error;
12pub mod capture_base;
13pub mod overlay;
14use crate::state::{attribute::Attribute, oca_bundle::capture_base::CaptureBase};
15use oca_ast::ast::{CaptureContent, Command, CommandType, OCAAst, ObjectKind, OverlayContent};
16
17#[derive(Serialize, Deserialize, Debug, Clone)]
18pub struct OCABundle {
19    /// CESR version of the OCA Bundle with OCAS prefix
20    #[serde(rename = "v")]
21    pub version: String,
22    pub digest: Option<said::SelfAddressingIdentifier>,
23    pub capture_base: CaptureBase,
24    pub overlays: Vec<Overlay>,
25}
26
27#[derive(Serialize, Debug, Deserialize, Clone, Default)]
28pub struct OCABundleModel {
29    /// CESR version of the OCA Bundle with OCAS prefix
30    #[serde(rename = "v")]
31    pub version: String,
32    pub digest: Option<said::SelfAddressingIdentifier>,
33    pub capture_base: CaptureBase,
34    pub overlays: Vec<OverlayModel>,
35    // Storing attributes in different model for easy read access
36    #[serde(skip)]
37    pub attributes: Option<HashMap<String, Attribute>>,
38}
39
40impl From<OCABundleModel> for OCABundle {
41    fn from(model: OCABundleModel) -> Self {
42        OCABundle {
43            version: model.version.clone(),
44            digest: model.digest.clone(),
45            capture_base: model.capture_base.clone(),
46            overlays: model.overlays.iter().map(Overlay::from).collect(),
47        }
48    }
49}
50
51pub struct OCABundleWithRegistry {
52    pub bundle: OCABundle,
53    pub registry: OverlayLocalRegistry,
54}
55
56impl From<OCABundleWithRegistry> for OCABundleModel {
57    fn from(br: OCABundleWithRegistry) -> Self {
58        OCABundleModel {
59            version: br.bundle.version.clone(),
60            digest: br.bundle.digest.clone(),
61            capture_base: br.bundle.capture_base.clone(),
62            overlays: br
63                .bundle
64                .overlays
65                .iter()
66                .map(|om| {
67                    let mut overlay = om.model.clone();
68                    overlay.overlay_def = br.registry.get_overlay(&overlay.name).ok().cloned();
69                    overlay
70                })
71                .collect(),
72            attributes: None,
73        }
74    }
75}
76
77impl OCABundleModel {
78    pub fn new(capture_base: CaptureBase, overlays: Vec<OverlayModel>) -> Self {
79        OCABundleModel {
80            version: "".to_string(),
81            digest: None,
82            capture_base,
83            overlays,
84            attributes: None,
85        }
86    }
87
88    pub fn to_ast(&self) -> OCAAst {
89        let mut ast = OCAAst::new();
90
91        let mut attributes = IndexMap::new();
92        self.capture_base
93            .attributes
94            .iter()
95            .for_each(|(attr_name, attr_type)| {
96                attributes.insert(attr_name.clone(), attr_type.clone());
97            });
98
99        let command = Command {
100            kind: CommandType::Add,
101            object_kind: ObjectKind::CaptureBase(CaptureContent {
102                attributes: Some(self.capture_base.attributes.clone()),
103            }),
104        };
105        ast.commands.push(command);
106
107        self.overlays.iter().for_each(|overlay| {
108            if let Some(overlay_def) = &overlay.overlay_def {
109                let overlay_content = OverlayContent {
110                    overlay_def: overlay_def.clone(),
111                    properties: overlay.properties.clone(),
112                };
113                let overlay_command = Command {
114                    kind: CommandType::Add,
115                    object_kind: ObjectKind::Overlay(overlay_content),
116                };
117                ast.commands.push(overlay_command);
118            }
119        });
120        ast
121    }
122
123    // Prepare list of attributes to fill with overlays properties
124    pub fn fill_attributes(&mut self) {
125        // to avoid misalignment, we will allways populate fresh attribute from main model
126        self.attributes = Some(HashMap::new());
127        if let Some(ref mut attrs) = self.attributes {
128            for (attr_name, attr_type) in self.capture_base.attributes.clone() {
129                let attr = Attribute {
130                    name: attr_name.clone(),
131                    attribute_type: Some(attr_type),
132                    ..Default::default()
133                };
134                attrs.insert(attr_name.clone(), attr);
135            }
136        }
137    }
138
139    /// Remove attribute from the OCA Bundle
140    /// if attribute does not exist, nothing will happen
141    pub fn remove_attribute(&mut self, attr_name: &String) {
142        self.capture_base.attributes.shift_remove(attr_name);
143    }
144
145    pub fn get_attribute_by_name(&self, name: &str) -> Option<&Attribute> {
146        self.attributes.as_ref().and_then(|attrs| attrs.get(name))
147    }
148
149    /// This method will compute digest for OCABundle and all it's members (capture_base and each
150    /// overlay) filling as well capture_base digest into overlays. It would use external
151    /// structures instead of the internal Models i.e. OCABundle vs OCABundleModel
152    /// Arguments:
153    /// * `self` - OCABundleModel to compute digest for
154    ///
155    /// Returns:
156    /// * `Result<said::SelfAddressingIdentifier, OCABundleSerializationError>` - Result with computed
157    ///   SAID or an error if serialization or digest computation fails.
158    pub fn compute_and_fill_digest(
159        &mut self,
160    ) -> Result<said::SelfAddressingIdentifier, OCABundleSerializationError> {
161        // Compute digest for all objects
162        info!("Computing digest for OCABundle");
163        // TODO change to compute and fill
164        match self.capture_base.fill_digest() {
165            Ok(_) => info!("Capture base digest filled successfully"),
166            Err(e) => {
167                return Err(OCABundleSerializationError::SerializationError(
168                    e.to_string(),
169                ));
170            }
171        }
172        let cb_said = self.capture_base.digest.clone();
173        info!("Capture base SAID: {:?}", cb_said);
174        for overlay in &mut self.overlays {
175            overlay.capture_base_said = cb_said.clone();
176            // TODO change to compute and fill
177            match overlay.fill_digest() {
178                Ok(_) => info!("Overlay {} digest filled successfully", overlay.name),
179                Err(e) => {
180                    return Err(OCABundleSerializationError::SerializationError(
181                        e.to_string(),
182                    ));
183                }
184            }
185        }
186
187        let oca_bundle = OCABundle::from(self.clone());
188        let serialized_bundle = serde_json::to_string(&oca_bundle)
189            .map_err(|_| serde_json::Error::custom("Failed to serialize OCABundleModel"))
190            .unwrap();
191
192        let code = HashFunctionCode::Blake3_256;
193        let said_field = Some("digest");
194        let version = ProtocolVersion::new("OCAS", 2, 0).unwrap();
195        let input = serialized_bundle.as_str();
196        match make_me_sad(input, code, version, said_field) {
197            Ok(sad) => {
198                #[derive(Deserialize)]
199                struct OCABundlePartial {
200                    digest: String,
201                    #[serde(rename = "v")]
202                    version: String,
203                }
204
205                let bundle: OCABundlePartial = serde_json::from_str(&sad)
206                    .map_err(|e| OCABundleSerializationError::SerializationError(e.to_string()))?;
207                let said: SelfAddressingIdentifier = bundle.digest.parse().map_err(|_| {
208                    OCABundleSerializationError::SerializationError(
209                        "Failed to parse SAID".to_string(),
210                    )
211                })?;
212                self.digest = Some(said.clone());
213                self.version = bundle.version;
214                Ok(said)
215            }
216            Err(_) => Err(OCABundleSerializationError::SerializationError(
217                "Failed to compute digest for OCABundle".to_string(),
218            )),
219        }
220    }
221}
222#[derive(Error, Debug)]
223pub enum OCABundleSerializationError {
224    #[error("Failed to serialize OCA bundle: {0}")]
225    SerializationError(String),
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::build::from_ast;
232    use oca_file::ocafile::parse_from_string;
233    use overlay_file::overlay_registry::OverlayLocalRegistry;
234
235    #[test]
236    fn build_oca_bundle() {
237        let _ = env_logger::builder().is_test(true).try_init();
238        let unparsed_file = r#"
239-- version=2.0.0
240-- name=プラスウルトラ
241ADD ATTRIBUTE remove=Text
242ADD ATTRIBUTE name=Text age=Numeric car=[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]
243REMOVE ATTRIBUTE remove
244ADD ATTRIBUTE incidentals_spare_parts=[[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]]
245ADD ATTRIBUTE d=Text i=Text passed=Boolean
246ADD Overlay META
247  language="en"
248  description="Entrance credential"
249  name="Entrance credential"
250ADD Overlay CHARACTER_ENCODING
251  attribute_character_encodings
252    d="utf-8"
253    i="utf-8"
254    passed="utf-8"
255ADD Overlay CONFORMANCE
256  attribute_conformances=["d", "i", "passed"]
257ADD Overlay LABEL
258  language="en"
259  attribute_labels
260    d="Schema digest"
261    i="Credential Issuee"
262    passed="Passed"
263ADD Overlay FORMAT
264  attribute_formats
265    d="image/jpeg"
266ADD Overlay UNIT
267  metric_system="SI"
268  attribute_units
269    i="m^2"
270    d="°"
271ADD ATTRIBUTE list=[Text] el=Text
272ADD Overlay CARDINALITY
273  attribute_cardinalities
274    list="1-2"
275ADD Overlay ENTRY_CODE
276  attribute_entry_codes
277    list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
278    el=["o1", "o2", "o3"]
279ADD Overlay ENTRY
280  language="en"
281  attribute_entries
282    list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
283    el
284     o1="o1_label"
285     o2="o2_label"
286     o3="o3_label"
287"#;
288        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays/").unwrap();
289        let oca_ast = parse_from_string(unparsed_file.to_string(), &registry).unwrap();
290
291        let bundle_json = r#"
292        {"v":"OCAS02JSON0009d7_","digest":"ECQDVFB4dcPgHfMQXg-9xDpeBr8_-iZzy6ermbBMcj50","capture_base":{"digest":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"capture_base/2.0.0","attributes":{"age":"Numeric","car":["refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu"],"d":"Text","el":"Text","i":"Text","incidentals_spare_parts":[["refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu"]],"list":["Text"],"name":"Text","passed":"Boolean"}},"overlays":[{"digest":"EEk6wQBfPuqddeVOPFLgSY9qv1ZorGCvip_oQtFdD9GV","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/meta/2.0.0","language":"en","description":"Entrance credential","name":"Entrance credential"},{"digest":"EPVOc4fR5Nwe2yHzFS-4wBf3kcm7C5D4XNjY9cxnFaQh","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/character_encoding/2.0.0","attribute_character_encodings":{"d":"utf-8","i":"utf-8","passed":"utf-8"}},{"digest":"EPB8YG6m6Q_uOWNKZp30qkN3_UlvTvLiTK_mm7ncCMiH","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/conformance/2.0.0","attribute_conformances":["d","i","passed"]},{"digest":"EEy4mJ4SIxauAyk8FI1QqBa26qG1Fqn2uhN_Vf4RMIbL","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/label/2.0.0","language":"en","attribute_labels":{"d":"Schema digest","i":"Credential Issuee","passed":"Passed"}},{"digest":"EJi35V6qV5tUhnjDR3qiB2irAKLkbQVu-rU_hehkhop1","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/format/2.0.0","attribute_formats":{"d":"image/jpeg"}},{"digest":"EPdl6CuC9i9IszrkqvEkv9qZPM-WnX47DOD80dwGiHpL","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/unit/2.0.0","metric_system":"SI","attribute_units":{"i":"m^2","d":"°"}},{"digest":"EFbS7GQMBi_RCk2Q8cJKR2ohCE--248bH1OQnwiFzmer","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/cardinality/2.0.0","attribute_cardinalities":{"list":"1-2"}},{"digest":"ED6ktKLPYEmJfYTEo7-YR-xyPwHUgpEOdEwOe_Kr6c22","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/entry_code/2.0.0","attribute_entry_codes":{"list":"refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu","el":["o1","o2","o3"]}},{"digest":"EIMaWbfJ98gO1sTucmYdgaZu_u94djMa75BYl8lzkvfc","capture_base":"EMDyoUr57UN7-Wy3kmF0WyG0xiQieckUdW18VGdEuve9","type":"overlay/entry/2.0.0","language":"en","attribute_entries":{"list":"refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu","el":{"o1":"o1_label","o2":"o2_label","o3":"o3_label"}}}]}
293"#;
294        let reference_json: serde_json::Value = serde_json::from_str(bundle_json).unwrap();
295        let mut oca_bundle = from_ast(None, &oca_ast).unwrap().oca_bundle;
296        let mut oca_bundle2 = from_ast(None, &oca_ast).unwrap().oca_bundle;
297        oca_bundle.compute_and_fill_digest().unwrap();
298        oca_bundle2.compute_and_fill_digest().unwrap();
299
300        let mut overlay_model = oca_bundle.overlays.first().unwrap().clone();
301        // compute digest on overlay directly to see if the process is deterministic and equals
302        // with compute on bundle
303        match overlay_model.fill_digest() {
304            Ok(_) => info!("Overlay model digest filled successfully"),
305            Err(e) => panic!("Failed to fill overlay model digest: {}", e),
306        }
307        let meta_said = overlay_model.digest.clone().unwrap();
308        let ref_said = "EEk6wQBfPuqddeVOPFLgSY9qv1ZorGCvip_oQtFdD9GV";
309        assert_eq!(meta_said.to_string(), ref_said.to_string());
310        assert_eq!(oca_bundle.version, "OCAS02JSON0009d7_");
311
312        let said = oca_bundle.digest.clone().unwrap();
313        let bundle = OCABundle::from(oca_bundle.clone());
314        let oca_bundle_json = serde_json::to_string(&bundle).unwrap();
315
316        println!("OCA Bundle JSON: \n{}", oca_bundle_json);
317
318        assert_eq!(
319            serde_json::from_str::<serde_json::Value>(&oca_bundle_json).unwrap(),
320            reference_json
321        );
322        let said2 = oca_bundle2.digest.unwrap();
323        // Check if process is deterministic and gives always same SAID
324        assert_eq!(said, said2);
325    }
326}