wavepeek 2.1.0

Command-line tool for RTL waveform inspection with deterministic machine-friendly output.
Documentation
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::Serialize;

use crate::diagnostic::Diagnostic;
use crate::engine::CommandName;
use crate::error::WavepeekError;

use super::common::ContractDiagnostic;
use super::output::{
    ChangeSnapshot, ExtractGenericRow, InfoData, PropertyRow, ScopeEntry, SignalEntry,
    ValueSnapshot,
};
use super::schema::STREAM_SCHEMA_URL;

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "beginRecord")]
#[schemars(extend("additionalProperties" = true))]
pub struct BeginRecord {
    #[serde(rename = "type")]
    #[schemars(schema_with = "begin_record_type_schema")]
    record_type: &'static str,
    #[schemars(schema_with = "sequence_ref_schema")]
    seq: usize,
    #[schemars(schema_with = "stream_command_ref_schema")]
    command: &'static str,
    #[serde(rename = "$schema")]
    #[schemars(schema_with = "stream_schema_url_schema")]
    schema: &'static str,
}

impl BeginRecord {
    pub fn new(seq: usize, command: CommandName) -> Result<Self, WavepeekError> {
        require_stream_command(command)?;
        Ok(Self {
            record_type: "begin",
            seq,
            command: command.as_str(),
            schema: STREAM_SCHEMA_URL,
        })
    }
}

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "itemRecord")]
#[schemars(extend("additionalProperties" = true))]
pub struct ItemRecord<'a> {
    #[serde(rename = "type")]
    #[schemars(schema_with = "item_record_type_schema")]
    record_type: &'static str,
    #[schemars(schema_with = "sequence_ref_schema")]
    seq: usize,
    #[schemars(schema_with = "stream_command_ref_schema")]
    command: &'static str,
    item: StreamItemData<'a>,
}

impl<'a> ItemRecord<'a> {
    pub fn new<T: StreamItem + ?Sized>(
        seq: usize,
        command: CommandName,
        item: &'a T,
    ) -> Result<Self, WavepeekError> {
        require_stream_command(command)?;
        Ok(Self {
            record_type: "item",
            seq,
            command: command.as_str(),
            item: item.stream_item(command)?,
        })
    }
}

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "diagnosticRecord")]
#[schemars(extend("additionalProperties" = true))]
pub struct DiagnosticRecord<'a> {
    #[serde(rename = "type")]
    #[schemars(schema_with = "diagnostic_record_type_schema")]
    record_type: &'static str,
    #[schemars(schema_with = "sequence_ref_schema")]
    seq: usize,
    #[schemars(schema_with = "stream_command_ref_schema")]
    command: &'static str,
    diagnostic: ContractDiagnostic<'a>,
}

impl<'a> DiagnosticRecord<'a> {
    pub fn new(
        seq: usize,
        command: CommandName,
        diagnostic: &'a Diagnostic,
    ) -> Result<Self, WavepeekError> {
        require_stream_command(command)?;
        Ok(Self {
            record_type: "diagnostic",
            seq,
            command: command.as_str(),
            diagnostic: ContractDiagnostic::from_diagnostic(diagnostic)?,
        })
    }
}

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "endRecord")]
#[schemars(extend("additionalProperties" = true))]
pub struct EndRecord {
    #[serde(rename = "type")]
    #[schemars(schema_with = "end_record_type_schema")]
    record_type: &'static str,
    #[schemars(schema_with = "sequence_ref_schema")]
    seq: usize,
    #[schemars(schema_with = "stream_command_ref_schema")]
    command: &'static str,
    summary: StreamSummary,
}

impl EndRecord {
    pub fn new(
        seq: usize,
        command: CommandName,
        items: usize,
        diagnostics: usize,
        truncated: bool,
    ) -> Result<Self, WavepeekError> {
        require_stream_command(command)?;
        Ok(Self {
            record_type: "end",
            seq,
            command: command.as_str(),
            summary: StreamSummary {
                status: "ok",
                items,
                diagnostics,
                truncated,
            },
        })
    }
}

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "streamSummary")]
#[schemars(extend("additionalProperties" = true))]
struct StreamSummary {
    #[schemars(schema_with = "ok_status_schema")]
    status: &'static str,
    #[schemars(schema_with = "nonnegative_count_schema")]
    items: usize,
    #[schemars(schema_with = "nonnegative_count_schema")]
    diagnostics: usize,
    truncated: bool,
}

