1use 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#[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 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}