├── .github └── workflows │ └── makefile.yml ├── .gitignore ├── Candid.md ├── LICENSE ├── bench.md ├── bench └── serde.bench.mo ├── dfx.json ├── makefile ├── mops.toml ├── readme.md ├── src ├── CBOR │ └── lib.mo ├── Candid │ ├── Blob │ │ ├── CandidUtils.mo │ │ ├── Decoder.mo │ │ ├── Encoder.ForwardReference.mo │ │ ├── Encoder.mo │ │ └── RepIndyHash.mo │ ├── ICRC3Value.mo │ ├── Text │ │ ├── Parser │ │ │ ├── Array.mo │ │ │ ├── Blob.mo │ │ │ ├── Bool.mo │ │ │ ├── Common.mo │ │ │ ├── Float.mo │ │ │ ├── Int.mo │ │ │ ├── IntX.mo │ │ │ ├── Nat.mo │ │ │ ├── NatX.mo │ │ │ ├── Option.mo │ │ │ ├── Principal.mo │ │ │ ├── Record.mo │ │ │ ├── Text.mo │ │ │ ├── Variant.mo │ │ │ └── lib.mo │ │ └── ToText.mo │ ├── Types.mo │ └── lib.mo ├── JSON │ ├── FromText.mo │ ├── ToText.mo │ └── lib.mo ├── UrlEncoded │ ├── FromText.mo │ ├── Parser.mo │ ├── ToText.mo │ └── lib.mo ├── Utils.mo ├── lib.mo └── libs │ └── motoko_candid │ └── utils.mo ├── tests ├── CBOR.Test.mo ├── Candid.ICRC3.Test.mo ├── Candid.Large.test.mo ├── Candid.Test.mo ├── JSON.Test.mo ├── RepIndyHash.Test.mo ├── UrlEncoded.Test.mo ├── one-shot.backward-reference.test.mo ├── one-shot.forward-reference.mo ├── test_template.md └── utils │ └── ActorSpec.mo └── usage.md /.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@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | 22 | - name: Cache Node modules 23 | uses: actions/cache@v2 24 | with: 25 | path: ~/.npm 26 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 27 | restore-keys: | 28 | ${{ runner.os }}-node- 29 | 30 | - name: Cache mops packages 31 | uses: actions/cache@v3 32 | with: 33 | key: mops-packages-${{ hashFiles('mops.toml') }} 34 | path: | 35 | ~/.cache/mops 36 | ~/mops 37 | 38 | - name: Install dfx 39 | uses: dfinity/setup-dfx@main 40 | - name: Confirm successful installation 41 | run: dfx --version 42 | 43 | - name: Install dfx cache 44 | run: dfx cache install 45 | 46 | - name: Install mops & mocv 47 | run: | 48 | npm --yes -g i ic-mops 49 | mops i 50 | mops toolchain init 51 | 52 | # set moc path for dfx to use 53 | echo "DFX_MOC_PATH=$(mops toolchain bin moc)" >> $GITHUB_ENV 54 | 55 | # - name: Detect warnings 56 | # run: make check 57 | 58 | - name: Run Tests 59 | run: mops test -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bench/serde.bench.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base/Iter"; 2 | import Debug "mo:base/Debug"; 3 | import Prelude "mo:base/Prelude"; 4 | import Text "mo:base/Text"; 5 | import Char "mo:base/Char"; 6 | import Buffer "mo:base/Buffer"; 7 | 8 | import Bench "mo:bench"; 9 | import Fuzz "mo:fuzz"; 10 | import Itertools "mo:itertools/Iter"; 11 | 12 | import Serde "../src"; 13 | import CandidEncoder "../src/Candid/Blob/Encoder"; 14 | import CandidEncoderFR "../src/Candid/Blob/Encoder.ForwardReference"; 15 | import CandidDecoder "../src/Candid/Blob/Decoder"; 16 | 17 | module { 18 | public func init() : Bench.Bench { 19 | let bench = Bench.Bench(); 20 | 21 | bench.name("Benchmarking Serde"); 22 | bench.description("Benchmarking the performance with 10k calls"); 23 | 24 | bench.rows([ 25 | "Serde: One Shot", 26 | "Serde: One Shot sans type inference", 27 | // "Serde 'mo:motoko_candid' lib", 28 | "Motoko (to_candid(), from_candid())", 29 | 30 | // "#Nat" 31 | ]); 32 | 33 | bench.cols([ 34 | "decode()", 35 | "encode()", 36 | ]); 37 | 38 | type Candid = Serde.Candid; 39 | 40 | let fuzz = Fuzz.Fuzz(); 41 | 42 | let limit = 1_000; 43 | 44 | type CustomerReview = { 45 | username : Text; 46 | rating : Nat; 47 | comment : Text; 48 | }; 49 | 50 | type AvailableSizes = { #xs; #s; #m; #l; #xl }; 51 | 52 | type ColorOption = { 53 | name : Text; 54 | hex : Text; 55 | }; 56 | 57 | // partial types for StoreItem 58 | // as mo:motoko_candid is limited and throws errors on some complex types 59 | type StoreItem = { 60 | name : Text; 61 | store : Text; 62 | customer_reviews : [CustomerReview]; 63 | available_sizes : AvailableSizes; 64 | color_options : [ColorOption]; 65 | price : Float; 66 | in_stock : Bool; 67 | address : (Text, Text, Text, Text); 68 | contact : { 69 | email : Text; 70 | phone : ?Text; 71 | }; 72 | }; 73 | 74 | let CustomerReview = #Record([ 75 | ("username", #Text), 76 | ("comment", #Text), 77 | ("rating", #Nat), 78 | ]); 79 | 80 | let AvailableSizes = #Variant([("xs", #Null), ("s", #Null), ("m", #Null), ("l", #Null), ("xl", #Null)]); 81 | 82 | let ColorOption = #Record([ 83 | ("name", #Text), 84 | ("hex", #Text), 85 | ]); 86 | 87 | let StoreItem : Serde.Candid.CandidType = #Record([ 88 | ("name", #Text), 89 | ("store", #Text), 90 | ("customer_reviews", #Array(CustomerReview)), 91 | ("available_sizes", AvailableSizes), 92 | ("color_options", #Array(ColorOption)), 93 | ("price", #Float), 94 | ("in_stock", #Bool), 95 | ("address", #Tuple([#Text, #Text, #Text, #Text])), 96 | ("contact", #Record([("email", #Text), ("phone", #Option(#Text))])), 97 | ]); 98 | 99 | let FormattedStoreItem = Serde.Candid.formatCandidType([StoreItem], null); 100 | 101 | let candify_store_item = { 102 | from_blob = func(blob : Blob) : StoreItem { 103 | let ?c : ?StoreItem = from_candid (blob); 104 | c; 105 | }; 106 | to_blob = func(c : StoreItem) : Blob { to_candid (c) }; 107 | }; 108 | 109 | let cities = ["Toronto", "Ottawa", "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose"]; 110 | let states = ["ON", "QC", "NY", "CA", "IL", "TX", "AZ", "PA", "TX", "CA", "TX", "CA"]; 111 | 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"]; 112 | 113 | let stores = ["h&m", "zara", "gap", "old navy", "forever 21", "uniqlo", "urban outfitters", "american eagle", "aeropostale", "abercrombie & fitch", "hollister", "express"]; 114 | let email_terminator = ["gmail.com", "yahoo.com", "outlook.com"]; 115 | 116 | let cs_starter_kid = ["black hoodie", "M1 macbook", "white hoodie", "air forces", "Algorithms textbook", "c the hard way", "Udemy subscription", "Nvidea RTX"]; 117 | 118 | let available_sizes = [#xs, #s, #m, #l, #xl]; 119 | 120 | func new_item() : StoreItem { 121 | let store_name = fuzz.array.randomEntry(stores).1; 122 | let store_item = { 123 | name = fuzz.array.randomEntry(cs_starter_kid).1; 124 | store = store_name; 125 | customer_reviews = [ 126 | { 127 | username = "user1"; 128 | rating = fuzz.nat.randomRange(0, 5); 129 | comment = "good"; 130 | }, 131 | { 132 | username = "user2"; 133 | rating = fuzz.nat.randomRange(0, 5); 134 | comment = "ok"; 135 | }, 136 | ]; 137 | available_sizes = fuzz.array.randomEntry(available_sizes).1; 138 | color_options = [ 139 | { name = "red"; hex = "#ff0000" }, 140 | { name = "blue"; hex = "#0000ff" }, 141 | ]; 142 | price = fuzz.float.randomRange(19.99, 399.99); 143 | in_stock = fuzz.bool.random(); 144 | address = ( 145 | fuzz.array.randomEntry(streets).1, 146 | fuzz.array.randomEntry(cities).1, 147 | fuzz.array.randomEntry(states).1, 148 | fuzz.text.randomAlphanumeric(6), 149 | ); 150 | contact = { 151 | email = store_name # "@" # fuzz.array.randomEntry(email_terminator).1; 152 | phone = if (fuzz.nat.randomRange(0, 100) % 3 == 0) { null } else { 153 | ?Text.fromIter( 154 | fuzz.array.randomArray(10, func() : Char { Char.fromNat32(fuzz.nat32.randomRange(0, 9) + Char.toNat32('0')) }).vals() : Iter.Iter 155 | ); 156 | }; 157 | }; 158 | }; 159 | }; 160 | 161 | let buffer = Buffer.Buffer(limit); 162 | let candid_blobs = Buffer.Buffer(limit); 163 | let candid_buffer = Buffer.Buffer<[Serde.Candid]>(limit); 164 | 165 | for (i in Itertools.range(0, limit)) { 166 | let item = new_item(); 167 | buffer.add(item); 168 | }; 169 | 170 | 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"]; 171 | 172 | bench.runner( 173 | func(row, col) = switch (row, col) { 174 | case ("Motoko (to_candid(), from_candid())", "encode()") { 175 | for (i in Itertools.range(0, limit)) { 176 | let item = buffer.get(i); 177 | let candid = to_candid (item); 178 | // candid_blobs.add(candid); 179 | }; 180 | }; 181 | case ("Motoko (to_candid(), from_candid())", "decode()") { 182 | for (i in Itertools.range(0, limit)) { 183 | let blob = candid_blobs.get(i); 184 | let ?store_item : ?StoreItem = from_candid (blob); 185 | }; 186 | }; 187 | 188 | // case ("Serde 'mo:motoko_candid' lib", "decode()") { 189 | // for (i in Itertools.range(0, limit)) { 190 | // let item : StoreItem = buffer.get(i); 191 | // let candid_blob = candify_store_item.to_blob(item); 192 | // candid_blobs.add(candid_blob); 193 | // let #ok(candid) = LegacyCandidDecoder.decode(candid_blob, StoreItemKeys, null); 194 | // candid_buffer.add(candid); 195 | // }; 196 | // }; 197 | // case ("Serde 'mo:motoko_candid' lib", "encode()") { 198 | // for (i in Itertools.range(0, limit)) { 199 | // let candid = candid_buffer.get(i); 200 | // let res = LegacyCandidEncoder.encode(candid, null); 201 | // let #ok(blob) = res; 202 | // }; 203 | // }; 204 | 205 | case ("Serde: One Shot", "decode()") { 206 | for (i in Itertools.range(0, limit)) { 207 | let item = buffer.get(i); 208 | let candid_blob = candify_store_item.to_blob(item); 209 | candid_blobs.add(candid_blob); 210 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, StoreItemKeys, null); 211 | candid_buffer.add(candid); 212 | }; 213 | }; 214 | case ("Serde: One Shot", "encode()") { 215 | for (i in Itertools.range(0, limit)) { 216 | let candid = candid_buffer.get(i); 217 | let res = CandidEncoder.one_shot(candid, null); 218 | let #ok(blob) = res; 219 | }; 220 | }; 221 | 222 | case ("Serde: One Shot sans type inference", "decode()") { 223 | for (i in Itertools.range(0, limit)) { 224 | let item = buffer.get(i); 225 | let candid_blob = candify_store_item.to_blob(item); 226 | 227 | let FormattedStoreItem = Serde.Candid.formatCandidType([StoreItem], null); 228 | 229 | let options = { 230 | Serde.Candid.defaultOptions with types = ?FormattedStoreItem 231 | }; 232 | 233 | let #ok(candid) = CandidDecoder.one_shot(candid_blob, StoreItemKeys, ?options); 234 | // candid_buffer.add(candid); 235 | }; 236 | }; 237 | 238 | case ("Serde: One Shot sans type inference", "encode()") { 239 | for (i in Itertools.range(0, limit)) { 240 | let candid = candid_buffer.get(i); 241 | 242 | let options = { 243 | Serde.Candid.defaultOptions with types = ?FormattedStoreItem 244 | }; 245 | let res = CandidEncoder.one_shot(candid, ?options); 246 | let #ok(blob) = res; 247 | }; 248 | }; 249 | 250 | case ("Serde: One Shot Forward Reference (FR)", "decode()") {}; 251 | case ("Serde: One Shot Forward Reference (FR)", "encode()") { 252 | for (i in Itertools.range(0, limit)) { 253 | let candid = candid_buffer.get(i); 254 | let res = CandidEncoderFR.one_shot(candid, null); 255 | let #ok(blob) = res; 256 | }; 257 | }; 258 | 259 | case ("Serde: One Shot FR sans type inference", "decode()") {}; 260 | 261 | case ("Serde: One Shot FR sans type inference", "encode()") { 262 | for (i in Itertools.range(0, limit)) { 263 | let candid = candid_buffer.get(i); 264 | 265 | let options = { 266 | Serde.Candid.defaultOptions with types = ?[StoreItem] 267 | }; 268 | let res = CandidEncoderFR.one_shot(candid, ?options); 269 | let #ok(blob) = res; 270 | }; 271 | }; 272 | 273 | // case ("#Nat", "decode()"){ 274 | // for (i in Itertools.range(0, limit)) { 275 | // let item = buffer.get(i); 276 | // let candid_blob = candify_store_item.to_blob(item); 277 | // let #ok(candid) = CandidDecoder.decode(candid_blob, StoreItemKeys, null); 278 | // // candid_buffer.add(candid); 279 | // }; 280 | // }; 281 | case (_, _) { 282 | Debug.trap("Should be unreachable:\n row = \"" # debug_show row # "\" and col = \"" # debug_show col # "\""); 283 | }; 284 | } 285 | ); 286 | 287 | bench; 288 | }; 289 | }; 290 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 incremental -------------------------------------------------------------------------------- /mops.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde" 3 | version = "3.2.2" 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.12.0" 11 | itertools = "0.2.1" 12 | candid = "1.1.1" 13 | xtended-numbers = "0.3.1" 14 | json-float = "https://github.com/NatLabs/json.mo#float@f3c8e7d418a7a8f2d6c0d7e2d276a0a82c2046ff" 15 | parser-combinators = "https://github.com/aviate-labs/parser-combinators.mo#v0.1.2@6a331bf78e9dcd7623977f06c8e561fd1a8c0103" 16 | cbor = "1.0.0" 17 | map = "9.0.1" 18 | sha2 = "0.1.0" 19 | 20 | [dev-dependencies] 21 | test = "2.0.0" 22 | bench = "1.0.0" 23 | fuzz = "0.2.1" 24 | rep-indy-hash = "0.1.1" 25 | 26 | [toolchain] 27 | wasmtime = "14.0.4" 28 | moc = "0.11.1" 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/CBOR/lib.mo: -------------------------------------------------------------------------------- 1 | import Buffer "mo:base/Buffer"; 2 | import Blob "mo:base/Blob"; 3 | import Int8 "mo:base/Int8"; 4 | import Int16 "mo:base/Int16"; 5 | import Int32 "mo:base/Int32"; 6 | import Int64 "mo:base/Int64"; 7 | import Option "mo:base/Option"; 8 | import Nat64 "mo:base/Nat64"; 9 | import Result "mo:base/Result"; 10 | import Principal "mo:base/Principal"; 11 | 12 | import CBOR_Types "mo:cbor/Types"; 13 | import CBOR_Encoder "mo:cbor/Encoder"; 14 | import CBOR_Decoder "mo:cbor/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.encode(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.decode(blob); 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/Candid/Blob/CandidUtils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Buffer "mo:base/Buffer"; 3 | import Result "mo:base/Result"; 4 | import Nat32 "mo:base/Nat32"; 5 | import Nat "mo:base/Nat"; 6 | import Iter "mo:base/Iter"; 7 | import Text "mo:base/Text"; 8 | import Order "mo:base/Order"; 9 | 10 | import Itertools "mo:itertools/Iter"; 11 | import Map "mo:map/Map"; 12 | import { hashName = hash_record_key } "mo:candid/Tag"; 13 | 14 | import T "../Types"; 15 | import TrieMap "mo:base/TrieMap"; 16 | import Utils "../../Utils"; 17 | 18 | module { 19 | 20 | type TrieMap = TrieMap.TrieMap; 21 | type Result = Result.Result; 22 | type Buffer = Buffer.Buffer; 23 | type Iter = Iter.Iter; 24 | type Hash = Nat32; 25 | type Map = Map.Map; 26 | type Order = Order.Order; 27 | 28 | type Candid = T.Candid; 29 | type CandidType = T.CandidType; 30 | type KeyValuePair = T.KeyValuePair; 31 | 32 | let { thash } = Map; 33 | 34 | public func cmp_fields(a : (Text, Any), b : (Text, Any)) : Order { 35 | 36 | let hash_a = hash_record_key(a.0); 37 | let hash_b = hash_record_key(b.0); 38 | 39 | Nat32.compare(hash_a, hash_b); 40 | }; 41 | 42 | public func cmp_nat_fields(a : (Text, Any), b : (Text, Any)) : Order { 43 | 44 | let n1 = Utils.text_to_nat(a.0); 45 | let n2 = Utils.text_to_nat(b.0); 46 | 47 | Nat.compare(n1, n2); 48 | }; 49 | 50 | public func is_record_tuple(record_fields : [(Text, Any)]) : Bool { 51 | Itertools.all( 52 | record_fields.vals(), 53 | func(field : (Text, Any)) : Bool { 54 | Utils.text_is_number(field.0); 55 | }, 56 | ); 57 | }; 58 | 59 | public func sort_candid_type(candid_type : CandidType) : CandidType { 60 | switch (candid_type) { 61 | case (#Record(fields)) { 62 | let is_tuple = Itertools.all( 63 | fields.vals(), 64 | func(field : (Text, Any)) : Bool { 65 | Utils.text_is_number(field.0); 66 | }, 67 | ); 68 | 69 | let sorted_fields = if (is_tuple) { 70 | (Array.sort(fields, cmp_nat_fields)); 71 | } else { 72 | (Array.sort(fields, cmp_fields)); 73 | }; 74 | 75 | let sorted_nested_fields = Array.map<(Text, CandidType), (Text, CandidType)>( 76 | sorted_fields, 77 | func(field : (Text, CandidType)) : (Text, CandidType) { 78 | (field.0, sort_candid_type(field.1)); 79 | }, 80 | ); 81 | 82 | #Record(sorted_nested_fields); 83 | }; 84 | case (#Variant(fields)) { 85 | let is_tuple = Itertools.all( 86 | fields.vals(), 87 | func(field : (Text, CandidType)) : Bool { 88 | Utils.text_is_number(field.0); 89 | }, 90 | ); 91 | 92 | let sorted_fields = if (is_tuple) { 93 | (Array.sort(fields, cmp_nat_fields)); 94 | } else { 95 | (Array.sort(fields, cmp_fields)); 96 | }; 97 | 98 | let sorted_nested_fields = Array.map<(Text, CandidType), (Text, CandidType)>( 99 | sorted_fields, 100 | func(field : (Text, CandidType)) : (Text, CandidType) { 101 | (field.0, sort_candid_type(field.1)); 102 | }, 103 | ); 104 | 105 | #Variant(sorted_nested_fields); 106 | }; 107 | case (#Array(arr_type)) #Array(sort_candid_type(arr_type)); 108 | case (#Option(opt_type)) #Option(sort_candid_type(opt_type)); 109 | case (#Tuple(tuple_types)) #Tuple(Array.map(tuple_types, sort_candid_type)); 110 | case (other_types) other_types; 111 | }; 112 | }; 113 | 114 | func tuple_type_to_record(tuple_types : [CandidType], mapper : (CandidType) -> CandidType) : [(Text, CandidType)] { 115 | Array.tabulate<(Text, CandidType)>( 116 | tuple_types.size(), 117 | func(i : Nat) : (Text, CandidType) { 118 | let key = Text.replace(debug_show (i), #char('_'), ""); 119 | (key, mapper(tuple_types[i])); 120 | }, 121 | ); 122 | }; 123 | 124 | /// Sorts fields by their hash value and renames changed fields 125 | /// 126 | /// Also resolves aliases by renaming them to their canonical name, 127 | /// for example #Blob to #Array(#Nat8) 128 | public func format_candid_type(candid_type : CandidType, renaming_map : Map) : CandidType { 129 | switch (candid_type) { 130 | case (#Record(fields) or #Map(fields)) { 131 | 132 | var is_tuple = true; 133 | 134 | let renamed_fields = Array.tabulate<(Text, CandidType)>( 135 | fields.size(), 136 | func(i : Nat) : (Text, CandidType) { 137 | let field_key = fields[i].0; 138 | let field_value = fields[i].1; 139 | 140 | let new_key = switch (Map.get(renaming_map, thash, field_key)) { 141 | case (?new_key) new_key; 142 | case (_) field_key; 143 | }; 144 | 145 | is_tuple := is_tuple and Utils.text_is_number(new_key); 146 | 147 | (new_key, format_candid_type(field_value, renaming_map)); 148 | }, 149 | ); 150 | 151 | let sorted_fields = if (is_tuple) { 152 | (Array.sort(renamed_fields, cmp_nat_fields)); 153 | } else { 154 | (Array.sort(renamed_fields, cmp_fields)); 155 | }; 156 | 157 | #Record(sorted_fields); 158 | }; 159 | case (#Variant(fields)) { 160 | var is_tuple = true; 161 | 162 | let renamed_fields = Array.tabulate<(Text, CandidType)>( 163 | fields.size(), 164 | func(i : Nat) : (Text, CandidType) { 165 | let field_key = fields[i].0; 166 | let field_value = fields[i].1; 167 | 168 | let new_key = switch (Map.get(renaming_map, thash, field_key)) { 169 | case (?new_key) new_key; 170 | case (_) field_key; 171 | }; 172 | 173 | is_tuple := is_tuple and Utils.text_is_number(new_key); 174 | 175 | (new_key, format_candid_type(field_value, renaming_map)); 176 | }, 177 | ); 178 | 179 | let sorted_fields = if (is_tuple) { 180 | (Array.sort(renamed_fields, cmp_nat_fields)); 181 | } else { 182 | (Array.sort(renamed_fields, cmp_fields)); 183 | }; 184 | 185 | #Variant(sorted_fields); 186 | }; 187 | case (#Blob) #Array(#Nat8); 188 | case (#Array(arr_type)) #Array(format_candid_type(arr_type, renaming_map)); 189 | case (#Option(opt_type)) #Option(format_candid_type(opt_type, renaming_map)); 190 | case (#Tuple(tuple_types)) #Record( 191 | tuple_type_to_record( 192 | tuple_types, 193 | func(candid_type : CandidType) : CandidType = format_candid_type(candid_type, renaming_map), 194 | ) 195 | ); 196 | case (other_types) other_types; 197 | }; 198 | }; 199 | 200 | public func sort_candid_value(candid_value : Candid) : Candid { 201 | switch (candid_value) { 202 | case (#Record(fields)) { 203 | let is_tuple = Itertools.all( 204 | fields.vals(), 205 | func(field : (Text, Any)) : Bool { 206 | Utils.text_is_number(field.0); 207 | }, 208 | ); 209 | 210 | if (is_tuple) { 211 | #Record(Array.sort(fields, cmp_nat_fields)); 212 | } else { 213 | #Record(Array.sort(fields, cmp_fields)); 214 | }; 215 | }; 216 | case (other_values) other_values; 217 | }; 218 | }; 219 | 220 | public func RecordType(records : [(Text, CandidType)]) : CandidType { 221 | #Record(Array.sort(records, cmp_fields)); 222 | }; 223 | 224 | public func RecordValue(records : [(Text, Candid)]) : Candid { 225 | #Record(Array.sort(records, cmp_fields)); 226 | }; 227 | }; 228 | -------------------------------------------------------------------------------- /src/Candid/Blob/RepIndyHash.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Buffer "mo:base/Buffer"; 4 | import Debug "mo:base/Debug"; 5 | import Nat64 "mo:base/Nat64"; 6 | import Int8 "mo:base/Int8"; 7 | import Int32 "mo:base/Int32"; 8 | import Nat8 "mo:base/Nat8"; 9 | import Nat32 "mo:base/Nat32"; 10 | import Nat16 "mo:base/Nat16"; 11 | import Int64 "mo:base/Int64"; 12 | import Nat "mo:base/Nat"; 13 | import Principal "mo:base/Principal"; 14 | import Text "mo:base/Text"; 15 | import Int16 "mo:base/Int16"; 16 | 17 | import T "../Types"; 18 | import Utils "../../Utils"; 19 | import Sha256 "mo:sha2/Sha256"; 20 | 21 | module { 22 | type Buffer = Buffer.Buffer; 23 | 24 | let { ReusableBuffer; unsigned_leb128; signed_leb128_64 } = Utils; 25 | 26 | public func hash(candid_value : T.Candid) : Blob { 27 | let buffer = ReusableBuffer(100); 28 | let sha256 = Sha256.Digest(#sha256); 29 | 30 | candid_hash(buffer, sha256, candid_value); 31 | }; 32 | 33 | func candid_hash( 34 | buffer : Utils.ReusableBuffer, 35 | sha256 : Sha256.Digest, 36 | candid_value : T.Candid, 37 | ) : Blob { 38 | switch (candid_value) { 39 | case (#Int(n)) signed_leb128_64(buffer, n); 40 | case (#Int8(i8)) { 41 | buffer.add(Int8.toNat8(i8)); 42 | }; 43 | case (#Int16(i16)) { 44 | let n16 = Int16.toNat16(i16); 45 | buffer.add((n16 & 0xFF) |> Nat16.toNat8(_)); 46 | buffer.add((n16 >> 8) |> Nat16.toNat8(_)); 47 | }; 48 | case (#Int32(i32)) { 49 | let n = Int32.toNat32(i32); 50 | 51 | buffer.add((n & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 52 | buffer.add(((n >> 8) & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 53 | buffer.add(((n >> 16) & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 54 | buffer.add((n >> 24) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 55 | }; 56 | case (#Int64(i64)) { 57 | let n = Int64.toNat64(i64); 58 | 59 | buffer.add((n & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 60 | buffer.add(((n >> 8) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 61 | buffer.add(((n >> 16) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 62 | buffer.add(((n >> 24) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 63 | buffer.add(((n >> 32) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 64 | buffer.add(((n >> 40) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 65 | buffer.add(((n >> 48) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 66 | buffer.add((n >> 56) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 67 | }; 68 | 69 | case (#Nat(n)) unsigned_leb128(buffer, n); 70 | 71 | case (#Nat8(n)) { 72 | buffer.add(n); 73 | }; 74 | case (#Nat16(n)) { 75 | buffer.add((n & 0xFF) |> Nat16.toNat8(_)); 76 | buffer.add((n >> 8) |> Nat16.toNat8(_)); 77 | }; 78 | case (#Nat32(n)) { 79 | buffer.add((n & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 80 | buffer.add(((n >> 8) & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 81 | buffer.add(((n >> 16) & 0xFF) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 82 | buffer.add((n >> 24) |> Nat32.toNat16(_) |> Nat16.toNat8(_)); 83 | }; 84 | case (#Nat64(n)) { 85 | buffer.add((n & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 86 | buffer.add(((n >> 8) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 87 | buffer.add(((n >> 16) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 88 | buffer.add(((n >> 24) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 89 | buffer.add(((n >> 32) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 90 | buffer.add(((n >> 40) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 91 | buffer.add(((n >> 48) & 0xFF) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 92 | buffer.add((n >> 56) |> Nat64.toNat(_) |> Nat8.fromNat(_)); 93 | 94 | }; 95 | 96 | case (#Float(f64)) { 97 | // let floatX : FloatX.FloatX = FloatX.fromFloat(f64, #f64); 98 | // FloatX.encode(buffer, floatX, #lsb); 99 | }; 100 | case (#Bool(b)) { 101 | buffer.add(if (b) (1) else (0)); 102 | }; 103 | case (#Null) {}; 104 | 105 | case (#Text(t)) { 106 | 107 | let utf8_bytes = Blob.toArray(Text.encodeUtf8(t)); 108 | 109 | var i = 0; 110 | while (i < utf8_bytes.size()) { 111 | buffer.add(utf8_bytes[i]); 112 | i += 1; 113 | }; 114 | 115 | }; 116 | case (#Blob(b)) { 117 | sha256.writeBlob(b); 118 | }; 119 | case (#Principal(p)) { 120 | 121 | let bytes = Blob.toArray(Principal.toBlob(p)); 122 | 123 | var i = 0; 124 | while (i < bytes.size()) { 125 | buffer.add(bytes[i]); 126 | i += 1; 127 | }; 128 | }; 129 | 130 | case (#Array(values)) { 131 | 132 | let hashes = Array.tabulate( 133 | values.size(), 134 | func(i : Nat) : Blob { 135 | candid_hash(buffer, sha256, values[i]); 136 | }, 137 | ); 138 | 139 | for (hash in hashes.vals()) { 140 | let hash_bytes = Blob.toArray(hash); 141 | for (byte in hash_bytes.vals()) { 142 | buffer.add(byte); 143 | }; 144 | }; 145 | }; 146 | case (#Record(records) or #Map(records)) { 147 | let hashes = Buffer.Buffer(8); 148 | label record_hashing for ((key, value) in records.vals()) { 149 | let key_hash = candid_hash(buffer, sha256, #Text(key)); 150 | 151 | let unwrapped_value = switch (value) { 152 | case (#Null) continue record_hashing; 153 | case (#Option(inner_type)) inner_type; 154 | case (value) value; 155 | }; 156 | 157 | let value_hash = candid_hash(buffer, sha256, unwrapped_value); 158 | 159 | let concatenated = Blob.fromArray( 160 | Array.append( 161 | Blob.toArray(key_hash), 162 | Blob.toArray(value_hash), 163 | ) 164 | ); 165 | 166 | hashes.add(concatenated); 167 | }; 168 | 169 | hashes.sort(Blob.compare); 170 | 171 | for (hash in hashes.vals()) { 172 | let hash_bytes = Blob.toArray(hash); 173 | for (byte in hash_bytes.vals()) { 174 | buffer.add(byte); 175 | }; 176 | }; 177 | 178 | }; 179 | case (candid) Debug.trap("oops: " # debug_show (candid)); 180 | }; 181 | 182 | sha256.writeIter(buffer.vals()); 183 | buffer.clear(); 184 | 185 | let resulting_hash = sha256.sum(); 186 | sha256.reset(); 187 | 188 | resulting_hash; 189 | 190 | }; 191 | }; 192 | -------------------------------------------------------------------------------- /src/Candid/ICRC3Value.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Nat8 "mo:base/Nat8"; 3 | import Nat16 "mo:base/Nat16"; 4 | import Nat64 "mo:base/Nat64"; 5 | import Nat32 "mo:base/Nat32"; 6 | import Int8 "mo:base/Int8"; 7 | import Int16 "mo:base/Int16"; 8 | import Int64 "mo:base/Int64"; 9 | import Int32 "mo:base/Int32"; 10 | import Debug "mo:base/Debug"; 11 | import Principal "mo:base/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 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Array.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base/List"; 2 | 3 | import C "mo:parser-combinators/Combinators"; 4 | import P "mo:parser-combinators/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/Blob.mo: -------------------------------------------------------------------------------- 1 | import Blob "mo:base/Blob"; 2 | import Iter "mo:base/Iter"; 3 | import List "mo:base/List"; 4 | 5 | import C "mo:parser-combinators/Combinators"; 6 | import P "mo:parser-combinators/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/Bool.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base/List"; 2 | 3 | import C "mo:parser-combinators/Combinators"; 4 | import P "mo:parser-combinators/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 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Common.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base/Char"; 2 | import Iter "mo:base/Iter"; 3 | import List "mo:base/List"; 4 | import Nat32 "mo:base/Nat32"; 5 | import Text "mo:base/Text"; 6 | 7 | import C "mo:parser-combinators/Combinators"; 8 | import P "mo:parser-combinators/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 | }; -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Float.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base/Float"; 2 | import List "mo:base/List"; 3 | 4 | import C "mo:parser-combinators/Combinators"; 5 | import P "mo:parser-combinators/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 | } -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Int.mo: -------------------------------------------------------------------------------- 1 | import Int "mo:base/Int"; 2 | import List "mo:base/List"; 3 | 4 | import C "mo:parser-combinators/Combinators"; 5 | import P "mo:parser-combinators/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/IntX.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import List "mo:base/List"; 3 | import Int8 "mo:base/Int8"; 4 | import Int16 "mo:base/Int16"; 5 | import Int32 "mo:base/Int32"; 6 | import Int64 "mo:base/Int64"; 7 | 8 | import C "mo:parser-combinators/Combinators"; 9 | import P "mo:parser-combinators/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/Nat.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base/Iter"; 2 | import List "mo:base/List"; 3 | import Nat64 "mo:base/Nat64"; 4 | 5 | import C "mo:parser-combinators/Combinators"; 6 | import P "mo:parser-combinators/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/NatX.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import List "mo:base/List"; 3 | import Nat8 "mo:base/Nat8"; 4 | import Nat16 "mo:base/Nat16"; 5 | import Nat32 "mo:base/Nat32"; 6 | import Nat64 "mo:base/Nat64"; 7 | 8 | import C "mo:parser-combinators/Combinators"; 9 | import P "mo:parser-combinators/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/Option.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base/List"; 2 | 3 | import C "mo:parser-combinators/Combinators"; 4 | import P "mo:parser-combinators/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/List"; 2 | import Principal "mo:base/Principal"; 3 | 4 | import C "mo:parser-combinators/Combinators"; 5 | import P "mo:parser-combinators/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 | -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Record.mo: -------------------------------------------------------------------------------- 1 | import Iter "mo:base/Iter"; 2 | import List "mo:base/List"; 3 | 4 | import C "mo:parser-combinators/Combinators"; 5 | import P "mo:parser-combinators/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/Candid/Text/Parser/Text.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base/Char"; 2 | import Iter "mo:base/Iter"; 3 | import List "mo:base/List"; 4 | import Text "mo:base/Text"; 5 | 6 | import C "mo:parser-combinators/Combinators"; 7 | import P "mo:parser-combinators/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(func (c : Char) : Bool { 41 | c != Char.fromNat32(0x22) and c != '\\'; 42 | }), 43 | C.right( 44 | C.Character.char('\\'), 45 | C.map( 46 | C.Character.oneOf([ 47 | Char.fromNat32(0x22), '\\', '/', 'b', 'f', 'n', 'r', 't', 48 | // TODO: u hex{4} 49 | ]), 50 | func (c : Char) : Char { 51 | switch (c) { 52 | case ('b') { Char.fromNat32(0x08); }; 53 | case ('f') { Char.fromNat32(0x0C); }; 54 | case ('n') { Char.fromNat32(0x0A); }; 55 | case ('r') { Char.fromNat32(0x0D); }; 56 | case ('t') { Char.fromNat32(0x09); }; 57 | case (_) { c; }; 58 | }; 59 | } 60 | ) 61 | ) 62 | ]); 63 | } -------------------------------------------------------------------------------- /src/Candid/Text/Parser/Variant.mo: -------------------------------------------------------------------------------- 1 | import List "mo:base/List"; 2 | 3 | import C "mo:parser-combinators/Combinators"; 4 | import P "mo:parser-combinators/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/lib.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base/Char"; 2 | import Debug "mo:base/Debug"; 3 | import Iter "mo:base/Iter"; 4 | import List "mo:base/List"; 5 | import TrieMap "mo:base/TrieMap"; 6 | 7 | import C "mo:parser-combinators/Combinators"; 8 | import P "mo:parser-combinators/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/Candid/Text/ToText.mo: -------------------------------------------------------------------------------- 1 | import Float "mo:base/Float"; 2 | import Array "mo:base/Array"; 3 | import Text "mo:base/Text"; 4 | import Principal "mo:base/Principal"; 5 | import TrieMap "mo:base/TrieMap"; 6 | 7 | import Itertools "mo:itertools/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; 78 | Bool : Nat8 = 0x7e; 79 | Nat : Nat8 = 0x7d; 80 | Int : Nat8 = 0x7c; 81 | Nat8 : Nat8 = 0x7b; 82 | Nat16 : Nat8 = 0x7a; 83 | Nat32 : Nat8 = 0x79; 84 | Nat64 : Nat8 = 0x78; 85 | Int8 : Nat8 = 0x77; 86 | Int16 : Nat8 = 0x76; 87 | Int32 : Nat8 = 0x75; 88 | Int64 : Nat8 = 0x74; 89 | // Float32 : Nat8 = 0x73; 90 | Float : Nat8 = 0x72; 91 | Text : Nat8 = 0x71; 92 | // Reserved : Nat8 = 0x70; 93 | Empty : Nat8 = 0x6f; 94 | Principal : Nat8 = 0x68; 95 | 96 | // compound types 97 | 98 | Option : Nat8 = 0x6e; 99 | Array : Nat8 = 0x6d; 100 | Record : Nat8 = 0x6c; 101 | Variant : Nat8 = 0x6b; 102 | // Func : Nat8 = 0x6a; 103 | // Service : Nat8 = 0x69; 104 | 105 | }; 106 | 107 | /// Encoding and Decoding options 108 | public type Options = { 109 | 110 | /// #### Encoding Options 111 | /// Contains an array of tuples of the form (old_name, new_name) to rename the record keys. 112 | renameKeys : [(Text, Text)]; 113 | 114 | // convertAllNumbersToFloats : Bool; 115 | 116 | /// Returns #Map instead of #Record supported by the icrc3 spec 117 | use_icrc_3_value_type : Bool; 118 | 119 | /// encodes faster if the complete type is known, but not necessary 120 | /// fails if types are incorrect 121 | /// 122 | /// Must call `Candid.formatCandidTypes` before passing in the types 123 | types : ?[CandidType]; 124 | 125 | /// #### Decoding Options 126 | /// When decoding, you have the option to pass in the Candid variant type 127 | /// and omit the type portion of the candid blob and only pass in the 128 | /// serialized values 129 | blob_contains_only_values : Bool; 130 | 131 | }; 132 | 133 | public type ICRC3Value = { 134 | #Blob : Blob; 135 | #Text : Text; 136 | #Nat : Nat; 137 | #Int : Int; 138 | #Array : [ICRC3Value]; 139 | #Map : [(Text, ICRC3Value)]; 140 | }; 141 | 142 | public let defaultOptions : Options = { 143 | renameKeys = []; 144 | // convertAllNumbersToFloats = false; 145 | use_icrc_3_value_type = false; 146 | 147 | types = null; 148 | 149 | blob_contains_only_values = false; 150 | }; 151 | 152 | }; 153 | -------------------------------------------------------------------------------- /src/Candid/lib.mo: -------------------------------------------------------------------------------- 1 | /// A representation of the Candid format with variants for all possible types. 2 | 3 | import Array "mo:base/Array"; 4 | import Text "mo:base/Text"; 5 | 6 | import Encoder "Blob/Encoder"; 7 | import Decoder "Blob/Decoder"; 8 | import RepIndyHash "Blob/RepIndyHash"; 9 | import CandidUtils "Blob/CandidUtils"; 10 | 11 | import Parser "Text/Parser"; 12 | import ToText "Text/ToText"; 13 | 14 | import T "Types"; 15 | import Utils "../Utils"; 16 | import ICRC3Value "ICRC3Value"; 17 | 18 | import Map "mo:map/Map"; 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 type CandidType = T.CandidType; 29 | 30 | /// Converts a motoko value to a [Candid](#Candid) value 31 | public let { encode; encodeOne } = Encoder; 32 | 33 | public let repIndyHash = RepIndyHash.hash; 34 | 35 | /// Converts a [Candid](#Candid) value to a motoko value 36 | public let { decode } = Decoder; 37 | 38 | public func fromText(t : Text) : [Candid] { 39 | Parser.parse(t); 40 | }; 41 | 42 | public let { toText } = ToText; 43 | 44 | /// Formats a user provided Candid type 45 | /// It is required to format the Candid type before passing it as an option to the Candid encoder/decoder 46 | /// 47 | /// 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 48 | /// Failure to do so will result in unexpected behavior 49 | public func formatCandidType(c : [CandidType], opt_rename_keys : ?[(Text, Text)]) : [CandidType] { 50 | let renaming_map = Map.new(); 51 | 52 | switch (opt_rename_keys) { 53 | case (?rename_keys) { 54 | for ((prev, new) in rename_keys.vals()) { 55 | ignore Map.put(renaming_map, thash, prev, new); 56 | }; 57 | }; 58 | case (_) {}; 59 | }; 60 | 61 | Array.map( 62 | c, 63 | func(c : CandidType) : CandidType { 64 | CandidUtils.format_candid_type(c, renaming_map); 65 | }, 66 | ); 67 | 68 | }; 69 | 70 | public let concatKeys = Utils.concatKeys; 71 | 72 | /// Converts an array of ICRC3Value values to [Candid](#Candid) values 73 | public func fromICRC3Value(icrc3_values : [T.ICRC3Value]) : [Candid] { 74 | ICRC3Value.fromICRC3Value(icrc3_values); 75 | }; 76 | 77 | /// Converts an array of [Candid](#Candid) values to ICRC3Value values 78 | public func toICRC3Value(candid_values : [Candid]) : [T.ICRC3Value] { 79 | ICRC3Value.toICRC3Value(candid_values); 80 | }; 81 | 82 | public type ICRC3Value = T.ICRC3Value; 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /src/JSON/FromText.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Result "mo:base/Result"; 3 | import Text "mo:base/Text"; 4 | import Int "mo:base/Int"; 5 | 6 | import JSON "mo:json-float/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 | -------------------------------------------------------------------------------- /src/JSON/ToText.mo: -------------------------------------------------------------------------------- 1 | import Buffer "mo:base/Buffer"; 2 | import Result "mo:base/Result"; 3 | import Text "mo:base/Text"; 4 | 5 | import JSON "mo:json-float/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/JSON/lib.mo: -------------------------------------------------------------------------------- 1 | /// A module for converting between JSON and Motoko values. 2 | 3 | import JSON "mo:json-float/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 | -------------------------------------------------------------------------------- /src/UrlEncoded/FromText.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Blob "mo:base/Blob"; 3 | import Buffer "mo:base/Buffer"; 4 | import Char "mo:base/Char"; 5 | import Debug "mo:base/Debug"; 6 | import Result "mo:base/Result"; 7 | import TrieMap "mo:base/TrieMap"; 8 | import Nat "mo:base/Nat"; 9 | import Text "mo:base/Text"; 10 | import Iter "mo:base/Iter"; 11 | import Option "mo:base/Option"; 12 | 13 | import Itertools "mo:itertools/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 | -------------------------------------------------------------------------------- /src/UrlEncoded/Parser.mo: -------------------------------------------------------------------------------- 1 | import Char "mo:base/Char"; 2 | import Iter "mo:base/Iter"; 3 | import Float "mo:base/Float"; 4 | import List "mo:base/List"; 5 | import Nat32 "mo:base/Nat32"; 6 | 7 | import C "mo:parser-combinators/Combinators"; 8 | import P "mo:parser-combinators/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 | -------------------------------------------------------------------------------- /src/UrlEncoded/ToText.mo: -------------------------------------------------------------------------------- 1 | import Result "mo:base/Result"; 2 | import Nat "mo:base/Nat"; 3 | import Nat32 "mo:base/Nat32"; 4 | import Text "mo:base/Text"; 5 | import TrieMap "mo:base/TrieMap"; 6 | import Iter "mo:base/Iter"; 7 | import Float "mo:base/Float"; 8 | import Principal "mo:base/Principal"; 9 | import Debug "mo:base/Debug"; 10 | 11 | import itertools "mo:itertools/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Utils.mo: -------------------------------------------------------------------------------- 1 | import Array "mo:base/Array"; 2 | import Char "mo:base/Char"; 3 | import Order "mo:base/Order"; 4 | import Float "mo:base/Float"; 5 | import Text "mo:base/Text"; 6 | import Iter "mo:base/Iter"; 7 | import Nat64 "mo:base/Nat64"; 8 | import Nat8 "mo:base/Nat8"; 9 | import Int "mo:base/Int"; 10 | import Buffer "mo:base/Buffer"; 11 | import Result "mo:base/Result"; 12 | 13 | import Prelude "mo:base/Prelude"; 14 | import Nat32 "mo:base/Nat32"; 15 | import Debug "mo:base/Debug"; 16 | import Itertools "mo:itertools/Iter"; 17 | 18 | module { 19 | 20 | type Iter = Iter.Iter; 21 | type Buffer = Buffer.Buffer; 22 | type Result = Result.Result; 23 | 24 | public func reverse_order(fn : (A, A) -> Order.Order) : (A, A) -> Order.Order { 25 | func(a : A, b : A) : Order.Order { 26 | switch (fn(a, b)) { 27 | case (#less) #greater; 28 | case (#equal) #equal; 29 | case (#greater) #less; 30 | }; 31 | }; 32 | }; 33 | 34 | public func array_slice(arr : [A], start : Nat, end : Nat) : [A] { 35 | Array.tabulate( 36 | end - start, 37 | func(i : Nat) = arr[start + i], 38 | ); 39 | }; 40 | 41 | public func concatKeys(keys : [[Text]]) : [Text] { 42 | Iter.toArray( 43 | Itertools.flattenArray(keys) 44 | ); 45 | }; 46 | 47 | public func sized_iter_to_array(iter : Iter, size : Nat) : [A] { 48 | Array.tabulate( 49 | size, 50 | func(i : Nat) { 51 | switch (iter.next()) { 52 | case (?x) x; 53 | case (_) Prelude.unreachable(); 54 | }; 55 | }, 56 | ); 57 | }; 58 | 59 | public func send_error(res : Result) : Result { 60 | switch (res) { 61 | case (#ok(_)) Prelude.unreachable(); 62 | case (#err(errorMsg)) #err(errorMsg); 63 | }; 64 | }; 65 | 66 | public func subText(text : Text, start : Nat, end : Nat) : Text { 67 | Itertools.toText( 68 | Itertools.skip( 69 | Itertools.take(text.chars(), end), 70 | start, 71 | ) 72 | ); 73 | }; 74 | 75 | public func cmpRecords(a : (Text, Any), b : (Text, Any)) : Order.Order { 76 | Text.compare(a.0, b.0); 77 | }; 78 | 79 | public func stripStart(text : Text, prefix : Text.Pattern) : Text { 80 | switch (Text.stripStart(text, prefix)) { 81 | case (?t) t; 82 | case (_) text; 83 | }; 84 | }; 85 | 86 | public func log2(n : Float) : Float { 87 | Float.log(n) / Float.log(2); 88 | }; 89 | 90 | public func isHash(key : Text) : Bool { 91 | Itertools.all( 92 | key.chars(), 93 | func(c : Char) : Bool { 94 | c == '_' or Char.isDigit(c); 95 | }, 96 | ); 97 | }; 98 | 99 | public func text_to_nat32(text : Text) : Nat32 { 100 | Itertools.fold( 101 | text.chars(), 102 | 0 : Nat32, 103 | func(acc : Nat32, c : Char) : Nat32 { 104 | if (c == '_') { 105 | acc; 106 | } else { 107 | acc * 10 + Char.toNat32(c) - Char.toNat32('0'); 108 | }; 109 | }, 110 | ); 111 | }; 112 | 113 | public func text_to_nat(text : Text) : Nat { 114 | Itertools.fold( 115 | text.chars(), 116 | 0 : Nat, 117 | func(acc : Nat, c : Char) : Nat { 118 | if (c == '_') { 119 | acc; 120 | } else { 121 | acc * 10 + Nat32.toNat(Char.toNat32(c) - Char.toNat32('0')); 122 | }; 123 | }, 124 | ); 125 | }; 126 | 127 | public func text_is_number(text : Text) : Bool { 128 | Itertools.all( 129 | text.chars(), 130 | func(c : Char) : Bool { 131 | Char.isDigit(c) or c == '_'; 132 | }, 133 | ); 134 | }; 135 | 136 | type AddToBuffer = { 137 | add : (A) -> (); 138 | }; 139 | 140 | // https://en.wikipedia.org/wiki/LEB128 141 | // limited to 64-bit unsigned integers 142 | // more performant than the general unsigned_leb128 143 | public func unsigned_leb128_64(buffer : AddToBuffer, n : Nat) { 144 | var n64 : Nat64 = Nat64.fromNat(n); 145 | 146 | loop { 147 | var byte = n64 & 0x7F |> Nat64.toNat(_) |> Nat8.fromNat(_); 148 | n64 >>= 7; 149 | 150 | if (n64 > 0) byte := (byte | 0x80); 151 | buffer.add(byte); 152 | 153 | } while (n64 > 0); 154 | }; 155 | 156 | public func unsigned_leb128(buffer : AddToBuffer, n : Nat) { 157 | let nat64_bound = 18_446_744_073_709_551_616; 158 | 159 | if (n < nat64_bound) { 160 | // more performant than the general unsigned_leb128 161 | var n64 : Nat64 = Nat64.fromNat(n); 162 | 163 | loop { 164 | var byte = n64 & 0x7F |> Nat64.toNat(_) |> Nat8.fromNat(_); 165 | n64 >>= 7; 166 | 167 | if (n64 > 0) byte := (byte | 0x80); 168 | buffer.add(byte); 169 | 170 | } while (n64 > 0); 171 | 172 | return; 173 | }; 174 | 175 | var num = n; 176 | 177 | loop { 178 | var byte = num % 0x80 |> Nat8.fromNat(_); 179 | num /= 0x80; 180 | 181 | if (num > 0) byte := (byte | 0x80); 182 | buffer.add(byte); 183 | 184 | } while (num > 0); 185 | }; 186 | 187 | public func signed_leb128_64(buffer : AddToBuffer, num : Int) { 188 | 189 | let is_negative = num < 0; 190 | 191 | // because we extract bytes in multiple of 7 bits 192 | // to extract the 64th bit we pad the number with 6 extra bits 193 | // to make it 70 which is a multiple of 7 194 | // however, because nat64 is bounded by 64 bits 195 | // the extra 6 bits are not flipped which leads to an incorrect result 196 | 197 | let nat64_bound = 18_446_744_073_709_551_616; 198 | 199 | if (Int.abs(num) < nat64_bound) { 200 | var n64 = Nat64.fromNat(Int.abs(num)); 201 | 202 | let bit_length = Nat64.toNat(64 - Nat64.bitcountLeadingZero(n64)); 203 | var n7bits = (bit_length / 7) + 1; 204 | if (is_negative) n64 := Nat64.fromNat(Int.abs(num) - 1); 205 | 206 | loop { 207 | var word = if (is_negative) ^n64 else n64; 208 | var byte = word & 0x7F |> Nat64.toNat(_) |> Nat8.fromNat(_); 209 | n64 >>= 7; 210 | n7bits -= 1; 211 | 212 | if (n7bits > 0) byte := (byte | 0x80); 213 | buffer.add(byte); 214 | 215 | } while (n7bits > 0); 216 | return; 217 | }; 218 | 219 | Debug.trap("numbers greater than 18_446_744_073_709_551_616 are not supported"); 220 | 221 | var n = Int.abs(num); 222 | 223 | loop { 224 | var word = if (is_negative) ^Nat8.fromNat(n % 0x80) & 0x7f else Nat8.fromNat(n % 0x80) & 0x7f; 225 | var byte = word; 226 | n /= 0x80; 227 | 228 | if (n > 0) { byte := (byte | 0x80) } else { 229 | if (is_negative) byte := byte +% 1; 230 | }; 231 | buffer.add(byte); 232 | 233 | } while (n > 0); 234 | 235 | // buffer.add(if (is_negative) 0x7f else 0x00); 236 | 237 | }; 238 | 239 | // public func signed_leb128(buffer : AddToBuffer, num : Int) { 240 | // let nat64_bound = 18_446_744_073_709_551_616; 241 | 242 | // if (num < nat64_bound and num > -nat64_bound) return signed_leb128_64(buffer, num); 243 | 244 | // var n = num; 245 | // let is_negative = n < 0; 246 | 247 | // }; 248 | 249 | public class ReusableBuffer(init_capacity : Nat) { 250 | var elems : [var ?A] = Array.init(init_capacity, null); 251 | var count : Nat = 0; 252 | 253 | public func size() : Nat = count; 254 | 255 | public func add(elem : A) { 256 | if (count == elems.size()) { 257 | elems := Array.tabulateVar( 258 | elems.size() * 2, 259 | func(i : Nat) : ?A { 260 | if (i < count) { 261 | elems[i]; 262 | } else { 263 | null; 264 | }; 265 | }, 266 | ); 267 | }; 268 | 269 | elems[count] := ?elem; 270 | count += 1; 271 | }; 272 | 273 | public func clear() { 274 | count := 0; 275 | }; 276 | 277 | public func get(i : Nat) : A { 278 | switch (elems[i]) { 279 | case (?elem) elem; 280 | case (null) Debug.trap "Index out of bounds"; 281 | }; 282 | }; 283 | 284 | public func vals() : Iter.Iter { 285 | var i = 0; 286 | 287 | object { 288 | public func next() : ?A { 289 | if (i < count) { 290 | let res = elems[i]; 291 | i += 1; 292 | res; 293 | } else { 294 | null; 295 | }; 296 | }; 297 | }; 298 | }; 299 | }; 300 | 301 | }; 302 | -------------------------------------------------------------------------------- /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/CBOR.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Array "mo:base/Array"; 3 | import Blob "mo:base/Blob"; 4 | import Debug "mo:base/Debug"; 5 | import Iter "mo:base/Iter"; 6 | import Principal "mo:base/Principal"; 7 | import Text "mo:base/Text"; 8 | 9 | import { test; suite } "mo:test"; 10 | 11 | import { CBOR } "../src"; 12 | 13 | suite( 14 | "CBOR Test", 15 | func() { 16 | test("options", func() { 17 | let opt_nat_null: ?Nat = null; 18 | let opt_nat : ?Nat = ?123; 19 | let opt_text_null: ?Text = null; 20 | let opt_text : ?Text = ?"hello"; 21 | 22 | let opt_nat_null_candid = to_candid(opt_nat_null); 23 | let opt_nat_candid = to_candid(opt_nat); 24 | let opt_text_null_candid = to_candid(opt_text_null); 25 | let opt_text_candid = to_candid(opt_text); 26 | 27 | let #ok(opt_nat_null_cbor) = CBOR.encode(opt_nat_null_candid, [], null); 28 | let #ok(opt_nat_cbor) = CBOR.encode(opt_nat_candid, [], null); 29 | let #ok(opt_text_null_cbor) = CBOR.encode(opt_text_null_candid, [], null); 30 | let #ok(opt_text_cbor) = CBOR.encode(opt_text_candid, [], null); 31 | 32 | let #ok(opt_nat_null_candid2) = CBOR.decode(opt_nat_null_cbor, null); 33 | let #ok(opt_nat_candid2) = CBOR.decode(opt_nat_cbor, null); 34 | let #ok(opt_text_null_candid2) = CBOR.decode(opt_text_null_cbor, null); 35 | let #ok(opt_text_candid2) = CBOR.decode(opt_text_cbor, null); 36 | 37 | assert opt_nat_null_candid != opt_nat_null_candid2; 38 | assert opt_nat_candid != opt_nat_candid2; 39 | assert opt_text_null_candid != opt_text_null_candid2; 40 | assert opt_text_candid != opt_text_candid2; 41 | 42 | let ?opt_nat_null2 : ?(?Nat) = from_candid(opt_nat_null_candid2); 43 | let ?opt_nat2 : ?(?Nat) = from_candid(opt_nat_candid2); 44 | let ?opt_text_null2 : ?(?Text) = from_candid(opt_text_null_candid2); 45 | let ?opt_text2 : ?(?Text) = from_candid(opt_text_candid2); 46 | 47 | assert opt_nat_null2 == opt_nat_null; 48 | assert opt_nat2 == opt_nat; 49 | assert opt_text_null2 == opt_text_null; 50 | assert opt_text2 == opt_text; 51 | 52 | }); 53 | 54 | test( 55 | "primitives", 56 | func() { 57 | 58 | let nat : Nat = 123; 59 | let int : Int = -123; 60 | let float : Float = 123.456; 61 | let bool : Bool = true; 62 | let text: Text = "hello"; 63 | let blob: Blob = "\01\02\03"; 64 | let _null: Null = null; 65 | let empty = (); 66 | let list: [Nat] = [1, 2, 3]; 67 | let record = { a = 1; b = 2; }; 68 | let principal = Principal.fromText("bkyz2-fmaaa-aaaaa-qaaaq-cai"); 69 | 70 | let nat_candid = to_candid(nat); 71 | let int_candid = to_candid(int); 72 | let float_candid = to_candid(float); 73 | let bool_candid = to_candid(bool); 74 | let text_candid = to_candid(text); 75 | let blob_candid = to_candid(blob); 76 | let null_candid = to_candid(_null); 77 | let empty_candid = to_candid(empty); 78 | let list_candid = to_candid(list); 79 | let record_candid = to_candid(record); 80 | let principal_candid = to_candid(principal); 81 | 82 | let #ok(nat_cbor) = CBOR.encode(nat_candid, [], null); 83 | let #ok(int_cbor) = CBOR.encode(int_candid, [], null); 84 | let #ok(float_cbor) = CBOR.encode(float_candid, [], null); 85 | let #ok(bool_cbor) = CBOR.encode(bool_candid, [], null); 86 | let #ok(text_cbor) = CBOR.encode(text_candid, [], null); 87 | let #ok(blob_cbor) = CBOR.encode(blob_candid, [], null); 88 | let #ok(null_cbor) = CBOR.encode(null_candid, [], null); 89 | let #ok(empty_cbor) = CBOR.encode(empty_candid, [], null); 90 | let #ok(list_cbor) = CBOR.encode(list_candid, [], null); 91 | let #ok(record_cbor) = CBOR.encode(record_candid, ["a", "b"], null); 92 | let #ok(principal_cbor) = CBOR.encode(principal_candid, [], null); 93 | 94 | let self_describe_tag : Blob = "\D9\D9\F7"; 95 | 96 | func blob_concat(b1: Blob, b2: Blob): Blob = Blob.fromArray(Array.append(Blob.toArray(b1), Blob.toArray(b2))); 97 | func sdt(blob: Blob): Blob = blob_concat(self_describe_tag, blob); 98 | func strip(text: Text, to_strip: Text): Text = Text.replace(text, #text(to_strip), ""); 99 | 100 | // cbor encodings from https://cbor.me/ 101 | assert nat_cbor == sdt("\18\7B"); 102 | assert int_cbor == sdt("\38\7A"); 103 | assert float_cbor == sdt("\FB\40\5E\DD\2F\1A\9F\BE\77"); 104 | assert bool_cbor == sdt("\F5"); 105 | assert text_cbor == sdt("\65\68\65\6C\6C\6F"); 106 | assert blob_cbor == sdt("\43\01\02\03"); 107 | assert null_cbor == sdt("\F6"); 108 | // assert empty_cbor == sdt("\F7"); // mapped to undefined 109 | assert list_cbor == sdt("\83\01\02\03"); 110 | assert record_cbor == sdt("\A2\61\61\01\61\62\02"); 111 | assert strip(debug_show(principal_cbor), "\"") == strip((debug_show ("\D9\D9\F7\4A": Blob) # debug_show(Principal.toBlob(principal))), "\""); 112 | 113 | let #ok(nat_candid2) = CBOR.decode(nat_cbor, null); 114 | let #ok(int_candid2) = CBOR.decode(int_cbor, null); 115 | let #ok(float_candid2) = CBOR.decode(float_cbor, null); 116 | let #ok(bool_candid2) = CBOR.decode(bool_cbor, null); 117 | let #ok(text_candid2) = CBOR.decode(text_cbor, null); 118 | let #ok(blob_candid2) = CBOR.decode(blob_cbor, null); 119 | let #ok(null_candid2) = CBOR.decode(null_cbor, null); 120 | let #ok(empty_candid2) = CBOR.decode(empty_cbor, null); 121 | let #ok(list_candid2) = CBOR.decode(list_cbor, null); 122 | let #ok(record_candid2) = CBOR.decode(record_cbor, null); 123 | let #ok(principal_candid2) = CBOR.decode(principal_cbor, null); 124 | 125 | assert nat_candid == nat_candid2; 126 | assert int_candid == int_candid2; 127 | assert float_candid == float_candid2; 128 | assert bool_candid == bool_candid2; 129 | assert text_candid == text_candid2; 130 | assert blob_candid == blob_candid2; 131 | assert null_candid == null_candid2; 132 | assert empty_candid == empty_candid2; 133 | assert list_candid == list_candid2; 134 | assert record_candid == record_candid2; 135 | // assert principal_candid == principal_candid2; 136 | 137 | let ?nat2 : ?Nat = from_candid(nat_candid2); 138 | let ?int2 : ?Int = from_candid(int_candid2); 139 | let ?float2 : ?Float = from_candid(float_candid2); 140 | let ?bool2 : ?Bool = from_candid(bool_candid2); 141 | let ?text2 : ?Text = from_candid(text_candid2); 142 | let ?blob2 : ?Blob = from_candid(blob_candid2); 143 | let ?null2 : ?Null = from_candid(null_candid2); 144 | let ?empty2 : ?() = from_candid(empty_candid2); 145 | let ?list2 : ?[Nat] = from_candid(list_candid2); 146 | let ?record2 : ?{a: Nat; b: Nat;} = from_candid(record_candid2); 147 | let ?principal_as_blob : ?Blob = from_candid(principal_candid2); 148 | 149 | assert nat2 == nat; 150 | assert int2 == int; 151 | assert float2 == float; 152 | assert bool2 == bool; 153 | assert text2 == text; 154 | assert blob2 == blob; 155 | assert null2 == _null; 156 | assert empty2 == empty; 157 | assert list2 == list; 158 | assert record2 == record; 159 | assert principal_as_blob == Principal.toBlob(principal); 160 | 161 | }, 162 | ); 163 | 164 | 165 | }, 166 | ); 167 | -------------------------------------------------------------------------------- /tests/Candid.ICRC3.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Array "mo:base/Array"; 3 | import Blob "mo:base/Blob"; 4 | import Debug "mo:base/Debug"; 5 | import Iter "mo:base/Iter"; 6 | import Principal "mo:base/Principal"; 7 | import Text "mo:base/Text"; 8 | 9 | import { test; suite } "mo:test"; 10 | 11 | import Serde "../src"; 12 | 13 | let { Candid } = Serde; 14 | 15 | suite( 16 | "Candid ICRC3 compatability Test", 17 | func() { 18 | test( 19 | "#Map", 20 | func() { 21 | let record = { a = 1; b = 2 }; 22 | 23 | let record_candid_blob = to_candid (record); 24 | 25 | let options = { 26 | Candid.defaultOptions with use_icrc_3_value_type = true; 27 | }; 28 | let #ok(record_candid) = Candid.decode(record_candid_blob, ["a", "b"], ?options); 29 | 30 | assert record_candid[0] == #Map([ 31 | ("a", #Nat(1)), 32 | ("b", #Nat(2)), 33 | ]); 34 | 35 | let #ok(record_candid_blob2) = Candid.encode(record_candid, ?options); 36 | 37 | assert record_candid_blob == record_candid_blob2; 38 | 39 | let ?record2 : ?{ a : Nat; b : Nat } = from_candid (record_candid_blob2); 40 | 41 | assert record2 == record; 42 | 43 | }, 44 | ); 45 | 46 | }, 47 | 48 | ); 49 | 50 | suite( 51 | "Connvert between motoko and ICRC3", 52 | func() { 53 | test( 54 | "motoko -> ICRC3", 55 | func() { 56 | 57 | type User = { id : Nat; name : Text }; 58 | 59 | let user : User = { name = "bar"; id = 112 }; 60 | 61 | let blob = to_candid (user); 62 | let #ok(candid_values) = Candid.decode(blob, ["name", "id"], null); 63 | let icrc3_values = Candid.toICRC3Value(candid_values); 64 | 65 | assert icrc3_values[0] == #Map([ 66 | ("id", #Nat(112)), 67 | ("name", #Text("bar")), 68 | ]); 69 | }, 70 | ); 71 | 72 | test( 73 | "ICRC3 -> motoko", 74 | func() { 75 | type User = { name : Text; id : Nat }; 76 | 77 | let icrc3 : Serde.ICRC3Value = #Map([ 78 | ("id", #Nat(112)), 79 | ("name", #Text("bar")), 80 | ]); 81 | 82 | let candid_values = Candid.fromICRC3Value([icrc3]); 83 | 84 | let #ok(blob) = Candid.encode(candid_values, null); 85 | let user : ?User = from_candid (blob); 86 | 87 | assert user == ?{ name = "bar"; id = 112 }; 88 | }, 89 | ); 90 | }, 91 | 92 | ); 93 | -------------------------------------------------------------------------------- /tests/Candid.Large.test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Iter "mo:base/Iter"; 3 | import Debug "mo:base/Debug"; 4 | import Prelude "mo:base/Prelude"; 5 | import Text "mo:base/Text"; 6 | import Char "mo:base/Char"; 7 | import Buffer "mo:base/Buffer"; 8 | 9 | import Fuzz "mo:fuzz"; 10 | import Itertools "mo:itertools/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/JSON.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Blob "mo:base/Blob"; 3 | import Debug "mo:base/Debug"; 4 | import Iter "mo:base/Iter"; 5 | import Nat "mo:base/Nat"; 6 | 7 | import ActorSpec "./utils/ActorSpec"; 8 | import { test; suite } "mo:test"; 9 | 10 | import { Candid; JSON } "../src"; 11 | 12 | let { 13 | assertAllTrue; 14 | } = ActorSpec; 15 | 16 | type User = { 17 | name : Text; 18 | id : ?Int; 19 | }; 20 | 21 | suite( 22 | "JSON fromText()", 23 | func() { 24 | suite( 25 | "float type", 26 | func() { 27 | test( 28 | "2 dp", 29 | func() { 30 | let text = "123.45"; 31 | let #ok(blob) = JSON.fromText(text, null); 32 | let val : ?Float = from_candid (blob); 33 | 34 | assert val == ?123.45; 35 | }, 36 | ); 37 | test( 38 | "8 dp", 39 | func() { 40 | let text = "123.123456789"; 41 | let #ok(blob) = JSON.fromText(text, null); 42 | let val : ?Float = from_candid (blob); 43 | 44 | assert val == ?123.123456789; 45 | }, 46 | ); 47 | 48 | test( 49 | "negative", 50 | func() { 51 | let text = "-123.123456789"; 52 | let #ok(blob) = JSON.fromText(text, null); 53 | let val : ?Float = from_candid (blob); 54 | 55 | assert val == ?-123.123456789; 56 | }, 57 | ); 58 | }, 59 | ); 60 | test( 61 | "record type", 62 | func() { 63 | let text = "{\"name\": \"Tomi\", \"id\": 32}"; 64 | let #ok(blob) = JSON.fromText(text, null); 65 | let user : ?User = from_candid (blob); 66 | 67 | assert user == ?{ name = "Tomi"; id = ?32 }; 68 | }, 69 | ); 70 | test( 71 | "variant types", 72 | func() { 73 | 74 | type Variant = { 75 | #text : Text; 76 | #nat : Nat; 77 | #bool : Bool; 78 | #record : { site : Text }; 79 | #user : User; 80 | #array : [Nat]; 81 | }; 82 | 83 | let text = "{\"#text\": \"hello\"}"; 84 | let nat = "{\"#nat\": 123}"; 85 | let bool = "{\"#bool\": true }"; 86 | let record = "{\"#record\": {\"site\": \"github\"}}"; 87 | let array = "{\"#array\": [1, 2, 3] }"; 88 | 89 | let #ok(text_blob) = JSON.fromText(text, null); 90 | let #ok(nat_blob) = JSON.fromText(nat, null); 91 | let #ok(bool_blob) = JSON.fromText(bool, null); 92 | let #ok(record_blob) = JSON.fromText(record, null); 93 | let #ok(array_blob) = JSON.fromText(array, null); 94 | 95 | let text_val : ?Variant = from_candid (text_blob); 96 | let nat_val : ?Variant = from_candid (nat_blob); 97 | let bool_val : ?Variant = from_candid (bool_blob); 98 | let record_val : ?Variant = from_candid (record_blob); 99 | let array_val : ?Variant = from_candid (array_blob); 100 | 101 | assert assertAllTrue([ 102 | text_val == ? #text("hello"), 103 | nat_val == ? #nat(123), 104 | bool_val == ? #bool(true), 105 | record_val == ? #record({ 106 | site = "github"; 107 | }), 108 | array_val == ? #array([1, 2, 3]), 109 | ]); 110 | }, 111 | ); 112 | test( 113 | "multi-dimensional arrays", 114 | func() { 115 | let arr2 = "[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]"; 116 | let arr3 = "[[[\"hello\", \"world\"], [\"foo\", \"bar\"]], [[\"hello\", \"world\"], [\"foo\", \"bar\"]], [[\"hello\", \"world\"], [\"foo\", \"bar\"]]]"; 117 | 118 | let #ok(arr2_blob) = JSON.fromText(arr2, null); 119 | let #ok(arr3_blob) = JSON.fromText(arr3, null); 120 | 121 | let arr2_encoded : ?[[Nat]] = from_candid (arr2_blob); 122 | let arr3_encoded : ?[[[Text]]] = from_candid (arr3_blob); 123 | 124 | assert assertAllTrue([ 125 | arr2_encoded == ?[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]], 126 | arr3_encoded == ?[ 127 | [["hello", "world"], ["foo", "bar"]], 128 | [["hello", "world"], ["foo", "bar"]], 129 | [["hello", "world"], ["foo", "bar"]], 130 | ], 131 | ]); 132 | }, 133 | ); 134 | test( 135 | "renaming record fields", 136 | func() { 137 | // type Original = { 138 | // label : Nat; 139 | // query : Text; 140 | // }; 141 | 142 | type UserData = { 143 | account_label : Nat; 144 | user_query : Text; 145 | }; 146 | 147 | let text = "{\"label\": 123, \"query\": \"?user_id=12&address=2014%20Forest%20Hill%20Drive\"}"; 148 | 149 | let options = { 150 | Candid.defaultOptions with 151 | renameKeys = [("label", "account_label"), ("query", "user_query")]; 152 | }; 153 | 154 | let #ok(blob) = JSON.fromText(text, ?options); 155 | 156 | let user : ?UserData = from_candid (blob); 157 | assert user == ?{ 158 | account_label = 123; 159 | user_query = "?user_id=12&address=2014%20Forest%20Hill%20Drive"; 160 | }; 161 | }, 162 | ); 163 | }, 164 | ); 165 | 166 | suite( 167 | "JSON toText()", 168 | func() { 169 | test( 170 | "float", 171 | func() { 172 | let float : Float = 123.123456789; 173 | let blob = to_candid (float); 174 | let (jsonText) = JSON.toText(blob, [], null); 175 | 176 | assert jsonText == #ok("123.12"); 177 | }, 178 | ); 179 | test( 180 | "toText()", 181 | func() { 182 | let user = { name = "Tomi"; id = null }; 183 | let blob = to_candid (user); 184 | let (jsonText) = JSON.toText(blob, ["name", "id"], null); 185 | 186 | assert jsonText == #ok("{\"id\": null, \"name\": \"Tomi\"}"); 187 | }, 188 | ); 189 | test( 190 | "variant types", 191 | func() { 192 | type Variant = { 193 | #text : Text; 194 | #nat : Nat; 195 | #bool : Bool; 196 | #record : { site : Text }; 197 | #array : [Nat]; 198 | }; 199 | 200 | let text = #text("hello"); 201 | let nat = #nat(123); 202 | let bool = #bool(true); 203 | let record = #record({ site = "github" }); 204 | let array = #array([1, 2, 3]); 205 | 206 | let text_blob = to_candid (text); 207 | let nat_blob = to_candid (nat); 208 | let bool_blob = to_candid (bool); 209 | let record_blob = to_candid (record); 210 | let array_blob = to_candid (array); 211 | 212 | let text_json = JSON.toText(text_blob, ["#text"], null); 213 | let nat_json = JSON.toText(nat_blob, ["#nat"], null); 214 | let bool_json = JSON.toText(bool_blob, ["bool"], null); 215 | let record_json = JSON.toText(record_blob, ["record", "site"], null); 216 | let array_json = JSON.toText(array_blob, ["array"], null); 217 | 218 | assert assertAllTrue([ 219 | text_json == #ok("{\"#text\": \"hello\"}"), 220 | nat_json == #ok("{\"#nat\": 123}"), 221 | bool_json == #ok("{\"#bool\": true}"), 222 | record_json == #ok("{\"#record\": {\"site\": \"github\"}}"), 223 | array_json == #ok("{\"#array\": [1, 2, 3]}"), 224 | ]); 225 | }, 226 | ); 227 | test( 228 | "multi-dimensional arrays", 229 | func() { 230 | let arr2 : [[Nat]] = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]; 231 | 232 | let arr3 : [[[Text]]] = [ 233 | [["hello", "world"], ["foo", "bar"]], 234 | [["hello", "world"], ["foo", "bar"]], 235 | [["hello", "world"], ["foo", "bar"]], 236 | ]; 237 | 238 | assert assertAllTrue([ 239 | JSON.toText(to_candid (arr2), [], null) == #ok("[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11]]"), 240 | JSON.toText(to_candid (arr3), [], null) == #ok("[[[\"hello\", \"world\"], [\"foo\", \"bar\"]], [[\"hello\", \"world\"], [\"foo\", \"bar\"]], [[\"hello\", \"world\"], [\"foo\", \"bar\"]]]"), 241 | ]); 242 | }, 243 | ); 244 | test( 245 | "renaming record fields", 246 | func() { 247 | // type Original = { 248 | // label : Nat; // reserved keyword that is renamed to account_label 249 | // query : Text; // reserved keyword that is renamed to user_query 250 | // }; 251 | 252 | type UserData = { 253 | account_label : Nat; 254 | user_query : Text; 255 | }; 256 | 257 | let UserDataKeys = ["account_label", "user_query"]; 258 | let options = { 259 | Candid.defaultOptions with 260 | renameKeys = [("account_label", "label"), ("user_query", "query")]; 261 | }; 262 | 263 | let data : UserData = { 264 | account_label = 123; 265 | user_query = "?user_id=12&address=2014%20Forest%20Hill%20Drive"; 266 | }; 267 | let blob = to_candid (data); 268 | let jsonText = JSON.toText(blob, UserDataKeys, ?options); 269 | 270 | assert jsonText == #ok("{\"query\": \"?user_id=12&address=2014%20Forest%20Hill%20Drive\", \"label\": 123}"); 271 | }, 272 | ); 273 | }, 274 | ); 275 | -------------------------------------------------------------------------------- /tests/RepIndyHash.Test.mo: -------------------------------------------------------------------------------- 1 | import Blob "mo:base/Blob"; 2 | import Debug "mo:base/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 | 29 | test( 30 | "#Int", 31 | func() { 32 | let candid_record : Candid.Candid = #Int(42); 33 | let icrc3_value_record : RepIndyHash.Value = #Int(42); 34 | 35 | assert validate_hash(candid_record, icrc3_value_record); 36 | 37 | }, 38 | ); 39 | 40 | test( 41 | "#Text", 42 | func() { 43 | let candid_record : Candid.Candid = #Text("hello"); 44 | let icrc3_value_record : RepIndyHash.Value = #Text("hello"); 45 | 46 | assert validate_hash(candid_record, icrc3_value_record); 47 | }, 48 | ); 49 | 50 | test( 51 | "#Blob", 52 | func() { 53 | let candid_record : Candid.Candid = #Blob("\00\01\02"); 54 | let icrc3_value_record : RepIndyHash.Value = #Blob("\00\01\02"); 55 | 56 | assert validate_hash(candid_record, icrc3_value_record); 57 | }, 58 | ); 59 | 60 | test( 61 | "#Array", 62 | func() { 63 | let candid_record : Candid.Candid = #Array([#Text("hello"), #Text("world")]); 64 | let icrc3_value_record : RepIndyHash.Value = #Array([#Text("hello"), #Text("world")]); 65 | 66 | assert validate_hash(candid_record, icrc3_value_record); 67 | }, 68 | ); 69 | 70 | test( 71 | "#Record/#Map", 72 | func() { 73 | let candid_record : Candid.Candid = #Map([ 74 | ("a", #Nat(1)), 75 | ("b", #Array([#Text("hello"), #Text("world")])), 76 | ("c", #Blob("\00\01\02")), 77 | ("d", #Int(42)), 78 | ]); 79 | let icrc3_value_record : RepIndyHash.Value = #Map([ 80 | ("a", #Nat(1)), 81 | ("b", #Array([#Text("hello"), #Text("world")])), 82 | ("c", #Blob("\00\01\02")), 83 | ("d", #Int(42)), 84 | ]); 85 | 86 | assert validate_hash(candid_record, icrc3_value_record); 87 | 88 | }, 89 | ); 90 | }, 91 | ); 92 | -------------------------------------------------------------------------------- /tests/UrlEncoded.Test.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Debug "mo:base/Debug"; 3 | import Iter "mo:base/Iter"; 4 | 5 | import ActorSpec "./utils/ActorSpec"; 6 | 7 | import UrlEncoded "../src/UrlEncoded"; 8 | 9 | let { 10 | assertTrue; 11 | assertFalse; 12 | assertAllTrue; 13 | describe; 14 | it; 15 | skip; 16 | pending; 17 | run; 18 | } = ActorSpec; 19 | 20 | type User = { 21 | name : Text; 22 | msg : Text; 23 | }; 24 | 25 | let success = run([ 26 | describe( 27 | "UrlEncoded Pairs", 28 | [ 29 | describe( 30 | "fromText to motoko", 31 | [ 32 | it( 33 | "single record", 34 | do { 35 | 36 | let blob = switch(UrlEncoded.fromText("msg=Hello World&name=John", null)){ 37 | case (#ok(b)) b; 38 | case (#err(errorMsg)) Debug.trap(errorMsg); 39 | }; 40 | 41 | let res : ?User = from_candid (blob); 42 | 43 | assertTrue( 44 | res == ?{ 45 | name = "John"; 46 | msg = "Hello World"; 47 | } 48 | ); 49 | }, 50 | ), 51 | it( 52 | "pairs with empty values", 53 | do { 54 | 55 | let #ok(unknown_blob) = UrlEncoded.fromText("msg=Hello&name=", null); 56 | let #ok(known_blob) = UrlEncoded.fromText("msg=Hello&name=John", null); 57 | 58 | type UserOptionalName = { 59 | name : ?Text; 60 | msg : Text; 61 | }; 62 | 63 | let unknown_user : ?UserOptionalName = from_candid (unknown_blob); 64 | let known_user : ?UserOptionalName = from_candid (known_blob); 65 | 66 | assertAllTrue([ 67 | unknown_user == ?{ 68 | name = null; 69 | msg = "Hello"; 70 | }, 71 | known_user == ?{ 72 | name = ?"John"; 73 | msg = "Hello"; 74 | }, 75 | ]); 76 | }, 77 | ), 78 | it( 79 | "record with array", 80 | do { 81 | 82 | let text = "users[0][name]=John&users[0][msg]=Hello World&users[1][name]=Jane&users[1][msg]=testing"; 83 | let #ok(blob) = UrlEncoded.fromText(text, null); 84 | 85 | let res : ?{ users : [User] } = from_candid (blob); 86 | assertTrue( 87 | res == ?{ 88 | users = [ 89 | { 90 | name = "John"; 91 | msg = "Hello World"; 92 | }, 93 | { 94 | name = "Jane"; 95 | msg = "testing"; 96 | }, 97 | ]; 98 | } 99 | ); 100 | }, 101 | ), 102 | it( 103 | "variant type", 104 | do { 105 | type Variant = { 106 | #text : Text; 107 | #nat : Nat; 108 | #int : Int; 109 | #float : Float; 110 | #bool : Bool; 111 | #record : { site : Text }; 112 | #user : User; 113 | #array : [Nat]; 114 | }; 115 | 116 | let text = "variant[#text]=hello"; 117 | let nat = "variant[#nat]=123"; 118 | let int = "variant[#int]=-123"; 119 | let float = "variant[#float]=-1.23"; 120 | let bool = "variant[#bool]=true"; 121 | let record = "variant[#record][site]=github"; 122 | let user = "variant[#user][name]=John&variant[#user][msg]=Hello World"; 123 | let array = "variant[#array][0]=1&variant[#array][1]=2&variant[#array][2]=3"; 124 | 125 | let #ok(text_blob) = UrlEncoded.fromText(text, null); 126 | let #ok(nat_blob) = UrlEncoded.fromText(nat, null); 127 | let #ok(int_blob) = UrlEncoded.fromText(int, null); 128 | let #ok(float_blob) = UrlEncoded.fromText(float, null); 129 | let #ok(bool_blob) = UrlEncoded.fromText(bool, null); 130 | let #ok(record_blob) = UrlEncoded.fromText(record, null); 131 | let #ok(user_blob) = UrlEncoded.fromText(user, null); 132 | let #ok(array_blob) = UrlEncoded.fromText(array, null); 133 | 134 | let text_val : ?{ variant : Variant } = from_candid (text_blob); 135 | let nat_val : ?{ variant : Variant } = from_candid (nat_blob); 136 | let int_val : ?{ variant : Variant } = from_candid (int_blob); 137 | let float_val : ?{ variant : Variant } = from_candid (float_blob); 138 | let bool_val : ?{ variant : Variant } = from_candid (bool_blob); 139 | let record_val : ?{ variant : Variant } = from_candid (record_blob); 140 | let user_val : ?{ variant : Variant } = from_candid (user_blob); 141 | let array_val : ?{ variant : Variant } = from_candid (array_blob); 142 | 143 | assertAllTrue([ 144 | text_val == ?{ variant = #text("hello") }, 145 | nat_val == ?{ variant = #nat(123) }, 146 | int_val == ?{ variant = #int(-123) }, 147 | 148 | float_val == ?{ variant = #float(-1.23) }, 149 | bool_val == ?{ variant = #bool(true) }, 150 | record_val == ?{ 151 | variant = #record({ site = "github" }); 152 | }, 153 | user_val == ?{ 154 | variant = #user({ 155 | name = "John"; 156 | msg = "Hello World"; 157 | }); 158 | }, 159 | array_val == ?{ variant = #array([1, 2, 3]) }, 160 | 161 | ]); 162 | }, 163 | ), 164 | ], 165 | ), 166 | describe( 167 | "motoko toText", 168 | [ 169 | it( 170 | "single record", 171 | do { 172 | 173 | let info : User = { 174 | msg = "Hello World"; 175 | name = "John"; 176 | }; 177 | 178 | let blob = to_candid (info); 179 | let text = UrlEncoded.toText(blob, ["name", "msg"], null); 180 | Debug.print("single record: " #debug_show(text)); 181 | assertTrue(text == #ok("msg=Hello World&name=John")); 182 | }, 183 | ), 184 | it( 185 | "record with array", 186 | do { 187 | let users = [ 188 | { 189 | name = "John"; 190 | msg = "Hello World"; 191 | }, 192 | { 193 | name = "Jane"; 194 | msg = "testing"; 195 | }, 196 | ]; 197 | 198 | let blob = to_candid ({ users }); 199 | 200 | let text = UrlEncoded.toText(blob, ["users", "name", "msg"], null); 201 | 202 | Debug.print("record with array: " #debug_show(text)); 203 | 204 | assertTrue( 205 | text == #ok("users[0][msg]=Hello World&users[0][name]=John&users[1][msg]=testing&users[1][name]=Jane") 206 | ); 207 | }, 208 | ), 209 | ], 210 | ), 211 | ], 212 | ), 213 | ]); 214 | 215 | if (success == false) { 216 | Debug.trap("\1b[46;41mTests failed\1b[0m"); 217 | } else { 218 | Debug.print("\1b[23;42;3m Success!\1b[0m"); 219 | }; 220 | -------------------------------------------------------------------------------- /tests/one-shot.forward-reference.mo: -------------------------------------------------------------------------------- 1 | // @testmode wasi 2 | import Array "mo:base/Array"; 3 | import Blob "mo:base/Blob"; 4 | import Debug "mo:base/Debug"; 5 | import Iter "mo:base/Iter"; 6 | import Principal "mo:base/Principal"; 7 | import Text "mo:base/Text"; 8 | import TrieMap "mo:base/TrieMap"; 9 | import Option "mo:base/Option"; 10 | 11 | import Arg "mo:candid/Arg"; 12 | import Decoder "mo:candid/Decoder"; 13 | import Encoder "mo:candid/Encoder"; 14 | import Type "mo:candid/Type"; 15 | import Value "mo:candid/Value"; 16 | import { test; suite } "mo:test"; 17 | 18 | import Candid "../src/Candid"; 19 | import CandidEncoder "../src/Candid/Blob/Encoder.ForwardReference"; 20 | 21 | type CandidType = Candid.CandidType; 22 | 23 | let { toArgs } = CandidEncoder; 24 | 25 | let empty_map = TrieMap.TrieMap(Text.equal, Text.hash); 26 | 27 | func validate_encoding(candid_values : [Candid.Candid]) : Bool { 28 | let #ok(encoded) = CandidEncoder.one_shot(candid_values, null); 29 | 30 | let #ok(args) = toArgs(candid_values, empty_map); 31 | let expected = Encoder.encode(args); 32 | 33 | Debug.print("(encoded, expected): " # debug_show (encoded, expected)); 34 | return encoded == expected; 35 | }; 36 | 37 | func validate_encoding_with_types(candid_values : [Candid.Candid], types : [CandidType]) : Bool { 38 | let #ok(encoded) = CandidEncoder.one_shot(candid_values, ?{ Candid.defaultOptions with types = ?types }); 39 | 40 | let #ok(args) = toArgs(candid_values, empty_map); 41 | let arg_types = Array.map(types, toArgType); 42 | 43 | let arg_types_iter = arg_types.vals(); 44 | let augmented_args = Array.map( 45 | args, 46 | func(arg : Arg.Arg) : Arg.Arg { 47 | let ?arg_type = arg_types_iter.next(); 48 | 49 | { arg with type_ = arg_type }; 50 | }, 51 | ); 52 | 53 | let expected = Encoder.encode(augmented_args); 54 | 55 | Debug.print("(encoded, expected): " # debug_show (encoded, expected)); 56 | return encoded == expected; 57 | }; 58 | 59 | func encode(candid_values : [Candid.Candid],) : Blob { 60 | let #ok(encoded) = CandidEncoder.one_shot(candid_values, null); 61 | return encoded; 62 | }; 63 | 64 | func encode_with_types(candid_values : [Candid.Candid], types : [CandidType]) : Blob { 65 | let #ok(encoded) = CandidEncoder.one_shot(candid_values, ?{ Candid.defaultOptions with types = ?types }); 66 | return encoded; 67 | }; 68 | 69 | func equals(encoding : Blob, expected : Blob) : Bool { 70 | Debug.print("(encoding, expected): " # debug_show (encoding, expected)); 71 | return encoding == expected; 72 | }; 73 | 74 | 75 | func toArgType(candid : CandidType) : (Type.Type) { 76 | let (arg_type) : (Type.Type) = switch (candid) { 77 | case (#Nat) (#nat); 78 | case (#Nat8) (#nat8); 79 | case (#Nat16) (#nat16); 80 | case (#Nat32) (#nat32); 81 | case (#Nat64) (#nat64); 82 | 83 | case (#Int) (#int); 84 | case (#Int8) (#int8); 85 | case (#Int16) (#int16); 86 | case (#Int32) (#int32); 87 | case (#Int64) (#int64); 88 | 89 | case (#Float) (#float64); 90 | 91 | case (#Bool) (#bool); 92 | 93 | case (#Principal) (#principal); 94 | 95 | case (#Text) (#text); 96 | 97 | case (#Null) (#null_); 98 | case (#Empty) (#empty); 99 | 100 | case (#Blob(blob)) #vector(#nat8); 101 | 102 | case (#Option(optType)) #opt(toArgType(optType)); 103 | case (#Array(arr_type)) #vector(toArgType(arr_type)); 104 | 105 | case (#Record(records) or #Map(records)) #record( 106 | Array.map( 107 | records, 108 | func((key, val) : (Text, CandidType)) : Type.RecordFieldType = { 109 | tag = #name(key); 110 | type_ = toArgType(val); 111 | }, 112 | ) 113 | ); 114 | 115 | case (#Variant(variants)) #variant( 116 | Array.map( 117 | variants, 118 | func((key, val) : (Text, CandidType)) : Type.RecordFieldType = { 119 | tag = #name(key); 120 | type_ = toArgType(val); 121 | }, 122 | ) 123 | ); 124 | }; 125 | }; 126 | 127 | var encoding : Blob = ""; 128 | var expected : Blob = ""; 129 | 130 | suite( 131 | "One Shot Candid Test", 132 | func() { 133 | 134 | test( 135 | "primitives", 136 | func() { 137 | 138 | assert validate_encoding([]); 139 | assert validate_encoding([#Nat(0)]); 140 | assert validate_encoding([#Nat(1)]); 141 | assert validate_encoding([#Nat(9_223_372_036_854_775_807)]); 142 | assert validate_encoding([#Nat(18_446_744_073_709_551_615)]); // limited to Nat64 for now 143 | assert validate_encoding([#Nat(0), #Nat(1), #Nat(18_446_744_073_709_551_615)]); 144 | 145 | assert validate_encoding([#Nat8(0)]); 146 | assert validate_encoding([#Nat8(1)]); 147 | assert validate_encoding([#Nat8(255)]); 148 | assert validate_encoding([#Nat8(0), #Nat8(1), #Nat8(255)]); 149 | 150 | assert validate_encoding([#Nat16(0)]); 151 | assert validate_encoding([#Nat16(1)]); 152 | assert validate_encoding([#Nat16(65_535)]); 153 | assert validate_encoding([#Nat16(0), #Nat16(1), #Nat16(65_535)]); 154 | 155 | assert validate_encoding([#Nat32(0)]); 156 | assert validate_encoding([#Nat32(1)]); 157 | assert validate_encoding([#Nat32(4_294_967_295)]); 158 | assert validate_encoding([#Nat32(0), #Nat32(1), #Nat32(4_294_967_295)]); 159 | 160 | assert validate_encoding([#Nat64(0)]); 161 | assert validate_encoding([#Nat64(1)]); 162 | assert validate_encoding([#Nat64(18_446_744_073_709_551_615)]); 163 | assert validate_encoding([#Nat64(0), #Nat64(1), #Nat64(18_446_744_073_709_551_615)]); 164 | 165 | assert validate_encoding([#Int(1)]); 166 | assert validate_encoding([#Int(-1)]); 167 | assert validate_encoding([#Int(127)]); 168 | assert validate_encoding([#Int(-127)]); 169 | assert validate_encoding([#Int(123_456_789)]); 170 | assert validate_encoding([#Int(-123_456_789)]); 171 | assert validate_encoding([#Int(2_147_483_647)]); 172 | assert validate_encoding([#Int(-2_147_483_648)]); 173 | assert validate_encoding([#Int(9_223_372_036_854_775_807)]); // limited to Int64 for now 174 | // assert validate_encoding([#Int(-9_223_372_036_854_775_807)]); // fails 175 | 176 | assert validate_encoding([#Int8(1)]); 177 | assert validate_encoding([#Int8(-1)]); 178 | assert validate_encoding([#Int8(127)]); 179 | assert validate_encoding([#Int8(-128)]); 180 | 181 | assert validate_encoding([#Int16(1)]); 182 | assert validate_encoding([#Int16(-1)]); 183 | assert validate_encoding([#Int16(32_767)]); 184 | assert validate_encoding([#Int16(-32_768)]); 185 | 186 | assert validate_encoding([#Int32(1)]); 187 | assert validate_encoding([#Int32(-1)]); 188 | assert validate_encoding([#Int32(2_147_483_647)]); 189 | assert validate_encoding([#Int32(-2_147_483_648)]); 190 | 191 | assert validate_encoding([#Int64(1)]); 192 | assert validate_encoding([#Int64(-1)]); 193 | assert validate_encoding([#Int64(9_223_372_036_854_775_807)]); 194 | assert validate_encoding([#Int64(-9_223_372_036_854_775_808)]); 195 | 196 | assert validate_encoding([#Bool(true)]); 197 | assert validate_encoding([#Bool(false)]); 198 | 199 | assert validate_encoding([#Null]); 200 | assert validate_encoding([#Empty]); 201 | assert validate_encoding([#Text("")]); 202 | assert validate_encoding([#Text("random text")]); 203 | 204 | assert validate_encoding([#Principal(Principal.fromText("bkyz2-fmaaa-aaaaa-qaaaq-cai"))]); 205 | 206 | assert validate_encoding([#Float(0.0)]); 207 | assert validate_encoding([#Float(0.6)]); 208 | assert validate_encoding([#Float(-0.6)]); 209 | assert validate_encoding([#Float(1.1), #Float(-1.1)]); 210 | }, 211 | ); 212 | 213 | }, 214 | ); 215 | 216 | suite( 217 | "compound types", 218 | func() { 219 | // test( 220 | // "option types", 221 | // func() { 222 | // 223 | // encoding := encode([#Option(#Null)]); 224 | // expected := to_candid(?null : ?Null); 225 | // assert equals(encoding, expected); 226 | 227 | // encoding := encode([#Option(#Int(6)), #Option(#Int(7))]); // does it reference the same type? 228 | // expected := to_candid(?6 : ?Int, ?7 : ?Int); 229 | // assert equals(encoding, expected); 230 | 231 | // encoding := encode([#Option(#Option(#Int(6)))]); // does it reference nested types? 232 | // expected := to_candid(?(?6) : ?(?Int)); 233 | // assert equals(encoding, expected); 234 | 235 | // encoding := encode([#Option(#Option(#Option(#Null)))]); 236 | // expected := to_candid(???null : ???Null); 237 | // assert equals(encoding, expected); 238 | 239 | // encoding := encode([#Option(#Null), #Option(#Option(#Null)), #Option(#Option(#Option(#Null)))]); 240 | // expected := to_candid(?null : ?Null, ??null : ??Null, ???null : ???Null); 241 | // assert equals(encoding, expected); 242 | 243 | // encoding := encode([#Option(#Nat(0)), #Option(#Text("random text")), #Option(#Option(#Option(#Option(#Option(#Option(#Option(#Text("nested option"))))))))]); 244 | // expected := to_candid(?0 : ?Nat, ?"random text" : ?Text, ???????("nested option") : ???????Text); 245 | // assert equals(encoding, expected); 246 | 247 | // // test early null terminated option types 248 | // encoding := encode_with_types([#Null], [#Option(#Int)]); // can't express null as the result of an option over the primitive null type without specifying the type 249 | // expected := to_candid(null : ?Int); 250 | // assert equals(encoding, expected); 251 | 252 | // encoding := encode_with_types([#Option(#Null)], [#Option(#Option(#Option(#Text)))]); 253 | // expected := to_candid(?null : ???Text); 254 | // assert equals(encoding, expected); 255 | // }, 256 | // ); 257 | 258 | test( 259 | "array types", 260 | func() { 261 | 262 | encoding := encode([#Array([])]); // transpiles to [#Array(#Empty)] 263 | expected := to_candid([] : [None]); 264 | assert equals(encoding, expected); 265 | 266 | encoding := encode_with_types([#Array([])], [#Array(#Nat)]); 267 | expected := to_candid([] : [Nat]); 268 | assert equals(encoding, expected); 269 | 270 | encoding := encode([#Array([#Nat(0)])]); 271 | expected := to_candid([0] : [Nat]); 272 | assert equals(encoding, expected); 273 | 274 | encoding := encode([#Array([#Nat(0), #Nat(1), #Nat(2)])]); 275 | expected := to_candid([0, 1, 2] : [Nat]); 276 | assert equals(encoding, expected); 277 | 278 | // encoding := encode([#Array([#Option(#Int(6)), #Option(#Int(7)), #Null])]); 279 | // expected := to_candid([?6, ?7, null] : [?Int]); 280 | // assert equals(encoding, expected); 281 | 282 | encoding := encode([#Array([#Array([#Nat(0)])])]); // nested array 283 | expected := to_candid([[0]] : [[Nat]]); 284 | assert equals(encoding, expected); 285 | 286 | encoding := encode([#Array([#Array([#Array([#Nat(0)]), #Array([#Nat(1)])])])]); 287 | expected := to_candid([[[0], [1]]] : [[[Nat]]]); 288 | assert equals(encoding, expected); 289 | 290 | encoding := encode([#Array([#Array([#Array([#Array([#Nat(0)])])])])]); 291 | expected := to_candid([[[[0]]]] : [[[[Nat]]]]); 292 | assert equals(encoding, expected); 293 | 294 | // equivalent motoko type -> [?[?[Nat]]] 295 | // encoding := encode([ 296 | // #Array([ 297 | // #Option(#Array([#Option(#Array([#Nat(0)])), #Null])), 298 | // #Null, 299 | // ]) 300 | // ]); 301 | // expected := to_candid([?[?[0], null], null] : [?[?[Nat]]]); 302 | // assert equals(encoding, expected); 303 | 304 | // infered types test by switching #Option(#Null) to first element 305 | // encoding := encode([ 306 | // #Array([ 307 | // #Null, 308 | // #Option(#Array([#Null, #Option(#Array([#Text("random")]))])), 309 | // ]) 310 | // ]); 311 | // expected := to_candid([null, ?[null, ?["random"]] ] : [?[?[Text]]]); 312 | // assert equals(encoding, expected); 313 | 314 | }, 315 | ); 316 | 317 | test( 318 | "record types", 319 | func() { 320 | encoding := encode([#Record([])]); 321 | expected := to_candid({}); 322 | assert equals(encoding, expected); 323 | 324 | encoding := encode([#Record([("a", #Nat(0))])]); 325 | expected := to_candid({a = 0}); 326 | assert equals(encoding, expected); 327 | 328 | // the library encoding no longer matches with to_candid's encoding, the sequences are in different order but the result is the same 329 | // so the testing method is switched to compare their motoko representations 330 | encoding := encode([#Record([("a", #Nat(0)), ("b", #Int(1)), ("c", #Text("random"))])]); 331 | expected := to_candid({a = 0; b = (1: Int); c = "random"}); 332 | assert equals(encoding, expected); 333 | 334 | // encoding := encode([#Record([("a", #Option(#Text("random"))), ("b", #Nat(1))])]); 335 | // expected := to_candid({a = ?"random"; b = 1}); 336 | // assert equals(encoding, expected); 337 | // assert from_candid(encoding) : ?({a: ?Text; b: Nat}) == ?({a = ?"random"; b = 1}); 338 | 339 | 340 | // encoding := encode([#Record([("a", #Nat(1)), ("b", #Option(#Text("random")))])]); 341 | // expected := to_candid({a = 1; b = ?"random"}); 342 | // assert equals(encoding, expected); 343 | // assert (from_candid(encoding) : ?({a: Nat; b: ?Text})) == ?({a = 1; b = ?"random"}); 344 | 345 | // encoding := encode([#Option(#Text("random")), #Record([("a", #Nat(1)), ("b", #Option(#Text("random")))])]); 346 | // expected := to_candid(?("random"), {a = 1; b = ?"random"}); 347 | // assert equals(encoding, expected); 348 | // assert (from_candid(encoding) : ?(?Text, ({a: Nat; b: ?Text}))) == ?(?"random", {a = 1; b = ?"random"}); 349 | 350 | encoding := encode([#Record([("int", #Int(28)), ("nums", #Array([#Nat(0), #Nat(1), #Nat(2), #Nat(3)]))])]); 351 | expected := to_candid({nums = [0]; int = (28: Int)}); 352 | ignore equals(encoding, expected); 353 | assert (from_candid(encoding) : ?({nums: [Nat]; int: Int})) == ?({nums = [0, 1, 2, 3]; int = 28}); 354 | 355 | // encoding := encode([#Record([("nums", #Array([#Nat(0)])), ("opt_text", #Option(#Text("random"))), ("int", #Int(1))])]); 356 | // expected := to_candid({nums = [0]; opt_text = ?"random"; int = (1: Int)}); 357 | // assert equals(encoding, expected); 358 | // assert (from_candid(encoding) : ?({nums: [Nat]; opt_text: ?Text; int: Int})) == ?({nums = [0]; opt_text = ?"random"; int = 1}); 359 | 360 | // encoding := encode([#Array([#Nat(0)]), #Option(#Text("random")), #Record([("nums", #Array([#Nat(0)])), ("opt_text", #Option(#Text("random"))), ("int", #Int(1))])]); 361 | // expected := to_candid([0], "random", {nums = [0]; opt_text = ?"random"; int = (1: Int)}); 362 | // assert equals(encoding, expected); 363 | // assert (from_candid(encoding) : ?([Nat], ?Text, {nums: [Nat]; opt_text: ?Text; int: Int})) == ?([0], ?"random", {nums = [0]; opt_text = ?"random"; int = 1}); 364 | 365 | // nested records 366 | encoding := encode([#Record([("a", #Record([("b", #Nat(0))]))])]); 367 | expected := to_candid({a = {b = 0}}); 368 | ignore equals(encoding, expected); 369 | Debug.print(debug_show (from_candid(encoding): ?({a: {b: Nat}}))); 370 | assert (from_candid(encoding) : ?({a: {b: Nat}})) == ?({a = {b = 0}}); 371 | 372 | encoding := encode([#Record([("c", #Nat(0))]), #Record([("a", #Record([("b", #Record([("c", #Nat(0))]))]))])]); 373 | expected := to_candid({c = 0}, {a = {b = {c = 0}}}); 374 | ignore equals(encoding, expected); 375 | Debug.print(debug_show (from_candid(encoding): ?({c: Nat}, {a: {b: {c: Nat}}}))); 376 | assert (from_candid(encoding) : ?({c: Nat}, {a: {b: {c: Nat}}})) == ?({c = 0}, {a = {b = {c = 0}}}); 377 | 378 | encoding := encode([#Record([("a", #Record([("b", #Record([("c", #Record([("d", #Record([("e", #Nat(0))]))]))]))]))])]); 379 | expected := to_candid({a = {b = {c = {d = {e = 0}}}}}); 380 | ignore equals(encoding, expected); 381 | Debug.print(debug_show (from_candid(encoding) : ?({a: {b: {c: {d: {e: Nat}}}}}))); 382 | assert (from_candid(encoding) : ?({a: {b: {c: {d: {e: Nat}}}}})) == ?({a = {b = {c = {d = {e = 0}}}}}); 383 | 384 | encoding := encode([ 385 | #Record([ 386 | ("a", #Array([#Record([("a", #Nat(1))]), #Record([("a", #Nat(2))])])), 387 | ]) 388 | ]); 389 | expected := to_candid({a = [{a = 1}, {a = 2}] }); 390 | ignore equals(encoding, expected); 391 | Debug.print(debug_show (from_candid(encoding) : ?({a: [{a: Nat}]}))); 392 | assert (from_candid(encoding) : ?({a: [{a: Nat}]})) == ?({a = [{a = 1}, {a = 2}]}); 393 | 394 | 395 | // encoding := encode([ 396 | // #Record([ 397 | // ("a", #Array([#Nat(23), #Nat(34)])), 398 | // ("b", #Array([#Record([("a", #Nat(1))]), #Record([("a", #Nat(2))])])), 399 | // ("c", #Array([ 400 | // #Record([("a", #Nat(1)), ("b", #Text("random" ))]), 401 | // #Record([("a", #Nat(2)), ("b", #Text("random 2"))]) 402 | // ])), 403 | // ]) 404 | // ]); 405 | // expected := to_candid({a = [23, 34]; b = [{a = 1}, {a = 2}]; c = [{a = 1; b = "random"}, {a = 2; b = "random 2"}]}); 406 | // ignore equals(encoding, expected); 407 | // Debug.print(debug_show (from_candid(encoding) : ?({a: [Nat]; b: [{a: Nat}]; c: [{a: Nat; b: Text}]})); 408 | // assert (from_candid(encoding) : ?({a: [Nat]; b: [{a: Nat}]; c: [{a: Nat; b: Text}]})) == ?({a = [23, 34]; b = [{a = 1}, {a = 2}]; c = [{a = 1; b = "random"}, {a = 2; b = "random 2"}]}); 409 | 410 | //encoding := encode([ 411 | // #Option(#Record([("b", #Nat(0))])), 412 | // #Record([ 413 | // ("a", #Option(#Record([("b", #Nat(0))]))), 414 | // ("b", #Array([#Option(#Record([("b", #Array([#Option(#Record([("b", #Nat(0))])), #Option(#Record([("b", #Nat(0))]))]))])), #Null])), 415 | // ]), 416 | //]); 417 | // expected := to_candid( 418 | // ?{b = 0}, 419 | // {a = ?{b = 0}; b = [?{b = [?{b = 0}, ?{b = 0}]}, null]} 420 | // ); 421 | // assert equals(encoding, expected); 422 | //assert ( 423 | // from_candid(encoding) : ?(?{b: Nat}, {a: ?{b: Nat}; b: [?{b: [?{b: Nat}]}]}) 424 | //) == ?(?{b = 0}, {a = ?{b = 0}; b = [?{b = [?{b = 0}, ?{b = 0}]}, null]}); 425 | 426 | // tuples 427 | // assert validate_encoding([#Record([("0", #Nat(0)), ("1", #Nat(1))])]); 428 | 429 | }, 430 | ); 431 | 432 | // test( 433 | // "variant types", 434 | // func() { 435 | // // assert validate_encoding([#Variant("nat", #Nat(21))]); 436 | // // assert validate_encoding([#Variant("opt_int", #Option(#Int(21)))]); 437 | 438 | // // type V = { 439 | // // #nat : Nat; 440 | // // #opt_int : ?Int; 441 | // // // #texts: [Text]; 442 | // // // #record: {a: Nat}; 443 | // // }; 444 | 445 | // // var encoding = (encode([#Variant(("nat", #Nat(1)))], ?[#Variant([("nat", #Nat), ("opt_int", #Option(#Int)), ("texts", #Array(#Text)), ("record", #Record([("a", #Nat)]))])])); 446 | // // var expected = (to_candid (#nat(1) : V)); 447 | 448 | // // Debug.print(debug_show (encoding, expected)); 449 | // // assert (from_candid(expected) : ?V) == ?(#nat(1)); 450 | 451 | // // assert validate_encoding_with_types([#Variant("nat", #Nat(21))], [#Variant([("nat", #Nat), ("opt_int", #Option(#Int))])]); 452 | // // assert validate_encoding_with_types([#Variant("opt_int", #Option(#Int(21)))], [#Variant([("nat", #Nat), ("opt_int", #Option(#Int))])]); 453 | 454 | // // // fails in mo:motoko_candid lib Encoder 455 | // // // assert validate_encoding_with_types([#Variant("nat", #Nat(21))], [#Variant([("nat", #Nat), ("opt_int", #Option(#Int)), ("texts", #Array(#Text)), 456 | // // // ("record", #Record([("a", #Nat)])) 457 | // // // ])]); 458 | 459 | 460 | // }, 461 | // ); 462 | }, 463 | ); 464 | -------------------------------------------------------------------------------- /tests/test_template.md: -------------------------------------------------------------------------------- 1 | Filename: `[Section]/[Function].Test.mo` 2 | 3 | ```motoko 4 | import Debug "mo:base/Debug"; 5 | import Iter "mo:base/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 | ``` -------------------------------------------------------------------------------- /tests/utils/ActorSpec.mo: -------------------------------------------------------------------------------- 1 | import Debug "mo:base/Debug"; 2 | import Array "mo:base/Array"; 3 | import Iter "mo:base/Iter"; 4 | import Int "mo:base/Int"; 5 | import Nat "mo:base/Nat"; 6 | import Text "mo:base/Text"; 7 | 8 | module { 9 | public type Group = { 10 | name : Text; 11 | groups : [Group]; 12 | status : Status; 13 | }; 14 | 15 | type Status = { 16 | failed : Nat; 17 | passed : Nat; 18 | pending : Nat; 19 | skipped : Nat; 20 | }; 21 | 22 | func eqStatus(x : Status, y : Status) : Bool { 23 | x.failed == y.failed and x.passed == y.passed and x.pending == y.pending and x.skipped == y.skipped; 24 | }; 25 | 26 | let emptyStatus : Status = { 27 | failed = 0; 28 | passed = 0; 29 | pending = 0; 30 | skipped = 0; 31 | }; 32 | 33 | func appendStatus(x : Status, y : Status) : Status { 34 | { 35 | failed = x.failed + y.failed; 36 | passed = x.passed + y.passed; 37 | pending = x.pending + y.pending; 38 | skipped = x.skipped + y.skipped; 39 | }; 40 | }; 41 | 42 | func printStatus(status : Status) : Text { 43 | "Failed: " # Int.toText(status.failed) # ", Passed: " # Int.toText(status.passed) # ", Pending: " # Int.toText(status.pending) # ", Skipped: " # Int.toText(status.skipped); 44 | }; 45 | 46 | 47 | public func run(groups_ : [Group]) : Bool { 48 | let (groups, status) = getGroups(groups_); 49 | printGroups(groups, ""); 50 | Debug.print("\n"); 51 | Debug.print(printStatus(status)); 52 | Debug.print("\n"); 53 | status.failed == 0; 54 | }; 55 | 56 | func getGroups(groups_ : [Group]) : ([Group], Status) { 57 | let groups = Array.thaw(groups_); 58 | var status = emptyStatus; 59 | for (index in groups_.keys()) { 60 | let group = groups[index]; 61 | let (newGroups, newGroupsStatus) = getGroups(group.groups); 62 | let newStatus = appendStatus(group.status, newGroupsStatus); 63 | status := appendStatus(status, newStatus); 64 | let newGroup = { 65 | name = group.name; 66 | groups = newGroups; 67 | status = newStatus; 68 | }; 69 | groups[index] := newGroup; 70 | }; 71 | (Array.freeze(groups), status); 72 | }; 73 | 74 | func printGroups(groups_ : [Group], indent : Text) { 75 | for (group in groups_.vals()) { 76 | let isDescribe = Iter.size(Array.keys(group.groups)) > 0; 77 | let newline = if isDescribe "\n" else ""; 78 | let status = group.status; 79 | let statusText = if (isDescribe) { 80 | ": " # printStatus(status); 81 | } else { 82 | let failed = status.failed; 83 | let passed = status.passed; 84 | let pending = status.pending; 85 | let skipped = status.skipped; 86 | switch(failed, passed, pending, skipped) { 87 | case (0, 0, 0, 0) { ""; }; 88 | case (1, 0, 0, 0) { ": Failed"; }; 89 | case (0, 1, 0, 0) { ": Passed"; }; 90 | case (0, 0, 1, 0) { ": Pending"; }; 91 | case (0, 0, 0, 1) { ": Skipped"; }; 92 | case (_, _, _, _) { ":" # printStatus(status); }; 93 | }; 94 | }; 95 | Debug.print(newline # indent # group.name # statusText # "\n"); 96 | printGroups(group.groups, indent # " "); 97 | }; 98 | }; 99 | 100 | 101 | public func describe(name_ : Text, groups_ : [Group]) : Group { 102 | { 103 | name = name_; 104 | groups = groups_; 105 | status = emptyStatus; 106 | }; 107 | }; 108 | 109 | public func it(name_ : Text, passed_ : Bool) : Group { 110 | { 111 | name = name_; 112 | groups = []; 113 | status = { 114 | failed = if passed_ 0 else 1; 115 | passed = if passed_ 1 else 0; 116 | pending = 0; 117 | skipped = 0; 118 | }; 119 | }; 120 | }; 121 | 122 | public let test = it; 123 | 124 | public func skip(name_ : Text, passed_ : Bool) : Group { 125 | { 126 | name = name_; 127 | groups = []; 128 | status = { 129 | failed = 0; 130 | passed = 0; 131 | pending = 0; 132 | skipped = 1; 133 | }; 134 | }; 135 | }; 136 | 137 | public func pending(name_ : Text) : Group { 138 | { 139 | name = name_; 140 | groups = []; 141 | status = { 142 | failed = 0; 143 | passed = 0; 144 | pending = 1; 145 | skipped = 0; 146 | }; 147 | }; 148 | }; 149 | 150 | public func assertTrue(x : Bool) : Bool { 151 | x == true; 152 | }; 153 | 154 | public func assertFalse(x : Bool) : Bool { 155 | x == false; 156 | }; 157 | 158 | public func assertAllTrue(xs : [Bool]) : Bool { 159 | var allTrue = true; 160 | for (val in xs.vals()) { 161 | if (val == false) { 162 | return false; 163 | }; 164 | allTrue := allTrue and val; 165 | }; 166 | allTrue; 167 | }; 168 | }; 169 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------