hayson-engine 0.26.0

Storage engine for hayson document databases — the embeddable core behind hayson-server, hayson-cli and hayson-browser
Documentation
use serde_json::{Map, Value};

use crate::schema::DefaultValue;

use super::types::{
    Catalog, CatalogCompaction, CatalogFieldSpec, CheckEntry, HistoryEntry, IndexEntry, Relation, TableEntry,
    TableOptions, TriggerEffect, TriggerEntry, TriggerFieldMapping, TriggerValueSource, ViewEntry,
};

fn field_document(spec: &CatalogFieldSpec) -> Value {
    let mut doc = Map::new();
    doc.insert("type".to_string(), Value::from(spec.spec.field_type.as_str()));
    doc.insert("required".to_string(), Value::from(spec.spec.required));
    doc.insert("unique".to_string(), Value::from(spec.spec.unique));
    if let Some(index) = spec.spec.index {
        doc.insert(
            "index".to_string(),
            Value::from(match index {
                crate::schema::IndexKind::Exact => "exact",
                crate::schema::IndexKind::Sorted => "sorted",
            }),
        );
    }
    doc.insert("fulltext".to_string(), Value::from(spec.spec.fulltext));
    doc.insert("cypher".to_string(), Value::from(spec.spec.cypher));
    // NOTE: only when set -- rendering the default would rewrite every existing catalog.
    if spec.spec.password_hash {
        doc.insert("passwordHash".to_string(), Value::Bool(true));
    }
    // NOTE: only when opted out -- rendering the default would rewrite every existing catalog.
    if !spec.spec.addressable {
        doc.insert("addressable".to_string(), Value::Bool(false));
    }
    if let Some(values) = &spec.spec.values {
        doc.insert("values".to_string(), Value::from(values.clone()));
    }
    if let Some(default) = &spec.spec.default {
        let value = match default {
            DefaultValue::Now => Value::from("now"),
            DefaultValue::Value(v) => v.clone(),
        };
        doc.insert("default".to_string(), value);
    }
    if let Some(description) = &spec.description {
        doc.insert("description".to_string(), Value::from(description.clone()));
    }
    if let Some(deprecated) = &spec.deprecated {
        doc.insert("deprecated".to_string(), Value::from(deprecated.clone()));
    }
    Value::Object(doc)
}

fn index_document(index: &IndexEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("name".to_string(), Value::from(index.name.clone()));
    doc.insert("fields".to_string(), Value::from(index.fields.clone()));
    doc.insert("kind".to_string(), Value::from(index.kind.as_str()));
    if index.unique {
        doc.insert("unique".to_string(), Value::from(true));
    }
    if let Some(where_) = &index.where_ {
        let clauses: Vec<Value> = where_
            .iter()
            .map(|clause| {
                let mut c = Map::new();
                c.insert("field".to_string(), Value::from(clause.field.clone()));
                c.insert("op".to_string(), Value::from(clause.op.clone()));
                c.insert("value".to_string(), clause.value.clone());
                Value::Object(c)
            })
            .collect();
        doc.insert("where".to_string(), Value::Array(clauses));
    }
    if let Some(expire) = index.expire_after_seconds {
        doc.insert("expireAfterSeconds".to_string(), Value::from(expire));
    }
    Value::Object(doc)
}

fn check_document(check: &CheckEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("name".to_string(), Value::from(check.name.clone()));
    doc.insert("expr".to_string(), Value::from(check.expr.clone()));
    Value::Object(doc)
}

fn history_document(entry: &HistoryEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("revision".to_string(), Value::from(entry.revision));
    doc.insert("at".to_string(), Value::from(entry.at));
    if let Some(note) = &entry.note {
        doc.insert("note".to_string(), Value::from(note.clone()));
    }
    Value::Object(doc)
}

