1use std::collections::HashMap;
2
3use candid::CandidType;
4use serde::{Deserialize, Serialize};
5
6fn wrapped_key_word(name: &str) -> String {
8 if match name {
9 "bool" => true,
10 "nat" => true,
11 "int" => true,
12 "nat8" => true,
13 "nat16" => true,
14 "nat32" => true,
15 "nat64" => true,
16 "int8" => true,
17 "int16" => true,
18 "int32" => true,
19 "int64" => true,
20 "float32" => true,
21 "float64" => true,
22 "null" => true,
23 "text" => true,
24 "principal" => true,
25 "vec" => true,
26 "opt" => true,
27 "record" => true,
28 "variant" => true,
29 "unknown" => true,
31 "empty" => true,
32 "reserved" => true,
33 "func" => true,
34 "service" => true,
35 "rec" => true, _ => false,
37 } || name.contains(' ')
38 || name.contains('-')
39 || name.contains('\\')
40 {
41 format!("\"{}\"", name)
42 } else {
43 name.to_string()
44 }
45}
46
47#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq, Default)]
49pub struct WrappedCandidTypeName {
50 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
52 pub name: Option<String>,
53}
54
55impl WrappedCandidTypeName {
56 pub(super) fn from(name: Option<String>) -> Self {
57 Self { name }
58 }
59}
60
61#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
63pub struct WrappedCandidTypeSubtype {
64 #[serde(rename = "subtype")]
66 pub subtype: Box<WrappedCandidType>,
67
68 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
70 pub name: Option<String>,
71}
72
73#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
75pub struct WrappedCandidTypeRecord {
76 #[serde(rename = "subitems")]
78 pub subitems: Vec<(String, WrappedCandidType)>,
79
80 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
82 pub name: Option<String>,
83}
84
85impl WrappedCandidTypeRecord {
86 pub fn to_text(&self) -> String {
88 let Self { subitems, .. } = self;
89
90 if subitems.is_empty() {
91 return "record {}".to_string();
92 }
93
94 format!(
95 "record {{ {} }}",
96 subitems
97 .iter()
98 .map(|(name, subtype)| format!("{} : {}", wrapped_key_word(name), subtype.to_text()))
99 .collect::<Vec<_>>()
100 .join("; ")
101 )
102 }
103}
104
105#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
107pub struct WrappedCandidTypeVariant {
108 #[serde(rename = "subitems")]
110 pub subitems: Vec<(String, Option<WrappedCandidType>)>,
111
112 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
114 pub name: Option<String>,
115}
116
117impl WrappedCandidTypeVariant {
118 pub fn to_text(&self) -> String {
120 let Self { subitems, .. } = self;
121
122 if subitems.is_empty() {
123 return "variant {}".to_string();
124 }
125
126 format!(
127 "variant {{ {} }}",
128 subitems
129 .iter()
130 .map(|(name, subtype)| {
131 if let Some(subtype) = subtype {
132 format!("{} : {}", wrapped_key_word(name), subtype.to_text())
133 } else {
134 wrapped_key_word(name)
135 }
136 })
137 .collect::<Vec<_>>()
138 .join("; ")
139 )
140 }
141}
142
143#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
145pub struct WrappedCandidTypeTuple {
146 #[serde(rename = "subitems")]
148 pub subitems: Vec<WrappedCandidType>,
149
150 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
152 pub name: Option<String>,
153}
154
155impl WrappedCandidTypeTuple {
156 pub fn to_text(&self) -> String {
158 let Self { subitems, .. } = self;
159
160 if subitems.is_empty() {
161 return "record {}".to_string();
162 }
163
164 format!(
165 "record {{ {} }}",
166 subitems
167 .iter()
168 .map(|subtype| subtype.to_text())
169 .collect::<Vec<_>>()
170 .join("; ")
171 )
172 }
173}
174
175#[derive(Debug, Copy, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
177pub enum FunctionAnnotation {
178 #[serde(rename = "query")]
180 Query,
181 #[serde(rename = "composite_query")]
183 CompositeQuery,
184 #[serde(rename = "oneway")]
186 Oneway,
187}
188
189#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
191pub struct WrappedCandidTypeFunction {
192 #[serde(rename = "args", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
194 pub args: Vec<WrappedCandidType>,
195 #[serde(rename = "rets", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
197 pub rets: Vec<WrappedCandidType>,
198 #[serde(rename = "annotation", skip_serializing_if = "Option::is_none")]
200 pub annotation: Option<FunctionAnnotation>,
201
202 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
204 pub name: Option<String>,
205}
206
207impl WrappedCandidTypeFunction {
208 pub fn to_text(&self) -> String {
210 let Self {
211 args, rets, annotation, ..
212 } = self;
213
214 format!(
215 "func ({}) -> ({}){}",
216 args.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", "),
217 rets.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", "),
218 match annotation.as_ref() {
219 Some(annotation) => match annotation {
220 FunctionAnnotation::Query => " query",
221 FunctionAnnotation::CompositeQuery => " composite_query",
222 FunctionAnnotation::Oneway => " oneway",
223 },
224 None => "",
225 }
226 )
227 }
228}
229
230#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
232pub struct WrappedCandidTypeService {
233 #[serde(rename = "args", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
235 pub args: Vec<WrappedCandidType>,
236 #[serde(rename = "methods", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
238 pub methods: Vec<(String, WrappedCandidTypeFunction)>,
239
240 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
242 pub name: Option<String>,
243}
244
245impl WrappedCandidTypeService {
246 pub fn to_text(&self) -> String {
248 let Self { args, methods, .. } = self;
249
250 format!(
251 "service :{} {{\n{}\n}}",
252 if args.is_empty() {
253 "".to_string()
254 } else {
255 format!(
256 " ({}) ->",
257 args.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", ")
258 )
259 },
260 if methods.is_empty() {
261 "".to_string()
262 } else {
263 format!(
264 "\n{}\n",
265 methods
266 .iter()
267 .map(|(name, func)| format!(
268 " {} : {};",
269 wrapped_key_word(name),
270 func.to_text().trim_start_matches("func ")
271 ))
272 .collect::<Vec<_>>()
273 .join("\n")
274 )
275 }
276 )
277 }
278
279 pub fn to_methods(&self) -> HashMap<String, String> {
281 self.methods
282 .iter()
283 .map(|(method, candid)| {
284 (method.to_string(), {
285 let func = candid.to_text();
286 if let Some(func) = func.strip_prefix("func ") {
287 func.to_string()
288 } else {
289 func
290 }
291 })
292 })
293 .collect()
294 }
295}
296
297#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
299pub struct WrappedCandidTypeRecursion {
300 #[serde(rename = "ty")]
302 pub ty: Box<WrappedCandidType>,
303 #[serde(rename = "id")]
305 pub id: u32,
306
307 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
309 pub name: Option<String>,
310}
311
312impl WrappedCandidTypeRecursion {
313 pub fn to_text(&self) -> String {
315 let Self { ty, id, .. } = self;
316
317 format!("μrec_{}.{}", id, ty.to_text())
318 }
319}
320
321#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
323pub struct WrappedCandidTypeReference {
324 #[serde(rename = "id")]
326 pub id: u32,
327
328 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
330 pub name: Option<String>,
331}
332
333impl WrappedCandidTypeReference {
334 pub fn to_text(&self) -> String {
336 let Self { id, .. } = self;
337
338 format!("rec_{}", id,)
339 }
340}
341
342#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
344pub enum WrappedCandidType {
345 #[serde(rename = "bool")]
350 Bool(WrappedCandidTypeName),
351 #[serde(rename = "nat")]
355 Nat(WrappedCandidTypeName),
356 #[serde(rename = "int")]
360 Int(WrappedCandidTypeName),
361 #[serde(rename = "nat8")]
365 Nat8(WrappedCandidTypeName),
366 #[serde(rename = "nat16")]
370 Nat16(WrappedCandidTypeName),
371 #[serde(rename = "nat32")]
375 Nat32(WrappedCandidTypeName),
376 #[serde(rename = "nat64")]
380 Nat64(WrappedCandidTypeName),
381 #[serde(rename = "int8")]
385 Int8(WrappedCandidTypeName),
386 #[serde(rename = "int16")]
390 Int16(WrappedCandidTypeName),
391 #[serde(rename = "int32")]
395 Int32(WrappedCandidTypeName),
396 #[serde(rename = "int64")]
400 Int64(WrappedCandidTypeName),
401 #[serde(rename = "float32")]
405 Float32(WrappedCandidTypeName),
406 #[serde(rename = "float64")]
410 Float64(WrappedCandidTypeName),
411 #[serde(rename = "null")]
415 Null(WrappedCandidTypeName),
416 #[serde(rename = "text")]
420 Text(WrappedCandidTypeName),
421 #[serde(rename = "principal")]
425 Principal(WrappedCandidTypeName),
426 #[serde(rename = "vec")]
434 Vec(WrappedCandidTypeSubtype),
435 #[serde(rename = "opt")]
439 Opt(WrappedCandidTypeSubtype),
440 #[serde(rename = "record")]
445 Record(WrappedCandidTypeRecord),
446 #[serde(rename = "variant")]
450 Variant(WrappedCandidTypeVariant),
451 #[serde(rename = "tuple")]
455 Tuple(WrappedCandidTypeTuple),
456 #[serde(rename = "unknown")]
460 Unknown(WrappedCandidTypeName),
461 #[serde(rename = "empty")]
465 Empty(WrappedCandidTypeName), #[serde(rename = "reserved")]
470 Reserved(WrappedCandidTypeName), #[serde(rename = "func")]
475 Func(WrappedCandidTypeFunction),
476 #[serde(rename = "service")]
480 Service(WrappedCandidTypeService),
481 #[serde(rename = "rec")]
484 Rec(WrappedCandidTypeRecursion), #[serde(rename = "ref")]
487 Reference(WrappedCandidTypeReference), }
489
490impl WrappedCandidType {
491 pub fn to_text(&self) -> String {
493 match self {
494 Self::Bool(_) => String::from("bool"),
495 Self::Nat(_) => String::from("nat"),
496 Self::Int(_) => String::from("int"),
497 Self::Nat8(_) => String::from("nat8"),
498 Self::Nat16(_) => String::from("nat16"),
499 Self::Nat32(_) => String::from("nat32"),
500 Self::Nat64(_) => String::from("nat64"),
501 Self::Int8(_) => String::from("int8"),
502 Self::Int16(_) => String::from("int16"),
503 Self::Int32(_) => String::from("int32"),
504 Self::Int64(_) => String::from("int64"),
505 Self::Float32(_) => String::from("float32"),
506 Self::Float64(_) => String::from("float64"),
507 Self::Null(_) => String::from("null"),
508 Self::Text(_) => String::from("text"),
509 Self::Principal(_) => String::from("principal"),
510 Self::Vec(WrappedCandidTypeSubtype { subtype, .. }) => {
511 format!("vec {}", subtype.to_text())
512 }
513 Self::Opt(WrappedCandidTypeSubtype { subtype, .. }) => {
514 format!("opt {}", subtype.to_text())
515 }
516 Self::Record(record) => record.to_text(),
517 Self::Variant(variant) => variant.to_text(),
518 Self::Tuple(tuple) => tuple.to_text(),
519 Self::Unknown(_) => String::from("unknown"),
520 Self::Empty(_) => String::from("empty"),
521 Self::Reserved(_) => String::from("reserved"),
522 Self::Func(func) => func.to_text(),
523 Self::Service(service) => service.to_text(),
524 Self::Rec(recursion) => recursion.to_text(),
525 Self::Reference(reference) => reference.to_text(),
526 }
527 }
528}
529
530#[cfg(test)]
531mod serialization_tests {
532 use ciborium::value::Value;
533 use serde::Serialize;
534
535 use super::*;
536
537 #[derive(Serialize)]
538 struct RecordPayload {
539 subitems: Vec<(String, WrappedCandidType)>,
540 name: Option<String>,
541 }
542
543 #[derive(Serialize)]
544 struct VariantPayload {
545 subitems: Vec<(String, Option<WrappedCandidType>)>,
546 name: Option<String>,
547 }
548
549 #[derive(Serialize)]
550 struct TuplePayload {
551 subitems: Vec<WrappedCandidType>,
552 name: Option<String>,
553 }
554
555 #[derive(Serialize)]
556 struct LegacyFunction {
557 args: Vec<WrappedCandidType>,
558 rets: Vec<WrappedCandidType>,
559 annotation: Option<FunctionAnnotation>,
560 name: Option<String>,
561 }
562
563 #[derive(Serialize)]
564 struct LegacyRecursion {
565 ty: Box<WrappedCandidType>,
566 id: u32,
567 name: Option<String>,
568 }
569
570 fn nat() -> WrappedCandidType {
571 WrappedCandidType::Nat(WrappedCandidTypeName::default())
572 }
573
574 fn round_trip_legacy<Legacy: Serialize, Current: serde::de::DeserializeOwned>(legacy: &Legacy) -> Current {
575 let mut cbor = Vec::new();
576 ciborium::ser::into_writer(legacy, &mut cbor).unwrap();
577 ciborium::de::from_reader(cbor.as_slice()).unwrap()
578 }
579
580 fn serialized_keys(value: &impl Serialize) -> Vec<String> {
581 let mut cbor = Vec::new();
582 ciborium::ser::into_writer(value, &mut cbor).unwrap();
583 let value: Value = ciborium::de::from_reader(cbor.as_slice()).unwrap();
584 let Value::Map(entries) = value else {
585 panic!("expected a CBOR map")
586 };
587 entries
588 .into_iter()
589 .filter_map(|(key, _)| match key {
590 Value::Text(key) => Some(key),
591 _ => None,
592 })
593 .collect()
594 }
595
596 #[test]
597 fn deserializes_legacy_candid_type_fields_and_serializes_current_names() {
598 let record: WrappedCandidTypeRecord = round_trip_legacy(&RecordPayload {
599 subitems: vec![("value".to_string(), nat())],
600 name: None,
601 });
602 assert_eq!(record.subitems.len(), 1);
603 assert_eq!(serialized_keys(&record), vec!["subitems"]);
604
605 let variant: WrappedCandidTypeVariant = round_trip_legacy(&VariantPayload {
606 subitems: vec![("ok".to_string(), Some(nat()))],
607 name: None,
608 });
609 assert_eq!(variant.subitems.len(), 1);
610 assert_eq!(serialized_keys(&variant), vec!["subitems"]);
611
612 let tuple: WrappedCandidTypeTuple = round_trip_legacy(&TuplePayload {
613 subitems: vec![nat()],
614 name: None,
615 });
616 assert_eq!(tuple.subitems.len(), 1);
617 assert_eq!(serialized_keys(&tuple), vec!["subitems"]);
618
619 let function: WrappedCandidTypeFunction = round_trip_legacy(&LegacyFunction {
620 args: Vec::new(),
621 rets: vec![nat()],
622 annotation: None,
623 name: None,
624 });
625 assert_eq!(function.rets.len(), 1);
626 assert_eq!(serialized_keys(&function), vec!["rets"]);
627
628 let recursion: WrappedCandidTypeRecursion = round_trip_legacy(&LegacyRecursion {
629 ty: Box::new(nat()),
630 id: 1,
631 name: None,
632 });
633 assert!(matches!(*recursion.ty, WrappedCandidType::Nat(_)));
634 assert_eq!(serialized_keys(&recursion), vec!["ty", "id"]);
635 }
636}