alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

use crate::core::config::e2e::ArgMapping;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TemplateReturnForm {
    #[default]
    Dict,
    BareString,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixtureEnv {
    #[serde(default)]
    pub api_key_var: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupCall {
    pub call: String,
    #[serde(default)]
    pub input: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FixtureDocs {
    pub topic: String,
    #[serde(default)]
    pub stem: Option<String>,
    #[serde(default)]
    pub paths: BTreeMap<String, String>,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub presentation: Option<FixtureDocsPresentation>,
    #[serde(default)]
    pub side_effects: SideEffectClass,
    #[serde(default)]
    pub coverage_exceptions: BTreeMap<String, SnippetCoverageException>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FixtureDocsPresentation {
    #[serde(default)]
    pub call: Option<String>,
    #[serde(default)]
    pub input: Option<serde_json::Value>,
    #[serde(default)]
    pub args: Option<Vec<ArgMapping>>,
    #[serde(default)]
    pub files: Vec<FixtureDocsFileInput>,
    #[serde(default)]
    pub operations: Vec<FixtureDocsOperation>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FixtureDocsFileInput {
    pub field: String,
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum FixtureDocsOperation {
    Show {
        path: String,
    },
    Iterate {
        path: String,
        item: String,
        #[serde(default)]
        fields: Vec<String>,
        #[serde(default)]
        display: bool,
        #[serde(default)]
        optional: bool,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SnippetCoverageException {
    pub reason: String,
    pub documentation: String,
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SideEffectClass {
    #[default]
    #[serde(alias = "none", alias = "local")]
    Safe,
    Network,
    Process,
    Install,
    Server,
}

#[cfg(test)]
mod tests {
    use super::{FixtureDocs, FixtureDocsOperation, SideEffectClass};

    #[test]
    fn side_effects_round_trip_without_collapsing_classes() {
        for class in [
            SideEffectClass::Safe,
            SideEffectClass::Network,
            SideEffectClass::Process,
            SideEffectClass::Install,
            SideEffectClass::Server,
        ] {
            let encoded = serde_json::to_string(&class).unwrap();
            assert_eq!(serde_json::from_str::<SideEffectClass>(&encoded).unwrap(), class);
        }
    }

    #[test]
    fn docs_paths_deserialize_as_target_specific_relative_paths() {
        let docs: FixtureDocs = serde_json::from_value(serde_json::json!({
            "topic": "fallback",
            "paths": {
                "node": "config/example.md",
                "wasm": "browser/example.md"
            }
        }))
        .expect("fixture docs deserialize");

        assert_eq!(docs.paths["node"], "config/example.md");
        assert_eq!(docs.paths["wasm"], "browser/example.md");
    }

    #[test]
    fn legacy_safe_aliases_remain_accepted_but_external_mutation_is_rejected() {
        assert_eq!(
            serde_json::from_str::<SideEffectClass>(r#""none""#).unwrap(),
            SideEffectClass::Safe
        );
        assert_eq!(
            serde_json::from_str::<SideEffectClass>(r#""local""#).unwrap(),
            SideEffectClass::Safe
        );
        assert!(serde_json::from_str::<SideEffectClass>(r#""external_mutation""#).is_err());
    }

    #[test]
    fn structured_presentation_deserializes_without_language_code() {
        let docs: FixtureDocs = serde_json::from_value(serde_json::json!({
            "topic": "configuration",
            "presentation": {
                "call": "process_file",
                "input": {"source": "guide.txt"},
                "args": [{"name": "source", "field": "source", "type": "string"}],
                "files": [{"field": "/source", "path": "examples/guide.txt"}],
                "operations": [
                    {"op": "show", "path": "summary"},
                    {"op": "iterate", "path": "items", "item": "item", "fields": ["text"], "optional": true}
                ]
            }
        }))
        .expect("fixture docs presentation deserialize");

        let presentation = docs.presentation.expect("presentation");
        assert_eq!(presentation.input, Some(serde_json::json!({"source": "guide.txt"})));
        assert_eq!(presentation.call.as_deref(), Some("process_file"));
        assert_eq!(presentation.files[0].field, "/source");
        assert!(matches!(presentation.operations[0], FixtureDocsOperation::Show { .. }));
        assert!(matches!(
            presentation.operations[1],
            FixtureDocsOperation::Iterate { optional: true, .. }
        ));
    }
}