wavepeek 2.1.0

Command-line tool for RTL waveform inspection with deterministic machine-friendly output.
Documentation
use schemars::SchemaGenerator;
use serde_json::{Map, Value, json};

use super::common::{
    CanonicalPath, ContractDiagnostic, NormalizedTime, SampledValue, ScopeKind, SignalKind,
};
use super::input::{ExtractGenericSource, ExtractGenericSourcesInput};
use super::output::{
    ChangeSignalValue, ChangeSnapshot, DocsSearchData, DocsSearchMatch, DocsTopicsData,
    ExtractGenericRow, ExtractPayloadValue, InfoData, PropertyRow, SampledSignalValue, ScopeEntry,
    SignalEntry, TopicSummary, ValueSnapshot,
};
use super::stream::{BeginRecord, DiagnosticRecord, EndRecord};

pub const OUTPUT_SCHEMA_ID: &str = "wavepeek.output";
pub const STREAM_SCHEMA_ID: &str = "wavepeek.stream-record";
pub const INPUT_SCHEMA_ID: &str = "wavepeek.input";
pub const OUTPUT_SCHEMA_VERSION_STR: &str = "2.1";
pub const STREAM_SCHEMA_VERSION_STR: &str = "2.1";
pub const INPUT_SCHEMA_VERSION_STR: &str = "2.1";
pub const OUTPUT_SCHEMA_ARTIFACT_PATH: &str = "schema/output.json";
pub const STREAM_SCHEMA_ARTIFACT_PATH: &str = "schema/stream.json";
pub const INPUT_SCHEMA_ARTIFACT_PATH: &str = "schema/input.json";
pub const SCHEMA_CATALOG_PATH: &str = "schema/catalog.json";
pub const OUTPUT_SCHEMA_ARTIFACT_NAME: &str = "schema-output-v2.1.json";
pub const STREAM_SCHEMA_ARTIFACT_NAME: &str = "schema-stream-v2.1.json";
pub const INPUT_SCHEMA_ARTIFACT_NAME: &str = "schema-input-v2.1.json";
pub const OUTPUT_SCHEMA_URL: &str = "https://kleverhq.github.io/wavepeek/schema-output-v2.1.json";
pub const STREAM_SCHEMA_URL: &str = "https://kleverhq.github.io/wavepeek/schema-stream-v2.1.json";
pub const INPUT_SCHEMA_URL: &str = "https://kleverhq.github.io/wavepeek/schema-input-v2.1.json";

const JSON_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";

pub fn output_schema_json() -> String {
    pretty_json(&output_schema_value())
}

pub fn stream_schema_json() -> String {
    pretty_json(&stream_schema_value())
}

pub fn input_schema_json() -> String {
    pretty_json(&input_schema_value())
}

pub fn catalog_json() -> String {
    pretty_json(&catalog_value())
}

fn pretty_json(value: &Value) -> String {
    let mut output = serde_json::to_string_pretty(value).expect("schema JSON should serialize");
    output.push('\n');
    output
}

fn output_schema_value() -> Value {
    json!({
        "$schema": JSON_SCHEMA_DRAFT,
        "$id": OUTPUT_SCHEMA_URL,
        "title": "wavepeek JSON output envelope",
        "description": "Canonical schema for wavepeek --json command outputs.",
        "type": "object",
        "additionalProperties": true,
        "required": ["$schema", "command", "data", "diagnostics"],
        "properties": {
            "$schema": {"type": "string", "const": OUTPUT_SCHEMA_URL},
            "command": {
                "type": "string",
                "enum": output_commands(),
            },
            "data": {
                "anyOf": [
                    ref_schema("infoData"),
                    ref_schema("scopeData"),
                    ref_schema("signalData"),
                    ref_schema("valueData"),
                    ref_schema("changeData"),
                    ref_schema("propertyData"),
                    ref_schema("extractGenericData"),
                    ref_schema("docsTopicsData"),
                    ref_schema("docsSearchData"),
                ],
            },
            "diagnostics": {"type": "array", "items": ref_schema("diagnostic")},
        },
        "allOf": [
            command_data_branch("info", "infoData"),
            command_data_branch("scope", "scopeData"),
            command_data_branch("signal", "signalData"),
            command_data_branch("value", "valueData"),
            command_data_branch("change", "changeData"),
            command_data_branch("property", "propertyData"),
            command_data_branch("extract generic", "extractGenericData"),
            command_data_branch("docs topics", "docsTopicsData"),
            command_data_branch("docs search", "docsSearchData"),
        ],
        "$defs": output_defs(),
    })
}

fn stream_schema_value() -> Value {
    json!({
        "$schema": JSON_SCHEMA_DRAFT,
        "$id": STREAM_SCHEMA_URL,
        "title": "wavepeek JSONL stream record",
        "description": "Canonical schema for one wavepeek --jsonl stream record. Validate each JSONL line independently; stream order and summary counts are stateful invariants.",
        "oneOf": [
            ref_schema("beginRecord"),
            ref_schema("itemRecord"),
            ref_schema("diagnosticRecord"),
            ref_schema("endRecord"),
        ],
        "$defs": stream_defs(),
    })
}

