1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{Endpoint, TypeRef};
7
8#[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#[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#[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#[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}