rototo 0.1.0-alpha.6

Control plane for runtime configuration of your application.
Documentation
use std::collections::BTreeSet;

use serde_json::Value as JsonValue;

use crate::lint::RuntimePackage;

pub(super) fn catalog_entry_view(
    runtime: &RuntimePackage,
    catalog: &str,
    name: &str,
    entry: &JsonValue,
) -> JsonValue {
    let mut stack = BTreeSet::new();
    hydrate_catalog_entry(runtime, catalog, name, entry, &mut stack)
}

fn hydrate_catalog_entry(
    runtime: &RuntimePackage,
    catalog: &str,
    name: &str,
    entry: &JsonValue,
    stack: &mut BTreeSet<(String, String)>,
) -> JsonValue {
    let mut value = entry.clone();
    if let JsonValue::Object(object) = &mut value {
        object.insert("id".to_owned(), JsonValue::String(name.to_owned()));
    }
    if !stack.insert((catalog.to_owned(), name.to_owned())) {
        return value;
    }
    let hydrated = runtime
        .catalog_schemas
        .get(catalog)
        .map(|schema| hydrate_schema_value(runtime, catalog, schema, &value, stack))
        .unwrap_or(value);
    stack.remove(&(catalog.to_owned(), name.to_owned()));
    hydrated
}

fn hydrate_schema_value(
    runtime: &RuntimePackage,
    schema_catalog: &str,
    schema: &JsonValue,
    value: &JsonValue,
    stack: &mut BTreeSet<(String, String)>,
) -> JsonValue {
    if let Some(reference) = schema.get("$ref").and_then(JsonValue::as_str)
        && let Some((catalog, schema)) = resolve_schema_ref(runtime, schema_catalog, reference)
    {
        return hydrate_schema_value(runtime, &catalog, schema, value, stack);
    }

    if let Some(ref_value) = schema.get("x-rototo-catalog-ref")
        && let Some(hydrated) = hydrate_catalog_reference(runtime, ref_value, value, stack)
    {
        return hydrated;
    }

    let mut hydrated = value.clone();

    if let (Some(properties), JsonValue::Object(object)) = (
        schema.get("properties").and_then(JsonValue::as_object),
        &mut hydrated,
    ) {
        for (key, subschema) in properties {
            let Some(child) = object.get(key).cloned() else {
                continue;
            };
            object.insert(
                key.clone(),
                hydrate_schema_value(runtime, schema_catalog, subschema, &child, stack),
            );
        }
    }

    if let (Some(items), JsonValue::Array(values)) = (schema.get("items"), &mut hydrated) {
        for value in values {
            let child = value.clone();
            *value = hydrate_schema_value(runtime, schema_catalog, items, &child, stack);
        }
    }

    for keyword in ["allOf", "anyOf", "oneOf"] {
        let Some(schemas) = schema.get(keyword).and_then(JsonValue::as_array) else {
            continue;
        };
        for subschema in schemas {
            hydrated = hydrate_schema_value(runtime, schema_catalog, subschema, &hydrated, stack);
        }
    }

    hydrated
}

fn hydrate_catalog_reference(
    runtime: &RuntimePackage,
    ref_spec: &JsonValue,
    value: &JsonValue,
    stack: &mut BTreeSet<(String, String)>,
) -> Option<JsonValue> {
    if ref_spec == &JsonValue::Bool(true) {
        let object = value.as_object()?;
        let catalog = object.get("catalog")?.as_str()?;
        let entry = object.get("entry")?.as_str()?;
        let pointer = object.get("pointer").and_then(JsonValue::as_str);
        return hydrate_catalog_reference_target(runtime, catalog, entry, pointer, value, stack);
    }

    let target_catalogs = if let Some(catalog) = ref_spec.as_str() {
        vec![catalog]
    } else if let Some(catalogs) = ref_spec.as_array() {
        catalogs.iter().filter_map(JsonValue::as_str).collect()
    } else {
        return None;
    };

    let target = value.as_str()?;
    let (entry, pointer) = split_catalog_entry_ref(target)?;
    for catalog in target_catalogs {
        if runtime
            .catalog_entries
            .get(catalog)
            .is_some_and(|entries| entries.contains_key(entry))
        {
            return hydrate_catalog_reference_target(
                runtime, catalog, entry, pointer, value, stack,
            );
        }
    }
    None
}

fn hydrate_catalog_reference_target(
    runtime: &RuntimePackage,
    catalog: &str,
    entry: &str,
    pointer: Option<&str>,
    fallback: &JsonValue,
    stack: &mut BTreeSet<(String, String)>,
) -> Option<JsonValue> {
    if stack.contains(&(catalog.to_owned(), entry.to_owned())) {
        return Some(fallback.clone());
    }
    let entry_value = runtime.catalog_entries.get(catalog)?.get(entry)?;
    let hydrated = hydrate_catalog_entry(runtime, catalog, entry, entry_value, stack);
    match pointer {
        Some(pointer) => hydrated
            .pointer(pointer)
            .cloned()
            .or_else(|| Some(fallback.clone())),
        None => Some(hydrated),
    }
}

fn split_catalog_entry_ref(value: &str) -> Option<(&str, Option<&str>)> {
    let Some((entry, pointer)) = value.split_once('#') else {
        return (!value.is_empty()).then_some((value, None));
    };
    if entry.is_empty() || !is_json_pointer(pointer) {
        return None;
    }
    Some((entry, (!pointer.is_empty()).then_some(pointer)))
}

fn is_json_pointer(value: &str) -> bool {
    value.is_empty() || value.starts_with('/')
}

fn resolve_schema_ref<'a>(
    runtime: &'a RuntimePackage,
    current_catalog: &str,
    reference: &str,
) -> Option<(String, &'a JsonValue)> {
    let (uri, pointer) = reference
        .split_once('#')
        .map_or((reference, ""), |(uri, pointer)| (uri, pointer));
    let catalog = if uri.is_empty() {
        current_catalog.to_owned()
    } else if let Some(catalog) = uri
        .strip_prefix("rototo://catalogs/")
        .and_then(|path| path.strip_suffix(".schema.json"))
    {
        catalog.to_owned()
    } else {
        runtime
            .catalog_schemas
            .iter()
            .find_map(|(catalog, schema)| {
                (schema.get("$id").and_then(JsonValue::as_str) == Some(uri))
                    .then(|| catalog.clone())
            })?
    };
    let schema = runtime.catalog_schemas.get(&catalog)?;
    if pointer.is_empty() {
        return Some((catalog, schema));
    }
    let pointer = if pointer.starts_with('/') {
        pointer
    } else {
        return None;
    };
    schema.pointer(pointer).map(|schema| (catalog, schema))
}