fn input_schema_value() -> Value {
    let mut defs = generated_input_payload_defs();
    let root = defs
        .remove("extractGenericSourcesInput")
        .expect("input root definition should be generated");
    let mut schema = match root {
        Value::Object(object) => object,
        _ => Map::new(),
    };
    schema.insert("$schema".to_string(), json!(JSON_SCHEMA_DRAFT));
    schema.insert("$id".to_string(), json!(INPUT_SCHEMA_URL));
    schema.insert("title".to_string(), json!("wavepeek JSON input documents"));
    schema.insert(
        "description".to_string(),
        json!("Canonical schema for wavepeek JSON input documents."),
    );
    schema.insert("$defs".to_string(), Value::Object(defs));
    Value::Object(schema)
}

fn catalog_value() -> Value {
    json!({
        "families": [
            {
                "id": OUTPUT_SCHEMA_ID,
                "version": OUTPUT_SCHEMA_VERSION_STR,
                "path": OUTPUT_SCHEMA_ARTIFACT_PATH,
                "url": OUTPUT_SCHEMA_URL,
            },
            {
                "id": STREAM_SCHEMA_ID,
                "version": STREAM_SCHEMA_VERSION_STR,
                "path": STREAM_SCHEMA_ARTIFACT_PATH,
                "url": STREAM_SCHEMA_URL,
            },
            {
                "id": INPUT_SCHEMA_ID,
                "version": INPUT_SCHEMA_VERSION_STR,
                "path": INPUT_SCHEMA_ARTIFACT_PATH,
                "url": INPUT_SCHEMA_URL,
            },
        ],
    })
}

fn output_defs() -> Value {
    let mut object = generated_output_payload_defs();
    object.insert(
        "scopeData".to_string(),
        json!({"type": "array", "items": ref_schema("scopeEntry")}),
    );
    object.insert(
        "signalData".to_string(),
        json!({"type": "array", "items": ref_schema("signalEntry")}),
    );
    object.insert(
        "valueData".to_string(),
        json!({
            "type": "array",
            "description": "Ordered snapshots sampled by the value command.",
            "items": ref_schema("valueSnapshot"),
        }),
    );
    object.insert(
        "changeData".to_string(),
        json!({"type": "array", "items": ref_schema("changeSnapshot")}),
    );
    object.insert(
        "propertyData".to_string(),
        json!({"type": "array", "items": ref_schema("propertyRow")}),
    );
    object.insert(
        "extractGenericData".to_string(),
        json!({"type": "array", "items": ref_schema("extractGenericRow")}),
    );
    Value::Object(object)
}

fn stream_defs() -> Value {
    let mut object = generated_waveform_payload_defs();
    object.extend(generated_stream_record_defs());
    object.insert(
        "streamCommand".to_string(),
        json!({"type": "string", "enum": stream_commands()}),
    );
    object.insert(
        "sequence".to_string(),
        json!({"type": "integer", "minimum": 0}),
    );
    object.insert(
        "itemRecord".to_string(),
        json!({
            "oneOf": [
                ref_schema("infoItemRecord"),
                ref_schema("scopeItemRecord"),
                ref_schema("signalItemRecord"),
                ref_schema("valueItemRecord"),
                ref_schema("changeItemRecord"),
                ref_schema("propertyItemRecord"),
                ref_schema("extractGenericItemRecord"),
            ]
        }),
    );
    for (alias, wrapper) in [
        ("infoItemRecord", "itemRecordForInfoData"),
        ("scopeItemRecord", "itemRecordForScopeEntry"),
        ("signalItemRecord", "itemRecordForSignalEntry"),
        ("valueItemRecord", "itemRecordForValueSnapshot"),
        ("changeItemRecord", "itemRecordForChangeSnapshot"),
        ("propertyItemRecord", "itemRecordForPropertyRow"),
        ("extractGenericItemRecord", "itemRecordForExtractGenericRow"),
    ] {
        object.insert(alias.to_string(), ref_schema(wrapper));
    }
    for (name, command, item_ref) in [
        ("itemRecordForInfoData", "info", "infoData"),
        ("itemRecordForScopeEntry", "scope", "scopeEntry"),
        ("itemRecordForSignalEntry", "signal", "signalEntry"),
        ("itemRecordForValueSnapshot", "value", "valueSnapshot"),
        ("itemRecordForChangeSnapshot", "change", "changeSnapshot"),
        ("itemRecordForPropertyRow", "property", "propertyRow"),
        (
            "itemRecordForExtractGenericRow",
            "extract generic",
            "extractGenericRow",
        ),
    ] {
        object.insert(name.to_string(), item_record_for(command, item_ref));
    }
    Value::Object(object)
}

