Skip to main content

zynk_schema/
graph.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{Endpoint, TypeRef};
7
8/// A model field exposed to generated clients.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Field {
12    pub source_name: String,
13    pub wire_name: String,
14    pub ty: TypeRef,
15    pub required: bool,
16    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
17    pub optional: bool,
18    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
19    pub nullable: bool,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub doc: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub default: Option<Value>,
24}
25
26impl Field {
27    pub fn new(
28        source_name: impl Into<String>,
29        wire_name: impl Into<String>,
30        ty: TypeRef,
31        required: bool,
32    ) -> Self {
33        let optional = ty.optional;
34        let nullable = ty.nullable;
35
36        Self {
37            source_name: source_name.into(),
38            wire_name: wire_name.into(),
39            ty,
40            required,
41            optional,
42            nullable,
43            doc: None,
44            default: None,
45        }
46    }
47}
48
49/// A structured object definition referenced by endpoint signatures.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct ModelDef {
53    pub name: String,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub fields: Vec<Field>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub doc: Option<String>,
58}
59
60impl ModelDef {
61    pub fn new(name: impl Into<String>) -> Self {
62        Self {
63            name: name.into(),
64            fields: Vec::new(),
65            doc: None,
66        }
67    }
68}
69
70/// An enum definition referenced by endpoint signatures.
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct EnumDef {
74    pub name: String,
75    #[serde(default, skip_serializing_if = "Vec::is_empty")]
76    pub values: Vec<Value>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub doc: Option<String>,
79}
80
81impl EnumDef {
82    pub fn new(name: impl Into<String>) -> Self {
83        Self {
84            name: name.into(),
85            values: Vec::new(),
86            doc: None,
87        }
88    }
89}
90
91/// Complete language-neutral API graph passed between Zynk modules.
92#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ApiGraph {
95    #[serde(default)]
96    pub endpoints: BTreeMap<String, Endpoint>,
97    #[serde(default)]
98    pub models: BTreeMap<String, ModelDef>,
99    #[serde(default)]
100    pub enums: BTreeMap<String, EnumDef>,
101}
102
103impl ApiGraph {
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn insert_endpoint(&mut self, endpoint: Endpoint) -> Option<Endpoint> {
109        self.endpoints.insert(endpoint.name.clone(), endpoint)
110    }
111
112    pub fn insert_model(&mut self, model: ModelDef) -> Option<ModelDef> {
113        self.models.insert(model.name.clone(), model)
114    }
115
116    pub fn insert_enum(&mut self, enum_def: EnumDef) -> Option<EnumDef> {
117        self.enums.insert(enum_def.name.clone(), enum_def)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use crate::{Endpoint, EndpointKind, Param, TypeRef};
124
125    use super::{ApiGraph, EnumDef, Field, ModelDef};
126
127    #[test]
128    fn serializes_endpoint_graph_without_io() {
129        let mut graph = ApiGraph::new();
130        let mut endpoint = Endpoint::new("get_user", EndpointKind::Rpc, TypeRef::model("User"));
131        endpoint.params.push(Param::new(
132            "user_id",
133            "userId",
134            TypeRef::primitive("number"),
135            true,
136        ));
137        graph.insert_endpoint(endpoint);
138
139        let json = serde_json::to_string(&graph).expect("serialize graph");
140        assert!(json.contains("get_user"));
141        assert!(json.contains("userId"));
142    }
143
144    #[test]
145    fn field_new_populates_optional_nullable_from_type_ref() {
146        let mut ty = TypeRef::primitive("string");
147        ty.optional = true;
148        ty.nullable = true;
149
150        let field = super::Field::new("display_name", "displayName", ty, false);
151
152        assert!(field.optional);
153        assert!(field.nullable);
154    }
155
156    #[test]
157    fn field_optional_true_round_trips_and_skips_false_nullable() {
158        let mut field = super::Field::new(
159            "display_name",
160            "displayName",
161            TypeRef::primitive("string"),
162            false,
163        );
164        field.optional = true;
165        field.nullable = false;
166
167        let json = serde_json::to_value(&field).expect("serialize field");
168        let object = json.as_object().expect("field serializes as object");
169        assert_eq!(object.get("optional"), Some(&serde_json::Value::Bool(true)));
170        assert!(!object.contains_key("nullable"));
171
172        let round_tripped: super::Field = serde_json::from_value(json).expect("deserialize field");
173        assert_eq!(round_tripped, field);
174    }
175
176    #[test]
177    fn field_missing_optional_nullable_defaults_to_false_and_serializes_without_keys() {
178        let json = r#"{
179            "sourceName": "display_name",
180            "wireName": "displayName",
181            "ty": { "kind": "primitive", "name": "string" },
182            "required": true
183        }"#;
184
185        let field: super::Field = serde_json::from_str(json).expect("deserialize field");
186        assert!(!field.optional);
187        assert!(!field.nullable);
188
189        let serialized = serde_json::to_value(&field).expect("serialize field");
190        let object = serialized.as_object().expect("field serializes as object");
191        assert!(!object.contains_key("optional"));
192        assert!(!object.contains_key("nullable"));
193    }
194
195    #[test]
196    fn api_graph_round_trips_all_endpoint_and_type_features() {
197        let mut graph = ApiGraph::new();
198
199        let mut priority = EnumDef::new("Priority");
200        priority.doc = Some("Explicit enum values for code generators.".to_string());
201        priority.values = vec![
202            serde_json::json!("low"),
203            serde_json::json!("medium"),
204            serde_json::json!("high"),
205            serde_json::json!(3),
206        ];
207        graph.insert_enum(priority);
208
209        let mut metadata = ModelDef::new("Metadata");
210        metadata.doc = Some("Nested metadata model.".to_string());
211        metadata.fields.push(Field::new(
212            "created_at",
213            "createdAt",
214            TypeRef::primitive("string"),
215            true,
216        ));
217        metadata.fields.push(Field::new(
218            "tags",
219            "tags",
220            TypeRef::array(TypeRef::primitive("string")),
221            true,
222        ));
223        metadata.fields.push(Field::new(
224            "attributes",
225            "attributes",
226            TypeRef::record(TypeRef::primitive("string"), TypeRef::any()),
227            false,
228        ));
229        graph.insert_model(metadata);
230
231        let mut user = ModelDef::new("User");
232        user.doc = Some("User model with nested, enum, and literal fields.".to_string());
233        user.fields
234            .push(Field::new("id", "id", TypeRef::primitive("number"), true));
235        user.fields.push(Field::new(
236            "metadata",
237            "metadata",
238            TypeRef::model("Metadata"),
239            true,
240        ));
241        user.fields.push(Field::new(
242            "priority",
243            "priority",
244            TypeRef::enum_ref("Priority"),
245            true,
246        ));
247        user.fields.push(Field::new(
248            "role",
249            "role",
250            TypeRef::literal(serde_json::json!("admin")),
251            true,
252        ));
253        user.fields.push(Field::new(
254            "retries",
255            "retries",
256            TypeRef::literal(serde_json::json!(3)),
257            true,
258        ));
259        user.fields.push(Field::new(
260            "enabled",
261            "enabled",
262            TypeRef::literal(serde_json::json!(true)),
263            true,
264        ));
265        user.fields.push(Field::new(
266            "deleted_marker",
267            "deletedMarker",
268            TypeRef::literal(serde_json::Value::Null),
269            false,
270        ));
271
272        let mut optional_but_not_nullable = TypeRef::primitive("string");
273        optional_but_not_nullable.optional = true;
274        user.fields.push(Field::new(
275            "display_name",
276            "displayName",
277            optional_but_not_nullable,
278            false,
279        ));
280
281        let mut nullable_but_required = TypeRef::model("Metadata");
282        nullable_but_required.nullable = true;
283        user.fields.push(Field::new(
284            "secondary_metadata",
285            "secondaryMetadata",
286            nullable_but_required,
287            true,
288        ));
289
290        let mut optional_and_nullable = TypeRef::union(vec![
291            TypeRef::literal(serde_json::json!("email")),
292            TypeRef::literal(serde_json::json!("sms")),
293        ]);
294        optional_and_nullable.optional = true;
295        optional_and_nullable.nullable = true;
296        let mut preferred_contact = Field::new(
297            "preferred_contact",
298            "preferredContact",
299            optional_and_nullable,
300            false,
301        );
302        preferred_contact.doc = Some("Optional nullable union of literal values.".to_string());
303        preferred_contact.default = Some(serde_json::json!("email"));
304        user.fields.push(preferred_contact);
305
306        graph.insert_model(user);
307
308        let mut rpc = Endpoint::new("get_user", EndpointKind::Rpc, TypeRef::model("User"));
309        rpc.module = Some("users".to_string());
310        rpc.doc = Some("Fetches a user.".to_string());
311        rpc.params.push(Param::new(
312            "user_id",
313            "userId",
314            TypeRef::primitive("number"),
315            true,
316        ));
317        let mut include_deleted = Param::new(
318            "include_deleted",
319            "includeDeleted",
320            TypeRef::primitive("boolean"),
321            false,
322        );
323        include_deleted.default = Some(serde_json::json!(false));
324        rpc.params.push(include_deleted);
325        graph.insert_endpoint(rpc);
326
327        let mut channel = Endpoint::new("watch_users", EndpointKind::Channel, TypeRef::void());
328        channel.channel_item = Some(TypeRef::model("User"));
329        channel.params.push(Param::new(
330            "priority",
331            "priority",
332            TypeRef::enum_ref("Priority"),
333            false,
334        ));
335        graph.insert_endpoint(channel);
336
337        let mut upload = Endpoint::new(
338            "upload_avatar",
339            EndpointKind::Upload,
340            TypeRef::model("User"),
341        );
342        upload.file_param = Some("avatar".to_string());
343        upload.multi_file = false;
344        upload.max_size = Some(10_485_760);
345        upload.allowed_types = vec!["image/png".to_string(), "image/jpeg".to_string()];
346        upload.params.push(Param::new(
347            "user_id",
348            "userId",
349            TypeRef::primitive("number"),
350            true,
351        ));
352        graph.insert_endpoint(upload);
353
354        let mut static_endpoint = Endpoint::new(
355            "download_report",
356            EndpointKind::Static,
357            TypeRef::primitive("string"),
358        );
359        static_endpoint.params.push(Param::new(
360            "report_id",
361            "reportId",
362            TypeRef::primitive("string"),
363            true,
364        ));
365        graph.insert_endpoint(static_endpoint);
366
367        let mut websocket = Endpoint::new("user_socket", EndpointKind::Ws, TypeRef::void());
368        websocket.server_events.push(Param::new(
369            "user_updated",
370            "userUpdated",
371            TypeRef::model("User"),
372            true,
373        ));
374        websocket.server_events.push(Param::new(
375            "heartbeat",
376            "heartbeat",
377            TypeRef::literal(serde_json::json!("ok")),
378            true,
379        ));
380        websocket.client_events.push(Param::new(
381            "subscribe",
382            "subscribe",
383            TypeRef::record(TypeRef::primitive("string"), TypeRef::any()),
384            true,
385        ));
386        websocket.client_events.push(Param::new(
387            "set_priority",
388            "setPriority",
389            TypeRef::enum_ref("Priority"),
390            false,
391        ));
392        graph.insert_endpoint(websocket);
393
394        let json = serde_json::to_string_pretty(&graph).expect("serialize graph");
395        let round_tripped: ApiGraph = serde_json::from_str(&json).expect("deserialize graph");
396
397        assert_eq!(round_tripped, graph);
398        assert_eq!(graph.endpoints.len(), 5);
399        assert!(graph
400            .endpoints
401            .values()
402            .any(|endpoint| endpoint.kind == EndpointKind::Rpc));
403        assert!(graph
404            .endpoints
405            .values()
406            .any(|endpoint| endpoint.kind == EndpointKind::Channel));
407        assert!(graph
408            .endpoints
409            .values()
410            .any(|endpoint| endpoint.kind == EndpointKind::Upload));
411        assert!(graph
412            .endpoints
413            .values()
414            .any(|endpoint| endpoint.kind == EndpointKind::Static));
415        assert!(graph
416            .endpoints
417            .values()
418            .any(|endpoint| endpoint.kind == EndpointKind::Ws));
419
420        let user = graph.models.get("User").expect("User model exists");
421        assert!(user.fields.iter().any(|field| {
422            field.wire_name == "displayName" && field.optional && !field.nullable
423        }));
424        assert!(user.fields.iter().any(|field| {
425            field.wire_name == "secondaryMetadata" && !field.optional && field.nullable
426        }));
427        assert!(user.fields.iter().any(|field| {
428            field.wire_name == "preferredContact" && field.optional && field.nullable
429        }));
430    }
431}