fn options_document(options: &TableOptions) -> Value {
    let mut doc = Map::new();
    if let Some(sync) = options.sync {
        doc.insert("sync".to_string(), Value::from(sync.as_str()));
    }
    if let Some(compaction) = options.compaction {
        let value = match compaction {
            CatalogCompaction::Disabled => Value::Bool(false),
            CatalogCompaction::Enabled {
                dead_bytes_ratio,
                min_dead_bytes,
            } => {
                let mut c = Map::new();
                c.insert("deadBytesRatio".to_string(), Value::from(dead_bytes_ratio));
                c.insert("minDeadBytes".to_string(), Value::from(min_dead_bytes));
                Value::Object(c)
            }
        };
        doc.insert("compaction".to_string(), value);
    }
    if let Some(link_parsing) = options.link_parsing {
        doc.insert("linkParsing".to_string(), Value::from(link_parsing.as_str()));
    }
    if let Some(image_dir) = &options.image_dir {
        doc.insert("imageDir".to_string(), Value::from(image_dir.clone()));
    }
    Value::Object(doc)
}

fn table_document(entry: &TableEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("revision".to_string(), Value::from(entry.revision));
    if let Some(description) = &entry.description {
        doc.insert("description".to_string(), Value::from(description.clone()));
    }
    doc.insert("primaryKey".to_string(), Value::from(entry.primary_key.clone()));
    if let Some(partition_by) = &entry.partition_by {
        doc.insert("partitionBy".to_string(), Value::from(partition_by.clone()));
    }
    if let Some(created_at) = entry.created_at {
        doc.insert("createdAt".to_string(), Value::from(created_at));
    }
    if let Some(updated_at) = entry.updated_at {
        doc.insert("updatedAt".to_string(), Value::from(updated_at));
    }
    if let Some(validation) = entry.validation {
        doc.insert("validation".to_string(), Value::from(validation.as_str()));
    }
    if let Some(options) = &entry.options {
        doc.insert("options".to_string(), options_document(options));
    }
    let mut fields = Map::new();
    for (name, spec) in &entry.fields {
        fields.insert(name.clone(), field_document(spec));
    }
    doc.insert("fields".to_string(), Value::Object(fields));
    if let Some(indexes) = &entry.indexes {
        let mut sorted = indexes.clone();
        sorted.sort_by(|a, b| a.name.cmp(&b.name));
        doc.insert(
            "indexes".to_string(),
            Value::Array(sorted.iter().map(index_document).collect()),
        );
    }
    if let Some(checks) = &entry.checks {
        let mut sorted = checks.clone();
        sorted.sort_by(|a, b| a.name.cmp(&b.name));
        doc.insert(
            "checks".to_string(),
            Value::Array(sorted.iter().map(check_document).collect()),
        );
    }
    if let Some(history) = &entry.history {
        doc.insert(
            "history".to_string(),
            Value::Array(history.iter().map(history_document).collect()),
        );
    }
    Value::Object(doc)
}

fn relation_document(relation: &Relation) -> Value {
    let mut doc = Map::new();
    doc.insert("name".to_string(), Value::from(relation.name.clone()));
    let mut from = Map::new();
    from.insert("table".to_string(), Value::from(relation.from.table.clone()));
    from.insert("field".to_string(), Value::from(relation.from.field.clone()));
    doc.insert("from".to_string(), Value::Object(from));
    let mut to = Map::new();
    to.insert("table".to_string(), Value::from(relation.to.table.clone()));
    to.insert("field".to_string(), Value::from(relation.to.field.clone()));
    doc.insert("to".to_string(), Value::Object(to));
    doc.insert("cardinality".to_string(), Value::from(relation.cardinality.as_str()));
    if let Some(on_delete) = relation.on_delete {
        doc.insert("onDelete".to_string(), Value::from(on_delete.as_str()));
    }
    if let Some(enforce) = relation.enforce {
        doc.insert("enforce".to_string(), Value::from(enforce));
    }
    Value::Object(doc)
}

fn view_document(view: &ViewEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("name".to_string(), Value::from(view.name.clone()));
    doc.insert("query".to_string(), Value::from(view.query.clone()));
    if let Some(description) = &view.description {
        doc.insert("description".to_string(), Value::from(description.clone()));
    }
    Value::Object(doc)
}