fn generated_output_payload_defs() -> Map<String, Value> {
    let mut defs = generated_waveform_payload_defs();
    defs.extend(generated_docs_payload_defs());
    defs
}

fn generated_waveform_payload_defs() -> Map<String, Value> {
    let mut generator = SchemaGenerator::default();
    generator.subschema_for::<ContractDiagnostic<'static>>();
    generator.subschema_for::<NormalizedTime<'static>>();
    generator.subschema_for::<CanonicalPath<'static>>();
    generator.subschema_for::<SampledValue<'static>>();
    generator.subschema_for::<ScopeKind<'static>>();
    generator.subschema_for::<SignalKind<'static>>();
    generator.subschema_for::<InfoData<'static>>();
    generator.subschema_for::<ScopeEntry<'static>>();
    generator.subschema_for::<SignalEntry<'static>>();
    generator.subschema_for::<SampledSignalValue<'static>>();
    generator.subschema_for::<ChangeSignalValue<'static>>();
    generator.subschema_for::<ValueSnapshot<'static>>();
    generator.subschema_for::<ChangeSnapshot<'static>>();
    generator.subschema_for::<PropertyRow<'static>>();
    generator.subschema_for::<ExtractPayloadValue<'static>>();
    generator.subschema_for::<ExtractGenericRow<'static>>();
    generator.take_definitions(true)
}

fn generated_input_payload_defs() -> Map<String, Value> {
    let mut generator = SchemaGenerator::default();
    generator.subschema_for::<ExtractGenericSourcesInput<'static>>();
    generator.subschema_for::<ExtractGenericSource<'static>>();
    generator.take_definitions(true)
}

fn generated_docs_payload_defs() -> Map<String, Value> {
    let mut generator = SchemaGenerator::default();
    generator.subschema_for::<TopicSummary<'static>>();
    generator.subschema_for::<DocsTopicsData<'static>>();
    generator.subschema_for::<DocsSearchMatch<'static>>();
    generator.subschema_for::<DocsSearchData<'static>>();
    generator.take_definitions(true)
}

fn generated_stream_record_defs() -> Map<String, Value> {
    let mut generator = SchemaGenerator::default();
    generator.subschema_for::<BeginRecord>();
    generator.subschema_for::<DiagnosticRecord<'static>>();
    generator.subschema_for::<EndRecord>();
    generator.take_definitions(true)
}

fn command_data_branch(command: &str, data_def: &str) -> Value {
    json!({
        "if": {"properties": {"command": {"const": command}}, "required": ["command"]},
        "then": {"properties": {"data": ref_schema(data_def)}},
    })
}

fn item_record_for(command: &str, item_def: &str) -> Value {
    json!({
        "type": "object",
        "additionalProperties": true,
        "required": ["type", "seq", "command", "item"],
        "properties": {
            "type": {"const": "item"},
            "seq": ref_schema("sequence"),
            "command": {"const": command},
            "item": ref_schema(item_def),
        }
    })
}

fn ref_schema(def_name: &str) -> Value {
    json!({"$ref": format!("#/$defs/{def_name}")})
}

fn output_commands() -> Vec<&'static str> {
    vec![
        "info",
        "scope",
        "signal",
        "value",
        "change",
        "property",
        "extract generic",
        "docs topics",
        "docs search",
    ]
}

fn stream_commands() -> Vec<&'static str> {
    vec![
        "info",
        "scope",
        "signal",
        "value",
        "change",
        "property",
        "extract generic",
    ]
}

#[cfg(test)]
mod tests {
    use super::{
        INPUT_SCHEMA_ARTIFACT_PATH, OUTPUT_SCHEMA_ARTIFACT_PATH, STREAM_SCHEMA_ARTIFACT_PATH,
        catalog_value, input_schema_value, output_schema_value, stream_schema_value,
    };

    #[test]
    fn catalog_uses_canonical_paths_independent_of_output_directory() {
        let catalog = catalog_value();
        let families = catalog["families"]
            .as_array()
            .expect("families should be array");
        assert_eq!(families[0]["path"], OUTPUT_SCHEMA_ARTIFACT_PATH);
        assert_eq!(families[1]["path"], STREAM_SCHEMA_ARTIFACT_PATH);
        assert_eq!(families[2]["path"], INPUT_SCHEMA_ARTIFACT_PATH);
    }

    #[test]
    fn generated_schemas_use_exact_schema_urls() {
        let output = output_schema_value();
        assert_eq!(
            output["properties"]["$schema"]["const"],
            "https://kleverhq.github.io/wavepeek/schema-output-v2.1.json"
        );
        let stream = stream_schema_value();
        assert_eq!(
            stream["$defs"]["beginRecord"]["properties"]["$schema"]["const"],
            "https://kleverhq.github.io/wavepeek/schema-stream-v2.1.json"
        );
        let input = input_schema_value();
        assert_eq!(
            input["properties"]["$schema"]["const"],
            "https://kleverhq.github.io/wavepeek/schema-input-v2.1.json"
        );
    }
}