diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index 7cd52d3..a704e6c 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -14,28 +14,31 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 14 - - uses: aviate-labs/setup-dfx@v0.2.3 + node-version: 18 + - uses: aviate-labs/setup-dfx@v0.2.5 with: - vessel-version: 0.6.3 - dfx-version: 0.12.1 + dfx-version: 0.14.1 + + - name: install wasmtime + run: | + curl https://wasmtime.dev/install.sh -sSf | bash + echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH + + - name: Install mocv + run: | + npm --yes -g i mocv + + - name: Select mocv version + run: mocv use 0.9.4 - name: install mops run: | npm --yes -g i ic-mops mops i mops sources - - - name: install wasmtime - run: | - curl https://wasmtime.dev/install.sh -sSf | bash - echo "$HOME/.wasmtime/bin" >> $GITHUB_PATH - name: Detect Warnings run: make no-warn - name: Run Tests - run: make compile-tests - - # - name: Generate Docs - # run: make docs + run: mops test diff --git a/.gitignore b/.gitignore index c0b1f14..fb50435 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ # frontend code node_modules/ dist/ - diff --git a/compile-tests.mjs b/compile-tests.mjs new file mode 100644 index 0000000..015ce42 --- /dev/null +++ b/compile-tests.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env zx + +import fs, { createReadStream, existsSync, statSync } from "fs"; +import events from "events"; +import { readFile, stat } from "fs/promises"; +import readline from "readline"; +import { spawn } from "child_process"; +import path from "path"; +// import { chalk } from "zx"; +// import 'zx/globals' + +Array.prototype.last = function () { + return this[this.length - 1]; +}; + +const getImports = async (file) => { + let rl = readline.createInterface({ + input: fs.createReadStream(file), + crlfDelay: Infinity, + }); + + let import_paths = []; + + rl.on("line", (line) => { + if (line.startsWith("import")) { + let path = line.split(" ").last(); + path = path.slice(1, path.length - 2); + import_paths.push(path); + } else if (line.length !== 0 && !line.startsWith("//")) { + rl.close(); + rl.removeAllListeners(); + } + }); + + await events.once(rl, "close"); + + return import_paths; +}; + +const wasm_path = (file) => { + let segments = file.split("/"); + segments.splice(1, 0, ".wasm"); + let basename = segments.last().replace(".mo", ".wasm"); + segments[segments.length - 1] = basename; + return segments.join("/"); +}; + +let test_files = await glob("./test?(s)/**/*.(test|Test).mo"); + +let moc = (await $`dfx cache show`).stdout.toString().trim() + "/moc"; +let mops_sources = (await $`mops sources`).stdout.toString().split("\n"); + +let packages = {}; +let package_args = []; + + +for (const source of mops_sources) { + let segments = source.split(" "); + package_args.push(...segments); + packages[segments[1]] = segments[2]; +} + +const compile_motoko = async (src, dest) => { + await $`${moc} ${package_args} -wasi-system-api ${src} -o ${dest}`; +}; + +if (test_files.length) { + let wasm_dir = test_files[0].split("/")[0] + "/.wasm"; + await $`mkdir -p ${wasm_dir}`; +} + +const last_modified_cache = {}; + +const is_recently_modified = (file, time) => { + let cached_mtime = last_modified_cache[file]; + + if (cached_mtime) { + return cached_mtime > time; + } + + let file_mtime = statSync(file).mtimeMs; + last_modified_cache[file] = file_mtime; + + return file_mtime > time; +}; + +const is_dep_tree_recently_modified = async (file, wasm_mtime, visited) => { + let modified = is_recently_modified(file, wasm_mtime); + + if (modified) { + return true; + } + + let imports = await getImports(file); + + // console.log({file, imports}) + + for (let imp_path of imports) { + if (imp_path.startsWith("mo:")) { + let pkg = imp_path.slice(3).split("/")[0]; + let pkg_path = packages[pkg]; + + pkg_path = (await glob(pkg_path + "/**/*.mo"))[0]; + + if (pkg_path && !visited.has(pkg_path)) { + visited.add(pkg_path); + modified = is_recently_modified(pkg_path, wasm_mtime); + } + + continue; + } + + imp_path = path.resolve(path.dirname(file), imp_path); + + if (existsSync(imp_path.concat(".mo"))) { + imp_path = imp_path.concat(".mo"); + } else { + imp_path = imp_path.concat("/lib.mo"); + } + + if (visited.has(imp_path)) { continue; } + + visited.add(imp_path); + + // console.log({imp_path}) + + if (await is_dep_tree_recently_modified(imp_path, wasm_mtime, visited)) { + return true + }; + + } + + return false; +}; + +const compile_test = async (test_file) => { + const wasm_file = wasm_path(test_file); + + let should_compile = !existsSync(wasm_file); + + if (!should_compile) { + let wasm_mtime = statSync(wasm_file).mtimeMs; + should_compile = await is_dep_tree_recently_modified(test_file, wasm_mtime, new Set()); + } + + if (should_compile) { + console.log(`Compiling ${test_file}`); + await compile_motoko(test_file, wasm_file); + } + + return wasm_file; +} + +const wasm_files = await Promise.all( + test_files.map(compile_test) +); + +for (const wasm_file of wasm_files) { + await $`wasmtime ${wasm_file}`; +} \ No newline at end of file diff --git a/dfx.json b/dfx.json index 994a73b..9f142cb 100644 --- a/dfx.json +++ b/dfx.json @@ -1,6 +1,5 @@ { "version": 1, - "dfx": "0.12.1", "defaults": { "build": { "packtool": "mops sources", diff --git a/docs/Candid/Decoder.md b/docs/Candid/Decoder.md new file mode 100644 index 0000000..6570cf9 --- /dev/null +++ b/docs/Candid/Decoder.md @@ -0,0 +1,25 @@ +# Candid/Decoder + +## Type `Options` +``` motoko no-repl +type Options = { renameKeys : [(Text, Text)] } +``` + + +## Function `decode` +``` motoko no-repl +func decode(blob : Blob, record_keys : [Text], options : ?Options) : [Candid] +``` + +Decodes a blob encoded in the candid format into a list of the [Candid](./Types.mo#Candid) type in motoko + +### Inputs +- **blob** - A blob encoded in the candid format +**record_keys** - The record keys to use when decoding a record. +**options** - An optional arguement to specify options for decoding. + +## Function `fromArgs` +``` motoko no-repl +func fromArgs(args : [Arg], recordKeyMap : TrieMap.TrieMap) : [Candid] +``` + diff --git a/docs/Candid/Encoder.md b/docs/Candid/Encoder.md new file mode 100644 index 0000000..64cd7f3 --- /dev/null +++ b/docs/Candid/Encoder.md @@ -0,0 +1,19 @@ +# Candid/Encoder + +## Function `encode` +``` motoko no-repl +func encode(candid_values : [Candid]) : Blob +``` + + +## Function `encodeOne` +``` motoko no-repl +func encodeOne(candid : Candid) : Blob +``` + + +## Function `toArgs` +``` motoko no-repl +func toArgs(candid_values : [Candid]) : [Arg] +``` + diff --git a/docs/Candid/Parser/Array.md b/docs/Candid/Parser/Array.md new file mode 100644 index 0000000..57e825b --- /dev/null +++ b/docs/Candid/Parser/Array.md @@ -0,0 +1,7 @@ +# Candid/Parser/Array + +## Function `arrayParser` +``` motoko no-repl +func arrayParser(valueParser : () -> Parser) : Parser +``` + diff --git a/docs/Candid/Parser/Blob.md b/docs/Candid/Parser/Blob.md new file mode 100644 index 0000000..2ae2606 --- /dev/null +++ b/docs/Candid/Parser/Blob.md @@ -0,0 +1,7 @@ +# Candid/Parser/Blob + +## Function `blobParser` +``` motoko no-repl +func blobParser() : Parser +``` + diff --git a/docs/Candid/Parser/Bool.md b/docs/Candid/Parser/Bool.md new file mode 100644 index 0000000..c75d6c2 --- /dev/null +++ b/docs/Candid/Parser/Bool.md @@ -0,0 +1,7 @@ +# Candid/Parser/Bool + +## Function `boolParser` +``` motoko no-repl +func boolParser() : Parser +``` + diff --git a/docs/Candid/Parser/Common.md b/docs/Candid/Parser/Common.md new file mode 100644 index 0000000..5cd7311 --- /dev/null +++ b/docs/Candid/Parser/Common.md @@ -0,0 +1,49 @@ +# Candid/Parser/Common + +## Function `ignoreSpace` +``` motoko no-repl +func ignoreSpace(parser : P.Parser) : P.Parser +``` + + +## Function `removeUnderscore` +``` motoko no-repl +func removeUnderscore(parser : P.Parser) : P.Parser> +``` + + +## Function `any` +``` motoko no-repl +func any() : Parser +``` + + +## Function `hexChar` +``` motoko no-repl +func hexChar() : Parser +``` + + +## Function `consIf` +``` motoko no-repl +func consIf(parserA : Parser, parserAs : Parser>, cond : (A, List) -> Bool) : Parser> +``` + + +## Function `fromHex` +``` motoko no-repl +func fromHex(char : Char) : Nat8 +``` + + +## Function `toText` +``` motoko no-repl +func toText(chars : List) : Text +``` + + +## Function `listToNat` +``` motoko no-repl +func listToNat(digits : List) : Nat +``` + diff --git a/docs/Candid/Parser/Float.md b/docs/Candid/Parser/Float.md new file mode 100644 index 0000000..9ead753 --- /dev/null +++ b/docs/Candid/Parser/Float.md @@ -0,0 +1,7 @@ +# Candid/Parser/Float + +## Function `floatParser` +``` motoko no-repl +func floatParser() : Parser +``` + diff --git a/docs/Candid/Parser/Int.md b/docs/Candid/Parser/Int.md new file mode 100644 index 0000000..788996b --- /dev/null +++ b/docs/Candid/Parser/Int.md @@ -0,0 +1,13 @@ +# Candid/Parser/Int + +## Function `intParser` +``` motoko no-repl +func intParser() : Parser +``` + + +## Function `parseInt` +``` motoko no-repl +func parseInt() : Parser +``` + diff --git a/docs/Candid/Parser/IntX.md b/docs/Candid/Parser/IntX.md new file mode 100644 index 0000000..38fcb70 --- /dev/null +++ b/docs/Candid/Parser/IntX.md @@ -0,0 +1,7 @@ +# Candid/Parser/IntX + +## Function `intXParser` +``` motoko no-repl +func intXParser() : Parser +``` + diff --git a/docs/Candid/Parser/Nat.md b/docs/Candid/Parser/Nat.md new file mode 100644 index 0000000..3cf0548 --- /dev/null +++ b/docs/Candid/Parser/Nat.md @@ -0,0 +1,13 @@ +# Candid/Parser/Nat + +## Function `natParser` +``` motoko no-repl +func natParser() : Parser +``` + + +## Function `parseNat` +``` motoko no-repl +func parseNat() : Parser +``` + diff --git a/docs/Candid/Parser/NatX.md b/docs/Candid/Parser/NatX.md new file mode 100644 index 0000000..89ac957 --- /dev/null +++ b/docs/Candid/Parser/NatX.md @@ -0,0 +1,7 @@ +# Candid/Parser/NatX + +## Function `natXParser` +``` motoko no-repl +func natXParser() : Parser +``` + diff --git a/docs/Candid/Parser/Option.md b/docs/Candid/Parser/Option.md new file mode 100644 index 0000000..efd2a96 --- /dev/null +++ b/docs/Candid/Parser/Option.md @@ -0,0 +1,13 @@ +# Candid/Parser/Option + +## Function `optionParser` +``` motoko no-repl +func optionParser(candidParser : () -> Parser) : Parser +``` + + +## Function `nullParser` +``` motoko no-repl +func nullParser() : Parser +``` + diff --git a/docs/Candid/Parser/Principal.md b/docs/Candid/Parser/Principal.md new file mode 100644 index 0000000..e5ef0d1 --- /dev/null +++ b/docs/Candid/Parser/Principal.md @@ -0,0 +1,7 @@ +# Candid/Parser/Principal + +## Function `principalParser` +``` motoko no-repl +func principalParser() : Parser +``` + diff --git a/docs/Candid/Parser/Record.md b/docs/Candid/Parser/Record.md new file mode 100644 index 0000000..0c9e737 --- /dev/null +++ b/docs/Candid/Parser/Record.md @@ -0,0 +1,19 @@ +# Candid/Parser/Record + +## Function `recordParser` +``` motoko no-repl +func recordParser(candidParser : () -> Parser) : Parser +``` + + +## Function `fieldParser` +``` motoko no-repl +func fieldParser(valueParser : () -> Parser) : Parser +``` + + +## Function `keyParser` +``` motoko no-repl +func keyParser() : Parser +``` + diff --git a/docs/Candid/Parser/Text.md b/docs/Candid/Parser/Text.md new file mode 100644 index 0000000..780b1f6 --- /dev/null +++ b/docs/Candid/Parser/Text.md @@ -0,0 +1,13 @@ +# Candid/Parser/Text + +## Function `textParser` +``` motoko no-repl +func textParser() : Parser +``` + + +## Function `parseText` +``` motoko no-repl +func parseText() : Parser +``` + diff --git a/docs/Candid/Parser/Variant.md b/docs/Candid/Parser/Variant.md new file mode 100644 index 0000000..3cc3544 --- /dev/null +++ b/docs/Candid/Parser/Variant.md @@ -0,0 +1,7 @@ +# Candid/Parser/Variant + +## Function `variantParser` +``` motoko no-repl +func variantParser(candidParser : () -> Parser) : Parser +``` + diff --git a/docs/Candid/Parser/lib.md b/docs/Candid/Parser/lib.md new file mode 100644 index 0000000..6008015 --- /dev/null +++ b/docs/Candid/Parser/lib.md @@ -0,0 +1,19 @@ +# Candid/Parser/lib + +## Function `parse` +``` motoko no-repl +func parse(text : Text) : [Candid] +``` + + +## Function `multiValueCandidParser` +``` motoko no-repl +func multiValueCandidParser() : Parser +``` + + +## Function `candidParser` +``` motoko no-repl +func candidParser() : Parser +``` + diff --git a/docs/Candid/ToText.md b/docs/Candid/ToText.md new file mode 100644 index 0000000..d13d5ca --- /dev/null +++ b/docs/Candid/ToText.md @@ -0,0 +1,7 @@ +# Candid/ToText + +## Function `toText` +``` motoko no-repl +func toText(candid_values : [Candid]) : Text +``` + diff --git a/docs/Candid/Types.md b/docs/Candid/Types.md new file mode 100644 index 0000000..c34cff3 --- /dev/null +++ b/docs/Candid/Types.md @@ -0,0 +1,14 @@ +# Candid/Types + +## Type `KeyValuePair` +``` motoko no-repl +type KeyValuePair = (Text, Candid) +``` + + +## Type `Candid` +``` motoko no-repl +type Candid = {#Int : Int; #Int8 : Int8; #Int16 : Int16; #Int32 : Int32; #Int64 : Int64; #Nat : Nat; #Nat8 : Nat8; #Nat16 : Nat16; #Nat32 : Nat32; #Nat64 : Nat64; #Bool : Bool; #Float : Float; #Text : Text; #Blob : Blob; #Null; #Empty; #Principal : Principal; #Option : Candid; #Array : [Candid]; #Record : [KeyValuePair]; #Variant : KeyValuePair} +``` + +A standard representation of the Candid type diff --git a/docs/Candid/lib.md b/docs/Candid/lib.md new file mode 100644 index 0000000..056c6c5 --- /dev/null +++ b/docs/Candid/lib.md @@ -0,0 +1,14 @@ +# Candid/lib + +## Type `Candid` +``` motoko no-repl +type Candid = T.Candid +``` + +A representation of the Candid format with variants for all possible types. + +## Function `fromText` +``` motoko no-repl +func fromText(t : Text) : [Candid] +``` + diff --git a/docs/JSON/FromText.md b/docs/JSON/FromText.md new file mode 100644 index 0000000..ff37fbd --- /dev/null +++ b/docs/JSON/FromText.md @@ -0,0 +1,15 @@ +# JSON/FromText + +## Function `fromText` +``` motoko no-repl +func fromText(rawText : Text) : Blob +``` + +Converts JSON text to a serialized Candid blob that can be decoded to motoko values using `from_candid()` + +## Function `toCandid` +``` motoko no-repl +func toCandid(rawText : Text) : Candid +``` + +Convert JSON text to a Candid value diff --git a/docs/JSON/ToText.md b/docs/JSON/ToText.md new file mode 100644 index 0000000..9bc7ded --- /dev/null +++ b/docs/JSON/ToText.md @@ -0,0 +1,15 @@ +# JSON/ToText + +## Function `toText` +``` motoko no-repl +func toText(blob : Blob, keys : [Text]) : Text +``` + +Converts serialized Candid blob to JSON text + +## Function `fromCandid` +``` motoko no-repl +func fromCandid(candid : Candid) : Text +``` + +Convert a Candid value to JSON text diff --git a/docs/JSON/lib.md b/docs/JSON/lib.md new file mode 100644 index 0000000..8c2f1fd --- /dev/null +++ b/docs/JSON/lib.md @@ -0,0 +1,8 @@ +# JSON/lib +A module for converting between JSON and Motoko values. + +## Type `JSON` +``` motoko no-repl +type JSON = JSON.JSON +``` + diff --git a/docs/UrlEncoded/FromText.md b/docs/UrlEncoded/FromText.md new file mode 100644 index 0000000..bcf41fe --- /dev/null +++ b/docs/UrlEncoded/FromText.md @@ -0,0 +1,15 @@ +# UrlEncoded/FromText + +## Function `fromText` +``` motoko no-repl +func fromText(text : Text) : Blob +``` + +Converts a Url-Encoded Text to a serialized Candid Record + +## Function `toCandid` +``` motoko no-repl +func toCandid(text : Text) : Candid +``` + +Converts a Url-Encoded Text to a Candid Record diff --git a/docs/UrlEncoded/Parser.md b/docs/UrlEncoded/Parser.md new file mode 100644 index 0000000..730b87c --- /dev/null +++ b/docs/UrlEncoded/Parser.md @@ -0,0 +1,7 @@ +# UrlEncoded/Parser + +## Function `parseValue` +``` motoko no-repl +func parseValue(text : Text) : Candid +``` + diff --git a/docs/UrlEncoded/ToText.md b/docs/UrlEncoded/ToText.md new file mode 100644 index 0000000..6b85f22 --- /dev/null +++ b/docs/UrlEncoded/ToText.md @@ -0,0 +1,15 @@ +# UrlEncoded/ToText + +## Function `toText` +``` motoko no-repl +func toText(blob : Blob, keys : [Text]) : Text +``` + +Converts a serialized Candid blob to a URL-Encoded string. + +## Function `fromCandid` +``` motoko no-repl +func fromCandid(candid : Candid) : Text +``` + +Convert a Candid Record to a URL-Encoded string. diff --git a/docs/UrlEncoded/lib.md b/docs/UrlEncoded/lib.md new file mode 100644 index 0000000..42640bb --- /dev/null +++ b/docs/UrlEncoded/lib.md @@ -0,0 +1,2 @@ +# UrlEncoded/lib +A module for converting between Motoko values and Url-Encoded `Text`. diff --git a/docs/Utils.md b/docs/Utils.md new file mode 100644 index 0000000..c19b496 --- /dev/null +++ b/docs/Utils.md @@ -0,0 +1,25 @@ +# Utils + +## Function `subText` +``` motoko no-repl +func subText(text : Text, start : Nat, end : Nat) : Text +``` + + +## Function `cmpRecords` +``` motoko no-repl +func cmpRecords(a : (Text, Any), b : (Text, Any)) : Order.Order +``` + + +## Function `stripStart` +``` motoko no-repl +func stripStart(text : Text, prefix : Text.Pattern) : Text +``` + + +## Function `log2` +``` motoko no-repl +func log2(n : Float) : Float +``` + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..840b6e6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +# Index + +* [Candid/Decoder](Candid/Decoder.md) +* [Candid/Encoder](Candid/Encoder.md) +* [Candid/Parser/Array](Candid/Parser/Array.md) +* [Candid/Parser/Blob](Candid/Parser/Blob.md) +* [Candid/Parser/Bool](Candid/Parser/Bool.md) +* [Candid/Parser/Common](Candid/Parser/Common.md) +* [Candid/Parser/Float](Candid/Parser/Float.md) +* [Candid/Parser/Int](Candid/Parser/Int.md) +* [Candid/Parser/IntX](Candid/Parser/IntX.md) +* [Candid/Parser/Nat](Candid/Parser/Nat.md) +* [Candid/Parser/NatX](Candid/Parser/NatX.md) +* [Candid/Parser/Option](Candid/Parser/Option.md) +* [Candid/Parser/Principal](Candid/Parser/Principal.md) +* [Candid/Parser/Record](Candid/Parser/Record.md) +* [Candid/Parser/Text](Candid/Parser/Text.md) +* [Candid/Parser/Variant](Candid/Parser/Variant.md) +* [Candid/Parser/lib](Candid/Parser/lib.md) +* [Candid/ToText](Candid/ToText.md) +* [Candid/Types](Candid/Types.md) +* [Candid/lib](Candid/lib.md) +* [JSON/FromText](JSON/FromText.md) +* [JSON/ToText](JSON/ToText.md) +* [JSON/lib](JSON/lib.md) A module for converting between JSON and Motoko values. +* [UrlEncoded/FromText](UrlEncoded/FromText.md) +* [UrlEncoded/Parser](UrlEncoded/Parser.md) +* [UrlEncoded/ToText](UrlEncoded/ToText.md) +* [UrlEncoded/lib](UrlEncoded/lib.md) A module for converting between Motoko values and Url-Encoded `Text`. +* [Utils](Utils.md) diff --git a/makefile b/makefile index 65e93b5..078a134 100644 --- a/makefile +++ b/makefile @@ -4,8 +4,8 @@ compile-tests: bash compile-tests.sh $(file) no-warn: - find src -type f -name '*.mo' -print0 | xargs -0 $(shell vessel bin)/moc -r $(shell mops sources) -Werror -wasi-system-api + find src -type f -name '*.mo' -print0 | xargs -0 $(shell mocv bin)/moc -r $(shell mops sources) -Werror -wasi-system-api docs: - $(shell vessel bin)/mo-doc - $(shell vessel bin)/mo-doc --format plain + $(shell mocv bin)/mo-doc + $(shell mocv bin)/mo-doc --format plain diff --git a/readme.md b/readme.md index a3a6805..6492a68 100644 --- a/readme.md +++ b/readme.md @@ -9,73 +9,79 @@ A serialisation and deserialisation library for Motoko. - Run `mops install serde`, in your project directory ## Usage - -### JSON - +#### Import statement ```motoko - import serdeJson "mo:serde/JSON"; - - type User = { - name: Text; - id: Nat; - }; - - let blob = serdeJson.fromText("{\"name\": \"bar\", \"id\": 112}"); - let user : ?User = from_candid(blob); - - assert user == ?{ name = "bar"; id = 112 }; - +import { JSON; Candid; UrlEncoded } "mo:serde"; ``` -### Candid Text -```motoko - import serdeCandid "mo:serde/Candid"; +#### JSON - type User = { - name: Text; - id: Nat; - }; +- Converting a specific data type, for example `User`: + ```motoko + type User = { + name: Text; + id: Nat; + email: ?Text; + }; + ``` - let blob = serdeCandid.fromText("(record({ name = \"bar\"; id = 112 })"); - let user : ?User = from_candid(blob); + - JSON to Motoko + ```motoko + let blob = JSON.fromText("{\"name\": \"bar\", \"id\": 112}", null); + let user : ?User = from_candid(blob); - assert user == ?{ name = "bar"; id = 112 }; + assert user == ?{ name = "bar"; id = 112; email = null }; + ``` -``` + - Motoko to JSON + ```motoko + let UserKeys = ["name", "id", "email"]; -### URL-Encoded Pairs -Serialization and deserialization for `application/x-www-form-urlencoded`. + let user : User = { name = "bar"; id = 112; email = null }; + let blob = to_candid(user); + let json = JSON.toText(blob, UserKeys, null); -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`." + assert json == "{\"name\": \"bar\", \"id\": 112, \"email\": null}"; + ``` +- Renaming field keys (Useful for fields with reserved keywords in Motoko ) ```motoko - import serde_urlencoded "mo:serde/URLEncoded"; - - type User = { - name: Text; - id: Nat; + import Serde "mo:serde"; + + // type JsonItemSchemaWithReservedKeys = { + // type: Text; // reserved + // label: Text; // reserved + // id: Nat; + // }; + + type Item = { + item_type: Text; + item_label: Text; + id: Nat + }; + + let jsonText = "{\"type\": \"bar\", \"label\": \"foo\", \"id\": 112}"; + let options : Serde.Options = { + renameKeys = [("type", "item_type"), ("label", "item_label")] }; - - let payload = "users[0][id]=123&users[0][name]=John&users[1][id]=456&users[1][name]=Jane"; - - let blob = serde_urlencoded.fromText(payload); - let res : ?{ users: [User]} = from_candid(blob); - - assert res == ?{ users = [ - { - name = "John"; - id = 123; - }, - { - name = "Jane"; - id = 456; - }, - ] }; + let blob = Serde.JSON.fromText(jsonText, ?options); + let renamedKeys : ?Item = from_candid(blob); + + assert renamedKeys == ?{ item_type = "bar"; item_label = "foo"; id = 112 }; ``` +For more usage examples see [usage.md](https://github.com/NatLabs/serde/blob/main/usage.md): +- [Candid Text](https://github.com/NatLabs/serde/blob/main/usage.md#candid-text) +- [URL-Encoded Pairs](https://github.com/NatLabs/serde/blob/main/usage.md#url-encoded-pairs) + +## Limitations +- Requires that the user provides a list of record keys and variant names when converting from Motoko. This is because the candid format used for serializing Motoko stores record keys as their hash, making it impossible to retrieve the original key names. +- Does not have specific syntax to support the conversion between `Blob`, `Principal`, and Bounded `Nat`/`Int` types. + + ## Tests - Install [mops](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/#/docs/install) -- Install [vessel](https://github.com/dfinity/vessel) +- Install [mocv](https://github.com/ZenVoich/mocv) - Install [wasmtime](https://github.com/bytecodealliance/wasmtime/blob/main/README.md#wasmtime) -- Run `make compile-tests` \ No newline at end of file +- Run `mops test` in the project directory \ No newline at end of file diff --git a/src/Candid/Decoder.mo b/src/Candid/Blob/Decoder.mo similarity index 84% rename from src/Candid/Decoder.mo rename to src/Candid/Blob/Decoder.mo index b8d5241..725bc61 100644 --- a/src/Candid/Decoder.mo +++ b/src/Candid/Blob/Decoder.mo @@ -20,8 +20,8 @@ import Tag "mo:candid/Tag"; import { hashName } "mo:candid/Tag"; -import T "Types"; -import U "../Utils"; +import T "../Types"; +import U "../../Utils"; module { type Arg = Arg.Arg; @@ -30,16 +30,34 @@ module { type RecordFieldType = Type.RecordFieldType; type RecordFieldValue = Value.RecordFieldValue; + type TrieMap = TrieMap.TrieMap; type Candid = T.Candid; type KeyValuePair = T.KeyValuePair; - public func decode(blob : Blob, recordKeys : [Text]) : [Candid] { + /// Decodes a blob encoded in the candid format into a list of the [Candid](./Types.mo#Candid) type in motoko + /// + /// ### Inputs + /// - **blob** - A blob encoded in the candid format + /// - **record_keys** - The record keys to use when decoding a record. + /// - **options** - An optional arguement to specify options for decoding. + + public func decode(blob : Blob, record_keys: [Text], options: ?T.Options) : [Candid] { let res = Decoder.decode(blob); + let renaming_map : TrieMap = switch (options) { + case (?{renameKeys}) TrieMap.fromEntries(renameKeys.vals(), Text.equal, Text.hash); + case (_) TrieMap.TrieMap(Text.equal, Text.hash); + }; + let keyEntries = Iter.map( - recordKeys.vals(), - func(key : Text) : (Nat32, Text) { - (hashName(key), key); + record_keys.vals(), + func(original_key : Text) : (Nat32, Text) { + let new_key = switch(renaming_map.get(original_key)) { + case (?key) key; + case (_) original_key; + }; + + (hashName(original_key), new_key); }, ); diff --git a/src/Candid/Encoder.mo b/src/Candid/Blob/Encoder.mo similarity index 63% rename from src/Candid/Encoder.mo rename to src/Candid/Blob/Encoder.mo index 7e59cce..eb8ff59 100644 --- a/src/Candid/Encoder.mo +++ b/src/Candid/Blob/Encoder.mo @@ -3,6 +3,7 @@ import Blob "mo:base/Blob"; import Debug "mo:base/Debug"; import Result "mo:base/Result"; import Prelude "mo:base/Prelude"; +import Text "mo:base/Text"; import Encoder "mo:candid/Encoder"; import Decoder "mo:candid/Decoder"; @@ -10,8 +11,9 @@ import Arg "mo:candid/Arg"; import Value "mo:candid/Value"; import Type "mo:candid/Type"; -import T "Types"; -import U "../Utils"; +import T "../Types"; +import U "../../Utils"; +import TrieMap "mo:base/TrieMap"; module { type Arg = Arg.Arg; @@ -19,33 +21,42 @@ module { type Value = Value.Value; type RecordFieldType = Type.RecordFieldType; type RecordFieldValue = Value.RecordFieldValue; + type TrieMap = TrieMap.TrieMap; type Candid = T.Candid; type KeyValuePair = T.KeyValuePair; - public func encode(candid_values : [Candid]) : Blob { - let args = toArgs(candid_values); + public func encode(candid_values : [Candid], options: ?T.Options) : Blob { + let renaming_map = TrieMap.TrieMap(Text.equal, Text.hash); + + ignore do ? { + let renameKeys = options!.renameKeys; + for ((k, v) in renameKeys.vals()) { + renaming_map.put(k, v); + }; + }; + + let args = toArgs(candid_values, renaming_map); Encoder.encode(args); }; - public func encodeOne(candid : Candid) : Blob { - let args = toArgs([candid]); - Encoder.encode(args); + public func encodeOne(candid : Candid, options: ?T.Options) : Blob { + encode([candid], options); }; - public func toArgs(candid_values : [Candid]) : [Arg] { + public func toArgs(candid_values : [Candid], renaming_map: TrieMap) : [Arg] { Array.map( candid_values, func(candid : Candid) : Arg { { - _type = toArgType(candid); - value = toArgValue(candid); + _type = toArgType(candid, renaming_map); + value = toArgValue(candid, renaming_map); }; }, ); }; - func toArgType(candid : Candid) : Type { + func toArgType(candid : Candid, renaming_map: TrieMap) : Type { switch (candid) { case (#Nat(_)) #nat; case (#Nat8(_)) #nat8; @@ -71,11 +82,11 @@ module { case (#Null) #_null; case (#Option(optType)) { - #opt(toArgType(optType)); + #opt(toArgType(optType, renaming_map)); }; case (#Array(arr)) { if (arr.size() > 0) { - #vector(toArgType(arr[0])); + #vector(toArgType(arr[0], renaming_map)); } else { #vector(#empty); }; @@ -85,9 +96,11 @@ module { let newRecords = Array.map( Array.sort(records, U.cmpRecords), func((key, val) : KeyValuePair) : RecordFieldType { + let renamed_key = get_renamed_key(renaming_map, key); + { - tag = #name(key); - _type = toArgType(val); + tag = #name(renamed_key); + _type = toArgType(val, renaming_map); }; }, ); @@ -96,10 +109,11 @@ module { }; case (#Variant((key, val))) { + let renamed_key = get_renamed_key(renaming_map, key); #variant([{ - tag = #name(key); - _type = toArgType(val); + tag = #name(renamed_key); + _type = toArgType(val, renaming_map); }]); }; @@ -107,7 +121,7 @@ module { }; }; - func toArgValue(candid : Candid) : Value { + func toArgValue(candid : Candid, renaming_map: TrieMap) : Value { switch (candid) { case (#Nat(n)) #nat(n); case (#Nat8(n)) #nat8(n); @@ -132,13 +146,13 @@ module { case (#Null) #_null; case (#Option(optVal)) { - #opt(?toArgValue(optVal)); + #opt(?toArgValue(optVal, renaming_map)); }; case (#Array(arr)) { let transformedArr = Array.map( arr, func(elem : Candid) : Value { - toArgValue(elem); + toArgValue(elem, renaming_map); }, ); @@ -162,9 +176,11 @@ module { let newRecords = Array.map( records, func((key, val) : KeyValuePair) : RecordFieldValue { + let renamed_key = get_renamed_key(renaming_map, key); + { - tag = #name(key); - value = toArgValue(val); + tag = #name(renamed_key); + value = toArgValue(val, renaming_map); }; }, ); @@ -173,14 +189,22 @@ module { }; case (#Variant((key, val))) { + let renamed_key = get_renamed_key(renaming_map, key); #variant({ - tag = #name(key); - value = toArgValue(val); + tag = #name(renamed_key); + value = toArgValue(val, renaming_map); }); }; case (c) Prelude.unreachable(); }; }; + + func get_renamed_key(renaming_map: TrieMap, key: Text) : Text { + switch (renaming_map.get(key)) { + case (?v) v; + case (_) key; + }; + } }; diff --git a/src/Candid/Parser/Array.mo b/src/Candid/Text/Parser/Array.mo similarity index 97% rename from src/Candid/Parser/Array.mo rename to src/Candid/Text/Parser/Array.mo index 11aa74f..207e909 100644 --- a/src/Candid/Parser/Array.mo +++ b/src/Candid/Text/Parser/Array.mo @@ -4,7 +4,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace } "Common"; diff --git a/src/Candid/Parser/Blob.mo b/src/Candid/Text/Parser/Blob.mo similarity index 98% rename from src/Candid/Parser/Blob.mo rename to src/Candid/Text/Parser/Blob.mo index 1615ebe..1338cfd 100644 --- a/src/Candid/Parser/Blob.mo +++ b/src/Candid/Text/Parser/Blob.mo @@ -5,7 +5,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; hexChar; fromHex } "Common"; module { diff --git a/src/Candid/Parser/Bool.mo b/src/Candid/Text/Parser/Bool.mo similarity index 96% rename from src/Candid/Parser/Bool.mo rename to src/Candid/Text/Parser/Bool.mo index 017e44a..e6da61a 100644 --- a/src/Candid/Parser/Bool.mo +++ b/src/Candid/Text/Parser/Bool.mo @@ -5,7 +5,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; hexChar; fromHex } "Common"; module { diff --git a/src/Candid/Parser/Common.mo b/src/Candid/Text/Parser/Common.mo similarity index 99% rename from src/Candid/Parser/Common.mo rename to src/Candid/Text/Parser/Common.mo index 1ffc11e..d533dd5 100644 --- a/src/Candid/Parser/Common.mo +++ b/src/Candid/Text/Parser/Common.mo @@ -8,7 +8,7 @@ import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; import NatX "mo:xtended-numbers/NatX"; -import Candid "../Types"; +import Candid "../../Types"; module { type Candid = Candid.Candid; diff --git a/src/Candid/Parser/Float.mo b/src/Candid/Text/Parser/Float.mo similarity index 97% rename from src/Candid/Parser/Float.mo rename to src/Candid/Text/Parser/Float.mo index 688c4d1..b9db4aa 100644 --- a/src/Candid/Parser/Float.mo +++ b/src/Candid/Text/Parser/Float.mo @@ -4,7 +4,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { listToNat } "Common"; import { parseInt } "Int"; diff --git a/src/Candid/Parser/Int.mo b/src/Candid/Text/Parser/Int.mo similarity index 98% rename from src/Candid/Parser/Int.mo rename to src/Candid/Text/Parser/Int.mo index 761b0d0..4618698 100644 --- a/src/Candid/Parser/Int.mo +++ b/src/Candid/Text/Parser/Int.mo @@ -5,7 +5,7 @@ import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; import NatX "mo:xtended-numbers/NatX"; -import Candid "../Types"; +import Candid "../../Types"; import { listToNat } "Common"; import { parseNat } "Nat"; diff --git a/src/Candid/Parser/IntX.mo b/src/Candid/Text/Parser/IntX.mo similarity index 98% rename from src/Candid/Parser/IntX.mo rename to src/Candid/Text/Parser/IntX.mo index 0fa6a27..4246214 100644 --- a/src/Candid/Parser/IntX.mo +++ b/src/Candid/Text/Parser/IntX.mo @@ -9,7 +9,7 @@ import Int64 "mo:base/Int64"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; toText } "Common"; import { parseInt; intParser } "Int"; diff --git a/src/Candid/Parser/Nat.mo b/src/Candid/Text/Parser/Nat.mo similarity index 98% rename from src/Candid/Parser/Nat.mo rename to src/Candid/Text/Parser/Nat.mo index 516d42c..5980150 100644 --- a/src/Candid/Parser/Nat.mo +++ b/src/Candid/Text/Parser/Nat.mo @@ -7,7 +7,7 @@ import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; import NatX "mo:xtended-numbers/NatX"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; hexChar; fromHex; removeUnderscore; listToNat } "Common"; module { diff --git a/src/Candid/Parser/NatX.mo b/src/Candid/Text/Parser/NatX.mo similarity index 98% rename from src/Candid/Parser/NatX.mo rename to src/Candid/Text/Parser/NatX.mo index 7778ae3..f916c6e 100644 --- a/src/Candid/Parser/NatX.mo +++ b/src/Candid/Text/Parser/NatX.mo @@ -9,7 +9,7 @@ import Nat64 "mo:base/Nat64"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; toText } "Common"; import { parseNat; natParser } "Nat"; diff --git a/src/Candid/Parser/Option.mo b/src/Candid/Text/Parser/Option.mo similarity index 97% rename from src/Candid/Parser/Option.mo rename to src/Candid/Text/Parser/Option.mo index fba8414..338ff3c 100644 --- a/src/Candid/Parser/Option.mo +++ b/src/Candid/Text/Parser/Option.mo @@ -4,7 +4,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace } "Common"; diff --git a/src/Candid/Parser/Principal.mo b/src/Candid/Text/Parser/Principal.mo similarity index 97% rename from src/Candid/Parser/Principal.mo rename to src/Candid/Text/Parser/Principal.mo index f077b57..be12212 100644 --- a/src/Candid/Parser/Principal.mo +++ b/src/Candid/Text/Parser/Principal.mo @@ -6,7 +6,7 @@ import Principal "mo:base/Principal"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; toText } "Common"; module { diff --git a/src/Candid/Parser/Record.mo b/src/Candid/Text/Parser/Record.mo similarity index 98% rename from src/Candid/Parser/Record.mo rename to src/Candid/Text/Parser/Record.mo index 0129ba5..84658f0 100644 --- a/src/Candid/Parser/Record.mo +++ b/src/Candid/Text/Parser/Record.mo @@ -4,7 +4,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; hexChar; fromHex; toText } "Common"; import { parseText } "Text"; diff --git a/src/Candid/Parser/Text.mo b/src/Candid/Text/Parser/Text.mo similarity index 98% rename from src/Candid/Parser/Text.mo rename to src/Candid/Text/Parser/Text.mo index 540f3b3..4d015cf 100644 --- a/src/Candid/Parser/Text.mo +++ b/src/Candid/Text/Parser/Text.mo @@ -6,7 +6,7 @@ import Text "mo:base/Text"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; module{ type Candid = Candid.Candid; diff --git a/src/Candid/Parser/Variant.mo b/src/Candid/Text/Parser/Variant.mo similarity index 97% rename from src/Candid/Parser/Variant.mo rename to src/Candid/Text/Parser/Variant.mo index 37818e4..442ee6d 100644 --- a/src/Candid/Parser/Variant.mo +++ b/src/Candid/Text/Parser/Variant.mo @@ -4,7 +4,7 @@ import List "mo:base/List"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace } "Common"; import { keyParser; fieldParser } = "Record"; diff --git a/src/Candid/Parser/lib.mo b/src/Candid/Text/Parser/lib.mo similarity index 95% rename from src/Candid/Parser/lib.mo rename to src/Candid/Text/Parser/lib.mo index 38b4ebc..3686b3e 100644 --- a/src/Candid/Parser/lib.mo +++ b/src/Candid/Text/Parser/lib.mo @@ -2,11 +2,12 @@ import Char "mo:base/Char"; import Debug "mo:base/Debug"; import Iter "mo:base/Iter"; import List "mo:base/List"; +import TrieMap "mo:base/TrieMap"; import C "mo:parser-combinators/Combinators"; import P "mo:parser-combinators/Parser"; -import Candid "../Types"; +import Candid "../../Types"; import { ignoreSpace; any } "Common"; @@ -27,6 +28,7 @@ import { variantParser } "Variant"; module CandidParser { type Candid = Candid.Candid; type List = List.List; + type TrieMap = TrieMap.TrieMap; type Parser = P.Parser; @@ -35,7 +37,7 @@ module CandidParser { switch (parseCandid(chars)) { case (?candid) candid; - case (null) Debug.trap("Failed to parse Candid text for input: " # debug_show (chars)); + case (null) Debug.trap("Failed to parse Candid text from input: " # debug_show (chars)); }; }; diff --git a/src/Candid/ToText.mo b/src/Candid/Text/ToText.mo similarity index 93% rename from src/Candid/ToText.mo rename to src/Candid/Text/ToText.mo index 0a3fd67..062ebbb 100644 --- a/src/Candid/ToText.mo +++ b/src/Candid/Text/ToText.mo @@ -1,15 +1,17 @@ import Float "mo:base/Float"; import Text "mo:base/Text"; import Principal "mo:base/Principal"; +import TrieMap "mo:base/TrieMap"; import Itertools "mo:itertools/Iter"; -import Candid "Types"; +import CandidTypes "../Types"; -import U "../Utils"; +import U "../../Utils"; module { - type Candid = Candid.Candid; + type Candid = CandidTypes.Candid; + type TrieMap = TrieMap.TrieMap; public func toText(candid_values : [Candid]) : Text { var text = ""; @@ -18,7 +20,7 @@ module { for (val in candid_iter) { if (candid_iter.peek() == null){ - text #= candidToText(val); + text #= candidToText(val, ); } else { text #= candidToText(val) # ", "; }; diff --git a/src/Candid/Types.mo b/src/Candid/Types.mo index b7e7a29..0b95131 100644 --- a/src/Candid/Types.mo +++ b/src/Candid/Types.mo @@ -32,4 +32,10 @@ module { }; + /// Encoding and Decoding options + public type Options = { + /// Contains an array of tuples of the form (old_name, new_name) to rename the record keys. + renameKeys : [(Text, Text)]; + }; + }; diff --git a/src/Candid/lib.mo b/src/Candid/lib.mo index db78496..d6da587 100644 --- a/src/Candid/lib.mo +++ b/src/Candid/lib.mo @@ -5,10 +5,10 @@ import Debug "mo:base/Debug"; import Result "mo:base/Result"; import Prelude "mo:base/Prelude"; -import Encoder "Encoder"; -import Decoder "Decoder"; -import Parser "Parser"; -import ToText "ToText"; +import Encoder "Blob/Encoder"; +import Decoder "Blob/Decoder"; +import Parser "Text/Parser"; +import ToText "Text/ToText"; import T "Types"; diff --git a/src/JSON/FromText.mo b/src/JSON/FromText.mo index 7117f76..8015ff2 100644 --- a/src/JSON/FromText.mo +++ b/src/JSON/FromText.mo @@ -14,15 +14,16 @@ import JSON "mo:json/JSON"; import Candid "../Candid"; import U "../Utils"; +import CandidTypes "../Candid/Types"; module { type JSON = JSON.JSON; type Candid = Candid.Candid; /// Converts JSON text to a serialized Candid blob that can be decoded to motoko values using `from_candid()` - public func fromText(rawText : Text) : Blob { + public func fromText(rawText : Text, options: ?CandidTypes.Options) : Blob { let candid = toCandid(rawText); - Candid.encodeOne(candid); + Candid.encodeOne(candid, options); }; /// Convert JSON text to a Candid value diff --git a/src/JSON/ToText.mo b/src/JSON/ToText.mo index 252d90c..0f890ab 100644 --- a/src/JSON/ToText.mo +++ b/src/JSON/ToText.mo @@ -14,14 +14,15 @@ import NatX "mo:xtended-numbers/NatX"; import IntX "mo:xtended-numbers/IntX"; import Candid "../Candid"; +import CandidTypes "../Candid/Types"; module { type JSON = JSON.JSON; type Candid = Candid.Candid; /// Converts serialized Candid blob to JSON text - public func toText(blob : Blob, keys : [Text]) : Text { - let candid = Candid.decode(blob, keys); + public func toText(blob : Blob, keys : [Text], options: ?CandidTypes.Options) : Text { + let candid = Candid.decode(blob, keys, options); fromCandid(candid[0]); }; @@ -84,7 +85,8 @@ module { let (key, val) = variant; #Object([("#" # key, candidToJSON(val))]); }; - + + // #Blob(_), #Empty and #Principal(_) are not supported case (_) Prelude.unreachable(); }; }; diff --git a/src/UrlEncoded/FromText.mo b/src/UrlEncoded/FromText.mo index 48c9f88..e251662 100644 --- a/src/UrlEncoded/FromText.mo +++ b/src/UrlEncoded/FromText.mo @@ -17,6 +17,7 @@ import Prelude "mo:base/Prelude"; import itertools "mo:itertools/Iter"; import Candid "../Candid"; +import CandidTypes "../Candid/Types"; import { parseValue } "./Parser"; import U "../Utils"; @@ -39,9 +40,9 @@ module { func newMap() : NestedTrieMap = TrieMap.TrieMap(Text.equal, Text.hash); /// Converts a Url-Encoded Text to a serialized Candid Record - public func fromText(text : Text) : Blob { + public func fromText(text : Text, options: ?CandidTypes.Options) : Blob { let candid = toCandid(text); - Candid.encodeOne(candid); + Candid.encodeOne(candid, options); }; /// Converts a Url-Encoded Text to a Candid Record @@ -203,6 +204,7 @@ module { return #Array(array); }; + // check if single value is a variant if (triemap.size() == 1) { let (variant_key, value) = switch (triemap.entries().next()) { case (?(k, v))(k, v); diff --git a/src/UrlEncoded/ToText.mo b/src/UrlEncoded/ToText.mo index e786a93..aad3ead 100644 --- a/src/UrlEncoded/ToText.mo +++ b/src/UrlEncoded/ToText.mo @@ -20,14 +20,15 @@ import itertools "mo:itertools/Iter"; import Candid "../Candid"; import U "../Utils"; +import CandidTypes "../Candid/Types"; module { type Candid = Candid.Candid; type TrieMap = TrieMap.TrieMap; /// Converts a serialized Candid blob to a URL-Encoded string. - public func toText(blob : Blob, keys : [Text]) : Text { - let candid = Candid.decode(blob, keys); + public func toText(blob : Blob, keys : [Text], options: ?CandidTypes.Options) : Text { + let candid = Candid.decode(blob, keys, options); fromCandid(candid[0]); }; diff --git a/src/UrlEncoded/lib.mo b/src/UrlEncoded/lib.mo index 90c7d15..25c88dc 100644 --- a/src/UrlEncoded/lib.mo +++ b/src/UrlEncoded/lib.mo @@ -22,4 +22,5 @@ module { public let { fromText; toCandid } = FromText; public let { toText; fromCandid } = ToText; + }; diff --git a/src/lib.mo b/src/lib.mo new file mode 100644 index 0000000..cb2a69b --- /dev/null +++ b/src/lib.mo @@ -0,0 +1,16 @@ +import CandidTypes "Candid/Types"; + +import UrlEncodedModule "UrlEncoded"; +import JsonModule "JSON"; +import CandidModule "Candid"; + +module { + + public type Options = CandidTypes.Options; + + public type Candid = CandidTypes.Candid; + + public let Candid = CandidModule; + public let JSON = JsonModule; + public let URLEncoded = UrlEncodedModule; +} \ No newline at end of file diff --git a/test.md b/test.md deleted file mode 100644 index d3f4822..0000000 --- a/test.md +++ /dev/null @@ -1,3 +0,0 @@ -https://m7sm4-2iaaa-aaaab-qabra-cai.raw.ic0.app/?tag=3994830100 - -https://m7sm4-2iaaa-aaaab-qabra-cai.raw.ic0.app/?tag=55444955 \ No newline at end of file diff --git a/tests/Candid.Test.mo b/tests/Candid.Test.mo index 2c90528..15d1ebb 100644 --- a/tests/Candid.Test.mo +++ b/tests/Candid.Test.mo @@ -1,3 +1,4 @@ +// @testmode wasi import Blob "mo:base/Blob"; import Debug "mo:base/Debug"; import Iter "mo:base/Iter"; @@ -7,7 +8,7 @@ import Principal "mo:base/Principal"; import ActorSpec "./utils/ActorSpec"; import Candid "../src/Candid"; -import Encoder "../src/Candid/Encoder"; +import Encoder "../src/Candid/Blob/Encoder"; let { assertTrue; @@ -20,6 +21,8 @@ let { run; } = ActorSpec; +type Candid = Candid.Candid; + let success = run( [ describe( @@ -28,12 +31,38 @@ let success = run( describe( "decode()", [ + it( + "renaming keys", + do { + let motoko = [{ name = "candid"; arr = [1, 2, 3, 4] }, { name = "motoko"; arr = [5, 6, 7, 8] }, { name = "rust"; arr = [9, 10, 11, 12] }]; + let blob = to_candid (motoko); + let options = { renameKeys = [("arr", "array"), ("name", "username")] }; + let candid = Candid.decode(blob, ["name", "arr"], ?options); + + candid == [ + #Array([ + #Record([ + ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), + ("username", #Text("candid")), + ]), + #Record([ + ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), + ("username", #Text("motoko")), + ]), + #Record([ + ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), + ("username", #Text("rust")), + ]), + ]) + ]; + } + ), it( "record type: {name: Text}", do { let motoko = { name = "candid" }; let blob = to_candid (motoko); - let candid = Candid.decode(blob, ["name"]); + let candid = Candid.decode(blob, ["name"], null); candid == [#Record([("name", #Text("candid"))])]; }, @@ -43,7 +72,7 @@ let success = run( do { let arr = [1, 2, 3, 4]; let blob = to_candid (arr); - let candid = Candid.decode(blob, []); + let candid = Candid.decode(blob, [], null); candid == [#Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])]; }, @@ -57,8 +86,8 @@ let success = run( let bytes_array = to_candid (motoko_blob); let bytes_blob = to_candid (motoko_blob); - let candid_array = Candid.decode(bytes_array, []); - let candid_blob = Candid.decode(bytes_blob, []); + let candid_array = Candid.decode(bytes_array, [], null); + let candid_blob = Candid.decode(bytes_blob, [], null); assertAllTrue([ // All [Nat8] types are decoded as #Blob @@ -91,11 +120,11 @@ let success = run( let record_blob = to_candid (record); let array_blob = to_candid (array); - let text_candid = Candid.decode(text_blob, ["text"]); - let nat_candid = Candid.decode(nat_blob, ["nat"]); - let bool_candid = Candid.decode(bool_blob, ["bool"]); - let record_candid = Candid.decode(record_blob, ["record", "site"]); - let array_candid = Candid.decode(array_blob, ["array"]); + let text_candid = Candid.decode(text_blob, ["text"], null); + let nat_candid = Candid.decode(nat_blob, ["nat"], null); + let bool_candid = Candid.decode(bool_blob, ["bool"], null); + let record_candid = Candid.decode(record_blob, ["record", "site"], null); + let array_candid = Candid.decode(array_blob, ["array"], null); assertAllTrue([ text_candid == [#Variant("text", #Text("hello"))], @@ -139,7 +168,7 @@ let success = run( ]; let blob = to_candid (users); - let candid = Candid.decode(blob, record_keys); + let candid = Candid.decode(blob, record_keys, null); candid == [ #Array([ @@ -171,6 +200,33 @@ let success = run( describe( "encode()", [ + it("renaming keys", do { + let candid : Candid = #Array([ + #Record([ + ("array", #Array([#Nat(1), #Nat(2), #Nat(3), #Nat(4)])), + ("name", #Text("candid")), + ]), + #Record([ + ("array", #Array([#Nat(5), #Nat(6), #Nat(7), #Nat(8)])), + ("name", #Text("motoko")), + ]), + #Record([ + ("array", #Array([#Nat(9), #Nat(10), #Nat(11), #Nat(12)])), + ("name", #Text("rust")), + ]), + ]); + + type Data = { + language: Text; + daily_downloads: [Nat] + }; + + let options = { renameKeys = [("array", "daily_downloads"), ("name", "language")] }; + let blob = Candid.encodeOne(candid, ?options); + let motoko : ?[Data] = from_candid (blob); + // true + motoko == ?[{ language = "candid"; daily_downloads = [1, 2, 3, 4] }, { language = "motoko"; daily_downloads = [5, 6, 7, 8] }, { language = "rust"; daily_downloads = [9, 10, 11, 12] }]; + }), it( "record type {name: Text}", do { @@ -179,7 +235,7 @@ let success = run( name : Text; }; - let blob = Candid.encodeOne(candid); + let blob = Candid.encodeOne(candid, null); let user : ?User = from_candid (blob); user == ?{ name = "candid" }; @@ -193,8 +249,8 @@ let success = run( let candid_1 = #Array([#Nat8(1 : Nat8), #Nat8(2 : Nat8), #Nat8(3 : Nat8), #Nat8(4 : Nat8)]); let candid_2 = #Blob(motoko_blob); - let serialized_1 = Candid.encodeOne(candid_1); - let serialized_2 = Candid.encodeOne(candid_2); + let serialized_1 = Candid.encodeOne(candid_1, null); + let serialized_2 = Candid.encodeOne(candid_2, null); let blob_1 : ?Blob = from_candid (serialized_1); let blob_2 : ?Blob = from_candid (serialized_2); @@ -210,47 +266,47 @@ let success = run( ]); }, ), - it( - "variant", - do { - - type Variant = { - #text : Text; - #nat : Nat; - #bool : Bool; - #record : { site : Text }; - #array : [Nat]; - }; - - let text = #Variant("text", #Text("hello")); - let nat = #Variant("nat", #Nat(123)); - let bool = #Variant("bool", #Bool(true)); - let record = #Variant("record", #Record([("site", #Text("github"))])); - let array = #Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)])); - - let text_blob = Candid.encodeOne(text); - let nat_blob = Candid.encodeOne(nat); - let bool_blob = Candid.encodeOne(bool); - let record_blob = Candid.encodeOne(record); - let array_blob = Candid.encodeOne(array); - - let text_val : ?Variant = from_candid (text_blob); - let nat_val : ?Variant = from_candid (nat_blob); - let bool_val : ?Variant = from_candid (bool_blob); - let record_val : ?Variant = from_candid (record_blob); - let array_val : ?Variant = from_candid (array_blob); - - assertAllTrue([ - text_val == ?#text("hello"), - nat_val == ?#nat(123), - bool_val == ?#bool(true), - record_val == ?#record({ - site = "github"; - }), - array_val == ?#array([1, 2, 3]), - ]); - }, - ), + // it( + // "variant", + // do { + + // type Variant = { + // #text : Text; + // #nat : Nat; + // #bool : Bool; + // #record : { site : Text }; + // #array : [Nat]; + // }; + + // let text = #Variant("text", #Text("hello")); + // let nat = #Variant("nat", #Nat(123)); + // let bool = #Variant("bool", #Bool(true)); + // let record = #Variant("record", #Record([("site", #Text("github"))])); + // let array = #Variant("array", #Array([#Nat(1), #Nat(2), #Nat(3)])); + + // let text_blob = Candid.encodeOne(text, null); + // let nat_blob = Candid.encodeOne(nat, null); + // let bool_blob = Candid.encodeOne(bool, null); + // let record_blob = Candid.encodeOne(record, null); + // let array_blob = Candid.encodeOne(array, null); + + // let text_val : ?Variant = from_candid (text_blob); + // let nat_val : ?Variant = from_candid (nat_blob); + // let bool_val : ?Variant = from_candid (bool_blob); + // let record_val : ?Variant = from_candid (record_blob); + // let array_val : ?Variant = from_candid (array_blob); + + // assertAllTrue([ + // text_val == ?#text("hello"), + // nat_val == ?#nat(123), + // bool_val == ?#bool(true), + // record_val == ?#record({ + // site = "github"; + // }), + // array_val == ?#array([1, 2, 3]), + // ]); + // }, + // ), ], ), @@ -271,7 +327,7 @@ let success = run( ("details", #Record([("age", #Nat(32)), ("email", #Option(#Text("example@gmail.com"))), ("registered", #Bool(true))])), ]); - let blob = Candid.encodeOne(candid); + let blob = Candid.encodeOne(candid, null); let mo : ?User = from_candid (blob); mo == ?{ diff --git a/tests/JSON.Test.mo b/tests/JSON.Test.mo index 87afd6b..5ccc9ab 100644 --- a/tests/JSON.Test.mo +++ b/tests/JSON.Test.mo @@ -1,3 +1,4 @@ +// @testmode wasi import Blob "mo:base/Blob"; import Debug "mo:base/Debug"; import Iter "mo:base/Iter"; @@ -33,10 +34,10 @@ let success = run( "fromText()", [ it( - "fromText()", + "record type", do { let text = "{\"name\": \"Tomi\", \"id\": 32}"; - let blob = JSON.fromText(text); + let blob = JSON.fromText(text, null); let user : ?User = from_candid (blob); user == ?{ name = "Tomi"; id = ?32 }; @@ -61,11 +62,11 @@ let success = run( let record = "{\"#record\": {\"site\": \"github\"}}"; let array = "{\"#array\": [1, 2, 3] }"; - let text_blob = JSON.fromText(text); - let nat_blob = JSON.fromText(nat); - let bool_blob = JSON.fromText(bool); - let record_blob = JSON.fromText(record); - let array_blob = JSON.fromText(array); + let text_blob = JSON.fromText(text, null); + let nat_blob = JSON.fromText(nat, null); + let bool_blob = JSON.fromText(bool, null); + let record_blob = JSON.fromText(record, null); + let array_blob = JSON.fromText(array, null); let text_val : ?Variant = from_candid (text_blob); let nat_val : ?Variant = from_candid (nat_blob); @@ -84,6 +85,28 @@ let success = run( ]); }, ), + it( + "renaming record fields", + do { + // type Original = { + // label : Nat; + // query : Text; + // }; + + type UserData = { + account_label : Nat; + user_query : Text; + }; + + let text = "{\"label\": 123, \"query\": \"?user_id=12&address=2014%20Forest%20Hill%20Drive\"}"; + let options = { renameKeys = [("label", "account_label"), ("query", "user_query")] }; + let blob = JSON.fromText(text, ?options); + + let user : ?UserData = from_candid (blob); + + user == ?{ account_label = 123; user_query = "?user_id=12&address=2014%20Forest%20Hill%20Drive" }; + }, + ), ], ), describe( @@ -94,7 +117,7 @@ let success = run( do { let user = { name = "Tomi"; id = null }; let blob = to_candid (user); - let jsonText = JSON.toText(blob, ["name", "id"]); + let jsonText = JSON.toText(blob, ["name", "id"], null); jsonText == "{\"id\": null, \"name\": \"Tomi\"}"; }, @@ -122,11 +145,11 @@ let success = run( let record_blob = to_candid (record); let array_blob = to_candid (array); - let text_json = JSON.toText(text_blob, ["text"]); - let nat_json = JSON.toText(nat_blob, ["nat"]); - let bool_json = JSON.toText(bool_blob, ["bool"]); - let record_json = JSON.toText(record_blob, ["record", "site"]); - let array_json = JSON.toText(array_blob, ["array"]); + let text_json = JSON.toText(text_blob, ["text"], null); + let nat_json = JSON.toText(nat_blob, ["nat"], null); + let bool_json = JSON.toText(bool_blob, ["bool"], null); + let record_json = JSON.toText(record_blob, ["record", "site"], null); + let array_json = JSON.toText(array_blob, ["array"], null); assertAllTrue([ text_json == "{\"#text\": \"hello\"}", @@ -137,6 +160,29 @@ let success = run( ]); }, ), + it( + "renaming record fields", + do { + // type Original = { + // label : Nat; // reserved keyword that is renamed to account_label + // query : Text; // reserved keyword that is renamed to user_query + // }; + + type UserData = { + account_label : Nat; + user_query : Text; + }; + + let UserDataKeys = ["account_label", "user_query"]; + let options = { renameKeys = [("account_label", "label"), ("user_query", "query")] }; + + let data : UserData = { account_label = 123; user_query = "?user_id=12&address=2014%20Forest%20Hill%20Drive" }; + let blob = to_candid (data); + let jsonText = JSON.toText(blob, UserDataKeys, ?options); + + jsonText == "{\"label\": 123, \"query\": \"?user_id=12&address=2014%20Forest%20Hill%20Drive\"}"; + }, + ), ], ), ], diff --git a/tests/UrlEncoded.Test.mo b/tests/UrlEncoded.Test.mo index a175e7a..0f3ebef 100644 --- a/tests/UrlEncoded.Test.mo +++ b/tests/UrlEncoded.Test.mo @@ -1,3 +1,4 @@ +// @testmode wasi import Debug "mo:base/Debug"; import Iter "mo:base/Iter"; @@ -32,7 +33,7 @@ let success = run([ "single record", do { - let blob = UrlEncoded.fromText("msg=Hello World&name=John"); + let blob = UrlEncoded.fromText("msg=Hello World&name=John", null); let res : ?User = from_candid (blob); @@ -47,9 +48,9 @@ let success = run([ it( "record with array", do { - let blob = UrlEncoded.fromText( - "users[0][name]=John&users[0][msg]=Hello World&users[1][name]=Jane&users[1][msg]=testing", - ); + + let text = "users[0][name]=John&users[0][msg]=Hello World&users[1][name]=Jane&users[1][msg]=testing"; + let blob = UrlEncoded.fromText(text, null); let res : ?{ users : [User] } = from_candid (blob); assertTrue( @@ -91,14 +92,14 @@ let success = run([ let user = "variant[#user][name]=John&variant[#user][msg]=Hello World"; let array = "variant[#array][0]=1&variant[#array][1]=2&variant[#array][2]=3"; - let text_blob = UrlEncoded.fromText(text); - let nat_blob = UrlEncoded.fromText(nat); - let int_blob = UrlEncoded.fromText(int); - let float_blob = UrlEncoded.fromText(float); - let bool_blob = UrlEncoded.fromText(bool); - let record_blob = UrlEncoded.fromText(record); - let user_blob = UrlEncoded.fromText(user); - let array_blob = UrlEncoded.fromText(array); + let text_blob = UrlEncoded.fromText(text, null); + let nat_blob = UrlEncoded.fromText(nat, null); + let int_blob = UrlEncoded.fromText(int, null); + let float_blob = UrlEncoded.fromText(float, null); + let bool_blob = UrlEncoded.fromText(bool, null); + let record_blob = UrlEncoded.fromText(record, null); + let user_blob = UrlEncoded.fromText(user, null); + let array_blob = UrlEncoded.fromText(array, null); let text_val : ?{ variant : Variant } = from_candid (text_blob); let nat_val : ?{ variant : Variant } = from_candid (nat_blob); @@ -145,7 +146,7 @@ let success = run([ }; let blob = to_candid (info); - let text = UrlEncoded.toText(blob, ["name", "msg"]); + let text = UrlEncoded.toText(blob, ["name", "msg"], null); assertTrue(text == "msg=Hello World&name=John"); }, ), @@ -165,7 +166,7 @@ let success = run([ let blob = to_candid ({ users }); - let text = UrlEncoded.toText(blob, ["users", "name", "msg"]); + let text = UrlEncoded.toText(blob, ["users", "name", "msg"], null); assertTrue( text == "users[0][msg]=Hello World&users[0][name]=John&users[1][msg]=testing&users[1][name]=Jane", diff --git a/usage.md b/usage.md new file mode 100644 index 0000000..a5fe069 --- /dev/null +++ b/usage.md @@ -0,0 +1,49 @@ + +## Usage Examples + +### Candid Text +```motoko + import { Candid } "mo:serde"; + + type User = { + name: Text; + id: Nat; + }; + + let blob = serdeCandid.fromText("(record({ name = \"bar\"; id = 112 }))", null); + let user : ?User = from_candid(blob); + + assert user == ?{ name = "bar"; id = 112 }; + +``` + +### URL-Encoded Pairs +Serialization and deserialization for `application/x-www-form-urlencoded`. + +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`." + +```motoko + import { URLEncoded } "mo:serde"; + + type User = { + name: Text; + id: Nat; + }; + + let payload = "users[0][id]=123&users[0][name]=John&users[1][id]=456&users[1][name]=Jane"; + + let blob = URLEncoded.fromText(payload, null); + let res : ?{ users: [User]} = from_candid(blob); + + assert res == ?{ users = [ + { + name = "John"; + id = 123; + }, + { + name = "Jane"; + id = 456; + }, + ] }; + +```