Skip to main content

oca_store/facade/
build.rs

1use super::Facade;
2use super::fetch::get_oca_bundle_model;
3use crate::data_storage::{DataStorage, Namespace};
4#[cfg(feature = "local-references")]
5use crate::local_references;
6#[cfg(feature = "local-references")]
7pub use crate::local_references::References;
8use crate::repositories::{
9    CaptureBaseCacheRecord, CaptureBaseCacheRepo, OCABundleCacheRecord, OCABundleCacheRepo,
10};
11#[cfg(feature = "local-references")]
12use log::debug;
13use log::info;
14use oca_ast::ast::{OCAAst, ObjectKind, RefValue, ReferenceAttrType};
15use oca_bundle::build::{OCABuild, OCABuildStep};
16use oca_bundle::state::oca_bundle::{OCABundle, OCABundleModel};
17use oca_dag::build_core_db_model;
18use oca_file::ocafile;
19use overlay_file::overlay_registry::OverlayLocalRegistry;
20use std::str::FromStr;
21#[cfg(feature = "local-references")]
22use said::SelfAddressingIdentifier;
23
24#[derive(thiserror::Error, Debug, serde::Serialize)]
25#[serde(untagged)]
26pub enum Error {
27    #[error("Validation error")]
28    ValidationError(Vec<ValidationError>),
29    #[error("Deprecated")]
30    Deprecated,
31}
32
33#[derive(thiserror::Error, Debug, serde::Serialize)]
34#[serde(untagged)]
35pub enum ValidationError {
36    #[error(transparent)]
37    OCAFileParse(#[from] oca_file::ocafile::error::ParseError),
38    #[error(transparent)]
39    OCABundleBuild(#[from] oca_bundle::build::Error),
40    #[error("Error at line {line_number} ({raw_line}): {message}")]
41    InvalidCommand {
42        #[serde(rename = "ln")]
43        line_number: usize,
44        #[serde(rename = "c")]
45        raw_line: String,
46        #[serde(rename = "e")]
47        message: String,
48    },
49    #[cfg(feature = "local-references")]
50    #[error("Reference {0} not found")]
51    UnknownRefn(String),
52    #[error("Local references not enabled")]
53    LocalReferencesNotEnabled(),
54    #[error("Storage error: {0}")]
55    StorageError(String),
56}
57
58#[cfg(feature = "local-references")]
59impl References for Box<dyn DataStorage> {
60    fn find(&self, refn: &str) -> Option<String> {
61        self.get(Namespace::OCAReferences, refn)
62            .unwrap()
63            .map(|said| String::from_utf8(said).unwrap())
64    }
65
66    fn save(&mut self, refn: &str, value: String) {
67        self.insert(Namespace::OCAReferences, refn, value.to_string().as_bytes())
68            .unwrap()
69    }
70}
71
72#[cfg(feature = "local-references")]
73impl References for dyn DataStorage + '_ {
74    fn find(&self, refn: &str) -> Option<String> {
75        self.get(Namespace::OCAReferences, refn)
76            .unwrap()
77            .map(|said| String::from_utf8(said).unwrap())
78    }
79
80    fn save(&mut self, refn: &str, value: String) {
81        self.insert(Namespace::OCAReferences, refn, value.to_string().as_bytes())
82            .unwrap()
83    }
84}
85
86#[cfg(feature = "local-references")]
87struct DataStorageReferences<'a>(&'a mut dyn DataStorage);
88
89#[cfg(feature = "local-references")]
90impl References for DataStorageReferences<'_> {
91    fn find(&self, refn: &str) -> Option<String> {
92        self.0
93            .get(Namespace::OCAReferences, refn)
94            .unwrap()
95            .map(|said| String::from_utf8(said).unwrap())
96    }
97
98    fn save(&mut self, refn: &str, value: String) {
99        self.0
100            .insert(Namespace::OCAReferences, refn, value.to_string().as_bytes())
101            .unwrap()
102    }
103}
104
105/// Build an OCABundle from OCAFILE
106pub fn build_from_ocafile(
107    ocafile: String,
108    registry: OverlayLocalRegistry,
109) -> Result<OCABundle, Error> {
110    let ast = ocafile::parse_from_string(ocafile.clone(), &registry)
111        .map_err(|e| Error::ValidationError(vec![ValidationError::OCAFileParse(e)]))?;
112    let oca_build = oca_bundle::build::from_ast(None, &ast)
113        .map_err(|e| {
114            e.iter()
115                .map(|e| ValidationError::OCABundleBuild(e.clone()))
116                .collect::<Vec<_>>()
117        })
118        .map_err(Error::ValidationError)?;
119
120    let bundle = OCABundle::from(oca_build.oca_bundle.clone());
121    Ok(bundle)
122}
123
124pub fn parse_oca_bundle_to_ocafile(bundle: &OCABundleModel) -> String {
125    ocafile::generate_from_ast(&bundle.to_ast())
126}
127
128impl Facade {
129    // TODO this name is misleading, it does not only validate ocafile, it builds
130    #[cfg(not(feature = "local-references"))]
131    pub fn validate_ocafile(
132        &self,
133        ocafile: String,
134        registry: OverlayLocalRegistry,
135    ) -> Result<OCABuild, Vec<ValidationError>> {
136        let (base, oca_ast) = Self::parse_and_check_base(self.storage(), ocafile, registry, None)?;
137        oca_bundle::build::from_ast(base, &oca_ast).map_err(|e| {
138            e.iter()
139                .map(|e| ValidationError::OCABundleBuild(e.clone()))
140                .collect::<Vec<_>>()
141        })
142    }
143
144    /// Validate ocafile using internal references for dereferencing `refn`.
145    /// Mapping between `refn` -> `said` is saved in facades database, so it can
146    /// be dereferenced in other ocafiles later.
147    // TODO this name is misleading, it does not only validate ocafile, it builds
148    #[cfg(feature = "local-references")]
149    pub fn validate_ocafile(
150        &mut self,
151        ocafile: String,
152        registry: OverlayLocalRegistry,
153    ) -> Result<OCABuild, Vec<ValidationError>> {
154        let storage: &dyn DataStorage = &*self.db_cache;
155        let (base, oca_ast) = {
156            let refs = DataStorageReferences(self.db.as_mut());
157            Self::parse_and_check_base(storage, ocafile, registry, Some(&refs))?
158        };
159        Self::oca_ast_to_oca_build_with_references(base, oca_ast, &mut self.db)
160    }
161
162    /// Validate ocafile using external references for dereferencing `refn`.  It
163    /// won't update facade internal database with `refn`-> `said` mapping, so `refn`
164    /// can't be dereferenced in ocafiles processed later.
165    #[cfg(feature = "local-references")]
166    pub fn validate_ocafile_with_external_references<R: References>(
167        &self,
168        ocafile: String,
169        references: &mut R,
170        registry: OverlayLocalRegistry,
171    ) -> Result<OCABuild, Vec<ValidationError>> {
172        let refs: &dyn References = references;
173        let (base, oca_ast) =
174            Self::parse_and_check_base(self.storage(), ocafile, registry, Some(refs))?;
175        Self::oca_ast_to_oca_build_with_references(base, oca_ast, references)
176    }
177
178    pub fn build(&mut self, oca_build: &mut OCABuild) -> Result<OCABundleModel, Error> {
179        self.build_cache(&oca_build.oca_bundle);
180        self.build_meta(&oca_build.oca_bundle);
181
182        oca_build
183            .steps
184            .iter()
185            .for_each(|step| self.build_step(step));
186
187        let _ = self.add_relations(&oca_build.oca_bundle);
188
189        self.build_models(oca_build);
190
191        Ok(oca_build.oca_bundle.clone())
192    }
193
194    /// Build an OCABundle from OCAFILE
195    pub fn build_from_ocafile(
196        &mut self,
197        ocafile: String,
198        registry: OverlayLocalRegistry,
199    ) -> Result<OCABundleModel, Error> {
200        let mut oca_build = self
201            .validate_ocafile(ocafile, registry)
202            .map_err(Error::ValidationError)?;
203
204        self.build(&mut oca_build)
205    }
206
207    fn parse_and_check_base(
208        storage: &dyn DataStorage,
209        ocafile: String,
210        registry: OverlayLocalRegistry,
211        #[cfg(feature = "local-references")] references: Option<&dyn References>,
212        #[cfg(not(feature = "local-references"))] _references: Option<()>,
213    ) -> Result<(Option<OCABundleModel>, OCAAst), Vec<ValidationError>> {
214        // Parse the OCAFILE into an AST, collecting any parse errors.
215        let mut oca_ast = ocafile::parse_from_string(ocafile, &registry).map_err(|e| {
216            vec![ValidationError::OCAFileParse(
217                oca_file::ocafile::error::ParseError::Custom(e.to_string()),
218            )]
219        })?;
220
221        // Attempt to determine if the first command is a FROM command that references
222        // an existing OCABundle. If so, load that bundle into `base`.
223        let mut base: Option<OCABundleModel> = None;
224
225        if let Some(first_command) = oca_ast.commands.first()
226            && let (oca_ast::ast::CommandType::From, ObjectKind::OCABundle(content)) = (
227                first_command.clone().kind,
228                first_command.clone().object_kind,
229            )
230        {
231            let default_command_meta = oca_ast::ast::CommandMeta {
232                line_number: 0,
233                raw_line: "unknown".to_string(),
234            };
235            let command_meta = oca_ast
236                .commands_meta
237                .get(&0)
238                .unwrap_or(&default_command_meta);
239            let invalid_command = |message: String| {
240                Vec::from([ValidationError::InvalidCommand {
241                    line_number: command_meta.line_number,
242                    raw_line: command_meta.raw_line.clone(),
243                    message,
244                }])
245            };
246
247            match content.said {
248                ReferenceAttrType::Reference(refs) => match refs {
249                    RefValue::Said(said) => match get_oca_bundle_model(storage, said) {
250                        Ok(bundle_model) => {
251                            info!("Base OCABundle found: {:?}", bundle_model.digest);
252                            base = Some(bundle_model.clone());
253                        }
254                        Err(e) => {
255                            return Err(invalid_command(format!("Failed to load bundle: {:?}", e)));
256                        }
257                    },
258                    RefValue::Name(name) => {
259                        #[allow(unused_variables)]
260
261                        #[cfg(feature = "local-references")]
262                        {
263                            match references.and_then(|refs| refs.find(&name)) {
264                                Some(said) => match SelfAddressingIdentifier::from_str(&said) {
265                                    Ok(said_identifier) => {
266                                        match get_oca_bundle_model(storage, said_identifier) {
267                                            Ok(bundle_model) => {
268                                                info!(
269                                                    "Base OCABundle found from refn: {:?}",
270                                                    bundle_model.digest
271                                                );
272                                                base = Some(bundle_model.clone());
273                                            }
274                                            Err(e) => {
275                                                return Err(invalid_command(format!(
276                                                    "Failed to load bundle: {:?}",
277                                                    e
278                                                )));
279                                            }
280                                        }
281                                    }
282                                    Err(e) => {
283                                        return Err(invalid_command(format!(
284                                            "Invalid SAID format: {}",
285                                            e
286                                        )));
287                                    }
288                                },
289                                None => {
290                                    return Err(invalid_command(format!(
291                                        "Reference {} not found",
292                                        name
293                                    )));
294                                }
295                            }
296                        }
297                        #[cfg(not(feature = "local-references"))]
298                        {
299                            return Err(vec![ValidationError::LocalReferencesNotEnabled()]);
300                        }
301                    }
302                },
303            }
304            // Remove the FROM command from the AST as it has been processed.
305            let _ = oca_ast.commands.remove(0);
306        }
307
308        Ok((base, oca_ast))
309    }
310
311    #[cfg(feature = "local-references")]
312    fn oca_ast_to_oca_build_with_references<R: References>(
313        base: Option<OCABundleModel>,
314        mut oca_ast: OCAAst,
315        references: &mut R,
316    ) -> Result<OCABuild, Vec<ValidationError>> {
317        // Dereference (refn -> refs) the AST before it start processing bundle steps, otherwise the SAID would
318        // not match.
319        local_references::replace_refn_with_refs(&mut oca_ast, references).map_err(|e| vec![e])?;
320
321        let oca_build = oca_bundle::build::from_ast(base, &oca_ast).map_err(|e| {
322            e.iter()
323                .map(|e| ValidationError::OCABundleBuild(e.clone()))
324                .collect::<Vec<_>>()
325        })?;
326
327        let schema_name = oca_ast.meta.get("name");
328        debug!("Schema name found: {:?}", schema_name);
329
330        if let Some(schema_name) = schema_name {
331            debug!("Said of new bundle: {:?}", oca_build.oca_bundle.digest);
332            let said = oca_build.oca_bundle.digest.clone().unwrap().to_string();
333            references.save(schema_name, said.clone());
334        };
335        Ok(oca_build)
336    }
337
338    fn build_cache(&self, oca_bundle: &OCABundleModel) {
339        let oca_bundle_cache_repo = OCABundleCacheRepo::new(self.connection());
340        let oca_bundle_cache_record = OCABundleCacheRecord::new(oca_bundle);
341        oca_bundle_cache_repo.insert(oca_bundle_cache_record);
342
343        let capture_base_cache_repo = CaptureBaseCacheRepo::new(self.connection());
344        let capture_base_cache_record = CaptureBaseCacheRecord::new(&oca_bundle.capture_base);
345        capture_base_cache_repo.insert(capture_base_cache_record);
346    }
347
348    fn build_step(&mut self, step: &OCABuildStep) {
349        let mut input: Vec<u8> = vec![];
350        match &step.parent_said {
351            Some(said) => {
352                input.push(said.to_string().len().try_into().unwrap());
353                input.extend(said.to_string().as_bytes());
354            }
355            None => {
356                input.push(0);
357            }
358        }
359
360        let command_str = serde_json::to_string(&step.command).unwrap();
361        input.extend(command_str.as_bytes());
362        let _ = self.db.insert(
363            Namespace::OCA,
364            &format!("oca.{}.operation", step.result.digest.clone().unwrap()),
365            &input,
366        );
367
368        let _ = self.db_cache.insert(
369            Namespace::OCABundlesJSON,
370            &step.result.digest.clone().unwrap().to_string(),
371            &serde_json::to_string(&step.result).unwrap().into_bytes(),
372        );
373        self.db_cache
374            .insert(
375                Namespace::OCAObjectsJSON,
376                &step.result.capture_base.digest.clone().unwrap().to_string(),
377                &serde_json::to_string(&step.result.capture_base)
378                    .unwrap()
379                    .into_bytes(),
380            )
381            .unwrap();
382        step.result.overlays.iter().for_each(|overlay| {
383            self.db_cache
384                .insert(
385                    Namespace::OCAObjectsJSON,
386                    &overlay.digest.clone().unwrap().to_string(),
387                    &serde_json::to_string(&overlay).unwrap().into_bytes(),
388                )
389                .unwrap();
390        });
391    }
392
393    fn build_models(&mut self, oca_build: &OCABuild) {
394        let result_models = build_core_db_model(oca_build);
395        result_models.iter().for_each(|model| {
396            if let Some(command_model) = &model.command {
397                self.db
398                    .insert(
399                        Namespace::CoreModel,
400                        &format!("core_model.{}", command_model.digest),
401                        &command_model.json.clone().into_bytes(),
402                    )
403                    .unwrap();
404            }
405
406            if let Some(capture_base_model) = &model.capture_base {
407                let mut input: Vec<u8> = vec![];
408                match &capture_base_model.parent {
409                    Some(said) => {
410                        input.push(said.to_string().len().try_into().unwrap());
411                        input.extend(said.to_string().as_bytes());
412                    }
413                    None => {
414                        input.push(0);
415                    }
416                }
417
418                input.push(
419                    capture_base_model
420                        .command_digest
421                        .to_string()
422                        .len()
423                        .try_into()
424                        .unwrap(),
425                );
426                input.extend(capture_base_model.command_digest.to_string().as_bytes());
427
428                self.db
429                    .insert(
430                        Namespace::CoreModel,
431                        &format!("core_model.{}", capture_base_model.capture_base_said),
432                        &input,
433                    )
434                    .unwrap();
435            }
436
437            if let Some(overlay_model) = &model.overlay {
438                let mut input: Vec<u8> = vec![];
439                match &overlay_model.parent {
440                    Some(said) => {
441                        input.push(said.to_string().len().try_into().unwrap());
442                        input.extend(said.to_string().as_bytes());
443                    }
444                    None => {
445                        input.push(0);
446                    }
447                }
448
449                input.push(
450                    overlay_model
451                        .command_digest
452                        .to_string()
453                        .len()
454                        .try_into()
455                        .unwrap(),
456                );
457                input.extend(overlay_model.command_digest.to_string().as_bytes());
458
459                self.db
460                    .insert(
461                        Namespace::CoreModel,
462                        &format!("core_model.{}", overlay_model.overlay_said),
463                        &input,
464                    )
465                    .unwrap();
466            }
467
468            if let Some(oca_bundle_model) = &model.oca_bundle {
469                let mut input: Vec<u8> = vec![];
470                match &oca_bundle_model.parent {
471                    Some(said) => {
472                        input.push(said.to_string().len().try_into().unwrap());
473                        input.extend(said.to_string().as_bytes());
474                    }
475                    None => {
476                        input.push(0);
477                    }
478                }
479
480                input.push(
481                    oca_bundle_model
482                        .capture_base_said
483                        .to_string()
484                        .len()
485                        .try_into()
486                        .unwrap(),
487                );
488                input.extend(oca_bundle_model.capture_base_said.to_string().as_bytes());
489
490                for said in &oca_bundle_model.overlays_said {
491                    input.push(said.to_string().len().try_into().unwrap());
492                    input.extend(said.to_string().as_bytes());
493                }
494
495                self.db
496                    .insert(
497                        Namespace::CoreModel,
498                        &format!("core_model.{}", oca_bundle_model.oca_bundle_said),
499                        &input,
500                    )
501                    .unwrap();
502            }
503        });
504    }
505}
506
507#[cfg(all(test, feature = "local-references"))]
508mod tests {
509    use super::*;
510    use crate::data_storage::{DataStorage, Namespace, SledDataStorage, SledDataStorageConfig};
511    use crate::repositories::SQLiteConfig;
512    use overlay_file::overlay_registry::OverlayLocalRegistry;
513    use std::fs;
514    use std::str::FromStr;
515    use std::time::{SystemTime, UNIX_EPOCH};
516
517    fn temp_storage_dir() -> std::path::PathBuf {
518        let mut path = std::env::temp_dir();
519        let nanos = SystemTime::now()
520            .duration_since(UNIX_EPOCH)
521            .unwrap()
522            .as_nanos();
523        path.push(format!("oca_store_refn_test_{}", nanos));
524        path
525    }
526
527    #[test]
528    fn stores_and_resolves_refn() {
529        let _ = env_logger::builder().is_test(true).try_init();
530        let storage_dir = temp_storage_dir();
531        fs::create_dir_all(&storage_dir).unwrap();
532
533        let storage_config = SledDataStorageConfig::build()
534            .path(storage_dir.clone())
535            .unwrap();
536        let db = SledDataStorage::new().config(storage_config);
537        let db_cache = db.clone();
538        let cache_storage_config = SQLiteConfig::build().unwrap();
539        let mut facade = Facade::new(Box::new(db), Box::new(db_cache), cache_storage_config);
540
541        let registry = OverlayLocalRegistry::from_dir("tests/core_overlays").unwrap();
542        let base_ocafile = r#"
543-- name=base
544ADD ATTRIBUTE name=Text
545"#
546        .to_string();
547        let _ = facade
548            .build_from_ocafile(base_ocafile, registry.clone())
549            .unwrap();
550
551        let stored = facade
552            .storage()
553            .get(Namespace::OCAReferences, "base")
554            .unwrap();
555        assert!(stored.is_some(), "refn should be stored in storage");
556
557        let refn_ocafile = r#"
558ADD ATTRIBUTE linked=refn:base
559"#
560        .to_string();
561        let bundle = facade.build_from_ocafile(refn_ocafile, registry).unwrap();
562
563        let stored = stored
564            .and_then(|bytes| String::from_utf8(bytes).ok())
565            .expect("refn should be stored as utf-8");
566        let stored_said = said::SelfAddressingIdentifier::from_str(&stored).unwrap();
567        match bundle.capture_base.attributes.get("linked") {
568            Some(oca_ast::ast::NestedAttrType::Reference(oca_ast::ast::RefValue::Said(said))) => {
569                assert_eq!(said, &stored_said);
570            }
571            other => panic!("expected linked to resolve to said, got {:?}", other),
572        }
573
574        let _ = fs::remove_dir_all(storage_dir);
575    }
576}