Skip to main content

fv_compute/
catalog.rs

1//! A **catalog** view over the registry (/) — the UX read-model.
2//!
3//! This is pure metadata projection (manifest → serializable descriptor): **no backend, no
4//! execution runtime**. A service that only *lists* transforms or models (a console catalog page)
5//! depends on `fv-compute` alone — it does not pull `fv-compute-wasm`'s wasmtime or `fv-infer`'s
6//! ONNX runtime. Execution crates stay separate from the browse surface.
7//!
8//! `descriptors` projects every unit; `model_descriptors` is the `impl = onnx` subset (the model
9//! catalog). Both feed the same `FieldInfo`/`TransformDescriptor` shape a UI renders.
10
11use crate::manifest::ImplKind;
12use crate::registry::{RegisteredTransform, Registry};
13use crate::types::ColumnSpec;
14use serde::Serialize;
15
16/// One column of a signature, UX-friendly (`type` is the manifest's own snake_case name).
17#[derive(Debug, Clone, Serialize, PartialEq)]
18pub struct FieldInfo {
19    pub name: String,
20    #[serde(rename = "type")]
21    pub dtype: String,
22}
23
24impl From<&ColumnSpec> for FieldInfo {
25    fn from(c: &ColumnSpec) -> Self {
26        let dtype = serde_json::to_value(c.dtype)
27            .ok()
28            .and_then(|v| v.as_str().map(str::to_string))
29            .unwrap_or_else(|| "unknown".into());
30        FieldInfo {
31            name: c.name.clone(),
32            dtype,
33        }
34    }
35}
36
37/// A registry unit (transform or model) as the UI sees it.
38#[derive(Debug, Clone, Serialize, PartialEq)]
39pub struct TransformDescriptor {
40    pub id: String,
41    pub version: String,
42    /// `id@version` — the selector used everywhere (pipeline step, UDF registration, provenance).
43    pub key: String,
44    /// Which registry root supplied it (the provided package or a business bundle).
45    pub root: String,
46    #[serde(rename = "impl")]
47    pub impl_kind: String,
48    /// Input signature (for a model: the feature columns). First input only, flattened for the UI.
49    pub inputs: Vec<FieldInfo>,
50    /// Output signature (for a model: prediction + any pass-through columns).
51    pub outputs: Vec<FieldInfo>,
52    pub hardware: String,
53    pub deterministic: bool,
54    pub io: bool,
55    pub streaming: bool,
56    /// The artifact/payload path within the unit directory (`.onnx`, `.wasm`, `expr.fvx`, …).
57    pub artifact: Option<String>,
58}
59
60/// Serialize a small `serde` enum to its string name (its `rename_all` form).
61fn enum_str<T: Serialize>(v: T) -> Option<String> {
62    serde_json::to_value(v)
63        .ok()
64        .and_then(|v| v.as_str().map(str::to_string))
65}
66
67impl From<&RegisteredTransform> for TransformDescriptor {
68    fn from(r: &RegisteredTransform) -> Self {
69        let m = &r.manifest;
70        let inputs = m
71            .inputs
72            .first()
73            .map(|s| s.columns.iter().map(FieldInfo::from).collect())
74            .unwrap_or_default();
75        let outputs = m.output.columns.iter().map(FieldInfo::from).collect();
76        TransformDescriptor {
77            id: m.id.clone(),
78            version: m.version.clone(),
79            key: r.key(),
80            root: r.root.clone(),
81            impl_kind: enum_str(m.impl_kind).unwrap_or_else(|| "builtin".into()),
82            inputs,
83            outputs,
84            hardware: enum_str(m.capabilities.hardware).unwrap_or_else(|| "cpu".into()),
85            deterministic: m.capabilities.deterministic,
86            io: m.capabilities.io,
87            streaming: m.capabilities.streaming,
88            artifact: m.entry.clone(),
89        }
90    }
91}
92
93/// Every unit in the registry, as descriptors (sorted by `key`).
94pub fn descriptors(registry: &Registry) -> Vec<TransformDescriptor> {
95    let mut out: Vec<TransformDescriptor> = registry.iter().map(TransformDescriptor::from).collect();
96    out.sort_by(|a, b| a.key.cmp(&b.key));
97    out
98}
99
100/// Only the `impl = onnx` units — the **model catalog**.
101pub fn model_descriptors(registry: &Registry) -> Vec<TransformDescriptor> {
102    let mut out: Vec<TransformDescriptor> = registry
103        .iter()
104        .filter(|r| r.manifest.impl_kind == ImplKind::Onnx)
105        .map(TransformDescriptor::from)
106        .collect();
107    out.sort_by(|a, b| a.key.cmp(&b.key));
108    out
109}
110
111/// The whole transform catalog as JSON (`{ "transforms": [ … ] }`).
112pub fn catalog_json(registry: &Registry) -> String {
113    serde_json::json!({ "transforms": descriptors(registry) }).to_string()
114}
115
116/// The model catalog as JSON (`{ "models": [ … ] }`) — what a model-management UI fetches.
117pub fn models_json(registry: &Registry) -> String {
118    serde_json::json!({ "models": model_descriptors(registry) }).to_string()
119}