Skip to main content

seam_codegen/manifest/
mod.rs

1/* src/cli/codegen/src/manifest/mod.rs */
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8#[cfg(test)]
9mod tests;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum ProcedureType {
14	Query,
15	Command,
16	Subscription,
17	Stream,
18	Upload,
19}
20
21impl std::fmt::Display for ProcedureType {
22	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23		match self {
24			Self::Query => write!(f, "query"),
25			Self::Command => write!(f, "command"),
26			Self::Subscription => write!(f, "subscription"),
27			Self::Stream => write!(f, "stream"),
28			Self::Upload => write!(f, "upload"),
29		}
30	}
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum TransportPreference {
36	Http,
37	Sse,
38	Ws,
39	Ipc,
40}
41
42impl std::fmt::Display for TransportPreference {
43	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44		match self {
45			Self::Http => write!(f, "http"),
46			Self::Sse => write!(f, "sse"),
47			Self::Ws => write!(f, "ws"),
48			Self::Ipc => write!(f, "ipc"),
49		}
50	}
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TransportConfig {
55	pub prefer: TransportPreference,
56	#[serde(default, skip_serializing_if = "Option::is_none")]
57	pub fallback: Option<Vec<TransportPreference>>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ContextSchema {
62	pub extract: String,
63	pub schema: Value,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct Manifest {
68	pub version: u32,
69	#[serde(default)]
70	pub context: BTreeMap<String, ContextSchema>,
71	pub procedures: BTreeMap<String, ProcedureSchema>,
72	#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
73	pub channels: BTreeMap<String, ChannelSchema>,
74	#[serde(default, rename = "transportDefaults")]
75	pub transport_defaults: BTreeMap<String, TransportConfig>,
76}
77
78impl Manifest {
79	pub fn validate_context_refs(&self) -> Result<(), Vec<String>> {
80		let mut errors = vec![];
81		for (proc_name, schema) in &self.procedures {
82			if let Some(ctx_keys) = &schema.context {
83				for key in ctx_keys {
84					if !self.context.contains_key(key) {
85						errors.push(format!("Procedure '{proc_name}' references undefined context '{key}'"));
86					}
87				}
88			}
89		}
90		if errors.is_empty() { Ok(()) } else { Err(errors) }
91	}
92}
93
94/// Cache hint for query procedures: `{ "ttl": 30 }` or `false`.
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96#[serde(untagged)]
97pub enum CacheHint {
98	Config { ttl: u64 },
99	Disabled(bool),
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ProcedureSchema {
104	#[serde(rename = "kind", alias = "type")]
105	pub proc_type: ProcedureType,
106	pub input: Value,
107	#[serde(default, skip_serializing_if = "Option::is_none")]
108	pub output: Option<Value>,
109	#[serde(default, skip_serializing_if = "Option::is_none", rename = "chunkOutput")]
110	pub chunk_output: Option<Value>,
111	#[serde(default, skip_serializing_if = "Option::is_none")]
112	pub error: Option<Value>,
113	#[serde(default, skip_serializing_if = "Option::is_none")]
114	pub invalidates: Option<Vec<InvalidateTarget>>,
115	#[serde(default, skip_serializing_if = "Option::is_none")]
116	pub context: Option<Vec<String>>,
117	#[serde(default, skip_serializing_if = "Option::is_none")]
118	pub transport: Option<TransportConfig>,
119	#[serde(default, skip_serializing_if = "Option::is_none")]
120	pub suppress: Option<Vec<String>>,
121	#[serde(default, skip_serializing_if = "Option::is_none")]
122	pub cache: Option<CacheHint>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct InvalidateTarget {
127	pub query: String,
128	#[serde(default, skip_serializing_if = "Option::is_none")]
129	pub mapping: Option<BTreeMap<String, MappingValue>>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MappingValue {
134	pub from: String,
135	#[serde(default, skip_serializing_if = "Option::is_none")]
136	pub each: Option<bool>,
137}
138
139impl ProcedureSchema {
140	/// Return the effective output schema: chunkOutput for streams, output for others.
141	pub fn effective_output(&self) -> Option<&Value> {
142		self.chunk_output.as_ref().or(self.output.as_ref())
143	}
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ChannelSchema {
148	pub input: Value,
149	pub incoming: BTreeMap<String, IncomingSchema>,
150	pub outgoing: BTreeMap<String, Value>,
151	#[serde(default, skip_serializing_if = "Option::is_none")]
152	pub transport: Option<TransportConfig>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct IncomingSchema {
157	pub input: Value,
158	pub output: Value,
159	#[serde(default, skip_serializing_if = "Option::is_none")]
160	pub error: Option<Value>,
161}