1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct GeneratorInfo {
12 pub gi_tool: String,
14 pub gi_version: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct GenerationMetadata {
22 pub gm_generators: Vec<GeneratorInfo>,
24 pub gm_timestamp: String,
26 pub gm_ir_version: String,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct IR {
34 pub ir_version: String,
36 pub ir_backend: String,
38 #[serde(default)]
40 pub ir_hash: Option<String>,
41 #[serde(default)]
43 pub ir_metadata: Option<GenerationMetadata>,
44 pub ir_types: HashMap<String, TypeDef>,
46 pub ir_methods: HashMap<String, MethodDef>,
48 pub ir_plugins: HashMap<String, Vec<String>>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct TypeDef {
56 pub td_name: String,
57 pub td_namespace: String,
58 #[serde(default)]
59 pub td_description: Option<String>,
60 pub td_kind: TypeKind,
61}
62
63impl TypeDef {
64 pub fn full_name(&self) -> String {
66 format!("{}.{}", self.td_namespace, self.td_name)
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(tag = "tag")]
73pub enum TypeKind {
74 KindStruct {
76 #[serde(rename = "ksFields")]
77 ks_fields: Vec<FieldDef>,
78 },
79 KindEnum {
81 #[serde(rename = "keDiscriminator")]
83 ke_discriminator: String,
84 #[serde(rename = "keVariants")]
85 ke_variants: Vec<VariantDef>,
86 },
87 KindAlias {
89 #[serde(rename = "kaTarget")]
90 ka_target: TypeRef,
91 },
92 KindPrimitive {
94 #[serde(rename = "kpType")]
95 kp_type: String,
96 #[serde(rename = "kpFormat")]
97 kp_format: Option<String>,
98 },
99 KindStringEnum {
101 #[serde(rename = "kseValues")]
102 kse_values: Vec<String>,
103 },
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct FieldDef {
110 pub fd_name: String,
111 pub fd_type: TypeRef,
112 #[serde(default)]
113 pub fd_description: Option<String>,
114 #[serde(default)]
115 pub fd_required: bool,
116 #[serde(default)]
117 pub fd_default: Option<serde_json::Value>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct VariantDef {
124 pub vd_name: String,
125 #[serde(default)]
126 pub vd_description: Option<String>,
127 #[serde(default)]
128 pub vd_fields: Vec<FieldDef>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[serde(rename_all = "camelCase")]
134pub struct QualifiedName {
135 pub qn_namespace: String,
136 pub qn_local_name: String,
137}
138
139impl QualifiedName {
140 pub fn full_name(&self) -> String {
142 if self.qn_namespace.is_empty() {
143 self.qn_local_name.clone()
144 } else {
145 format!("{}.{}", self.qn_namespace, self.qn_local_name)
146 }
147 }
148
149 pub fn namespace(&self) -> Option<&str> {
151 if self.qn_namespace.is_empty() {
152 None
153 } else {
154 Some(&self.qn_namespace)
155 }
156 }
157
158 pub fn local_name(&self) -> &str {
160 &self.qn_local_name
161 }
162}
163
164#[derive(Debug, Clone, Serialize)]
172pub enum TypeRef {
173 RefNamed(QualifiedName),
175 RefPrimitive(String, Option<String>),
177 RefArray(Box<TypeRef>),
179 RefOptional(Box<TypeRef>),
181 RefAny,
183 RefUnknown,
185}
186
187impl<'de> serde::Deserialize<'de> for TypeRef {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: serde::Deserializer<'de>,
191 {
192 use serde::de::Error;
193
194 let value = serde_json::Value::deserialize(deserializer)?;
195 let obj = value.as_object().ok_or_else(|| D::Error::custom("expected object"))?;
196
197 let tag = obj.get("tag")
198 .and_then(|v| v.as_str())
199 .ok_or_else(|| D::Error::custom("missing tag field"))?;
200
201 match tag {
202 "RefNamed" => {
203 let contents = obj.get("contents")
204 .ok_or_else(|| D::Error::custom("RefNamed requires contents"))?;
205 let qname: QualifiedName = serde_json::from_value(contents.clone())
206 .map_err(|e| D::Error::custom(format!("RefNamed contents must be QualifiedName: {}", e)))?;
207 Ok(TypeRef::RefNamed(qname))
208 }
209 "RefPrimitive" => {
210 let contents = obj.get("contents")
211 .and_then(|v| v.as_array())
212 .ok_or_else(|| D::Error::custom("RefPrimitive requires array contents"))?;
213 let prim = contents.get(0)
214 .and_then(|v| v.as_str())
215 .ok_or_else(|| D::Error::custom("RefPrimitive[0] must be string"))?;
216 let format = contents.get(1)
217 .and_then(|v| v.as_str())
218 .map(|s| s.to_string());
219 Ok(TypeRef::RefPrimitive(prim.to_string(), format))
220 }
221 "RefArray" => {
222 let contents = obj.get("contents")
223 .ok_or_else(|| D::Error::custom("RefArray requires contents"))?;
224 let inner: TypeRef = serde_json::from_value(contents.clone())
225 .map_err(|e| D::Error::custom(format!("RefArray inner: {}", e)))?;
226 Ok(TypeRef::RefArray(Box::new(inner)))
227 }
228 "RefOptional" => {
229 let contents = obj.get("contents")
230 .ok_or_else(|| D::Error::custom("RefOptional requires contents"))?;
231 let inner: TypeRef = serde_json::from_value(contents.clone())
232 .map_err(|e| D::Error::custom(format!("RefOptional inner: {}", e)))?;
233 Ok(TypeRef::RefOptional(Box::new(inner)))
234 }
235 "RefAny" => Ok(TypeRef::RefAny),
236 "RefUnknown" => Ok(TypeRef::RefUnknown),
237 other => Err(D::Error::custom(format!("unknown TypeRef tag: {}", other))),
238 }
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct MethodDef {
246 pub md_name: String,
247 pub md_full_path: String,
248 pub md_namespace: String,
249 #[serde(default)]
250 pub md_description: Option<String>,
251 #[serde(default)]
252 pub md_streaming: bool,
253 #[serde(default)]
254 pub md_params: Vec<ParamDef>,
255 pub md_returns: TypeRef,
256 #[serde(default)]
273 pub md_bidir_type: Option<TypeRef>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct ParamDef {
280 pub pd_name: String,
281 pub pd_type: TypeRef,
282 #[serde(default)]
283 pub pd_description: Option<String>,
284 #[serde(default)]
285 pub pd_required: bool,
286 #[serde(default)]
287 pub pd_default: Option<serde_json::Value>,
288}
289
290impl TypeRef {
293 pub fn to_ts(&self) -> String {
295 match self {
296 TypeRef::RefNamed(qname) => to_upper_camel(&qname.full_name()),
297 TypeRef::RefPrimitive(prim, format) => primitive_to_ts(prim, format.as_deref()),
298 TypeRef::RefArray(inner) => format!("{}[]", inner.to_ts()),
299 TypeRef::RefOptional(inner) => format!("{} | null", inner.to_ts()),
300 TypeRef::RefAny => "unknown".to_string(), TypeRef::RefUnknown => "unknown".to_string(), }
303 }
304
305 pub fn to_ts_in_namespace(&self, current_namespace: &str) -> String {
308 match self {
309 TypeRef::RefNamed(qname) => {
310 to_upper_camel(qname.local_name())
312 }
313 TypeRef::RefPrimitive(prim, format) => primitive_to_ts(prim, format.as_deref()),
314 TypeRef::RefArray(inner) => format!("{}[]", inner.to_ts_in_namespace(current_namespace)),
315 TypeRef::RefOptional(inner) => format!("{} | null", inner.to_ts_in_namespace(current_namespace)),
316 TypeRef::RefAny => "unknown".to_string(),
317 TypeRef::RefUnknown => "unknown".to_string(),
318 }
319 }
320
321 pub fn get_namespace(&self) -> Option<&str> {
323 match self {
324 TypeRef::RefNamed(qname) => qname.namespace(),
325 _ => None,
326 }
327 }
328
329 pub fn is_unknown(&self) -> bool {
331 matches!(self, TypeRef::RefUnknown)
332 }
333
334 pub fn contains_unknown(&self) -> bool {
336 match self {
337 TypeRef::RefUnknown => true,
338 TypeRef::RefArray(inner) => inner.contains_unknown(),
339 TypeRef::RefOptional(inner) => inner.contains_unknown(),
340 _ => false,
341 }
342 }
343}
344
345fn primitive_to_ts(prim: &str, format: Option<&str>) -> String {
346 match (prim, format) {
347 ("string", Some("uuid")) => "string".to_string(), ("string", _) => "string".to_string(),
349 ("integer", _) | ("number", _) => "number".to_string(),
350 ("boolean", _) => "boolean".to_string(),
351 ("array", _) => "unknown[]".to_string(),
352 ("object", _) => "Record<string, unknown>".to_string(),
353 _ => "unknown".to_string(),
354 }
355}
356
357fn to_upper_camel(s: &str) -> String {
358 s.split('.')
360 .map(|part| {
361 let mut result = String::new();
362 let mut capitalize = true;
363 for c in part.chars() {
364 if c == '_' || c == '-' {
365 capitalize = true;
366 } else if capitalize {
367 result.push(c.to_ascii_uppercase());
368 capitalize = false;
369 } else {
370 result.push(c);
371 }
372 }
373 result
374 })
375 .collect::<Vec<_>>()
376 .join("")
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_qualified_name() {
385 let qn = QualifiedName {
387 qn_namespace: "cone".to_string(),
388 qn_local_name: "UUID".to_string(),
389 };
390 assert_eq!(qn.full_name(), "cone.UUID");
391 assert_eq!(qn.namespace(), Some("cone"));
392 assert_eq!(qn.local_name(), "UUID");
393
394 let qn_no_ns = QualifiedName {
396 qn_namespace: "".to_string(),
397 qn_local_name: "LocalType".to_string(),
398 };
399 assert_eq!(qn_no_ns.full_name(), "LocalType");
400 assert_eq!(qn_no_ns.namespace(), None);
401 assert_eq!(qn_no_ns.local_name(), "LocalType");
402 }
403
404 #[test]
405 fn test_qualified_name_deserialization() {
406 let json = r#"{
408 "tag": "RefNamed",
409 "contents": {
410 "qnNamespace": "cone",
411 "qnLocalName": "UUID"
412 }
413 }"#;
414 let type_ref: TypeRef = serde_json::from_str(json).unwrap();
415
416 if let TypeRef::RefNamed(qname) = type_ref {
417 assert_eq!(qname.qn_namespace, "cone");
418 assert_eq!(qname.qn_local_name, "UUID");
419 assert_eq!(qname.full_name(), "cone.UUID");
420 } else {
421 panic!("Expected RefNamed variant");
422 }
423 }
424
425 #[test]
426 fn test_type_ref_to_ts() {
427 let chat_event = TypeRef::RefNamed(QualifiedName {
428 qn_namespace: "".to_string(),
429 qn_local_name: "ChatEvent".to_string(),
430 });
431 assert_eq!(chat_event.to_ts(), "ChatEvent");
432
433 assert_eq!(TypeRef::RefPrimitive("string".to_string(), None).to_ts(), "string");
434 assert_eq!(TypeRef::RefPrimitive("string".to_string(), Some("uuid".to_string())).to_ts(), "string");
435 assert_eq!(TypeRef::RefPrimitive("integer".to_string(), Some("int64".to_string())).to_ts(), "number");
436
437 let node = TypeRef::RefNamed(QualifiedName {
438 qn_namespace: "".to_string(),
439 qn_local_name: "Node".to_string(),
440 });
441 assert_eq!(TypeRef::RefArray(Box::new(node)).to_ts(), "Node[]");
442
443 let pos = TypeRef::RefNamed(QualifiedName {
444 qn_namespace: "".to_string(),
445 qn_local_name: "Pos".to_string(),
446 });
447 assert_eq!(TypeRef::RefOptional(Box::new(pos)).to_ts(), "Pos | null");
448
449 assert_eq!(TypeRef::RefAny.to_ts(), "unknown"); assert_eq!(TypeRef::RefUnknown.to_ts(), "unknown"); }
452
453 #[test]
454 fn test_unknown_detection() {
455 assert!(!TypeRef::RefAny.is_unknown());
456 assert!(TypeRef::RefUnknown.is_unknown());
457
458 let foo = TypeRef::RefNamed(QualifiedName {
459 qn_namespace: "".to_string(),
460 qn_local_name: "Foo".to_string(),
461 });
462 assert!(!foo.is_unknown());
463
464 assert!(!TypeRef::RefAny.contains_unknown());
466 assert!(TypeRef::RefUnknown.contains_unknown());
467 assert!(TypeRef::RefArray(Box::new(TypeRef::RefUnknown)).contains_unknown());
468 assert!(!TypeRef::RefArray(Box::new(TypeRef::RefAny)).contains_unknown());
469 }
470
471 #[test]
479 fn test_method_def_bidir_type_deserialization() {
480 let json_no_field = r#"{
482 "mdName": "wizard",
483 "mdFullPath": "interactive.wizard",
484 "mdNamespace": "interactive",
485 "mdStreaming": true,
486 "mdParams": [],
487 "mdReturns": {"tag": "RefAny"}
488 }"#;
489 let method: MethodDef = serde_json::from_str(json_no_field).unwrap();
490 assert!(method.md_bidir_type.is_none(), "absent field should default to None");
491
492 let json_null = r#"{
494 "mdName": "wizard",
495 "mdFullPath": "interactive.wizard",
496 "mdNamespace": "interactive",
497 "mdStreaming": true,
498 "mdParams": [],
499 "mdReturns": {"tag": "RefAny"},
500 "mdBidirType": null
501 }"#;
502 let method: MethodDef = serde_json::from_str(json_null).unwrap();
503 assert!(method.md_bidir_type.is_none(), "null should deserialize to None");
504
505 let json_ref_any = r#"{
507 "mdName": "wizard",
508 "mdFullPath": "interactive.wizard",
509 "mdNamespace": "interactive",
510 "mdStreaming": true,
511 "mdParams": [],
512 "mdReturns": {"tag": "RefAny"},
513 "mdBidirType": {"tag": "RefAny"}
514 }"#;
515 let method: MethodDef = serde_json::from_str(json_ref_any).unwrap();
516 assert!(
517 matches!(method.md_bidir_type, Some(TypeRef::RefAny)),
518 "RefAny tag should deserialize to Some(RefAny)"
519 );
520
521 let json_ref_named = r#"{
523 "mdName": "wizard",
524 "mdFullPath": "interactive.wizard",
525 "mdNamespace": "interactive",
526 "mdStreaming": true,
527 "mdParams": [],
528 "mdReturns": {"tag": "RefAny"},
529 "mdBidirType": {
530 "tag": "RefNamed",
531 "contents": {
532 "qnNamespace": "interactive",
533 "qnLocalName": "WizardRequest"
534 }
535 }
536 }"#;
537 let method: MethodDef = serde_json::from_str(json_ref_named).unwrap();
538 if let Some(TypeRef::RefNamed(qn)) = method.md_bidir_type {
539 assert_eq!(qn.qn_namespace, "interactive");
540 assert_eq!(qn.qn_local_name, "WizardRequest");
541 } else {
542 panic!("Expected Some(RefNamed(...)) for typed bidirectional");
543 }
544 }
545}