omena-engine-input-producers 0.3.0

Rust input-derived producer artifacts for Omena CSS Modules EngineInputV2
Documentation
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use engine_input_producers::EngineInputV2;
use engine_input_producers::engine_contract_v2_idl_generated::{
    EngineInputV2Json, EngineOutputV2Json, OmenaQueryCodeActionPlanV0Json,
};
use serde::Serialize;
use serde_json::Value;

type TestResult = Result<(), Box<dyn std::error::Error>>;

#[test]
fn engine_contract_v2_idl_fixtures_round_trip_canonically() -> TestResult {
    let mut fixture_paths = fs::read_dir(contract_parity_v2_fixture_dir())?
        .map(|entry| entry.map(|entry| entry.path()))
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .filter(|path| {
            path.extension()
                .is_some_and(|extension| extension == "json")
        })
        .collect::<Vec<_>>();
    fixture_paths.sort();

    let mut query_kinds = Vec::<String>::new();
    for fixture_path in fixture_paths {
        let fixture = read_json(&fixture_path)?;
        let input_value = fixture
            .get("input")
            .ok_or_else(|| missing_fixture_field(&fixture_path, "input"))?
            .clone();
        let output_value = fixture
            .get("output")
            .ok_or_else(|| missing_fixture_field(&fixture_path, "output"))?
            .clone();

        let input: EngineInputV2Json = serde_json::from_value(input_value.clone())?;
        let output: EngineOutputV2Json = serde_json::from_value(output_value.clone())?;

        assert_eq!(
            canonical_json(&input_value)?,
            canonical_json(&serde_json::to_value(input)?)?,
            "{} input canonical round-trip drifted",
            fixture_path.display()
        );
        assert_eq!(
            canonical_json(&output_value)?,
            canonical_json(&serde_json::to_value(&output)?)?,
            "{} output canonical round-trip drifted",
            fixture_path.display()
        );

        for result in output.query_results {
            query_kinds.push(result_kind(&serde_json::to_value(result)?)?.to_string());
        }
    }

    query_kinds.sort();
    query_kinds.dedup();
    assert_eq!(
        query_kinds,
        vec![
            "expression-semantics".to_string(),
            "selector-usage".to_string(),
            "source-expression-resolution".to_string(),
        ]
    );
    Ok(())
}

#[test]
fn engine_contract_v2_idl_code_action_plan_round_trips_canonically() -> TestResult {
    let value = serde_json::json!({
        "schemaVersion": "0",
        "product": "omena-query.code-actions",
        "fileUri": "file:///repo/src/App.module.scss",
        "fileKind": "style",
        "actionCount": 1,
        "actions": [
            {
                "title": "Extract CSS custom property",
                "kind": "refactor.extract",
                "edits": [
                    {
                        "uri": "file:///repo/src/App.module.scss",
                        "range": {
                            "start": { "line": 0, "character": 0 },
                            "end": { "line": 0, "character": 5 }
                        },
                        "newText": "var(--token)"
                    }
                ],
                "source": "omenaQueryStyleExtractCodeActions"
            }
        ],
        "readySurfaces": ["productFacingCodeActions"]
    });

    let plan: OmenaQueryCodeActionPlanV0Json = serde_json::from_value(value.clone())?;

    assert_eq!(
        canonical_json(&value)?,
        canonical_json(&serde_json::to_value(plan)?)?,
    );
    Ok(())
}

#[test]
fn engine_contract_v2_idl_runtime_projection_uses_generated_wire_input() -> TestResult {
    let fixture_path = contract_parity_v2_fixture_dir().join("type-fact-parity-v2.json");
    let fixture = read_json(&fixture_path)?;
    let input_value = fixture
        .get("input")
        .ok_or_else(|| missing_fixture_field(&fixture_path, "input"))?
        .clone();

    let projected: EngineInputV2 = serde_json::from_value(input_value.clone())?;
    assert_eq!(projected.version, "2");
    assert!(!projected.sources.is_empty());
    assert!(!projected.styles.is_empty());
    assert!(!projected.type_facts.is_empty());

    let mut missing_workspace = input_value;
    missing_workspace
        .as_object_mut()
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "input must be an object"))?
        .remove("workspace");

    assert!(
        serde_json::from_value::<EngineInputV2>(missing_workspace).is_err(),
        "runtime projection must reject JSON that fails the generated EngineInputV2 wire contract",
    );
    Ok(())
}

fn contract_parity_v2_fixture_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../..")
        .join("test/_fixtures/contract-parity-v2")
}

fn read_json(path: &Path) -> Result<Value, Box<dyn std::error::Error>> {
    Ok(serde_json::from_str(&fs::read_to_string(path)?)?)
}

fn canonical_json<T: Serialize>(value: &T) -> serde_json::Result<String> {
    let value = serde_json::to_value(value)?;
    serde_json::to_string(&sort_json(value))
}

fn sort_json(value: Value) -> Value {
    match value {
        Value::Array(items) => Value::Array(items.into_iter().map(sort_json).collect()),
        Value::Object(map) => {
            let mut entries = map.into_iter().collect::<Vec<_>>();
            entries.sort_by(|left, right| left.0.cmp(&right.0));
            Value::Object(
                entries
                    .into_iter()
                    .map(|(key, value)| (key, sort_json(value)))
                    .collect(),
            )
        }
        other => other,
    }
}

fn result_kind(value: &Value) -> io::Result<&str> {
    value
        .get("kind")
        .and_then(Value::as_str)
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "query result must carry kind"))
}

fn missing_fixture_field(path: &Path, field: &str) -> io::Error {
    io::Error::new(
        io::ErrorKind::InvalidData,
        format!("{} fixture must carry {field}", path.display()),
    )
}