fn trigger_value_source_document(source: &TriggerValueSource) -> Value {
    let mut doc = Map::new();
    match source {
        TriggerValueSource::NewField(field) => {
            doc.insert("kind".to_string(), Value::from("new"));
            doc.insert("field".to_string(), Value::from(field.clone()));
        }
        TriggerValueSource::OldField(field) => {
            doc.insert("kind".to_string(), Value::from("old"));
            doc.insert("field".to_string(), Value::from(field.clone()));
        }
        TriggerValueSource::Constant(value) => {
            doc.insert("kind".to_string(), Value::from("constant"));
            doc.insert("value".to_string(), value.clone());
        }
        TriggerValueSource::Now => {
            doc.insert("kind".to_string(), Value::from("now"));
        }
    }
    Value::Object(doc)
}

fn trigger_field_mapping_document(mapping: &TriggerFieldMapping) -> Value {
    let mut doc = Map::new();
    doc.insert("target".to_string(), Value::from(mapping.target.clone()));
    doc.insert("source".to_string(), trigger_value_source_document(&mapping.source));
    Value::Object(doc)
}

fn trigger_effect_document(effect: &TriggerEffect) -> Value {
    let mut doc = Map::new();
    doc.insert("table".to_string(), Value::from(effect.table.clone()));
    doc.insert(
        "fields".to_string(),
        Value::Array(effect.fields.iter().map(trigger_field_mapping_document).collect()),
    );
    Value::Object(doc)
}

fn trigger_document(trigger: &TriggerEntry) -> Value {
    let mut doc = Map::new();
    doc.insert("name".to_string(), Value::from(trigger.name.clone()));
    doc.insert("table".to_string(), Value::from(trigger.table.clone()));
    doc.insert("event".to_string(), Value::from(trigger.event.as_str()));
    doc.insert("effect".to_string(), trigger_effect_document(&trigger.effect));
    Value::Object(doc)
}

/// Builds the ordered document `serialize_catalog` renders to bytes — sections and known fields in
/// the fixed order of the format (`docs/format.md`, „hayson.json w formacie 2"), `tables` and
/// `fields` sorted alphabetically by key, `indexes`/`checks`/`relations`/`views` sorted by `name`,
/// and `history` left in the order it arrived.
pub fn catalog_document(catalog: &Catalog) -> Value {
    let mut doc = Map::new();
    doc.insert("catalogVersion".to_string(), Value::from(catalog.catalog_version));
    doc.insert("formatVersion".to_string(), Value::from(catalog.format_version));
    doc.insert("createdAt".to_string(), Value::from(catalog.created_at));
    doc.insert("updatedAt".to_string(), Value::from(catalog.updated_at));
    if let Some(engine) = &catalog.engine {
        doc.insert("engine".to_string(), Value::from(engine.clone()));
    }
    if let Some(layout) = catalog.layout {
        doc.insert("layout".to_string(), Value::from(layout.as_str()));
    }
    if let Some(defaults) = &catalog.defaults {
        doc.insert("defaults".to_string(), options_document(defaults));
    }
    let mut tables = Map::new();
    for (name, entry) in &catalog.tables {
        tables.insert(name.clone(), table_document(entry));
    }
    doc.insert("tables".to_string(), Value::Object(tables));
    if let Some(relations) = &catalog.relations {
        let mut sorted = relations.clone();
        sorted.sort_by(|a, b| a.name.cmp(&b.name));
        doc.insert(
            "relations".to_string(),
            Value::Array(sorted.iter().map(relation_document).collect()),
        );
    }
    if let Some(views) = &catalog.views {
        let mut sorted = views.clone();
        sorted.sort_by(|a, b| a.name.cmp(&b.name));
        doc.insert(
            "views".to_string(),
            Value::Array(sorted.iter().map(view_document).collect()),
        );
    }
    if let Some(triggers) = &catalog.triggers {
        let mut sorted = triggers.clone();
        sorted.sort_by(|a, b| a.name.cmp(&b.name));
        doc.insert(
            "triggers".to_string(),
            Value::Array(sorted.iter().map(trigger_document).collect()),
        );
    }
    Value::Object(doc)
}

pub fn serialize_catalog(catalog: &Catalog) -> String {
    format!(
        "{}\n",
        serde_json::to_string_pretty(&catalog_document(catalog)).expect("catalog document is valid JSON")
    )
}