coil-wasm 0.1.1

WASM extension runtime and host APIs for the Coil framework.
Documentation
use std::collections::BTreeMap;
use std::fmt;

use crate::error::WasmModelError;
use crate::validation::{require_non_empty, validate_token};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RobotsDirective {
    Index,
    NoIndex,
    Follow,
    NoFollow,
    NoArchive,
}

impl fmt::Display for RobotsDirective {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Index => f.write_str("index"),
            Self::NoIndex => f.write_str("noindex"),
            Self::Follow => f.write_str("follow"),
            Self::NoFollow => f.write_str("nofollow"),
            Self::NoArchive => f.write_str("noarchive"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonLdValue {
    String(String),
    Number(String),
    Bool(bool),
    Node(JsonLdNode),
    List(Vec<JsonLdValue>),
}

impl JsonLdValue {
    fn render(&self) -> String {
        match self {
            Self::String(value) => format!("\"{}\"", escape_json(value)),
            Self::Number(value) => value.clone(),
            Self::Bool(value) => value.to_string(),
            Self::Node(node) => node.render(),
            Self::List(values) => format!(
                "[{}]",
                values
                    .iter()
                    .map(JsonLdValue::render)
                    .collect::<Vec<_>>()
                    .join(",")
            ),
        }
    }

    pub(crate) fn validate(&self) -> Result<(), WasmModelError> {
        match self {
            Self::String(value) => {
                let _ = require_non_empty("json_ld_string", value.clone())?;
            }
            Self::Number(value) => {
                if value
                    .parse::<f64>()
                    .ok()
                    .filter(|value| value.is_finite())
                    .is_none()
                {
                    return Err(WasmModelError::InvalidJsonLdNumber {
                        property: "json_ld_number".to_string(),
                        value: value.clone(),
                    });
                }
            }
            Self::Bool(_) => {}
            Self::Node(node) => node.validate()?,
            Self::List(values) => {
                for value in values {
                    value.validate()?;
                }
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonLdNode {
    schema_type: String,
    properties: BTreeMap<String, JsonLdValue>,
}

impl JsonLdNode {
    pub fn new(schema_type: impl Into<String>) -> Result<Self, WasmModelError> {
        Ok(Self {
            schema_type: validate_token("schema_type", schema_type.into())?,
            properties: BTreeMap::new(),
        })
    }

    pub fn set_string(
        mut self,
        property: impl Into<String>,
        value: impl Into<String>,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        self.properties.insert(
            property,
            JsonLdValue::String(require_non_empty("json_ld_string", value.into())?),
        );
        Ok(self)
    }

    pub fn set_number(
        mut self,
        property: impl Into<String>,
        value: f64,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        if !value.is_finite() {
            return Err(WasmModelError::InvalidJsonLdNumber {
                property,
                value: value.to_string(),
            });
        }
        self.properties
            .insert(property, JsonLdValue::Number(value.to_string()));
        Ok(self)
    }

    pub fn set_bool(
        mut self,
        property: impl Into<String>,
        value: bool,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        self.properties.insert(property, JsonLdValue::Bool(value));
        Ok(self)
    }

    pub fn set_node(
        mut self,
        property: impl Into<String>,
        node: JsonLdNode,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        self.properties.insert(property, JsonLdValue::Node(node));
        Ok(self)
    }

    pub fn set_list(
        mut self,
        property: impl Into<String>,
        values: Vec<JsonLdValue>,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        self.properties.insert(property, JsonLdValue::List(values));
        Ok(self)
    }

    pub fn render(&self) -> String {
        let mut segments = vec![format!("\"@type\":\"{}\"", escape_json(&self.schema_type))];
        for (property, value) in &self.properties {
            segments.push(format!("\"{}\":{}", escape_json(property), value.render()));
        }
        format!("{{{}}}", segments.join(","))
    }

    pub(crate) fn schema_type(&self) -> &str {
        &self.schema_type
    }

    pub(crate) fn properties(&self) -> &BTreeMap<String, JsonLdValue> {
        &self.properties
    }

    pub(crate) fn insert_value(
        mut self,
        property: impl Into<String>,
        value: JsonLdValue,
    ) -> Result<Self, WasmModelError> {
        let property = validate_property_name(property.into())?;
        if self.properties.contains_key(&property) {
            return Err(WasmModelError::DuplicateJsonLdProperty { property });
        }
        self.properties.insert(property, value);
        Ok(self)
    }

    pub(crate) fn validate(&self) -> Result<(), WasmModelError> {
        let _ = validate_token("schema_type", self.schema_type.clone())?;
        for (property, value) in &self.properties {
            let _ = validate_property_name(property.clone())?;
            value.validate()?;
        }
        Ok(())
    }
}

fn validate_property_name(value: String) -> Result<String, WasmModelError> {
    let trimmed = value.trim();
    if trimmed.is_empty()
        || !trimmed
            .chars()
            .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '@' | '_' | '-'))
    {
        Err(WasmModelError::InvalidJsonLdProperty {
            property: trimmed.to_string(),
        })
    } else {
        Ok(trimmed.to_string())
    }
}

fn escape_json(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}