├── submodules ├── json.mo │ ├── .gitignore │ ├── Makefile │ ├── .github │ │ └── workflows │ │ │ └── test.yml │ ├── test │ │ ├── Array.mo │ │ ├── Show.mo │ │ └── Value.mo │ ├── README.md │ └── src │ │ └── JSON.mo └── parser-combinators.mo │ ├── .gitignore │ ├── README.md │ ├── Makefile │ ├── test │ ├── Nat.mo │ └── Int.mo │ ├── .github │ └── workflows │ │ └── test.yml │ └── src │ ├── List.mo │ ├── Parser.mo │ └── Combinators.mo ├── .prettierrc.json ├── .gitignore ├── makefile ├── src ├── UrlEncoded │ ├── lib.mo │ ├── ToText.mo │ ├── Parser.mo │ └── FromText.mo ├── JSON │ ├── lib.mo │ ├── FromText.mo │ └── ToText.mo ├── lib.mo ├── Candid │ ├── Text │ │ ├── Parser │ │ │ ├── Bool.mo │ │ │ ├── Option.mo │ │ │ ├── Principal.mo │ │ │ ├── Variant.mo │ │ │ ├── Float.mo │ │ │ ├── Array.mo │ │ │ ├── Int.mo │ │ │ ├── Blob.mo │ │ │ ├── Nat.mo │ │ │ ├── IntX.mo │ │ │ ├── NatX.mo │ │ │ ├── Text.mo │ │ │ ├── Record.mo │ │ │ ├── Common.mo │ │ │ └── lib.mo │ │ └── ToText.mo │ ├── lib.mo │ ├── Types.mo │ ├── ICRC3Value.mo │ └── Blob │ │ ├── RepIndyHash.mo │ │ └── CandidUtils.mo ├── CBOR │ └── lib.mo └── Utils.mo ├── dfx.json ├── mops.toml ├── tests ├── test_template.md ├── RepIndyHash.Test.mo ├── ICRC3.Test.mo ├── CandidTestUtils.mo ├── Candid.Large.test.mo ├── CBOR.Test.mo └── UrlEncoded.Test.mo ├── LICENSE ├── .github └── workflows │ ├── makefile.yml │ └── benchmarks.yml ├── usage.md ├── .bench └── serde.bench.json ├── bench.md ├── readme.md └── bench └── serde.bench.mo /submodules/json.mo/.gitignore: -------------------------------------------------------------------------------- 1 | .vessel/ 2 | 3 | .mops 4 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/.gitignore: -------------------------------------------------------------------------------- 1 | .vessel/ 2 | .mops/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/README.md: -------------------------------------------------------------------------------- 1 | # Parser Combinators 2 | 3 | Based on Monadic Parser Combinators by Graham Hutton and Erik Meijer. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Various IDEs and Editors 2 | .vscode/ 3 | .idea/ 4 | **/*~ 5 | 6 | # Mac OSX temporary files 7 | .DS_Store 8 | **/.DS_Store 9 | 10 | # dfx temporary files 11 | .dfx/ 12 | .vessel/ 13 | .mops/ 14 | .wasm/ 15 | 16 | # frontend code 17 | node_modules/ 18 | dist/ 19 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test compile-tests docs no-warn 2 | 3 | test: 4 | mops test 5 | 6 | check: 7 | find src -type f -name '*.mo' -print0 | \ 8 | xargs -0 $(shell mops toolchain bin moc) -r $(shell mops sources) -Werror -wasi-system-api 9 | 10 | bench: 11 | mops bench --gc generational -------------------------------------------------------------------------------- /submodules/json.mo/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check test 2 | check: 3 | find src -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources 2>/dev/null) --check 4 | test: 5 | find test -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources 2>/dev/null) -r 6 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check test 2 | check: 3 | find src -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources 2>/dev/null) --check 4 | test: 5 | find test -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc $(shell vessel sources 2>/dev/null) -r 6 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/test/Nat.mo: -------------------------------------------------------------------------------- 1 | import C "../src/Combinators"; 2 | import L "../src/List"; 3 | 4 | let nat = C.Nat.nat(); 5 | switch (nat(L.fromText("100"))) { 6 | case (null) { assert(false); }; 7 | case (? (x, xs)) { 8 | assert(x == 100); 9 | assert(xs == null); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /submodules/json.mo/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v3 7 | - uses: aviate-labs/setup-dfx@v0.2.3 8 | with: 9 | dfx-version: 0.12.1 10 | vessel-version: 0.6.3 11 | - run: | 12 | make check 13 | make test 14 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v3 7 | - uses: aviate-labs/setup-dfx@v0.2.3 8 | with: 9 | dfx-version: 0.12.1 10 | vessel-version: 0.6.3 11 | - run: | 12 | make check 13 | make test 14 | -------------------------------------------------------------------------------- /src/UrlEncoded/lib.mo: -------------------------------------------------------------------------------- 1 | /// A module for converting between Motoko values and Url-Encoded `Text`. 2 | 3 | import Candid "../Candid"; 4 | import FromText "./FromText"; 5 | import ToText "./ToText"; 6 | 7 | import Utils "../Utils"; 8 | module { 9 | public let { fromText; toCandid } = FromText; 10 | 11 | public let { toText; fromCandid } = ToText; 12 | 13 | public let concatKeys = Utils.concatKeys; 14 | public let defaultOptions = Candid.defaultOptions; 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "defaults": { 4 | "build": { 5 | "packtool": "mops sources", 6 | "args": "" 7 | } 8 | }, 9 | "canisters": { 10 | "benchmarks": { 11 | "type": "motoko", 12 | "main": "benchmarks/serde.bench.mo" 13 | } 14 | }, 15 | "networks": { 16 | "local": { 17 | "bind": "127.0.0.1:8000", 18 | "type": "ephemeral" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/JSON/lib.mo: -------------------------------------------------------------------------------- 1 | /// A module for converting between JSON and Motoko values. 2 | 3 | import JSON "../../submodules/json.mo/src/JSON"; 4 | 5 | import Candid "../Candid"; 6 | import FromText "FromText"; 7 | import ToText "ToText"; 8 | import Utils "../Utils"; 9 | 10 | module { 11 | public type JSON = JSON.JSON; 12 | public let defaultOptions = Candid.defaultOptions; 13 | 14 | public let { fromText; toCandid } = FromText; 15 | 16 | public let { toText; fromCandid } = ToText; 17 | 18 | public let concatKeys = Utils.concatKeys; 19 | }; 20 | -------------------------------------------------------------------------------- /submodules/json.mo/test/Array.mo: -------------------------------------------------------------------------------- 1 | import JSON "../src/JSON"; 2 | 3 | let v = switch (JSON.parse("[ 5, 1, 2, 4, 5, 6 ]")) { 4 | case (null) { 5 | assert(false); 6 | #Null; 7 | }; 8 | case (? v) switch (v) { 9 | case (#Array(v)) { 10 | assert(v.size() == 6); 11 | for (v in v.vals()) switch (v) { 12 | case (#Number(v)) {}; 13 | case (_) assert(false); 14 | }; 15 | #Array(v); 16 | }; 17 | case (_) { 18 | assert(false); 19 | #Null; 20 | }; 21 | }; 22 | }; 23 | 24 | assert(JSON.show(v) == "[5, 1, 2, 4, 5, 6]"); 25 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/test/Int.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.7.3/List"; 2 | 3 | import C "../src/Combinators"; 4 | import L "../src/List"; 5 | 6 | let int = C.Int.int(); 7 | switch (int(L.fromText("-100"))) { 8 | case (null) { assert (false) }; 9 | case (?(x, xs)) { 10 | assert (x == -100); 11 | assert (xs == null); 12 | }; 13 | }; 14 | 15 | let ints = C.bracket( 16 | C.Character.char('['), 17 | C.sepBy1(int, C.Character.char(',')), 18 | C.Character.char(']'), 19 | ); 20 | switch (ints(L.fromText("[-100,1,15]"))) { 21 | case (null) { assert (false) }; 22 | case (?(x, xs)) { 23 | assert (List.toArray(x) == [-100, 1, 15]); 24 | assert (xs == null); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /submodules/json.mo/test/Show.mo: -------------------------------------------------------------------------------- 1 | import JSON "../src/JSON"; 2 | import Text "mo:base@0.7.3/Text"; 3 | import Debug "mo:base@0.7.3/Debug"; 4 | 5 | assert (JSON.show(#String("hello")) == "\"hello\""); 6 | assert (JSON.show(#Number(1)) == "1"); 7 | assert (JSON.show(#Number(-1)) == "-1"); 8 | 9 | assert (JSON.show(#Float(-3.14)) == "-3.14"); 10 | assert (JSON.show(#Float(1.234e-4)) == "0.00"); 11 | assert (JSON.show(#Float(43e-02)) == "0.43"); 12 | 13 | assert ( 14 | JSON.show( 15 | #Object([ 16 | ("givenName", #String("John")), 17 | ("familyName", #String("Doe")), 18 | ("favNumber", #Number(5)), 19 | ]) 20 | ) == "{\"givenName\": \"John\", \"familyName\": \"Doe\", \"favNumber\": 5}" 21 | ); 22 | -------------------------------------------------------------------------------- /mops.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde" 3 | version = "3.4.0" 4 | description = "A serialisation and deserialisation library for Motoko." 5 | repository = "https://github.com/NatLabs/serde" 6 | keywords = ["json", "candid", "cbor", "urlencoded", "serialization"] 7 | license = "MIT" 8 | 9 | [dependencies] 10 | "base@0.16.0" = "0.16.0" 11 | "itertools@0.2.2" = "0.2.2" 12 | "xtended-numbers@2.0.0" = "2.0.0" 13 | "cbor@4.0.0" = "4.0.0" 14 | "map@9.0.1" = "9.0.1" 15 | "sha2@0.1.6" = "0.1.6" 16 | "byte-utils@0.1.2" = "0.1.2" 17 | "base@0.7.3" = "0.7.3" 18 | 19 | [dev-dependencies] 20 | test = "2.1.1" 21 | fuzz = "1.0.0" 22 | rep-indy-hash = "0.1.1" 23 | candid = "2.0.0" 24 | bench = "1.0.0" 25 | 26 | [toolchain] 27 | wasmtime = "33.0.1" 28 | moc = "0.16.2" 29 | -------------------------------------------------------------------------------- /src/lib.mo: -------------------------------------------------------------------------------- 1 | import CandidType "Candid/Types"; 2 | import UrlEncodedModule "UrlEncoded"; 3 | import JsonModule "JSON"; 4 | import CandidModule "Candid"; 5 | import CborModule "CBOR"; 6 | 7 | import Utils "Utils"; 8 | 9 | module { 10 | 11 | public type Options = CandidType.Options; 12 | 13 | public type Candid = CandidType.Candid; 14 | public let Candid = CandidModule; 15 | 16 | public type CandidType = CandidType.CandidType; 17 | 18 | public type ICRC3Value = CandidType.ICRC3Value; 19 | 20 | public let JSON = JsonModule; 21 | public let URLEncoded = UrlEncodedModule; 22 | public let CBOR = CborModule; 23 | 24 | public let concatKeys = Utils.concatKeys; 25 | public let defaultOptions = CandidType.defaultOptions; 26 | }; 27 | -------------------------------------------------------------------------------- /tests/test_template.md: -------------------------------------------------------------------------------- 1 | Filename: `[Section]/[Function].Test.mo` 2 | 3 | ```motoko 4 | import Debug "mo:base@0.16.0/Debug"; 5 | import Iter "mo:base@0.16.0/Iter"; 6 | 7 | import ActorSpec "../utils/ActorSpec"; 8 | import Algo "../../src"; 9 | // import [FnName] "../../src/[section]/[FnName]"; 10 | 11 | let { 12 | assertTrue; assertFalse; assertAllTrue; 13 | describe; it; skip; pending; run 14 | } = ActorSpec; 15 | 16 | let success = run([ 17 | describe(" (Function Name) ", [ 18 | it("(test name)", do { 19 | 20 | // ... 21 | }), 22 | ]) 23 | ]); 24 | 25 | if(success == false){ 26 | Debug.trap("\1b[46;41mTests failed\1b[0m"); 27 | }else{ 28 | Debug.print("\1b[23;42;3m Success!\1b[0m"); 29 | }; 30 | 31 | ``` -------------------------------------------------------------------------------- /submodules/json.mo/README.md: -------------------------------------------------------------------------------- 1 | # JSON 2 | 3 | [JSON](https://www.json.org/) 4 | 5 | ## Example 6 | 7 | ```motoko 8 | import JSON "mo:json/JSON"; 9 | import Debug "mo:base@0.16.0/Debug"; 10 | 11 | let obj : JSON.JSON = #Object([]); 12 | Debug.print(JSON.show(obj)); 13 | // {} 14 | 15 | Debug.print(JSON.show(#Object([("username", #String("di-wu"))]))); 16 | // {"username": "di-wu"} 17 | 18 | Debug.print(JSON.show(#Object([ 19 | ("name", #Object([ 20 | ("firstName", #String("quint")) 21 | ])), 22 | ("username", #String("di-wu")) 23 | ]))); 24 | // {"name": {"firstName": "quint"}, "username": "di-wu"} 25 | ``` 26 | 27 | > Note: The `#Float` type only formats to 2 decimal places. 28 | 29 | ```motoko 30 | import JSON "mo:json/JSON"; 31 | import Debug "mo:base@0.16.0/Debug"; 32 | 33 | Debug.print(JSON.show(#Object([("amount", #Float(32.4829))]))); 34 | // {"amount": "32.48"} 35 | ``` -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/src/List.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base@0.7.3/Iter"; 2 | import List "mo:base@0.7.3/List"; 3 | 4 | module { 5 | private type List = List.List; 6 | 7 | public func fromText(t : Text) : List { 8 | fromIter(t.chars()); 9 | }; 10 | 11 | public func fromIter(i : Iter.Iter) : List { 12 | switch (i.next()) { 13 | case (null) { null }; 14 | case (?v) { ?(v, fromIter(i)) }; 15 | }; 16 | }; 17 | 18 | public class toIter(xs : List) : Iter.Iter { 19 | var list = xs; 20 | public func next() : ?T { 21 | switch (list) { 22 | case (null) { null }; 23 | case (?ys) { 24 | let (x, xs) = ys; 25 | list := xs; 26 | ?x; 27 | }; 28 | }; 29 | }; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Bool.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.16.0/List"; 2 | 3 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 4 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 5 | 6 | import Candid "../../Types"; 7 | import { ignoreSpace } "Common"; 8 | 9 | module { 10 | type Candid = Candid.Candid; 11 | type List = List.List; 12 | 13 | type Parser = P.Parser; 14 | 15 | public func boolParser() : Parser { 16 | C.map(parseBool(), func(b : Bool) : Candid { #Bool(b) }); 17 | }; 18 | 19 | func parseBool() : Parser { 20 | C.map( 21 | ignoreSpace( 22 | C.oneOf([ 23 | C.String.string("true"), 24 | C.String.string("false"), 25 | ]) 26 | ), 27 | func(t : Text) : Bool { 28 | switch (t) { 29 | case ("true") true; 30 | case (_) false; 31 | }; 32 | }, 33 | ); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomi Jaga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/src/Parser.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.7.3/List"; 2 | 3 | module Parser { 4 | private type List = List.List; 5 | public type Parser = List -> ?(A, List); 6 | 7 | // Succeeds without consuming any of the input, and returns the single result x. 8 | public func result(a : A) : Parser { 9 | func(xs : List) { ?(a, xs) }; 10 | }; 11 | 12 | // Always fails, regardless of the input. 13 | public func zero() : Parser { 14 | func(_ : List) { null }; 15 | }; 16 | 17 | // Successfully consumes the first item if the input is non-empty, and fails otherwise. 18 | public func item() : Parser { 19 | func(xs : List) { 20 | switch (xs) { 21 | case (null) { null }; 22 | case (?(x, xs)) { 23 | ?(x, xs); 24 | }; 25 | }; 26 | }; 27 | }; 28 | 29 | // Delays the recursion. 30 | public func delay( 31 | function : () -> Parser 32 | ) : Parser { 33 | func(xs : List) { function()(xs) }; 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Option.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.16.0/List"; 2 | 3 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 4 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 5 | 6 | import Candid "../../Types"; 7 | 8 | import { ignoreSpace } "Common"; 9 | 10 | module { 11 | type Candid = Candid.Candid; 12 | type List = List.List; 13 | 14 | type Parser = P.Parser; 15 | 16 | public func optionParser(candidParser : () -> Parser) : Parser { 17 | C.map( 18 | ignoreSpace( 19 | C.right( 20 | C.String.string("opt"), 21 | ignoreSpace( 22 | P.delay(candidParser) 23 | ), 24 | ) 25 | ), 26 | func(candid : Candid) : Candid { 27 | #Option(candid); 28 | }, 29 | ); 30 | }; 31 | 32 | public func nullParser() : Parser { 33 | C.map( 34 | ignoreSpace(C.String.string("null")), 35 | func(_ : Text) : Candid { 36 | #Null; 37 | }, 38 | ); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Principal.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.16.0/List"; 2 | import Principal "mo:base@0.16.0/Principal"; 3 | 4 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 5 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 6 | 7 | import Candid "../../Types"; 8 | import { ignoreSpace; toText } "Common"; 9 | 10 | module { 11 | type Candid = Candid.Candid; 12 | type List = List.List; 13 | 14 | type Parser = P.Parser; 15 | 16 | public func principalParser() : Parser { 17 | C.map( 18 | C.right( 19 | C.String.string("principal"), 20 | ignoreSpace( 21 | C.bracket( 22 | C.String.string("\""), 23 | C.many1( 24 | C.oneOf([ 25 | C.Character.alphanum(), 26 | C.Character.char('-'), 27 | ]) 28 | ), 29 | C.String.string("\""), 30 | ) 31 | ), 32 | ), 33 | func(chars : List) : Candid { 34 | let text = toText(chars); 35 | #Principal(Principal.fromText(text)); 36 | }, 37 | ); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | name: Build and test 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - name: Cache Node modules 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | 29 | - name: Install dfx 30 | uses: dfinity/setup-dfx@main 31 | - name: Confirm successful installation 32 | run: dfx --version 33 | 34 | - name: Install dfx cache 35 | run: dfx cache install 36 | 37 | - name: Install mops & mocv 38 | run: | 39 | npm --yes -g i ic-mops 40 | mops i 41 | mops toolchain init 42 | mops toolchain use moc latest 43 | mops toolchain use wasmtime 14.0.4 44 | 45 | # set moc path for dfx to use 46 | echo "DFX_MOC_PATH=$(mops toolchain bin moc)" >> $GITHUB_ENV 47 | 48 | # - name: Detect warnings 49 | # run: make check 50 | 51 | - name: Run Tests 52 | run: make test 53 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Variant.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.16.0/List"; 2 | 3 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 4 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 5 | 6 | import Candid "../../Types"; 7 | import { ignoreSpace } "Common"; 8 | import { keyParser; fieldParser } = "Record"; 9 | 10 | module { 11 | type Candid = Candid.Candid; 12 | type List = List.List; 13 | 14 | type Parser = P.Parser; 15 | 16 | public func variantParser(candidParser : () -> Parser) : Parser { 17 | let emptyValueParser = C.map( 18 | keyParser(), 19 | func(key : Text) : (Text, Candid) { 20 | (key, #Null); 21 | }, 22 | ); 23 | 24 | C.map( 25 | C.right( 26 | C.String.string("variant"), 27 | ignoreSpace( 28 | C.bracket( 29 | C.String.string("{"), 30 | ignoreSpace( 31 | C.oneOf([ 32 | fieldParser(candidParser), 33 | emptyValueParser, 34 | ]) 35 | ), 36 | ignoreSpace(C.String.string("}")), 37 | ) 38 | ), 39 | ), 40 | func(variant : (Text, Candid)) : Candid { 41 | #Variant(variant); 42 | }, 43 | ); 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Float.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base@0.16.0/Float"; 2 | import List "mo:base@0.16.0/List"; 3 | 4 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 5 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 6 | 7 | import Candid "../../Types"; 8 | 9 | import { listToNat } "Common"; 10 | import { parseInt } "Int"; 11 | 12 | module { 13 | type Candid = Candid.Candid; 14 | type List = List.List; 15 | 16 | type Parser = P.Parser; 17 | 18 | public func floatParser() : Parser { 19 | 20 | func toFloat(tuple : (Int, List)) : Candid { 21 | let (n, d_chars) = tuple; 22 | 23 | let n_of_decimals = Float.fromInt(List.size(d_chars)); 24 | 25 | let num = Float.fromInt(n); 26 | let decimals = Float.fromInt(listToNat(d_chars)) / (10 ** n_of_decimals); 27 | 28 | let isNegative = num < 0; 29 | 30 | let float = if (isNegative) { 31 | num - decimals; 32 | } else { 33 | num + decimals; 34 | }; 35 | 36 | #Float(float); 37 | }; 38 | 39 | C.map( 40 | parseFloat(), 41 | toFloat, 42 | ); 43 | }; 44 | 45 | func parseFloat() : Parser)> { 46 | C.seq>( 47 | parseInt(), 48 | C.right( 49 | C.Character.char('.'), 50 | C.many1(C.Character.digit()), 51 | ), 52 | ); 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Array.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base@0.16.0/List"; 2 | 3 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 4 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 5 | 6 | import Candid "../../Types"; 7 | 8 | import { ignoreSpace } "Common"; 9 | 10 | module { 11 | type Candid = Candid.Candid; 12 | type List = List.List; 13 | 14 | type Parser = P.Parser; 15 | 16 | public func arrayParser(valueParser : () -> Parser) : Parser { 17 | C.map( 18 | C.right( 19 | ignoreSpace( 20 | C.String.string("vec") 21 | ), 22 | ignoreSpace( 23 | C.bracket( 24 | C.String.string("{"), 25 | ignoreSpace( 26 | C.sepBy( 27 | P.delay(valueParser), 28 | ignoreSpace(C.Character.char(';')), 29 | ) 30 | ), 31 | C.oneOf([ 32 | C.right( 33 | ignoreSpace(C.Character.char(';')), 34 | ignoreSpace(C.String.string("}")), 35 | ), 36 | ignoreSpace(C.String.string("}")), 37 | ]), 38 | ) 39 | ), 40 | ), 41 | func(list : List) : Candid { 42 | #Array(List.toArray(list)); 43 | }, 44 | ); 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Int.mo: -------------------------------------------------------------------------------- 1 | import Int "mo:base@0.16.0/Int"; 2 | import List "mo:base@0.16.0/List"; 3 | 4 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 5 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 6 | 7 | import Candid "../../Types"; 8 | 9 | import { parseNat } "Nat"; 10 | 11 | module { 12 | type Candid = Candid.Candid; 13 | type List = List.List; 14 | 15 | type Parser = P.Parser; 16 | 17 | public func intParser() : Parser { 18 | C.map( 19 | wrapNatToIntParser(parseNat()), 20 | func(n : Int) : Candid { 21 | if (n < 0) { 22 | #Int(n); 23 | } else { 24 | #Nat(Int.abs(n)); 25 | }; 26 | }, 27 | ); 28 | }; 29 | 30 | public func parseInt() : Parser { 31 | wrapNatToIntParser(parseNat()); 32 | }; 33 | 34 | func wrapNatToIntParser(natParser : Parser) : Parser { 35 | func(xs : List) : ?(Int, List) { 36 | 37 | let parseSign = C.oneOf([ 38 | C.Character.char('+'), 39 | C.Character.char('-'), 40 | ]); 41 | 42 | let (toInt, ys) = switch (parseSign(xs)) { 43 | case (null) { (func(n : Nat) : Int { n }, xs) }; 44 | case (?('+', xs)) { (func(n : Nat) : Int { n }, xs) }; 45 | case (?(_, xs)) { (func(n : Nat) : Int { -n }, xs) }; 46 | }; 47 | 48 | let mapToInt = C.map( 49 | natParser, 50 | toInt, 51 | ); 52 | 53 | mapToInt(ys); 54 | }; 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Blob.mo: -------------------------------------------------------------------------------- 1 | import Blob "mo:base@0.16.0/Blob"; 2 | import Iter "mo:base@0.16.0/Iter"; 3 | import List "mo:base@0.16.0/List"; 4 | 5 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 6 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 7 | 8 | import Candid "../../Types"; 9 | import { ignoreSpace; hexChar; fromHex } "Common"; 10 | 11 | module { 12 | type Candid = Candid.Candid; 13 | type List = List.List; 14 | 15 | type Parser = P.Parser; 16 | 17 | public func blobParser() : Parser { 18 | C.map( 19 | C.right( 20 | C.String.string("blob"), 21 | ignoreSpace( 22 | C.bracket( 23 | C.String.string("\""), 24 | C.sepBy( 25 | C.map( 26 | C.right( 27 | C.String.string("\\"), 28 | C.seq( 29 | hexChar(), 30 | hexChar(), 31 | ), 32 | ), 33 | func((c1, c2) : (Char, Char)) : Nat8 { 34 | (fromHex(c1) << 4) + fromHex(c2); 35 | }, 36 | ), 37 | C.String.string(""), // escapes char: '\' 38 | ), 39 | C.String.string("\""), 40 | ) 41 | ), 42 | ), 43 | func(chars : List) : Candid { 44 | let blob = Blob.fromArray(Iter.toArray(Iter.fromList(chars))); 45 | #Blob(blob); 46 | }, 47 | ); 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Nat.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base@0.16.0/Iter"; 2 | import List "mo:base@0.16.0/List"; 3 | import Nat64 "mo:base@0.16.0/Nat64"; 4 | 5 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 6 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 7 | import NatX "mo:xtended-numbers/NatX"; 8 | 9 | import Candid "../../Types"; 10 | import { ignoreSpace; hexChar; fromHex; removeUnderscore; listToNat } "Common"; 11 | 12 | module { 13 | type Candid = Candid.Candid; 14 | type List = List.List; 15 | 16 | type Parser = P.Parser; 17 | 18 | public func natParser() : Parser { 19 | C.map( 20 | parseNat(), 21 | func(n : Nat) : Candid { 22 | #Nat(n); 23 | }, 24 | ); 25 | }; 26 | 27 | public func parseNat() : Parser { 28 | C.oneOf([ 29 | parseNatFromHex(), 30 | parseNatWithUnderscore(), 31 | C.Nat.nat(), 32 | ]); 33 | }; 34 | 35 | func parseNatWithUnderscore() : Parser { 36 | C.map( 37 | ignoreSpace( 38 | removeUnderscore(C.Character.digit()) 39 | ), 40 | listToNat, 41 | ); 42 | }; 43 | 44 | func parseNatFromHex() : Parser { 45 | C.map( 46 | C.right( 47 | C.String.string("0x"), 48 | removeUnderscore(hexChar()), 49 | ), 50 | func(chars : List) : Nat { 51 | var n : Nat64 = 0; 52 | 53 | for (hex in Iter.fromList(chars)) { 54 | n := (n << 4) + NatX.from8To64(fromHex(hex)); 55 | }; 56 | 57 | // debug { Debug.print("hex_chars: " # debug_show (chars, n)) }; 58 | 59 | Nat64.toNat(n); 60 | }, 61 | ); 62 | }; 63 | 64 | }; 65 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/IntX.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base@0.16.0/Debug"; 2 | import List "mo:base@0.16.0/List"; 3 | import Int8 "mo:base@0.16.0/Int8"; 4 | import Int16 "mo:base@0.16.0/Int16"; 5 | import Int32 "mo:base@0.16.0/Int32"; 6 | import Int64 "mo:base@0.16.0/Int64"; 7 | 8 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 9 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 10 | 11 | import Candid "../../Types"; 12 | import { ignoreSpace } "Common"; 13 | import { parseInt } "Int"; 14 | 15 | module { 16 | type Candid = Candid.Candid; 17 | type List = List.List; 18 | 19 | type Parser = P.Parser; 20 | 21 | public func intXParser() : Parser { 22 | C.map( 23 | parseIntX(), 24 | func((int, intType) : (Int, Text)) : Candid { 25 | switch (intType) { 26 | case ("int") #Int(int); 27 | case ("int8") #Int8(Int8.fromInt(int)); 28 | case ("int16") #Int16(Int16.fromInt(int)); 29 | case ("int32") #Int32(Int32.fromInt(int)); 30 | case ("int64") #Int64(Int64.fromInt(int)); 31 | case (_) Debug.trap("Only int8, int16, int32, int64 int bit types but got '" # intType # "'"); 32 | }; 33 | }, 34 | ); 35 | }; 36 | 37 | func parseIntX() : Parser { 38 | C.seq( 39 | ignoreSpace( 40 | parseInt() 41 | ), 42 | C.right( 43 | ignoreSpace( 44 | C.Character.char(':') 45 | ), 46 | ignoreSpace( 47 | C.oneOf([ 48 | C.String.string("int64"), 49 | C.String.string("int32"), 50 | C.String.string("int16"), 51 | C.String.string("int8"), 52 | C.String.string("int"), 53 | ]) 54 | ), 55 | ), 56 | ); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/NatX.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base@0.16.0/Debug"; 2 | import List "mo:base@0.16.0/List"; 3 | import Nat8 "mo:base@0.16.0/Nat8"; 4 | import Nat16 "mo:base@0.16.0/Nat16"; 5 | import Nat32 "mo:base@0.16.0/Nat32"; 6 | import Nat64 "mo:base@0.16.0/Nat64"; 7 | 8 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 9 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 10 | 11 | import Candid "../../Types"; 12 | import { ignoreSpace } "Common"; 13 | import { parseNat } "Nat"; 14 | 15 | module { 16 | type Candid = Candid.Candid; 17 | type List = List.List; 18 | 19 | type Parser = P.Parser; 20 | 21 | public func natXParser() : Parser { 22 | C.map( 23 | parseNatX(), 24 | func((nat, natType) : (Nat, Text)) : Candid { 25 | switch (natType) { 26 | case ("nat") #Nat(nat); 27 | case ("nat8") #Nat8(Nat8.fromNat(nat)); 28 | case ("nat16") #Nat16(Nat16.fromNat(nat)); 29 | case ("nat32") #Nat32(Nat32.fromNat(nat)); 30 | case ("nat64") #Nat64(Nat64.fromNat(nat)); 31 | case (_) Debug.trap("Only nat8, nat16, nat32, nat64 nat bit types but got '" # natType # "'"); 32 | }; 33 | }, 34 | ); 35 | }; 36 | 37 | func parseNatX() : Parser { 38 | ignoreSpace( 39 | C.seq( 40 | parseNat(), 41 | ignoreSpace( 42 | C.right( 43 | C.Character.char(':'), 44 | ignoreSpace( 45 | C.oneOf([ 46 | C.String.string("nat64"), 47 | C.String.string("nat32"), 48 | C.String.string("nat16"), 49 | C.String.string("nat8"), 50 | C.String.string("nat"), 51 | ]) 52 | ), 53 | ) 54 | ), 55 | ) 56 | ); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Text.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.16.0/Char"; 2 | import Iter "mo:base@0.16.0/Iter"; 3 | import List "mo:base@0.16.0/List"; 4 | import Text "mo:base@0.16.0/Text"; 5 | 6 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 7 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 8 | 9 | import Candid "../../Types"; 10 | 11 | module { 12 | type Candid = Candid.Candid; 13 | type List = List.List; 14 | 15 | type Parser = P.Parser; 16 | 17 | public func textParser() : Parser { 18 | C.map( 19 | parseText(), 20 | func(text : Text) : Candid { 21 | #Text(text); 22 | }, 23 | ); 24 | }; 25 | 26 | public func parseText() : Parser { 27 | C.map( 28 | C.bracket( 29 | C.String.string("\""), 30 | C.many(textChar()), 31 | C.String.string("\""), 32 | ), 33 | func(chars : List) : Text { 34 | Text.fromIter(Iter.fromList(chars)); 35 | }, 36 | ); 37 | }; 38 | 39 | func textChar() : P.Parser = C.oneOf([ 40 | C.sat( 41 | func(c : Char) : Bool { 42 | c != Char.fromNat32(0x22) and c != '\\'; 43 | } 44 | ), 45 | C.right( 46 | C.Character.char('\\'), 47 | C.map( 48 | C.Character.oneOf([ 49 | Char.fromNat32(0x22), 50 | '\\', 51 | '/', 52 | 'b', 53 | 'f', 54 | 'n', 55 | 'r', 56 | 't', 57 | // TODO: u hex{4} 58 | ]), 59 | func(c : Char) : Char { 60 | switch (c) { 61 | case ('b') { Char.fromNat32(0x08) }; 62 | case ('f') { Char.fromNat32(0x0C) }; 63 | case ('n') { Char.fromNat32(0x0A) }; 64 | case ('r') { Char.fromNat32(0x0D) }; 65 | case ('t') { Char.fromNat32(0x09) }; 66 | case (_) { c }; 67 | }; 68 | }, 69 | ), 70 | ), 71 | ]); 72 | }; 73 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Record.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base@0.16.0/Iter"; 2 | import List "mo:base@0.16.0/List"; 3 | 4 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 5 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 6 | 7 | import Candid "../../Types"; 8 | import { ignoreSpace; toText } "Common"; 9 | import { parseText } "Text"; 10 | 11 | module { 12 | type Candid = Candid.Candid; 13 | type List = List.List; 14 | 15 | type Parser = P.Parser; 16 | 17 | public func recordParser(candidParser : () -> Parser) : Parser { 18 | C.map( 19 | C.right( 20 | C.String.string("record"), 21 | ignoreSpace( 22 | C.bracket( 23 | C.String.string("{"), 24 | C.sepBy( 25 | fieldParser(candidParser), 26 | ignoreSpace(C.Character.char(';')), 27 | ), 28 | C.oneOf([ 29 | C.right( 30 | ignoreSpace(C.Character.char(';')), 31 | ignoreSpace(C.String.string("}")), 32 | ), 33 | ignoreSpace(C.String.string("}")), 34 | ]), 35 | ) 36 | ), 37 | ), 38 | func(xs : List<(Text, Candid)>) : Candid { 39 | let records = Iter.toArray(Iter.fromList(xs)); 40 | #Record(records); 41 | }, 42 | ); 43 | }; 44 | 45 | public func fieldParser(valueParser : () -> Parser) : Parser { 46 | C.seq( 47 | ignoreSpace( 48 | C.left( 49 | keyParser(), 50 | ignoreSpace(C.Character.char('=')), 51 | ) 52 | ), 53 | ignoreSpace(P.delay(valueParser)), 54 | ); 55 | 56 | }; 57 | 58 | public func keyParser() : Parser { 59 | C.oneOf([ 60 | C.map( 61 | C.many1( 62 | C.choose( 63 | C.Character.alphanum(), 64 | C.Character.char('_'), 65 | ) 66 | ), 67 | toText, 68 | ), 69 | parseText(), 70 | ]); 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/JSON/FromText.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Result "mo:base@0.16.0/Result"; 3 | import Text "mo:base@0.16.0/Text"; 4 | import Int "mo:base@0.16.0/Int"; 5 | 6 | import JSON "../../submodules/json.mo/src/JSON"; 7 | 8 | import Candid "../Candid"; 9 | import U "../Utils"; 10 | import CandidType "../Candid/Types"; 11 | import Utils "../Utils"; 12 | 13 | module { 14 | type JSON = JSON.JSON; 15 | type Candid = Candid.Candid; 16 | type Result = Result.Result; 17 | 18 | /// Converts JSON text to a serialized Candid blob that can be decoded to motoko values using `from_candid()` 19 | public func fromText(rawText : Text, options : ?CandidType.Options) : Result { 20 | let candid_res = toCandid(rawText); 21 | let #ok(candid) = candid_res else return Utils.send_error(candid_res); 22 | Candid.encodeOne(candid, options); 23 | }; 24 | 25 | /// Convert JSON text to a Candid value 26 | public func toCandid(rawText : Text) : Result { 27 | let json = JSON.parse(rawText); 28 | 29 | switch (json) { 30 | case (?json) #ok(jsonToCandid(json)); 31 | case (_) #err("Failed to parse JSON text"); 32 | }; 33 | }; 34 | 35 | func jsonToCandid(json : JSON) : Candid { 36 | switch (json) { 37 | case (#Null) #Null; 38 | case (#Boolean(n)) #Bool(n); 39 | case (#Number(n)) { 40 | if (n < 0) { 41 | return #Int(n); 42 | }; 43 | 44 | #Nat(Int.abs(n)); 45 | }; 46 | case (#Float(n)) #Float(n); 47 | case (#String(n)) #Text(Text.replace(n, #text("\\\""), ("\""))); 48 | case (#Array(arr)) { 49 | let newArr = Array.map( 50 | arr, 51 | func(n : JSON) : Candid { 52 | jsonToCandid(n); 53 | }, 54 | ); 55 | 56 | #Array(newArr); 57 | }; 58 | case (#Object(objs)) { 59 | 60 | if (objs.size() == 1) { 61 | let (key, val) = objs[0]; 62 | 63 | if (Text.startsWith(key, #text "#")) { 64 | let tag = U.stripStart(key, #text "#"); 65 | return #Variant(tag, jsonToCandid(val)); 66 | }; 67 | }; 68 | 69 | let records = Array.map<(Text, JSON), (Text, Candid)>( 70 | objs, 71 | func((key, val) : (Text, JSON)) : (Text, Candid) { 72 | (key, jsonToCandid(val)); 73 | }, 74 | ); 75 | 76 | #Record(records); 77 | }; 78 | }; 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /tests/RepIndyHash.Test.mo: -------------------------------------------------------------------------------- 1 | import Blob "mo:base@0.16.0/Blob"; 2 | import Debug "mo:base@0.16.0/Debug"; 3 | 4 | import { test; suite } "mo:test"; 5 | 6 | import { Candid } "../src"; 7 | 8 | import RepIndyHash "mo:rep-indy-hash"; 9 | 10 | func validate_hash(candid_record : Candid.Candid, icrc3_value_record : RepIndyHash.Value) : Bool { 11 | let candid_hash = Candid.repIndyHash(candid_record); 12 | let expected = RepIndyHash.hash_val(icrc3_value_record) |> Blob.fromArray(_); 13 | 14 | candid_hash == expected; 15 | }; 16 | 17 | suite( 18 | "Representation Independent Hash Test", 19 | func() { 20 | test( 21 | "#Nat", 22 | func() { 23 | assert validate_hash(#Nat(1), #Nat(1)); 24 | assert validate_hash(#Nat(22345), #Nat(22345)); 25 | }, 26 | ); 27 | 28 | test( 29 | "#Int", 30 | func() { 31 | let candid_record : Candid.Candid = #Int(42); 32 | let icrc3_value_record : RepIndyHash.Value = #Int(42); 33 | 34 | assert validate_hash(candid_record, icrc3_value_record); 35 | 36 | }, 37 | ); 38 | 39 | test( 40 | "#Text", 41 | func() { 42 | let candid_record : Candid.Candid = #Text("hello"); 43 | let icrc3_value_record : RepIndyHash.Value = #Text("hello"); 44 | 45 | assert validate_hash(candid_record, icrc3_value_record); 46 | }, 47 | ); 48 | 49 | test( 50 | "#Blob", 51 | func() { 52 | let candid_record : Candid.Candid = #Blob("\00\01\02"); 53 | let icrc3_value_record : RepIndyHash.Value = #Blob("\00\01\02"); 54 | 55 | assert validate_hash(candid_record, icrc3_value_record); 56 | }, 57 | ); 58 | 59 | test( 60 | "#Array", 61 | func() { 62 | let candid_record : Candid.Candid = #Array([#Text("hello"), #Text("world")]); 63 | let icrc3_value_record : RepIndyHash.Value = #Array([#Text("hello"), #Text("world")]); 64 | 65 | assert validate_hash(candid_record, icrc3_value_record); 66 | }, 67 | ); 68 | 69 | test( 70 | "#Record/#Map", 71 | func() { 72 | let candid_record : Candid.Candid = #Map([ 73 | ("a", #Nat(1)), 74 | ("b", #Array([#Text("hello"), #Text("world")])), 75 | ("c", #Blob("\00\01\02")), 76 | ("d", #Int(42)), 77 | ]); 78 | let icrc3_value_record : RepIndyHash.Value = #Map([ 79 | ("a", #Nat(1)), 80 | ("b", #Array([#Text("hello"), #Text("world")])), 81 | ("c", #Blob("\00\01\02")), 82 | ("d", #Int(42)), 83 | ]); 84 | 85 | assert validate_hash(candid_record, icrc3_value_record); 86 | 87 | }, 88 | ); 89 | }, 90 | ); 91 | -------------------------------------------------------------------------------- /tests/ICRC3.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Array "mo:base@0.16.0/Array"; 3 | import Blob "mo:base@0.16.0/Blob"; 4 | import Debug "mo:base@0.16.0/Debug"; 5 | import Iter "mo:base@0.16.0/Iter"; 6 | import Principal "mo:base@0.16.0/Principal"; 7 | import Text "mo:base@0.16.0/Text"; 8 | 9 | import { test; suite } "mo:test"; 10 | 11 | import Serde "../src"; 12 | import CandidTestUtils "CandidTestUtils"; 13 | 14 | let { Candid } = Serde; 15 | 16 | suite( 17 | "Candid ICRC3 compatability Test", 18 | func() { 19 | test( 20 | "#Map", 21 | func() { 22 | let record = { a = 1; b = 2 }; 23 | 24 | let record_candid_blob = to_candid (record); 25 | 26 | let options = { 27 | Candid.defaultOptions with use_icrc_3_value_type = true; 28 | }; 29 | let #ok(record_candid) = Candid.decode(record_candid_blob, ["a", "b"], ?options); 30 | 31 | assert record_candid[0] == #Map([ 32 | ("a", #Nat(1)), 33 | ("b", #Nat(2)), 34 | ]); 35 | 36 | let RecordType : Candid.CandidType = #Map([("a", #Nat), ("b", #Nat)]); 37 | 38 | let #ok(record_candid_blob2) = CandidTestUtils.encode_with_types([RecordType], record_candid, ?options) else return assert false; 39 | 40 | assert record_candid_blob == record_candid_blob2; 41 | 42 | let ?record2 : ?{ a : Nat; b : Nat } = from_candid (record_candid_blob2); 43 | 44 | assert record2 == record; 45 | 46 | }, 47 | ); 48 | 49 | }, 50 | 51 | ); 52 | 53 | suite( 54 | "Connvert between motoko and ICRC3", 55 | func() { 56 | test( 57 | "motoko -> ICRC3", 58 | func() { 59 | 60 | type User = { id : Nat; name : Text }; 61 | 62 | let user : User = { name = "bar"; id = 112 }; 63 | 64 | let blob = to_candid (user); 65 | let #ok(candid_values) = Candid.decode(blob, ["name", "id"], null); 66 | let icrc3_values = Candid.toICRC3Value(candid_values); 67 | 68 | assert icrc3_values[0] == #Map([ 69 | ("id", #Nat(112)), 70 | ("name", #Text("bar")), 71 | ]); 72 | }, 73 | ); 74 | 75 | test( 76 | "ICRC3 -> motoko", 77 | func() { 78 | type User = { name : Text; id : Nat }; 79 | 80 | let icrc3 : Serde.ICRC3Value = #Map([ 81 | ("id", #Nat(112)), 82 | ("name", #Text("bar")), 83 | ]); 84 | 85 | let candid_values = Candid.fromICRC3Value([icrc3]); 86 | 87 | let UserType : Candid.CandidType = #Record([("name", #Text), ("id", #Nat)]); 88 | 89 | let #ok(blob) = CandidTestUtils.encode_with_types([UserType], candid_values, null) else return assert false; 90 | let user : ?User = from_candid (blob); 91 | 92 | assert user == ?{ name = "bar"; id = 112 }; 93 | }, 94 | ); 95 | }, 96 | 97 | ); 98 | -------------------------------------------------------------------------------- /src/Candid/lib.mo: -------------------------------------------------------------------------------- 1 | /// A representation of the Candid format with variants for all possible types. 2 | 3 | import Array "mo:base@0.16.0/Array"; 4 | import Text "mo:base@0.16.0/Text"; 5 | import Map "mo:map@9.0.1/Map"; 6 | 7 | import CandidEncoder "Blob/Encoder"; 8 | import Decoder "Blob/Decoder"; 9 | import RepIndyHash "Blob/RepIndyHash"; 10 | import CandidUtils "Blob/CandidUtils"; 11 | import TypedSerializerModule "Blob/TypedSerializer"; 12 | 13 | import Parser "Text/Parser"; 14 | import ToText "Text/ToText"; 15 | 16 | import T "Types"; 17 | import Utils "../Utils"; 18 | import ICRC3Value "ICRC3Value"; 19 | 20 | module { 21 | let { thash } = Map; 22 | 23 | /// A representation of the Candid format with variants for all possible types. 24 | public type Candid = T.Candid; 25 | public type Options = T.Options; 26 | public let defaultOptions = T.defaultOptions; 27 | 28 | public let TypedSerializer = TypedSerializerModule; 29 | public type TypedSerializer = TypedSerializerModule.TypedSerializer; 30 | 31 | public type CandidType = T.CandidType; 32 | 33 | /// Converts a motoko value to a [Candid](#Candid) value 34 | public let { encode; encodeOne } = CandidEncoder; 35 | 36 | public let repIndyHash = RepIndyHash.hash; 37 | 38 | /// Converts a [Candid](#Candid) value to a motoko value 39 | public let { decode } = Decoder; 40 | 41 | public func fromText(t : Text) : [Candid] { 42 | Parser.parse(t); 43 | }; 44 | 45 | public let { toText } = ToText; 46 | 47 | /// Formats a user provided Candid type 48 | /// It is required to format the Candid type before passing it as an option to the Candid encoder/decoder 49 | /// 50 | /// Additionally, all fields that have a name mapping added as a 'renameKeys' option should add the mapping to the function or rename the keys in the Candid type before passing it to the encoder/decoder 51 | /// Failure to do so will result in unexpected behavior 52 | public func formatCandidType(c : [CandidType], opt_rename_keys : ?[(Text, Text)]) : [CandidType] { 53 | let renaming_map = Map.new(); 54 | 55 | switch (opt_rename_keys) { 56 | case (?rename_keys) { 57 | for ((prev, new) in rename_keys.vals()) { 58 | ignore Map.put(renaming_map, thash, prev, new); 59 | }; 60 | }; 61 | case (_) {}; 62 | }; 63 | 64 | Array.map( 65 | c, 66 | func(c : CandidType) : CandidType { 67 | CandidUtils.format_candid_type(c, renaming_map); 68 | }, 69 | ); 70 | 71 | }; 72 | 73 | public func sortCandidType(c : [CandidType]) : [CandidType] { 74 | Array.map( 75 | c, 76 | func(c : CandidType) : CandidType { 77 | CandidUtils.sort_candid_type(c); 78 | }, 79 | ); 80 | }; 81 | 82 | public let concatKeys = Utils.concatKeys; 83 | 84 | /// Converts an array of ICRC3Value values to [Candid](#Candid) values 85 | public func fromICRC3Value(icrc3_values : [T.ICRC3Value]) : [Candid] { 86 | ICRC3Value.fromICRC3Value(icrc3_values); 87 | }; 88 | 89 | /// Converts an array of [Candid](#Candid) values to ICRC3Value values 90 | public func toICRC3Value(candid_values : [Candid]) : [T.ICRC3Value] { 91 | ICRC3Value.toICRC3Value(candid_values); 92 | }; 93 | 94 | public type ICRC3Value = T.ICRC3Value; 95 | 96 | }; 97 | -------------------------------------------------------------------------------- /submodules/json.mo/test/Value.mo: -------------------------------------------------------------------------------- 1 | import JSON "../src/JSON"; 2 | 3 | switch (JSON.parse("{ }")) { 4 | case (null) { assert(false); }; 5 | case (?v) { 6 | switch (v) { 7 | case (#Object(v)) { 8 | assert(v.size() == 0); 9 | }; 10 | case (_) { assert(false); }; 11 | } 12 | }; 13 | }; 14 | 15 | switch (JSON.parse("{ \"v\": 1 }")) { 16 | case (null) { assert(false); }; 17 | case (?v) { 18 | switch (v) { 19 | case (#Object(v)) { 20 | assert(v.size() == 1); 21 | switch (v[0]) { 22 | case (("v", #Number(v))) { 23 | assert(v == 1); 24 | }; 25 | case (_) { assert(false); }; 26 | }; 27 | }; 28 | case (_) { assert(false); }; 29 | } 30 | }; 31 | }; 32 | 33 | switch (JSON.parse("[ ]")) { 34 | case (null) { assert(false); }; 35 | case (?v) { 36 | switch (v) { 37 | case (#Array(v)) { 38 | assert(v.size() == 0); 39 | }; 40 | case (_) { assert(false); }; 41 | } 42 | }; 43 | }; 44 | 45 | switch (JSON.parse("[1, \"txt\"]")) { 46 | case (null) { assert(false); }; 47 | case (?v) { 48 | switch (v) { 49 | case (#Array(v)) { 50 | assert(v.size() == 2); 51 | switch (v[0]) { 52 | case (#Number(v)) { assert(v == 1); }; 53 | case (_) { assert(false); }; 54 | }; 55 | switch (v[1]) { 56 | case (#String(v)) { assert(v == "txt"); }; 57 | case (_) { assert(false); }; 58 | }; 59 | }; 60 | case (_) { assert(false); }; 61 | } 62 | }; 63 | }; 64 | 65 | switch (JSON.parse("\"\\\"quoted\\\"\"")) { 66 | case (null) { assert(false); }; 67 | case (?v) { 68 | switch (v) { 69 | case (#String(v)) { 70 | assert(v == "\"quoted\""); 71 | }; 72 | case (_) { assert(false); }; 73 | } 74 | }; 75 | }; 76 | 77 | switch (JSON.parse("-100")) { 78 | case (null) { assert(false); }; 79 | case (?v) { 80 | switch (v) { 81 | case (#Number(v)) { 82 | assert(v == -100); 83 | }; 84 | case (_) { assert(false); }; 85 | } 86 | }; 87 | }; 88 | 89 | switch (JSON.parse("true")) { 90 | case (null) { assert(false); }; 91 | case (?v) { 92 | switch (v) { 93 | case (#Boolean(v)) { 94 | assert(v == true); 95 | }; 96 | case (_) { assert(false); }; 97 | } 98 | }; 99 | }; 100 | 101 | switch (JSON.parse(" null")) { // Test with spaces. 102 | case (null) { assert(false); }; 103 | case (?v) { 104 | switch (v) { 105 | case (#Null) {}; 106 | case (_) { assert(false); }; 107 | } 108 | }; 109 | }; 110 | 111 | switch (JSON.parse(" 1.23")) { 112 | // Test with spaces. 113 | case (null) { assert (false) }; 114 | case (?v) { 115 | switch (v) { 116 | case (#Float(1.23)) {}; 117 | case (_) { assert (false) }; 118 | }; 119 | }; 120 | }; 121 | 122 | switch (JSON.parse(" 1.234e-4")) { 123 | // Test with spaces. 124 | case (null) { assert (false) }; 125 | case (?v) { 126 | switch (v) { 127 | case (#Float(1.234e-4)) {}; 128 | case (_) { assert (false) }; 129 | }; 130 | }; 131 | }; -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Common.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.16.0/Char"; 2 | import Iter "mo:base@0.16.0/Iter"; 3 | import List "mo:base@0.16.0/List"; 4 | import Nat32 "mo:base@0.16.0/Nat32"; 5 | import Text "mo:base@0.16.0/Text"; 6 | 7 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 8 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 9 | import NatX "mo:xtended-numbers/NatX"; 10 | 11 | import Candid "../../Types"; 12 | 13 | module { 14 | type Candid = Candid.Candid; 15 | type List = List.List; 16 | 17 | type Parser = P.Parser; 18 | 19 | // Parsers 20 | 21 | public func ignoreSpace(parser : P.Parser) : P.Parser { 22 | C.right( 23 | C.many(C.Character.space()), 24 | parser, 25 | ); 26 | }; 27 | 28 | public func removeUnderscore(parser : P.Parser) : P.Parser> { 29 | C.map( 30 | ignoreSpace( 31 | C.sepBy1, Char>( 32 | C.many1(parser), 33 | C.Character.char('_'), 34 | ) 35 | ), 36 | func(nested_lists : List>) : List { 37 | List.flatten(nested_lists); 38 | }, 39 | ); 40 | }; 41 | 42 | public func any() : Parser { 43 | C.sat( 44 | func(c : T) : Bool { true } 45 | ); 46 | }; 47 | 48 | public func hexChar() : Parser { 49 | C.sat( 50 | func(x : Char) : Bool { 51 | '0' <= x and x <= '9' or 'a' <= x and x <= 'f' or 'A' <= x and x <= 'F'; 52 | } 53 | ); 54 | }; 55 | 56 | public func consIf( 57 | parserA : Parser, 58 | parserAs : Parser>, 59 | cond : (A, List) -> Bool, 60 | ) : Parser> { 61 | C.bind( 62 | parserA, 63 | func(a : A) : Parser> { 64 | C.bind( 65 | parserAs, 66 | func(as : List) : Parser> { 67 | if (cond(a, as)) { 68 | P.result>(List.push(a, as)); 69 | } else { 70 | P.zero(); 71 | }; 72 | }, 73 | ); 74 | }, 75 | ); 76 | }; 77 | 78 | // Utilities 79 | 80 | public func fromHex(char : Char) : Nat8 { 81 | let charCode = Char.toNat32(char); 82 | 83 | if (Char.isDigit(char)) { 84 | let digit = charCode - Char.toNat32('0'); 85 | 86 | return NatX.from32To8(digit); 87 | }; 88 | 89 | if (Char.isUppercase(char)) { 90 | let digit = charCode - Char.toNat32('A') + 10; 91 | 92 | return NatX.from32To8(digit); 93 | }; 94 | 95 | // lowercase 96 | let digit = charCode - Char.toNat32('a') + 10; 97 | 98 | return NatX.from32To8(digit); 99 | }; 100 | 101 | public func toText(chars : List) : Text { 102 | let iter = Iter.fromList(chars); 103 | Text.fromIter(iter); 104 | }; 105 | 106 | public func listToNat(digits : List) : Nat { 107 | List.foldLeft( 108 | digits, 109 | 0, 110 | func(n : Nat, c : Char) : Nat { 111 | let digit = Nat32.toNat( 112 | Char.toNat32(c) - Char.toNat32('0') 113 | ); 114 | 115 | (10 * n) + digit; 116 | }, 117 | ); 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | ## Usage Examples 2 | 3 | ### CBOR 4 | 5 | ```motoko 6 | 7 | import { CBOR } "mo:serde"; 8 | 9 | type User = { 10 | name: Text; 11 | id: Nat; 12 | }; 13 | 14 | let user : User = { name = "bar"; id = 112 }; 15 | 16 | let candid = to_candid (user); 17 | let cbor_res = CBOR.encode(candid, ["name", "id"], null); 18 | let #ok(cbor) = cbor_res; 19 | ``` 20 | 21 | #### Candid Variant 22 | 23 | ```motoko 24 | 25 | import { Candid } "mo:serde"; 26 | 27 | type User = { 28 | name: Text; 29 | id: Nat; 30 | }; 31 | 32 | let candid_variant = #Record([ 33 | ("name", #Text("bar")), 34 | ("id", #Nat(112)) 35 | ]); 36 | 37 | let #ok(blob) = Candid.encode(candid_variant, null); 38 | let user : ?User = from_candid(blob); 39 | 40 | assert user == ?{ name = "bar"; id = 112 }; 41 | 42 | ``` 43 | 44 | #### ICRC3 Value 45 | 46 | - The [`ICRC3` value type](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-3#value) is a representation of candid types in motoko used for sending information without breaking compatibility between canisters that might change their api/data types over time. 47 | 48 | - **Converting from ICRC3 to motoko** 49 | 50 | ```motoko 51 | import Serde "mo:serde"; 52 | 53 | let { Candid } = Serde; 54 | 55 | type User = { name : Text; id : Nat }; 56 | 57 | let icrc3 : Serde.ICRC3Value = #Map([ 58 | ("id", #Nat(112)), 59 | ("name", #Text("bar")), 60 | ]); 61 | 62 | let candid_values = Candid.fromICRC3Value([icrc3]); 63 | 64 | let #ok(blob) = Candid.encode(candid_values, null); 65 | let user : ?User = from_candid (blob); 66 | 67 | assert user == ?{ name = "bar"; id = 112 }; 68 | 69 | ``` 70 | 71 | - **Converting from motoko to ICRC3** 72 | 73 | ```motoko 74 | import Serde "mo:serde"; 75 | 76 | let { Candid } = Serde; 77 | 78 | type User = { id : Nat; name : Text }; 79 | 80 | let user : User = { name = "bar"; id = 112 }; 81 | 82 | let blob = to_candid (user); 83 | let #ok(candid_values) = Candid.decode(blob, ["name", "id"], null); 84 | let icrc3_values = Candid.toICRC3Value(candid_values); 85 | 86 | assert icrc3_values[0] == #Map([ 87 | ("id", #Nat(112)), 88 | ("name", #Text("bar")), 89 | ]); 90 | 91 | ``` 92 | 93 | ### Candid Text 94 | 95 | ```motoko 96 | import { Candid } "mo:serde"; 97 | 98 | type User = { 99 | name: Text; 100 | id: Nat; 101 | }; 102 | 103 | let #ok(blob) = Candid.fromText("(record({ name = \"bar\"; id = 112 }))", null); 104 | let user : ?User = from_candid(blob); 105 | 106 | assert user == ?{ name = "bar"; id = 112 }; 107 | 108 | ``` 109 | 110 | ### URL-Encoded Pairs 111 | 112 | Serialization and deserialization for `application/x-www-form-urlencoded`. 113 | 114 | This implementation supports URL query strings and URL-encoded pairs, including arrays and nested objects, using the format `items[0]=value&items[1]=value` and `items[subKey]=value`." 115 | 116 | ```motoko 117 | import { URLEncoded } "mo:serde"; 118 | 119 | type User = { 120 | name: Text; 121 | id: Nat; 122 | }; 123 | 124 | let payload = "users[0][id]=123&users[0][name]=John&users[1][id]=456&users[1][name]=Jane"; 125 | 126 | let #ok(blob) = URLEncoded.fromText(payload, null); 127 | let res : ?{ users: [User]} = from_candid(blob); 128 | 129 | assert res == ?{ users = [ 130 | { 131 | name = "John"; 132 | id = 123; 133 | }, 134 | { 135 | name = "Jane"; 136 | id = 456; 137 | }, 138 | ] }; 139 | 140 | ``` 141 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/lib.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.16.0/Char"; 2 | import Debug "mo:base@0.16.0/Debug"; 3 | import Iter "mo:base@0.16.0/Iter"; 4 | import List "mo:base@0.16.0/List"; 5 | import TrieMap "mo:base@0.16.0/TrieMap"; 6 | 7 | import C "../../../../submodules/parser-combinators.mo/src/Combinators"; 8 | import P "../../../../submodules/parser-combinators.mo/src/Parser"; 9 | 10 | import Candid "../../Types"; 11 | 12 | import { ignoreSpace } "Common"; 13 | 14 | import { arrayParser } "Array"; 15 | import { blobParser } "Blob"; 16 | import { boolParser } "Bool"; 17 | import { floatParser } "Float"; 18 | import { intParser } "Int"; 19 | import { intXParser } "IntX"; 20 | import { natParser } "Nat"; 21 | import { natXParser } "NatX"; 22 | import { optionParser; nullParser } "Option"; 23 | import { principalParser } "Principal"; 24 | import { recordParser } "Record"; 25 | import { textParser } "Text"; 26 | import { variantParser } "Variant"; 27 | 28 | module CandidParser { 29 | type Candid = Candid.Candid; 30 | type List = List.List; 31 | type TrieMap = TrieMap.TrieMap; 32 | 33 | type Parser = P.Parser; 34 | 35 | public func parse(text : Text) : [Candid] { 36 | let chars = Iter.toList(text.chars()); 37 | 38 | switch (parseCandid(chars)) { 39 | case (?candid) candid; 40 | case (null) Debug.trap("Failed to parse Candid text from input: " # debug_show (chars)); 41 | }; 42 | }; 43 | 44 | func parseCandid(l : List.List) : ?[Candid] { 45 | switch (multiValueCandidParser()(l)) { 46 | case (null) { null }; 47 | case (?(x, xs)) { 48 | switch (xs) { 49 | case (null) { ?x }; 50 | case (_xs) { 51 | Debug.print("Failed to parse Candid: " # debug_show (x, _xs)); 52 | null; 53 | }; 54 | }; 55 | }; 56 | }; 57 | }; 58 | 59 | public func multiValueCandidParser() : Parser { 60 | C.bracket( 61 | C.String.string("("), 62 | C.map( 63 | C.sepBy( 64 | ignoreSpace(candidParser()), 65 | ignoreSpace(C.Character.char(',')), 66 | ), 67 | func(list : List) : [Candid] { 68 | List.toArray(list); 69 | }, 70 | ), 71 | ignoreSpace(C.String.string(")")), 72 | ); 73 | }; 74 | 75 | public func candidParser() : Parser { 76 | let supportedParsers = [ 77 | intXParser(), 78 | natXParser(), 79 | 80 | intParser(), 81 | natParser(), 82 | 83 | textParser(), 84 | 85 | blobParser(), 86 | arrayParser(candidParser), 87 | optionParser(candidParser), 88 | recordParser(candidParser), 89 | variantParser(candidParser), 90 | boolParser(), 91 | principalParser(), 92 | floatParser(), 93 | nullParser(), 94 | bracketParser(candidParser), 95 | ]; 96 | 97 | C.oneOf([ 98 | C.bracket( 99 | ignoreSpace(C.String.string("(")), 100 | ignoreSpace(C.oneOf(supportedParsers)), 101 | ignoreSpace(C.String.string(")")), 102 | ), 103 | C.bracket( 104 | C.many(C.Character.space()), 105 | C.oneOf(supportedParsers), 106 | C.many(C.Character.space()), 107 | ), 108 | ]); 109 | }; 110 | 111 | func bracketParser(parser : () -> Parser) : Parser { 112 | C.bracket( 113 | ignoreSpace(C.String.string("(")), 114 | ignoreSpace(P.delay(parser)), 115 | ignoreSpace(C.String.string(")")), 116 | ); 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/JSON/ToText.mo: -------------------------------------------------------------------------------- 1 | import Buffer "mo:base@0.16.0/Buffer"; 2 | import Result "mo:base@0.16.0/Result"; 3 | import Text "mo:base@0.16.0/Text"; 4 | 5 | import JSON "../../submodules/json.mo/src/JSON"; 6 | import NatX "mo:xtended-numbers/NatX"; 7 | import IntX "mo:xtended-numbers/IntX"; 8 | 9 | import Candid "../Candid"; 10 | import CandidType "../Candid/Types"; 11 | import Utils "../Utils"; 12 | 13 | module { 14 | type JSON = JSON.JSON; 15 | type Candid = Candid.Candid; 16 | type Result = Result.Result; 17 | 18 | /// Converts serialized Candid blob to JSON text 19 | public func toText(blob : Blob, keys : [Text], options : ?CandidType.Options) : Result { 20 | let decoded_res = Candid.decode(blob, keys, options); 21 | let #ok(candid) = decoded_res else return Utils.send_error(decoded_res); 22 | 23 | let json_res = fromCandid(candid[0]); 24 | let #ok(json) = json_res else return Utils.send_error(json_res); 25 | #ok(json); 26 | }; 27 | 28 | /// Convert a Candid value to JSON text 29 | public func fromCandid(candid : Candid) : Result { 30 | let res = candidToJSON(candid); 31 | let #ok(json) = res else return Utils.send_error(res); 32 | 33 | #ok(JSON.show(json)); 34 | }; 35 | 36 | func candidToJSON(candid : Candid) : Result { 37 | let json : JSON = switch (candid) { 38 | case (#Null) #Null; 39 | case (#Bool(n)) #Boolean(n); 40 | case (#Text(n)) #String(Text.replace(n, #text("\""), ("\\\""))); 41 | 42 | case (#Int(n)) #Number(n); 43 | case (#Int8(n)) #Number(IntX.from8ToInt(n)); 44 | case (#Int16(n)) #Number(IntX.from16ToInt(n)); 45 | case (#Int32(n)) #Number(IntX.from32ToInt(n)); 46 | case (#Int64(n)) #Number(IntX.from64ToInt(n)); 47 | 48 | case (#Nat(n)) #Number(n); 49 | case (#Nat8(n)) #Number(NatX.from8ToNat(n)); 50 | case (#Nat16(n)) #Number(NatX.from16ToNat(n)); 51 | case (#Nat32(n)) #Number(NatX.from32ToNat(n)); 52 | case (#Nat64(n)) #Number(NatX.from64ToNat(n)); 53 | 54 | case (#Float(n)) #Float(n); 55 | 56 | case (#Option(val)) { 57 | let res = switch (val) { 58 | case (#Null) return #ok(#Null); 59 | case (v) candidToJSON(v); 60 | }; 61 | 62 | let #ok(optional_val) = res else return Utils.send_error(res); 63 | optional_val; 64 | }; 65 | case (#Array(arr)) { 66 | let newArr = Buffer.Buffer(arr.size()); 67 | 68 | for (item in arr.vals()) { 69 | let res = candidToJSON(item); 70 | let #ok(json) = res else return Utils.send_error(res); 71 | newArr.add(json); 72 | }; 73 | 74 | #Array(Buffer.toArray(newArr)); 75 | }; 76 | 77 | case (#Record(records) or #Map(records)) { 78 | let newRecords = Buffer.Buffer<(Text, JSON)>(records.size()); 79 | 80 | for ((key, val) in records.vals()) { 81 | let res = candidToJSON(val); 82 | let #ok(json) = res else return Utils.send_error(res); 83 | newRecords.add((key, json)); 84 | }; 85 | 86 | #Object(Buffer.toArray(newRecords)); 87 | }; 88 | 89 | case (#Variant(variant)) { 90 | let (key, val) = variant; 91 | let res = candidToJSON(val); 92 | let #ok(json_val) = res else return Utils.send_error(res); 93 | 94 | #Object([("#" # key, json_val)]); 95 | }; 96 | 97 | case (_) { 98 | return #err(debug_show candid # " is not supported by JSON"); 99 | }; 100 | 101 | }; 102 | 103 | #ok(json); 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /src/Candid/Text/ToText.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base@0.16.0/Float"; 2 | import Array "mo:base@0.16.0/Array"; 3 | import Text "mo:base@0.16.0/Text"; 4 | import Principal "mo:base@0.16.0/Principal"; 5 | import TrieMap "mo:base@0.16.0/TrieMap"; 6 | 7 | import Itertools "mo:itertools@0.2.2/Iter"; 8 | 9 | import CandidType "../Types"; 10 | 11 | import U "../../Utils"; 12 | 13 | module { 14 | type Candid = CandidType.Candid; 15 | type TrieMap = TrieMap.TrieMap; 16 | 17 | public func toText(candid_values : [Candid]) : Text { 18 | var text = ""; 19 | 20 | let candid_iter = Itertools.peekable(candid_values.vals()); 21 | 22 | for (val in candid_iter) { 23 | if (candid_iter.peek() == null) { 24 | text #= candidToText(val); 25 | } else { 26 | text #= candidToText(val) # ", "; 27 | }; 28 | }; 29 | 30 | if (candid_values.size() == 1 and Text.startsWith(text, #text("("))) { 31 | text; 32 | } else { 33 | "(" # text # ")"; 34 | }; 35 | }; 36 | 37 | func candidToText(candid : Candid) : Text { 38 | switch (candid) { 39 | case (#Nat(n)) removeUnderscore(debug_show (n)); 40 | case (#Nat8(n)) addBrackets(removeUnderscore(debug_show (n)) # " : nat8"); 41 | case (#Nat16(n)) addBrackets(removeUnderscore(debug_show (n)) # " : nat16"); 42 | case (#Nat32(n)) addBrackets(removeUnderscore(debug_show (n)) # " : nat32"); 43 | case (#Nat64(n)) addBrackets(removeUnderscore(debug_show (n)) # " : nat64"); 44 | 45 | case (#Int(n)) U.stripStart(removeUnderscore(debug_show (n)), #char '+'); 46 | case (#Int8(n)) addBrackets(U.stripStart(removeUnderscore(debug_show (n)), #char '+') # " : int8"); 47 | case (#Int16(n)) addBrackets(U.stripStart(removeUnderscore(debug_show (n)), #char '+') # " : int16"); 48 | case (#Int32(n)) addBrackets(U.stripStart(removeUnderscore(debug_show (n)), #char '+') # " : int32"); 49 | case (#Int64(n)) addBrackets(U.stripStart(removeUnderscore(debug_show (n)), #char '+') # " : int64"); 50 | 51 | case (#Float(n)) Float.format(#exact, n); 52 | 53 | case (#Null) "null"; 54 | case (#Empty) "()"; 55 | case (#Bool(b)) debug_show (b); 56 | case (#Text(text)) "\"" # text # "\""; 57 | 58 | case (#Blob(bytes)) "blob " # debug_show (bytes); 59 | case (#Principal(p)) "principal \"" # Principal.toText(p) # "\""; 60 | 61 | case (#Option(value)) "opt (" # candidToText(value) # ")"; 62 | 63 | case (#Array(values)) { 64 | var text = "vec { "; 65 | 66 | for (value in values.vals()) { 67 | text #= candidToText(value) # "; "; 68 | }; 69 | 70 | text # "}"; 71 | }; 72 | case (#Tuple(values)) return candidToText( 73 | #Record( 74 | Array.tabulate<(Text, Candid)>( 75 | values.size(), 76 | func(i : Nat) : (Text, Candid) { 77 | (debug_show i, values[i]); 78 | }, 79 | ) 80 | ) 81 | ); 82 | 83 | case (#Record(fields) or #Map(fields)) { 84 | var text = "record { "; 85 | 86 | for ((key, val) in fields.vals()) { 87 | text #= key # " = " # candidToText(val) # "; "; 88 | }; 89 | 90 | text # "}"; 91 | }; 92 | 93 | case (#Variant((key, val))) { 94 | "variant { " # key # " = " # candidToText(val) # " }"; 95 | }; 96 | }; 97 | }; 98 | 99 | func removeUnderscore(text : Text) : Text { 100 | Text.replace(text, #text "_", ""); 101 | }; 102 | 103 | func addBrackets(text : Text) : Text { 104 | "(" # text # ")"; 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /src/Candid/Types.mo: -------------------------------------------------------------------------------- 1 | module { 2 | 3 | public type KeyValuePair = (Text, Candid); 4 | 5 | /// A standard representation of the Candid type 6 | public type Candid = { 7 | #Int : Int; 8 | #Int8 : Int8; 9 | #Int16 : Int16; 10 | #Int32 : Int32; 11 | #Int64 : Int64; 12 | 13 | #Nat : Nat; 14 | #Nat8 : Nat8; 15 | #Nat16 : Nat16; 16 | #Nat32 : Nat32; 17 | #Nat64 : Nat64; 18 | 19 | #Bool : Bool; 20 | #Float : Float; 21 | #Text : Text; 22 | #Blob : Blob; 23 | 24 | #Null; 25 | #Empty; 26 | #Principal : Principal; 27 | 28 | #Option : Candid; 29 | #Array : [Candid]; 30 | #Record : [KeyValuePair]; 31 | #Tuple : [Candid]; // shorthand for record with indexed keys -> #Record([(0, Candid), (1, Candid), ...]) 32 | #Map : [KeyValuePair]; 33 | #Variant : KeyValuePair; 34 | }; 35 | 36 | public type CandidType = { 37 | #Int; 38 | #Int8; 39 | #Int16; 40 | #Int32; 41 | #Int64; 42 | 43 | #Nat; 44 | #Nat8; 45 | #Nat16; 46 | #Nat32; 47 | #Nat64; 48 | #Bool; 49 | #Float; 50 | #Text; 51 | #Blob; 52 | #Null; 53 | #Empty; 54 | #Principal; 55 | 56 | #Option : CandidType; 57 | #Array : CandidType; 58 | #Record : [(Text, CandidType)]; 59 | #Tuple : [CandidType]; 60 | #Map : [(Text, CandidType)]; // ICRC3 version of #Record 61 | #Variant : [(Text, CandidType)]; 62 | #Recursive : (Nat); 63 | 64 | }; 65 | 66 | // nat values could be either reference pointers to compound types 67 | // or actual primitive value codes 68 | public type ShallowCandidTypes = { 69 | #OptionRef : Nat; 70 | #ArrayRef : Nat; 71 | #RecordRef : [(Text, Nat)]; 72 | #VariantRef : [(Text, Nat)]; 73 | }; 74 | 75 | public let TypeCode = { 76 | // primitive types 77 | Null : Nat8 = 0x7f; // 127 78 | Bool : Nat8 = 0x7e; // 126 79 | Nat : Nat8 = 0x7d; // 125 80 | Int : Nat8 = 0x7c; // 124 81 | Nat8 : Nat8 = 0x7b; // 123 82 | Nat16 : Nat8 = 0x7a; // 122 83 | Nat32 : Nat8 = 0x79; // 121 84 | Nat64 : Nat8 = 0x78; // 120 85 | Int8 : Nat8 = 0x77; // 119 86 | Int16 : Nat8 = 0x76; // 118 87 | Int32 : Nat8 = 0x75; // 117 88 | Int64 : Nat8 = 0x74; // 116 89 | // Float32 : Nat8 = 0x73; // 115 90 | Float : Nat8 = 0x72; // 114 91 | Text : Nat8 = 0x71; // 113 92 | // Reserved : Nat8 = 0x70; // 112 93 | Empty : Nat8 = 0x6f; // 111 94 | 95 | // compound types 96 | 97 | Option : Nat8 = 0x6e; // 110 98 | Array : Nat8 = 0x6d; // 109 99 | Record : Nat8 = 0x6c; // 108 100 | Variant : Nat8 = 0x6b; // 107 101 | // Func : Nat8 = 0x6a; // 106 102 | // Service : Nat8 = 0x69; // 105 103 | 104 | Principal : Nat8 = 0x68; // 104 105 | 106 | }; 107 | 108 | /// Encoding and Decoding options 109 | public type Options = { 110 | 111 | /// #### Encoding Options 112 | /// Contains an array of tuples of the form (old_name, new_name) to rename the record keys. 113 | renameKeys : [(Text, Text)]; 114 | 115 | // convertAllNumbersToFloats : Bool; 116 | 117 | /// Returns #Map instead of #Record supported by the icrc3 spec 118 | use_icrc_3_value_type : Bool; 119 | 120 | /// encodes faster if the complete type is known, but not necessary 121 | /// fails if types are incorrect 122 | /// 123 | /// Must call `Candid.formatCandidTypes` before passing in the types 124 | types : ?[CandidType]; 125 | 126 | }; 127 | 128 | public type ICRC3Value = { 129 | #Blob : Blob; 130 | #Text : Text; 131 | #Nat : Nat; 132 | #Int : Int; 133 | #Array : [ICRC3Value]; 134 | #Map : [(Text, ICRC3Value)]; 135 | }; 136 | 137 | public let defaultOptions : Options = { 138 | renameKeys = []; 139 | // convertAllNumbersToFloats = false; 140 | use_icrc_3_value_type = false; 141 | 142 | types = null; 143 | 144 | }; 145 | 146 | }; 147 | -------------------------------------------------------------------------------- /.bench/serde.bench.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "moc": "0.14.13", 4 | "replica": "dfx", 5 | "replicaVersion": "0.29.2", 6 | "gc": "generational", 7 | "forceGc": true, 8 | "results": [ 9 | [ 10 | "Serde: One Shot:decode()", 11 | { 12 | "rts_stable_memory_size": 0, 13 | "stable_memory_size": 0, 14 | "instructions": 394461139, 15 | "rts_memory_size": 17563648, 16 | "rts_total_allocation": 27418360, 17 | "rts_collector_instructions": -29134, 18 | "rts_mutator_instructions": -10415, 19 | "rts_logical_stable_memory_size": 0, 20 | "rts_heap_size": 1580216, 21 | "rts_reclaimed": 25838144 22 | } 23 | ], 24 | [ 25 | "Serde: One Shot:encode()", 26 | { 27 | "rts_stable_memory_size": 0, 28 | "stable_memory_size": 0, 29 | "instructions": 1105298555, 30 | "rts_memory_size": 40894464, 31 | "rts_total_allocation": 65569368, 32 | "rts_collector_instructions": -7907, 33 | "rts_mutator_instructions": -8138, 34 | "rts_logical_stable_memory_size": 0, 35 | "rts_heap_size": 308, 36 | "rts_reclaimed": 65569060 37 | } 38 | ], 39 | [ 40 | "Serde: One Shot sans type inference:decode()", 41 | { 42 | "rts_stable_memory_size": 0, 43 | "stable_memory_size": 0, 44 | "instructions": 225586733, 45 | "rts_memory_size": 0, 46 | "rts_total_allocation": 18405496, 47 | "rts_collector_instructions": -7907, 48 | "rts_mutator_instructions": -8138, 49 | "rts_logical_stable_memory_size": 0, 50 | "rts_heap_size": 272, 51 | "rts_reclaimed": 18405224 52 | } 53 | ], 54 | [ 55 | "Serde: One Shot sans type inference:encode()", 56 | { 57 | "rts_stable_memory_size": 0, 58 | "stable_memory_size": 0, 59 | "instructions": 891671974, 60 | "rts_memory_size": 0, 61 | "rts_total_allocation": 35602928, 62 | "rts_collector_instructions": -7907, 63 | "rts_mutator_instructions": -8138, 64 | "rts_logical_stable_memory_size": 0, 65 | "rts_heap_size": 272, 66 | "rts_reclaimed": 35602656 67 | } 68 | ], 69 | [ 70 | "Motoko (to_candid(), from_candid()):decode()", 71 | { 72 | "rts_stable_memory_size": 0, 73 | "stable_memory_size": 0, 74 | "instructions": 31314021, 75 | "rts_memory_size": 0, 76 | "rts_total_allocation": 555728, 77 | "rts_collector_instructions": -7907, 78 | "rts_mutator_instructions": -8138, 79 | "rts_logical_stable_memory_size": 0, 80 | "rts_heap_size": 272, 81 | "rts_reclaimed": 555456 82 | } 83 | ], 84 | [ 85 | "Motoko (to_candid(), from_candid()):encode()", 86 | { 87 | "rts_stable_memory_size": 0, 88 | "stable_memory_size": 0, 89 | "instructions": 9061804, 90 | "rts_memory_size": 0, 91 | "rts_total_allocation": 611788, 92 | "rts_collector_instructions": -7907, 93 | "rts_mutator_instructions": -8138, 94 | "rts_logical_stable_memory_size": 0, 95 | "rts_heap_size": 272, 96 | "rts_reclaimed": 611516 97 | } 98 | ], 99 | [ 100 | "Serde: Single Type Serializer:decode()", 101 | { 102 | "rts_stable_memory_size": 0, 103 | "stable_memory_size": 0, 104 | "instructions": 111955117, 105 | "rts_memory_size": 0, 106 | "rts_total_allocation": 6834008, 107 | "rts_collector_instructions": -7907, 108 | "rts_mutator_instructions": -8138, 109 | "rts_logical_stable_memory_size": 0, 110 | "rts_heap_size": 272, 111 | "rts_reclaimed": 6833736 112 | } 113 | ], 114 | [ 115 | "Serde: Single Type Serializer:encode()", 116 | { 117 | "rts_stable_memory_size": 0, 118 | "stable_memory_size": 0, 119 | "instructions": 222580666, 120 | "rts_memory_size": 0, 121 | "rts_total_allocation": 8413432, 122 | "rts_collector_instructions": -7907, 123 | "rts_mutator_instructions": -8138, 124 | "rts_logical_stable_memory_size": 0, 125 | "rts_heap_size": 272, 126 | "rts_reclaimed": 8413160 127 | } 128 | ] 129 | ] 130 | } -------------------------------------------------------------------------------- /src/Candid/ICRC3Value.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Nat8 "mo:base@0.16.0/Nat8"; 3 | import Nat16 "mo:base@0.16.0/Nat16"; 4 | import Nat64 "mo:base@0.16.0/Nat64"; 5 | import Nat32 "mo:base@0.16.0/Nat32"; 6 | import Int8 "mo:base@0.16.0/Int8"; 7 | import Int16 "mo:base@0.16.0/Int16"; 8 | import Int64 "mo:base@0.16.0/Int64"; 9 | import Int32 "mo:base@0.16.0/Int32"; 10 | import Debug "mo:base@0.16.0/Debug"; 11 | import Principal "mo:base@0.16.0/Principal"; 12 | 13 | import T "Types"; 14 | 15 | module { 16 | public func toICRC3Value(candid_values : [T.Candid]) : [T.ICRC3Value] { 17 | 18 | func convert(candid : T.Candid) : T.ICRC3Value { 19 | switch (candid) { 20 | case (#Text(t)) #Text(t); 21 | case (#Nat(n)) #Nat(n); 22 | case (#Nat8(n)) #Nat(Nat8.toNat(n)); 23 | case (#Nat16(n)) #Nat(Nat16.toNat(n)); 24 | case (#Nat32(n)) #Nat(Nat32.toNat(n)); 25 | case (#Nat64(n)) #Nat(Nat64.toNat(n)); 26 | case (#Int(n)) #Int(n); 27 | case (#Int8(n)) #Int(Int8.toInt(n)); 28 | case (#Int16(n)) #Int(Int16.toInt(n)); 29 | case (#Int32(n)) #Int(Int32.toInt(n)); 30 | case (#Int64(n)) #Int(Int64.toInt(n)); 31 | case (#Blob(b)) #Blob(b); 32 | case (#Principal(p)) #Blob(Principal.toBlob(p)); 33 | case (#Array(array_vals)) #Array( 34 | Array.tabulate( 35 | array_vals.size(), 36 | func(i : Nat) : T.ICRC3Value { 37 | convert(array_vals.get(i)); 38 | }, 39 | ) 40 | ); 41 | case (#Record(record_vals) or #Map(record_vals)) #Map( 42 | Array.tabulate<(Text, T.ICRC3Value)>( 43 | record_vals.size(), 44 | func(i : Nat) : (Text, T.ICRC3Value) { 45 | let (key, value) = record_vals.get(i); 46 | let icrc3_value = convert(value); 47 | (key, icrc3_value); 48 | }, 49 | ) 50 | ); 51 | case (#Bool(_) or #Option(_) or #Variant(_) or #Tuple(_)) Debug.trap(debug_show candid # " not suppported in ICRC3Value"); 52 | case (#Empty) Debug.trap("Empty not suppported in ICRC3Value"); 53 | case (#Float(f)) Debug.trap("Float not suppported in ICRC3Value"); 54 | case (#Null) Debug.trap("Null not suppported in ICRC3Value"); 55 | 56 | }; 57 | 58 | }; 59 | 60 | Array.tabulate( 61 | candid_values.size(), 62 | func(i : Nat) : T.ICRC3Value { 63 | convert(candid_values.get(i)); 64 | }, 65 | ); 66 | 67 | }; 68 | 69 | public func fromICRC3Value(icrc3_values : [T.ICRC3Value]) : [T.Candid] { 70 | func convert(candid : T.ICRC3Value) : T.Candid { 71 | switch (candid) { 72 | case (#Text(t)) #Text(t); 73 | case (#Nat(n)) #Nat(n); 74 | case (#Int(n)) #Int(n); 75 | case (#Blob(b)) #Blob(b); 76 | case (#Array(array_vals)) #Array( 77 | Array.tabulate( 78 | array_vals.size(), 79 | func(i : Nat) : T.Candid { 80 | convert(array_vals.get(i)); 81 | }, 82 | ) 83 | ); 84 | case (#Map(record_vals)) #Map( 85 | Array.tabulate<(Text, T.Candid)>( 86 | record_vals.size(), 87 | func(i : Nat) : (Text, T.Candid) { 88 | let (key, value) = record_vals.get(i); 89 | let candid_value = convert(value); 90 | (key, candid_value); 91 | }, 92 | ) 93 | ); 94 | }; 95 | }; 96 | 97 | Array.tabulate( 98 | icrc3_values.size(), 99 | func(i : Nat) : T.Candid { 100 | convert(icrc3_values.get(i)); 101 | }, 102 | ) 103 | 104 | }; 105 | }; 106 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | benchmark: 13 | runs-on: ubuntu-latest 14 | name: Run Benchmarks and Comment 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - name: Cache Node modules 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | 29 | - name: Cache mops packages 30 | uses: actions/cache@v4 31 | with: 32 | key: mops-packages-${{ hashFiles('mops.toml') }} 33 | path: | 34 | ~/.cache/mops 35 | 36 | - name: Install dfx 37 | uses: dfinity/setup-dfx@main 38 | - name: Confirm successful installation 39 | run: dfx --version 40 | 41 | - name: Install dfx cache 42 | run: dfx cache install 43 | 44 | - name: Install mops & mocv 45 | run: | 46 | npm --yes -g i ic-mops 47 | mops i 48 | mops toolchain init 49 | 50 | # set moc path for dfx to use 51 | echo "DFX_MOC_PATH=$(mops toolchain bin moc)" >> $GITHUB_ENV 52 | 53 | - name: Create benchmark-results branch if it doesn't exist 54 | uses: peterjgrainger/action-create-branch@v2.2.0 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | with: 58 | branch: "benchmark-results" 59 | sha: "${{ github.event.pull_request.head.sha }}" 60 | continue-on-error: true 61 | 62 | - name: Checkout out the branch with benchmark results 63 | uses: actions/checkout@v4 64 | with: 65 | ref: benchmark-results 66 | path: .benchmark-results-branch/ 67 | 68 | - name: Move Saved Benchmarks 69 | run: mv .benchmark-results-branch/.bench .bench 2>/dev/null || mkdir -p .bench 70 | 71 | - name: Pull Request Benchmarks 72 | if: github.event_name == 'pull_request' 73 | id: benchmarks 74 | run: | 75 | mops bench --gc generational --compare 2>&1 | tee benchmark_results.txt 76 | echo "result<> $GITHUB_OUTPUT 77 | cat benchmark_results.txt >> $GITHUB_OUTPUT 78 | echo "EOF" >> $GITHUB_OUTPUT 79 | 80 | - name: Find Benchmark Comment 81 | if: github.event_name == 'pull_request' 82 | uses: peter-evans/find-comment@v3 83 | id: fc 84 | with: 85 | issue-number: ${{ github.event.pull_request.number }} 86 | comment-author: "github-actions[bot]" 87 | body-includes: Benchmark Results 88 | 89 | - name: Create or update comment 90 | if: github.event_name == 'pull_request' 91 | uses: peter-evans/create-or-update-comment@v4 92 | with: 93 | comment-id: ${{ steps.fc.outputs.comment-id }} 94 | issue-number: ${{ github.event.pull_request.number }} 95 | body: ${{ steps.benchmarks.outputs.result }} 96 | edit-mode: replace 97 | 98 | - name: Generate Benchmark Results 99 | if: github.event_name != 'pull_request' 100 | run: | 101 | set -o pipefail 102 | mops bench --gc generational --save --compare 2>&1 | tee bench-results.md 103 | - name: Move Generated Results 104 | if: github.event_name != 'pull_request' 105 | run: | 106 | mkdir .bench-br 107 | mv .bench .bench-br/.bench 108 | mv bench-results.md .bench-br/README.md 2>/dev/null || mkdir -p .bench 109 | - name: Upload benchmark results 110 | if: github.event_name != 'pull_request' 111 | uses: JamesIves/github-pages-deploy-action@v4 112 | with: 113 | token: ${{ secrets.GITHUB_TOKEN }} 114 | branch: benchmark-results 115 | folder: .bench-br 116 | -------------------------------------------------------------------------------- /src/UrlEncoded/ToText.mo: -------------------------------------------------------------------------------- 1 | import Result "mo:base@0.16.0/Result"; 2 | import Nat "mo:base@0.16.0/Nat"; 3 | import Nat32 "mo:base@0.16.0/Nat32"; 4 | import Text "mo:base@0.16.0/Text"; 5 | import TrieMap "mo:base@0.16.0/TrieMap"; 6 | import Iter "mo:base@0.16.0/Iter"; 7 | import Float "mo:base@0.16.0/Float"; 8 | import Principal "mo:base@0.16.0/Principal"; 9 | import Debug "mo:base@0.16.0/Debug"; 10 | 11 | import itertools "mo:itertools@0.2.2/Iter"; 12 | 13 | import Candid "../Candid"; 14 | import U "../Utils"; 15 | import Utils "../Utils"; 16 | import CandidType "../Candid/Types"; 17 | 18 | module { 19 | type Candid = Candid.Candid; 20 | type TrieMap = TrieMap.TrieMap; 21 | type Result = Result.Result; 22 | 23 | /// Converts a serialized Candid blob to a URL-Encoded string. 24 | public func toText(blob : Blob, keys : [Text], options : ?CandidType.Options) : Result { 25 | let res = Candid.decode(blob, keys, options); 26 | let #ok(candid) = res else return Utils.send_error(res); 27 | fromCandid(candid[0]); 28 | }; 29 | 30 | /// Convert a Candid Record to a URL-Encoded string. 31 | public func fromCandid(candid : Candid) : Result { 32 | 33 | let records = switch (candid) { 34 | case (#Record(records) or #Map(records)) records; 35 | case (_) return #err("invalid type: the value must be a record"); 36 | }; 37 | 38 | let pairs = TrieMap.TrieMap(Text.equal, Text.hash); 39 | 40 | for ((key, value) in records.vals()) { 41 | toKeyValuePairs(pairs, key, value); 42 | }; 43 | 44 | var url_encoding = ""; 45 | 46 | let entries = Iter.map( 47 | pairs.entries(), 48 | func((key, value) : (Text, Text)) : Text { 49 | key # "=" # value; 50 | }, 51 | ); 52 | 53 | for (t in entries) { 54 | url_encoding := if (url_encoding == "") { 55 | t; 56 | } else { 57 | t # "&" # url_encoding; 58 | }; 59 | }; 60 | 61 | #ok(url_encoding); 62 | }; 63 | 64 | func toKeyValuePairs( 65 | pairs : TrieMap, 66 | storedKey : Text, 67 | candid : Candid, 68 | ) { 69 | switch (candid) { 70 | case (#Array(arr)) { 71 | for ((i, value) in itertools.enumerate(arr.vals())) { 72 | let array_key = storedKey # "[" # Nat.toText(i) # "]"; 73 | toKeyValuePairs(pairs, array_key, value); 74 | }; 75 | }; 76 | 77 | case (#Record(records) or #Map(records)) { 78 | for ((key, value) in records.vals()) { 79 | let record_key = storedKey # "[" # key # "]"; 80 | toKeyValuePairs(pairs, record_key, value); 81 | }; 82 | }; 83 | 84 | case (#Variant(key, val)) { 85 | let variant_key = storedKey # "#" # key; 86 | toKeyValuePairs(pairs, variant_key, val); 87 | }; 88 | 89 | // TODO: convert blob to hex 90 | // case (#Blob(blob)) pairs.put(storedKey, "todo: Blob.toText(blob)"); 91 | 92 | case (#Option(p)) toKeyValuePairs(pairs, storedKey, p); 93 | case (#Text(t)) pairs.put(storedKey, t); 94 | case (#Principal(p)) pairs.put(storedKey, Principal.toText(p)); 95 | 96 | case (#Nat(n)) pairs.put(storedKey, Nat.toText(n)); 97 | case (#Nat8(n)) pairs.put(storedKey, debug_show (n)); 98 | case (#Nat16(n)) pairs.put(storedKey, debug_show (n)); 99 | case (#Nat32(n)) pairs.put(storedKey, Nat32.toText(n)); 100 | case (#Nat64(n)) pairs.put(storedKey, debug_show (n)); 101 | 102 | case (#Int(n)) pairs.put(storedKey, U.stripStart(debug_show (n), #char '+')); 103 | case (#Int8(n)) pairs.put(storedKey, U.stripStart(debug_show (n), #char '+')); 104 | case (#Int16(n)) pairs.put(storedKey, U.stripStart(debug_show (n), #char '+')); 105 | case (#Int32(n)) pairs.put(storedKey, U.stripStart(debug_show (n), #char '+')); 106 | case (#Int64(n)) pairs.put(storedKey, U.stripStart(debug_show (n), #char '+')); 107 | 108 | case (#Float(n)) pairs.put(storedKey, Float.toText(n)); 109 | case (#Null) pairs.put(storedKey, "null"); 110 | case (#Empty) pairs.put(storedKey, ""); 111 | 112 | case (#Bool(b)) pairs.put(storedKey, debug_show (b)); 113 | 114 | case (_) Debug.trap(debug_show candid # " is not supported by URL-Encoded"); 115 | 116 | }; 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /tests/CandidTestUtils.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Blob "mo:base@0.16.0/Blob"; 3 | import Debug "mo:base@0.16.0/Debug"; 4 | import Iter "mo:base@0.16.0/Iter"; 5 | import Nat "mo:base@0.16.0/Nat"; 6 | import Principal "mo:base@0.16.0/Principal"; 7 | import Result "mo:base@0.16.0/Result"; 8 | import Option "mo:base@0.16.0/Option"; 9 | 10 | import { test; suite } "mo:test"; 11 | 12 | import Serde "../src"; 13 | import Candid "../src/Candid"; 14 | import Encoder "../src/Candid/Blob/Encoder"; 15 | 16 | module { 17 | type Candid = Candid.Candid; 18 | 19 | public func encode_with_types(types : [Candid.CandidType], vals : [Candid], _options : ?Candid.Options) : Result.Result { 20 | let options = Option.get(_options, Candid.defaultOptions); 21 | 22 | let single_function_encoding = switch (Candid.encode(vals, ?options)) { 23 | case (#ok(blob)) blob; 24 | case (#err(err)) { 25 | Debug.print("encode function failed: " # err); 26 | return #err("encode function failed: " #err); 27 | }; 28 | }; 29 | 30 | let encoder = Candid.TypedSerializer.new(types, ?options); 31 | let encoded_blob_res = Candid.TypedSerializer.encode(encoder, vals); 32 | let encoder_instance_blob = switch (encoded_blob_res) { 33 | case (#ok(blob)) blob; 34 | case (#err(err)) { 35 | Debug.print("encoder instance failed: " # err); 36 | return #err("encoder instance failed: " # err); 37 | }; 38 | }; 39 | 40 | if (encoder_instance_blob != single_function_encoding) { 41 | 42 | let single_function_encoding_with_types = switch (Candid.encode(vals, ?{ options with types = ?Candid.formatCandidType(types, ?options.renameKeys) })) { 43 | case (#ok(blob)) blob; 44 | case (#err(err)) { 45 | Debug.print("encode function failed: " # err); 46 | return #err("encode function failed: " #err); 47 | }; 48 | }; 49 | 50 | if (encoder_instance_blob != single_function_encoding_with_types) { 51 | Debug.print("Encoded blob does not match the original encoding:"); 52 | Debug.print("Single function: " # debug_show (Blob.toArray(single_function_encoding_with_types))); 53 | Debug.print("Encoder instance: " # debug_show (Blob.toArray(encoder_instance_blob))); 54 | return #err("Encoded blob does not match the original encoding: " # debug_show ({ single_function_encoding_with_types; encoder_instance_blob })); 55 | }; 56 | }; 57 | 58 | // assert Candid.TypedSerializer.equal(encoder, Candid.TypedSerializer.fromBlob(encoder_instance_blob, [], ?options)); 59 | 60 | return #ok(encoder_instance_blob); 61 | }; 62 | 63 | public func decode_with_types(types : [Candid.CandidType], record_keys : [Text], blob : Blob, _options : ?Candid.Options) : Result.Result<[Candid], Text> { 64 | let options = Option.get(_options, Candid.defaultOptions); 65 | 66 | let single_function_decoding = switch (Candid.decode(blob, record_keys, ?options)) { 67 | case (#ok(vals)) vals; 68 | case (#err(err)) { 69 | Debug.print("decode function failed: " # err); 70 | return #err("decode function failed: " # err); 71 | }; 72 | }; 73 | 74 | let decoder = Candid.TypedSerializer.fromBlob(blob, record_keys, ?options); 75 | let decoded_vals_res = Candid.TypedSerializer.decode(decoder, blob); 76 | let decoder_instance_vals = switch (decoded_vals_res) { 77 | case (#ok(vals)) vals; 78 | case (#err(err)) { 79 | Debug.print("decoder instance failed: " # err); 80 | return #err("decoder instance failed: " # err); 81 | }; 82 | }; 83 | 84 | if (decoder_instance_vals != single_function_decoding) { 85 | Debug.print("Decoded values do not match the original decoding:"); 86 | Debug.print("Candid.decode() function: " # debug_show (single_function_decoding)); 87 | Debug.print("TypedSerializer instance: " # debug_show (decoder_instance_vals)); 88 | return #err("Decoded values do not match the original decoding: " # debug_show ({ single_function_decoding; decoder_instance_vals })); 89 | }; 90 | 91 | // !important: the decoder instance should be equal to a new TypedSerializer created with the same types and options 92 | // if (not Candid.TypedSerializer.equal(decoder, Candid.TypedSerializer.new(types, ?options))) { 93 | // Debug.print("Decoder and new TypedSerializer are not equal."); 94 | // Debug.print("Decoder: " # Candid.TypedSerializer.toText(decoder)); 95 | // Debug.print("New TypedSerializer: " # Candid.TypedSerializer.toText(Candid.TypedSerializer.new(types, ?options))); 96 | // return #err("Decoder and new TypedSerializer are not equal."); 97 | // }; 98 | 99 | return #ok(decoder_instance_vals); 100 | }; 101 | 102 | }; 103 | -------------------------------------------------------------------------------- /src/Candid/Blob/RepIndyHash.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Blob "mo:base@0.16.0/Blob"; 3 | import Buffer "mo:base@0.16.0/Buffer"; 4 | import Debug "mo:base@0.16.0/Debug"; 5 | import Nat64 "mo:base@0.16.0/Nat64"; 6 | import Int8 "mo:base@0.16.0/Int8"; 7 | import Int32 "mo:base@0.16.0/Int32"; 8 | import Nat8 "mo:base@0.16.0/Nat8"; 9 | import Nat32 "mo:base@0.16.0/Nat32"; 10 | import Nat16 "mo:base@0.16.0/Nat16"; 11 | import Int64 "mo:base@0.16.0/Int64"; 12 | import Nat "mo:base@0.16.0/Nat"; 13 | import Principal "mo:base@0.16.0/Principal"; 14 | import Text "mo:base@0.16.0/Text"; 15 | import Int16 "mo:base@0.16.0/Int16"; 16 | 17 | import T "../Types"; 18 | import Utils "../../Utils"; 19 | import Sha256 "mo:sha2@0.1.6/Sha256"; 20 | 21 | import ByteUtils "mo:byte-utils@0.1.2"; 22 | 23 | module { 24 | type Buffer = Buffer.Buffer; 25 | 26 | let { ReusableBuffer; unsigned_leb128; signed_leb128_64 } = Utils; 27 | 28 | public func hash(candid_value : T.Candid) : Blob { 29 | // let buffer = ReusableBuffer(100); 30 | let buffer = Buffer.Buffer(100); 31 | let sha256 = Sha256.Digest(#sha256); 32 | 33 | candid_hash(buffer, sha256, candid_value); 34 | }; 35 | 36 | func candid_hash( 37 | buffer : Buffer.Buffer, 38 | sha256 : Sha256.Digest, 39 | candid_value : T.Candid, 40 | ) : Blob { 41 | switch (candid_value) { 42 | case (#Int(n)) signed_leb128_64(buffer, n); 43 | case (#Int8(i8)) { 44 | ByteUtils.Buffer.LE.addInt8(buffer, i8); 45 | }; 46 | case (#Int16(i16)) { 47 | ByteUtils.Buffer.LE.addInt16(buffer, i16); 48 | }; 49 | case (#Int32(i32)) { 50 | ByteUtils.Buffer.LE.addInt32(buffer, i32); 51 | }; 52 | case (#Int64(i64)) { 53 | ByteUtils.Buffer.LE.addInt64(buffer, i64); 54 | }; 55 | 56 | case (#Nat(n)) unsigned_leb128(buffer, n); 57 | 58 | case (#Nat8(n)) { 59 | buffer.add(n); 60 | }; 61 | case (#Nat16(n)) { 62 | ByteUtils.Buffer.LE.addNat16(buffer, n); 63 | }; 64 | case (#Nat32(n)) { 65 | ByteUtils.Buffer.LE.addNat32(buffer, n); 66 | }; 67 | case (#Nat64(n)) { 68 | ByteUtils.Buffer.LE.addNat64(buffer, n); 69 | }; 70 | 71 | case (#Float(f64)) { 72 | ByteUtils.Buffer.LE.addFloat(buffer, f64); 73 | }; 74 | case (#Bool(b)) { 75 | buffer.add(if (b) (1) else (0)); 76 | }; 77 | case (#Null) {}; 78 | 79 | case (#Text(t)) { 80 | 81 | let utf8_bytes = Blob.toArray(Text.encodeUtf8(t)); 82 | 83 | var i = 0; 84 | while (i < utf8_bytes.size()) { 85 | buffer.add(utf8_bytes[i]); 86 | i += 1; 87 | }; 88 | 89 | }; 90 | case (#Blob(b)) { 91 | sha256.writeBlob(b); 92 | }; 93 | case (#Principal(p)) { 94 | 95 | let bytes = Blob.toArray(Principal.toBlob(p)); 96 | 97 | var i = 0; 98 | while (i < bytes.size()) { 99 | buffer.add(bytes[i]); 100 | i += 1; 101 | }; 102 | }; 103 | 104 | case (#Array(values)) { 105 | 106 | let hashes = Array.tabulate( 107 | values.size(), 108 | func(i : Nat) : Blob { 109 | candid_hash(buffer, sha256, values[i]); 110 | }, 111 | ); 112 | 113 | for (hash in hashes.vals()) { 114 | let hash_bytes = Blob.toArray(hash); 115 | for (byte in hash_bytes.vals()) { 116 | buffer.add(byte); 117 | }; 118 | }; 119 | }; 120 | case (#Record(records) or #Map(records)) { 121 | let hashes = Buffer.Buffer(8); 122 | label record_hashing for ((key, value) in records.vals()) { 123 | let key_hash = candid_hash(buffer, sha256, #Text(key)); 124 | 125 | let unwrapped_value = switch (value) { 126 | case (#Null) continue record_hashing; 127 | case (#Option(inner_type)) inner_type; 128 | case (value) value; 129 | }; 130 | 131 | let value_hash = candid_hash(buffer, sha256, unwrapped_value); 132 | 133 | let concatenated = Blob.fromArray( 134 | Array.append( 135 | Blob.toArray(key_hash), 136 | Blob.toArray(value_hash), 137 | ) 138 | ); 139 | 140 | hashes.add(concatenated); 141 | }; 142 | 143 | hashes.sort(Blob.compare); 144 | 145 | for (hash in hashes.vals()) { 146 | let hash_bytes = Blob.toArray(hash); 147 | for (byte in hash_bytes.vals()) { 148 | buffer.add(byte); 149 | }; 150 | }; 151 | 152 | }; 153 | case (candid) Debug.trap("oops: " # debug_show (candid)); 154 | }; 155 | 156 | sha256.writeIter(buffer.vals()); 157 | buffer.clear(); 158 | 159 | let resulting_hash = sha256.sum(); 160 | sha256.reset(); // !important to reset the sha256 instance for future use 161 | 162 | resulting_hash; 163 | 164 | }; 165 | }; 166 | -------------------------------------------------------------------------------- /bench.md: -------------------------------------------------------------------------------- 1 | ## Benchmarks 2 | 3 | #### mops version `2.2.0` 4 | Benchmarking the performance with 10k calls 5 | 6 | Instructions 7 | 8 | | | to_candid() | from_candid() | decode() | encode() | 9 | | :---- | ----------: | ------------: | ------------: | ------------: | 10 | | Serde | 6_847_934 | 20_275_957 | 1_053_547_402 | 2_000_462_160 | 11 | 12 | 13 | Heap 14 | 15 | | | to_candid() | from_candid() | decode() | encode() | 16 | | :---- | ----------: | ------------: | ---------: | ---------: | 17 | | Serde | 373_360 | 397_012 | 10_425_876 | -1_620_764 | 18 | 19 | 20 | #### one_shot: backward referencing 21 | 22 | Instructions 23 | 24 | | | decode() / too_candid() | encode() / from_candid() | 25 | | :---------------------------------- | ----------------------: | -----------------------: | 26 | | Serde 'mo:motoko_candid' lib | 1_053_963_520 | 2_000_335_920 | 27 | | Motoko (to_candid(), from_candid()) | 6_851_133 | 20_284_651 | 28 | | Serde: One Shot | 2_984 | 978_890_225 | 29 | | Serde: One Shot sans type inference | 4_556 | 749_880_806 | 30 | 31 | 32 | Heap 33 | 34 | | | decode() / too_candid() | encode() / from_candid() | 35 | | :---------------------------------- | ----------------------: | -----------------------: | 36 | | Serde 'mo:motoko_candid' lib | 10_271_552 | -1_821_176 | 37 | | Motoko (to_candid(), from_candid()) | 374_264 | 397_532 | 38 | | Serde: One Shot | 8_904 | -10_726_840 | 39 | | Serde: One Shot sans type inference | 8_904 | 28_404_880 | 40 | 41 | 42 | #### one_show: optimized back reference and implemented forwad reference 43 | Instructions 44 | 45 | | | decode() / too_candid() | encode() / from_candid() | 46 | | :------------------------------------- | ----------------------: | -----------------------: | 47 | | Serde 'mo:motoko_candid' lib | 1_053_794_644 | 2_000_106_476 | 48 | | Motoko (to_candid(), from_candid()) | 6_849_520 | 20_277_779 | 49 | | Serde: One Shot Back Reference (BR) | 3_444 | 814_495_115 | 50 | | Serde: One Shot BR sans type inference | 4_719 | 570_941_476 | 51 | | Serde: One Shot Forward Reference (FR) | 5_861 | 1_002_895_852 | 52 | | Serde: One Shot FR sans type inference | 7_067 | 759_206_807 | 53 | 54 | 55 | Heap 56 | 57 | | | decode() / too_candid() | encode() / from_candid() | 58 | | :------------------------------------- | ----------------------: | -----------------------: | 59 | | Serde 'mo:motoko_candid' lib | 10_271_024 | -1_831_772 | 60 | | Motoko (to_candid(), from_candid()) | 373_872 | 397_408 | 61 | | Serde: One Shot Back Reference (BR) | 8_904 | 14_959_180 | 62 | | Serde: One Shot BR sans type inference | 8_904 | -7_202_408 | 63 | | Serde: One Shot Forward Reference (FR) | 8_904 | -11_924_836 | 64 | | Serde: One Shot FR sans type inference | 8_904 | -4_800_284 | 65 | 66 | #### one_shot: br decoding 67 | 68 | Instructions 69 | 70 | | | decode() / too_candid() | encode() / from_candid() | 71 | | :------------------------------------- | ----------------------: | -----------------------: | 72 | | Motoko (to_candid(), from_candid()) | 6_852_599 | 20_277_307 | 73 | | Serde 'mo:motoko_candid' lib | 1_054_078_235 | 2_000_977_097 | 74 | | Serde: One Shot Back Reference (BR) | 283_341_316 | 814_744_565 | 75 | | Serde: One Shot BR sans type inference | 259_040_591 | 576_643_675 | 76 | | Serde: One Shot Forward Reference (FR) | 5_861 | 1_003_145_085 | 77 | | Serde: One Shot FR sans type inference | 7_067 | 765_997_023 | 78 | 79 | 80 | Heap 81 | 82 | | | decode() / too_candid() | encode() / from_candid() | 83 | | :------------------------------------- | ----------------------: | -----------------------: | 84 | | Motoko (to_candid(), from_candid()) | 374_696 | 397_764 | 85 | | Serde 'mo:motoko_candid' lib | 10_441_244 | -1_598_868 | 86 | | Serde: One Shot Back Reference (BR) | 17_063_820 | -16_414_880 | 87 | | Serde: One Shot BR sans type inference | 15_095_820 | -9_291_632 | 88 | | Serde: One Shot Forward Reference (FR) | 8_904 | -11_919_024 | 89 | | Serde: One Shot FR sans type inference | 8_904 | -4_795_636 | 90 | 91 | #### sorting user provided types before encoding or decoding 92 | 93 | Instructions 94 | 95 | | | decode() | encode() | 96 | | :---------------------------------- | ----------: | ----------: | 97 | | Serde: One Shot | 298_167_258 | 864_784_948 | 98 | | Serde: One Shot sans type inference | 343_730_830 | 654_138_729 | 99 | | Motoko (to_candid(), from_candid()) | 21_778_910 | 7_236_572 | 100 | 101 | 102 | Heap 103 | 104 | | | decode() | encode() | 105 | | :---------------------------------- | ---------: | ----------: | 106 | | Serde: One Shot | 18_078_612 | -11_697_036 | 107 | | Serde: One Shot sans type inference | 19_894_612 | -4_651_180 | 108 | | Motoko (to_candid(), from_candid()) | 433_756 | 376_168 | -------------------------------------------------------------------------------- /src/UrlEncoded/Parser.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.16.0/Char"; 2 | import Iter "mo:base@0.16.0/Iter"; 3 | import Float "mo:base@0.16.0/Float"; 4 | import List "mo:base@0.16.0/List"; 5 | import Nat32 "mo:base@0.16.0/Nat32"; 6 | 7 | import C "../../submodules/parser-combinators.mo/src/Combinators"; 8 | import P "../../submodules/parser-combinators.mo/src/Parser"; 9 | 10 | import Candid "../Candid"; 11 | 12 | module { 13 | type Candid = Candid.Candid; 14 | type List = List.List; 15 | 16 | type Parser = P.Parser; 17 | 18 | // Parse value to Candid 19 | // -------------------------------------------------- 20 | // "true" => #Bool(true) 21 | // "false" => #Bool(false) 22 | // "null" || "" => #Null 23 | // "42" => #Nat(42) 24 | // "-42" => #Int(-42) 25 | // "3.14" => #Float(3.14) 26 | // "hello" => #Text("hello") 27 | // "0042" => #Text("0042") 28 | // -------------------------------------------------- 29 | 30 | public func parseValue(text : Text) : Candid { 31 | let t = switch (text) { 32 | case ("true") return #Bool(true); 33 | case ("false") return #Bool(false); 34 | case ("null") return #Null; 35 | case ("") return #Null; 36 | case (t) t; 37 | }; 38 | 39 | let list = Iter.toList(t.chars()); 40 | 41 | // todo: parse Principal 42 | switch (parseCharList(list)) { 43 | case (?candid) candid; 44 | case (null) #Text(text); 45 | }; 46 | }; 47 | 48 | func parseCharList(l : List.List) : ?Candid { 49 | switch (valueParser()(l)) { 50 | case (null) { null }; 51 | case (?(x, xs)) { 52 | switch (xs) { 53 | case (null) { ?x }; 54 | case (_) { 55 | null; 56 | }; 57 | }; 58 | }; 59 | }; 60 | }; 61 | 62 | func valueParser() : Parser { 63 | C.oneOf([ 64 | floatParser(), 65 | natParser(), 66 | intParser(), 67 | ]); 68 | }; 69 | 70 | func floatParser() : Parser { 71 | 72 | func toFloat(tuple : (Int, List)) : Candid { 73 | let (n, d_chars) = tuple; 74 | 75 | let n_of_decimals = Float.fromInt(List.size(d_chars)); 76 | 77 | let num = Float.fromInt(n); 78 | let decimals = Float.fromInt(listToNat(d_chars)) / (10 ** n_of_decimals); 79 | 80 | let isNegative = num < 0; 81 | 82 | let float = if (isNegative) { 83 | num - decimals; 84 | } else { 85 | num + decimals; 86 | }; 87 | 88 | #Float(float); 89 | }; 90 | 91 | C.map( 92 | parseFloat(), 93 | toFloat, 94 | ); 95 | }; 96 | 97 | func parseFloat() : Parser)> { 98 | C.seq>( 99 | parseInt(), 100 | C.right( 101 | C.Character.char('.'), 102 | C.many1(C.Character.digit()), 103 | ), 104 | ); 105 | }; 106 | 107 | func parseInt() : Parser { 108 | func(xs : List) : ?(Int, List) { 109 | let parseSign = C.oneOf([ 110 | C.Character.char('+'), 111 | C.Character.char('-'), 112 | ]); 113 | 114 | let (op, ys) = switch (parseSign(xs)) { 115 | case (null) { (func(n : Nat) : Int { n }, xs) }; 116 | case (?('+', xs)) { (func(n : Nat) : Int { n }, xs) }; 117 | case (?(_, xs)) { (func(n : Nat) : Int { -n }, xs) }; 118 | }; 119 | 120 | let mapToInt = C.map( 121 | parseNat(), 122 | op, 123 | ); 124 | 125 | mapToInt(ys); 126 | }; 127 | }; 128 | 129 | func intParser() : Parser { 130 | C.map( 131 | parseInt(), 132 | func(n : Int) : Candid { 133 | #Int(n); 134 | }, 135 | ); 136 | }; 137 | 138 | func parseNat() : Parser { 139 | let noLeadingZeroes = consIf( 140 | C.Character.digit(), 141 | C.many(C.Character.digit()), 142 | 143 | func(first_digit : Char, digits : List) : Bool { 144 | let size_eq_1 = switch (List.pop(digits)) { 145 | case ((_, xs)) xs == null; 146 | }; 147 | 148 | first_digit != '0' or size_eq_1; 149 | }, 150 | ); 151 | 152 | C.map( 153 | noLeadingZeroes, 154 | listToNat, 155 | ); 156 | }; 157 | 158 | func natParser() : Parser { 159 | C.map( 160 | parseNat(), 161 | func(n : Nat) : Candid { 162 | #Nat(n); 163 | }, 164 | ); 165 | }; 166 | 167 | func listToNat(digits : List) : Nat { 168 | List.foldLeft( 169 | digits, 170 | 0, 171 | func(n : Nat, c : Char) : Nat { 172 | let digit = Nat32.toNat( 173 | Char.toNat32(c) - Char.toNat32('0') 174 | ); 175 | 176 | (10 * n) + digit; 177 | }, 178 | ); 179 | }; 180 | 181 | func consIf( 182 | parserA : Parser, 183 | parserAs : Parser>, 184 | cond : (A, List) -> Bool, 185 | ) : Parser> { 186 | C.bind( 187 | parserA, 188 | func(a : A) : Parser> { 189 | C.bind( 190 | parserAs, 191 | func(as : List) : Parser> { 192 | if (cond(a, as)) { 193 | P.result>(List.push(a, as)); 194 | } else { 195 | P.zero(); 196 | }; 197 | }, 198 | ); 199 | }, 200 | ); 201 | }; 202 | 203 | }; 204 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `serde` for Motoko 2 | 3 | An efficient serialization and deserialization library for Motoko. 4 | 5 | The library contains four modules: 6 | 7 | - **Candid** 8 | 9 | - `fromText()` - Converts [Candid text](https://internetcomputer.org/docs/current/tutorials/developer-journey/level-2/2.4-intro-candid/#candid-textual-values) to its serialized form. 10 | - `toText()` - Converts serialized candid to its [textual representation](https://internetcomputer.org/docs/current/tutorials/developer-journey/level-2/2.4-intro-candid/#candid-textual-values). 11 | - `encode()` - Converts the [Candid variant](./src/Candid/Types.mo#L6) to a blob. 12 | - `decode()` - Converts a blob to the [Candid variant](./src/Candid/Types.mo#L6). 13 | 14 | - encoding and decoding functions also support conversion between the [`ICRC3` value type](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-3#value) and candid. Checkout the example in the [usage guide](./usage.md#icrc3-value) 15 | 16 | - `fromICRC3Value()` - Converts an ICRC3 value to a candid variant. 17 | - `toICRC3Value()` - Converts a candid variant to an ICRC3 value. 18 | 19 | - **CBOR** 20 | 21 | - `encode()` - Converts serialized candid to CBOR. 22 | - `decode()` - Converts CBOR to a serialized candid. 23 | 24 | - **JSON** 25 | 26 | - `fromText()` - Converts JSON text to serialized candid. 27 | - `toText()` - Converts serialized candid to JSON text. 28 | 29 | - **URL-Encoded Pairs** 30 | - `fromText()` - Converts URL-encoded text to serialized candid. 31 | - `toText()` - Converts serialized candid to URL-encoded text. 32 | 33 | ## Getting Started 34 | 35 | ### Installation 36 | 37 | [![mops](https://oknww-riaaa-aaaam-qaf6a-cai.raw.ic0.app/badge/mops/serde)](https://mops.one/serde) 38 | 39 | 1. Install [`mops`](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install). 40 | 2. Inside your project directory, run: 41 | 42 | ```bash 43 | mops install serde 44 | ``` 45 | 46 | ### Usage 47 | 48 | To start, import the necessary modules: 49 | 50 | ```motoko 51 | import { JSON; Candid; CBOR; UrlEncoded } from "mo:serde"; 52 | ``` 53 | 54 | #### JSON 55 | 56 | > The following code can be used for converting data between the other modules (Candid and URL-Encoded Pairs). 57 | 58 | **Example: JSON to Motoko** 59 | 60 | 1. **Defining Data Type**: This critical step informs the conversion functions (`from_candid` and `to_candid`) about how to handle the data. 61 | 62 | Consider the following JSON data: 63 | 64 | ```json 65 | [ 66 | { 67 | "name": "John", 68 | "id": 123 69 | }, 70 | { 71 | "name": "Jane", 72 | "id": 456, 73 | "email": "jane@gmail.com" 74 | } 75 | ] 76 | ``` 77 | 78 | The optional `email` field translates to: 79 | 80 | ```motoko 81 | type User = { 82 | name: Text; 83 | id: Nat; 84 | email: ?Text; 85 | }; 86 | ``` 87 | 88 | 2. **Conversion**: 89 | a. Parse JSON text into a candid blob using `JSON.fromText`. 90 | b. Convert the blob to a Motoko data type with `from_candid`. 91 | 92 | ```motoko 93 | let jsonText = "[{\"name\": \"John\", \"id\": 123}, {\"name\": \"Jane\", \"id\": 456, \"email\": \"jane@gmail.com\"}]"; 94 | 95 | let #ok(blob) = JSON.fromText(jsonText, null); // you probably want to handle the error case here :) 96 | let users : ?[User] = from_candid(blob); 97 | 98 | assert users == ?[ 99 | { 100 | name = "John"; 101 | id = 123; 102 | email = null; 103 | }, 104 | { 105 | name = "Jane"; 106 | id = 456; 107 | email = ?"jane@gmail.com"; 108 | }, 109 | ]; 110 | ``` 111 | 112 | **Example: Motoko to JSON** 113 | 114 | 1. **Record Keys**: Collect all unique record keys from your data type into an array. This helps the module convert the record keys correctly instead of returning its hash. 115 | 116 | ```motoko 117 | let UserKeys = ["name", "id", "email"]; 118 | ``` 119 | 120 | 2. **Conversion**: 121 | 122 | ```motoko 123 | let users: [User] = [ 124 | { 125 | name = "John"; 126 | id = 123; 127 | email = null; 128 | }, 129 | { 130 | name = "Jane"; 131 | id = 456; 132 | email = ?"jane@gmail.com"; 133 | }, 134 | ]; 135 | 136 | let blob = to_candid(users); 137 | let json_result = JSON.toText(blob, UserKeys, null); 138 | 139 | assert json_result == #ok( 140 | "[{\"name\": \"John\",\"id\": 123},{\"name\": \"Jane\",\"id\":456,\"email\":\"jane@gmail.com\"}]" 141 | ); 142 | ``` 143 | 144 | **Example: Renaming Fields** 145 | 146 | - Useful way to rename fields with reserved keywords in Motoko. 147 | 148 | ```motoko 149 | import Serde from "mo:serde"; 150 | 151 | // type JsonSchemaWithReservedKeys = { 152 | // type: Text; // reserved 153 | // label: Text; // reserved 154 | // id: Nat; 155 | // }; 156 | 157 | type Item = { 158 | item_type: Text; 159 | item_label: Text; 160 | id: Nat 161 | }; 162 | 163 | let jsonText = "{\"type\": \"bar\", \"label\": \"foo\", \"id\": 112}"; 164 | let options: Serde.Options = { 165 | renameKeys = [("type", "item_type"), ("label", "item_label")] 166 | }; 167 | 168 | let #ok(blob) = Serde.JSON.fromText(jsonText, ?options); 169 | let renamedKeys: ?Item = from_candid(blob); 170 | 171 | assert renamedKeys == ?{ item_type = "bar"; item_label = "foo"; id = 112 }; 172 | ``` 173 | 174 | Checkout the [usage guide](https://github.com/NatLabs/serde/blob/main/usage.md) for additional examples: 175 | 176 | - [Candid](https://github.com/NatLabs/serde/blob/main/usage.md#candid-text) 177 | - [URL-Encoded Pairs](https://github.com/NatLabs/serde/blob/main/usage.md#url-encoded-pairs) 178 | 179 | ## Limitations 180 | 181 | - Users must provide a list of record keys and variant names during conversions from Motoko to other data formats due to constraints in the candid format. 182 | - Lack of specific syntax for conversion between `Blob`, `Principal`, and bounded `Nat`/`Int` types. 183 | - Cannot deserialize Tuples as they are not candid types. They are just shorthands for records with unnamed fields. See https://forum.dfinity.org/t/candid-and-tuples/17800/7 184 | - Floats are only recognised if they have a decimal point, e.g., `1.0` is a Float, but `1` is an `Int` / `Nat`. 185 | - Only supports candid data types (i.e primitive and constructed types). Service and function reference types are not supported. 186 | - A new rewrite of the library to improve performance has limited the max value of the `Nat` and `Int` to 64 bits. 187 | 188 | ## Running Tests 189 | 190 | 1. Install dependencies: 191 | 192 | - [mops](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install) 193 | - [mocv](https://github.com/ZenVoich/mocv) 194 | - [wasmtime](https://github.com/bytecodealliance/wasmtime/blob/main/README.md#wasmtime) 195 | 196 | 2. Inside the project directory, run: 197 | 198 | ```bash 199 | mops test 200 | ``` 201 | 202 | --- 203 | 204 | Happy coding with `serde`! 🚀 205 | -------------------------------------------------------------------------------- /tests/Candid.Large.test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Iter "mo:base@0.16.0/Iter"; 3 | import Debug "mo:base@0.16.0/Debug"; 4 | import Prelude "mo:base@0.16.0/Prelude"; 5 | import Text "mo:base@0.16.0/Text"; 6 | import Char "mo:base@0.16.0/Char"; 7 | import Buffer "mo:base@0.16.0/Buffer"; 8 | 9 | import Fuzz "mo:fuzz"; 10 | import Itertools "mo:itertools@0.2.2/Iter"; 11 | import { test; suite } "mo:test"; 12 | 13 | import Serde "../src"; 14 | import CandidEncoder "../src/Candid/Blob/Encoder"; 15 | import CandidDecoder "../src/Candid/Blob/Decoder"; 16 | import LegacyCandidDecoder "../src/Candid/Blob/Decoder"; 17 | import LegacyCandidEncoder "../src/Candid/Blob/Encoder"; 18 | 19 | let fuzz = Fuzz.fromSeed(0x7eadbeef); 20 | 21 | let limit = 1_000; 22 | 23 | let candify_store_item = { 24 | from_blob = func(blob : Blob) : StoreItem { 25 | let ?c : ?StoreItem = from_candid (blob); 26 | c; 27 | }; 28 | to_blob = func(c : StoreItem) : Blob { to_candid (c) }; 29 | }; 30 | 31 | type X = { name : Text; x : X }; 32 | 33 | // let x: X = { 34 | // name = "yo"; 35 | // x = { 36 | // name = "yo"; 37 | // x = {}; 38 | // }; 39 | // }; 40 | 41 | // let x : Serde.Candid = #Record([ 42 | // ("name", #Text("yo")), 43 | // ("x", x) 44 | // ]); 45 | type CustomerReview = { 46 | username : Text; 47 | rating : Nat; 48 | comment : Text; 49 | }; 50 | 51 | let CustomerReview = #Record([ 52 | ("username", #Text), 53 | ("rating", #Nat), 54 | ("comment", #Text), 55 | ]); 56 | 57 | type AvailableSizes = { #xs; #s; #m; #l; #xl }; 58 | 59 | let AvailableSizes = #Variant([ 60 | ("xs", #Null), 61 | ("s", #Null), 62 | ("m", #Null), 63 | ("l", #Null), 64 | ("xl", #Null), 65 | ]); 66 | 67 | type ColorOption = { 68 | name : Text; 69 | hex : Text; 70 | }; 71 | 72 | let ColorOption = #Record([ 73 | ("name", #Text), 74 | ("hex", #Text), 75 | ]); 76 | 77 | type StoreItem = { 78 | name : Text; 79 | store : Text; 80 | customer_reviews : [CustomerReview]; 81 | available_sizes : AvailableSizes; 82 | color_options : [ColorOption]; 83 | price : Float; 84 | in_stock : Bool; 85 | address : (Text, Text, Text, Text); 86 | contact : { 87 | email : Text; 88 | phone : ?Text; 89 | }; 90 | }; 91 | 92 | let StoreItem : Serde.Candid.CandidType = #Record([ 93 | ("name", #Text), 94 | ("store", #Text), 95 | ("customer_reviews", #Array(CustomerReview)), 96 | ("available_sizes", AvailableSizes), 97 | ("color_options", #Array(ColorOption)), 98 | ("price", #Float), 99 | ("in_stock", #Bool), 100 | ("address", #Tuple([#Text, #Text, #Text, #Text])), 101 | ("contact", #Record([("email", #Text), ("phone", #Option(#Text))])), 102 | ]); 103 | 104 | let FormattedStoreItem = Serde.Candid.formatCandidType([StoreItem], null); 105 | 106 | let cities = ["Toronto", "Ottawa", "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose"]; 107 | let states = ["ON", "QC", "NY", "CA", "IL", "TX", "AZ", "PA", "TX", "CA", "TX", "CA"]; 108 | let streets = ["King St", "Queen St", "Yonge St", "Bay St", "Bloor St", "Dundas St", "College St", "Spadina Ave", "St Clair Ave", "Danforth Ave", "Eglinton Ave", "Lawrence Ave"]; 109 | 110 | let stores = ["h&m", "zara", "gap", "old navy", "forever 21", "uniqlo", "urban outfitters", "american eagle", "aeropostale", "abercrombie & fitch", "hollister", "express"]; 111 | let email_terminator = ["gmail.com", "yahoo.com", "outlook.com"]; 112 | 113 | let cs_starter_kid = ["black hoodie", "M1 macbook", "white hoodie", "air forces", "Algorithms textbook", "c the hard way", "Udemy subscription", "Nvidea RTX"]; 114 | 115 | let available_sizes = [#xs, #s, #m, #l, #xl]; 116 | 117 | func new_item() : StoreItem { 118 | let store_name = fuzz.array.randomEntry(stores).1; 119 | let store_item : StoreItem = { 120 | name = fuzz.array.randomEntry(cs_starter_kid).1; 121 | store = store_name; 122 | customer_reviews = [ 123 | { 124 | username = "user1"; 125 | rating = fuzz.nat.randomRange(0, 5); 126 | comment = "good"; 127 | }, 128 | { 129 | username = "user2"; 130 | rating = fuzz.nat.randomRange(0, 5); 131 | comment = "ok"; 132 | }, 133 | ]; 134 | available_sizes = fuzz.array.randomEntry(available_sizes).1; 135 | color_options = [ 136 | { name = "red"; hex = "#ff0000" }, 137 | { name = "blue"; hex = "#0000ff" }, 138 | ]; 139 | price = fuzz.float.randomRange(19.99, 399.99); 140 | in_stock = fuzz.bool.random(); 141 | address = ( 142 | fuzz.array.randomEntry(streets).1, 143 | fuzz.array.randomEntry(cities).1, 144 | fuzz.array.randomEntry(states).1, 145 | fuzz.text.randomAlphanumeric(6), 146 | ); 147 | contact = { 148 | email = store_name # "@" # fuzz.array.randomEntry(email_terminator).1; 149 | phone = if (fuzz.nat.randomRange(0, 100) % 3 == 0) { null } else { 150 | ?Text.fromIter( 151 | fuzz.array.randomArray(10, func() : Char { Char.fromNat32(fuzz.nat32.randomRange(0, 9) + Char.toNat32('0')) }).vals() : Iter.Iter 152 | ); 153 | }; 154 | }; 155 | }; 156 | }; 157 | 158 | let store_item_keys = ["name", "store", "customer_reviews", "username", "rating", "comment", "available_sizes", "xs", "s", "m", "l", "xl", "color_options", "name", "hex", "price", "in_stock", "address", "contact", "email", "phone"]; 159 | 160 | let candid_buffer = Buffer.Buffer<[Serde.Candid]>(limit); 161 | let store_items = Buffer.Buffer(limit); 162 | 163 | let candid_buffer_with_types = Buffer.Buffer<[Serde.Candid]>(limit); 164 | let store_items_with_types = Buffer.Buffer(limit); 165 | 166 | suite( 167 | "Serde.Candid", 168 | func() { 169 | test( 170 | "decode()", 171 | func() { 172 | for (i in Itertools.range(0, limit)) { 173 | let item = new_item(); 174 | store_items.add(item); 175 | let candid_blob = candify_store_item.to_blob(item); 176 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, store_item_keys, null); 177 | candid_buffer.add(candid); 178 | }; 179 | }, 180 | ); 181 | test( 182 | "encode()", 183 | func() { 184 | for (i in Itertools.range(0, limit)) { 185 | let candid = candid_buffer.get(i); 186 | let res = LegacyCandidEncoder.encode(candid, null); 187 | let #ok(blob) = res; 188 | let item = candify_store_item.from_blob(blob); 189 | assert item == store_items.get(i); 190 | }; 191 | }, 192 | ); 193 | test( 194 | "decode() with types", 195 | func() { 196 | for (i in Itertools.range(0, limit)) { 197 | let item = new_item(); 198 | store_items_with_types.add(item); 199 | let candid_blob = candify_store_item.to_blob(item); 200 | let #ok(split_blob) = CandidDecoder.split(candid_blob, null); 201 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, store_item_keys, ?{ Serde.Candid.defaultOptions with types = ?FormattedStoreItem }); 202 | candid_buffer_with_types.add(candid); 203 | }; 204 | }, 205 | ); 206 | test( 207 | "encode() with types", 208 | func() { 209 | for (i in Itertools.range(0, limit)) { 210 | let candid = candid_buffer_with_types.get(i); 211 | let res = LegacyCandidEncoder.encode(candid, ?{ Serde.Candid.defaultOptions with types = ?FormattedStoreItem }); 212 | let #ok(blob) = res; 213 | let item = candify_store_item.from_blob(blob); 214 | assert item == store_items_with_types.get(i); 215 | }; 216 | }, 217 | ); 218 | }, 219 | ); 220 | -------------------------------------------------------------------------------- /tests/CBOR.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Array "mo:base@0.16.0/Array"; 3 | import Blob "mo:base@0.16.0/Blob"; 4 | import Debug "mo:base@0.16.0/Debug"; 5 | import Iter "mo:base@0.16.0/Iter"; 6 | import Principal "mo:base@0.16.0/Principal"; 7 | import Text "mo:base@0.16.0/Text"; 8 | 9 | import { test; suite } "mo:test"; 10 | 11 | import { CBOR } "../src"; 12 | 13 | suite( 14 | "CBOR Test", 15 | func() { 16 | test( 17 | "options", 18 | func() { 19 | let opt_nat_null : ?Nat = null; 20 | let opt_nat : ?Nat = ?123; 21 | let opt_text_null : ?Text = null; 22 | let opt_text : ?Text = ?"hello"; 23 | 24 | let opt_nat_null_candid = to_candid (opt_nat_null); 25 | let opt_nat_candid = to_candid (opt_nat); 26 | let opt_text_null_candid = to_candid (opt_text_null); 27 | let opt_text_candid = to_candid (opt_text); 28 | 29 | let #ok(opt_nat_null_cbor) = CBOR.encode(opt_nat_null_candid, [], null); 30 | let #ok(opt_nat_cbor) = CBOR.encode(opt_nat_candid, [], null); 31 | let #ok(opt_text_null_cbor) = CBOR.encode(opt_text_null_candid, [], null); 32 | let #ok(opt_text_cbor) = CBOR.encode(opt_text_candid, [], null); 33 | 34 | let #ok(opt_nat_null_candid2) = CBOR.decode(opt_nat_null_cbor, null); 35 | let #ok(opt_nat_candid2) = CBOR.decode(opt_nat_cbor, null); 36 | let #ok(opt_text_null_candid2) = CBOR.decode(opt_text_null_cbor, null); 37 | let #ok(opt_text_candid2) = CBOR.decode(opt_text_cbor, null); 38 | 39 | assert opt_nat_null_candid != opt_nat_null_candid2; 40 | assert opt_nat_candid != opt_nat_candid2; 41 | assert opt_text_null_candid != opt_text_null_candid2; 42 | assert opt_text_candid != opt_text_candid2; 43 | 44 | let ?opt_nat_null2 : ?(?Nat) = from_candid (opt_nat_null_candid2); 45 | let ?opt_nat2 : ?(?Nat) = from_candid (opt_nat_candid2); 46 | let ?opt_text_null2 : ?(?Text) = from_candid (opt_text_null_candid2); 47 | let ?opt_text2 : ?(?Text) = from_candid (opt_text_candid2); 48 | 49 | assert opt_nat_null2 == opt_nat_null; 50 | assert opt_nat2 == opt_nat; 51 | assert opt_text_null2 == opt_text_null; 52 | assert opt_text2 == opt_text; 53 | 54 | }, 55 | ); 56 | 57 | test( 58 | "primitives", 59 | func() { 60 | 61 | let nat : Nat = 123; 62 | let int : Int = -123; 63 | let float : Float = 123.456; 64 | let bool : Bool = true; 65 | let text : Text = "hello"; 66 | let blob : Blob = "\01\02\03"; 67 | let _null : Null = null; 68 | let empty = (); 69 | let list : [Nat] = [1, 2, 3]; 70 | let record = { a = 1; b = 2 }; 71 | let principal = Principal.fromText("bkyz2-fmaaa-aaaaa-qaaaq-cai"); 72 | 73 | let nat_candid = to_candid (nat); 74 | let int_candid = to_candid (int); 75 | let float_candid = to_candid (float); 76 | let bool_candid = to_candid (bool); 77 | let text_candid = to_candid (text); 78 | let blob_candid = to_candid (blob); 79 | let null_candid = to_candid (_null); 80 | let empty_candid = to_candid (empty); 81 | let list_candid = to_candid (list); 82 | let record_candid = to_candid (record); 83 | let principal_candid = to_candid (principal); 84 | 85 | let #ok(nat_cbor) = CBOR.encode(nat_candid, [], null); 86 | let #ok(int_cbor) = CBOR.encode(int_candid, [], null); 87 | let #ok(float_cbor) = CBOR.encode(float_candid, [], null); 88 | let #ok(bool_cbor) = CBOR.encode(bool_candid, [], null); 89 | let #ok(text_cbor) = CBOR.encode(text_candid, [], null); 90 | let #ok(blob_cbor) = CBOR.encode(blob_candid, [], null); 91 | let #ok(null_cbor) = CBOR.encode(null_candid, [], null); 92 | let #ok(empty_cbor) = CBOR.encode(empty_candid, [], null); 93 | let #ok(list_cbor) = CBOR.encode(list_candid, [], null); 94 | let #ok(record_cbor) = CBOR.encode(record_candid, ["a", "b"], null); 95 | let #ok(principal_cbor) = CBOR.encode(principal_candid, [], null); 96 | 97 | let self_describe_tag : Blob = "\D9\D9\F7"; 98 | 99 | func blob_concat(b1 : Blob, b2 : Blob) : Blob = Blob.fromArray(Array.append(Blob.toArray(b1), Blob.toArray(b2))); 100 | func sdt(blob : Blob) : Blob = blob_concat(self_describe_tag, blob); 101 | func strip(text : Text, to_strip : Text) : Text = Text.replace(text, #text(to_strip), ""); 102 | 103 | // cbor encodings from https://cbor.me/ 104 | assert nat_cbor == sdt("\18\7B"); 105 | assert int_cbor == sdt("\38\7A"); 106 | assert float_cbor == sdt("\FB\40\5E\DD\2F\1A\9F\BE\77"); 107 | assert bool_cbor == sdt("\F5"); 108 | assert text_cbor == sdt("\65\68\65\6C\6C\6F"); 109 | assert blob_cbor == sdt("\43\01\02\03"); 110 | assert null_cbor == sdt("\F6"); 111 | // assert empty_cbor == sdt("\F7"); // mapped to undefined 112 | assert list_cbor == sdt("\83\01\02\03"); 113 | assert record_cbor == sdt("\A2\61\61\01\61\62\02"); 114 | assert strip(debug_show (principal_cbor), "\"") == strip((debug_show ("\D9\D9\F7\4A" : Blob) # debug_show (Principal.toBlob(principal))), "\""); 115 | 116 | let #ok(nat_candid2) = CBOR.decode(nat_cbor, null); 117 | let #ok(int_candid2) = CBOR.decode(int_cbor, null); 118 | let #ok(float_candid2) = CBOR.decode(float_cbor, null); 119 | let #ok(bool_candid2) = CBOR.decode(bool_cbor, null); 120 | let #ok(text_candid2) = CBOR.decode(text_cbor, null); 121 | let #ok(blob_candid2) = CBOR.decode(blob_cbor, null); 122 | let #ok(null_candid2) = CBOR.decode(null_cbor, null); 123 | let #ok(empty_candid2) = CBOR.decode(empty_cbor, null); 124 | let #ok(list_candid2) = CBOR.decode(list_cbor, null); 125 | let #ok(record_candid2) = CBOR.decode(record_cbor, null); 126 | let #ok(principal_candid2) = CBOR.decode(principal_cbor, null); 127 | 128 | assert nat_candid == nat_candid2; 129 | assert int_candid == int_candid2; 130 | assert float_candid == float_candid2; 131 | assert bool_candid == bool_candid2; 132 | assert text_candid == text_candid2; 133 | assert blob_candid == blob_candid2; 134 | assert null_candid == null_candid2; 135 | assert empty_candid == empty_candid2; 136 | assert list_candid == list_candid2; 137 | assert record_candid == record_candid2; 138 | // assert principal_candid == principal_candid2; 139 | 140 | let ?nat2 : ?Nat = from_candid (nat_candid2); 141 | let ?int2 : ?Int = from_candid (int_candid2); 142 | let ?float2 : ?Float = from_candid (float_candid2); 143 | let ?bool2 : ?Bool = from_candid (bool_candid2); 144 | let ?text2 : ?Text = from_candid (text_candid2); 145 | let ?blob2 : ?Blob = from_candid (blob_candid2); 146 | let ?null2 : ?Null = from_candid (null_candid2); 147 | let ?empty2 : ?() = from_candid (empty_candid2); 148 | let ?list2 : ?[Nat] = from_candid (list_candid2); 149 | let ?record2 : ?{ a : Nat; b : Nat } = from_candid (record_candid2); 150 | let ?principal_as_blob : ?Blob = from_candid (principal_candid2); 151 | 152 | assert nat2 == nat; 153 | assert int2 == int; 154 | assert float2 == float; 155 | assert bool2 == bool; 156 | assert text2 == text; 157 | assert blob2 == blob; 158 | assert null2 == _null; 159 | assert empty2 == empty; 160 | assert list2 == list; 161 | assert record2 == record; 162 | assert principal_as_blob == Principal.toBlob(principal); 163 | 164 | }, 165 | ); 166 | 167 | }, 168 | ); 169 | -------------------------------------------------------------------------------- /src/Candid/Blob/CandidUtils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Buffer "mo:base@0.16.0/Buffer"; 3 | import Result "mo:base@0.16.0/Result"; 4 | import Nat32 "mo:base@0.16.0/Nat32"; 5 | import Nat "mo:base@0.16.0/Nat"; 6 | import Iter "mo:base@0.16.0/Iter"; 7 | import Text "mo:base@0.16.0/Text"; 8 | import Order "mo:base@0.16.0/Order"; 9 | 10 | import Itertools "mo:itertools@0.2.2/Iter"; 11 | import Map "mo:map@9.0.1/Map"; 12 | 13 | import T "../Types"; 14 | import TrieMap "mo:base@0.16.0/TrieMap"; 15 | import Utils "../../Utils"; 16 | 17 | module { 18 | 19 | type TrieMap = TrieMap.TrieMap; 20 | type Result = Result.Result; 21 | type Buffer = Buffer.Buffer; 22 | type Iter = Iter.Iter; 23 | type Hash = Nat32; 24 | type Map = Map.Map; 25 | type Order = Order.Order; 26 | 27 | type Candid = T.Candid; 28 | type CandidType = T.CandidType; 29 | type KeyValuePair = T.KeyValuePair; 30 | 31 | let { thash } = Map; 32 | 33 | public func cmp_fields(a : (Text, Any), b : (Text, Any)) : Order { 34 | 35 | let hash_a = Utils.hash_record_key(a.0); 36 | let hash_b = Utils.hash_record_key(b.0); 37 | 38 | Nat32.compare(hash_a, hash_b); 39 | }; 40 | 41 | public func cmp_nat_fields(a : (Text, Any), b : (Text, Any)) : Order { 42 | 43 | let n1 = Utils.text_to_nat(a.0); 44 | let n2 = Utils.text_to_nat(b.0); 45 | 46 | Nat.compare(n1, n2); 47 | }; 48 | 49 | public func is_record_tuple(record_fields : [(Text, Any)]) : Bool { 50 | Itertools.all( 51 | record_fields.vals(), 52 | func(field : (Text, Any)) : Bool { 53 | Utils.text_is_number(field.0); 54 | }, 55 | ); 56 | }; 57 | 58 | public func sort_candid_type(candid_type : CandidType) : CandidType { 59 | switch (candid_type) { 60 | case (#Record(fields)) { 61 | let is_tuple = Itertools.all( 62 | fields.vals(), 63 | func(field : (Text, Any)) : Bool { 64 | Utils.text_is_number(field.0); 65 | }, 66 | ); 67 | 68 | let sorted_fields = if (is_tuple) { 69 | (Array.sort(fields, cmp_nat_fields)); 70 | } else { 71 | (Array.sort(fields, cmp_fields)); 72 | }; 73 | 74 | let sorted_nested_fields = Array.map<(Text, CandidType), (Text, CandidType)>( 75 | sorted_fields, 76 | func(field : (Text, CandidType)) : (Text, CandidType) { 77 | (field.0, sort_candid_type(field.1)); 78 | }, 79 | ); 80 | 81 | #Record(sorted_nested_fields); 82 | }; 83 | case (#Variant(fields)) { 84 | let is_tuple = Itertools.all( 85 | fields.vals(), 86 | func(field : (Text, CandidType)) : Bool { 87 | Utils.text_is_number(field.0); 88 | }, 89 | ); 90 | 91 | let sorted_fields = if (is_tuple) { 92 | (Array.sort(fields, cmp_nat_fields)); 93 | } else { 94 | (Array.sort(fields, cmp_fields)); 95 | }; 96 | 97 | let sorted_nested_fields = Array.map<(Text, CandidType), (Text, CandidType)>( 98 | sorted_fields, 99 | func(field : (Text, CandidType)) : (Text, CandidType) { 100 | (field.0, sort_candid_type(field.1)); 101 | }, 102 | ); 103 | 104 | #Variant(sorted_nested_fields); 105 | }; 106 | case (#Array(arr_type)) #Array(sort_candid_type(arr_type)); 107 | case (#Option(opt_type)) #Option(sort_candid_type(opt_type)); 108 | case (#Tuple(tuple_types)) #Tuple(Array.map(tuple_types, sort_candid_type)); 109 | case (other_types) other_types; 110 | }; 111 | }; 112 | 113 | func tuple_type_to_record(tuple_types : [CandidType], mapper : (CandidType) -> CandidType) : [(Text, CandidType)] { 114 | Array.tabulate<(Text, CandidType)>( 115 | tuple_types.size(), 116 | func(i : Nat) : (Text, CandidType) { 117 | let key = Text.replace(debug_show (i), #char('_'), ""); 118 | (key, mapper(tuple_types[i])); 119 | }, 120 | ); 121 | }; 122 | 123 | /// Sorts fields by their hash value and renames changed fields 124 | /// 125 | /// Also resolves aliases by renaming them to their canonical name, 126 | /// for example #Blob to #Array(#Nat8) 127 | public func format_candid_type(candid_type : CandidType, renaming_map : Map) : CandidType { 128 | switch (candid_type) { 129 | case (#Record(fields) or #Map(fields)) { 130 | 131 | var is_tuple = true; 132 | 133 | let renamed_fields = Array.tabulate<(Text, CandidType)>( 134 | fields.size(), 135 | func(i : Nat) : (Text, CandidType) { 136 | let field_key = fields[i].0; 137 | let field_value = fields[i].1; 138 | 139 | let new_key = switch (Map.get(renaming_map, thash, field_key)) { 140 | case (?new_key) new_key; 141 | case (_) field_key; 142 | }; 143 | 144 | is_tuple := is_tuple and Utils.text_is_number(new_key); 145 | 146 | (new_key, format_candid_type(field_value, renaming_map)); 147 | }, 148 | ); 149 | 150 | let sorted_fields = if (is_tuple) { 151 | (Array.sort(renamed_fields, cmp_nat_fields)); 152 | } else { 153 | (Array.sort(renamed_fields, cmp_fields)); 154 | }; 155 | 156 | #Record(sorted_fields); 157 | }; 158 | case (#Variant(fields)) { 159 | var is_tuple = true; 160 | 161 | let renamed_fields = Array.tabulate<(Text, CandidType)>( 162 | fields.size(), 163 | func(i : Nat) : (Text, CandidType) { 164 | let field_key = fields[i].0; 165 | let field_value = fields[i].1; 166 | 167 | let new_key = switch (Map.get(renaming_map, thash, field_key)) { 168 | case (?new_key) new_key; 169 | case (_) field_key; 170 | }; 171 | 172 | is_tuple := is_tuple and Utils.text_is_number(new_key); 173 | 174 | (new_key, format_candid_type(field_value, renaming_map)); 175 | }, 176 | ); 177 | 178 | let sorted_fields = if (is_tuple) { 179 | (Array.sort(renamed_fields, cmp_nat_fields)); 180 | } else { 181 | (Array.sort(renamed_fields, cmp_fields)); 182 | }; 183 | 184 | #Variant(sorted_fields); 185 | }; 186 | case (#Blob) #Array(#Nat8); 187 | case (#Array(arr_type)) #Array(format_candid_type(arr_type, renaming_map)); 188 | case (#Option(opt_type)) #Option(format_candid_type(opt_type, renaming_map)); 189 | case (#Tuple(tuple_types)) #Record( 190 | tuple_type_to_record( 191 | tuple_types, 192 | func(candid_type : CandidType) : CandidType = format_candid_type(candid_type, renaming_map), 193 | ) 194 | ); 195 | case (other_types) other_types; 196 | }; 197 | }; 198 | 199 | public func sort_candid_value(candid_value : Candid) : Candid { 200 | switch (candid_value) { 201 | case (#Record(fields)) { 202 | let is_tuple = Itertools.all( 203 | fields.vals(), 204 | func(field : (Text, Any)) : Bool { 205 | Utils.text_is_number(field.0); 206 | }, 207 | ); 208 | 209 | if (is_tuple) { 210 | #Record(Array.sort(fields, cmp_nat_fields)); 211 | } else { 212 | #Record(Array.sort(fields, cmp_fields)); 213 | }; 214 | }; 215 | case (other_values) other_values; 216 | }; 217 | }; 218 | 219 | public func RecordType(records : [(Text, CandidType)]) : CandidType { 220 | #Record(Array.sort(records, cmp_fields)); 221 | }; 222 | 223 | public func RecordValue(records : [(Text, Candid)]) : Candid { 224 | #Record(Array.sort(records, cmp_fields)); 225 | }; 226 | }; 227 | -------------------------------------------------------------------------------- /tests/UrlEncoded.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Debug "mo:base@0.16.0/Debug"; 3 | import Iter "mo:base@0.16.0/Iter"; 4 | 5 | import { test; suite } "mo:test"; 6 | 7 | import UrlEncoded "../src/UrlEncoded"; 8 | 9 | type User = { 10 | name : Text; 11 | msg : Text; 12 | }; 13 | 14 | suite( 15 | "UrlEncoded Pairs", 16 | func() { 17 | suite( 18 | "fromText to motoko", 19 | func() { 20 | test( 21 | "single record", 22 | func() { 23 | 24 | let blob = switch (UrlEncoded.fromText("msg=Hello World&name=John", null)) { 25 | case (#ok(b)) b; 26 | case (#err(errorMsg)) Debug.trap(errorMsg); 27 | }; 28 | 29 | let res : ?User = from_candid (blob); 30 | 31 | assert ( 32 | res == ?{ 33 | name = "John"; 34 | msg = "Hello World"; 35 | } 36 | ); 37 | }, 38 | ); 39 | test( 40 | "pairs with empty values", 41 | func() { 42 | 43 | let #ok(unknown_blob) = UrlEncoded.fromText("msg=Hello&name=", null); 44 | let #ok(known_blob) = UrlEncoded.fromText("msg=Hello&name=John", null); 45 | 46 | type UserOptionalName = { 47 | name : ?Text; 48 | msg : Text; 49 | }; 50 | 51 | let unknown_user : ?UserOptionalName = from_candid (unknown_blob); 52 | let known_user : ?UserOptionalName = from_candid (known_blob); 53 | 54 | assert ( 55 | unknown_user == ?{ 56 | name = null; 57 | msg = "Hello"; 58 | } 59 | ); 60 | assert ( 61 | known_user == ?{ 62 | name = ?"John"; 63 | msg = "Hello"; 64 | } 65 | ); 66 | }, 67 | ); 68 | test( 69 | "record with array", 70 | func() { 71 | 72 | let text = "users[0][name]=John&users[0][msg]=Hello World&users[1][name]=Jane&users[1][msg]=testing"; 73 | let #ok(blob) = UrlEncoded.fromText(text, null); 74 | 75 | let res : ?{ users : [User] } = from_candid (blob); 76 | assert ( 77 | res == ?{ 78 | users = [ 79 | { 80 | name = "John"; 81 | msg = "Hello World"; 82 | }, 83 | { 84 | name = "Jane"; 85 | msg = "testing"; 86 | }, 87 | ]; 88 | } 89 | ); 90 | }, 91 | ); 92 | test( 93 | "variant type", 94 | func() { 95 | type Variant = { 96 | #text : Text; 97 | #nat : Nat; 98 | #int : Int; 99 | #float : Float; 100 | #bool : Bool; 101 | #record : { site : Text }; 102 | #user : User; 103 | #array : [Nat]; 104 | }; 105 | 106 | let text = "variant[#text]=hello"; 107 | let nat = "variant[#nat]=123"; 108 | let int = "variant[#int]=-123"; 109 | let float = "variant[#float]=-1.23"; 110 | let bool = "variant[#bool]=true"; 111 | let record = "variant[#record][site]=github"; 112 | let user = "variant[#user][name]=John&variant[#user][msg]=Hello World"; 113 | let array = "variant[#array][0]=1&variant[#array][1]=2&variant[#array][2]=3"; 114 | 115 | let #ok(text_blob) = UrlEncoded.fromText(text, null); 116 | let #ok(nat_blob) = UrlEncoded.fromText(nat, null); 117 | let #ok(int_blob) = UrlEncoded.fromText(int, null); 118 | let #ok(float_blob) = UrlEncoded.fromText(float, null); 119 | let #ok(bool_blob) = UrlEncoded.fromText(bool, null); 120 | let #ok(record_blob) = UrlEncoded.fromText(record, null); 121 | let #ok(user_blob) = UrlEncoded.fromText(user, null); 122 | let #ok(array_blob) = UrlEncoded.fromText(array, null); 123 | 124 | let text_val : ?{ variant : Variant } = from_candid (text_blob); 125 | let nat_val : ?{ variant : Variant } = from_candid (nat_blob); 126 | let int_val : ?{ variant : Variant } = from_candid (int_blob); 127 | let float_val : ?{ variant : Variant } = from_candid (float_blob); 128 | let bool_val : ?{ variant : Variant } = from_candid (bool_blob); 129 | let record_val : ?{ variant : Variant } = from_candid (record_blob); 130 | let user_val : ?{ variant : Variant } = from_candid (user_blob); 131 | let array_val : ?{ variant : Variant } = from_candid (array_blob); 132 | 133 | assert (text_val == ?{ variant = #text("hello") }); 134 | assert (nat_val == ?{ variant = #nat(123) }); 135 | assert (int_val == ?{ variant = #int(-123) }); 136 | assert (float_val == ?{ variant = #float(-1.23) }); 137 | assert (bool_val == ?{ variant = #bool(true) }); 138 | assert ( 139 | record_val == ?{ 140 | variant = #record({ site = "github" }); 141 | } 142 | ); 143 | assert ( 144 | user_val == ?{ 145 | variant = #user({ 146 | name = "John"; 147 | msg = "Hello World"; 148 | }); 149 | } 150 | ); 151 | assert (array_val == ?{ variant = #array([1, 2, 3]) }); 152 | }, 153 | ); 154 | }, 155 | ); 156 | suite( 157 | "motoko toText", 158 | func() { 159 | test( 160 | "single record", 161 | func() { 162 | 163 | let info : User = { 164 | msg = "Hello World"; 165 | name = "John"; 166 | }; 167 | 168 | let blob = to_candid (info); 169 | let text = UrlEncoded.toText(blob, ["name", "msg"], null); 170 | Debug.print("single record: " #debug_show (text)); 171 | assert (text == #ok("msg=Hello World&name=John")); 172 | }, 173 | ); 174 | test( 175 | "record with array", 176 | func() { 177 | let users = [ 178 | { 179 | name = "John"; 180 | msg = "Hello World"; 181 | }, 182 | { 183 | name = "Jane"; 184 | msg = "testing"; 185 | }, 186 | ]; 187 | 188 | let blob = to_candid ({ users }); 189 | 190 | let text = UrlEncoded.toText(blob, ["users", "name", "msg"], null); 191 | 192 | Debug.print("record with array: " #debug_show (text)); 193 | 194 | assert ( 195 | text == #ok("users[0][msg]=Hello World&users[0][name]=John&users[1][msg]=testing&users[1][name]=Jane") 196 | ); 197 | }, 198 | ); 199 | }, 200 | ); 201 | }, 202 | ); 203 | -------------------------------------------------------------------------------- /src/CBOR/lib.mo: -------------------------------------------------------------------------------- 1 | import Buffer "mo:base@0.16.0/Buffer"; 2 | import Blob "mo:base@0.16.0/Blob"; 3 | import Int8 "mo:base@0.16.0/Int8"; 4 | import Int16 "mo:base@0.16.0/Int16"; 5 | import Int32 "mo:base@0.16.0/Int32"; 6 | import Int64 "mo:base@0.16.0/Int64"; 7 | import Option "mo:base@0.16.0/Option"; 8 | import Nat64 "mo:base@0.16.0/Nat64"; 9 | import Result "mo:base@0.16.0/Result"; 10 | import Principal "mo:base@0.16.0/Principal"; 11 | 12 | import CBOR_Types "mo:cbor@4.0.0/Types"; 13 | import CBOR_Encoder "mo:cbor@4.0.0/Encoder"; 14 | import CBOR_Decoder "mo:cbor@4.0.0/Decoder"; 15 | import NatX "mo:xtended-numbers/NatX"; 16 | import FloatX "mo:xtended-numbers/FloatX"; 17 | 18 | import Candid "../Candid"; 19 | import CandidType "../Candid/Types"; 20 | 21 | import Utils "../Utils"; 22 | 23 | module { 24 | public type Candid = CandidType.Candid; 25 | type Result = Result.Result; 26 | type CBOR = CBOR_Types.Value; 27 | 28 | public type Options = CandidType.Options; 29 | 30 | /// Converts serialized Candid blob to CBOR blob 31 | public func encode(blob : Blob, keys : [Text], options : ?Options) : Result { 32 | let decoded_res = Candid.decode(blob, keys, options); 33 | let #ok(candid) = decoded_res else return Utils.send_error(decoded_res); 34 | 35 | let json_res = fromCandid(candid[0], Option.get(options, CandidType.defaultOptions)); 36 | let #ok(json) = json_res else return Utils.send_error(json_res); 37 | #ok(json); 38 | }; 39 | 40 | /// Convert a Candid value to CBOR blob 41 | public func fromCandid(candid : Candid, options : CandidType.Options) : Result { 42 | let res = transpile_candid_to_cbor(candid, options); 43 | let #ok(transpiled_cbor) = res else return Utils.send_error(res); 44 | 45 | let cbor_with_self_describe_tag = #majorType6({ 46 | tag = 55799 : Nat64; 47 | value = transpiled_cbor; 48 | }); 49 | 50 | switch (CBOR_Encoder.toBytes(cbor_with_self_describe_tag)) { 51 | case (#ok(encoded_cbor)) { #ok(Blob.fromArray(encoded_cbor)) }; 52 | case (#err(#invalidValue(errMsg))) { 53 | #err("Invalid value error while encoding CBOR: " # errMsg); 54 | }; 55 | }; 56 | }; 57 | 58 | func transpile_candid_to_cbor(candid : Candid, options : CandidType.Options) : Result { 59 | let transpiled_cbor : CBOR = switch (candid) { 60 | case (#Empty) #majorType7(#_undefined); 61 | case (#Null) #majorType7(#_null); 62 | case (#Bool(n)) #majorType7(#bool(n)); 63 | case (#Float(n)) #majorType7(#float(FloatX.fromFloat(n, #f64))); 64 | 65 | case (#Nat8(n)) #majorType7(#integer(n)); 66 | case (#Nat16(n)) #majorType0(NatX.from16To64(n)); 67 | case (#Nat32(n)) #majorType0(NatX.from32To64(n)); 68 | case (#Nat64(n)) #majorType0(n); 69 | case (#Nat(n)) #majorType0(Nat64.fromNat(n)); 70 | 71 | case (#Int8(n)) #majorType1(Int8.toInt(n)); 72 | case (#Int16(n)) #majorType1(Int16.toInt(n)); 73 | case (#Int32(n)) #majorType1(Int32.toInt(n)); 74 | case (#Int64(n)) #majorType1(Int64.toInt(n)); 75 | case (#Int(n)) #majorType1(n); 76 | 77 | case (#Blob(blob)) #majorType2(Blob.toArray(blob)); 78 | case (#Text(n)) #majorType3(n); 79 | case (#Array(arr)) { 80 | let buffer = Buffer.Buffer(arr.size()); 81 | 82 | for (item in arr.vals()) { 83 | let res = transpile_candid_to_cbor(item, options); 84 | let #ok(cbor_val) = res else return Utils.send_error(res); 85 | buffer.add(cbor_val); 86 | }; 87 | 88 | #majorType4(Buffer.toArray(buffer)); 89 | }; 90 | case (#Record(records) or #Map(records)) { 91 | let newRecords = Buffer.Buffer<(CBOR, CBOR)>(records.size()); 92 | 93 | for ((key, val) in records.vals()) { 94 | let res = transpile_candid_to_cbor(val, options); 95 | let #ok(cbor_val) = res else return Utils.send_error(res); 96 | newRecords.add((#majorType3(key), cbor_val)); 97 | }; 98 | 99 | #majorType5(Buffer.toArray(newRecords)); 100 | }; 101 | 102 | // Candid can make variables optional, when it is decoded using 103 | // `from_candid` if its specified in the type defination 104 | // This features allow us to handle optional values when decoding CBOR 105 | // 106 | // check out "CBOR Tests.options" in the tests folder to see how this in action 107 | case (#Option(option)) { 108 | let res = transpile_candid_to_cbor(option, options); 109 | let #ok(cbor_val) = res else return Utils.send_error(res); 110 | cbor_val; 111 | }; 112 | 113 | case (#Principal(p)) #majorType2(Blob.toArray(Principal.toBlob(p))); 114 | 115 | case (#Variant(_) or #Tuple(_)) { 116 | return #err(debug_show candid # " is not supported in this implementation of CBOR"); 117 | }; 118 | }; 119 | 120 | #ok(transpiled_cbor); 121 | }; 122 | 123 | public func decode(blob : Blob, options : ?Options) : Result { 124 | let candid_res = toCandid(blob, Option.get(options, CandidType.defaultOptions)); 125 | let #ok(candid) = candid_res else return Utils.send_error(candid_res); 126 | Candid.encodeOne(candid, options); 127 | }; 128 | 129 | public func toCandid(blob : Blob, options : CandidType.Options) : Result { 130 | let cbor_res = CBOR_Decoder.fromBytes(blob.vals()); 131 | 132 | let candid_res = switch (cbor_res) { 133 | case (#ok(cbor)) { 134 | let #majorType6({ tag = 55799; value }) = cbor else return transpile_cbor_to_candid(cbor, options); 135 | transpile_cbor_to_candid(value, options); 136 | }; 137 | case (#err(cbor_error)) { 138 | switch (cbor_error) { 139 | case (#unexpectedBreak) { 140 | return #err("Error decoding CBOR: Unexpected break"); 141 | }; 142 | case (#unexpectedEndOfBytes) { 143 | return #err("Error decoding CBOR: Unexpected end of bytes"); 144 | }; 145 | case (#invalid(errMsg)) { 146 | return #err("Invalid CBOR: " # errMsg); 147 | }; 148 | }; 149 | }; 150 | }; 151 | 152 | let #ok(candid) = candid_res else return Utils.send_error(candid_res); 153 | #ok(candid); 154 | }; 155 | 156 | public func transpile_cbor_to_candid(cbor : CBOR, options : CandidType.Options) : Result { 157 | let transpiled_candid = switch (cbor) { 158 | case (#majorType0(n)) #Nat(Nat64.toNat(n)); 159 | case (#majorType1(n)) #Int(n); 160 | case (#majorType2(n)) #Blob(Blob.fromArray(n)); 161 | case (#majorType3(n)) #Text(n); 162 | case (#majorType4(arr)) { 163 | let buffer = Buffer.Buffer(arr.size()); 164 | for (item in arr.vals()) { 165 | let res = transpile_cbor_to_candid(item, options); 166 | let #ok(candid_val) = res else return Utils.send_error(res); 167 | buffer.add(candid_val); 168 | }; 169 | #Array(Buffer.toArray(buffer)); 170 | }; 171 | case (#majorType5(records)) { 172 | let buffer = Buffer.Buffer<(Text, Candid)>(records.size()); 173 | for ((cbor_text, val) in records.vals()) { 174 | let #majorType3(key) = cbor_text else return #err("Error decoding CBOR: Unexpected key type"); 175 | 176 | let res = transpile_cbor_to_candid(val, options); 177 | let #ok(candid_val) = res else return Utils.send_error(res); 178 | buffer.add((key, candid_val)); 179 | }; 180 | 181 | if (options.use_icrc_3_value_type) { 182 | #Map(Buffer.toArray(buffer)); 183 | } else { 184 | #Record(Buffer.toArray(buffer)); 185 | }; 186 | }; 187 | case (#majorType7(#_undefined)) #Empty; 188 | case (#majorType7(#_null)) #Null; 189 | case (#majorType7(#bool(n))) #Bool(n); 190 | case (#majorType7(#integer(n))) #Nat8(n); 191 | case (#majorType7(#float(n))) #Float(FloatX.toFloat(n)); 192 | 193 | case (#majorType7(#_break)) { 194 | return #err("Error decoding CBOR: #_break is not supported"); 195 | }; 196 | case (#majorType6(_)) { 197 | return #err("Error decoding CBOR: Tagged values are not supported"); 198 | }; 199 | }; 200 | 201 | #ok(transpiled_candid); 202 | }; 203 | }; 204 | -------------------------------------------------------------------------------- /src/Utils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Char "mo:base@0.16.0/Char"; 3 | import Order "mo:base@0.16.0/Order"; 4 | import Float "mo:base@0.16.0/Float"; 5 | import Text "mo:base@0.16.0/Text"; 6 | import Iter "mo:base@0.16.0/Iter"; 7 | import Nat64 "mo:base@0.16.0/Nat64"; 8 | import Nat32 "mo:base@0.16.0/Nat32"; 9 | import Nat8 "mo:base@0.16.0/Nat8"; 10 | import Int "mo:base@0.16.0/Int"; 11 | import Buffer "mo:base@0.16.0/Buffer"; 12 | import Result "mo:base@0.16.0/Result"; 13 | import Int64 "mo:base@0.16.0/Int64"; 14 | import Blob "mo:base@0.16.0/Blob"; 15 | 16 | import Prelude "mo:base@0.16.0/Prelude"; 17 | import Debug "mo:base@0.16.0/Debug"; 18 | import Itertools "mo:itertools@0.2.2/Iter"; 19 | import Map "mo:map@9.0.1/Map"; 20 | import MapConst "mo:map@9.0.1/Map/const"; 21 | 22 | import ByteUtils "mo:byte-utils@0.1.2"; 23 | 24 | module { 25 | 26 | type Iter = Iter.Iter; 27 | type Buffer = Buffer.Buffer; 28 | type Result = Result.Result; 29 | 30 | public func create_map(map_size : Nat) : Map.Map = [ 31 | var ?( 32 | Array.init(map_size, null), 33 | Array.init(map_size, null), 34 | Array.init(map_size * 2, MapConst.NULL), 35 | Array.init(3, 0), 36 | ) 37 | ]; 38 | 39 | /// Function copied from mo:candid/Tag: https://github.com/edjCase/motoko_candid/blob/d038b7bd953fb8826ae66a5f34bf06dcc29b2e0f/src/Tag.mo#L14-L30 40 | /// 41 | /// Computes the hash of a given record field key. 42 | /// 43 | public func hash_record_key(name : Text) : Nat32 { 44 | // hash(name) = ( Sum_(i=0..k) utf8(name)[i] * 223^(k-i) ) mod 2^32 where k = |utf8(name)|-1 45 | let bytes : [Nat8] = Blob.toArray(Text.encodeUtf8(name)); 46 | Array.foldLeft( 47 | bytes, 48 | 0, 49 | func(accum : Nat32, byte : Nat8) : Nat32 { 50 | (accum *% 223) +% Nat32.fromNat(Nat8.toNat(byte)); 51 | }, 52 | ); 53 | }; 54 | 55 | public func reverse_order(fn : (A, A) -> Order.Order) : (A, A) -> Order.Order { 56 | func(a : A, b : A) : Order.Order { 57 | switch (fn(a, b)) { 58 | case (#less) #greater; 59 | case (#equal) #equal; 60 | case (#greater) #less; 61 | }; 62 | }; 63 | }; 64 | 65 | public type ArrayLike = { 66 | size : () -> Nat; 67 | get : (Nat) -> A; 68 | }; 69 | 70 | public func array_slice(arr : ArrayLike, start : Nat, end : Nat) : [A] { 71 | Array.tabulate( 72 | end - start, 73 | func(i : Nat) = arr.get(start + i), 74 | ); 75 | }; 76 | 77 | public func blob_slice(blob : Blob, start : Nat, end : Nat) : [Nat8] { 78 | Array.tabulate( 79 | end - start, 80 | func(i : Nat) = blob.get(start + i), 81 | ); 82 | }; 83 | 84 | public func concatKeys(keys : [[Text]]) : [Text] { 85 | Iter.toArray( 86 | Itertools.flattenArray(keys) 87 | ); 88 | }; 89 | 90 | public func sized_iter_to_array(iter : Iter, size : Nat) : [A] { 91 | Array.tabulate( 92 | size, 93 | func(i : Nat) { 94 | switch (iter.next()) { 95 | case (?x) x; 96 | case (_) Prelude.unreachable(); 97 | }; 98 | }, 99 | ); 100 | }; 101 | 102 | public func send_error(res : Result) : Result { 103 | switch (res) { 104 | case (#ok(_)) Prelude.unreachable(); 105 | case (#err(errorMsg)) #err(errorMsg); 106 | }; 107 | }; 108 | 109 | public func subText(text : Text, start : Nat, end : Nat) : Text { 110 | Itertools.toText( 111 | Itertools.skip( 112 | Itertools.take(text.chars(), end), 113 | start, 114 | ) 115 | ); 116 | }; 117 | 118 | public func cmpRecords(a : (Text, Any), b : (Text, Any)) : Order.Order { 119 | Text.compare(a.0, b.0); 120 | }; 121 | 122 | public func stripStart(text : Text, prefix : Text.Pattern) : Text { 123 | switch (Text.stripStart(text, prefix)) { 124 | case (?t) t; 125 | case (_) text; 126 | }; 127 | }; 128 | 129 | public func log2(n : Float) : Float { 130 | Float.log(n) / Float.log(2); 131 | }; 132 | 133 | public func isHash(key : Text) : Bool { 134 | Itertools.all( 135 | key.chars(), 136 | func(c : Char) : Bool { 137 | c == '_' or Char.isDigit(c); 138 | }, 139 | ); 140 | }; 141 | 142 | public func text_to_nat32(text : Text) : Nat32 { 143 | Itertools.fold( 144 | text.chars(), 145 | 0 : Nat32, 146 | func(acc : Nat32, c : Char) : Nat32 { 147 | if (c == '_') { 148 | acc; 149 | } else { 150 | acc * 10 + Char.toNat32(c) - Char.toNat32('0'); 151 | }; 152 | }, 153 | ); 154 | }; 155 | 156 | public func text_to_nat(text : Text) : Nat { 157 | Itertools.fold( 158 | text.chars(), 159 | 0 : Nat, 160 | func(acc : Nat, c : Char) : Nat { 161 | if (c == '_') { 162 | acc; 163 | } else { 164 | acc * 10 + Nat32.toNat(Char.toNat32(c) - Char.toNat32('0')); 165 | }; 166 | }, 167 | ); 168 | }; 169 | 170 | public func text_is_number(text : Text) : Bool { 171 | Itertools.all( 172 | text.chars(), 173 | func(c : Char) : Bool { 174 | Char.isDigit(c) or c == '_'; 175 | }, 176 | ); 177 | }; 178 | 179 | type AddToBuffer = { 180 | add : (A) -> (); 181 | }; 182 | 183 | // https://en.wikipedia.org/wiki/LEB128 184 | // limited to 64-bit unsigned integers 185 | // more performant than the general unsigned_leb128 186 | public func unsigned_leb128_64(buffer : ByteUtils.BufferLike, n : Nat) { 187 | var value = Nat64.fromNat(n); 188 | while (value >= 0x80) { 189 | buffer.add(Nat8.fromNat(Nat64.toNat(value & 0x7F)) | 0x80); 190 | value >>= 7; 191 | }; 192 | buffer.add(Nat8.fromNat(Nat64.toNat(value))); 193 | }; 194 | 195 | public func unsigned_leb128(buffer : ByteUtils.BufferLike, n : Nat) { 196 | var value = Nat64.fromNat(n); 197 | while (value >= 0x80) { 198 | buffer.add(Nat8.fromNat(Nat64.toNat(value & 0x7F)) | 0x80); 199 | value >>= 7; 200 | }; 201 | buffer.add(Nat8.fromNat(Nat64.toNat(value))); 202 | }; 203 | 204 | public func signed_leb128_64(buffer : ByteUtils.BufferLike, num : Int) { 205 | ByteUtils.Buffer.addSLEB128_64(buffer, Int64.fromInt(num)); 206 | }; 207 | 208 | // public func signed_leb128(buffer : AddToBuffer, num : Int) { 209 | // let nat64_bound = 18_446_744_073_709_551_616; 210 | 211 | // if (num < nat64_bound and num > -nat64_bound) return signed_leb128_64(buffer, num); 212 | 213 | // var n = num; 214 | // let is_negative = n < 0; 215 | 216 | // }; 217 | 218 | public class ReusableBuffer(init_capacity : Nat) { 219 | var elems : [var ?A] = Array.init(init_capacity, null); 220 | var count : Nat = 0; 221 | 222 | public func size() : Nat = count; 223 | 224 | public func add(elem : A) { 225 | if (count == elems.size()) { 226 | elems := Array.tabulateVar( 227 | elems.size() * 2, 228 | func(i : Nat) : ?A { 229 | if (i < count) { 230 | elems[i]; 231 | } else { 232 | null; 233 | }; 234 | }, 235 | ); 236 | }; 237 | 238 | elems[count] := ?elem; 239 | count += 1; 240 | }; 241 | 242 | public func clear() { 243 | count := 0; 244 | }; 245 | 246 | public func get(i : Nat) : A { 247 | switch (elems[i]) { 248 | case (?elem) elem; 249 | case (null) Debug.trap "Index out of bounds"; 250 | }; 251 | }; 252 | 253 | public func put(i : Nat, elem : A) { 254 | if (i >= count) Debug.trap "Index out of bounds"; 255 | elems[i] := ?elem; 256 | }; 257 | 258 | public func vals() : Iter.Iter { 259 | var i = 0; 260 | 261 | object { 262 | public func next() : ?A { 263 | if (i < count) { 264 | let res = elems[i]; 265 | i += 1; 266 | res; 267 | } else { 268 | null; 269 | }; 270 | }; 271 | }; 272 | }; 273 | }; 274 | 275 | }; 276 | -------------------------------------------------------------------------------- /submodules/json.mo/src/JSON.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.7.3/Char"; 2 | import Iter "mo:base@0.7.3/Iter"; 3 | import Int "mo:base@0.7.3/Int"; 4 | import Float "mo:base@0.7.3/Float"; 5 | import List "mo:base@0.7.3/List"; 6 | import Nat8 "mo:base@0.7.3/Nat8"; 7 | import Nat32 "mo:base@0.7.3/Nat32"; 8 | import Result "mo:base@0.7.3/Result"; 9 | import Text "mo:base@0.7.3/Text"; 10 | 11 | import C "../../parser-combinators.mo/src/Combinators"; 12 | import L "../../parser-combinators.mo/src/List"; 13 | import P "../../parser-combinators.mo/src/Parser"; 14 | 15 | module JSON { 16 | public type JSON = { 17 | #Number : Int; 18 | #Float : Float; 19 | #String : Text; 20 | #Array : [JSON]; 21 | #Object : [(Text, JSON)]; 22 | #Boolean : Bool; 23 | #Null; 24 | }; 25 | 26 | public func show(json : JSON) : Text = switch (json) { 27 | case (#Number(v)) { Int.toText(v) }; 28 | case (#Float(v)) { Float.format(#fix(2), v) }; 29 | case (#String(v)) { "\"" # v # "\"" }; 30 | case (#Array(v)) { 31 | var s = "["; 32 | for (i in v.vals()) { 33 | if (s != "[") { s #= ", " }; 34 | s #= show(i); 35 | }; 36 | s # "]"; 37 | }; 38 | case (#Object(v)) { 39 | var s = "{"; 40 | for ((k, v) in v.vals()) { 41 | if (s != "{") { s #= ", " }; 42 | s #= "\"" # k # "\"" # ": " # show(v); 43 | }; 44 | s # "}"; 45 | }; 46 | case (#Boolean(v)) { 47 | if (v) { return "true" }; 48 | "false"; 49 | }; 50 | case (#Null) { "null" }; 51 | }; 52 | 53 | private func character() : P.Parser = C.oneOf([ 54 | C.sat( 55 | func(c : Char) : Bool { 56 | c != Char.fromNat32(0x22) and c != '\\'; 57 | } 58 | ), 59 | C.right( 60 | C.Character.char('\\'), 61 | C.map( 62 | C.Character.oneOf([ 63 | Char.fromNat32(0x22), 64 | '\\', 65 | '/', 66 | 'b', 67 | 'f', 68 | 'n', 69 | 'r', 70 | 't', 71 | // TODO: u hex{4} 72 | ]), 73 | func(c : Char) : Char { 74 | switch (c) { 75 | case ('b') { Char.fromNat32(0x08) }; 76 | case ('f') { Char.fromNat32(0x0C) }; 77 | case ('n') { Char.fromNat32(0x0A) }; 78 | case ('r') { Char.fromNat32(0x0D) }; 79 | case ('t') { Char.fromNat32(0x09) }; 80 | case (_) { c }; 81 | }; 82 | }, 83 | ), 84 | ), 85 | ]); 86 | 87 | private func ignoreSpace(parserA : P.Parser) : P.Parser = C.right( 88 | C.many(C.Character.space()), 89 | parserA, 90 | ); 91 | 92 | public func parse(t : Text) : ?JSON = parseValues(L.fromText(t)); 93 | 94 | public func parseRaw(data : [Char]) : ?JSON = parseValues(L.fromIter(data.vals())); 95 | 96 | public func parseRawASCII(data : [Nat8]) : ?JSON = parseValues(nat8sToCharList(data.vals())); 97 | 98 | private func parseValues(l : List.List) : ?JSON = switch (valueParser()(l)) { 99 | case (null) { null }; 100 | case (?(x, xs)) { 101 | switch (xs) { 102 | case (null) { ?x }; 103 | case (_) { null }; 104 | }; 105 | }; 106 | }; 107 | 108 | private func nat8sToCharList(i : Iter.Iter) : List.List = switch (i.next()) { 109 | case (null) { null }; 110 | case (?v) { 111 | ?(Char.fromNat32(Nat32.fromNat(Nat8.toNat(v))), nat8sToCharList(i)); 112 | }; 113 | }; 114 | 115 | private func valueParser() : P.Parser = C.bracket( 116 | C.many(C.Character.space()), 117 | C.oneOf([ 118 | objectParser(), 119 | arrayParser(), 120 | stringParser(), 121 | floatParser(), 122 | numberParser(), 123 | boolParser(), 124 | nullParser(), 125 | ]), 126 | C.many(C.Character.space()), 127 | ); 128 | 129 | private func objectParser() : P.Parser = C.map( 130 | C.bracket( 131 | C.Character.char('{'), 132 | ignoreSpace( 133 | C.sepBy( 134 | C.seq( 135 | C.left( 136 | ignoreSpace(string()), 137 | ignoreSpace(C.Character.char(':')), 138 | ), 139 | P.delay(valueParser), 140 | ), 141 | C.Character.char(','), 142 | ) 143 | ), 144 | C.Character.char('}'), 145 | ), 146 | func(t : List.List<(Text, JSON)>) : JSON { 147 | #Object(List.toArray(t)); 148 | }, 149 | ); 150 | 151 | private func arrayParser() : P.Parser = C.map( 152 | C.bracket( 153 | C.Character.char('['), 154 | ignoreSpace( 155 | C.sepBy( 156 | P.delay(valueParser), 157 | C.Character.char(','), 158 | ) 159 | ), 160 | C.Character.char(']'), 161 | ), 162 | func(t : List.List) : JSON { 163 | #Array(List.toArray(t)); 164 | }, 165 | ); 166 | 167 | private func string() : P.Parser = C.map( 168 | C.bracket( 169 | C.Character.char(Char.fromNat32(0x22)), 170 | C.many(character()), 171 | C.Character.char(Char.fromNat32(0x22)), 172 | ), 173 | func(t : List.List) : Text { 174 | Text.fromIter(L.toIter(t)); 175 | }, 176 | ); 177 | 178 | private func stringParser() : P.Parser = C.map( 179 | C.map( 180 | C.bracket( 181 | C.Character.char(Char.fromNat32(0x22)), 182 | C.many(character()), 183 | C.Character.char(Char.fromNat32(0x22)), 184 | ), 185 | func(t : List.List) : Text { 186 | Text.fromIter(L.toIter(t)); 187 | }, 188 | ), 189 | func(t : Text) : JSON { 190 | #String(t); 191 | }, 192 | ); 193 | 194 | private func floatParser() : P.Parser = C.map( 195 | C.oneOf([ 196 | parseFloatWithExp(), 197 | parseFloat(), 198 | ]), 199 | func(n : Float) : JSON = #Float(n), 200 | ); 201 | 202 | private func parseFloat() : P.Parser { 203 | C.map( 204 | C.seq>( 205 | C.Int.int(), 206 | C.right( 207 | C.Character.char('.'), 208 | C.many1(C.Character.digit()), 209 | ), 210 | ), 211 | func((n, decimal_list) : (Int, List.List)) : Float { 212 | let isNegative = n < 0; 213 | var num = n; 214 | var n_of_decimals : Float = 0; 215 | 216 | for (char in Iter.fromList(decimal_list)) { 217 | let digit = Nat32.toNat( 218 | Char.toNat32(char) - Char.toNat32('0') 219 | ); 220 | 221 | if (isNegative) { 222 | num := num * 10 - digit; 223 | } else { 224 | num := num * 10 + digit; 225 | }; 226 | 227 | n_of_decimals += 1; 228 | }; 229 | 230 | let float = Float.fromInt(num) / (10 ** n_of_decimals); 231 | }, 232 | ); 233 | }; 234 | 235 | private func parseFloatWithExp() : P.Parser = C.map( 236 | C.seq( 237 | C.oneOf([ 238 | parseFloat(), 239 | C.map( 240 | C.Int.int(), 241 | func(i : Int) : Float = Float.fromInt(i), 242 | ), 243 | ]), 244 | C.right( 245 | C.oneOf([C.String.string("e"), C.String.string("E")]), 246 | C.Int.int(), 247 | ), 248 | ), 249 | func((n, exponent) : (Float, Int)) : Float { 250 | let exp = Float.fromInt(exponent); 251 | let isNegative = exp < 0; 252 | 253 | if (isNegative) { 254 | return n / (10 ** -exp); 255 | }; 256 | 257 | n * (10 ** exp); 258 | }, 259 | ); 260 | 261 | private func numberParser() : P.Parser = C.map( 262 | C.Int.int(), 263 | func(i : Int) : JSON { 264 | #Number(i); 265 | }, 266 | ); 267 | 268 | private func boolParser() : P.Parser = C.map( 269 | C.choose( 270 | C.String.string("true"), 271 | C.String.string("false"), 272 | ), 273 | func(t : Text) : JSON { 274 | if (t == "true") return #Boolean(true); 275 | #Boolean(false); 276 | }, 277 | ); 278 | 279 | private func nullParser() : P.Parser = C.map( 280 | C.String.string("null"), 281 | func(_ : Text) : JSON { 282 | #Null; 283 | }, 284 | ); 285 | }; 286 | -------------------------------------------------------------------------------- /src/UrlEncoded/FromText.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base@0.16.0/Array"; 2 | import Blob "mo:base@0.16.0/Blob"; 3 | import Buffer "mo:base@0.16.0/Buffer"; 4 | import Char "mo:base@0.16.0/Char"; 5 | import Debug "mo:base@0.16.0/Debug"; 6 | import Result "mo:base@0.16.0/Result"; 7 | import TrieMap "mo:base@0.16.0/TrieMap"; 8 | import Nat "mo:base@0.16.0/Nat"; 9 | import Text "mo:base@0.16.0/Text"; 10 | import Iter "mo:base@0.16.0/Iter"; 11 | import Option "mo:base@0.16.0/Option"; 12 | 13 | import Itertools "mo:itertools@0.2.2/Iter"; 14 | 15 | import Candid "../Candid"; 16 | import T "../Candid/Types"; 17 | import { parseValue } "./Parser"; 18 | import U "../Utils"; 19 | import Utils "../Utils"; 20 | 21 | module { 22 | let { subText } = U; 23 | 24 | type Candid = Candid.Candid; 25 | 26 | type Buffer = Buffer.Buffer; 27 | type Iter = Iter.Iter; 28 | type TrieMap = TrieMap.TrieMap; 29 | type Result = Result.Result; 30 | 31 | type TextOrTrieMap = { 32 | #text : Text; 33 | #triemap : TrieMap; 34 | }; 35 | 36 | type NestedTrieMap = TrieMap; 37 | 38 | func newMap() : NestedTrieMap = TrieMap.TrieMap(Text.equal, Text.hash); 39 | 40 | /// Converts a Url-Encoded Text to a serialized Candid Record 41 | public func fromText(text : Text, options : ?T.Options) : Result { 42 | let res = toCandid(text, Option.get(options, T.defaultOptions)); 43 | let #ok(candid) = res else return Utils.send_error(res); 44 | 45 | Candid.encodeOne(candid, options); 46 | }; 47 | 48 | /// Converts a Url-Encoded Text to a Candid Record 49 | public func toCandid(text : Text, options : T.Options) : Result { 50 | let triemap_res = entriesToTrieMap(text, options); 51 | 52 | let #ok(triemap) = triemap_res else return Utils.send_error(triemap_res); 53 | 54 | trieMapToCandid(triemap, options); 55 | }; 56 | 57 | // Converting entries from UrlSearchParams 58 | // -------------------------------------------------- 59 | // 'users[0][name]=peter' 60 | // 'users[0][age]=20' 61 | // 'users[1][name]=john' 62 | // 'users[1][age]=30' 63 | // 64 | // 'settings[theme]=dark' 65 | // 'settings[language]=en' 66 | // 67 | // -------------------------------------------------- 68 | // Into a nested TrieMap 69 | // -------------------------------------------------- 70 | // TrieMap { 71 | // 'users' => TrieMap { 72 | // '0' => TrieMap { 73 | // 'name' => 'peter', 74 | // 'age' => '20', 75 | // }, 76 | // '1' => TrieMap { 77 | // 'name' => 'john', 78 | // 'age' => '30', 79 | // }, 80 | // }, 81 | // 'settings' => TrieMap { 82 | // 'theme' => 'dark', 83 | // 'language' => 'en', 84 | // }, 85 | // } 86 | // -------------------------------------------------- 87 | func entriesToTrieMap(text : Text, options : T.Options) : Result { 88 | let entries : [Text] = Array.sort( 89 | Iter.toArray(Text.split(text, #char '&')), 90 | Text.compare, 91 | ); 92 | 93 | let triemap : NestedTrieMap = newMap(); 94 | 95 | for (entry in entries.vals()) { 96 | let entry_iter = Text.split(entry, #char '='); 97 | let key = switch (entry_iter.next()) { 98 | case (?_key) _key; 99 | case (_) return #err("Missing key: improper formatting of key-value pair in '" # entry # "'"); 100 | }; 101 | 102 | let value = switch (entry_iter.next()) { 103 | case (?val) val; 104 | case (_) return #err("Missing value: improper formatting of key value pair in '" # entry # "'"); 105 | }; 106 | 107 | switch ( 108 | Itertools.findIndex( 109 | key.chars(), 110 | func(c : Char) : Bool = c == '[', 111 | ) 112 | ) { 113 | case (?index) { 114 | let first_field = subText(key, 0, index); 115 | 116 | let stripped_key = switch (Text.stripEnd(key, #text "]")) { 117 | case (?stripped_key) stripped_key; 118 | case (_) return #err("Improper formatting of key value pair in '" # entry # "' -> Missing closing bracket ']'"); 119 | }; 120 | 121 | if (first_field.size() == 0) { 122 | return return #err("Missing field name between brackets '[]' in '" # entry # "'"); 123 | }; 124 | 125 | let other_fields = Text.split( 126 | subText(stripped_key, index + 1, stripped_key.size()), 127 | #text "][", 128 | ); 129 | 130 | let res = insert(triemap, first_field, other_fields, value); 131 | let #ok(_) = res else return Utils.send_error(res); 132 | }; 133 | case (_) { 134 | let res = insert(triemap, key, Itertools.empty(), value); 135 | let #ok(_) = res else return Utils.send_error(res); 136 | }; 137 | }; 138 | }; 139 | 140 | #ok(triemap); 141 | }; 142 | 143 | // Convert from a nested TrieMap 144 | // -------------------------------------------------- 145 | // TrieMap { 146 | // 'users' => TrieMap { 147 | // '0' => TrieMap { 148 | // 'name' => 'peter', 149 | // 'age' => '20', 150 | // }, 151 | // '1' => TrieMap { 152 | // 'name' => 'john', 153 | // 'age' => '30', 154 | // }, 155 | // }, 156 | // 'settings' => TrieMap { 157 | // 'theme' => 'dark', 158 | // 'language' => 'en', 159 | // }, 160 | // } 161 | // -------------------------------------------------- 162 | // Into a Candid Record 163 | // -------------------------------------------------- 164 | // { 165 | // users : [ 166 | // { 167 | // name : "peter", 168 | // age : 20, 169 | // }, 170 | // { 171 | // name : "john", 172 | // age : 30, 173 | // }, 174 | // ], 175 | // settings : { 176 | // theme : "dark", 177 | // language : "en", 178 | // }, 179 | // } 180 | // -------------------------------------------------- 181 | 182 | func trieMapToCandid(triemap : NestedTrieMap, options : T.Options) : Result { 183 | var i = 0; 184 | let isArray = Itertools.all( 185 | Iter.sort(triemap.keys(), Text.compare), 186 | func(key : Text) : Bool { 187 | let res = key == Nat.toText(i); 188 | i += 1; 189 | res; 190 | }, 191 | ); 192 | 193 | if (isArray) { 194 | let buffer = Buffer.Buffer(triemap.size()); 195 | 196 | for (i in Itertools.range(0, triemap.size())) { 197 | 198 | switch (triemap.get(Nat.toText(i))) { 199 | case (?(#text(text))) { 200 | let candid = parseValue(text); 201 | buffer.add(candid); 202 | }; 203 | case (?(#triemap(map))) { 204 | let res = trieMapToCandid(map, options); 205 | let #ok(candid) = res else return Utils.send_error(res); 206 | buffer.add(candid); 207 | }; 208 | 209 | case (_) Debug.trap("Array might be improperly formatted"); 210 | }; 211 | }; 212 | 213 | let arr = Buffer.toArray(buffer); 214 | 215 | return #ok(#Array(arr)); 216 | }; 217 | 218 | // check if single value is a variant 219 | if (triemap.size() == 1) { 220 | let (variant_key, value) = switch (triemap.entries().next()) { 221 | case (?(k, v)) { (k, v) }; 222 | case (_) { Debug.trap("Variant might be improperly formatted") }; 223 | }; 224 | 225 | let isVariant = Text.startsWith(variant_key, #text "#"); 226 | 227 | if (isVariant) { 228 | let key = U.stripStart(variant_key, #text "#"); 229 | 230 | let value_res = switch (value) { 231 | case (#text(text)) #ok(parseValue(text)); 232 | case (#triemap(map)) trieMapToCandid(map, options); 233 | }; 234 | 235 | let #ok(val) = value_res else return Utils.send_error(value_res); 236 | 237 | return #ok(#Variant((key, val))); 238 | }; 239 | }; 240 | 241 | let buffer = Buffer.Buffer<(Text, Candid)>(triemap.size()); 242 | 243 | for ((key, field) in triemap.entries()) { 244 | switch (field) { 245 | case (#text(text)) { 246 | let candid = parseValue(text); 247 | buffer.add((key, candid)); 248 | }; 249 | case (#triemap(map)) { 250 | let res = trieMapToCandid(map, options); 251 | let #ok(candid) = res else return Utils.send_error(res); 252 | buffer.add((key, candid)); 253 | }; 254 | }; 255 | }; 256 | 257 | let records = Buffer.toArray(buffer); 258 | 259 | // let map_or_record = if () 260 | #ok(#Record(records)); 261 | }; 262 | 263 | // Inserts a key value pair from UrlSearchParams into a nested TrieMap 264 | func insert(map : NestedTrieMap, field : Text, fields_iter : Iter, value : Text) : Result<(), Text> { 265 | let next_field = switch (fields_iter.next()) { 266 | case (?_field) _field; 267 | case (_) { 268 | map.put(field, #text(value)); 269 | return #ok(); 270 | }; 271 | }; 272 | 273 | let nestedTriemap = switch (map.get(field)) { 274 | case (?val) { 275 | switch (val) { 276 | case (#text(prevValue)) { 277 | return #err("field name '" # field # "' cannot have multiple values: '" # prevValue # "' and '" # value # "'"); 278 | }; 279 | case (#triemap(nestedTriemap)) nestedTriemap; 280 | }; 281 | }; 282 | case (_) { 283 | let nestedTriemap = newMap(); 284 | map.put(field, #triemap(nestedTriemap)); 285 | nestedTriemap; 286 | }; 287 | }; 288 | 289 | insert(nestedTriemap, next_field, fields_iter, value); 290 | }; 291 | }; 292 | -------------------------------------------------------------------------------- /submodules/parser-combinators.mo/src/Combinators.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base@0.7.3/Char"; 2 | import Iter "mo:base@0.7.3/Iter"; 3 | import List "mo:base@0.7.3/List"; 4 | import Nat32 "mo:base@0.7.3/Nat32"; 5 | import Text "mo:base@0.7.3/Text"; 6 | 7 | import P "Parser"; 8 | import L "List"; 9 | 10 | module { 11 | private type List = List.List; 12 | private type Parser = P.Parser; 13 | 14 | // Integrates the sequencing of parsers with the processing of their result values. 15 | public func bind( 16 | parserA : Parser, 17 | function : A -> Parser, 18 | ) : Parser { 19 | func(xs : List) { 20 | switch (parserA(xs)) { 21 | case (null) { null }; 22 | case (?(a, xs)) { 23 | function(a)(xs); 24 | }; 25 | }; 26 | }; 27 | }; 28 | 29 | // Applies one parser after another, with the results from the two parsers being combined as pairs. 30 | public func seq( 31 | parserA : Parser, 32 | parserB : Parser, 33 | ) : Parser { 34 | bind( 35 | parserA, 36 | func(a : A) : Parser { 37 | bind( 38 | parserB, 39 | func(b : B) : Parser { 40 | P.result((a, b)); 41 | }, 42 | ); 43 | }, 44 | ); 45 | }; 46 | 47 | // Yields a parser that consumes a single item if it satisfies the predicate, and fails otherwise. 48 | public func sat( 49 | ok : T -> Bool 50 | ) : Parser { 51 | bind( 52 | P.item(), 53 | func(t : T) : Parser { 54 | if (ok(t)) { 55 | return P.result(t); 56 | }; 57 | P.zero(); 58 | }, 59 | ); 60 | }; 61 | 62 | // Decides between parsing a single item and recursing, or parsing nothing further and terminating. 63 | public func choose( 64 | parser1 : Parser, 65 | parser2 : Parser, 66 | ) : Parser { 67 | func(xs : List) { 68 | switch (parser1(xs)) { 69 | case (?x) { ?x }; 70 | case (null) { 71 | parser2(xs); 72 | }; 73 | }; 74 | }; 75 | }; 76 | 77 | // => 78 | public func map( 79 | parserA : Parser, 80 | function : A -> B, 81 | ) : Parser { 82 | bind( 83 | parserA, 84 | func(a : A) : Parser { 85 | P.result(function(a)); 86 | }, 87 | ); 88 | }; 89 | 90 | // >> 91 | public func right( 92 | parserA : Parser, 93 | parserB : Parser, 94 | ) : Parser { 95 | bind( 96 | parserA, 97 | func(_ : A) : Parser { 98 | parserB; 99 | }, 100 | ); 101 | }; 102 | 103 | // << 104 | public func left( 105 | parserA : Parser, 106 | parserB : Parser, 107 | ) : Parser { 108 | bind( 109 | parserA, 110 | func(a : A) : Parser { 111 | bind( 112 | parserB, 113 | func(_ : B) : Parser { 114 | P.result(a); 115 | }, 116 | ); 117 | }, 118 | ); 119 | }; 120 | 121 | // <~> 122 | public func cons( 123 | parserA : Parser, 124 | parserAs : Parser>, 125 | ) : Parser> { 126 | bind( 127 | parserA, 128 | func(a : A) : Parser> { 129 | bind( 130 | parserAs, 131 | func(as : List) : Parser> { 132 | P.result>(List.push(a, as)); 133 | }, 134 | ); 135 | }, 136 | ); 137 | }; 138 | 139 | // Applies a parser p zero or more times to the input. 140 | public func many( 141 | parserA : Parser 142 | ) : Parser> { 143 | choose( 144 | // Same as <~> parserA (many parserA), but not 145 | // possible because of recursive call. 146 | bind( 147 | parserA, 148 | func(a : A) : Parser> { 149 | bind( 150 | many(parserA), 151 | func(as : List) : Parser> { 152 | P.result>(List.push(a, as)); 153 | }, 154 | ); 155 | }, 156 | ), 157 | P.result>(List.nil()), 158 | ); 159 | }; 160 | 161 | // Non-empty sequences of items. 162 | public func many1( 163 | parserA : Parser 164 | ) : Parser> { 165 | cons( 166 | parserA, 167 | many(parserA), 168 | ); 169 | }; 170 | 171 | // Recognises non-empty sequences of a given parser p, but different in that the instances of p are separated by a 172 | // parser sep whose result values are ignored. 173 | public func sepBy1( 174 | parserA : Parser, 175 | parserB : Parser, // sep 176 | ) : Parser> { 177 | cons( 178 | parserA, 179 | many(right(parserB, parserA)), 180 | ); 181 | }; 182 | 183 | // Bracketing of parsers by other parsers whose results are ignored. 184 | public func bracket( 185 | parserA : Parser, // left bracket 186 | parserB : Parser, 187 | parserC : Parser, // right bracket 188 | ) : Parser { 189 | right( 190 | parserA, 191 | left(parserB, parserC), 192 | ); 193 | }; 194 | 195 | // Parses sequences of a given parser p, separated by a parser sep whose result values are ignored. 196 | public func sepBy( 197 | parserA : Parser, 198 | parserB : Parser, // sep 199 | ) : Parser> { 200 | choose( 201 | sepBy1(parserA, parserB), 202 | P.result>(List.nil()), 203 | ); 204 | }; 205 | 206 | public func oneOf( 207 | parsers : [Parser] 208 | ) : Parser { 209 | func(xs : List) { 210 | for (parser in parsers.vals()) { 211 | switch (parser(xs)) { 212 | case (?v) { return ?v }; 213 | case (_) {}; 214 | }; 215 | }; 216 | null; 217 | }; 218 | }; 219 | 220 | public func count( 221 | parserA : Parser, 222 | n : Nat, 223 | ) : Parser> { 224 | if (0 < n) { cons(parserA, count(parserA, n - 1 : Nat)) } else { 225 | P.result(List.nil()); 226 | }; 227 | }; 228 | 229 | public module Character { 230 | private type CharParser = Parser; 231 | 232 | public func char(x : Char) : CharParser { 233 | sat(func(y : Char) : Bool { x == y }); 234 | }; 235 | 236 | public func digit() : CharParser { 237 | sat( 238 | func(x : Char) : Bool { 239 | '0' <= x and x <= '9'; 240 | } 241 | ); 242 | }; 243 | 244 | public func hex() : CharParser { 245 | sat( 246 | func(x : Char) : Bool { 247 | '0' <= x and x <= '9' or 'a' <= x and x <= 'f' or 'A' <= x and x <= 'A'; 248 | } 249 | ); 250 | }; 251 | 252 | public func lower() : CharParser { 253 | sat( 254 | func(x : Char) : Bool { 255 | 'a' <= x and x <= 'z'; 256 | } 257 | ); 258 | }; 259 | 260 | public func upper() : CharParser { 261 | sat( 262 | func(x : Char) : Bool { 263 | 'A' <= x and x <= 'Z'; 264 | } 265 | ); 266 | }; 267 | 268 | public func letter() : CharParser { 269 | choose(lower(), upper()); 270 | }; 271 | 272 | public func alphanum() : CharParser { 273 | choose(letter(), digit()); 274 | }; 275 | 276 | public func oneOf( 277 | xs : [Char] 278 | ) : CharParser { 279 | sat( 280 | func(c : Char) : Bool { 281 | for (x in xs.vals()) { 282 | if (c == x) { return true }; 283 | }; 284 | false; 285 | } 286 | ); 287 | }; 288 | 289 | public func space() : CharParser { 290 | oneOf([' ', '\n', '\t', '\r']); 291 | }; 292 | }; 293 | 294 | public module String { 295 | private type StringParser = Parser; 296 | 297 | public func word() : StringParser { 298 | map( 299 | many(Character.letter()), 300 | func(xs : List) : Text { 301 | Text.fromIter(L.toIter(xs)); 302 | }, 303 | ); 304 | }; 305 | 306 | public func string(t : Text) : StringParser { 307 | func iter(i : Iter.Iter) : StringParser { 308 | switch (i.next()) { 309 | case (null) { P.result(t) }; 310 | case (?v) { 311 | right( 312 | Character.char(v), 313 | iter(i), 314 | ); 315 | }; 316 | }; 317 | }; 318 | iter(t.chars()); 319 | }; 320 | }; 321 | 322 | public module Nat { 323 | func toNat(xs : List) : Nat { 324 | let ord0 = Char.toNat32('0'); 325 | let n = List.foldLeft( 326 | xs, 327 | 0, 328 | func(n : Nat, c : Char) : Nat { 329 | 10 * n + Nat32.toNat((Char.toNat32(c) - ord0)); 330 | }, 331 | ); 332 | n; 333 | }; 334 | 335 | public func nat() : Parser { 336 | map( 337 | many1(Character.digit()), 338 | toNat, 339 | ); 340 | }; 341 | }; 342 | 343 | public module Int { 344 | public func int() : Parser { 345 | func(xs : List) { 346 | let (op, ys) = switch (Character.char('-')(xs)) { 347 | case (null) { (func(n : Nat) : Int { n }, xs) }; 348 | case (?(_, xs)) { (func(n : Nat) : Int { -n }, xs) }; 349 | }; 350 | map( 351 | Nat.nat(), 352 | op, 353 | )(ys); 354 | }; 355 | }; 356 | }; 357 | }; 358 | -------------------------------------------------------------------------------- /bench/serde.bench.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base@0.16.0/Iter"; 2 | import Debug "mo:base@0.16.0/Debug"; 3 | import Prelude "mo:base@0.16.0/Prelude"; 4 | import Text "mo:base@0.16.0/Text"; 5 | import Char "mo:base@0.16.0/Char"; 6 | import Buffer "mo:base@0.16.0/Buffer"; 7 | 8 | import Bench "mo:bench"; 9 | import Fuzz "mo:fuzz"; 10 | import Itertools "mo:itertools@0.2.2/Iter"; 11 | 12 | import Serde "../src"; 13 | import CandidEncoder "../src/Candid/Blob/Encoder"; 14 | import CandidDecoder "../src/Candid/Blob/Decoder"; 15 | 16 | module { 17 | public func init() : Bench.Bench { 18 | let bench = Bench.Bench(); 19 | 20 | bench.name("Benchmarking Serde"); 21 | bench.description("Benchmarking the performance with 1k calls"); 22 | 23 | bench.rows([ 24 | "Serde: One Shot", 25 | "Serde: One Shot sans type inference", 26 | "Motoko (to_candid(), from_candid())", 27 | "Serde: Single Type Serializer", 28 | ]); 29 | 30 | bench.cols([ 31 | "decode()", 32 | "encode()", 33 | ]); 34 | 35 | type Candid = Serde.Candid; 36 | 37 | let fuzz = Fuzz.Fuzz(); 38 | 39 | let limit = 1_000; 40 | 41 | type CustomerReview = { 42 | username : Text; 43 | rating : Nat; 44 | comment : Text; 45 | }; 46 | 47 | type AvailableSizes = { #xs; #s; #m; #l; #xl }; 48 | 49 | type ColorOption = { 50 | name : Text; 51 | hex : Text; 52 | }; 53 | 54 | // partial types for StoreItem 55 | // as mo:motoko_candid is limited and throws errors on some complex types 56 | type StoreItem = { 57 | name : Text; 58 | store : Text; 59 | customer_reviews : [CustomerReview]; 60 | available_sizes : AvailableSizes; 61 | color_options : [ColorOption]; 62 | price : Float; 63 | in_stock : Bool; 64 | address : (Text, Text, Text, Text); 65 | contact : { 66 | email : Text; 67 | phone : ?Text; 68 | }; 69 | }; 70 | 71 | let CustomerReview = #Record([ 72 | ("username", #Text), 73 | ("comment", #Text), 74 | ("rating", #Nat), 75 | ]); 76 | 77 | let AvailableSizes = #Variant([("xs", #Null), ("s", #Null), ("m", #Null), ("l", #Null), ("xl", #Null)]); 78 | 79 | let ColorOption = #Record([ 80 | ("name", #Text), 81 | ("hex", #Text), 82 | ]); 83 | 84 | let StoreItem : Serde.Candid.CandidType = #Record([ 85 | ("name", #Text), 86 | ("store", #Text), 87 | ("customer_reviews", #Array(CustomerReview)), 88 | ("available_sizes", AvailableSizes), 89 | ("color_options", #Array(ColorOption)), 90 | ("price", #Float), 91 | ("in_stock", #Bool), 92 | ("address", #Tuple([#Text, #Text, #Text, #Text])), 93 | ("contact", #Record([("email", #Text), ("phone", #Option(#Text))])), 94 | ]); 95 | 96 | let FormattedStoreItem = Serde.Candid.formatCandidType([StoreItem], null); 97 | 98 | let candify_store_item = { 99 | from_blob = func(blob : Blob) : StoreItem { 100 | let ?c : ?StoreItem = from_candid (blob); 101 | c; 102 | }; 103 | to_blob = func(c : StoreItem) : Blob { to_candid (c) }; 104 | }; 105 | 106 | let cities = ["Toronto", "Ottawa", "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose"]; 107 | let states = ["ON", "QC", "NY", "CA", "IL", "TX", "AZ", "PA", "TX", "CA", "TX", "CA"]; 108 | let streets = ["King St", "Queen St", "Yonge St", "Bay St", "Bloor St", "Dundas St", "College St", "Spadina Ave", "St Clair Ave", "Danforth Ave", "Eglinton Ave", "Lawrence Ave"]; 109 | 110 | let stores = ["h&m", "zara", "gap", "old navy", "forever 21", "uniqlo", "urban outfitters", "american eagle", "aeropostale", "abercrombie & fitch", "hollister", "express"]; 111 | let email_terminator = ["gmail.com", "yahoo.com", "outlook.com"]; 112 | 113 | let cs_starter_kid = ["black hoodie", "M1 macbook", "white hoodie", "air forces", "Algorithms textbook", "c the hard way", "Udemy subscription", "Nvidea RTX"]; 114 | 115 | let available_sizes = [#xs, #s, #m, #l, #xl]; 116 | 117 | func new_item() : StoreItem { 118 | let store_name = fuzz.array.randomEntry(stores).1; 119 | let store_item = { 120 | name = fuzz.array.randomEntry(cs_starter_kid).1; 121 | store = store_name; 122 | customer_reviews = [ 123 | { 124 | username = "user1"; 125 | rating = fuzz.nat.randomRange(0, 5); 126 | comment = "good"; 127 | }, 128 | { 129 | username = "user2"; 130 | rating = fuzz.nat.randomRange(0, 5); 131 | comment = "ok"; 132 | }, 133 | ]; 134 | available_sizes = fuzz.array.randomEntry(available_sizes).1; 135 | color_options = [ 136 | { name = "red"; hex = "#ff0000" }, 137 | { name = "blue"; hex = "#0000ff" }, 138 | ]; 139 | price = fuzz.float.randomRange(19.99, 399.99); 140 | in_stock = fuzz.bool.random(); 141 | address = ( 142 | fuzz.array.randomEntry(streets).1, 143 | fuzz.array.randomEntry(cities).1, 144 | fuzz.array.randomEntry(states).1, 145 | fuzz.text.randomAlphanumeric(6), 146 | ); 147 | contact = { 148 | email = store_name # "@" # fuzz.array.randomEntry(email_terminator).1; 149 | phone = if (fuzz.nat.randomRange(0, 100) % 3 == 0) { null } else { 150 | ?Text.fromIter( 151 | fuzz.array.randomArray(10, func() : Char { Char.fromNat32(fuzz.nat32.randomRange(0, 9) + Char.toNat32('0')) }).vals() : Iter.Iter 152 | ); 153 | }; 154 | }; 155 | }; 156 | }; 157 | 158 | let buffer = Buffer.Buffer(limit); 159 | let candid_blobs = Buffer.Buffer(limit); 160 | let candid_buffer = Buffer.Buffer<[Serde.Candid]>(limit); 161 | 162 | for (i in Itertools.range(0, limit)) { 163 | let item = new_item(); 164 | buffer.add(item); 165 | }; 166 | 167 | let StoreItemKeys = ["name", "store", "customer_reviews", "username", "rating", "comment", "available_sizes", "xs", "s", "m", "l", "xl", "color_options", "name", "hex", "price", "in_stock", "address", "contact", "email", "phone"]; 168 | 169 | bench.runner( 170 | func(row, col) = switch (row, col) { 171 | case ("Motoko (to_candid(), from_candid())", "encode()") { 172 | for (i in Itertools.range(0, limit)) { 173 | let item = buffer.get(i); 174 | let candid = to_candid (item); 175 | // candid_blobs.add(candid); 176 | }; 177 | }; 178 | case ("Motoko (to_candid(), from_candid())", "decode()") { 179 | for (i in Itertools.range(0, limit)) { 180 | let blob = candid_blobs.get(i); 181 | let ?store_item : ?StoreItem = from_candid (blob); 182 | }; 183 | }; 184 | 185 | case ("Serde: One Shot", "decode()") { 186 | for (i in Itertools.range(0, limit)) { 187 | let item = buffer.get(i); 188 | let candid_blob = candify_store_item.to_blob(item); 189 | candid_blobs.add(candid_blob); 190 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, StoreItemKeys, null); 191 | candid_buffer.add(candid); 192 | }; 193 | }; 194 | case ("Serde: One Shot", "encode()") { 195 | for (i in Itertools.range(0, limit)) { 196 | let candid = candid_buffer.get(i); 197 | let res = CandidEncoder.one_shot(candid, null); 198 | let #ok(blob) = res; 199 | }; 200 | }; 201 | 202 | case ("Serde: One Shot sans type inference", "decode()") { 203 | for (i in Itertools.range(0, limit)) { 204 | let item = buffer.get(i); 205 | let candid_blob = candify_store_item.to_blob(item); 206 | 207 | let options = { 208 | Serde.Candid.defaultOptions with types = ?FormattedStoreItem 209 | }; 210 | 211 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, StoreItemKeys, ?options); 212 | // candid_buffer.add(candid); 213 | }; 214 | }; 215 | 216 | case ("Serde: One Shot sans type inference", "encode()") { 217 | for (i in Itertools.range(0, limit)) { 218 | let candid = candid_buffer.get(i); 219 | 220 | let options = { 221 | Serde.Candid.defaultOptions with types = ?FormattedStoreItem 222 | }; 223 | let res = CandidEncoder.one_shot(candid, ?options); 224 | let #ok(blob) = res; 225 | }; 226 | }; 227 | 228 | case ("Serde: Single Type Serializer", "decode()") { 229 | let options = { 230 | Serde.Candid.defaultOptions with types = ?FormattedStoreItem 231 | }; 232 | let serializer = Serde.Candid.TypedSerializer.fromBlob(candify_store_item.to_blob(buffer.get(0)), StoreItemKeys, ?options); 233 | 234 | for (i in Itertools.range(0, limit)) { 235 | let item = buffer.get(i); 236 | let candid_blob = candify_store_item.to_blob(item); 237 | 238 | let #ok(candid) = Serde.Candid.TypedSerializer.decode(serializer, candid_blob); 239 | // candid_buffer.add(candid); 240 | }; 241 | }; 242 | 243 | case ("Serde: Single Type Serializer", "encode()") { 244 | 245 | let serializer = Serde.Candid.TypedSerializer.new(FormattedStoreItem, null); 246 | 247 | for (i in Itertools.range(0, limit)) { 248 | let candid = candid_buffer.get(i); 249 | let res = Serde.Candid.TypedSerializer.encode(serializer, candid); 250 | }; 251 | }; 252 | 253 | case (_, _) { 254 | Debug.trap("Should be unreachable:\n row = \"" # debug_show row # "\" and col = \"" # debug_show col # "\""); 255 | }; 256 | } 257 | ); 258 | 259 | bench; 260 | }; 261 | }; 262 | --------------------------------------------------------------------------------