From 66e1bcf259c15e0ac83a73259c76632f28c10360 Mon Sep 17 00:00:00 2001 From: Tomi Jaga Date: Mon, 11 Sep 2023 20:47:33 -0400 Subject: [PATCH] Optimizing bfs and merge algorithm in Candid.encode() --- benchmarks/serde.bench.mo | 86 ++ dfx.json | 6 + mops.toml | 5 +- src/Candid/Blob/Decoder.mo | 5 +- src/Candid/Blob/Encoder.mo | 511 +++++++---- src/Utils.mo | 15 + tests/Candid.Test.mo | 1738 ++++++++++++++++++------------------ 7 files changed, 1311 insertions(+), 1055 deletions(-) create mode 100644 benchmarks/serde.bench.mo diff --git a/benchmarks/serde.bench.mo b/benchmarks/serde.bench.mo new file mode 100644 index 0000000..d97a514 --- /dev/null +++ b/benchmarks/serde.bench.mo @@ -0,0 +1,86 @@ +import Prim "mo:prim"; +import Cycles "mo:base/ExperimentalCycles"; +import IC "mo:base/ExperimentalInternetComputer"; +import Debug "mo:base/Debug"; +import Iter "mo:base/Iter"; +import Serde "../src"; + +actor { + type Candid = Serde.Candid; + let { Candid } = Serde; + + type Permission = { + #read : [Text]; + #write : [Text]; + #read_all : (); + #write_all : (); + #admin : (); + }; + + type User = { + name : Text; + age : Nat; + permission : Permission; + }; + + type Record = { + group : Text; + users : ?[User]; + }; + + public query func cycles() : async Nat { Cycles.balance()}; + + public query func deserialize(n: Nat) : async (Nat64, Nat, Nat, Nat) { + let init_cycles = Cycles.balance(); + + let init_heap = Prim.rts_heap_size(); + let init_memory = Prim.rts_memory_size(); + + let calls = IC.countInstructions( + func() { + + let admin_record_candid : Candid = #Record([ + ("group", #Text("admins")), + ("users", #Option(#Array([#Record([("age", #Nat(32)), ("name", #Text("John")), ("permission", #Variant("admin", #Null))])]))), + ]); + + let user_record_candid : Candid = #Record([ + ("group", #Text("users")), + ("users", #Option(#Array([#Record([("age", #Nat(28)), ("name", #Text("Ali")), ("permission", #Variant("read_all", #Null))]), #Record([("age", #Nat(40)), ("name", #Text("James")), ("permission", #Variant("write_all", #Null))])]))), + ]); + + let empty_record_candid : Candid = #Record([ + ("group", #Text("empty")), + ("users", #Option(#Array([]))), + ]); + + let null_record_candid : Candid = #Record([ + ("group", #Text("null")), + ("users", #Option(#Null)), + ]); + + let base_record_candid : Candid = #Record([ + ("group", #Text("base")), + ("users", #Option(#Array([#Record([("age", #Nat(32)), ("name", #Text("Henry")), ("permission", #Variant("read", #Array([#Text("posts"), #Text("comments")])))]), #Record([("age", #Nat(32)), ("name", #Text("Steven")), ("permission", #Variant("write", #Array([#Text("posts"), #Text("comments")])))])]))), + ]); + + let records : Candid = #Array([ + null_record_candid, + empty_record_candid, + admin_record_candid, + user_record_candid, + base_record_candid, + ]); + + for (_ in Iter.range(1, n)){ + let #ok(blob) = Candid.encodeOne(records, null); + let motoko : ?[Record] = from_candid (blob); + Debug.print(debug_show (motoko)); + }; + } + ); + + (calls, Prim.rts_heap_size() - init_heap, Prim.rts_memory_size() - init_memory, init_cycles - Cycles.balance()) + }; + +}; diff --git a/dfx.json b/dfx.json index 9f142cb..ddbad29 100644 --- a/dfx.json +++ b/dfx.json @@ -6,6 +6,12 @@ "args": "" } }, + "canisters": { + "benchmarks": { + "type": "motoko", + "main": "benchmarks/serde.bench.mo" + } + }, "networks": { "local": { "bind": "127.0.0.1:8000", diff --git a/mops.toml b/mops.toml index 320fdad..4abe098 100644 --- a/mops.toml +++ b/mops.toml @@ -1,6 +1,6 @@ [package] name = "serde" -version = "1.1.0" +version = "2.0.0" description = "A serialisation and deserialisation library for Motoko." repository = "https://github.com/NatLabs/serde" keywords = [ "json", "candid", "parser", "urlencoded", "serialization" ] @@ -11,5 +11,4 @@ itertools = "0.1.2" candid = "1.0.2" xtended-numbers = "0.2.1" json = "https://github.com/aviate-labs/json.mo#v0.2.0" -parser-combinators = "https://github.com/aviate-labs/parser-combinators.mo#v0.1.2" -buffer-deque = "0.0.1" \ No newline at end of file +parser-combinators = "https://github.com/aviate-labs/parser-combinators.mo#v0.1.2" \ No newline at end of file diff --git a/src/Candid/Blob/Decoder.mo b/src/Candid/Blob/Decoder.mo index abf3a3e..94a56c7 100644 --- a/src/Candid/Blob/Decoder.mo +++ b/src/Candid/Blob/Decoder.mo @@ -266,10 +266,7 @@ module { case (x) { return #err( - " - Serde Decoding Error from fromArg() fn in Candid/Blob/Decoder.mo - Error Log: Could not match '" # debug_show (x) # "' type to any case - " + "\nSerde Decoding Error from fromArg() fn in Candid/Blob/Decoder.mo\n\tError Log: Could not match '" # debug_show (x) # "' type to any case" ); }; }; diff --git a/src/Candid/Blob/Encoder.mo b/src/Candid/Blob/Encoder.mo index ade2c30..b53c943 100644 --- a/src/Candid/Blob/Encoder.mo +++ b/src/Candid/Blob/Encoder.mo @@ -3,6 +3,7 @@ import Blob "mo:base/Blob"; import Buffer "mo:base/Buffer"; import Debug "mo:base/Debug"; import Result "mo:base/Result"; +import Nat "mo:base/Nat"; import Iter "mo:base/Iter"; import Prelude "mo:base/Prelude"; import Text "mo:base/Text"; @@ -13,7 +14,6 @@ import Arg "mo:candid/Arg"; import Value "mo:candid/Value"; import Type "mo:candid/Type"; import Tag "mo:candid/Tag"; -import BufferDeque "mo:buffer-deque/BufferDeque"; import Itertools "mo:itertools/Iter"; import PeekableIter "mo:itertools/PeekableIter"; @@ -22,6 +22,7 @@ import U "../../Utils"; import TrieMap "mo:base/TrieMap"; import Utils "../../Utils"; import Order "mo:base/Order"; +import Func "mo:base/Func"; module { type Arg = Arg.Arg; @@ -32,7 +33,6 @@ module { type RecordFieldValue = Value.RecordFieldValue; type TrieMap = TrieMap.TrieMap; type Result = Result.Result; - type BufferDeque = BufferDeque.BufferDeque; type Buffer = Buffer.Buffer; type Candid = T.Candid; @@ -58,102 +58,95 @@ module { encode([candid], options); }; + type UpdatedTypeNode = { + var type_ : UpdatedType; + height : Nat; + parent_index : Nat; + var children : ?{ + start : Nat; + n : Nat; + }; + tag : Tag; + }; + + type TypeNode = { + var type_ : Type; + height : Nat; + parent_index : Nat; + var children : ?{ + start : Nat; + n : Nat; + }; + tag : Tag; + }; + public func toArgs(candid_values : [Candid], renaming_map : TrieMap) : Result<[Arg], Text> { let buffer = Buffer.Buffer(candid_values.size()); for (candid in candid_values.vals()) { - let type_res = toArgTypeWithHeight(candid, renaming_map); - let #ok(type_, height) = type_res else return Utils.send_error(type_res); + let updated_arg_type = toUpdatedArgType(candid, renaming_map); + + let rows = Buffer.Buffer<[UpdatedTypeNode]>(8); + let node : UpdatedTypeNode = { + var type_ = updated_arg_type; + height = 0; + parent_index = 0; + var children = null; + tag = #name(""); + }; + + rows.add([node]); + order_types_by_height_bfs(rows); + + let merged_type = merge_variants_in_array_type(rows); let value_res = toArgValue(candid, renaming_map); let #ok(value) = value_res else return Utils.send_error(value_res); - buffer.add({ type_; value }); + buffer.add({ type_ = merged_type; value }); }; #ok(Buffer.toArray(buffer)); }; - func toArgType(candid : Candid, renaming_map : TrieMap) : Result { - let arg_type : Type = switch (candid) { - case (#Nat(_)) #nat; - case (#Nat8(_)) #nat8; - case (#Nat16(_)) #nat16; - case (#Nat32(_)) #nat32; - case (#Nat64(_)) #nat64; - - case (#Int(_)) #int; - case (#Int8(_)) #int8; - case (#Int16(_)) #int16; - case (#Int32(_)) #int32; - case (#Int64(_)) #int64; - - case (#Float(_)) #float64; - - case (#Bool(_)) #bool; - - case (#Principal(_)) #principal; - - case (#Text(_)) #text; - case (#Blob(_)) #vector(#nat8); - - case (#Null) #null_; - - case (#Option(optType)) { - let res = toArgType(optType, renaming_map); - let #ok(type_) = res else return Utils.send_error(res); - #opt(type_); - }; - case (#Array(arr)) { - if (arr.size() > 0) { - let res = toArgType(arr[0], renaming_map); - let #ok(vector_type) = res else return Utils.send_error(res); - #vector(vector_type); - } else { - #vector(#empty); - }; - }; - - case (#Record(records)) { - let newRecords = Buffer.Buffer(records.size()); - - for ((key, val) in records.vals()) { - let renamed_key = get_renamed_key(renaming_map, key); - - let res = toArgType(val, renaming_map); - let #ok(type_) = res else return Utils.send_error(res); + type UpdatedKeyValuePair = { tag : Tag; type_ : UpdatedType }; + + type UpdatedCompoundType = { + #opt : UpdatedType; + #vector : [UpdatedType]; + #record : [UpdatedKeyValuePair]; + #variant : [UpdatedKeyValuePair]; + // #func_ : Type.FuncType; + // #service : Type.ServiceType; + // #recursiveType : { id : Text; type_ : UpdatedType }; + // #recursiveReference : Text; + }; - newRecords.add({ - tag = #name(renamed_key); - type_; - }); - }; + type UpdatedType = Type.PrimitiveType or UpdatedCompoundType; - #record(Buffer.toArray(newRecords)); - }; + func extract_top_level_type(type_ : UpdatedType) : (UpdatedType) { + switch (type_) { + case (#bool(_)) #bool; - case (#Variant((key, val))) { - let renamed_key = get_renamed_key(renaming_map, key); + case (#principal(_)) #principal; - let res = toArgType(val, renaming_map); - let #ok(type_) = res else return Utils.send_error(res); + case (#text(_)) #text; - #variant([{ tag = #name(renamed_key); type_ }]); - }; + case (#null_) return #null_; + case (#empty) return #empty; - case (#Empty) #empty; + case (#opt(_)) #opt(#empty); + case (#vector(_)) #vector([]); + case (#record(_)) #record([]); + case (#variant(_)) #variant([]); + case (x) x; }; - - #ok(arg_type); }; - // Include the height of the tree in the type - // to choose the best for data that might have different heights like optional types - // types like #Null and #Empty should have height 0 - func toArgTypeWithHeight(candid : Candid, renaming_map : TrieMap) : Result<(Type, height : Nat), Text> { + func toUpdatedArgType(candid : Candid, renaming_map : TrieMap) : UpdatedType { var curr_height = 0; - let arg_type : Type = switch (candid) { + let arg_type : UpdatedType = switch (candid) { case (#Nat(_)) #nat; case (#Nat8(_)) #nat8; case (#Nat16(_)) #nat16; @@ -173,66 +166,31 @@ module { case (#Principal(_)) #principal; case (#Text(_)) #text; - case (#Blob(_)) #vector(#nat8); + case (#Blob(_)) #vector([#nat8]); - case (#Null) return #ok(#null_, 0); - case (#Empty) return #ok(#empty, 0); + case (#Null) #null_; + case (#Empty) #empty; case (#Option(optType)) { - let res = toArgTypeWithHeight(optType, renaming_map); - let #ok(type_, height) = res else return Utils.send_error(res); - curr_height := height; - + let type_ = toUpdatedArgType(optType, renaming_map); #opt(type_); }; case (#Array(arr)) { - if (arr.size() == 0) return #ok(#vector(#empty), 1); - - let max = { - var height = 0; - var type_ : Type = #empty; - }; - - let buffer = Buffer.Buffer(arr.size()); - for ((id, item) in Itertools.enumerate(arr.vals())) { - let res = toArgTypeWithHeight(item, renaming_map); - let #ok(type_, height) = res else return Utils.send_error(res); - buffer.add((type_, id, #name(""))); - - if (height > max.height) { - max.height := height; - max.type_ := type_; - }; - }; - - buffer.sort(func ((a, _, _), (b, _, _)) : Order.Order = if (a == max.type_) { #less } else { #greater }); - - let rows = Buffer.Buffer<[TypeInfo]>(8); - rows.add(Buffer.toArray(buffer)); - Debug.print("rows before bfs: \n" # debug_show Buffer.toArray(rows)); - - bfs_get_types_by_height(rows); - Debug.print("rows after bfs: \n" # debug_show Buffer.toArray(rows)); - - let merged_type = merge_variants_in_array_type(rows); - - Debug.print("rows: \n" # debug_show Buffer.toArray(rows)); - - curr_height := max.height; + let vec_types = Array.map( + arr, + func(item : Candid) : UpdatedType = toUpdatedArgType(item, renaming_map), + ); - #vector(merged_type); + #vector(vec_types); }; case (#Record(records)) { - let newRecords = Buffer.Buffer(records.size()); + let newRecords : Buffer = Buffer.Buffer(records.size()); for ((key, val) in records.vals()) { let renamed_key = get_renamed_key(renaming_map, key); - let res = toArgTypeWithHeight(val, renaming_map); - let #ok(type_, height) = res else return Utils.send_error(res); - - curr_height := height; + let type_ = toUpdatedArgType(val, renaming_map); newRecords.add({ tag = #name(renamed_key); @@ -245,65 +203,179 @@ module { case (#Variant((key, val))) { let renamed_key = get_renamed_key(renaming_map, key); + let type_ = toUpdatedArgType(val, renaming_map); + #variant([{ tag = #name(renamed_key); type_ }]); + }; - let res = toArgTypeWithHeight(val, renaming_map); - let #ok(type_, height) = res else return Utils.send_error(res); + }; - curr_height := height; - #variant([{ tag = #name(renamed_key); type_ }]); + arg_type; + }; + + func updated_type_to_arg_type(updated_type : UpdatedType, vec_index : ?Nat) : Type { + switch (updated_type, vec_index) { + case (#vector(vec_types), ?vec_index) #vector(updated_type_to_arg_type(vec_types[vec_index], null)); + case (#vector(vec_types), _) #vector(updated_type_to_arg_type(vec_types[0], null)); + case (#opt(opt_type), _) #opt(updated_type_to_arg_type(opt_type, null)); + case (#record(record_types), _) { + let new_record_types = Array.map( + record_types, + func({ type_; tag } : UpdatedKeyValuePair) : RecordFieldType = { + type_ = updated_type_to_arg_type(type_, null); + tag; + }, + ); + + #record(new_record_types); + }; + case (#variant(variant_types), _) { + let new_variant_types = Array.map( + variant_types, + func({ type_; tag } : UpdatedKeyValuePair) : RecordFieldType = { + type_ = updated_type_to_arg_type(type_, null); + tag; + }, + ); + + #variant(new_variant_types); }; + case (#reserved, _) #reserved; + case (#null_, _) #null_; + case (#empty, _) #empty; + case (#bool, _) #bool; + case (#principal, _) #principal; + case (#text, _) #text; + case (#nat, _) #nat; + case (#nat8, _) #nat8; + case (#nat16, _) #nat16; + case (#nat32, _) #nat32; + case (#nat64, _) #nat64; + case (#int, _) #int; + case (#int8, _) #int8; + case (#int16, _) #int16; + case (#int32, _) #int32; + case (#int64, _) #int64; + case (#float32, _) #float32; + case (#float64, _) #float64; }; + }; - #ok(arg_type, curr_height + 1); + func to_record_field_type(node : TypeNode) : RecordFieldType = { + type_ = node.type_; + tag = node.tag; }; - type TypeInfo = (Type, id: Nat, tag: Tag); + func merge_variants_in_array_type(rows : Buffer<[UpdatedTypeNode]>) : Type { + let buffer = Buffer.Buffer(8); + let total_rows = rows.size(); + + func calc_height(parent : Nat, child : Nat) : Nat = parent + child; + + let ?_bottom = rows.removeLast() else return Debug.trap("trying to pop bottom but rows is empty"); - func merge_variants_in_array_type(types : Buffer<[TypeInfo]>) : Type { - let buffer = Buffer.Buffer(types.size()); - Debug.print("types.size(): " # debug_show types.size()); - Debug.print("types: " # debug_show Buffer.toArray(types)); - let ?_bottom = types.removeLast() else return #empty; - var bottom = _bottom; + var bottom = Array.map( + _bottom, + func(node : UpdatedTypeNode) : TypeNode = { + var type_ = updated_type_to_arg_type(node.type_, null); + height = node.height; + parent_index = node.parent_index; + var children = node.children; + tag = node.tag; + }, + ); var variants_exist = false; + + while (rows.size() > 0) { - while (types.size() > 0){ - Debug.print("bottom" # debug_show bottom); + let ?above_bottom = rows.removeLast() else return Debug.trap("trying to pop above_bottom but rows is empty"); - let ?above_bottom = types.removeLast() else return Prelude.unreachable(); - var bottom_iter = bottom.vals() |> Itertools.peekable(_); + var bottom_iter = Itertools.peekable(bottom.vals()); let variants = Buffer.Buffer(bottom.size()); let variant_indexes = Buffer.Buffer(bottom.size()); + for ((index, parent_node) in Itertools.enumerate(above_bottom.vals())) { + let tmp_bottom_iter = PeekableIter.takeWhile(bottom_iter, func({ parent_index; tag } : TypeNode) : Bool = index == parent_index); + let { parent_index; tag = parent_tag } = parent_node; - for ((index, (compound_type, parent_id, parent_tag)) in Itertools.enumerate(above_bottom.vals())){ - let tmp_bottom_iter = PeekableIter.takeWhile(bottom_iter, func((_, id, tag) : TypeInfo) : Bool = index == id); - - switch(compound_type){ + switch (parent_node.type_) { case (#opt(_)) { - let ?(opt_val, _, _) = tmp_bottom_iter.next() else return Prelude.unreachable(); - buffer.add((#opt(opt_val), parent_id, parent_tag)); + let ?child_node = tmp_bottom_iter.next() else return Debug.trap(" #opt error: no item in tmp_bottom_iter"); + + let merged_node : TypeNode = { + var type_ = #opt(child_node.type_); + height = calc_height(parent_node.height, child_node.height); + parent_index; + var children = null; + tag = parent_tag; + }; + buffer.add(merged_node); }; case (#vector(_)) { - let ?(vec_type, _, _) = tmp_bottom_iter.next() else return Prelude.unreachable(); + let vec_nodes = Iter.toArray(tmp_bottom_iter); + + let max = { + var height = 0; + var type_ : Type = #empty; + }; - buffer.add((#vector(vec_type), parent_id, parent_tag)); + for (node in vec_nodes.vals()) { + if (max.height < node.height) { + max.height := node.height; + max.type_ := node.type_; + }; + }; + + let best_node : TypeNode = { + var type_ = #vector(max.type_); + height = calc_height(parent_node.height, max.height); + parent_index; + var children = null; + tag = parent_tag; + }; + + buffer.add(best_node); }; case (#record(_)) { - let record_type = tmp_bottom_iter - |> Iter.map(_, func((type_, _, tag) : TypeInfo) : RecordFieldType = {type_; tag}) + var height = 0; + + func get_max_height(item : TypeNode) : TypeNode { + height := Nat.max(height, item.height); + item; + }; + + let composed_fn = Func.compose(to_record_field_type, get_max_height); + + let record_type = tmp_bottom_iter + |> Iter.map(_, composed_fn) |> Iter.toArray(_); - buffer.add((#record(record_type), parent_id, parent_tag)); + let merged_node : TypeNode = { + var type_ = #record(record_type); + height = calc_height(parent_node.height, height); + parent_index; + var children = null; + tag = parent_tag; + }; + buffer.add(merged_node); }; case (#variant(_)) { variants_exist := true; - let variant_types = tmp_bottom_iter - |> Iter.map(_, func((type_, _, tag) : TypeInfo) : RecordFieldType = {type_; tag}) - |> Iter.toArray(_); + + var height = 0; + + func get_max_height(item : TypeNode) : TypeNode { + height := Nat.max(height, item.height); + item; + }; + + let composed_fn = Func.compose(to_record_field_type, get_max_height); + + let variant_types = tmp_bottom_iter + |> Iter.map(_, composed_fn) + |> Iter.toArray(_); for (variant_type in variant_types.vals()) { variants.add(variant_type); @@ -311,11 +383,27 @@ module { variant_indexes.add(buffer.size()); - buffer.add((#variant(variant_types), parent_id, parent_tag)); + let merged_node : TypeNode = { + var type_ = #variant(variant_types); + height = calc_height(parent_node.height, height); + parent_index; + var children = null; + tag = parent_tag; + }; + + buffer.add(merged_node); }; - case (_){ - buffer.add(compound_type, parent_id, parent_tag); + case (_) { + let new_parent_node : TypeNode = { + var type_ = updated_type_to_arg_type(parent_node.type_, null); + height = parent_node.height; + parent_index; + var children = null; + tag = parent_tag; + }; + + buffer.add(new_parent_node); }; }; }; @@ -323,64 +411,129 @@ module { if (variants.size() > 0) { let full_variant_type : Type = #variant(Buffer.toArray(variants)); - for (index in variant_indexes.vals()){ - let (_, prev_id, prev_tag) = buffer.get(index); - buffer.put(index, (full_variant_type, prev_id, prev_tag)); - }; + for (index in variant_indexes.vals()) { + let prev_node = buffer.get(index); + let new_node : TypeNode = { + var type_ = full_variant_type; + height = prev_node.height; + parent_index = prev_node.parent_index; + var children = prev_node.children; + tag = prev_node.tag; + }; + buffer.put(index, new_node); + }; }; - + bottom := Buffer.toArray(buffer); buffer.clear(); }; - Debug.print("bottom" # debug_show bottom); - - bottom[0].0; + let merged_type = bottom[0].type_; + merged_type; }; - func bfs_get_types_by_height(types : Buffer<[TypeInfo]>) { + func get_height_value(type_ : UpdatedType) : Nat { + switch (type_) { + case (#empty or #null_) 0; + case (_) 1; + }; + }; - var merged_type : ?Type = null; + func order_types_by_height_bfs(rows : Buffer<[UpdatedTypeNode]>) { + var merged_type : ?UpdatedType = null; - label while_loop - while (types.size() > 0) { - let ?candid_values = types.removeLast() else return Prelude.unreachable(); - let buffer = Buffer.Buffer(8); + label while_loop while (rows.size() > 0) { + let candid_values = Buffer.last(rows) else return Prelude.unreachable(); + let buffer = Buffer.Buffer(8); var has_compound_type = false; - for ((id, (candid, _, tag)) in Itertools.enumerate(candid_values.vals())) { - switch (candid) { + for ((index, parent_node) in Itertools.enumerate(candid_values.vals())) { + + switch (parent_node.type_) { case (#opt(opt_val)) { has_compound_type := true; - buffer.add((opt_val, id, #name(""))); + let child_node : UpdatedTypeNode = { + var type_ = opt_val; + height = get_height_value(opt_val); + parent_index = index; + var children = null; + tag = #name(""); + }; + + parent_node.children := ?{ + start = buffer.size(); + n = 1; + }; + buffer.add(child_node); }; - case (#vector(vec_type)) { + case (#vector(vec_types)) { has_compound_type := true; - buffer.add((vec_type, id, #name(""))); + + parent_node.children := ?{ + start = buffer.size(); + n = vec_types.size(); + }; + + for (vec_type in vec_types.vals()) { + let child_node : UpdatedTypeNode = { + var type_ = vec_type; + height = get_height_value(vec_type); + parent_index = index; + var children = null; + tag = #name(""); + }; + + buffer.add(child_node); + }; + }; case (#record(records)) { - for ({tag; type_} in records.vals()) { + + parent_node.children := ?{ + start = buffer.size(); + n = records.size(); + }; + + for ({ tag; type_ } in records.vals()) { has_compound_type := true; - buffer.add((type_, id, tag)); + let child_node : UpdatedTypeNode = { + var type_ = type_; + height = get_height_value(type_); + parent_index = index; + var children = null; + tag; + }; + buffer.add(child_node); }; }; case (#variant(variants)) { has_compound_type := true; - for ({tag; type_} in variants.vals()) { + parent_node.children := ?{ + start = buffer.size(); + n = variants.size(); + }; + for ({ tag; type_ } in variants.vals()) { has_compound_type := true; - buffer.add((type_, id, tag)); + let child_node : UpdatedTypeNode = { + var type_ = type_; + height = get_height_value(type_); + parent_index = index; + var children = null; + tag; + }; + buffer.add(child_node); }; }; case (_) {}; }; - }; - types.add(candid_values); + parent_node.type_ := extract_top_level_type(parent_node.type_); + }; if (has_compound_type) { - types.add(Buffer.toArray(buffer)); + rows.add(Buffer.toArray(buffer)); } else { return; }; diff --git a/src/Utils.mo b/src/Utils.mo index 24be3ca..c218a5f 100644 --- a/src/Utils.mo +++ b/src/Utils.mo @@ -1,6 +1,8 @@ +import Array "mo:base/Array"; import Order "mo:base/Order"; import Float "mo:base/Float"; import Text "mo:base/Text"; +import Iter "mo:base/Iter"; import Result "mo:base/Result"; import Prelude "mo:base/Prelude"; @@ -8,8 +10,21 @@ import itertools "mo:itertools/Iter"; module { + type Iter = Iter.Iter; type Result = Result.Result; + public func sized_iter_to_array(iter: Iter, size: Nat): [A] { + Array.tabulate( + size, + func (i: Nat){ + switch(iter.next()){ + case (?x) x; + case (_) Prelude.unreachable(); + }; + } + ); + }; + public func send_error(res: Result): Result{ switch (res) { case (#ok(_)) Prelude.unreachable(); diff --git a/tests/Candid.Test.mo b/tests/Candid.Test.mo index 1959d93..d5d5d73 100644 --- a/tests/Candid.Test.mo +++ b/tests/Candid.Test.mo @@ -27,357 +27,357 @@ let success = run([ describe( "Candid", [ - // describe( - // "decode()", - // [ - // it( - // "find intended type of nested arrays with different types (null, empty, other_types)", - // do { - // type Permission = { - // #read : [Text]; - // #write : [Text]; - // #read_all : (); - // #write_all : (); - // #admin : (); - // }; - - // type User = { - // name : Text; - // age : Nat; - // permission : Permission; - // }; - - // type Record = { - // group : Text; - // users : ?[User]; - // }; - - // let admin_record : Record = { - // group = "admins"; - // users = ?[ - // { - // name = "John"; - // age = 32; - // permission = #admin; - // }, - // ]; - // }; - - // let user_record : Record = { - // group = "users"; - // users = ?[ - // { - // name = "Ali"; - // age = 28; - // permission = #read_all; - // }, - // { - // name = "James"; - // age = 40; - // permission = #write_all; - // }, - // ]; - // }; - - // let empty_record : Record = { - // group = "empty"; - // users = ?[]; - // }; - - // let null_record : Record = { - // group = "null"; - // users = null; - // }; - - // let base_record : Record = { - // group = "base"; - // users = ?[ - // { - // name = "Henry"; - // age = 32; - // permission = #read(["posts", "comments"]); - // }, - // { - // name = "Steven"; - // age = 32; - // permission = #write(["posts", "comments"]); - // }, - // ]; - // }; - - // let records : [Record] = [null_record, empty_record, admin_record, user_record, base_record]; - // true - // }, - // ), - // it( - // "duplicate compound types in record", - // do { - // type User = { - // name : Text; - // age : Nat; - // }; - - // let user_james = { - // name = "James"; - // age = 23; - // }; - - // let user_steven = { - // name = "Steven"; - // age = 32; - // }; - - // type Record = { - // first : User; - // second : User; - // }; - - // let record = { - // first = user_james; - // second = user_steven; - // }; - - // let record_blob = to_candid (record); - // let candid = Candid.decode(record_blob, ["first", "second", "name", "age"], null); - - // candid == #ok([ - // #Record([ - // ("first", #Record([("age", #Nat(23)), ("name", #Text("James"))])), - // ("second", #Record([("age", #Nat(32)), ("name", #Text("Steven"))])), - // ]), - // ]); - - // }, - // ), - // it( - // "recursive types", - // do { - // type RecursiveType = { - // user : Text; - // next : ?RecursiveType; - // }; - - // let rust : RecursiveType = { - // user = "rust"; - // next = null; - // }; - - // let typescript : RecursiveType = { - // user = "typescript"; - // next = ?rust; - // }; - - // let motoko : RecursiveType = { - // user = "motoko"; - // next = ?typescript; - // }; - - // let blob = to_candid (motoko); - // let candid = Candid.decode(blob, ["next", "user"], null); - - // let candid_rust = #Record([("next", #Option(#Null)), ("user", #Text("rust"))]); - // let candid_typescript = #Record([("next", #Option(candid_rust)), ("user", #Text("typescript"))]); - // let candid_motoko = #Record([("next", #Option(candid_typescript)), ("user", #Text("motoko"))]); - - // candid == #ok([candid_motoko]); - // }, - // ), - // it( - // "renaming keys", - // do { - // let motoko = [{ name = "candid"; arr = [1, 2, 3, 4] }, { name = "motoko"; arr = [5, 6, 7, 8] }, { name = "rust"; arr = [9, 10, 11, 12] }]; - // let blob = to_candid (motoko); - // let options = { - // renameKeys = [("arr", "array"), ("name", "username")]; - // }; - // let candid = Candid.decode(blob, ["name", "arr"], ?options); - - // candid == #ok([ - // #Array([ - // #Record([ - // ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), - // ("username", #Text("candid")), - // ]), - // #Record([ - // ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), - // ("username", #Text("motoko")), - // ]), - // #Record([ - // ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), - // ("username", #Text("rust")), - // ]), - // ]) - // ]); - // }, - // ), - // it( - // "record type: {name: Text}", - // do { - // let motoko = { name = "candid" }; - // let blob = to_candid (motoko); - // let candid = Candid.decode(blob, ["name"], null); - - // candid == #ok([#Record([("name", #Text("candid"))])]); - // }, - // ), - // it( - // "array: [1, 2, 3, 4]", - // do { - // let arr = [1, 2, 3, 4]; - // let blob = to_candid (arr); - // let candid = Candid.decode(blob, [], null); - - // candid == #ok([#Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])]); - // }, - // ), - // it( - // "multi-dimensional arrays", - // do { - // let arr2 : [[Nat]] = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]; - - // let arr3 : [[[Text]]] = [ - // [["hello", "world"], ["foo", "bar"]], - // [["hello", "world"], ["foo", "bar"]], - // [["hello", "world"], ["foo", "bar"]], - // ]; - - // assertAllTrue([ - // Candid.decode(to_candid (arr2), [], null) == #ok([ - // #Array([ - // #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)]), - // #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)]), - // #Array([#Nat(9), #Nat(10), #Nat(11)]), - // ]) - // ]), - // Candid.decode(to_candid (arr3), [], null) == #ok([ - // #Array([ - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // ]) - // ]), - // ]); - // }, - // ), - // it( - // "blob and [Nat8] types", - // do { - // let motoko_blob = Blob.fromArray([1, 2, 3, 4]); - // let motoko_array : [Nat8] = [1, 2, 3, 4]; - - // let bytes_array = to_candid (motoko_blob); - // let bytes_blob = to_candid (motoko_blob); - - // let candid_array = Candid.decode(bytes_array, [], null); - // let candid_blob = Candid.decode(bytes_blob, [], null); - - // assertAllTrue([ - // // All [Nat8] types are decoded as #Blob - // candid_array != #ok([#Array([#Nat8(1), #Nat8(2), #Nat8(3), #Nat8(4)])]), - // candid_array == #ok([#Blob(motoko_blob)]), - // candid_blob == #ok([#Blob(motoko_blob)]), - // ]); - // }, - // ), - // it( - // "variant", - // do { - // type Variant = { - // #text : Text; - // #nat : Nat; - // #bool : Bool; - // #record : { site : Text }; - // #array : [Nat]; - // }; - - // let text = #text("hello"); - // let nat = #nat(123); - // let bool = #bool(true); - // let record = #record({ site = "github" }); - // let array = #array([1, 2, 3]); - - // let text_blob = to_candid (text); - // let nat_blob = to_candid (nat); - // let bool_blob = to_candid (bool); - // let record_blob = to_candid (record); - // let array_blob = to_candid (array); - - // let text_candid = Candid.decode(text_blob, ["text"], null); - // let nat_candid = Candid.decode(nat_blob, ["nat"], null); - // let bool_candid = Candid.decode(bool_blob, ["bool"], null); - // let record_candid = Candid.decode(record_blob, ["record", "site"], null); - // let array_candid = Candid.decode(array_blob, ["array"], null); - - // assertAllTrue([ - // text_candid == #ok([#Variant("text", #Text("hello"))]), - // nat_candid == #ok([#Variant("nat", #Nat(123))]), - // bool_candid == #ok([#Variant("bool", #Bool(true))]), - // record_candid == #ok([#Variant("record", #Record([("site", #Text("github"))]))]), - // array_candid == #ok([#Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)]))]), - // ]); - - // }, - // ), - // it( - // "complex type", - // do { - // type User = { - // name : Text; - // age : Nat8; - // email : ?Text; - // registered : Bool; - // }; - // let record_keys = ["name", "age", "email", "registered"]; - // let users : [User] = [ - // { - // name = "Henry"; - // age = 32; - // email = null; - // registered = false; - // }, - // { - // name = "Ali"; - // age = 28; - // email = ?"ali.abdull@gmail.com"; - // registered = false; - // }, - // { - // name = "James"; - // age = 40; - // email = ?"james.bond@gmail.com"; - // registered = true; - // }, - // ]; - - // let blob = to_candid (users); - // let candid = Candid.decode(blob, record_keys, null); - - // candid == #ok([ - // #Array([ - // #Record([ - // ("age", #Nat8(32)), - // ("email", #Option(#Null)), - // ("name", #Text("Henry")), - // ("registered", #Bool(false)), - // ]), - // #Record([ - // ("age", #Nat8(28)), - // ("email", #Option(#Text("ali.abdull@gmail.com"))), - // ("name", #Text("Ali")), - // ("registered", #Bool(false)), - // ]), - // #Record([ - // ("age", #Nat8(40)), - // ("email", #Option(#Text("james.bond@gmail.com"))), - // ("name", #Text("James")), - // ("registered", #Bool(true)), - // ]), - // ]), - // ]); - // }, - // ), - // ], - // ), + describe( + "decode()", + [ + it( + "find intended type of nested arrays with different types (null, empty, other_types)", + do { + type Permission = { + #read : [Text]; + #write : [Text]; + #read_all : (); + #write_all : (); + #admin : (); + }; + + type User = { + name : Text; + age : Nat; + permission : Permission; + }; + + type Record = { + group : Text; + users : ?[User]; + }; + + let admin_record : Record = { + group = "admins"; + users = ?[ + { + name = "John"; + age = 32; + permission = #admin; + }, + ]; + }; + + let user_record : Record = { + group = "users"; + users = ?[ + { + name = "Ali"; + age = 28; + permission = #read_all; + }, + { + name = "James"; + age = 40; + permission = #write_all; + }, + ]; + }; + + let empty_record : Record = { + group = "empty"; + users = ?[]; + }; + + let null_record : Record = { + group = "null"; + users = null; + }; + + let base_record : Record = { + group = "base"; + users = ?[ + { + name = "Henry"; + age = 32; + permission = #read(["posts", "comments"]); + }, + { + name = "Steven"; + age = 32; + permission = #write(["posts", "comments"]); + }, + ]; + }; + + let records : [Record] = [null_record, empty_record, admin_record, user_record, base_record]; + true + }, + ), + it( + "duplicate compound types in record", + do { + type User = { + name : Text; + age : Nat; + }; + + let user_james = { + name = "James"; + age = 23; + }; + + let user_steven = { + name = "Steven"; + age = 32; + }; + + type Record = { + first : User; + second : User; + }; + + let record = { + first = user_james; + second = user_steven; + }; + + let record_blob = to_candid (record); + let candid = Candid.decode(record_blob, ["first", "second", "name", "age"], null); + + candid == #ok([ + #Record([ + ("first", #Record([("age", #Nat(23)), ("name", #Text("James"))])), + ("second", #Record([("age", #Nat(32)), ("name", #Text("Steven"))])), + ]), + ]); + + }, + ), + it( + "recursive types", + do { + type RecursiveType = { + user : Text; + next : ?RecursiveType; + }; + + let rust : RecursiveType = { + user = "rust"; + next = null; + }; + + let typescript : RecursiveType = { + user = "typescript"; + next = ?rust; + }; + + let motoko : RecursiveType = { + user = "motoko"; + next = ?typescript; + }; + + let blob = to_candid (motoko); + let candid = Candid.decode(blob, ["next", "user"], null); + + let candid_rust = #Record([("next", #Option(#Null)), ("user", #Text("rust"))]); + let candid_typescript = #Record([("next", #Option(candid_rust)), ("user", #Text("typescript"))]); + let candid_motoko = #Record([("next", #Option(candid_typescript)), ("user", #Text("motoko"))]); + + candid == #ok([candid_motoko]); + }, + ), + it( + "renaming keys", + do { + let motoko = [{ name = "candid"; arr = [1, 2, 3, 4] }, { name = "motoko"; arr = [5, 6, 7, 8] }, { name = "rust"; arr = [9, 10, 11, 12] }]; + let blob = to_candid (motoko); + let options = { + renameKeys = [("arr", "array"), ("name", "username")]; + }; + let candid = Candid.decode(blob, ["name", "arr"], ?options); + + candid == #ok([ + #Array([ + #Record([ + ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), + ("username", #Text("candid")), + ]), + #Record([ + ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), + ("username", #Text("motoko")), + ]), + #Record([ + ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), + ("username", #Text("rust")), + ]), + ]) + ]); + }, + ), + it( + "record type: {name: Text}", + do { + let motoko = { name = "candid" }; + let blob = to_candid (motoko); + let candid = Candid.decode(blob, ["name"], null); + + candid == #ok([#Record([("name", #Text("candid"))])]); + }, + ), + it( + "array: [1, 2, 3, 4]", + do { + let arr = [1, 2, 3, 4]; + let blob = to_candid (arr); + let candid = Candid.decode(blob, [], null); + + candid == #ok([#Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])]); + }, + ), + it( + "multi-dimensional arrays", + do { + let arr2 : [[Nat]] = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]; + + let arr3 : [[[Text]]] = [ + [["hello", "world"], ["foo", "bar"]], + [["hello", "world"], ["foo", "bar"]], + [["hello", "world"], ["foo", "bar"]], + ]; + + assertAllTrue([ + Candid.decode(to_candid (arr2), [], null) == #ok([ + #Array([ + #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)]), + #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)]), + #Array([#Nat(9), #Nat(10), #Nat(11)]), + ]) + ]), + Candid.decode(to_candid (arr3), [], null) == #ok([ + #Array([ + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + ]) + ]), + ]); + }, + ), + it( + "blob and [Nat8] types", + do { + let motoko_blob = Blob.fromArray([1, 2, 3, 4]); + let motoko_array : [Nat8] = [1, 2, 3, 4]; + + let bytes_array = to_candid (motoko_blob); + let bytes_blob = to_candid (motoko_blob); + + let candid_array = Candid.decode(bytes_array, [], null); + let candid_blob = Candid.decode(bytes_blob, [], null); + + assertAllTrue([ + // All [Nat8] types are decoded as #Blob + candid_array != #ok([#Array([#Nat8(1), #Nat8(2), #Nat8(3), #Nat8(4)])]), + candid_array == #ok([#Blob(motoko_blob)]), + candid_blob == #ok([#Blob(motoko_blob)]), + ]); + }, + ), + it( + "variant", + do { + type Variant = { + #text : Text; + #nat : Nat; + #bool : Bool; + #record : { site : Text }; + #array : [Nat]; + }; + + let text = #text("hello"); + let nat = #nat(123); + let bool = #bool(true); + let record = #record({ site = "github" }); + let array = #array([1, 2, 3]); + + let text_blob = to_candid (text); + let nat_blob = to_candid (nat); + let bool_blob = to_candid (bool); + let record_blob = to_candid (record); + let array_blob = to_candid (array); + + let text_candid = Candid.decode(text_blob, ["text"], null); + let nat_candid = Candid.decode(nat_blob, ["nat"], null); + let bool_candid = Candid.decode(bool_blob, ["bool"], null); + let record_candid = Candid.decode(record_blob, ["record", "site"], null); + let array_candid = Candid.decode(array_blob, ["array"], null); + + assertAllTrue([ + text_candid == #ok([#Variant("text", #Text("hello"))]), + nat_candid == #ok([#Variant("nat", #Nat(123))]), + bool_candid == #ok([#Variant("bool", #Bool(true))]), + record_candid == #ok([#Variant("record", #Record([("site", #Text("github"))]))]), + array_candid == #ok([#Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)]))]), + ]); + + }, + ), + it( + "complex type", + do { + type User = { + name : Text; + age : Nat8; + email : ?Text; + registered : Bool; + }; + let record_keys = ["name", "age", "email", "registered"]; + let users : [User] = [ + { + name = "Henry"; + age = 32; + email = null; + registered = false; + }, + { + name = "Ali"; + age = 28; + email = ?"ali.abdull@gmail.com"; + registered = false; + }, + { + name = "James"; + age = 40; + email = ?"james.bond@gmail.com"; + registered = true; + }, + ]; + + let blob = to_candid (users); + let candid = Candid.decode(blob, record_keys, null); + + candid == #ok([ + #Array([ + #Record([ + ("age", #Nat8(32)), + ("email", #Option(#Null)), + ("name", #Text("Henry")), + ("registered", #Bool(false)), + ]), + #Record([ + ("age", #Nat8(28)), + ("email", #Option(#Text("ali.abdull@gmail.com"))), + ("name", #Text("Ali")), + ("registered", #Bool(false)), + ]), + #Record([ + ("age", #Nat8(40)), + ("email", #Option(#Text("james.bond@gmail.com"))), + ("name", #Text("James")), + ("registered", #Bool(true)), + ]), + ]), + ]); + }, + ), + ], + ), describe( "encode()", @@ -515,7 +515,7 @@ let success = run([ empty_record_candid, admin_record_candid, user_record_candid, - // base_record_candid + base_record_candid ]); let #ok(blob) = Candid.encodeOne(records, null); @@ -526,530 +526,530 @@ let success = run([ empty_record, admin_record, user_record, - // base_record + base_record ]; }, ), - // it( - // "renaming keys", - // do { - // let candid : Candid = #Array([ - // #Record([ - // ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), - // ("name", #Text("candid")), - // ]), - // #Record([ - // ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), - // ("name", #Text("motoko")), - // ]), - // #Record([ - // ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), - // ("name", #Text("rust")), - // ]), - // ]); - - // type Data = { - // language : Text; - // daily_downloads : [Nat]; - // }; - - // let options = { - // renameKeys = [("array", "daily_downloads"), ("name", "language")]; - // }; - // let #ok(blob) = Candid.encodeOne(candid, ?options); - // let motoko : ?[Data] = from_candid (blob); - // // true - // motoko == ?[{ language = "candid"; daily_downloads = [1, 2, 3, 4] }, { language = "motoko"; daily_downloads = [5, 6, 7, 8] }, { language = "rust"; daily_downloads = [9, 10, 11, 12] }]; - // }, - // ), - // it( - // "record type {name: Text}", - // do { - // let candid = #Record([("name", #Text("candid"))]); - // type User = { - // name : Text; - // }; - - // let #ok(blob) = Candid.encodeOne(candid, null); - // let user : ?User = from_candid (blob); - - // user == ?{ name = "candid" }; - // }, - // ), - // it( - // "multi-dimensional arrays", - // do { - // let arr2 : Candid = #Array([ - // #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)]), - // #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)]), - // #Array([#Nat(9), #Nat(10), #Nat(11)]), - // ]); - - // let arr3 : Candid = #Array([ - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), - // ]); - - // let #ok(arr2_blob) = Candid.encodeOne(arr2, null); - // let #ok(arr3_blob) = Candid.encodeOne(arr3, null); - - // let arr2_encoded : ?[[Nat]] = from_candid (arr2_blob); - // let arr3_encoded : ?[[[Text]]] = from_candid (arr3_blob); - - // assertAllTrue([ - // arr2_encoded == ?[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]], - // arr3_encoded == ?[ - // [["hello", "world"], ["foo", "bar"]], - // [["hello", "world"], ["foo", "bar"]], - // [["hello", "world"], ["foo", "bar"]], - // ], - // ]); - // }, - // ), - // it( - // "blob and [Nat8] type", - // do { - // let motoko_blob = Blob.fromArray([1, 2, 3, 4]); - - // let candid_1 = #Array([#Nat8(1 : Nat8), #Nat8(2 : Nat8), #Nat8(3 : Nat8), #Nat8(4 : Nat8)]); - // let candid_2 = #Blob(motoko_blob); - - // let #ok(serialized_1) = Candid.encodeOne(candid_1, null); - // let #ok(serialized_2) = Candid.encodeOne(candid_2, null); - - // let blob_1 : ?Blob = from_candid (serialized_1); - // let blob_2 : ?Blob = from_candid (serialized_2); - - // let bytes_1 : ?[Nat8] = from_candid (serialized_1); - // let bytes_2 : ?[Nat8] = from_candid (serialized_1); - - // assertAllTrue([ - // blob_1 == ?motoko_blob, - // blob_2 == ?motoko_blob, - // bytes_1 == ?[1, 2, 3, 4], - // bytes_2 == ?[1, 2, 3, 4], - // ]); - // }, - // ), - // it( - // "variant", - // do { - - // type Variant = { - // #text : Text; - // #nat : Nat; - // #bool : Bool; - // #record : { site : Text }; - // #array : [Nat]; - // }; - - // let text = #Variant("text", #Text("hello")); - // let nat = #Variant("nat", #Nat(123)); - // let bool = #Variant("bool", #Bool(true)); - // let record = #Variant("record", #Record([("site", #Text("github"))])); - // let array = #Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)])); - - // let #ok(text_blob) = Candid.encodeOne(text, null); - // let #ok(nat_blob) = Candid.encodeOne(nat, null); - // let #ok(bool_blob) = Candid.encodeOne(bool, null); - // let #ok(record_blob) = Candid.encodeOne(record, null); - // let #ok(array_blob) = Candid.encodeOne(array, null); - - // let text_val : ?Variant = from_candid (text_blob); - // let nat_val : ?Variant = from_candid (nat_blob); - // let bool_val : ?Variant = from_candid (bool_blob); - // let record_val : ?Variant = from_candid (record_blob); - // let array_val : ?Variant = from_candid (array_blob); - - // assertAllTrue([ - // text_val == ? #text("hello"), - // nat_val == ? #nat(123), - // bool_val == ? #bool(true), - // record_val == ? #record({ - // site = "github"; - // }), - // array_val == ? #array([1, 2, 3]), - // ]); - // }, - // ), + it( + "renaming keys", + do { + let candid : Candid = #Array([ + #Record([ + ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), + ("name", #Text("candid")), + ]), + #Record([ + ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), + ("name", #Text("motoko")), + ]), + #Record([ + ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), + ("name", #Text("rust")), + ]), + ]); + + type Data = { + language : Text; + daily_downloads : [Nat]; + }; + + let options = { + renameKeys = [("array", "daily_downloads"), ("name", "language")]; + }; + let #ok(blob) = Candid.encodeOne(candid, ?options); + let motoko : ?[Data] = from_candid (blob); + // true + motoko == ?[{ language = "candid"; daily_downloads = [1, 2, 3, 4] }, { language = "motoko"; daily_downloads = [5, 6, 7, 8] }, { language = "rust"; daily_downloads = [9, 10, 11, 12] }]; + }, + ), + it( + "record type {name: Text}", + do { + let candid = #Record([("name", #Text("candid"))]); + type User = { + name : Text; + }; + + let #ok(blob) = Candid.encodeOne(candid, null); + let user : ?User = from_candid (blob); + + user == ?{ name = "candid" }; + }, + ), + it( + "multi-dimensional arrays", + do { + let arr2 : Candid = #Array([ + #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)]), + #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)]), + #Array([#Nat(9), #Nat(10), #Nat(11)]), + ]); + + let arr3 : Candid = #Array([ + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + #Array([#Array([#Text("hello"), #Text("world")]), #Array([#Text("foo"), #Text("bar")])]), + ]); + + let #ok(arr2_blob) = Candid.encodeOne(arr2, null); + let #ok(arr3_blob) = Candid.encodeOne(arr3, null); + + let arr2_encoded : ?[[Nat]] = from_candid (arr2_blob); + let arr3_encoded : ?[[[Text]]] = from_candid (arr3_blob); + + assertAllTrue([ + arr2_encoded == ?[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]], + arr3_encoded == ?[ + [["hello", "world"], ["foo", "bar"]], + [["hello", "world"], ["foo", "bar"]], + [["hello", "world"], ["foo", "bar"]], + ], + ]); + }, + ), + it( + "blob and [Nat8] type", + do { + let motoko_blob = Blob.fromArray([1, 2, 3, 4]); + + let candid_1 = #Array([#Nat8(1 : Nat8), #Nat8(2 : Nat8), #Nat8(3 : Nat8), #Nat8(4 : Nat8)]); + let candid_2 = #Blob(motoko_blob); + + let #ok(serialized_1) = Candid.encodeOne(candid_1, null); + let #ok(serialized_2) = Candid.encodeOne(candid_2, null); + + let blob_1 : ?Blob = from_candid (serialized_1); + let blob_2 : ?Blob = from_candid (serialized_2); + + let bytes_1 : ?[Nat8] = from_candid (serialized_1); + let bytes_2 : ?[Nat8] = from_candid (serialized_1); + + assertAllTrue([ + blob_1 == ?motoko_blob, + blob_2 == ?motoko_blob, + bytes_1 == ?[1, 2, 3, 4], + bytes_2 == ?[1, 2, 3, 4], + ]); + }, + ), + it( + "variant", + do { + + type Variant = { + #text : Text; + #nat : Nat; + #bool : Bool; + #record : { site : Text }; + #array : [Nat]; + }; + + let text = #Variant("text", #Text("hello")); + let nat = #Variant("nat", #Nat(123)); + let bool = #Variant("bool", #Bool(true)); + let record = #Variant("record", #Record([("site", #Text("github"))])); + let array = #Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)])); + + let #ok(text_blob) = Candid.encodeOne(text, null); + let #ok(nat_blob) = Candid.encodeOne(nat, null); + let #ok(bool_blob) = Candid.encodeOne(bool, null); + let #ok(record_blob) = Candid.encodeOne(record, null); + let #ok(array_blob) = Candid.encodeOne(array, null); + + let text_val : ?Variant = from_candid (text_blob); + let nat_val : ?Variant = from_candid (nat_blob); + let bool_val : ?Variant = from_candid (bool_blob); + let record_val : ?Variant = from_candid (record_blob); + let array_val : ?Variant = from_candid (array_blob); + + assertAllTrue([ + text_val == ? #text("hello"), + nat_val == ? #nat(123), + bool_val == ? #bool(true), + record_val == ? #record({ + site = "github"; + }), + array_val == ? #array([1, 2, 3]), + ]); + }, + ), ], ), - // it( - // "print out args", - // do { - // type User = { - // name : Text; - // details : { - // age : Nat; - // email : ?Text; - // registered : Bool; - // }; - // }; - - // let candid = #Record([ - // ("name", #Text("candid")), - // ("details", #Record([("age", #Nat(32)), ("email", #Option(#Text("example@gmail.com"))), ("registered", #Bool(true))])), - // ]); - - // let #ok(blob) = Candid.encodeOne(candid, null); - - // let mo : ?User = from_candid (blob); - // mo == ?{ - // name = "candid"; - // details = { - // age = 32; - // email = ?"example@gmail.com"; - // registered = true; - // }; - // }; - // }, - // ), - - // describe( - // "fromText()", - // [ - // describe( - // "parsing Int and Nat formats", - // [ - // it( - // "parse \"quoted text\" to #Text", - // do { - // assertAllTrue([ - // Candid.fromText("( \"\" )") == [#Text("")], - // Candid.fromText("( \"hello\" )") == [#Text("hello")], - // Candid.fromText("(\"hello world\")") == [#Text("hello world")], - // Candid.fromText("(\"1_000_000\")") == [#Text("1_000_000")], - // ]); - // }, - // ), - // it( - // "parse blob type", - // do { - // assertAllTrue([ - // Candid.fromText("(blob \"\")") == [#Blob(Blob.fromArray([]))], - // Candid.fromText("(blob \"\\AB\\CD\\EF\")") == [#Blob(Blob.fromArray([0xAB, 0xCD, 0xEF]))], - // Candid.fromText("(blob \"\\CA\\FF\\FE\")") == [#Blob(Blob.fromArray([0xCA, 0xFF, 0xFE]))], - // ]); - // }, - // ), - // it( - // "should parse principal type", - // do { - // assertAllTrue([ - // Candid.fromText("(principal \"aaaaa-aa\")") == [#Principal(Principal.fromText("aaaaa-aa"))], - // Candid.fromText("(principal \"w7x7r-cok77-xa\")") == [#Principal(Principal.fromText("w7x7r-cok77-xa"))], - // ]); - // }, - // ), - // it( - // "Positive integers to #Nat", - // do { - // assertAllTrue([ - // Candid.fromText("( 1000)") == [#Nat(1000)], - // Candid.fromText("(+2000)") == [#Nat(2000)], - // ]); - // }, - // ), - // it( - // "Negative integers to #Int", - // do { - // assertAllTrue([ - // Candid.fromText("(-3000)") == [#Int(-3000)], - // Candid.fromText("(-4000)") == [#Int(-4000)], - // ]); - // }, - // ), - // it( - // "should parse Int/Nats with leading zeroes", - // do { - // assertAllTrue([ - // Candid.fromText("(001)") == [#Nat(1)], - // Candid.fromText("((+00123))") == [#Nat(123)], - // Candid.fromText("(-0123)") == [#Int(-0123)], - // ]); - // }, - // ), - // it( - // "should parse Int/Nat with underscores", - // do { - // assertAllTrue([ - // Candid.fromText("( 1_000)") == [#Nat(1000)], - // Candid.fromText("(+1_000_000)") == [#Nat(1000000)], - - // Candid.fromText("(-1_000 )") == [#Int(-1000)], - // Candid.fromText("((-1_000_000))") == [#Int(-1000000)], - // ]); - // }, - // ), - // it( - // "should parse Int/Nat in hex format", - // do { - // assertAllTrue([ - // Candid.fromText("(0x10)") == [#Nat(16)], - // Candid.fromText("(0xdead_beef)") == [#Nat(3_735_928_559)], - // Candid.fromText("(0xDEAD_BEEF)") == [#Nat(3_735_928_559)], - - // Candid.fromText("(+0xa1_b2)") == [#Nat(41_394)], - // Candid.fromText("(-0xA1_B2)") == [#Int(-41_394)], - - // Candid.fromText("(-0xABC_def)") == [#Int(-11_259_375)], - // ]); - // }, - // ), - // it( - // "should parse types with nested brackets", - // do { - // Candid.fromText("( ( ( ( 100_000 ) ) ) )") == [#Nat(100_000)]; - // }, - // ), - // it( - // "should parse 'opt' type", - // do { - // assertAllTrue([ - // Candid.fromText("(opt 100)") == [#Option(#Nat(100))], - // Candid.fromText("(opt null)") == [#Option(#Null)], - // Candid.fromText("(opt (-0xdead_beef))") == [#Option(#Int(-3_735_928_559))], - // Candid.fromText("(opt \"hello\")") == [#Option(#Text("hello"))], - // Candid.fromText("(opt true)") == [#Option(#Bool(true))], - // Candid.fromText("(opt (blob \"\\AB\\CD\\EF\\12\"))") == [#Option(#Blob(Blob.fromArray([0xAB, 0xCD, 0xEF, 0x12])))], - // Candid.fromText("(opt (principal \"w7x7r-cok77-xa\"))") == [#Option(#Principal(Principal.fromText("w7x7r-cok77-xa")))], - // ]); - // }, - // ), - // describe( - // "should parse 'vec' type to #Array", - // [ - // it( - // "parse different element types", - // do { - // assertAllTrue([ - // Candid.fromText("(vec {})") == [#Array([])], - // Candid.fromText("(vec { 100; 200; 0xAB })") == [#Array([#Nat(100), #Nat(200), #Nat(0xAB)])], - // Candid.fromText("(vec { \"hello\"; \"world\"; })") == [#Array([#Text("hello"), #Text("world")])], - // Candid.fromText("(vec { true; false })") == [#Array([#Bool(true), #Bool(false)])], - // Candid.fromText("(vec { blob \"\\AB\\CD\"; blob \"\\EF\\12\" })") == [#Array([#Blob(Blob.fromArray([0xAB, 0xCD])), #Blob(Blob.fromArray([0xEF, 0x12]))])], - // Candid.fromText("(vec { principal \"w7x7r-cok77-xa\"; principal \"aaaaa-aa\"; })") == [#Array([#Principal(Principal.fromText("w7x7r-cok77-xa")), #Principal(Principal.fromText("aaaaa-aa"))])], - // ]); - // }, - // ), - // it( - // "parse nested array", - // do { - // assertAllTrue([ - // Candid.fromText("(vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } })") == [#Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])])], - - // Candid.fromText("(vec { vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } }; vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } } })") == [#Array([#Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])]), #Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])])])], - // ]); - // }, - // ), - // ], - // ), - - // it( - // "parse record type", - // do { - // assertAllTrue([ - // Candid.fromText("(record {})") == [#Record([])], - // Candid.fromText("(record { first_name = \"John\"; second_name = \"Doe\" })") == [#Record([("first_name", #Text("John")), ("second_name", #Text("Doe"))])], - // Candid.fromText("(record { \"name with spaces\" = 42; \"unicode, too: ☃\" = true; })") == [#Record([("name with spaces", #Nat(42)), ("unicode, too: ☃", #Bool(true))])], - // // nested record - // Candid.fromText("(record { first_name = \"John\"; second_name = \"Doe\"; address = record { street = \"Main Street\"; city = \"New York\"; } })") == [#Record([("first_name", #Text("John")), ("second_name", #Text("Doe")), ("address", #Record([("street", #Text("Main Street")), ("city", #Text("New York"))]))])], - // ]); - // }, - // ), - // it( - // "parser variant type", - // do { - // assertAllTrue([ - // Candid.fromText("(variant { ok = \"hello\" })") == [#Variant(("ok", #Text("hello")))], - - // // variant without a value - // Candid.fromText("(variant { \"ok\" })") == [#Variant(("ok", #Null))], - - // // variant with unicode key - // Candid.fromText("(variant { \"unicode, too: ☃\" = \"hello\" })") == [#Variant(("unicode, too: ☃", #Text("hello")))], - // Candid.fromText("(variant { \"☃\" })") == [#Variant(("☃", #Null))], - - // // variant with record value - // Candid.fromText("(variant { ok = record { \"first name\" = \"John\"; second_name = \"Doe\" } })") == [#Variant(("ok", #Record([("first name", #Text("John")), ("second_name", #Text("Doe"))])))], - - // // variant with array value - // Candid.fromText("(variant { ok = vec { 100; 200; 0xAB } })") == [#Variant(("ok", #Array([#Nat(100), #Nat(200), #Nat(0xAB)])))], - - // // variant with variant value - // Candid.fromText("(variant { ok = variant { status = \"active\" } })") == [#Variant(("ok", #Variant(("status", #Text("active")))))], - - // ]); - // }, - // ), - - // describe( - // "should parse NatX types with type annotations", - // [ - // it( - // "Nat8", - // do { - // assertAllTrue([ - // Candid.fromText("(100 : nat8)") == [#Nat8(100 : Nat8)], - // Candid.fromText("(00123:nat8)") == [#Nat8(123 : Nat8)], - // Candid.fromText("(1_2_3 : nat8)") == [#Nat8(123 : Nat8)], - // Candid.fromText("(0xA1 : nat8)") == [#Nat8(161 : Nat8)], - // ]); - // }, - // ), - // it( - // "Nat16", - // do { - // assertAllTrue([ - // Candid.fromText("((1000 : nat16))") == [#Nat16(1000 : Nat16)], - // Candid.fromText("(0061234 : nat16)") == [#Nat16(61234 : Nat16)], - // Candid.fromText("(32_892 : nat16)") == [#Nat16(32_892 : Nat16)], - // Candid.fromText("(0xBEEF : nat16)") == [#Nat16(48_879 : Nat16)], - // ]); - // }, - // ), - // it( - // "Nat32", - // do { - // assertAllTrue([ - // Candid.fromText("((1_000_000 : nat32))") == [#Nat32(1_000_000 : Nat32)], - // Candid.fromText("(0xdead_beef : nat32)") == [#Nat32(3_735_928_559 : Nat32)], - // ]); - // }, - // ), - // it( - // "Nat64", - // do { - // assertAllTrue([ - // Candid.fromText("((100_000_000_000 : nat64))") == [#Nat64(100_000_000_000 : Nat64)], - // Candid.fromText("(0xdead_beef_1234 : nat64)") == [#Nat64(244_837_814_047_284 : Nat64)], - // ]); - // }, - // ), - // it( - // "Nat", - // do { - // assertAllTrue([ - // Candid.fromText("((100_000_000_000 : nat))") == [#Nat(100_000_000_000)], - // Candid.fromText("(0xdead_beef_1234 : nat)") == [#Nat(244_837_814_047_284)], - // ]); - // }, - // ), - // ], - // ), - // describe( - // "should parse IntX types with type annotations", - // [ - // it( - // "Int8", - // do { - // assertAllTrue([ - // Candid.fromText("((+100 : int8))") == [#Int8(100 : Int8)], - // Candid.fromText("(-00123:int8)") == [#Int8(-123 : Int8)], - // Candid.fromText("(-1_2_3 : int8)") == [#Int8(-123 : Int8)], - // Candid.fromText("(-0x7A : int8)") == [#Int8(-122 : Int8)], - // ]); - // }, - // ), - // it( - // "Int16", - // do { - // assertAllTrue([ - // Candid.fromText("((+1000 : int16))") == [#Int16(1000 : Int16)], - // Candid.fromText("(+0031234 : int16)") == [#Int16(31234 : Int16)], - // Candid.fromText("(-31_234 : int16)") == [#Int16(-31_234 : Int16)], - // Candid.fromText("(-0x7A_BC : int16)") == [#Int16(-31_420 : Int16)], - // ]); - // }, - // ), - // it( - // "Int32", - // do { - // assertAllTrue([ - // Candid.fromText("((+1_000_000 : int32))") == [#Int32(1_000_000 : Int32)], - // Candid.fromText("(-0xbad_beef : int32)") == [#Int32(-195_935_983 : Int32)], - // ]); - // }, - // ), - // it( - // "Int64", - // do { - // assertAllTrue([ - // Candid.fromText("(+100_000_000_000 : int64)") == [#Int64(100_000_000_000 : Int64)], - // Candid.fromText("((-0xdead_beef_1234 : int64))") == [#Int64(-244_837_814_047_284 : Int64)], - // ]); - // }, - // ), - // it( - // "Int", - // do { - // assertAllTrue([ - // Candid.fromText("(+100_000_000_000 : int)") == [#Int(100_000_000_000)], - // Candid.fromText("((-0xdead_beef_1234 : int))") == [#Int(-244_837_814_047_284)], - // ]); - // }, - // ), - // ], - // ), - - // ], - // ), - // ], - // ), - // it( - // "toText() should parse candid text", - - // do { - // let candid = [ - // Candid.toText([#Nat(100)]), - // Candid.toText([#Nat8(200 : Nat8)]), - // Candid.toText([#Nat16(300 : Nat16)]), - // Candid.toText([#Nat32(400 : Nat32)]), - // Candid.toText([#Nat64(500 : Nat64)]), - - // Candid.toText([#Int(600)]), - // Candid.toText([#Int8(-70 : Int8)]), - // Candid.toText([#Int16(800 : Int16)]), - // Candid.toText([#Int32(-900 : Int32)]), - // Candid.toText([#Int64(1000 : Int64)]), - - // Candid.toText([#Nat8(100 : Nat8), #Int(-200)]), - - // Candid.toText([#Text("hello")]), - // Candid.toText([#Record([("name", #Text("John")), ("age", #Nat(30))])]), - // Candid.toText([#Array([#Nat((100))])]), - // Candid.toText([#Variant(("email", #Option(#Text("example@gmail.com"))))]), - // Candid.toText([#Principal(Principal.fromText("aaaaa-aa"))]), - // Candid.toText([#Nat(100), #Text("hello"), #Record([("name", #Text("John")), ("age", #Nat(30))])]), - // ]; - - // candid == [ - // "(100)", - // "(200 : nat8)", - // "(300 : nat16)", - // "(400 : nat32)", - // "(500 : nat64)", - - // "(600)", - // "(-70 : int8)", - // "(800 : int16)", - // "(-900 : int32)", - // "(1000 : int64)", - - // "((100 : nat8), -200)", - - // "(\"hello\")", - // "(record { name = \"John\"; age = 30; })", - // "(vec { 100; })", - // "(variant { email = opt (\"example@gmail.com\") })", - // "(principal \"aaaaa-aa\")", - // "(100, \"hello\", record { name = \"John\"; age = 30; })", - // ]; - // }, - // ), + it( + "print out args", + do { + type User = { + name : Text; + details : { + age : Nat; + email : ?Text; + registered : Bool; + }; + }; + + let candid = #Record([ + ("name", #Text("candid")), + ("details", #Record([("age", #Nat(32)), ("email", #Option(#Text("example@gmail.com"))), ("registered", #Bool(true))])), + ]); + + let #ok(blob) = Candid.encodeOne(candid, null); + + let mo : ?User = from_candid (blob); + mo == ?{ + name = "candid"; + details = { + age = 32; + email = ?"example@gmail.com"; + registered = true; + }; + }; + }, + ), + + describe( + "fromText()", + [ + describe( + "parsing Int and Nat formats", + [ + it( + "parse \"quoted text\" to #Text", + do { + assertAllTrue([ + Candid.fromText("( \"\" )") == [#Text("")], + Candid.fromText("( \"hello\" )") == [#Text("hello")], + Candid.fromText("(\"hello world\")") == [#Text("hello world")], + Candid.fromText("(\"1_000_000\")") == [#Text("1_000_000")], + ]); + }, + ), + it( + "parse blob type", + do { + assertAllTrue([ + Candid.fromText("(blob \"\")") == [#Blob(Blob.fromArray([]))], + Candid.fromText("(blob \"\\AB\\CD\\EF\")") == [#Blob(Blob.fromArray([0xAB, 0xCD, 0xEF]))], + Candid.fromText("(blob \"\\CA\\FF\\FE\")") == [#Blob(Blob.fromArray([0xCA, 0xFF, 0xFE]))], + ]); + }, + ), + it( + "should parse principal type", + do { + assertAllTrue([ + Candid.fromText("(principal \"aaaaa-aa\")") == [#Principal(Principal.fromText("aaaaa-aa"))], + Candid.fromText("(principal \"w7x7r-cok77-xa\")") == [#Principal(Principal.fromText("w7x7r-cok77-xa"))], + ]); + }, + ), + it( + "Positive integers to #Nat", + do { + assertAllTrue([ + Candid.fromText("( 1000)") == [#Nat(1000)], + Candid.fromText("(+2000)") == [#Nat(2000)], + ]); + }, + ), + it( + "Negative integers to #Int", + do { + assertAllTrue([ + Candid.fromText("(-3000)") == [#Int(-3000)], + Candid.fromText("(-4000)") == [#Int(-4000)], + ]); + }, + ), + it( + "should parse Int/Nats with leading zeroes", + do { + assertAllTrue([ + Candid.fromText("(001)") == [#Nat(1)], + Candid.fromText("((+00123))") == [#Nat(123)], + Candid.fromText("(-0123)") == [#Int(-0123)], + ]); + }, + ), + it( + "should parse Int/Nat with underscores", + do { + assertAllTrue([ + Candid.fromText("( 1_000)") == [#Nat(1000)], + Candid.fromText("(+1_000_000)") == [#Nat(1000000)], + + Candid.fromText("(-1_000 )") == [#Int(-1000)], + Candid.fromText("((-1_000_000))") == [#Int(-1000000)], + ]); + }, + ), + it( + "should parse Int/Nat in hex format", + do { + assertAllTrue([ + Candid.fromText("(0x10)") == [#Nat(16)], + Candid.fromText("(0xdead_beef)") == [#Nat(3_735_928_559)], + Candid.fromText("(0xDEAD_BEEF)") == [#Nat(3_735_928_559)], + + Candid.fromText("(+0xa1_b2)") == [#Nat(41_394)], + Candid.fromText("(-0xA1_B2)") == [#Int(-41_394)], + + Candid.fromText("(-0xABC_def)") == [#Int(-11_259_375)], + ]); + }, + ), + it( + "should parse types with nested brackets", + do { + Candid.fromText("( ( ( ( 100_000 ) ) ) )") == [#Nat(100_000)]; + }, + ), + it( + "should parse 'opt' type", + do { + assertAllTrue([ + Candid.fromText("(opt 100)") == [#Option(#Nat(100))], + Candid.fromText("(opt null)") == [#Option(#Null)], + Candid.fromText("(opt (-0xdead_beef))") == [#Option(#Int(-3_735_928_559))], + Candid.fromText("(opt \"hello\")") == [#Option(#Text("hello"))], + Candid.fromText("(opt true)") == [#Option(#Bool(true))], + Candid.fromText("(opt (blob \"\\AB\\CD\\EF\\12\"))") == [#Option(#Blob(Blob.fromArray([0xAB, 0xCD, 0xEF, 0x12])))], + Candid.fromText("(opt (principal \"w7x7r-cok77-xa\"))") == [#Option(#Principal(Principal.fromText("w7x7r-cok77-xa")))], + ]); + }, + ), + describe( + "should parse 'vec' type to #Array", + [ + it( + "parse different element types", + do { + assertAllTrue([ + Candid.fromText("(vec {})") == [#Array([])], + Candid.fromText("(vec { 100; 200; 0xAB })") == [#Array([#Nat(100), #Nat(200), #Nat(0xAB)])], + Candid.fromText("(vec { \"hello\"; \"world\"; })") == [#Array([#Text("hello"), #Text("world")])], + Candid.fromText("(vec { true; false })") == [#Array([#Bool(true), #Bool(false)])], + Candid.fromText("(vec { blob \"\\AB\\CD\"; blob \"\\EF\\12\" })") == [#Array([#Blob(Blob.fromArray([0xAB, 0xCD])), #Blob(Blob.fromArray([0xEF, 0x12]))])], + Candid.fromText("(vec { principal \"w7x7r-cok77-xa\"; principal \"aaaaa-aa\"; })") == [#Array([#Principal(Principal.fromText("w7x7r-cok77-xa")), #Principal(Principal.fromText("aaaaa-aa"))])], + ]); + }, + ), + it( + "parse nested array", + do { + assertAllTrue([ + Candid.fromText("(vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } })") == [#Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])])], + + Candid.fromText("(vec { vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } }; vec { vec { 100; 200; 0xAB }; vec { 100; 200; 0xAB } } })") == [#Array([#Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])]), #Array([#Array([#Nat(100), #Nat(200), #Nat(0xAB)]), #Array([#Nat(100), #Nat(200), #Nat(0xAB)])])])], + ]); + }, + ), + ], + ), + + it( + "parse record type", + do { + assertAllTrue([ + Candid.fromText("(record {})") == [#Record([])], + Candid.fromText("(record { first_name = \"John\"; second_name = \"Doe\" })") == [#Record([("first_name", #Text("John")), ("second_name", #Text("Doe"))])], + Candid.fromText("(record { \"name with spaces\" = 42; \"unicode, too: ☃\" = true; })") == [#Record([("name with spaces", #Nat(42)), ("unicode, too: ☃", #Bool(true))])], + // nested record + Candid.fromText("(record { first_name = \"John\"; second_name = \"Doe\"; address = record { street = \"Main Street\"; city = \"New York\"; } })") == [#Record([("first_name", #Text("John")), ("second_name", #Text("Doe")), ("address", #Record([("street", #Text("Main Street")), ("city", #Text("New York"))]))])], + ]); + }, + ), + it( + "parser variant type", + do { + assertAllTrue([ + Candid.fromText("(variant { ok = \"hello\" })") == [#Variant(("ok", #Text("hello")))], + + // variant without a value + Candid.fromText("(variant { \"ok\" })") == [#Variant(("ok", #Null))], + + // variant with unicode key + Candid.fromText("(variant { \"unicode, too: ☃\" = \"hello\" })") == [#Variant(("unicode, too: ☃", #Text("hello")))], + Candid.fromText("(variant { \"☃\" })") == [#Variant(("☃", #Null))], + + // variant with record value + Candid.fromText("(variant { ok = record { \"first name\" = \"John\"; second_name = \"Doe\" } })") == [#Variant(("ok", #Record([("first name", #Text("John")), ("second_name", #Text("Doe"))])))], + + // variant with array value + Candid.fromText("(variant { ok = vec { 100; 200; 0xAB } })") == [#Variant(("ok", #Array([#Nat(100), #Nat(200), #Nat(0xAB)])))], + + // variant with variant value + Candid.fromText("(variant { ok = variant { status = \"active\" } })") == [#Variant(("ok", #Variant(("status", #Text("active")))))], + + ]); + }, + ), + + describe( + "should parse NatX types with type annotations", + [ + it( + "Nat8", + do { + assertAllTrue([ + Candid.fromText("(100 : nat8)") == [#Nat8(100 : Nat8)], + Candid.fromText("(00123:nat8)") == [#Nat8(123 : Nat8)], + Candid.fromText("(1_2_3 : nat8)") == [#Nat8(123 : Nat8)], + Candid.fromText("(0xA1 : nat8)") == [#Nat8(161 : Nat8)], + ]); + }, + ), + it( + "Nat16", + do { + assertAllTrue([ + Candid.fromText("((1000 : nat16))") == [#Nat16(1000 : Nat16)], + Candid.fromText("(0061234 : nat16)") == [#Nat16(61234 : Nat16)], + Candid.fromText("(32_892 : nat16)") == [#Nat16(32_892 : Nat16)], + Candid.fromText("(0xBEEF : nat16)") == [#Nat16(48_879 : Nat16)], + ]); + }, + ), + it( + "Nat32", + do { + assertAllTrue([ + Candid.fromText("((1_000_000 : nat32))") == [#Nat32(1_000_000 : Nat32)], + Candid.fromText("(0xdead_beef : nat32)") == [#Nat32(3_735_928_559 : Nat32)], + ]); + }, + ), + it( + "Nat64", + do { + assertAllTrue([ + Candid.fromText("((100_000_000_000 : nat64))") == [#Nat64(100_000_000_000 : Nat64)], + Candid.fromText("(0xdead_beef_1234 : nat64)") == [#Nat64(244_837_814_047_284 : Nat64)], + ]); + }, + ), + it( + "Nat", + do { + assertAllTrue([ + Candid.fromText("((100_000_000_000 : nat))") == [#Nat(100_000_000_000)], + Candid.fromText("(0xdead_beef_1234 : nat)") == [#Nat(244_837_814_047_284)], + ]); + }, + ), + ], + ), + describe( + "should parse IntX types with type annotations", + [ + it( + "Int8", + do { + assertAllTrue([ + Candid.fromText("((+100 : int8))") == [#Int8(100 : Int8)], + Candid.fromText("(-00123:int8)") == [#Int8(-123 : Int8)], + Candid.fromText("(-1_2_3 : int8)") == [#Int8(-123 : Int8)], + Candid.fromText("(-0x7A : int8)") == [#Int8(-122 : Int8)], + ]); + }, + ), + it( + "Int16", + do { + assertAllTrue([ + Candid.fromText("((+1000 : int16))") == [#Int16(1000 : Int16)], + Candid.fromText("(+0031234 : int16)") == [#Int16(31234 : Int16)], + Candid.fromText("(-31_234 : int16)") == [#Int16(-31_234 : Int16)], + Candid.fromText("(-0x7A_BC : int16)") == [#Int16(-31_420 : Int16)], + ]); + }, + ), + it( + "Int32", + do { + assertAllTrue([ + Candid.fromText("((+1_000_000 : int32))") == [#Int32(1_000_000 : Int32)], + Candid.fromText("(-0xbad_beef : int32)") == [#Int32(-195_935_983 : Int32)], + ]); + }, + ), + it( + "Int64", + do { + assertAllTrue([ + Candid.fromText("(+100_000_000_000 : int64)") == [#Int64(100_000_000_000 : Int64)], + Candid.fromText("((-0xdead_beef_1234 : int64))") == [#Int64(-244_837_814_047_284 : Int64)], + ]); + }, + ), + it( + "Int", + do { + assertAllTrue([ + Candid.fromText("(+100_000_000_000 : int)") == [#Int(100_000_000_000)], + Candid.fromText("((-0xdead_beef_1234 : int))") == [#Int(-244_837_814_047_284)], + ]); + }, + ), + ], + ), + + ], + ), + ], + ), + it( + "toText() should parse candid text", + + do { + let candid = [ + Candid.toText([#Nat(100)]), + Candid.toText([#Nat8(200 : Nat8)]), + Candid.toText([#Nat16(300 : Nat16)]), + Candid.toText([#Nat32(400 : Nat32)]), + Candid.toText([#Nat64(500 : Nat64)]), + + Candid.toText([#Int(600)]), + Candid.toText([#Int8(-70 : Int8)]), + Candid.toText([#Int16(800 : Int16)]), + Candid.toText([#Int32(-900 : Int32)]), + Candid.toText([#Int64(1000 : Int64)]), + + Candid.toText([#Nat8(100 : Nat8), #Int(-200)]), + + Candid.toText([#Text("hello")]), + Candid.toText([#Record([("name", #Text("John")), ("age", #Nat(30))])]), + Candid.toText([#Array([#Nat((100))])]), + Candid.toText([#Variant(("email", #Option(#Text("example@gmail.com"))))]), + Candid.toText([#Principal(Principal.fromText("aaaaa-aa"))]), + Candid.toText([#Nat(100), #Text("hello"), #Record([("name", #Text("John")), ("age", #Nat(30))])]), + ]; + + candid == [ + "(100)", + "(200 : nat8)", + "(300 : nat16)", + "(400 : nat32)", + "(500 : nat64)", + + "(600)", + "(-70 : int8)", + "(800 : int16)", + "(-900 : int32)", + "(1000 : int64)", + + "((100 : nat8), -200)", + + "(\"hello\")", + "(record { name = \"John\"; age = 30; })", + "(vec { 100; })", + "(variant { email = opt (\"example@gmail.com\") })", + "(principal \"aaaaa-aa\")", + "(100, \"hello\", record { name = \"John\"; age = 30; })", + ]; + }, + ), ], ), ]);