#[derive(Debug, JsonSchema, Serialize)]
#[schemars(rename = "streamItemData")]
#[serde(untagged)]
pub enum StreamItemData<'a> {
    Info(InfoData<'a>),
    Scope(ScopeEntry<'a>),
    Signal(SignalEntry<'a>),
    Value(ValueSnapshot<'a>),
    Change(ChangeSnapshot<'a>),
    Property(PropertyRow<'a>),
    ExtractGeneric(ExtractGenericRow<'a>),
}

pub trait StreamItem {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError>;
}

impl StreamItem for crate::engine::info::InfoData {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Info)?;
        Ok(StreamItemData::Info(InfoData::from(self)))
    }
}

impl StreamItem for crate::engine::scope::ScopeEntry {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Scope)?;
        Ok(StreamItemData::Scope(ScopeEntry::try_from(self)?))
    }
}

impl StreamItem for crate::engine::signal::SignalEntry {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Signal)?;
        Ok(StreamItemData::Signal(SignalEntry::try_from(self)?))
    }
}

impl StreamItem for crate::engine::value::ValueSnapshot {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Value)?;
        Ok(StreamItemData::Value(ValueSnapshot::from(self)))
    }
}

impl StreamItem for crate::engine::change::ChangeSnapshot {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Change)?;
        Ok(StreamItemData::Change(ChangeSnapshot::from(self)))
    }
}

impl StreamItem for crate::engine::property::PropertyCaptureRow {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::Property)?;
        Ok(StreamItemData::Property(PropertyRow::from(self)))
    }
}

impl StreamItem for crate::engine::extract::ExtractGenericRow {
    fn stream_item(&self, command: CommandName) -> Result<StreamItemData<'_>, WavepeekError> {
        require_item_command(command, CommandName::ExtractGeneric)?;
        Ok(StreamItemData::ExtractGeneric(ExtractGenericRow::from(
            self,
        )))
    }
}

fn begin_record_type_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"const": "begin"})
}

fn item_record_type_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"const": "item"})
}

fn diagnostic_record_type_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"const": "diagnostic"})
}

fn end_record_type_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"const": "end"})
}

fn ok_status_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"const": "ok"})
}

fn sequence_ref_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"$ref": "#/$defs/sequence"})
}

fn nonnegative_count_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"type": "integer", "minimum": 0})
}

fn stream_command_ref_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"$ref": "#/$defs/streamCommand"})
}

fn stream_schema_url_schema(_: &mut SchemaGenerator) -> Schema {
    json_schema!({"type": "string", "const": STREAM_SCHEMA_URL})
}

fn require_item_command(actual: CommandName, expected: CommandName) -> Result<(), WavepeekError> {
    if actual == expected {
        Ok(())
    } else {
        Err(WavepeekError::Internal(format!(
            "JSONL item for {} cannot be written to {} stream",
            expected.as_str(),
            actual.as_str()
        )))
    }
}

fn require_stream_command(command: CommandName) -> Result<(), WavepeekError> {
    match command {
        CommandName::Info
        | CommandName::Scope
        | CommandName::Signal
        | CommandName::Value
        | CommandName::Change
        | CommandName::Property
        | CommandName::ExtractGeneric => Ok(()),
        _ => Err(WavepeekError::Args(
            "--jsonl is available only for waveform commands".to_string(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use crate::engine::CommandName;

    use super::{BeginRecord, ItemRecord};

    #[test]
    fn begin_record_uses_stream_schema_url() {
        let value = serde_json::to_value(
            BeginRecord::new(0, CommandName::Change).expect("change begin should convert"),
        )
        .expect("begin record should serialize");

        assert_eq!(value["type"], "begin");
        assert_eq!(
            value["$schema"],
            "https://kleverhq.github.io/wavepeek/schema-stream-v2.1.json"
        );
    }

    #[test]
    fn item_record_rejects_command_mismatch() {
        let item = crate::engine::info::InfoData {
            time_unit: "1ns".to_string(),
            time_start: "0ns".to_string(),
            time_end: "10ns".to_string(),
        };

        assert!(ItemRecord::new(1, CommandName::Change, &item).is_err());
    }

    #[test]
    fn item_record_uses_contract_payload_shape() {
        let item = crate::engine::signal::SignalEntry {
            display: "clk".to_string(),
            name: "clk".to_string(),
            path: "top.clk".to_string(),
            kind: "wire".to_string(),
            width: Some(1),
        };
        let value = serde_json::to_value(
            ItemRecord::new(1, CommandName::Signal, &item).expect("signal item should convert"),
        )
        .expect("item record should serialize");

        assert_eq!(
            value,
            json!({
                "type": "item",
                "seq": 1,
                "command": "signal",
                "item": {"name": "clk", "path": "top.clk", "kind": "wire", "width": 1}
            })
        );
    }
}