use std::collections::HashMap;
use candid::CandidType;
use serde::{Deserialize, Serialize};
fn wrapped_key_word(name: &str) -> String {
if match name {
"bool" => true,
"nat" => true,
"int" => true,
"nat8" => true,
"nat16" => true,
"nat32" => true,
"nat64" => true,
"int8" => true,
"int16" => true,
"int32" => true,
"int64" => true,
"float32" => true,
"float64" => true,
"null" => true,
"text" => true,
"principal" => true,
"vec" => true,
"opt" => true,
"record" => true,
"variant" => true,
"unknown" => true,
"empty" => true,
"reserved" => true,
"func" => true,
"service" => true,
"rec" => true, _ => false,
} || name.contains(' ')
|| name.contains('-')
|| name.contains('\\')
{
format!("\"{}\"", name)
} else {
name.to_string()
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq, Default)]
pub struct WrappedCandidTypeName {
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeName {
pub(super) fn from(name: Option<String>) -> Self {
Self { name }
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeSubtype {
#[serde(rename = "subtype")]
pub subtype: Box<WrappedCandidType>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeRecord {
#[serde(rename = "subitems")]
pub subitems: Vec<(String, WrappedCandidType)>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeRecord {
pub fn to_text(&self) -> String {
let Self { subitems, .. } = self;
if subitems.is_empty() {
return "record {}".to_string();
}
format!(
"record {{ {} }}",
subitems
.iter()
.map(|(name, subtype)| format!("{} : {}", wrapped_key_word(name), subtype.to_text()))
.collect::<Vec<_>>()
.join("; ")
)
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeVariant {
#[serde(rename = "subitems")]
pub subitems: Vec<(String, Option<WrappedCandidType>)>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeVariant {
pub fn to_text(&self) -> String {
let Self { subitems, .. } = self;
if subitems.is_empty() {
return "variant {}".to_string();
}
format!(
"variant {{ {} }}",
subitems
.iter()
.map(|(name, subtype)| {
if let Some(subtype) = subtype {
format!("{} : {}", wrapped_key_word(name), subtype.to_text())
} else {
wrapped_key_word(name)
}
})
.collect::<Vec<_>>()
.join("; ")
)
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeTuple {
#[serde(rename = "subitems")]
pub subitems: Vec<WrappedCandidType>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeTuple {
pub fn to_text(&self) -> String {
let Self { subitems, .. } = self;
if subitems.is_empty() {
return "record {}".to_string();
}
format!(
"record {{ {} }}",
subitems
.iter()
.map(|subtype| subtype.to_text())
.collect::<Vec<_>>()
.join("; ")
)
}
}
#[derive(Debug, Copy, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub enum FunctionAnnotation {
#[serde(rename = "query")]
Query,
#[serde(rename = "composite_query")]
CompositeQuery,
#[serde(rename = "oneway")]
Oneway,
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeFunction {
#[serde(rename = "args", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub args: Vec<WrappedCandidType>,
#[serde(rename = "rets", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub rets: Vec<WrappedCandidType>,
#[serde(rename = "annotation", skip_serializing_if = "Option::is_none")]
pub annotation: Option<FunctionAnnotation>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeFunction {
pub fn to_text(&self) -> String {
let Self {
args, rets, annotation, ..
} = self;
format!(
"func ({}) -> ({}){}",
args.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", "),
rets.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", "),
match annotation.as_ref() {
Some(annotation) => match annotation {
FunctionAnnotation::Query => " query",
FunctionAnnotation::CompositeQuery => " composite_query",
FunctionAnnotation::Oneway => " oneway",
},
None => "",
}
)
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeService {
#[serde(rename = "args", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub args: Vec<WrappedCandidType>,
#[serde(rename = "methods", skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub methods: Vec<(String, WrappedCandidTypeFunction)>,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeService {
pub fn to_text(&self) -> String {
let Self { args, methods, .. } = self;
format!(
"service :{} {{\n{}\n}}",
if args.is_empty() {
"".to_string()
} else {
format!(
" ({}) ->",
args.iter().map(|t| t.to_text()).collect::<Vec<_>>().join(", ")
)
},
if methods.is_empty() {
"".to_string()
} else {
format!(
"\n{}\n",
methods
.iter()
.map(|(name, func)| format!(
" {} : {};",
wrapped_key_word(name),
func.to_text().trim_start_matches("func ")
))
.collect::<Vec<_>>()
.join("\n")
)
}
)
}
pub fn to_methods(&self) -> HashMap<String, String> {
self.methods
.iter()
.map(|(method, candid)| {
(method.to_string(), {
let func = candid.to_text();
if let Some(func) = func.strip_prefix("func ") {
func.to_string()
} else {
func
}
})
})
.collect()
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeRecursion {
#[serde(rename = "ty")]
pub ty: Box<WrappedCandidType>,
#[serde(rename = "id")]
pub id: u32,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeRecursion {
pub fn to_text(&self) -> String {
let Self { ty, id, .. } = self;
format!("μrec_{}.{}", id, ty.to_text())
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub struct WrappedCandidTypeReference {
#[serde(rename = "id")]
pub id: u32,
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl WrappedCandidTypeReference {
pub fn to_text(&self) -> String {
let Self { id, .. } = self;
format!("rec_{}", id,)
}
}
#[derive(Debug, Clone, CandidType, Serialize, Deserialize, Eq, PartialEq)]
pub enum WrappedCandidType {
#[serde(rename = "bool")]
Bool(WrappedCandidTypeName),
#[serde(rename = "nat")]
Nat(WrappedCandidTypeName),
#[serde(rename = "int")]
Int(WrappedCandidTypeName),
#[serde(rename = "nat8")]
Nat8(WrappedCandidTypeName),
#[serde(rename = "nat16")]
Nat16(WrappedCandidTypeName),
#[serde(rename = "nat32")]
Nat32(WrappedCandidTypeName),
#[serde(rename = "nat64")]
Nat64(WrappedCandidTypeName),
#[serde(rename = "int8")]
Int8(WrappedCandidTypeName),
#[serde(rename = "int16")]
Int16(WrappedCandidTypeName),
#[serde(rename = "int32")]
Int32(WrappedCandidTypeName),
#[serde(rename = "int64")]
Int64(WrappedCandidTypeName),
#[serde(rename = "float32")]
Float32(WrappedCandidTypeName),
#[serde(rename = "float64")]
Float64(WrappedCandidTypeName),
#[serde(rename = "null")]
Null(WrappedCandidTypeName),
#[serde(rename = "text")]
Text(WrappedCandidTypeName),
#[serde(rename = "principal")]
Principal(WrappedCandidTypeName),
#[serde(rename = "vec")]
Vec(WrappedCandidTypeSubtype),
#[serde(rename = "opt")]
Opt(WrappedCandidTypeSubtype),
#[serde(rename = "record")]
Record(WrappedCandidTypeRecord),
#[serde(rename = "variant")]
Variant(WrappedCandidTypeVariant),
#[serde(rename = "tuple")]
Tuple(WrappedCandidTypeTuple),
#[serde(rename = "unknown")]
Unknown(WrappedCandidTypeName),
#[serde(rename = "empty")]
Empty(WrappedCandidTypeName), #[serde(rename = "reserved")]
Reserved(WrappedCandidTypeName), #[serde(rename = "func")]
Func(WrappedCandidTypeFunction),
#[serde(rename = "service")]
Service(WrappedCandidTypeService),
#[serde(rename = "rec")]
Rec(WrappedCandidTypeRecursion), #[serde(rename = "ref")]
Reference(WrappedCandidTypeReference), }
impl WrappedCandidType {
pub fn to_text(&self) -> String {
match self {
Self::Bool(_) => String::from("bool"),
Self::Nat(_) => String::from("nat"),
Self::Int(_) => String::from("int"),
Self::Nat8(_) => String::from("nat8"),
Self::Nat16(_) => String::from("nat16"),
Self::Nat32(_) => String::from("nat32"),
Self::Nat64(_) => String::from("nat64"),
Self::Int8(_) => String::from("int8"),
Self::Int16(_) => String::from("int16"),
Self::Int32(_) => String::from("int32"),
Self::Int64(_) => String::from("int64"),
Self::Float32(_) => String::from("float32"),
Self::Float64(_) => String::from("float64"),
Self::Null(_) => String::from("null"),
Self::Text(_) => String::from("text"),
Self::Principal(_) => String::from("principal"),
Self::Vec(WrappedCandidTypeSubtype { subtype, .. }) => {
format!("vec {}", subtype.to_text())
}
Self::Opt(WrappedCandidTypeSubtype { subtype, .. }) => {
format!("opt {}", subtype.to_text())
}
Self::Record(record) => record.to_text(),
Self::Variant(variant) => variant.to_text(),
Self::Tuple(tuple) => tuple.to_text(),
Self::Unknown(_) => String::from("unknown"),
Self::Empty(_) => String::from("empty"),
Self::Reserved(_) => String::from("reserved"),
Self::Func(func) => func.to_text(),
Self::Service(service) => service.to_text(),
Self::Rec(recursion) => recursion.to_text(),
Self::Reference(reference) => reference.to_text(),
}
}
}
#[cfg(test)]
mod serialization_tests {
use ciborium::value::Value;
use serde::Serialize;
use super::*;
#[derive(Serialize)]
struct RecordPayload {
subitems: Vec<(String, WrappedCandidType)>,
name: Option<String>,
}
#[derive(Serialize)]
struct VariantPayload {
subitems: Vec<(String, Option<WrappedCandidType>)>,
name: Option<String>,
}
#[derive(Serialize)]
struct TuplePayload {
subitems: Vec<WrappedCandidType>,
name: Option<String>,
}
#[derive(Serialize)]
struct LegacyFunction {
args: Vec<WrappedCandidType>,
rets: Vec<WrappedCandidType>,
annotation: Option<FunctionAnnotation>,
name: Option<String>,
}
#[derive(Serialize)]
struct LegacyRecursion {
ty: Box<WrappedCandidType>,
id: u32,
name: Option<String>,
}
fn nat() -> WrappedCandidType {
WrappedCandidType::Nat(WrappedCandidTypeName::default())
}
fn round_trip_legacy<Legacy: Serialize, Current: serde::de::DeserializeOwned>(legacy: &Legacy) -> Current {
let mut cbor = Vec::new();
ciborium::ser::into_writer(legacy, &mut cbor).unwrap();
ciborium::de::from_reader(cbor.as_slice()).unwrap()
}
fn serialized_keys(value: &impl Serialize) -> Vec<String> {
let mut cbor = Vec::new();
ciborium::ser::into_writer(value, &mut cbor).unwrap();
let value: Value = ciborium::de::from_reader(cbor.as_slice()).unwrap();
let Value::Map(entries) = value else {
panic!("expected a CBOR map")
};
entries
.into_iter()
.filter_map(|(key, _)| match key {
Value::Text(key) => Some(key),
_ => None,
})
.collect()
}
#[test]
fn deserializes_legacy_candid_type_fields_and_serializes_current_names() {
let record: WrappedCandidTypeRecord = round_trip_legacy(&RecordPayload {
subitems: vec![("value".to_string(), nat())],
name: None,
});
assert_eq!(record.subitems.len(), 1);
assert_eq!(serialized_keys(&record), vec!["subitems"]);
let variant: WrappedCandidTypeVariant = round_trip_legacy(&VariantPayload {
subitems: vec![("ok".to_string(), Some(nat()))],
name: None,
});
assert_eq!(variant.subitems.len(), 1);
assert_eq!(serialized_keys(&variant), vec!["subitems"]);
let tuple: WrappedCandidTypeTuple = round_trip_legacy(&TuplePayload {
subitems: vec![nat()],
name: None,
});
assert_eq!(tuple.subitems.len(), 1);
assert_eq!(serialized_keys(&tuple), vec!["subitems"]);
let function: WrappedCandidTypeFunction = round_trip_legacy(&LegacyFunction {
args: Vec::new(),
rets: vec![nat()],
annotation: None,
name: None,
});
assert_eq!(function.rets.len(), 1);
assert_eq!(serialized_keys(&function), vec!["rets"]);
let recursion: WrappedCandidTypeRecursion = round_trip_legacy(&LegacyRecursion {
ty: Box::new(nat()),
id: 1,
name: None,
});
assert!(matches!(*recursion.ty, WrappedCandidType::Nat(_)));
assert_eq!(serialized_keys(&recursion), vec!["ty", "id"]);
}
}