diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f37ce4d1a2..f42b89b687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,15 +81,14 @@ # "examples/update" # These are the ones that don't work -# "examples/tuple_types", -# "examples/service", -# "examples/robust_imports", -# "examples/run_time_errors", -# "examples/complex_types", # "examples/func_types", # "examples/generics", +# "examples/tuple_types", +# "examples/complex_types", # "examples/motoko_examples/superheroes", # "examples/motoko_examples/threshold_ecdsa", +# "examples/run_time_errors", +# "examples/robust_imports", name: Azle Tests on: @@ -183,6 +182,7 @@ jobs: "examples/query", "examples/randomness", "examples/rejections", + "examples/service", "examples/simple_erc20", "examples/simple_user_accounts", "examples/stable_memory", diff --git a/canisters/ledger/index.ts b/canisters/ledger/index.ts index 2d1c719b43..d66e9e0b20 100644 --- a/canisters/ledger/index.ts +++ b/canisters/ledger/index.ts @@ -25,7 +25,8 @@ import { Variant, Vec, candid, - principal + principal, + Func } from '../../src/lib_new'; import { ICRC1Account, @@ -279,7 +280,7 @@ export class QueryArchiveResult extends Variant { // A function that is used for fetching archived ledger blocks. @func([GetBlocksArgs], QueryArchiveResult, 'query') -class QueryArchiveFn {} +class QueryArchiveFn extends Func {} class ArchivedBlock extends Record { // The index of the first archived block that can be fetched using the callback. diff --git a/canisters/management/http_request.ts b/canisters/management/http_request.ts index 27d0cc76e3..49e225610f 100644 --- a/canisters/management/http_request.ts +++ b/canisters/management/http_request.ts @@ -9,7 +9,8 @@ import { Null, nat64, nat, - func + func, + Func } from '../../src/lib_new'; export class HttpHeader extends Record { @@ -63,7 +64,7 @@ export class HttpTransformArgs extends Record { } @func([HttpTransformArgs], HttpResponse, 'query') -export class HttpTransformFunc {} +export class HttpTransformFunc extends Func {} export class HttpTransform extends Record { /** diff --git a/examples/func_types/canisters/func_types/func_types.ts b/examples/func_types/canisters/func_types/func_types.ts deleted file mode 100644 index 8db374ad31..0000000000 --- a/examples/func_types/canisters/func_types/func_types.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - Func, - Query, - Update, - $init, - ic, - match, - nat64, - Opt, - Principal, - $query, - Record, - Result, - StableBTreeMap, - $update, - Variant, - Vec -} from 'azle'; -import { Notifier, NotifierFunc } from '../notifiers/types'; - -let stableStorage = new StableBTreeMap(0, 25, 1_000); - -type User = Record<{ - id: string; - basicFunc: BasicFunc; - complexFunc: ComplexFunc; -}>; - -type Reaction = Variant<{ - Good: null; - Bad: null; - BasicFunc: BasicFunc; - ComplexFunc: ComplexFunc; -}>; - -type BasicFunc = Func string>>; -type ComplexFunc = Func nat64>>; -type StableFunc = Func void>>; -type NullFunc = Func< - Query< - ( - param1: Opt, - param2: Vec, - param3: null, - param4: Vec>, - param5: Vec> - ) => null - > ->; - -$init; -export function init(): void { - stableStorage.insert('stableFunc', [ - Principal.from('aaaaa-aa'), - 'start_canister' - ]); -} - -$query; -export function getStableFunc(): StableFunc { - return match(stableStorage.get('stableFunc'), { - Some: (func) => func, - None: () => [Principal.from('aaaaa-aa'), 'raw_rand'] as StableFunc - }); -} - -$query; -export function basicFuncParam(basicFunc: BasicFunc): BasicFunc { - return basicFunc; -} - -$query; -export function nullFuncParam(nullFunc: NullFunc): NullFunc { - return nullFunc; -} - -$query; -export function basicFuncParamArray(basicFunc: Vec): Vec { - return basicFunc; -} - -$query; -export function basicFuncReturnType(): BasicFunc { - return [Principal.fromText('aaaaa-aa'), 'create_canister']; -} - -$query; -export function basicFuncReturnTypeArray(): Vec { - return [ - [Principal.fromText('aaaaa-aa'), 'create_canister'], - [Principal.fromText('aaaaa-aa'), 'update_settings'], - [Principal.fromText('aaaaa-aa'), 'install_code'] - ]; -} - -$query; -export function complexFuncParam(complexFunc: ComplexFunc): ComplexFunc { - return complexFunc; -} - -$query; -export function complexFuncReturnType(): ComplexFunc { - return [Principal.fromText('aaaaa-aa'), 'stop_canister']; -} - -$update; -export async function getNotifierFromNotifiersCanister(): Promise< - Result -> { - const notifiersCanister: Notifier = new Notifier( - Principal.fromText( - process.env.NOTIFIERS_PRINCIPAL ?? - ic.trap('process.env.NOTIFIERS_PRINCIPAL is undefined') - ) - ); - - const result = await notifiersCanister.getNotifier().call(); - - return match(result, { - Ok: (ok) => ({ Ok: ok }), - Err: (err) => ({ Err: err }) - }); -} diff --git a/examples/func_types/canisters/func_types/func_types.did b/examples/func_types/canisters/func_types/index.did similarity index 100% rename from examples/func_types/canisters/func_types/func_types.did rename to examples/func_types/canisters/func_types/index.did diff --git a/examples/func_types/canisters/func_types/index.ts b/examples/func_types/canisters/func_types/index.ts new file mode 100644 index 0000000000..b317653fd9 --- /dev/null +++ b/examples/func_types/canisters/func_types/index.ts @@ -0,0 +1,127 @@ +import { + Func, + init, + ic, + nat64, + Opt, + Principal, + query, + Record, + Service, + StableBTreeMap, + update, + Variant, + Vec, + candid, + text, + func, + Void, + Null +} from 'azle'; +import Notifier, { NotifierFunc } from '../notifiers'; + +@func([text], text, 'query') +class BasicFunc extends Func {} + +@func([User, Reaction], nat64, 'update') +class ComplexFunc extends Func {} + +class User extends Record { + @candid(text) + id: text; + + @candid(BasicFunc) + basicFunc: BasicFunc; + + @candid(ComplexFunc) + complexFunc: ComplexFunc; +} + +class Reaction extends Variant { + Good: null; + Bad: null; + BasicFunc: BasicFunc; + ComplexFunc: ComplexFunc; +} + +@func([nat64, text], Void, 'query') +class StableFunc extends Func {} + +@func( + [Opt(Null), Vec(Null), Null, Vec(Vec(Null)), Vec(Opt(Null))], + Null, + 'query' +) +class NullFunc extends Func {} + +export default class extends Service { + stableStorage = new StableBTreeMap(text, StableFunc, 0); + + @init([]) + init() { + this.stableStorage.insert( + 'stableFunc', + new StableFunc(Principal.from('aaaaa-aa'), 'start_canister') + ); + } + + @query([], StableFunc) + getStableFunc(): StableFunc { + const stableFuncOpt = this.stableStorage.get('stableFunc'); + if (stableFuncOpt.length === 1) { + return stableFuncOpt[0]; + } + return new StableFunc(Principal.from('aaaaa-aa'), 'raw_rand'); + } + + @query([BasicFunc], BasicFunc) + basicFuncParam(basicFunc: BasicFunc): BasicFunc { + return basicFunc; + } + + @query([NullFunc], NullFunc) + nullFuncParam(nullFunc: NullFunc): NullFunc { + return nullFunc; + } + + @query([Vec(BasicFunc)], Vec(BasicFunc)) + basicFuncParamArray(basicFunc: Vec): Vec { + return basicFunc; + } + + @query([], BasicFunc) + basicFuncReturnType(): BasicFunc { + return new BasicFunc(Principal.fromText('aaaaa-aa'), 'create_canister'); + } + + @query([], Vec(BasicFunc)) + basicFuncReturnTypeArray(): Vec { + return [ + new BasicFunc(Principal.fromText('aaaaa-aa'), 'create_canister'), + new BasicFunc(Principal.fromText('aaaaa-aa'), 'update_settings'), + new BasicFunc(Principal.fromText('aaaaa-aa'), 'install_code') + ]; + } + + @query([ComplexFunc], ComplexFunc) + complexFuncParam(complexFunc: ComplexFunc): ComplexFunc { + return complexFunc; + } + + @query([], ComplexFunc) + complexFuncReturnType(): ComplexFunc { + return [Principal.fromText('aaaaa-aa'), 'stop_canister']; + } + + @update([], NotifierFunc) + async getNotifierFromNotifiersCanister(): Promise { + const notifiersCanister: Notifier = new Notifier( + Principal.fromText( + process.env.NOTIFIERS_PRINCIPAL ?? + ic.trap('process.env.NOTIFIERS_PRINCIPAL is undefined') + ) + ); + + return await ic.call(notifiersCanister.getNotifier); + } +} diff --git a/examples/func_types/canisters/notifiers/index.did b/examples/func_types/canisters/notifiers/index.did new file mode 100644 index 0000000000..9ea58c8334 --- /dev/null +++ b/examples/func_types/canisters/notifiers/index.did @@ -0,0 +1,3 @@ +service: () -> { + getNotifier: () -> (func (vec nat8) -> () oneway) query; +} diff --git a/examples/func_types/canisters/notifiers/index.ts b/examples/func_types/canisters/notifiers/index.ts new file mode 100644 index 0000000000..ddfc393c19 --- /dev/null +++ b/examples/func_types/canisters/notifiers/index.ts @@ -0,0 +1,17 @@ +import { ic, Principal, query, blob, Func, Service, Void, func } from 'azle'; + +@func([blob], Void, 'oneway') +export class NotifierFunc extends Func {} + +export default class extends Service { + @query([], NotifierFunc) + getNotifier(): NotifierFunc { + return new NotifierFunc( + Principal.fromText( + process.env.NOTIFIERS_PRINCIPAL ?? + ic.trap('process.env.NOTIFIERS_PRINCIPAL is undefined') + ), + 'notify' + ); + } +} diff --git a/examples/func_types/canisters/notifiers/notifiers.did b/examples/func_types/canisters/notifiers/notifiers.did deleted file mode 100644 index 6be686c88b..0000000000 --- a/examples/func_types/canisters/notifiers/notifiers.did +++ /dev/null @@ -1 +0,0 @@ -service : () -> { getNotifier : () -> (func (vec nat8) -> () oneway) query } \ No newline at end of file diff --git a/examples/func_types/canisters/notifiers/notifiers.ts b/examples/func_types/canisters/notifiers/notifiers.ts deleted file mode 100644 index f5ac0101dd..0000000000 --- a/examples/func_types/canisters/notifiers/notifiers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ic, Principal, $query } from 'azle'; -import { NotifierFunc } from './types'; - -$query; -export function getNotifier(): NotifierFunc { - return [ - Principal.fromText( - process.env.NOTIFIERS_PRINCIPAL ?? - ic.trap('process.env.NOTIFIERS_PRINCIPAL is undefined') - ), - 'notify' - ]; -} diff --git a/examples/func_types/canisters/notifiers/types.ts b/examples/func_types/canisters/notifiers/types.ts deleted file mode 100644 index 1dc3865b36..0000000000 --- a/examples/func_types/canisters/notifiers/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { blob, CallResult, Func, Oneway, Service, serviceQuery } from 'azle'; - -export type NotifierFunc = Func void>>; - -export class Notifier extends Service { - @serviceQuery - getNotifier: () => CallResult; -} diff --git a/examples/func_types/dfx.json b/examples/func_types/dfx.json index b26d5f2ef5..ed806a8ba6 100644 --- a/examples/func_types/dfx.json +++ b/examples/func_types/dfx.json @@ -4,8 +4,8 @@ "type": "custom", "build": "npx azle func_types", "root": "src", - "ts": "canisters/func_types/func_types.ts", - "candid": "canisters/func_types/func_types.did", + "ts": "canisters/func_types/index.ts", + "candid": "canisters/func_types/index.did", "wasm": ".azle/func_types/func_types.wasm.gz", "declarations": { "output": "test/dfx_generated/func_types", @@ -17,8 +17,8 @@ "type": "custom", "build": "npx azle notifiers", "root": "src", - "ts": "canisters/notifiers/notifiers.ts", - "candid": "canisters/notifiers/notifiers.did", + "ts": "canisters/notifiers/index.ts", + "candid": "canisters/notifiers/index.did", "wasm": ".azle/notifiers/notifiers.wasm.gz", "declarations": { "output": "test/dfx_generated/notifiers", diff --git a/examples/func_types/test/tests.ts b/examples/func_types/test/tests.ts index de3027a73a..4925f54215 100644 --- a/examples/func_types/test/tests.ts +++ b/examples/func_types/test/tests.ts @@ -126,15 +126,13 @@ export function getTests(funcTypesCanister: ActorSubclass<_SERVICE>): Test[] { { name: 'getNotifierFromNotifiersCanister', test: async () => { - // TODO agent-js seems to be creating incorrect types here: https://github.com/dfinity/agent-js/issues/583 - const result: any = + const result = await funcTypesCanister.getNotifierFromNotifiersCanister(); return { Ok: - 'Ok' in result && - result.Ok[0].toText() === getCanisterId('notifiers') && - result.Ok[1] === 'notify' + result[0].toText() === getCanisterId('notifiers') && + result[1] === 'notify' }; } } diff --git a/examples/list_of_lists/src/index.ts b/examples/list_of_lists/src/index.ts index 11bc801afd..ce953a52cc 100644 --- a/examples/list_of_lists/src/index.ts +++ b/examples/list_of_lists/src/index.ts @@ -5,6 +5,7 @@ import { empty, float32, float64, + Func, func, int, int16, @@ -49,7 +50,7 @@ class State extends Variant { } @func([text], text, 'query') -class BasicFunc {} +class BasicFunc extends Func {} export default class extends Service { @query([Vec(text)], Vec(text)) diff --git a/examples/manual_reply/src/manual_reply.did b/examples/manual_reply/src/manual_reply.did index 61a0d57c4b..671739a3ff 100644 --- a/examples/manual_reply/src/manual_reply.did +++ b/examples/manual_reply/src/manual_reply.did @@ -16,7 +16,6 @@ service: () -> { manualUpdate: (text) -> (text); updateBlob: () -> (vec nat8); updateFloat32: () -> (float32); - updateInlineType: () -> (record {text; text}); updateInt8: () -> (int8); updateNat: () -> (nat); updateNull: () -> (null); diff --git a/examples/manual_reply/src/manual_reply.ts b/examples/manual_reply/src/manual_reply.ts index 30ba159265..8d5a4268ee 100644 --- a/examples/manual_reply/src/manual_reply.ts +++ b/examples/manual_reply/src/manual_reply.ts @@ -121,11 +121,6 @@ export default class extends Service { ic.reply(1245.678, float32); } - @update([], Tuple(text, text), { manual: true }) - updateInlineType(): Manual<[text, text]> { - ic.reply(['Hello', 'World'], Tuple(text, text)); - } - @update([], int8, { manual: true }) updateInt8(): Manual { ic.reply(-100, int8); @@ -217,12 +212,6 @@ export default class extends Service { ic.reply(1245.678, float32); } - // TODO: Inline Types not currently supported. - // See https://github.com/demergent-labs/azle/issues/474 - // queryInlineType(): Manual<{> prop: string } { - // ic.reply({ prop: 'prop' }); - // } - @query([], int8, { manual: true }) queryInt8(): Manual { ic.reply(-100, int8); diff --git a/examples/manual_reply/test/tests.ts b/examples/manual_reply/test/tests.ts index 66066e70f6..aaca7bd84e 100644 --- a/examples/manual_reply/test/tests.ts +++ b/examples/manual_reply/test/tests.ts @@ -63,16 +63,6 @@ export function getTests(manualReplyCanister: ActorSubclass<_SERVICE>): Test[] { }; } }, - { - name: 'update reply with inlineType', - test: async () => { - const result = await manualReplyCanister.updateInlineType(); - - return { - Ok: result[0] === 'Hello' && result[1] === 'World' - }; - } - }, { name: 'update reply with int8', test: async () => { diff --git a/examples/motoko_examples/http_counter/src/index.ts b/examples/motoko_examples/http_counter/src/index.ts index cb61abe26b..9a89fb1296 100644 --- a/examples/motoko_examples/http_counter/src/index.ts +++ b/examples/motoko_examples/http_counter/src/index.ts @@ -18,7 +18,8 @@ import { Some, None, bool, - Service + Service, + Func } from 'azle'; class Token extends Record { @@ -36,7 +37,7 @@ class StreamingCallbackHttpResponse extends Record { } @func([text], StreamingCallbackHttpResponse, 'query') -class Callback {} +class Callback extends Func {} class CallbackStrategy extends Record { @candid(Callback) @@ -106,7 +107,10 @@ export default class extends Service { body: encode('Counter'), streaming_strategy: Some({ Callback: { - callback: [ic.id(), 'http_streaming'], + callback: new Callback( + ic.id(), + 'http_streaming' + ), token: { arbitrary_data: 'start' } diff --git a/examples/service/src/index.did b/examples/service/src/index.did index 1fb291d01b..72535f4276 100644 --- a/examples/service/src/index.did +++ b/examples/service/src/index.did @@ -1,21 +1,8 @@ -type ManualReply = variant { Ok : text; Err : text }; -type _InlineServiceNestedReturnTypeReturnType = record { - someService : service { query1 : () -> (bool) query; update1 : () -> (text) }; -}; -service : () -> { - serviceCrossCanisterCall : ( - service { query1 : () -> (bool) query; update1 : () -> (text) }, - ) -> (ManualReply); - serviceList : ( - vec service { query1 : () -> (bool) query; update1 : () -> (text) }, - ) -> (vec service { query1 : () -> (bool) query; update1 : () -> (text) }); - serviceNestedReturnType : () -> (_InlineServiceNestedReturnTypeReturnType); - serviceParam : ( - service { query1 : () -> (bool) query; update1 : () -> (text) }, - ) -> ( - service { query1 : () -> (bool) query; update1 : () -> (text) }, - ) query; - serviceReturnType : () -> ( - service { query1 : () -> (bool) query; update1 : () -> (text) }, - ) query; -} \ No newline at end of file +type rec_0 = record {someService:service {query1:() -> (bool) query; update1:() -> (text) }}; +service: () -> { + serviceParam: (service {query1:() -> (bool) query; update1:() -> (text) }) -> (service {query1:() -> (bool) query; update1:() -> (text) }) query; + serviceReturnType: () -> (service {query1:() -> (bool) query; update1:() -> (text) }) query; + serviceNestedReturnType: () -> (rec_0); + serviceList: (vec service {query1:() -> (bool) query; update1:() -> (text) }) -> (vec service {query1:() -> (bool) query; update1:() -> (text) }); + serviceCrossCanisterCall: (service {query1:() -> (bool) query; update1:() -> (text) }) -> (text); +} diff --git a/examples/service/src/index.ts b/examples/service/src/index.ts index 6b83e5b744..6b931e575e 100644 --- a/examples/service/src/index.ts +++ b/examples/service/src/index.ts @@ -1,67 +1,59 @@ import { - CallResult, + candid, ic, Principal, - $query, + query, Record, Service, - serviceQuery, - serviceUpdate, - $update, - Variant, + text, + update, Vec } from 'azle'; -class SomeService extends Service { - @serviceQuery - query1: () => CallResult; +import SomeService from './some_service'; - @serviceUpdate - update1: () => CallResult; +class Wrapper extends Record { + @candid(SomeService) + someService: SomeService; } -$query; -export function serviceParam(someService: SomeService): SomeService { - return someService; -} +export default class extends Service { + @query([SomeService], SomeService) + serviceParam(someService: SomeService): SomeService { + return someService; + } -$query; -export function serviceReturnType(): SomeService { - return new SomeService( - Principal.fromText( - process.env.SOME_SERVICE_PRINCIPAL ?? - ic.trap('process.env.SOME_SERVICE_PRINCIPAL is undefined') - ) - ); -} - -$update; -export function serviceNestedReturnType(): Record<{ - someService: SomeService; -}> { - return { - someService: new SomeService( + @query([], SomeService) + serviceReturnType(): SomeService { + return new SomeService( Principal.fromText( process.env.SOME_SERVICE_PRINCIPAL ?? ic.trap('process.env.SOME_SERVICE_PRINCIPAL is undefined') ) - ) - }; -} + ); + } -$update; -export function serviceList(someServices: Vec): Vec { - return someServices; -} + @update([], Wrapper) + serviceNestedReturnType(): Wrapper { + return { + someService: new SomeService( + Principal.fromText( + process.env.SOME_SERVICE_PRINCIPAL ?? + ic.trap( + 'process.env.SOME_SERVICE_PRINCIPAL is undefined' + ) + ) + ) + }; + } + + @update([Vec(SomeService)], Vec(SomeService)) + serviceList(someServices: Vec): Vec { + return someServices; + } -$update; -export async function serviceCrossCanisterCall( - someService: SomeService -): Promise< - Variant<{ - Ok: string; - Err: string; - }> -> { - return await someService.update1().call(); + @update([SomeService], text) + async serviceCrossCanisterCall(someService: SomeService): Promise { + return await ic.call(someService.update1); + } } diff --git a/examples/service/src/some_service.did b/examples/service/src/some_service.did index 1539cd11df..f61de65f9f 100644 --- a/examples/service/src/some_service.did +++ b/examples/service/src/some_service.did @@ -1 +1,4 @@ -service : () -> { query1 : () -> (bool) query; update1 : () -> (text) } \ No newline at end of file +service: () -> { + query1: () -> (bool) query; + update1: () -> (text); +} diff --git a/examples/service/src/some_service.ts b/examples/service/src/some_service.ts index 67542a2b9a..f815dbc8a4 100644 --- a/examples/service/src/some_service.ts +++ b/examples/service/src/some_service.ts @@ -1,11 +1,13 @@ -import { $query, $update } from 'azle'; +import { bool, query, Service, text, update } from 'azle'; -$query; -export function query1(): boolean { - return true; -} +export default class extends Service { + @query([], bool) + query1(): bool { + return true; + } -$update; -export function update1(): string { - return 'SomeService update1'; + @update([], text) + update1(): text { + return 'SomeService update1'; + } } diff --git a/examples/service/test/tests.ts b/examples/service/test/tests.ts index 3e9feae042..5228c3d415 100644 --- a/examples/service/test/tests.ts +++ b/examples/service/test/tests.ts @@ -88,7 +88,7 @@ export function getTests(serviceCanister: ActorSubclass<_SERVICE>): Test[] { .trim(); return { - Ok: result === '(variant { Ok = "SomeService update1" })' + Ok: result === '("SomeService update1")' }; } } diff --git a/package-lock.json b/package-lock.json index ae8cd6c8eb..e6702c00dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.17.1", "license": "MIT", "dependencies": { - "@dfinity/candid": "^0.19.0", + "@dfinity/candid": "github:demergent-labs/candid", "@dfinity/principal": "^0.19.0", "@swc/core": "1.3.81", "azle-syn": "0.0.0", @@ -55,17 +55,17 @@ } }, "node_modules/@dfinity/candid": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.19.0.tgz", - "integrity": "sha512-cuK1PcLROTWhlOV6ew9K0HgVil6Og2pFdQz37jnpOmoYg+SrRzCQLcadjUrxy+RS4ryvkX8lCzH8liUiFYgPWg==", + "version": "0.19.2", + "resolved": "git+ssh://git@github.com/demergent-labs/candid.git#88ffa6d9a85b175fcf3ef2a79c9fe4c0f034c02d", + "license": "Apache-2.0", "peerDependencies": { - "@dfinity/principal": "^0.19.0" + "@dfinity/principal": "^0.19.2" } }, "node_modules/@dfinity/principal": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.19.0.tgz", - "integrity": "sha512-s7N+MNEnvEz4sbSv7mKvJiz9JnREu6LFtgNTJq4L9aBXgbWUe+bjdbVjqXz8Uk0kRgPAfnFfezM6ddVd/80tew==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.19.2.tgz", + "integrity": "sha512-vsKN6BKya70bQUsjgKRDlR2lOpv/XpUkCMIiji6rjMtKHIuWEB5Eu3JqZsOuBmWo3A3TT/K/osT9VPm0k4qdYQ==", "dependencies": { "@noble/hashes": "^1.3.1" } @@ -2671,15 +2671,14 @@ } }, "@dfinity/candid": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-0.19.0.tgz", - "integrity": "sha512-cuK1PcLROTWhlOV6ew9K0HgVil6Og2pFdQz37jnpOmoYg+SrRzCQLcadjUrxy+RS4ryvkX8lCzH8liUiFYgPWg==", + "version": "git+ssh://git@github.com/demergent-labs/candid.git#88ffa6d9a85b175fcf3ef2a79c9fe4c0f034c02d", + "from": "@dfinity/candid@github:demergent-labs/candid", "requires": {} }, "@dfinity/principal": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.19.0.tgz", - "integrity": "sha512-s7N+MNEnvEz4sbSv7mKvJiz9JnREu6LFtgNTJq4L9aBXgbWUe+bjdbVjqXz8Uk0kRgPAfnFfezM6ddVd/80tew==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-0.19.2.tgz", + "integrity": "sha512-vsKN6BKya70bQUsjgKRDlR2lOpv/XpUkCMIiji6rjMtKHIuWEB5Eu3JqZsOuBmWo3A3TT/K/osT9VPm0k4qdYQ==", "requires": { "@noble/hashes": "^1.3.1" } diff --git a/package.json b/package.json index 71af544469..0b62e85f2f 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/demergent-labs/azle#readme", "dependencies": { - "@dfinity/candid": "^0.19.0", + "@dfinity/candid": "github:demergent-labs/candid", "@dfinity/principal": "^0.19.0", "@swc/core": "1.3.81", "azle-syn": "0.0.0", diff --git a/src/lib_new/func.ts b/src/lib_new/func.ts index f0e2cc9b71..a0355c03bc 100644 --- a/src/lib_new/func.ts +++ b/src/lib_new/func.ts @@ -1,8 +1,9 @@ -import { IDL } from './index'; +import { IDL, Principal } from './index'; import { CandidClass, - toReturnCandidClass, - toParamCandidClasses + toReturnIDLType, + toParamIDLTypes, + ReturnCandidClass } from './utils'; type Mode = 'query' | 'update' | 'oneway'; @@ -13,17 +14,28 @@ const modeToCandid = { update: [] // TODO what is the proper way to do updates }; +export class Func { + constructor(principal: Principal, name: string) {} +} + export function func( paramsIdls: CandidClass[], - returnIdl: CandidClass, + returnIdl: ReturnCandidClass, mode: Mode ) { return (target: any) => { return class extends target { + principal: Principal; + name: string; + constructor(principal: Principal, name: string) { + super(); + this.principal = principal; + this.name = name; + } static getIDL() { return IDL.Func( - toParamCandidClasses(paramsIdls), - toReturnCandidClass(returnIdl), + toParamIDLTypes(paramsIdls), + toReturnIDLType(returnIdl), modeToCandid[mode] ); } diff --git a/src/lib_new/ic.ts b/src/lib_new/ic.ts index 99af9b64de..1268728043 100644 --- a/src/lib_new/ic.ts +++ b/src/lib_new/ic.ts @@ -4,7 +4,7 @@ import { IDL } from './index'; import { blob, nat, nat32, nat64, Void, Opt } from './primitives'; import { RejectionCode } from './system_types'; import { v4 } from 'uuid'; -import { CandidClass, toCandidClass } from './utils'; +import { CandidClass, toIDLType } from './utils'; // declare var globalThis: { // ic: Ic; @@ -732,8 +732,8 @@ export const ic: Ic = globalThis._azleIc const bytes = new Uint8Array(IDL.encode([], [])).buffer; return globalThis._azleIc.replyRaw(bytes); } - const candidType = toCandidClass(type, []); - const bytes = new Uint8Array(IDL.encode([candidType], [reply])) + const idlType = toIDLType(type, []); + const bytes = new Uint8Array(IDL.encode([idlType], [reply])) .buffer; return globalThis._azleIc.replyRaw(bytes); }, diff --git a/src/lib_new/method_decorators.ts b/src/lib_new/method_decorators.ts index 326a08b2cb..4627ce65aa 100644 --- a/src/lib_new/method_decorators.ts +++ b/src/lib_new/method_decorators.ts @@ -4,14 +4,20 @@ import { GuardResult, IDL } from './index'; import { CandidClass, ReturnCandidClass, - toParamCandidClasses, - toReturnCandidClass, + toParamIDLTypes, + toReturnIDLType, CandidTypesDefs, CandidDef, extractCandid } from './utils'; import { display } from './utils'; -import { serviceCall, serviceDecorator } from './service'; +import { + Service, + serviceCall, + ServiceConstructor, + serviceDecorator +} from './service'; +import { DecodeVisitor, EncodeVisitor } from './visitors/encode_decode/'; export type Manual = void; @@ -176,8 +182,8 @@ function newTypesToStingArr(newTypes: CandidTypesDefs): string[] { function handleRecursiveParams( idls: CandidClass[] -): [CandidClass[], CandidDef[], CandidTypesDefs] { - const paramIdls = toParamCandidClasses(idls); +): [IDL.Type[], CandidDef[], CandidTypesDefs] { + const paramIdls = toParamIDLTypes(idls); const paramInfo = paramIdls.map((paramIdl) => display(paramIdl, {})); return [paramIdls, ...extractCandid(paramInfo, {})]; } @@ -185,8 +191,8 @@ function handleRecursiveParams( function handleRecursiveReturn( returnIdl: ReturnCandidClass, paramCandidTypeDefs: CandidTypesDefs -): [CandidClass[], CandidDef[], CandidTypesDefs] { - const returnIdls = toReturnCandidClass(returnIdl); +): [IDL.Type[], CandidDef[], CandidTypesDefs] { + const returnIdls = toReturnIDLType(returnIdl); const returnInfo = returnIdls.map((returnIdl) => display(returnIdl, {})); return [returnIdls, ...extractCandid(returnInfo, paramCandidTypeDefs)]; } @@ -238,6 +244,16 @@ function setupCanisterMethod( modeToCandid[mode] };` ); + + if (target instanceof Service) { + addIDLForMethodToServiceConstructor( + target.constructor, + key, + paramsIdls, + returnIdl, + mode + ); + } } const originalMethod = descriptor.value; @@ -289,8 +305,14 @@ function setupCanisterMethod( } const decoded = IDL.decode(paramCandid[0], args[0]); + const myDecodedObject = paramCandid[0].map((idl, index) => { + return idl.accept(new DecodeVisitor(), { + js_class: paramsIdls[index], + js_data: decoded[index] + }); + }); - const result = originalMethod.apply(this, decoded); + const result = originalMethod.apply(this, myDecodedObject); if ( mode === 'init' || @@ -300,11 +322,12 @@ function setupCanisterMethod( return; } - if ( + const is_promise = result !== undefined && result !== null && - typeof result.then === 'function' - ) { + typeof result.then === 'function'; + + if (is_promise) { result .then((result) => { // TODO this won't be accurate because we have most likely had @@ -327,9 +350,13 @@ function setupCanisterMethod( ic.trap(error.toString()); }); } else { - const encodeReadyResult = result === undefined ? [] : [result]; - if (!manual) { + const encodeReadyResult = returnCandid[0].map((idl) => { + return idl.accept(new EncodeVisitor(), { + js_class: returnIdl, + js_data: result + }); + }); const encoded = IDL.encode(returnCandid[0], encodeReadyResult); ic.replyRaw(new Uint8Array(encoded)); } @@ -375,3 +402,38 @@ function createGlobalGuard( return guardName; } + +/** + * Stores an IDL representation of the canister method into a private + * `_azleFunctionInfo` object on the provided constructor. If that property doesn't + * exist, then it will be added as a side-effect. + * + * @param constructor The class on which to store the IDL information. This + * should probably be a Service. This type should probably be tightened down. + * @param methodName The public name of the canister method + * @param paramIdls The IDLs of the parameters, coming from the `@query` and + * `@update` decorators. + * @param returnIdl The IDL of the return type, coming from the `@query` and + * `@update` decorators. + * @param mode The mode in which the method should be executed. + */ +function addIDLForMethodToServiceConstructor( + constructor: T & ServiceConstructor, + methodName: string, + paramIdls: CandidClass[], + returnIdl: ReturnCandidClass, + mode: 'query' | 'update' +): void { + if (constructor._azleFunctionInfo === undefined) { + constructor._azleFunctionInfo = {}; + } + + // TODO: Technically, there is a possibility that the method name already + // exists. We may want to handle that case. + + constructor._azleFunctionInfo[methodName] = { + mode, + paramIdls, + returnIdl + }; +} diff --git a/src/lib_new/primitives.ts b/src/lib_new/primitives.ts index d259007edd..fe3b4c6492 100644 --- a/src/lib_new/primitives.ts +++ b/src/lib_new/primitives.ts @@ -1,5 +1,5 @@ import { IDL } from './index'; -import { CandidClass, Parent, toCandidClass } from './utils'; +import { CandidClass, Parent, toIDLType } from './utils'; export const bool = IDL.Bool; export type bool = boolean; @@ -63,54 +63,58 @@ export function Some(value: T): [T] { export const None: [] = []; // TODO what happens if we pass something to Opt() that can't be converted to CandidClass? -export function Opt(t: IDL.Type | any): AzleOpt { - // return IDL.Opt(toCandidClass(t)); +export function Opt(t: CandidClass): AzleOpt { + // return IDL.Opt(toIDLType(t)); return new AzleOpt(t); } -export class AzleOpt { - constructor(t: any) { +export interface GetIDL { + getIDL(parents: Parent[]): IDL.Type; +} + +export class AzleOpt implements GetIDL { + constructor(t: CandidClass) { this._azleType = t; } - _azleType: any; + _azleType: CandidClass; getIDL(parents: Parent[]) { - return IDL.Opt(toCandidClass(this._azleType, [])); + return IDL.Opt(toIDLType(this._azleType, [])); } } -export class AzleVec { - constructor(t: any) { +export class AzleVec implements GetIDL { + constructor(t: CandidClass) { this._azleType = t; } - _azleType: any; + _azleType: CandidClass; getIDL(parents: Parent[]) { - return IDL.Vec(toCandidClass(this._azleType, [])); + return IDL.Vec(toIDLType(this._azleType, [])); } } -export class AzleTuple { - constructor(t: any[]) { +export class AzleTuple implements GetIDL { + constructor(t: CandidClass[]) { this._azleTypes = t; } - _azleTypes: any[]; + _azleTypes: CandidClass[]; getIDL(parents: Parent[]) { const candidTypes = this._azleTypes.map((value) => { - return toCandidClass(value, parents); + return toIDLType(value, parents); }); return IDL.Tuple(...candidTypes); } } -export function Vec(t: IDL.Type | any): AzleVec { - // return IDL.Vec(toCandidClass(t)); +export function Vec(t: CandidClass): AzleVec { + // return IDL.Vec(toIDLType(t)); return new AzleVec(t); } // TODO I am not sure of any of these types... but its working so... -export function Tuple(...types: T): AzleTuple { - // const candidTypes = types.map((value) => { - // return toCandidClass(value); +export function Tuple(...types: CandidClass[]): AzleTuple { + // const idlTypes = types.map((value) => { + // return toIDLType(value); // }); - // return IDL.Tuple(...candidTypes); + // return IDL.Tuple(...idlTypes); return new AzleTuple(types); } diff --git a/src/lib_new/record.ts b/src/lib_new/record.ts index 4f9aa521a7..8aa065352a 100644 --- a/src/lib_new/record.ts +++ b/src/lib_new/record.ts @@ -5,7 +5,7 @@ import { Parent, processMap } from './utils'; // records. While the decorators are able to add constructors they are not // communicating that change to the type checker. If we can get it to do that // then we can get rid of this class -export class Record { +export abstract class Record { constructor(args: any) { if ( Object.entries(this.constructor._azleCandidMap).length !== diff --git a/src/lib_new/result.ts b/src/lib_new/result.ts index d780b330ed..ab4d195967 100644 --- a/src/lib_new/result.ts +++ b/src/lib_new/result.ts @@ -1,6 +1,6 @@ import { RequireExactlyOne } from '../lib/candid_types/variant'; import { IDL } from './index'; -import { CandidClass, Parent, toCandidClass } from './utils'; +import { CandidClass, Parent, toIDLType } from './utils'; export class AzleResult { constructor(ok: any, err: any) { @@ -13,8 +13,8 @@ export class AzleResult { getIDL(parents: Parent[]) { return IDL.Variant({ - Ok: toCandidClass(this._azleOk, parents), - Err: toCandidClass(this._azleErr, parents) + Ok: toIDLType(this._azleOk, parents), + Err: toIDLType(this._azleErr, parents) }); } } diff --git a/src/lib_new/service.ts b/src/lib_new/service.ts index a74a53518a..8a8784eb86 100644 --- a/src/lib_new/service.ts +++ b/src/lib_new/service.ts @@ -1,14 +1,29 @@ import { ic, IDL, Principal } from './index'; import { CandidClass, - toParamCandidClasses, - toReturnCandidClass + Parent, + ReturnCandidClass, + toParamIDLTypes, + toReturnIDLType } from './utils'; +export type FunctionInfo = { + mode: 'query' | 'update'; + paramIdls: CandidClass[]; + returnIdl: ReturnCandidClass; +}; + +export interface ServiceFunctionInfo { + [key: string]: FunctionInfo; +} + +export interface ServiceConstructor { + _azleFunctionInfo?: ServiceFunctionInfo; +} + type Constructor = new (...args: any[]) => T; -// TODO allow turning this into an IDL -export class Service { +export abstract class Service { canisterId: Principal; [key: string]: any; @@ -35,6 +50,34 @@ export class Service { ): InstanceType { return new this(props) as InstanceType; } + + static getIDL(parents: Parent[]): IDL.ServiceClass { + const serviceFunctionInfo: ServiceFunctionInfo = + // @ts-ignore - may be added by @query and @update decorators + this._azleFunctionInfo || {}; + + const record = Object.entries(serviceFunctionInfo).reduce( + (accumulator, [methodName, functionInfo]) => { + const paramRealIdls = toParamIDLTypes(functionInfo.paramIdls); + const returnRealIdl = toReturnIDLType(functionInfo.returnIdl); + + const annotations = + functionInfo.mode === 'update' ? [] : ['query']; + + return { + ...accumulator, + [methodName]: IDL.Func( + paramRealIdls, + returnRealIdl, + annotations + ) + }; + }, + {} as Record + ); + + return IDL.Service(record); + } } export function serviceDecorator( @@ -54,6 +97,7 @@ export function serviceCall( // This must remain a function and not an arrow function // in order to set the context (this) correctly return async function ( + this: Service, _: '_AZLE_CROSS_CANISTER_CALL', notify: boolean, callFunction: @@ -64,7 +108,7 @@ export function serviceCall( ...args: any[] ) { const encodedArgs = new Uint8Array( - IDL.encode(toParamCandidClasses(paramsIdls), args) + IDL.encode(toParamIDLTypes(paramsIdls), args) ); if (notify) { @@ -86,7 +130,7 @@ export function serviceCall( cycles ); - const returnIdls = toReturnCandidClass(returnIdl); + const returnIdls = toReturnIDLType(returnIdl); const decodedResult = IDL.decode(returnIdls, encodedResult)[0]; return decodedResult; diff --git a/src/lib_new/stable_b_tree_map.ts b/src/lib_new/stable_b_tree_map.ts index ce80e54b25..3cb1bc82b8 100644 --- a/src/lib_new/stable_b_tree_map.ts +++ b/src/lib_new/stable_b_tree_map.ts @@ -1,5 +1,5 @@ import { IDL, nat8, nat64, Opt } from './index'; -import { CandidClass, toCandidClass } from './utils'; +import { CandidClass, toIDLType } from './utils'; // TODO something like this is how we would do the inference // TODO but there seems to be a problem with the fixed int and nat classes @@ -8,13 +8,13 @@ import { CandidClass, toCandidClass } from './utils'; // type IDLToCandid = T extends nat64 ? bigint : T extends nat32 ? number : T; export class StableBTreeMap { - keyIdl: CandidClass; - valueIdl: CandidClass; + keyIdl: IDL.Type; + valueIdl: IDL.Type; memoryId: nat8; - constructor(keyIdl: IDL.Type, valueIdl: IDL.Type, memoryId: nat8) { - this.keyIdl = toCandidClass(keyIdl, []); - this.valueIdl = toCandidClass(valueIdl, []); + constructor(keyIdl: CandidClass, valueIdl: CandidClass, memoryId: nat8) { + this.keyIdl = toIDLType(keyIdl, []); + this.valueIdl = toIDLType(valueIdl, []); this.memoryId = memoryId; const candidEncodedMemoryId = new Uint8Array( diff --git a/src/lib_new/utils.ts b/src/lib_new/utils.ts index b808132b07..2deb573883 100644 --- a/src/lib_new/utils.ts +++ b/src/lib_new/utils.ts @@ -1,5 +1,5 @@ -import { IDL, Record, Variant } from './index'; -import { AzleTuple, AzleVec, AzleOpt } from './primitives'; +import { Func, IDL, Record, Service, Variant } from './index'; +import { GetIDL } from './primitives'; /* * Look at each type, @@ -32,7 +32,7 @@ export function extractCandid( } export function display( - idl: CandidClass, + idl: IDL.Type, candidTypeDefs: CandidTypesDefs ): [CandidDef, CandidTypesDefs] { if (idl instanceof IDL.RecClass) { @@ -116,15 +116,7 @@ export type Parent = { name: string; }; -type IDLable = { - getIDL: () => CandidClass; - name: string; -}; - -export function toCandidClass( - idl: CandidClass | IDLable, - parents: Parent[] -): CandidClass { +export function toIDLType(idl: CandidClass, parents: Parent[]): IDL.Type { if ('getIDL' in idl) { if ('name' in idl) { const parent = parents.find((parent) => parent.name === idl.name); @@ -140,13 +132,11 @@ export function toCandidClass( return idl; } -export function toParamCandidClasses(idl: CandidClass[]): CandidClass[] { - return idl.map((value) => toCandidClass(value, [])); +export function toParamIDLTypes(idl: CandidClass[]): IDL.Type[] { + return idl.map((value) => toIDLType(value, [])); } -export function toReturnCandidClass( - returnIdl: ReturnCandidClass -): CandidClass[] { +export function toReturnIDLType(returnIdl: ReturnCandidClass): IDL.Type[] { if (Array.isArray(returnIdl)) { // If Void if (returnIdl.length === 0) { @@ -155,7 +145,7 @@ export function toReturnCandidClass( // Should be unreachable return []; } - return [toCandidClass(returnIdl, [])]; + return [toIDLType(returnIdl, [])]; } type CandidMap = { [key: string]: any }; @@ -166,7 +156,7 @@ export function processMap(targetMap: CandidMap, parent: Parent[]): CandidMap { for (const key in targetMap) { if (targetMap.hasOwnProperty(key)) { const value = targetMap[key]; - const newValue = toCandidClass(value, parent); + const newValue = toIDLType(value, parent); newMap[key] = newValue; } } @@ -175,9 +165,7 @@ export function processMap(targetMap: CandidMap, parent: Parent[]): CandidMap { } export type CandidClass = - | AzleOpt - | AzleTuple - | AzleVec + | GetIDL | IDL.BoolClass | IDL.EmptyClass | IDL.FixedIntClass @@ -194,7 +182,9 @@ export type CandidClass = | IDL.TupleClass | IDL.VecClass | IDL.VecClass // blob - | Record - | Variant; + | typeof Record + | typeof Variant + | typeof Service + | typeof Func; export type ReturnCandidClass = CandidClass | never[]; diff --git a/src/lib_new/variant.ts b/src/lib_new/variant.ts index 745965627f..11bed94ad2 100644 --- a/src/lib_new/variant.ts +++ b/src/lib_new/variant.ts @@ -5,7 +5,7 @@ import { Parent, processMap } from './utils'; // records. While the decorators are able to add constructors they are not // communicating that change to the type checker. If we can get it to do that // then we can get rid of this class -export class Variant { +export abstract class Variant { static create( this: T, props: RequireExactlyOne> diff --git a/src/lib_new/visitors/did_visitor.ts b/src/lib_new/visitors/did_visitor.ts new file mode 100644 index 0000000000..93b6724dfe --- /dev/null +++ b/src/lib_new/visitors/did_visitor.ts @@ -0,0 +1,139 @@ +import { IDL } from '@dfinity/candid'; + +type VisitorData = { usedRecClasses: IDL.RecClass[]; is_on_service: boolean }; +type VisitorResult = [CandidDef, CandidTypesDefs]; + +type TypeName = string; +export type CandidDef = string; +export type CandidTypesDefs = { [key: TypeName]: CandidDef }; +export function extractCandid( + paramInfo: [CandidDef, CandidTypesDefs][] +): [CandidDef[], CandidTypesDefs] { + const paramCandid = paramInfo.map(([candid, _candidTypeDefs]) => { + return candid; + }); + const candidTypeDefs = paramInfo.reduce( + (acc, [_candid, candidTypeDefs]) => { + return { ...acc, ...candidTypeDefs }; + }, + {} + ); + return [paramCandid, candidTypeDefs]; +} + +class DidVisitor extends IDL.Visitor { + visitService(t: IDL.ServiceClass, data: VisitorData): VisitorResult { + const stuff = t._fields.map(([_name, func]) => + func.accept(this, { ...data, is_on_service: true }) + ); + const candid = extractCandid(stuff); + const funcStrings = candid[0] + .map((value, index) => { + return `\t${t._fields[index][0]}: ${value}`; + }) + .join('\n'); + return [`service {\n${funcStrings}\n}`, candid[1]]; + } + visitPrimitive( + t: IDL.PrimitiveType, + data: VisitorData + ): VisitorResult { + return [t.display(), {}]; + } + visitTuple( + t: IDL.TupleClass, + components: IDL.Type[], + data: VisitorData + ): VisitorResult { + const fields = components.map((value) => + value.accept(this, { ...data, is_on_service: false }) + ); + const candid = extractCandid(fields); + return [`record {${candid[0].join('; ')}}`, candid[1]]; + } + visitOpt( + t: IDL.OptClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + const candid = ty.accept(this, { ...data, is_on_service: false }); + return [`opt ${candid[0]}`, candid[1]]; + } + visitVec( + t: IDL.VecClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + const candid = ty.accept(this, { ...data, is_on_service: false }); + return [`vec ${candid[0]}`, candid[1]]; + } + visitFunc(t: IDL.FuncClass, data: VisitorData): VisitorResult { + const argsTypes = t.argTypes.map((value) => + value.accept(this, { ...data, is_on_service: false }) + ); + const candidArgs = extractCandid(argsTypes); + const retsTypes = t.retTypes.map((value) => + value.accept(this, { ...data, is_on_service: false }) + ); + const candidRets = extractCandid(retsTypes); + const args = candidArgs[0].join(', '); + const rets = candidRets[0].join(', '); + const annon = ' ' + t.annotations.join(' '); + return [ + `${ + data.is_on_service ? '' : 'func ' + }(${args}) -> (${rets})${annon}`, + { ...candidArgs[1], ...candidRets[1] } + ]; + } + visitRec( + t: IDL.RecClass, + ty: IDL.ConstructType, + data: VisitorData + ): VisitorResult { + // For RecClasses the definition will be the name, that name will + // reference the actual definition which will be added to the list of + // candid type defs that will get put at the top of the candid file + // Everything else will just be the normal inline candid def + const usedRecClasses = data.usedRecClasses; + if (!usedRecClasses.includes(t)) { + const candid = t.accept(this, { + usedRecClasses: [...usedRecClasses, t], + is_on_service: false + }); + return [t.name, { ...candid[1], [t.name]: candid[0][0] }]; + } + // If our list already includes this rec class then just return, we don't + // need the list because we will get it when we go through the arm above + return [t.name, {}]; + } + visitRecord( + t: IDL.RecordClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + const candidFields = fields.map(([key, value]) => + value.accept(this, { ...data, is_on_service: false }) + ); + const candid = extractCandid(candidFields); + const field_strings = fields.map( + ([key, value], index) => key + ':' + candid[0][index] + ); + return [`record {${field_strings.join('; ')}}`, candid[1]]; + } + visitVariant( + t: IDL.VariantClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + const candidFields = fields.map(([key, value]) => + value.accept(this, { ...data, is_on_service: false }) + ); + const candid = extractCandid(candidFields); + const fields_string = fields.map( + ([key, value], index) => + key + (value.name === 'null' ? '' : ':' + candid[0][index]) + ); + return [`variant {${fields_string.join('; ')}}`, candid[1]]; + } +} diff --git a/src/lib_new/visitors/encode_decode/decode_visitor.ts b/src/lib_new/visitors/encode_decode/decode_visitor.ts new file mode 100644 index 0000000000..852bab5662 --- /dev/null +++ b/src/lib_new/visitors/encode_decode/decode_visitor.ts @@ -0,0 +1,74 @@ +import { IDL } from '@dfinity/candid'; +import { + VisitorData, + VisitorResult, + visitOpt, + visitRec, + visitRecord, + visitTuple, + visitVariant, + visitVec +} from '.'; + +/** + * When we decode a Service we are given a principal. We need to use that + * principal to create a Service class. Same things applies to Funcs except + * that it has a principal and a name. + */ + +export class DecodeVisitor extends IDL.Visitor { + visitService(t: IDL.ServiceClass, data: VisitorData): VisitorResult { + return new data.js_class(data.js_data); + } + visitFunc(t: IDL.FuncClass, data: VisitorData): VisitorResult { + return new data.js_class(data.js_data[0], data.js_data[1]); + } + visitPrimitive( + t: IDL.PrimitiveType, + data: VisitorData + ): VisitorResult { + return data.js_data; + } + visitTuple( + t: IDL.TupleClass, + components: IDL.Type[], + data: VisitorData + ): VisitorResult { + return visitTuple(this, components, data); + } + visitOpt( + t: IDL.OptClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + return visitOpt(this, ty, data); + } + visitVec( + t: IDL.VecClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + return visitVec(this, ty, data); + } + visitRec( + t: IDL.RecClass, + ty: IDL.ConstructType, + data: VisitorData + ): VisitorResult { + return visitRec(this, ty, data); + } + visitRecord( + t: IDL.RecordClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + return visitRecord(this, fields, data); + } + visitVariant( + t: IDL.VariantClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + return visitVariant(this, fields, data); + } +} diff --git a/src/lib_new/visitors/encode_decode/encode_visitor.ts b/src/lib_new/visitors/encode_decode/encode_visitor.ts new file mode 100644 index 0000000000..ba12ab7dcd --- /dev/null +++ b/src/lib_new/visitors/encode_decode/encode_visitor.ts @@ -0,0 +1,75 @@ +import { IDL } from '@dfinity/candid'; +import { + VisitorData, + VisitorResult, + visitOpt, + visitRec, + visitRecord, + visitTuple, + visitVariant, + visitVec +} from '.'; + +/** + * When we encode a Service we have a service class and we need it to be only + * a principal. As a Service is visited the canisterId needs to be extracted so + * it will be encoded correctly. Same thing with Funcs except we need to extract + * the principal and name and put it into a tuple. + */ + +export class EncodeVisitor extends IDL.Visitor { + visitService(t: IDL.ServiceClass, data: VisitorData): VisitorResult { + return data.js_data.canisterId; + } + visitFunc(t: IDL.FuncClass, data: VisitorData): VisitorResult { + return [data.js_data.principal, data.js_data.name]; + } + visitPrimitive( + t: IDL.PrimitiveType, + data: VisitorData + ): VisitorResult { + return data.js_data; + } + visitTuple( + t: IDL.TupleClass, + components: IDL.Type[], + data: VisitorData + ): VisitorResult { + return visitTuple(this, components, data); + } + visitOpt( + t: IDL.OptClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + return visitOpt(this, ty, data); + } + visitVec( + t: IDL.VecClass, + ty: IDL.Type, + data: VisitorData + ): VisitorResult { + return visitVec(this, ty, data); + } + visitRec( + t: IDL.RecClass, + ty: IDL.ConstructType, + data: VisitorData + ): VisitorResult { + return visitRec(this, ty, data); + } + visitRecord( + t: IDL.RecordClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + return visitRecord(this, fields, data); + } + visitVariant( + t: IDL.VariantClass, + fields: [string, IDL.Type][], + data: VisitorData + ): VisitorResult { + return visitVariant(this, fields, data); + } +} diff --git a/src/lib_new/visitors/encode_decode/index.ts b/src/lib_new/visitors/encode_decode/index.ts new file mode 100644 index 0000000000..78d84715fd --- /dev/null +++ b/src/lib_new/visitors/encode_decode/index.ts @@ -0,0 +1,146 @@ +import { IDL } from '@dfinity/candid'; +import { DecodeVisitor } from './decode_visitor'; +import { EncodeVisitor } from './encode_visitor'; +import { AzleResult, Result } from '../../result'; +export { EncodeVisitor, DecodeVisitor }; + +/* + * The VisitorData gives us js_data which is the data that is about to be + * encoded or was just decoded. js_class is the CandidClass (IDLable) class that + * can be used to create the class. + */ +export type VisitorData = { js_data: any; js_class: any }; +/** + * The VisitorResult is the transformed version of js_data that is ready to + * be consumed by the js or ready to be encoded. + */ +export type VisitorResult = any; + +/* + * For most of the visitors the only thing that needs to happen is to visit each + * of the sub nodes. That is the same for both encoding and decoding. That logic + * is extracted into these helper methods. + */ + +export function visitTuple( + visitor: DecodeVisitor | EncodeVisitor, + components: IDL.Type[], + data: VisitorData +): VisitorResult { + const fields = components.map((value, index) => + value.accept(visitor, { + js_data: data.js_data[index], + js_class: data.js_class._azleTypes[index] + }) + ); + return [...fields]; +} + +export function visitOpt( + visitor: DecodeVisitor | EncodeVisitor, + ty: IDL.Type, + data: VisitorData +): VisitorResult { + if (data.js_data.length === 0) { + return data.js_data; + } + const candid = ty.accept(visitor, { + js_data: data.js_data[0], + js_class: data.js_class._azleType + }); + return [candid]; +} + +export function visitVec( + visitor: DecodeVisitor | EncodeVisitor, + ty: IDL.Type, + data: VisitorData +): VisitorResult { + if (ty instanceof IDL.PrimitiveType) { + return data.js_data; + } + const vec_elems = data.js_data.map((array_elem: any) => { + return ty.accept(visitor, { + js_data: array_elem, + js_class: data.js_class._azleType + }); + }); + return [...vec_elems]; +} + +export function visitRecord( + visitor: DecodeVisitor | EncodeVisitor, + fields: [string, IDL.Type][], + data: VisitorData +): VisitorResult { + const candidFields = fields.reduce((acc, [memberName, memberIdl]) => { + const fieldData = data.js_data[memberName]; + const fieldClass = data.js_class._azleCandidMap[memberName]; + + return { + ...acc, + [memberName]: memberIdl.accept(visitor, { + js_data: fieldData, + js_class: fieldClass + }) + }; + }, {}); + return data.js_class.create(candidFields); +} + +export function visitVariant( + visitor: DecodeVisitor | EncodeVisitor, + fields: [string, IDL.Type][], + data: VisitorData +): VisitorResult { + if (data.js_class instanceof AzleResult) { + if ('Ok' in data.js_data) { + const okField = fields[0]; + const okData = data.js_data['Ok']; + const okClass = data.js_class._azleOk; + return Result.Ok( + okField[1].accept(visitor, { + js_data: okData, + js_class: okClass + }) + ); + } + if ('Err' in data.js_data) { + const errField = fields[0]; + const errData = data.js_data['Err']; + const errClass = data.js_class._azleErr; + return Result.Err( + errField[1].accept(visitor, { + js_data: errData, + js_class: errClass + }) + ); + } + } + const candidFields = fields.reduce((acc, [memberName, memberIdl]) => { + const fieldData = data.js_data[memberName]; + const fieldClass = data.js_class._azleCandidMap[memberName]; + if (fieldData === undefined) { + // If the field data is undefined then it is not the variant that was used + return acc; + } + return { + ...acc, + [memberName]: memberIdl.accept(visitor, { + js_class: fieldClass, + js_data: fieldData + }) + }; + }, {}); + return data.js_class.create(candidFields); +} + +export function visitRec( + visitor: DecodeVisitor | EncodeVisitor, + ty: IDL.ConstructType, + data: VisitorData +): VisitorResult { + // TODO I imagine that this will be the spot of much torment when we get + // to doing actual recursive types, maybe + return ty.accept(visitor, data); +}