From 0c157748b46a9dcb2a6c1e622d525942441344c0 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Sat, 1 Apr 2023 00:41:21 -0700 Subject: [PATCH 1/4] feat: update result types in core & validator --- packages/core/package.json | 4 + packages/core/src/lib.js | 1 + packages/core/src/receipt.js | 4 +- packages/core/src/result.js | 51 +++ packages/core/test/result.spec.js | 101 +++++ packages/interface/src/capability.ts | 29 +- packages/interface/src/lib.ts | 29 +- packages/validator/package.json | 2 +- packages/validator/src/capability.js | 95 ++--- packages/validator/src/error.js | 32 +- packages/validator/src/lib.js | 116 +++--- packages/validator/src/schema/did.js | 2 +- packages/validator/src/schema/link.js | 5 +- packages/validator/src/schema/schema.js | 173 +++++---- packages/validator/src/schema/text.js | 2 +- packages/validator/src/schema/type.ts | 12 +- packages/validator/src/schema/uri.js | 2 +- .../validator/test/capability-access.spec.js | 39 +- packages/validator/test/capability.spec.js | 219 +++++------ packages/validator/test/delegate.spec.js | 8 +- packages/validator/test/error.spec.js | 4 - packages/validator/test/extra-schema.spec.js | 227 +++++++---- packages/validator/test/inference.spec.js | 20 +- packages/validator/test/lib.spec.js | 357 ++++++++++-------- packages/validator/test/link-schema.spec.js | 31 +- packages/validator/test/schema.spec.js | 271 ++++++------- packages/validator/test/schema/fixtures.js | 14 +- packages/validator/test/session.spec.js | 71 ++-- packages/validator/test/test.js | 10 + packages/validator/test/util.js | 33 +- packages/validator/test/voucher.js | 14 +- 31 files changed, 1147 insertions(+), 831 deletions(-) create mode 100644 packages/core/src/result.js create mode 100644 packages/core/test/result.spec.js diff --git a/packages/core/package.json b/packages/core/package.json index 17b02aad..04570ae4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,6 +91,10 @@ "./dag": { "types": "./dist/src/dag.d.ts", "import": "./src/dag.js" + }, + "./result": { + "types": "./dist/src/result.d.ts", + "import": "./src/result.js" } }, "c8": { diff --git a/packages/core/src/lib.js b/packages/core/src/lib.js index 225bbe80..478418a3 100644 --- a/packages/core/src/lib.js +++ b/packages/core/src/lib.js @@ -18,3 +18,4 @@ export { sha256 } from 'multiformats/hashes/sha2' export * as UCAN from '@ipld/dag-ucan' export * as DID from '@ipld/dag-ucan/did' export * as Signature from '@ipld/dag-ucan/signature' +export * from './result.js' diff --git a/packages/core/src/receipt.js b/packages/core/src/receipt.js index 2b87b107..dd4870e5 100644 --- a/packages/core/src/receipt.js +++ b/packages/core/src/receipt.js @@ -184,7 +184,7 @@ class ReceptBuilder { * @param {object} options * @param {API.Signer} options.issuer * @param {Ran|ReturnType} options.ran - * @param {API.ReceiptResult} options.result + * @param {API.Result} options.result * @param {API.EffectsModel} [options.fx] * @param {API.Proof[]} [options.proofs] * @param {Record} [options.meta] @@ -252,7 +252,7 @@ const NOFX = Object.freeze({ fork: Object.freeze([]) }) * @param {object} options * @param {API.Signer} options.issuer * @param {Ran|ReturnType} options.ran - * @param {API.ReceiptResult} options.result + * @param {API.Result} options.result * @param {API.EffectsModel} [options.fx] * @param {API.Proof[]} [options.proofs] * @param {Record} [options.meta] diff --git a/packages/core/src/result.js b/packages/core/src/result.js new file mode 100644 index 00000000..f35d8f3e --- /dev/null +++ b/packages/core/src/result.js @@ -0,0 +1,51 @@ +import * as API from '@ucanto/interface' + +/** + * @template {{}|string|boolean|number} T + * @param {T} value + * @returns {{ok: T, value?:undefined}} + */ +export const ok = value => { + if (value == null) { + throw new TypeError(`ok(${value}) is not allowed, consider ok({}) instead`) + } else { + return { ok: value } + } +} + +/** + * @template {{}|string|boolean|number} X + * @param {X} cause + * @returns {{ok?:undefined, error:X}} + */ +export const error = cause => { + if (cause == null) { + throw new TypeError( + `error(${cause}) is not allowed, consider passing an error instead` + ) + } else { + return { error: cause } + } +} + +/** + * @param {string} message + * @returns {{error:API.Failure, ok?:undefined}} + */ +export const fail = message => ({ error: new Failure(message) }) + +/** + * @implements {API.Failure} + */ +export class Failure extends Error { + describe() { + return this.toString() + } + get message() { + return this.describe() + } + toJSON() { + const { name, message, stack } = this + return { name, message, stack } + } +} diff --git a/packages/core/test/result.spec.js b/packages/core/test/result.spec.js new file mode 100644 index 00000000..ba70a2f5 --- /dev/null +++ b/packages/core/test/result.spec.js @@ -0,0 +1,101 @@ +import { assert, test } from './test.js' +import { ok, fail, error, Failure } from '../src/lib.js' + +test('Result.ok', async () => { + assert.deepEqual(ok(1), { ok: 1 }) + assert.deepEqual(ok('hello'), { ok: 'hello' }) + assert.deepEqual(ok(true), { ok: true }) + assert.deepEqual(ok(false), { ok: false }) + assert.deepEqual(ok({ x: 1, y: 2 }), { ok: { x: 1, y: 2 } }) + + assert.throws( + // @ts-expect-error + () => ok(), + /consider ok\({}\) instead/ + ) + + assert.throws( + // @ts-expect-error + () => ok(undefined), + /consider ok\({}\) instead/ + ) + + assert.throws( + // @ts-expect-error + () => ok(null), + /consider ok\({}\) instead/ + ) +}) + +test('Result.error', async () => { + assert.deepEqual(error(1), { error: 1 }) + assert.deepEqual(error('hello'), { error: 'hello' }) + assert.deepEqual(error(true), { error: true }) + assert.deepEqual(error(false), { error: false }) + assert.deepEqual(error({ x: 1, y: 2 }), { error: { x: 1, y: 2 } }) + + assert.throws( + // @ts-expect-error + () => error(), + /consider passing an error/ + ) + + assert.throws( + // @ts-expect-error + () => error(undefined), + /consider passing an error/ + ) + + assert.throws( + // @ts-expect-error + () => error(null), + /consider passing an error/ + ) +}) + +test('Result.fail', async () => { + const error = fail('boom') + + assert.equal(error.error.message, 'boom') + assert.equal(error.error.name, 'Error') + assert.equal(error.error instanceof Error, true) + assert.equal(typeof error.error.stack, 'string') + assert.equal( + JSON.stringify(error), + JSON.stringify({ + error: { + name: 'Error', + message: 'boom', + stack: error.error.stack, + }, + }) + ) +}) + +test('Result.Failure', async () => { + const error = new Failure('boom') + assert.equal(error.describe(), error.toString()) + assert.equal(error.message, 'boom') + + class Boom extends Failure { + constructor() { + super() + this.name = 'Boom' + } + describe() { + return 'BOOM' + } + } + + const boom = new Boom() + assert.equal(boom.message, 'BOOM') + assert.equal(boom.name, 'Boom') + assert.deepEqual( + JSON.stringify(boom), + JSON.stringify({ + name: 'Boom', + message: 'BOOM', + stack: boom.stack, + }) + ) +}) diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index e9bfde86..4ac442b8 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -54,11 +54,7 @@ export interface MatchSelector export interface DirectMatch extends Match> {} -export interface Reader< - O = unknown, - I = unknown, - X extends { error: true } = Failure -> { +export interface Reader { read: (input: I) => Result } @@ -109,7 +105,7 @@ type InferDeriveProofs = T extends [infer U, ...infer E] : never export interface Derives { - (claim: T, proof: U): Result + (claim: T, proof: U): Result<{}, Failure> } export interface View extends Matcher, Selector { @@ -237,16 +233,18 @@ export interface CapabilityParser extends View { * can: "file/read", * with: URI({ protocol: "file:" }), * derives: (claimed, delegated) => - * claimed.with.pathname.startsWith(delegated.with.pathname) || - * new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) + * claimed.with.pathname.startsWith(delegated.with.pathname) + * ? { ok: {} } + * : { error: new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) } * }) * * const write = capability({ * can: "file/write", * with: URI({ protocol: "file:" }), * derives: (claimed, delegated) => - * claimed.with.pathname.startsWith(delegated.with.pathname) || - * new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) + * claimed.with.pathname.startsWith(delegated.with.pathname) + * ? { ok: {} } + * : { error: new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) } * }) * * const readwrite = read.and(write).derive({ @@ -254,16 +252,17 @@ export interface CapabilityParser extends View { * can: "file/read+write", * with: URI({ protocol: "file:" }), * derives: (claimed, delegated) => - * claimed.with.pathname.startsWith(delegated.with.pathname) || - * new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) + * claimed.with.pathname.startsWith(delegated.with.pathname) + * ? { ok: {} } + * : { error: new Failure(`'${claimed.with.href}' is not contained in '${delegated.with.href}'`) } * }), * derives: (claimed, [read, write]) => { * if (!claimed.with.pathname.startsWith(read.with.pathname)) { - * return new Failure(`'${claimed.with.href}' is not contained in '${read.with.href}'`) + * return { error: new Failure(`'${claimed.with.href}' is not contained in '${read.with.href}'`) } * } else if (!claimed.with.pathname.startsWith(write.with.pathname)) { - * return new Failure(`'${claimed.with.href}' is not contained in '${write.with.href}'`) + * return { error: new Failure(`'${claimed.with.href}' is not contained in '${write.with.href}'`) } * } else { - * return true + * return { ok: {} } * } * } * }) diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 35bd9cfe..278c3320 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -396,7 +396,7 @@ export interface OutcomeModel< Ran extends Invocation = Invocation > { ran: ReturnType - out: ReceiptResult + out: Result fx: EffectsModel meta: Meta iss?: DID @@ -431,7 +431,7 @@ export interface Receipt< > extends IPLDView>, IPLDViewBuilder> { readonly ran: Ran | ReturnType - readonly out: ReceiptResult + readonly out: Result readonly fx: Effects readonly meta: Meta @@ -440,9 +440,7 @@ export interface Receipt< readonly signature: SignatureView, Alg> - verifySignature( - signer: Crypto.Verifier - ): Await> + verifySignature(signer: Crypto.Verifier): Await> buildIPLDView(): Receipt } @@ -474,7 +472,7 @@ export interface InstructionModel< * @see https://github.com/ucan-wg/invocation/#6-result */ -export type ReceiptResult = Variant<{ +export type Result = Variant<{ ok: T error: X }> @@ -565,11 +563,7 @@ export type InferInvocations = T extends [] * @typeParam O - type returned by the handler on success * @typeParam X - type returned by the handler on error */ -export interface ServiceMethod< - I extends Capability, - O, - X extends { error: true } -> { +export interface ServiceMethod { (input: Invocation, context: InvocationContext): Await< Result > @@ -705,14 +699,7 @@ export type ExecuteInvocation< ? Out : never -export type Result = - | (T extends null | undefined ? T : never) - | (T & { error?: never }) - | X - -export interface Failure extends Error { - error: true -} +export interface Failure extends Error {} export interface HandlerNotFound extends RangeError { error: true @@ -781,9 +768,7 @@ export interface InboundAcceptCodec { } export interface InboundCodec { - accept( - request: Transport.HTTPRequest - ): ReceiptResult + accept(request: Transport.HTTPRequest): Result } export interface HTTPError { diff --git a/packages/validator/package.json b/packages/validator/package.json index 1d96b76a..060b7b5f 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -21,7 +21,7 @@ "scripts": { "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", - "test": "npm run test:node", + "test": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha --bail test/**/*.spec.js", "coverage": "c8 --reporter=html mocha test/**/*.spec.js", "check": "tsc --build", "build": "tsc --build" diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index b6c03182..9684dca4 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -5,7 +5,6 @@ import { MalformedCapability, UnknownCapability, DelegationError as MatchError, - Failure, } from './error.js' import { invoke, delegate } from '@ucanto/core' import * as Schema from './schema.js' @@ -18,7 +17,7 @@ import * as Schema from './schema.js' * can: A * with: API.Reader * nb?: Schema.MapRepresentation - * derives?: (claim: {can:A, with: R, nb: C}, proof:{can:A, with:R, nb:C}) => API.Result + * derives?: (claim: {can:A, with: R, nb: C}, proof:{can:A, with:R, nb:C}) => API.Result<{}, API.Failure> * }} Descriptor */ @@ -79,11 +78,12 @@ class View { */ /* c8 ignore next 3 */ match(source) { - return new UnknownCapability(source.capability) + return { error: new UnknownCapability(source.capability) } } /** * @param {API.Source[]} capabilities + * @returns {API.Select} */ select(capabilities) { return select(this, capabilities) @@ -157,19 +157,22 @@ class Capability extends Unit { const resource = descriptor.with.read(options.with) if (resource.error) { - throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { - cause: resource, - }) + throw Object.assign( + new Error(`Invalid 'with' - ${resource.error.message}`), + { + cause: resource, + } + ) } const nb = descriptor.nb.read(data) if (nb.error) { - throw Object.assign(new Error(`Invalid 'nb' - ${nb.message}`), { + throw Object.assign(new Error(`Invalid 'nb' - ${nb.error.message}`), { cause: nb, }) } - return createCapability({ can, with: resource, nb }) + return createCapability({ can, with: resource.ok, nb: nb.ok }) } /** @@ -195,20 +198,23 @@ class Capability extends Unit { const resource = descriptor.with.read(with_) if (resource.error) { - throw Object.assign(new Error(`Invalid 'with' - ${resource.message}`), { - cause: resource, - }) + throw Object.assign( + new Error(`Invalid 'with' - ${resource.error.message}`), + { + cause: resource, + } + ) } const nb = descriptor.nb.partial().read(input) if (nb.error) { - throw Object.assign(new Error(`Invalid 'nb' - ${nb.message}`), { + throw Object.assign(new Error(`Invalid 'nb' - ${nb.error.message}`), { cause: nb, }) } return delegate({ - capabilities: [createCapability({ can, with: resource, nb })], + capabilities: [createCapability({ can, with: resource.ok, nb: nb.ok })], ...options, }) } @@ -223,7 +229,9 @@ class Capability extends Unit { */ match(source) { const result = parseCapability(this.descriptor, source) - return result.error ? result : new Match(source, result, this.descriptor) + return result.error + ? result + : { ok: new Match(source, result.ok, this.descriptor) } } toString() { return JSON.stringify({ can: this.descriptor.can }) @@ -281,7 +289,7 @@ class Or extends Unit { if (left.error) { const right = this.right.match(capability) if (right.error) { - return right.name === 'MalformedCapability' + return right.error.name === 'MalformedCapability' ? // right : // @@ -323,11 +331,13 @@ class And extends View { if (result.error) { return result } else { - group.push(result) + group.push(result.ok) } } - return new AndMatch(/** @type {API.InferMembers} */ (group)) + return { + ok: new AndMatch(/** @type {API.InferMembers} */ (group)), + } } /** @@ -400,7 +410,7 @@ class Derive extends Unit { if (match.error) { return match } else { - return new DerivedMatch(match, this.from, this.derives) + return { ok: new DerivedMatch(match.ok, this.from, this.derives) } } } toString() { @@ -459,26 +469,26 @@ class Match { const matches = [] for (const capability of capabilities) { const result = resolveCapability(this.descriptor, this.value, capability) - if (!result.error) { - const claim = this.descriptor.derives(this.value, result) + if (result.ok) { + const claim = this.descriptor.derives(this.value, result.ok) if (claim.error) { errors.push( new MatchError( - [new EscalatedCapability(this.value, result, claim)], + [new EscalatedCapability(this.value, result.ok, claim.error)], this ) ) } else { - matches.push(new Match(capability, result, this.descriptor)) + matches.push(new Match(capability, result.ok, this.descriptor)) } } else { - switch (result.name) { + switch (result.error.name) { case 'UnknownCapability': - unknown.push(result.capability) + unknown.push(result.error.capability) break case 'MalformedCapability': default: - errors.push(new MatchError([result], this)) + errors.push(new MatchError([result.error], this)) } } } @@ -558,7 +568,7 @@ class DerivedMatch { if (result.error) { errors.push( new MatchError( - [new EscalatedCapability(value, match.value, result)], + [new EscalatedCapability(value, match.value, result.error)], this ) ) @@ -738,20 +748,20 @@ const parseCapability = (descriptor, source) => { const capability = /** @type {API.Capability} */ (source.capability) if (descriptor.can !== capability.can) { - return new UnknownCapability(capability) + return { error: new UnknownCapability(capability) } } const uri = descriptor.with.read(capability.with) if (uri.error) { - return new MalformedCapability(capability, uri) + return { error: new MalformedCapability(capability, uri.error) } } const nb = descriptor.nb.read(capability.nb || {}) if (nb.error) { - return new MalformedCapability(capability, nb) + return { error: new MalformedCapability(capability, nb.error) } } - return new CapabilityView(descriptor.can, uri, nb, delegation) + return { ok: new CapabilityView(descriptor.can, uri.ok, nb.ok, delegation) } } /** @@ -774,7 +784,7 @@ const parseCapability = (descriptor, source) => { const resolveCapability = (descriptor, claimed, { capability, delegation }) => { const can = resolveAbility(capability.can, claimed.can, null) if (can == null) { - return new UnknownCapability(capability) + return { error: new UnknownCapability(capability) } } const resource = resolveResource( @@ -784,7 +794,7 @@ const resolveCapability = (descriptor, claimed, { capability, delegation }) => { ) const uri = descriptor.with.read(resource) if (uri.error) { - return new MalformedCapability(capability, uri) + return { error: new MalformedCapability(capability, uri.error) } } const nb = descriptor.nb.read({ @@ -793,10 +803,10 @@ const resolveCapability = (descriptor, claimed, { capability, delegation }) => { }) if (nb.error) { - return new MalformedCapability(capability, nb) + return { error: new MalformedCapability(capability, nb.error) } } - return new CapabilityView(can, uri, nb, delegation) + return { ok: new CapabilityView(can, uri.ok, nb.ok, delegation) } } /** @@ -823,6 +833,7 @@ class CapabilityView { * @template {API.Match} M * @param {API.Matcher} matcher * @param {API.Source[]} capabilities + * @returns {API.Select} */ const select = (matcher, capabilities) => { @@ -832,16 +843,16 @@ const select = (matcher, capabilities) => { for (const capability of capabilities) { const result = matcher.match(capability) if (result.error) { - switch (result.name) { + switch (result.error.name) { case 'UnknownCapability': - unknown.push(result.capability) + unknown.push(result.error.capability) break case 'MalformedCapability': default: - errors.push(new MatchError([result], result.capability)) + errors.push(new MatchError([result.error], result.error.capability)) } } else { - matches.push(result) + matches.push(result.ok) } } @@ -892,12 +903,12 @@ const selectGroup = (self, capabilities) => { const defaultDerives = (claimed, delegated) => { if (delegated.with.endsWith('*')) { if (!claimed.with.startsWith(delegated.with.slice(0, -1))) { - return new Failure( + return Schema.error( `Resource ${claimed.with} does not match delegated ${delegated.with} ` ) } } else if (delegated.with !== claimed.with) { - return new Failure( + return Schema.error( `Resource ${claimed.with} is not contained by ${delegated.with}` ) } @@ -909,9 +920,9 @@ const defaultDerives = (claimed, delegated) => { for (const [name, value] of kv) { if (nb[name] != value) { - return new Failure(`${String(name)}: ${nb[name]} violates ${value}`) + return Schema.error(`${String(name)}: ${nb[name]} violates ${value}`) } } - return true + return { ok: true } } diff --git a/packages/validator/src/error.js b/packages/validator/src/error.js index f23d30b0..682049b0 100644 --- a/packages/validator/src/error.js +++ b/packages/validator/src/error.js @@ -1,28 +1,9 @@ import * as API from '@ucanto/interface' import { the } from './util.js' import { isLink } from '@ucanto/core/link' +import { fail, Failure } from '@ucanto/core/result' -/** - * @implements {API.Failure} - */ -export class Failure extends Error { - /** @type {true} */ - get error() { - return true - } - /* c8 ignore next 3 */ - describe() { - return this.name - } - get message() { - return this.describe() - } - - toJSON() { - const { error, name, message, stack } = this - return { error, name, message, stack } - } -} +export { Failure, fail } export class EscalatedCapability extends Failure { /** @@ -200,9 +181,8 @@ export class PrincipalAlignmentError extends Failure { return `Delegation audience is '${this.delegation.audience.did()}' instead of '${this.audience.did()}'` } toJSON() { - const { error, name, audience, message, stack } = this + const { name, audience, message, stack } = this return { - error, name, audience: audience.did(), delegation: { audience: this.delegation.audience.did() }, @@ -269,9 +249,8 @@ export class Expired extends Failure { return this.delegation.expiration } toJSON() { - const { error, name, expiredAt, message, stack } = this + const { name, expiredAt, message, stack } = this return { - error, name, message, expiredAt, @@ -298,9 +277,8 @@ export class NotValidBefore extends Failure { return this.delegation.notBefore } toJSON() { - const { error, name, validAt, message, stack } = this + const { name, validAt, message, stack } = this return { - error, name, message, validAt, diff --git a/packages/validator/src/lib.js b/packages/validator/src/lib.js index 435c20dd..04693862 100644 --- a/packages/validator/src/lib.js +++ b/packages/validator/src/lib.js @@ -1,5 +1,5 @@ import * as API from '@ucanto/interface' -import { isDelegation, UCAN } from '@ucanto/core' +import { isDelegation, UCAN, ok, fail } from '@ucanto/core' import { capability } from './capability.js' import * as Schema from './schema.js' import { @@ -16,28 +16,31 @@ import { li, } from './error.js' +export { capability } from './capability.js' +export * from './schema.js' + export { + Schema, Failure, + fail, + ok, UnavailableProof, MalformedCapability, DIDKeyResolutionError as DIDResolutionError, } -export { capability } from './capability.js' -export * from './schema.js' -export { Schema } - /** * @param {UCAN.Link} proof + * @returns {{error:API.UnavailableProof}} */ -const unavailable = proof => new UnavailableProof(proof) +const unavailable = proof => ({ error: new UnavailableProof(proof) }) /** * * @param {UCAN.DID} did - * @returns {API.DIDKeyResolutionError} + * @returns {{error:API.DIDKeyResolutionError}} */ -const failDIDKeyResolution = did => new DIDKeyResolutionError(did) +const failDIDKeyResolution = did => ({ error: new DIDKeyResolutionError(did) }) /** * @param {Required} config @@ -94,9 +97,9 @@ const resolveProofs = async (proofs, config) => { try { const result = await config.resolve(proof) if (result.error) { - errors.push(result) + errors.push(result.error) } else { - delegations.push(result) + delegations.push(result.ok) } } catch (error) { errors.push( @@ -162,7 +165,7 @@ const resolveSources = async ({ delegation }, config) => { // signature) save a corresponding proof error. const validation = await validate(proof, proofs, config) if (validation.error) { - errors.push(new ProofError(proof.cid, validation)) + errors.push(new ProofError(proof.cid, validation.error)) } else { // otherwise create source objects for it's capabilities, so we could // track which proof in which capability the are from. @@ -248,19 +251,18 @@ export const claim = async ( for (const proof of delegations) { // Validate each proof if valid add ech capability to the list of sources. // otherwise collect the error. - const delegation = await validate(proof, delegations, config) - if (!delegation.error) { - for (const [index, capability] of delegation.capabilities.entries()) { + const validation = await validate(proof, delegations, config) + if (validation.ok) { + for (const capability of validation.ok.capabilities.values()) { sources.push( /** @type {API.Source} */ ({ capability, - delegation, - index, + delegation: validation.ok, }) ) } } else { - invalidProofs.push(delegation) + invalidProofs.push(validation.error) } } // look for the matching capability @@ -271,24 +273,26 @@ export const claim = async ( for (const matched of selection.matches) { const selector = matched.prune(config) if (selector == null) { - return new Authorization(matched, []) + return { ok: new Authorization(matched, []) } } else { const result = await authorize(selector, config) if (result.error) { - failedProofs.push(result) + failedProofs.push(result.error) } else { - return new Authorization(matched, [result]) + return { ok: new Authorization(matched, [result.ok]) } } } } - return new Unauthorized({ - capability, - delegationErrors, - unknownCapabilities, - invalidProofs, - failedProofs, - }) + return { + error: new Unauthorized({ + capability, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + }), + } } /** @@ -338,26 +342,28 @@ export const authorize = async (match, config) => { if (selector == null) { // @ts-expect-error - it may not be a parsed capability but rather a // group of capabilities but we can deal with that in the future. - return new Authorization(matched, []) + return { ok: new Authorization(matched, []) } } else { const result = await authorize(selector, config) if (result.error) { - failedProofs.push(result) + failedProofs.push(result.error) } else { // @ts-expect-error - it may not be a parsed capability but rather a // group of capabilities but we can deal with that in the future. - return new Authorization(matched, [result]) + return { ok: new Authorization(matched, [result.ok]) } } } } - return new InvalidClaim({ - match, - delegationErrors, - unknownCapabilities, - invalidProofs, - failedProofs, - }) + return { + error: new InvalidClaim({ + match, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + }), + } } class ProofError extends Failure { @@ -476,10 +482,6 @@ class Unauthorized extends Failure { : []), ].join('\n') } - toJSON() { - const { error, name, message, stack } = this - return { error, name, message, stack } - } } /** @@ -494,15 +496,19 @@ class Unauthorized extends Failure { */ const validate = async (delegation, proofs, config) => { if (UCAN.isExpired(delegation.data)) { - return new Expired( - /** @type {API.Delegation & {expiration: number}} */ (delegation) - ) + return { + error: new Expired( + /** @type {API.Delegation & {expiration: number}} */ (delegation) + ), + } } if (UCAN.isTooEarly(delegation.data)) { - return new NotValidBefore( - /** @type {API.Delegation & {notBefore: number}} */ (delegation) - ) + return { + error: new NotValidBefore( + /** @type {API.Delegation & {notBefore: number}} */ (delegation) + ), + } } return await verifyAuthorization(delegation, proofs, config) @@ -536,10 +542,12 @@ const verifyAuthorization = async (delegation, proofs, config) => { // attempt to resolve embedded authorization session from the authority. const session = await verifySession(delegation, proofs, config) // If we have valid session we consider authorization valid - if (!session.error) { - return delegation - } else if (session.failedProofs.length > 0) { - return new SessionEscalation({ delegation, cause: session }) + if (session.ok) { + return { ok: delegation } + } else if (session.error.failedProofs.length > 0) { + return { + error: new SessionEscalation({ delegation, cause: session.error }), + } } // Otherwise we try to resolve did:key from the DID instead // and use that to verify the signature @@ -550,7 +558,7 @@ const verifyAuthorization = async (delegation, proofs, config) => { } else { return verifySignature( delegation, - config.principal.parse(verifier).withDID(issuer) + config.principal.parse(verifier.ok).withDID(issuer) ) } } @@ -565,7 +573,9 @@ const verifyAuthorization = async (delegation, proofs, config) => { */ const verifySignature = async (delegation, verifier) => { const valid = await UCAN.verifySignature(delegation.data, verifier) - return valid ? delegation : new InvalidSignature(delegation, verifier) + return valid + ? { ok: delegation } + : { error: new InvalidSignature(delegation, verifier) } } /** diff --git a/packages/validator/src/schema/did.js b/packages/validator/src/schema/did.js index 752f0f9f..67fbaa37 100644 --- a/packages/validator/src/schema/did.js +++ b/packages/validator/src/schema/did.js @@ -15,7 +15,7 @@ class DIDSchema extends Schema.API { if (!source.startsWith(prefix)) { return Schema.error(`Expected a ${prefix} but got "${source}" instead`) } else { - return /** @type {API.DID} */ (source) + return { ok: /** @type {API.DID} */ (source) } } } } diff --git a/packages/validator/src/schema/link.js b/packages/validator/src/schema/link.js index a2f314c3..847764da 100644 --- a/packages/validator/src/schema/link.js +++ b/packages/validator/src/schema/link.js @@ -68,8 +68,9 @@ class LinkSchema extends Schema.API { ) } - // @ts-expect-error - can't infer version, code etc. - return cid + return { + ok: /** @type {API.Link} */ (cid), + } } } } diff --git a/packages/validator/src/schema/schema.js b/packages/validator/src/schema/schema.js index 7f07712e..cdcafc7b 100644 --- a/packages/validator/src/schema/schema.js +++ b/packages/validator/src/schema/schema.js @@ -1,7 +1,8 @@ import * as Schema from './type.js' - +import { ok } from '@ucanto/core/result' export * from './type.js' +export { ok } /** * @abstract * @template [T=unknown] @@ -54,10 +55,10 @@ export class API { */ from(value) { const result = this.read(/** @type {I} */ (value)) - if (result?.error) { - throw result + if (result.error) { + throw result.error } else { - return result + return result.ok } } @@ -128,7 +129,7 @@ export class API { // we also check that fallback is not undefined because that is the point // of having a fallback if (fallback === undefined) { - throw new Error(`Value of type undefined is not a vaild default`) + throw new Error(`Value of type undefined is not a valid default`) } const schema = new Default({ @@ -176,7 +177,7 @@ class Unknown extends API { * @param {I} input */ read(input) { - return /** @type {Schema.ReadResult}*/ (input) + return /** @type {Schema.ReadResult}*/ ({ ok: input }) } toString() { return 'unknown()' @@ -202,12 +203,17 @@ class Nullable extends API { */ readWith(input, reader) { const result = reader.read(input) - if (result?.error) { + if (result.error) { return input === null - ? null - : new UnionError({ - causes: [result, typeError({ expect: 'null', actual: input })], - }) + ? { ok: null } + : { + error: new UnionError({ + causes: [ + result.error, + typeError({ expect: 'null', actual: input }).error, + ], + }), + } } else { return result } @@ -242,7 +248,7 @@ class Optional extends API { */ readWith(input, reader) { const result = reader.read(input) - return result?.error && input === undefined ? undefined : result + return result.error && input === undefined ? { ok: undefined } : result } toString() { return `${this.settings}.optional()` @@ -274,13 +280,17 @@ class Default extends API { */ readWith(input, { reader, value }) { if (input === undefined) { - return /** @type {Schema.ReadResult} */ (value) + return /** @type {Schema.ReadResult} */ ({ ok: value }) } else { const result = reader.read(input) - return /** @type {Schema.ReadResult} */ ( - result === undefined ? value : result - ) + return result.error + ? result + : result.ok !== undefined + ? // We just checked that result.ok is not undefined but still needs + // reassurance + /** @type {Schema.ReadResult} */ (result) + : { ok: value } } } toString() { @@ -321,13 +331,13 @@ class ArrayOf extends API { const results = [] for (const [index, value] of input.entries()) { const result = schema.read(value) - if (result?.error) { - return memberError({ at: index, cause: result }) + if (result.error) { + return memberError({ at: index, cause: result.error }) } else { - results.push(result) + results.push(result.ok) } } - return results + return { ok: results } } get element() { return this.settings @@ -363,22 +373,20 @@ class Tuple extends API { return typeError({ expect: 'array', actual: input }) } if (input.length !== this.shape.length) { - return new SchemaError( - `Array must contain exactly ${this.shape.length} elements` - ) + return error(`Array must contain exactly ${this.shape.length} elements`) } const results = [] for (const [index, reader] of shape.entries()) { const result = reader.read(input[index]) - if (result?.error) { - return memberError({ at: index, cause: result }) + if (result.error) { + return memberError({ at: index, cause: result.error }) } else { - results[index] = result + results[index] = result.ok } } - return /** @type {Schema.InferTuple} */ (results) + return { ok: /** @type {Schema.InferTuple} */ (results) } } /** @type {U} */ @@ -426,22 +434,22 @@ class Dictionary extends API { for (const [k, v] of Object.entries(input)) { const keyResult = key.read(k) - if (keyResult?.error) { - return memberError({ at: k, cause: keyResult }) + if (keyResult.error) { + return memberError({ at: k, cause: keyResult.error }) } const valueResult = value.read(v) - if (valueResult?.error) { - return memberError({ at: k, cause: valueResult }) + if (valueResult.error) { + return memberError({ at: k, cause: valueResult.error }) } // skip undefined because they mess up CBOR and are generally useless. - if (valueResult !== undefined) { - dict[keyResult] = valueResult + if (valueResult.ok !== undefined) { + dict[keyResult.ok] = valueResult.ok } } - return dict + return { ok: dict } } get key() { return this.settings.key @@ -471,8 +479,11 @@ class Dictionary extends API { * @param {Schema.Reader} [shape.key] * @returns {Schema.DictionarySchema} */ -export const dictionary = ({ value, key = string() }) => - new Dictionary({ value, key }) +export const dictionary = ({ value, key }) => + new Dictionary({ + value, + key: key || /** @type {Schema.Reader} */ (string()), + }) /** * @template {[unknown, ...unknown[]]} T @@ -488,7 +499,7 @@ class Enum extends API { */ readWith(input, { variants, type }) { if (variants.has(input)) { - return /** @type {Schema.ReadResult} */ (input) + return /** @type {Schema.ReadResult} */ ({ ok: input }) } else { return typeError({ expect: type, actual: input }) } @@ -528,13 +539,13 @@ class Union extends API { const causes = [] for (const reader of variants) { const result = reader.read(input) - if (result?.error) { - causes.push(result) + if (result.error) { + causes.push(result.error) } else { - return result + return /** @type {Schema.ReadResult>} */ (result) } } - return new UnionError({ causes }) + return { error: new UnionError({ causes }) } } get variants() { @@ -567,27 +578,29 @@ export const or = (left, right) => union([left, right]) * @template {Schema.Reader} T * @template {[T, ...T[]]} U * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} + * @extends {API, I, U>} + * @implements {Schema.Schema, I>} */ class Intersection extends API { /** * @param {I} input * @param {U} schemas - * @returns {Schema.ReadResult>} + * @returns {Schema.ReadResult>} */ readWith(input, schemas) { const causes = [] for (const schema of schemas) { const result = schema.read(input) - if (result?.error) { - causes.push(result) + if (result.error) { + causes.push(result.error) } } return causes.length > 0 - ? new IntersectionError({ causes }) - : /** @type {Schema.ReadResult>} */ (input) + ? { error: new IntersectionError({ causes }) } + : /** @type {Schema.ReadResult>} */ ({ + ok: input, + }) } toString() { return `intersection([${this.settings @@ -601,7 +614,7 @@ class Intersection extends API { * @template {[T, ...T[]]} U * @template [I=unknown] * @param {U} variants - * @returns {Schema.Schema, I>} + * @returns {Schema.Schema, I>} */ export const intersection = variants => new Intersection(variants) @@ -626,7 +639,7 @@ class Boolean extends API { switch (input) { case true: case false: - return /** @type {boolean} */ (input) + return { ok: /** @type {boolean} */ (input) } default: return typeError({ expect: 'boolean', @@ -688,7 +701,7 @@ class AnyNumber extends UnknownNumber { */ readWith(input) { return typeof input === 'number' - ? input + ? { ok: input } : typeError({ expect: 'number', actual: input }) } toString() { @@ -715,7 +728,7 @@ class RefinedNumber extends UnknownNumber { */ readWith(input, { base, schema }) { const result = base.read(input) - return result?.error ? result : schema.read(result) + return result.error ? result : schema.read(result.ok) } toString() { return `${this.settings.base}.refine(${this.settings.schema})` @@ -734,7 +747,7 @@ class LessThan extends API { */ readWith(input, number) { if (input < number) { - return input + return { ok: input } } else { return error(`Expected ${input} < ${number}`) } @@ -763,7 +776,7 @@ class GreaterThan extends API { */ readWith(input, number) { if (input > number) { - return input + return { ok: input } } else { return error(`Expected ${input} > ${number}`) } @@ -787,7 +800,7 @@ const Integer = { */ read(input) { return Number.isInteger(input) - ? /** @type {Schema.Integer} */ (input) + ? { ok: /** @type {Schema.Integer} */ (input) } : typeError({ expect: 'integer', actual: input, @@ -808,7 +821,7 @@ const Float = { */ read(number) { return Number.isFinite(number) - ? /** @type {Schema.Float} */ (number) + ? { ok: /** @type {Schema.Float} */ (number) } : typeError({ expect: 'Float', actual: number, @@ -877,9 +890,9 @@ class RefinedString extends UnknownString { */ readWith(input, { base, schema }) { const result = base.read(input) - return result?.error + return result.error ? result - : /** @type {Schema.ReadResult} */ (schema.read(result)) + : /** @type {Schema.ReadResult} */ (schema.read(result.ok)) } toString() { return `${this.settings.base}.refine(${this.settings.schema})` @@ -898,7 +911,7 @@ class AnyString extends UnknownString { */ readWith(input) { return typeof input === 'string' - ? input + ? { ok: input } : typeError({ expect: 'string', actual: input }) } } @@ -919,9 +932,13 @@ class StartsWith extends API { * @param {Prefix} prefix */ readWith(input, prefix) { - return input.startsWith(prefix) - ? /** @type {Schema.ReadResult} */ (input) + const result = input.startsWith(prefix) + ? /** @type {Schema.ReadResult} */ ({ + ok: input, + }) : error(`Expect string to start with "${prefix}" instead got "${input}"`) + + return result } get prefix() { return this.settings @@ -951,7 +968,9 @@ class EndsWith extends API { */ readWith(input, suffix) { return input.endsWith(suffix) - ? /** @type {Schema.ReadResult} */ (input) + ? /** @type {Schema.ReadResult} */ ({ + ok: input, + }) : error(`Expect string to end with "${suffix}" instead got "${input}"`) } get suffix() { @@ -984,7 +1003,7 @@ class Refine extends API { */ readWith(input, { base, schema }) { const result = base.read(input) - return result?.error ? result : schema.read(result) + return result.error ? result : schema.read(result.ok) } toString() { return `${this.settings.base}.refine(${this.settings.schema})` @@ -1015,8 +1034,8 @@ class Literal extends API { */ readWith(input, expect) { return input !== /** @type {unknown} */ (expect) - ? new LiteralError({ expect, actual: input }) - : expect + ? { error: new LiteralError({ expect, actual: input }) } + : { ok: expect } } get value() { return /** @type {Exclude} */ (this.settings) @@ -1070,16 +1089,16 @@ class Struct extends API { for (const [at, reader] of entries) { const result = reader.read(source[at]) - if (result?.error) { - return memberError({ at, cause: result }) + if (result.error) { + return memberError({ at, cause: result.error }) } // skip undefined because they mess up CBOR and are generally useless. - else if (result !== undefined) { - struct[at] = /** @type {Schema.Infer} */ (result) + else if (result.ok !== undefined) { + struct[at] = /** @type {Schema.Infer} */ (result.ok) } } - return struct + return { ok: struct } } /** @@ -1162,9 +1181,9 @@ export const struct = fields => { /** * @param {string} message - * @returns {Schema.Error} + * @returns {{error: Schema.Error}} */ -export const error = message => new SchemaError(message) +export const error = message => ({ error: new SchemaError(message) }) class SchemaError extends Error { get name() { @@ -1211,9 +1230,9 @@ class TypeError extends SchemaError { * @param {object} data * @param {string} data.expect * @param {unknown} data.actual - * @returns {Schema.Error} + * @returns {{ error: Schema.Error }} */ -export const typeError = data => new TypeError(data) +export const typeError = data => ({ error: new TypeError(data) }) /** * @@ -1306,12 +1325,12 @@ class FieldError extends SchemaError { * @param {object} options * @param {string|number} options.at * @param {Schema.Error} options.cause - * @returns {Schema.Error} + * @returns {{error: Schema.Error}} */ export const memberError = ({ at, cause }) => typeof at === 'string' - ? new FieldError({ at, cause }) - : new ElementError({ at, cause }) + ? { error: new FieldError({ at, cause }) } + : { error: new ElementError({ at, cause }) } class UnionError extends SchemaError { /** diff --git a/packages/validator/src/schema/text.js b/packages/validator/src/schema/text.js index 78cd1cab..cbf13afe 100644 --- a/packages/validator/src/schema/text.js +++ b/packages/validator/src/schema/text.js @@ -29,7 +29,7 @@ class Match extends Schema.API { `Expected to match ${pattern} but got "${source}" instead` ) } else { - return source + return { ok: source } } } } diff --git a/packages/validator/src/schema/type.ts b/packages/validator/src/schema/type.ts index d26058fb..dabb3714 100644 --- a/packages/validator/src/schema/type.ts +++ b/packages/validator/src/schema/type.ts @@ -1,16 +1,12 @@ import { Failure as Error, Result, Phantom } from '@ucanto/interface' -export interface Reader< - O = unknown, - I = unknown, - X extends { error: true } = Error -> { +export interface Reader { read(input: I): Result } -export type { Error } +export type { Error, Result } -export type ReadResult = Result +export type ReadResult = Result export interface Schema< O extends unknown = unknown, @@ -136,7 +132,7 @@ export type Float = number & Phantom<{ typeof: 'float' }> export type Infer = T extends Reader ? T : never -export type InferIntesection = { +export type InferIntersection = { [K in keyof U]: (input: Infer) => void }[number] extends (input: infer T) => void ? T diff --git a/packages/validator/src/schema/uri.js b/packages/validator/src/schema/uri.js index 64796640..d2fb1b34 100644 --- a/packages/validator/src/schema/uri.js +++ b/packages/validator/src/schema/uri.js @@ -28,7 +28,7 @@ class URISchema extends Schema.API { if (protocol != null && url.protocol !== protocol) { return Schema.error(`Expected ${protocol} URI instead got ${url.href}`) } else { - return /** @type {API.URI} */ (url.href) + return { ok: /** @type {API.URI} */ (url.href) } } } catch (_) { return Schema.error(`Invalid URI`) diff --git a/packages/validator/test/capability-access.spec.js b/packages/validator/test/capability-access.spec.js index baa37371..aaf5cd8a 100644 --- a/packages/validator/test/capability-access.spec.js +++ b/packages/validator/test/capability-access.spec.js @@ -1,7 +1,6 @@ -import { test, assert } from './test.js' +import { test, assert, matchError } from './test.js' import { access, claim, DID } from '../src/lib.js' -import { capability, URI, Link, Schema } from '../src/lib.js' -import { Failure } from '../src/error.js' +import { capability, fail, URI, Link, Schema } from '../src/lib.js' import { ed25519, Verifier } from '@ucanto/principal' import * as Client from '@ucanto/client' import * as Core from '@ucanto/core' @@ -20,13 +19,13 @@ const capabilities = { }), derives: (claim, proof) => { if (claim.with !== proof.with) { - return new Failure('with field does not match') + return fail('with field does not match') } else if (proof.nb.size != null) { if ((claim.nb.size || Infinity) > proof.nb.size) { - return new Failure('Escalates size constraint') + return fail('Escalates size constraint') } } - return true + return { ok: {} } }, }), list: capability({ @@ -122,7 +121,7 @@ test('validates with patterns in chain', async () => { } ) - assert.match(r1.toString(), /Encountered unknown capabilities/) + matchError(r1, /Encountered unknown capabilities/) const r2 = await access( await Client.delegate({ @@ -198,7 +197,7 @@ test('invalid proof chain', async () => { } ) - assert.match(result.toString(), /Expected link to be a CID instead of \*/) + matchError(result, /Expected link to be a CID instead of \*/) }) test('restrictions in chain are respected', async () => { @@ -261,11 +260,7 @@ test('restrictions in chain are respected', async () => { } ) - assert.match( - boom.toString(), - /Unauthorized/, - 'should only allow dev/* capabilities' - ) + matchError(boom, /Unauthorized/, 'should only allow dev/* capabilities') const ping = capabilities.dev.ping.invoke({ issuer: jordan, @@ -318,11 +313,7 @@ test('unknown caveats do not apply', async () => { principal: Verifier, }) - assert.match( - boom.toString(), - /Constraint violation: message/, - 'message caveat applies' - ) + matchError(boom, /Constraint violation: message/, 'message caveat applies') const add = capabilities.store.add.invoke({ issuer: bob, @@ -371,7 +362,7 @@ test('with pattern requires delimiter', async () => { principal: Verifier, }) - assert.match(result.toString(), /capability not found/) + matchError(result, /capability not found/) }) test('can pattern requires delimiter', async () => { @@ -402,8 +393,8 @@ test('can pattern requires delimiter', async () => { principal: Verifier, }) - assert.match( - result.toString(), + matchError( + result, /capability not found/, 'can without delimiter is not allowed' ) @@ -448,7 +439,7 @@ test('patterns do not escalate', async () => { principal: Verifier, }) - assert.match(error.toString(), /Escalates size constraint/) + matchError(error, /Escalates size constraint/) const implicitEscalate = capabilities.store.add.invoke({ issuer: mallory, @@ -465,7 +456,7 @@ test('patterns do not escalate', async () => { principal: Verifier, }) - assert.match(stillError.toString(), /Escalates size constraint/) + matchError(stillError, /Escalates size constraint/) const add = capabilities.store.add.invoke({ issuer: mallory, @@ -537,7 +528,7 @@ test('without nb', async () => { capability: capabilities.store.add, principal: Verifier, }) - assert.match(addEscalateError.toString(), /Escalates size constraint/) + matchError(addEscalateError, /Escalates size constraint/) const list = capabilities.store.list.invoke({ issuer: bob, diff --git a/packages/validator/test/capability.spec.js b/packages/validator/test/capability.spec.js index 7d886d09..9e86f6fa 100644 --- a/packages/validator/test/capability.spec.js +++ b/packages/validator/test/capability.spec.js @@ -1,4 +1,4 @@ -import { capability, URI, Link, Schema } from '../src/lib.js' +import { capability, URI, Link, Schema, ok, fail } from '../src/lib.js' import { invoke, parseLink } from '@ucanto/core' import * as API from '@ucanto/interface' import { Failure } from '../src/error.js' @@ -33,11 +33,9 @@ test('capability selects matches', () => { with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => { if (claimed.with.startsWith(delegated.with)) { - return true + return ok({}) } else { - return new Failure( - `'${claimed.with}' is not contained in '${delegated.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${delegated.with}'`) } }, }) @@ -176,11 +174,9 @@ test('derived capability chain', () => { with: URI.match({ protocol: 'mailto:' }), derives: (claimed, delegated) => { if (claimed.with.startsWith(delegated.with)) { - return true + return ok({}) } else { - return new Failure( - `'${claimed.with}' is not contained in '${delegated.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${delegated.with}'`) } }, }) @@ -195,10 +191,9 @@ test('derived capability chain', () => { /** @type {"account/register"} */ const c2 = delegated.can - return ( - claimed.with === delegated.with || - new Failure(`'${claimed.with}' != '${delegated.with}'`) - ) + return claimed.with === delegated.with + ? ok({}) + : fail(`'${claimed.with}' != '${delegated.with}'`) }, }), derives: (claimed, delegated) => { @@ -207,10 +202,9 @@ test('derived capability chain', () => { /** @type {"account/verify"} */ const c2 = delegated.can - return ( - claimed.with === delegated.with || - new Failure(`'${claimed.with}' != '${delegated.with}'`) - ) + return claimed.with === delegated.with + ? ok({}) + : fail(`'${claimed.with}' != '${delegated.with}'`) }, }) @@ -420,16 +414,18 @@ test('capability amplification', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.and(write).derive({ @@ -437,22 +433,17 @@ test('capability amplification', () => { can: 'file/read+write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure( - `'${claimed.with}' is not contained in '${delegated.with}'` - ), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }), derives: (claimed, [read, write]) => { if (!claimed.with.startsWith(read.with)) { - return new Failure( - `'${claimed.with}' is not contained in '${read.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${read.with}'`) } else if (!claimed.with.startsWith(write.with)) { - return new Failure( - `'${claimed.with}' is not contained in '${write.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${write.with}'`) } else { - return true + return ok({}) } }, }) @@ -535,7 +526,6 @@ test('capability amplification', () => { unknown: [], errors: [ { - error: true, name: 'InvalidClaim', context: { value: { @@ -746,16 +736,18 @@ test('capability or combinator', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.or(write) @@ -802,20 +794,20 @@ test('parse with nb', () => { }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Failure( + return fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Failure( + return fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return ok({}) } }, }) @@ -1029,16 +1021,18 @@ test('and prune', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.and(write) @@ -1096,16 +1090,18 @@ test('toString methods', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }) assert.equal(read.toString(), '{"can":"file/read"}') @@ -1119,7 +1115,7 @@ test('toString methods', () => { }, }) assert.equal( - match.toString(), + `${match.ok}`, `{"can":"file/read","with":"file:///home/alice"}` ) } @@ -1162,22 +1158,17 @@ test('toString methods', () => { can: 'file/read+write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.with.startsWith(delegated.with) || - new Failure( - `'${claimed.with}' is not contained in '${delegated.with}'` - ), + claimed.with.startsWith(delegated.with) + ? ok({}) + : fail(`'${claimed.with}' is not contained in '${delegated.with}'`), }), derives: (claimed, [read, write]) => { if (!claimed.with.startsWith(read.with)) { - return new Failure( - `'${claimed.with}' is not contained in '${read.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${read.with}'`) } else if (!claimed.with.startsWith(write.with)) { - return new Failure( - `'${claimed.with}' is not contained in '${write.with}'` - ) + return fail(`'${claimed.with}' is not contained in '${write.with}'`) } else { - return true + return ok({}) } }, }) @@ -1193,7 +1184,7 @@ test('toString methods', () => { }) assert.equal( - match.toString(), + `${match.ok}`, `{"can":"file/read+write","with":"file:///home/alice"}` ) } @@ -1579,12 +1570,12 @@ test('and chain', () => { }), derives: (abc, [a, b, c]) => { return abc.with !== a.with - ? new Failure(`${abc.with} != ${a.with}`) + ? fail(`${abc.with} != ${a.with}`) : abc.with !== b.with - ? new Failure(`${abc.with} != ${b.with}`) + ? fail(`${abc.with} != ${b.with}`) : abc.with !== c.with - ? new Failure(`${abc.with} != ${c.with}`) - : true + ? fail(`${abc.with} != ${c.with}`) + : ok({}) }, }) @@ -1599,7 +1590,7 @@ test('and chain', () => { assert.containSubset( ABC.match(source({ can: 'test/c', with: 'file:///test' })), { - error: true, + error: {}, } ) @@ -1648,7 +1639,7 @@ test('.and(...).match', () => { ) if (m.error) { - return assert.fail(m.message) + return assert.fail(m.error.message) } assert.containSubset(AB.select([]), { @@ -1662,7 +1653,7 @@ test('.and(...).match', () => { { can: 'test/ab', with: 'data:1', nb: { a: 'A', b: 'b' } }, ]) - assert.containSubset(m.select(src), { + assert.containSubset(m.ok.select(src), { unknown: [], errors: [ { @@ -1711,8 +1702,9 @@ test('A.or(B).match', () => { const ab = AB.match(source({ can: 'test/c', with: 'data:0' })) assert.containSubset(ab, { - error: true, - name: 'UnknownCapability', + error: { + name: 'UnknownCapability', + }, }) assert.containSubset( @@ -1724,8 +1716,9 @@ test('A.or(B).match', () => { }) ), { - error: true, - name: 'MalformedCapability', + error: { + name: 'MalformedCapability', + }, } ) }) @@ -1750,18 +1743,24 @@ test('and with diff nb', () => { const AB = A.and(B) assert.containSubset(AB.match(source({ can: 'test/me', with: 'data:1' })), { - error: true, + error: { + name: 'MalformedCapability', + }, }) assert.containSubset( AB.match(source({ can: 'test/me', with: 'data:1', nb: { a: 'a' } })), { - error: true, + error: { + name: 'MalformedCapability', + }, } ) assert.containSubset( AB.match(source({ can: 'test/me', with: 'data:1', nb: { b: 'b' } })), { - error: true, + error: { + name: 'MalformedCapability', + }, } ) @@ -1772,11 +1771,13 @@ test('and with diff nb', () => { source({ can: 'test/me', with: 'data:1', nb: { a: 'a', b: 'b' } }, proof) ), { - proofs: [proof], - matches: [ - { value: { can: 'test/me', with: 'data:1', nb: { a: 'a' } } }, - { value: { can: 'test/me', with: 'data:1', nb: { b: 'b' } } }, - ], + ok: { + proofs: [proof], + matches: [ + { value: { can: 'test/me', with: 'data:1', nb: { a: 'a' } } }, + { value: { can: 'test/me', with: 'data:1', nb: { b: 'b' } } }, + ], + }, } ) }) @@ -1792,8 +1793,7 @@ test('derived capability DSL', () => { can: 'derive/a', with: Schema.URI, }), - derives: (b, a) => - b.with === a.with ? true : new Failure(`with don't match`), + derives: (b, a) => (b.with === a.with ? ok({}) : fail(`with don't match`)), }) assert.deepEqual( @@ -1832,12 +1832,14 @@ test('capability match', () => { const m = a.match(source({ can: 'test/a', with: 'data:a' }, proof)) assert.containSubset(m, { - can: 'test/a', - proofs: [proof], + ok: { + can: 'test/a', + proofs: [proof], + }, }) assert.equal( - m.toString(), + `${m.ok}`, JSON.stringify({ can: 'test/a', with: 'data:a', @@ -1846,7 +1848,7 @@ test('capability match', () => { const m2 = a.match(source({ can: 'test/a', with: 'data:a', nb: {} }, proof)) assert.equal( - m2.toString(), + `${m2.ok}`, JSON.stringify({ can: 'test/a', with: 'data:a', @@ -1873,17 +1875,19 @@ test('capability match', () => { ) assert.containSubset(m3, { - can: 'test/echo', - value: { + ok: { can: 'test/echo', - with: alice.did(), - nb: { message: 'data:hello' }, + value: { + can: 'test/echo', + with: alice.did(), + nb: { message: 'data:hello' }, + }, + proofs: [proof], }, - proofs: [proof], }) assert.equal( - m3.toString(), + `${m3.ok}`, JSON.stringify({ can: 'test/echo', with: alice.did(), @@ -1903,8 +1907,7 @@ test('derived capability match & select', () => { can: 'derive/a', with: Schema.URI, }), - derives: (b, a) => - b.with === a.with ? true : new Failure(`with don't match`), + derives: (b, a) => (b.with === a.with ? ok({}) : fail(`with don't match`)), }) assert.equal(AA.can, 'derive/a') @@ -1917,18 +1920,20 @@ test('derived capability match & select', () => { const m = AA.match(src) assert.containSubset(m, { - can: 'derive/a', - proofs: [proof], - value: { can: 'derive/a', with: 'data:a' }, - source: [src], + ok: { + can: 'derive/a', + proofs: [proof], + value: { can: 'derive/a', with: 'data:a' }, + source: [src], + }, }) if (m.error) { - return assert.fail(m.message) + return assert.fail(m.error.message) } - assert.notEqual(m.prune({ canIssue: () => false }), null) - assert.equal(m.prune({ canIssue: () => true }), null) + assert.notEqual(m.ok.prune({ canIssue: () => false }), null) + assert.equal(m.ok.prune({ canIssue: () => true }), null) }) test('default derive', () => { @@ -1941,11 +1946,11 @@ test('default derive', () => { source({ can: 'test/a', with: 'file:///home/bob/photo' }) ) if (home.error) { - return assert.fail(home.message) + return assert.fail(home.error.message) } assert.containSubset( - home.select( + home.ok.select( delegate([ { can: 'test/a', @@ -1970,7 +1975,7 @@ test('default derive', () => { ) assert.containSubset( - home.select( + home.ok.select( delegate([ { can: 'test/a', @@ -1995,7 +2000,7 @@ test('default derive', () => { ) assert.containSubset( - home.select( + home.ok.select( delegate([ { can: 'test/a', @@ -2038,11 +2043,11 @@ test('default derive with nb', () => { ) if (pic.error) { - return assert.fail(pic.message) + return assert.fail(pic.error.message) } assert.containSubset( - pic.select( + pic.ok.select( delegate([ { can: 'profile/set', diff --git a/packages/validator/test/delegate.spec.js b/packages/validator/test/delegate.spec.js index 172559be..8ff926c8 100644 --- a/packages/validator/test/delegate.spec.js +++ b/packages/validator/test/delegate.spec.js @@ -1,4 +1,4 @@ -import { capability, DID, URI, Link, Schema } from '../src/lib.js' +import { capability, DID, URI, Link, Schema, ok, fail } from '../src/lib.js' import { parseLink, delegate, UCAN } from '@ucanto/core' import * as API from '@ucanto/interface' import { Failure } from '../src/error.js' @@ -247,8 +247,7 @@ const nbchild = parent.derive({ limit: Schema.integer(), }), }), - derives: (b, a) => - b.with === a.with ? true : new Failure(`with don't match`), + derives: (b, a) => (b.with === a.with ? ok({}) : fail(`with don't match`)), }) const child = parent.derive({ @@ -256,8 +255,7 @@ const child = parent.derive({ can: 'test/child', with: Schema.DID.match({ method: 'key' }), }), - derives: (b, a) => - b.with === a.with ? true : new Failure(`with don't match`), + derives: (b, a) => (b.with === a.with ? ok({}) : fail(`with don't match`)), }) test('delegate derived capability', async () => { diff --git a/packages/validator/test/error.spec.js b/packages/validator/test/error.spec.js index 1fcfd8c0..87f44ea7 100644 --- a/packages/validator/test/error.spec.js +++ b/packages/validator/test/error.spec.js @@ -16,7 +16,6 @@ test('Failure', () => { assert.deepInclude(json, { name: 'Error', message: 'boom!', - error: true, stack: error.stack, }) @@ -39,7 +38,6 @@ test('InvalidAudience', async () => { const error = new PrincipalAlignmentError(bob, delegation) assert.deepEqual(error.toJSON(), { - error: true, name: 'InvalidAudience', audience: bob.did(), delegation: { audience: w3.did() }, @@ -90,7 +88,6 @@ test('Expired', async () => { JSON.stringify(error, null, 2), JSON.stringify( { - error: true, name: 'Expired', message: `Proof ${delegation.cid} has expired on ${new Date( expiration * 1000 @@ -128,7 +125,6 @@ test('NotValidBefore', async () => { JSON.stringify(error, null, 2), JSON.stringify( { - error: true, name: 'NotValidBefore', message: `Proof ${delegation.cid} is not valid before ${new Date( time * 1000 diff --git a/packages/validator/test/extra-schema.spec.js b/packages/validator/test/extra-schema.spec.js index a71005fb..98b5d660 100644 --- a/packages/validator/test/extra-schema.spec.js +++ b/packages/validator/test/extra-schema.spec.js @@ -3,11 +3,11 @@ import { test, assert } from './test.js' import * as API from '@ucanto/interface' { - /** @type {[string, string|{message:string}][]} */ + /** @type {[string, {ok:string}|{error:{message:string}}][]} */ const dataset = [ - ['', { message: 'Invalid URI' }], - ['did:key:zAlice', 'did:key:zAlice'], - ['mailto:alice@mail.net', 'mailto:alice@mail.net'], + ['', { error: { message: 'Invalid URI' } }], + ['did:key:zAlice', { ok: 'did:key:zAlice' }], + ['mailto:alice@mail.net', { ok: 'mailto:alice@mail.net' }], ] for (const [input, expect] of dataset) { @@ -30,22 +30,30 @@ test('URI.from', () => { }) { - /** @type {[unknown, `${string}:`, {message:string}|string][]} */ + /** @type {[unknown, `${string}:`, {ok:string}|{error:{message:string}}][]} */ const dataset = [ - [undefined, 'did:', { message: 'Expected URI but got undefined' }], - [null, 'did:', { message: 'Expected URI but got null' }], - ['', 'did:', { message: 'Invalid URI' }], - ['did:key:zAlice', 'did:', 'did:key:zAlice'], + [ + undefined, + 'did:', + { error: { message: 'Expected URI but got undefined' } }, + ], + [null, 'did:', { error: { message: 'Expected URI but got null' } }], + ['', 'did:', { error: { message: 'Invalid URI' } }], + ['did:key:zAlice', 'did:', { ok: 'did:key:zAlice' }], [ 'did:key:zAlice', 'mailto:', - { message: 'Expected mailto: URI instead got did:key:zAlice' }, + { error: { message: 'Expected mailto: URI instead got did:key:zAlice' } }, ], - ['mailto:alice@mail.net', 'mailto:', 'mailto:alice@mail.net'], + ['mailto:alice@mail.net', 'mailto:', { ok: 'mailto:alice@mail.net' }], [ 'mailto:alice@mail.net', 'did:', - { message: 'Expected did: URI instead got mailto:alice@mail.net' }, + { + error: { + message: 'Expected did: URI instead got mailto:alice@mail.net', + }, + }, ], ] @@ -59,22 +67,26 @@ test('URI.from', () => { } { - /** @type {[unknown, `${string}:`, {message:string}|string|undefined][]} */ + /** @type {[unknown, `${string}:`, {ok:string|undefined}|{error:{message:string}}][]} */ const dataset = [ - [undefined, 'did:', undefined], - [null, 'did:', { message: 'Expected URI but got null' }], - ['', 'did:', { message: 'Invalid URI' }], - ['did:key:zAlice', 'did:', 'did:key:zAlice'], + [undefined, 'did:', { ok: undefined }], + [null, 'did:', { error: { message: 'Expected URI but got null' } }], + ['', 'did:', { error: { message: 'Invalid URI' } }], + ['did:key:zAlice', 'did:', { ok: 'did:key:zAlice' }], [ 'did:key:zAlice', 'mailto:', - { message: 'Expected mailto: URI instead got did:key:zAlice' }, + { error: { message: 'Expected mailto: URI instead got did:key:zAlice' } }, ], - ['mailto:alice@mail.net', 'mailto:', 'mailto:alice@mail.net'], + ['mailto:alice@mail.net', 'mailto:', { ok: 'mailto:alice@mail.net' }], [ 'mailto:alice@mail.net', 'did:', - { message: 'Expected did: URI instead got mailto:alice@mail.net' }, + { + error: { + message: 'Expected did: URI instead got mailto:alice@mail.net', + }, + }, ], ] @@ -144,32 +156,50 @@ test('URI.from', () => { for (const [input, out1, out2, out3, out4, out5] of dataset) { test(`Link.read(${input})`, () => { - assert.containSubset(Link.read(input), out1 || input) + assert.containSubset( + Link.read(input), + out1 ? { error: out1 } : { ok: input } + ) }) test('Link.link()', () => { const schema = Link.link() - assert.containSubset(schema.read(input), out1 || input) + assert.containSubset( + schema.read(input), + out1 ? { error: out1 } : { ok: input } + ) }) test(`Link.match({ code: 0x70 }).read(${input})`, () => { const link = Link.match({ code: 0x70 }) - assert.containSubset(link.read(input), out2 || input) + assert.containSubset( + link.read(input), + out2 ? { error: out2 } : { ok: input } + ) }) test(`Link.match({ algorithm: 0x12 }).read(${input})`, () => { const link = Link.match({ multihash: { code: 0x12 } }) - assert.containSubset(link.read(input), out3 || input) + assert.containSubset( + link.read(input), + out3 ? { error: out3 } : { ok: input } + ) }) test(`Link.match({ version: 1 }).read(${input})`, () => { const link = Link.match({ version: 1 }) - assert.containSubset(link.read(input), out4 || input) + assert.containSubset( + link.read(input), + out4 ? { error: out4 } : { ok: input } + ) }) test(`Link.optional().read(${input})`, () => { const link = Link.optional() - assert.containSubset(link.read(input), out5 || input) + assert.containSubset( + link.read(input), + out5 ? { error: out5 } : { ok: input } + ) }) } } @@ -179,13 +209,22 @@ test('URI.from', () => { const dataset = [ [ undefined, - { message: 'Expected value of type string instead got undefined' }, + { + error: { + message: 'Expected value of type string instead got undefined', + }, + }, + ], + [ + null, + { error: { message: 'Expected value of type string instead got null' } }, ], - [null, { message: 'Expected value of type string instead got null' }], - ['hello', 'hello'], + ['hello', { ok: 'hello' }], [ new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], ] @@ -203,24 +242,32 @@ test('URI.from', () => { { pattern: /hello .*/ }, undefined, { - message: 'Expected value of type string instead got undefined', + error: { + message: 'Expected value of type string instead got undefined', + }, }, ], [ { pattern: /hello .*/ }, null, - { message: 'Expected value of type string instead got null' }, + { error: { message: 'Expected value of type string instead got null' } }, ], [ { pattern: /hello .*/ }, 'hello', - { message: 'Expected to match /hello .*/ but got "hello" instead' }, + { + error: { + message: 'Expected to match /hello .*/ but got "hello" instead', + }, + }, ], - [{ pattern: /hello .*/ }, 'hello world', 'hello world'], + [{ pattern: /hello .*/ }, 'hello world', { ok: 'hello world' }], [ { pattern: /hello .*/ }, new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], ] @@ -234,34 +281,48 @@ test('URI.from', () => { { /** @type {[{pattern?:RegExp}, unknown, unknown][]} */ const dataset = [ - [{}, undefined, undefined], - [{}, null, { message: 'Expected value of type string instead got null' }], - [{}, 'hello', 'hello'], + [{}, undefined, { ok: undefined }], + [ + {}, + null, + { error: { message: 'Expected value of type string instead got null' } }, + ], + [{}, 'hello', { ok: 'hello' }], [ {}, new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], - [{ pattern: /hello .*/ }, undefined, undefined], + [{ pattern: /hello .*/ }, undefined, { ok: undefined }], [ { pattern: /hello .*/ }, null, { - message: 'Expected value of type string instead got null', + error: { + message: 'Expected value of type string instead got null', + }, }, ], [ { pattern: /hello .*/ }, 'hello', - { message: 'Expected to match /hello .*/ but got "hello" instead' }, + { + error: { + message: 'Expected to match /hello .*/ but got "hello" instead', + }, + }, ], - [{ pattern: /hello .*/ }, 'hello world', 'hello world'], + [{ pattern: /hello .*/ }, 'hello world', { ok: 'hello world' }], [ { pattern: /hello .*/ }, new String('hello'), { - message: 'Expected value of type string instead got object', + error: { + message: 'Expected value of type string instead got object', + }, }, ], ] @@ -281,17 +342,29 @@ test('URI.from', () => { const dataset = [ [ undefined, - { message: 'Expected value of type string instead got undefined' }, + { + error: { + message: 'Expected value of type string instead got undefined', + }, + }, + ], + [ + null, + { error: { message: 'Expected value of type string instead got null' } }, + ], + [ + 'hello', + { error: { message: 'Expected a did: but got "hello" instead' } }, ], - [null, { message: 'Expected value of type string instead got null' }], - ['hello', { message: 'Expected a did: but got "hello" instead' }], [ new String('hello'), { - message: 'Expected value of type string instead got object', + error: { + message: 'Expected value of type string instead got object', + }, }, ], - ['did:echo:1', 'did:echo:1'], + ['did:echo:1', { ok: 'did:echo:1' }], ] for (const [input, out] of dataset) { @@ -307,28 +380,38 @@ test('URI.from', () => { [ { method: 'echo' }, undefined, - { message: 'Expected value of type string instead got undefined' }, + { + error: { + message: 'Expected value of type string instead got undefined', + }, + }, ], [ { method: 'echo' }, null, - { message: 'Expected value of type string instead got null' }, + { error: { message: 'Expected value of type string instead got null' } }, ], [ { method: 'echo' }, 'hello', - { message: 'Expected a did:echo: but got "hello" instead' }, + { error: { message: 'Expected a did:echo: but got "hello" instead' } }, ], - [{ method: 'echo' }, 'did:echo:hello', 'did:echo:hello'], + [{ method: 'echo' }, 'did:echo:hello', { ok: 'did:echo:hello' }], [ { method: 'foo' }, 'did:echo:hello', - { message: 'Expected a did:foo: but got "did:echo:hello" instead' }, + { + error: { + message: 'Expected a did:foo: but got "did:echo:hello" instead', + }, + }, ], [ { method: 'echo' }, new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], ] @@ -342,35 +425,51 @@ test('URI.from', () => { { /** @type {[{method?:string}, unknown, unknown][]} */ const dataset = [ - [{}, undefined, undefined], - [{}, null, { message: 'Expected value of type string instead got null' }], - [{}, 'did:echo:bar', 'did:echo:bar'], + [{}, undefined, { ok: undefined }], + [ + {}, + null, + { error: { message: 'Expected value of type string instead got null' } }, + ], + [{}, 'did:echo:bar', { ok: 'did:echo:bar' }], [ {}, new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], - [{ method: 'echo' }, undefined, undefined], + [{ method: 'echo' }, undefined, { ok: undefined }], [ { method: 'echo' }, null, - { message: 'Expected value of type string instead got null' }, + { error: { message: 'Expected value of type string instead got null' } }, ], [ { method: 'echo' }, 'did:hello:world', - { message: 'Expected a did:echo: but got "did:hello:world" instead' }, + { + error: { + message: 'Expected a did:echo: but got "did:hello:world" instead', + }, + }, ], [ { method: 'echo' }, 'hello world', - { message: 'Expected a did:echo: but got "hello world" instead' }, + { + error: { + message: 'Expected a did:echo: but got "hello world" instead', + }, + }, ], [ { method: 'echo' }, new String('hello'), - { message: 'Expected value of type string instead got object' }, + { + error: { message: 'Expected value of type string instead got object' }, + }, ], ] diff --git a/packages/validator/test/inference.spec.js b/packages/validator/test/inference.spec.js index f7a1a714..793faca2 100644 --- a/packages/validator/test/inference.spec.js +++ b/packages/validator/test/inference.spec.js @@ -1,7 +1,16 @@ import * as Voucher from './voucher.js' import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' -import { capability, URI, Link, DID, Failure, Schema } from '../src/lib.js' +import { + capability, + URI, + Link, + DID, + Failure, + Schema, + ok, + fail, +} from '../src/lib.js' import * as API from './types.js' test('execute capability', () => @@ -114,7 +123,7 @@ test('infers nb fields optional', () => { /** @type {API.URI<"data:">|undefined} */ const _4 = proof.nb.msg - return true + return ok({}) }, }) }) @@ -146,7 +155,7 @@ test('infers nb fields in derived capability', () => { /** @type {API.URI<"data:">|undefined} */ const _4 = proof.nb.msg - return true + return ok({}) }, }) }) @@ -194,7 +203,7 @@ test('infers nb fields in derived capability', () => { /** @type {API.URI<"data:">|undefined} */ const _6 = b.nb.b - return true + return ok({}) }, }) @@ -240,7 +249,8 @@ test('can create derived capability with dict schema in nb', () => { * @param {{ with: string }} proof */ const equalWith = (claim, proof) => - claim.with === proof.with || new Failure(`claim.with is not proven`) + claim.with === proof.with ? ok({}) : fail(`claim.with is not proven`) + const top = capability({ can: '*', with: URI.match({ protocol: 'did:' }), diff --git a/packages/validator/test/lib.spec.js b/packages/validator/test/lib.spec.js index 6d12bb69..d55bc85b 100644 --- a/packages/validator/test/lib.spec.js +++ b/packages/validator/test/lib.spec.js @@ -1,7 +1,14 @@ import { test, assert } from './test.js' -import { access, claim, Schema } from '../src/lib.js' -import { capability, URI, Link } from '../src/lib.js' -import { Failure } from '../src/error.js' +import { + access, + claim, + Schema, + capability, + URI, + Link, + ok, + fail, +} from '../src/lib.js' import { Verifier } from '@ucanto/principal' import * as Client from '@ucanto/client' @@ -18,20 +25,20 @@ const storeAdd = capability({ }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Failure( + return fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Failure( + return fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return ok({}) } }, }) @@ -53,14 +60,16 @@ test('authorize self-issued invocation', async () => { }) assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, + ok: { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.from(alice.did()), + audience: Principal.parse(bob.did()), + proofs: [], }, - issuer: Principal.from(alice.did()), - audience: Principal.parse(bob.did()), - proofs: [], }) }) @@ -85,20 +94,22 @@ test('unauthorized / expired invocation', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Proof ${invocation.cid} has expired on ${new Date(expiration * 1000)}`, + }, }) assert.deepEqual( JSON.stringify(result), JSON.stringify({ - error: true, - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Proof ${invocation.cid} has expired on ${new Date(expiration * 1000)}`, - stack: result.error ? result.stack : undefined, + stack: result.error ? result.error.stack : undefined, + }, }) ) }) @@ -122,10 +133,11 @@ test('unauthorized / not valid before invocation', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Proof ${invocation.cid} is not valid before ${new Date(notBefore * 1000)}`, + }, }) }) @@ -148,10 +160,11 @@ test('unauthorized / invalid signature', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Proof ${invocation.cid} does not has a valid signature from ${alice.did()}`, + }, }) }) @@ -176,12 +189,13 @@ test('unauthorized / unknown capability', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', - error: true, - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - No matching delegated capability found - Encountered unknown capabilities - {"can":"store/write","with":"${alice.did()}"}`, + }, }) }) @@ -209,25 +223,27 @@ test('authorize / delegated invocation', async () => { }) assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(w3.did()), - proofs: [ - { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - proofs: [], + ok: { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, }, - ], + issuer: Principal.parse(bob.did()), + audience: Principal.parse(w3.did()), + proofs: [ + { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), + proofs: [], + }, + ], + }, }) }) @@ -260,36 +276,38 @@ test('authorize / delegation chain', async () => { }) assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - issuer: Principal.parse(mallory.did()), - audience: Principal.parse(w3.did()), - proofs: [ - { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(mallory.did()), - proofs: [ - { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - proofs: [], - }, - ], + ok: { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, }, - ], + issuer: Principal.parse(mallory.did()), + audience: Principal.parse(w3.did()), + proofs: [ + { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(mallory.did()), + proofs: [ + { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), + proofs: [], + }, + ], + }, + ], + }, }) }) @@ -309,13 +327,15 @@ test('invalid claim / no proofs', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${bob.did()}","nb":${JSON.stringify({ - link, - })}} is not authorized because: + link, + })}} is not authorized because: - Capability can not be (self) issued by '${alice.did()}' - Delegated capability not found`, + }, }) }) @@ -346,14 +366,16 @@ test('invalid claim / expired', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - { link } - )}} is not authorized because: + { link } + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${delegation.cid} because: - Proof ${delegation.cid} has expired on ${new Date(expiration * 1000)}`, + }, }) }) @@ -384,15 +406,17 @@ test('invalid claim / not valid before', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - { link } - )}} is not authorized because: + { link } + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${proof.cid} because: - Proof ${proof.cid} is not valid before ${new Date(notBefore * 1000)}`, + }, }) }) @@ -423,15 +447,17 @@ test('invalid claim / invalid signature', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - { link } - )}} is not authorized because: + { link } + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${proof.cid} because: - Proof ${proof.cid} does not has a valid signature from ${alice.did()}`, + }, }) }) @@ -463,15 +489,17 @@ test('invalid claim / unknown capability', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - { link } - )}} is not authorized because: + { link } + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Delegated capability not found - Encountered unknown capabilities - {"can":"store/pin","with":"${alice.did()}"}`, + }, }) }) @@ -509,17 +537,19 @@ test('invalid claim / malformed capability', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Can not derive {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} from delegated capabilities: + nb + )}} from delegated capabilities: - Encountered malformed 'store/add' capability: {"can":"store/add","with":"${badDID}"} - Expected did: URI instead got ${badDID}`, + }, }) }) @@ -546,17 +576,19 @@ test('invalid claim / unavailable proof', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${delegation.cid} because: - Linked proof '${ delegation.cid }' is not included and could not be resolved`, + }, }) }) @@ -586,18 +618,20 @@ test('invalid claim / failed to resolve', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${delegation.cid} because: - Linked proof '${ delegation.cid }' is not included and could not be resolved - Proof resolution failed with: Boom!`, + }, }) }) @@ -624,15 +658,17 @@ test('invalid claim / invalid audience', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${mallory.did()}' - Capability can not be derived from prf:${delegation.cid} because: - Delegation audience is '${bob.did()}' instead of '${mallory.did()}'`, + }, }) }) @@ -659,17 +695,19 @@ test('invalid claim / invalid claim', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Can not derive {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} from delegated capabilities: + nb + )}} from delegated capabilities: - Constraint violation: Expected 'with: "${mallory.did()}"' instead got '${alice.did()}'`, + }, }) }) @@ -707,8 +745,9 @@ test('invalid claim / invalid sub delegation', async () => { )}}` assert.containSubset(result, { - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability ${capability} is not authorized because: - Capability can not be (self) issued by '${mallory.did()}' - Capability ${capability} is not authorized because: @@ -716,6 +755,7 @@ test('invalid claim / invalid sub delegation', async () => { - Capability ${capability} is not authorized because: - Capability can not be (self) issued by '${alice.did()}' - Delegated capability not found`, + }, }) }) @@ -742,36 +782,38 @@ test('authorize / resolve external proof', async () => { principal: Verifier, resolve: async link => { if (link.toString() === delegation.cid.toString()) { - return delegation + return { ok: delegation } } else { - return new UnavailableProof(link) + return { error: new UnavailableProof(link) } } }, capability: storeAdd, }) if (result.error) { - assert.fail(result.stack) + assert.fail(result.error.stack) } assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - nb: {}, - }, - proofs: [ - { - delegation, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - capability: { - can: 'store/add', - with: alice.did(), - }, - proofs: [], + ok: { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, }, - ], + proofs: [ + { + delegation, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), + capability: { + can: 'store/add', + with: alice.did(), + }, + proofs: [], + }, + ], + }, }) }) @@ -798,15 +840,17 @@ test('invalid claim / principal alignment', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${alice.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${mallory.did()}' - Capability can not be derived from prf:${proof.cid} because: - Delegation audience is '${bob.did()}' instead of '${mallory.did()}'`, + }, }) }) @@ -835,15 +879,16 @@ test('invalid claim / invalid delegation chain', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + error: { + name: 'Unauthorized', + message: `Claim ${storeAdd} is not authorized - Capability {"can":"store/add","with":"${space.did()}","nb":${JSON.stringify( - nb - )}} is not authorized because: + nb + )}} is not authorized because: - Capability can not be (self) issued by '${bob.did()}' - Capability can not be derived from prf:${proof.cid} because: - Delegation audience is '${w3.did()}' instead of '${bob.did()}'`, + }, }) }) @@ -860,10 +905,12 @@ test('claim without a proof', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Linked proof '${delegation.cid}' is not included and could not be resolved`, + }, }) }) @@ -883,12 +930,14 @@ test('mismatched signature', async () => { }) assert.containSubset(result, { - name: 'Unauthorized', + error: { + name: 'Unauthorized', - message: `Claim ${storeAdd} is not authorized + message: `Claim ${storeAdd} is not authorized - Proof ${ delegation.cid } issued by ${current.did()} does not has a valid signature from ${current.toDIDKey()} ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, + }, }) }) diff --git a/packages/validator/test/link-schema.spec.js b/packages/validator/test/link-schema.spec.js index b9daa885..67a66c53 100644 --- a/packages/validator/test/link-schema.spec.js +++ b/packages/validator/test/link-schema.spec.js @@ -1,6 +1,6 @@ import * as Schema from '../src/schema.js' import { base36 } from 'multiformats/bases/base36' -import { test, assert } from './test.js' +import { test, assert, matchError } from './test.js' const fixtures = { pb: Schema.Link.parse('QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8'), @@ -26,19 +26,16 @@ const digests = new Set(links.map(link => link.multihash.digest)) for (const link of links) { test(`${link} ➡ Schema.link()`, () => { - assert.deepEqual(Schema.link().read(link), link, `${link}`) + assert.deepEqual(Schema.link().read(link), { ok: link }, `${link}`) }) for (const version of versions) { test(`${link} ➡ Schema.link({ version: ${version}})`, () => { const schema = Schema.link({ version }) if (link.version === version) { - assert.deepEqual(schema.read(link), link) + assert.deepEqual(schema.read(link), { ok: link }) } else { - assert.match( - schema.read(link).toString(), - /Expected link to be CID version/ - ) + matchError(schema.read(link), /Expected link to be CID version/) } }) } @@ -47,12 +44,9 @@ for (const link of links) { test(`${link} ➡ Schema.link({ code: ${code}})`, () => { const schema = Schema.link({ code }) if (link.code === code) { - assert.deepEqual(schema.read(link), link) + assert.deepEqual(schema.read(link), { ok: link }) } else { - assert.match( - schema.read(link).toString(), - /Expected link to be CID with .* codec/ - ) + matchError(schema.read(link), /Expected link to be CID with .* codec/) } }) } @@ -61,10 +55,10 @@ for (const link of links) { test(`${link} ➡ Schema.link({ multihash: {code: ${code}} })`, () => { const schema = Schema.link({ multihash: { code } }) if (link.multihash.code === code) { - assert.deepEqual(schema.read(link), link) + assert.deepEqual(schema.read(link), { ok: link }) } else { - assert.match( - schema.read(link).toString(), + matchError( + schema.read(link), /Expected link to be CID with .* hashing algorithm/ ) } @@ -77,12 +71,9 @@ for (const link of links) { multihash: { digest: new Uint8Array(digest) }, }) if (link.multihash.digest === digest) { - assert.deepEqual(schema.read(link), link) + assert.deepEqual(schema.read(link), { ok: link }) } else { - assert.match( - schema.read(link).toString(), - /Expected link with .* hash digest/ - ) + matchError(schema.read(link), /Expected link with .* hash digest/) } }) } diff --git a/packages/validator/test/schema.spec.js b/packages/validator/test/schema.spec.js index 927e9d64..d6520a71 100644 --- a/packages/validator/test/schema.spec.js +++ b/packages/validator/test/schema.spec.js @@ -1,4 +1,4 @@ -import { test, assert } from './test.js' +import { test, assert, matchError } from './test.js' import * as Schema from '../src/schema.js' import fixtures from './schema/fixtures.js' @@ -9,10 +9,10 @@ for (const { input, schema, expect, inputLabel, skip, only } of fixtures()) { const result = schema.read(input) if (expect.error) { - assert.match(String(result), expect.error) + matchError(result, expect.error) } else { assert.deepEqual( - result, + result.ok, // if expected value is set to undefined use input expect.value === undefined ? input : expect.value ) @@ -46,13 +46,13 @@ test('string startsWith & endsWith', () => { 'string().refine(startsWith("hello")).refine(startsWith("hi"))' ) - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -60,7 +60,7 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.equal(hello.read('hello world'), 'hello world') + assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) }) test('string startsWith', () => { @@ -71,9 +71,8 @@ test('string startsWith', () => { /** @type {Schema.StringSchema<`hello${string}`>} */ const hello = Schema.string().startsWith('hello') - assert.equal(hello.read('hello world!'), 'hello world!') - assert.deepInclude(hello.read('hi world'), { - error: true, + assert.deepEqual(hello.read('hello world!'), { ok: 'hello world!' }) + assert.deepInclude(hello.read('hi world').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world"`, }) @@ -87,10 +86,9 @@ test('string endsWith', () => { /** @type {Schema.StringSchema<`${string} world`>} */ const greet = Schema.string().endsWith(' world') - assert.equal(greet.read('hello world'), 'hello world') - assert.equal(greet.read('hi world'), 'hi world') - assert.deepInclude(greet.read('hello world!'), { - error: true, + assert.deepEqual(greet.read('hello world'), { ok: 'hello world' }) + assert.deepEqual(greet.read('hi world'), { ok: 'hi world' }) + assert.deepInclude(greet.read('hello world!').error, { name: 'SchemaError', message: `Expect string to end with " world" instead got "hello world!"`, }) @@ -115,25 +113,24 @@ test('string startsWith/endsWith', () => { `string().refine(endsWith("!")).refine(startsWith("hello"))` ) - assert.equal(hello1.read('hello world!'), 'hello world!') - assert.equal(hello2.read('hello world!'), 'hello world!') - assert.deepInclude(hello1.read('hello world'), { - error: true, + assert.deepEqual(hello1.read('hello world!'), { ok: 'hello world!' }) + assert.deepEqual(hello2.read('hello world!'), { ok: 'hello world!' }) + assert.deepInclude(hello1.read('hello world').error, { name: 'SchemaError', message: `Expect string to end with "!" instead got "hello world"`, }) - assert.deepInclude(hello2.read('hello world'), { - error: true, + + assert.deepInclude(hello2.read('hello world').error, { name: 'SchemaError', message: `Expect string to end with "!" instead got "hello world"`, }) - assert.deepInclude(hello1.read('hi world!'), { - error: true, + + assert.deepInclude(hello1.read('hi world!').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world!"`, }) - assert.deepInclude(hello2.read('hi world!'), { - error: true, + + assert.deepInclude(hello2.read('hi world!').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world!"`, }) @@ -144,13 +141,13 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hi${string}`>} */ const typeofImpossible = impossible - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -158,7 +155,7 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.equal(hello.read('hello world'), 'hello world') + assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) }) test('string().refine', () => { @@ -169,13 +166,13 @@ test('string().refine', () => { /** @type {Schema.StringSchema<`hello${string}` & `hi${string}`>} */ const typeofImpossible = impossible - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world'), { - error: true, + assert.deepInclude(impossible.read('hello world').error, { + name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -186,7 +183,7 @@ test('string().refine', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.equal(hello.read('hello world'), 'hello world') + assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) const greet = hello.refine({ /** @@ -195,7 +192,7 @@ test('string().refine', () => { */ read(hello) { if (hello.length === 11) { - return /** @type {In & {length: 11}} */ (hello) + return Schema.ok(/** @type {In & {length: 11}} */ (hello)) } else { return Schema.error(`Expected string with 11 chars`) } @@ -205,16 +202,16 @@ test('string().refine', () => { const typeofGreet = greet assert.equal( - greet.read('hello world'), + greet.read('hello world').ok, /** @type {unknown} */ ('hello world') ) assert.equal( - greet.read('hello Julia'), + greet.read('hello Julia').ok, /** @type {unknown} */ ('hello Julia') ) - assert.deepInclude(greet.read('hello Jack'), { - error: true, + assert.deepInclude(greet.read('hello Jack').error, { + name: 'SchemaError', message: 'Expected string with 11 chars', }) }) @@ -241,11 +238,12 @@ test('literal("foo").default("bar") throws', () => { test('default on literal has default', () => { const schema = Schema.literal('foo').default() - assert.equal(schema.read(undefined), 'foo') + + assert.deepEqual(schema.read(undefined), Schema.ok('foo')) }) test('literal has value field', () => { - assert.equal(Schema.literal('foo').value, 'foo') + assert.deepEqual(Schema.literal('foo').value, 'foo') }) test('.default().optional() is noop', () => { @@ -285,9 +283,9 @@ test('struct', () => { x: 1, y: 2, }) - assert.equal(p1.error, true) + assert.equal(!p1.ok, true) - assert.match(String(p1), /field "type".*expect.*"Point".*got undefined/is) + matchError(p1, /field "type".*expect.*"Point".*got undefined/is) const p2 = Point.read({ type: 'Point', @@ -295,9 +293,11 @@ test('struct', () => { y: 1, }) assert.deepEqual(p2, { - type: 'Point', - x: Schema.integer().from(1), - y: Schema.integer().from(1), + ok: { + type: 'Point', + x: Schema.integer().from(1), + y: Schema.integer().from(1), + }, }) const p3 = Point.read({ @@ -306,11 +306,11 @@ test('struct', () => { y: 1.1, }) - assert.equal(p3.error, true) - assert.match(String(p3), /field "y".*expect.*integer.*got 1.1/is) + assert.equal(!p3.ok, true) + matchError(p3, /field "y".*expect.*integer.*got 1.1/is) - assert.match( - String(Point.read(['h', 'e', 'l', null, 'l', 'o'])), + matchError( + Point.read(['h', 'e', 'l', null, 'l', 'o']), /Expected value of type object instead got array/ ) }) @@ -321,10 +321,10 @@ test('struct with defaults', () => { y: Schema.number().default(0), }) - assert.deepEqual(Point.read({}), { x: 0, y: 0 }) - assert.deepEqual(Point.read({ x: 2 }), { x: 2, y: 0 }) - assert.deepEqual(Point.read({ x: 2, y: 7 }), { x: 2, y: 7 }) - assert.deepEqual(Point.read({ y: 7 }), { x: 0, y: 7 }) + assert.deepEqual(Point.read({}), { ok: { x: 0, y: 0 } }) + assert.deepEqual(Point.read({ x: 2 }), { ok: { x: 2, y: 0 } }) + assert.deepEqual(Point.read({ x: 2, y: 7 }), { ok: { x: 2, y: 7 } }) + assert.deepEqual(Point.read({ y: 7 }), { ok: { x: 0, y: 7 } }) }) test('struct with literals', () => { @@ -334,11 +334,10 @@ test('struct with literals', () => { y: Schema.number(), }) - assert.deepEqual(Point.read({ x: 0, y: 0, z: 0 }), { x: 0, y: 0, z: 0 }) - assert.match( - String(Point.read({ x: 1, y: 1, z: 1 })), - /"z".*expect.* 0 .* got 1/is - ) + assert.deepEqual(Point.read({ x: 0, y: 0, z: 0 }), { + ok: { x: 0, y: 0, z: 0 }, + }) + matchError(Point.read({ x: 1, y: 1, z: 1 }), /"z".*expect.* 0 .* got 1/is) }) test('bad struct def', () => { @@ -357,18 +356,20 @@ test('struct with null literal', () => { const schema = Schema.struct({ a: null, b: true, c: Schema.string() }) assert.deepEqual(schema.read({ a: null, b: true, c: 'hi' }), { - a: null, - b: true, - c: 'hi', + ok: { + a: null, + b: true, + c: 'hi', + }, }) - assert.match( - String(schema.read({ a: null, b: false, c: '' })), + matchError( + schema.read({ a: null, b: false, c: '' }), /"b".*expect.* true .* got false/is ) - assert.match( - String(schema.read({ b: true, c: '' })), + matchError( + schema.read({ b: true, c: '' }), /"a".*expect.* null .* got undefined/is ) }) @@ -376,76 +377,73 @@ test('struct with null literal', () => { test('lessThan', () => { const schema = Schema.number().lessThan(100) - assert.deepEqual(schema.read(10), 10) - assert.match(String(schema.read(127)), /127 < 100/) - assert.match(String(schema.read(Infinity)), /Infinity < 100/) - assert.match(String(schema.read(NaN)), /NaN < 100/) + assert.deepEqual(schema.read(10), { ok: 10 }) + matchError(schema.read(127), /127 < 100/) + matchError(schema.read(Infinity), /Infinity < 100/) + matchError(schema.read(NaN), /NaN < 100/) }) test('greaterThan', () => { const schema = Schema.number().greaterThan(100) - assert.deepEqual(schema.read(127), 127) - assert.match(String(schema.read(12)), /12 > 100/) - assert.equal(schema.read(Infinity), Infinity) - assert.match(String(schema.read(NaN)), /NaN > 100/) + assert.deepEqual(schema.read(127), { ok: 127 }) + matchError(schema.read(12), /12 > 100/) + assert.deepEqual(schema.read(Infinity), { ok: Infinity }) + matchError(schema.read(NaN), /NaN > 100/) }) test('number().greaterThan().lessThan()', () => { const schema = Schema.number().greaterThan(3).lessThan(117) - assert.equal(schema.read(4), 4) - assert.equal(schema.read(116), 116) - assert.match(String(schema.read(117)), /117 < 117/) - assert.match(String(schema.read(3)), /3 > 3/) - assert.match(String(schema.read(127)), /127 < 117/) - assert.match(String(schema.read(0)), /0 > 3/) - assert.match(String(schema.read(Infinity)), /Infinity < 117/) - assert.match(String(schema.read(NaN)), /NaN > 3/) + assert.deepEqual(schema.read(4), { ok: 4 }) + assert.deepEqual(schema.read(116), { ok: 116 }) + matchError(schema.read(117), /117 < 117/) + matchError(schema.read(3), /3 > 3/) + matchError(schema.read(127), /127 < 117/) + matchError(schema.read(0), /0 > 3/) + matchError(schema.read(Infinity), /Infinity < 117/) + matchError(schema.read(NaN), /NaN > 3/) }) test('enum', () => { const schema = Schema.enum(['Red', 'Green', 'Blue']) assert.equal(schema.toString(), 'Red|Green|Blue') - assert.equal(schema.read('Red'), 'Red') - assert.equal(schema.read('Blue'), 'Blue') - assert.equal(schema.read('Green'), 'Green') + assert.deepEqual(schema.read('Red'), { ok: 'Red' }) + assert.deepEqual(schema.read('Blue'), { ok: 'Blue' }) + assert.deepEqual(schema.read('Green'), { ok: 'Green' }) - assert.match( - String(schema.read('red')), - /expect.* Red\|Green\|Blue .* got "red"/is - ) - assert.match(String(schema.read(5)), /expect.* Red\|Green\|Blue .* got 5/is) + matchError(schema.read('red'), /expect.* Red\|Green\|Blue .* got "red"/is) + matchError(schema.read(5), /expect.* Red\|Green\|Blue .* got 5/is) }) test('tuple', () => { const schema = Schema.tuple([Schema.string(), Schema.integer()]) - assert.match( - String(schema.read([, undefined])), + matchError( + schema.read([, undefined]), /invalid element at 0.*expect.*string.*got undefined/is ) - assert.match( - String(schema.read([0, 'hello'])), + matchError( + schema.read([0, 'hello']), /invalid element at 0.*expect.*string.*got 0/is ) - assert.match( - String(schema.read(['0', '1'])), + matchError( + schema.read(['0', '1']), /invalid element at 1.*expect.*number.*got "1"/is ) - assert.match( - String(schema.read(['0', Infinity])), + matchError( + schema.read(['0', Infinity]), /invalid element at 1.*expect.*integer.*got Infinity/is ) - assert.match( - String(schema.read(['0', NaN])), + matchError( + schema.read(['0', NaN]), /invalid element at 1.*expect.*integer.*got NaN/is ) - assert.match( - String(schema.read(['0', 0.2])), + matchError( + schema.read(['0', 0.2]), /invalid element at 1.*expect.*integer.*got 0.2/is ) - assert.deepEqual(schema.read(['x', 0]), ['x', 0]) + assert.deepEqual(schema.read(['x', 0]), { ok: ['x', 0] }) }) test('extend API', () => { @@ -463,7 +461,7 @@ test('extend API', () => { readWith(source, method) { const string = String(source) if (string.startsWith(`did:${method}:`)) { - return /** @type {`did:${M}:${string}`} */ (method) + return { ok: /** @type {`did:${M}:${string}`} */ (method) } } else { return Schema.error( `Expected did:${method} URI instead got ${string}` @@ -474,26 +472,26 @@ test('extend API', () => { const schema = new DIDString('key') assert.equal(schema.toString(), 'new DIDString()') - assert.match( - String( + matchError( + schema.read( // @ts-expect-error - schema.read(54) + 54 ), /Expected did:key URI/ ) - assert.match( - String(schema.read('did:echo:foo')), + matchError( + schema.read('did:echo:foo'), /Expected did:key URI instead got did:echo:foo/ ) const didKey = Schema.string().refine(new DIDString('key')) - assert.match(String(didKey.read(54)), /Expect.* string instead got 54/is) + matchError(didKey.read(54), /Expect.* string instead got 54/is) } }) test('errors', () => { - const error = Schema.error('boom!') + const { error } = Schema.error('boom!') const json = JSON.parse(JSON.stringify(error)) assert.deepInclude(json, { name: 'SchemaError', @@ -515,7 +513,7 @@ test('refine', () => { */ read(array) { return array.length > 0 - ? array + ? Schema.ok(array) : Schema.error('Array expected to have elements') } } @@ -523,9 +521,9 @@ test('refine', () => { const schema = Schema.array(Schema.string()).refine(new NonEmpty()) assert.equal(schema.toString(), 'array(string()).refine(new NonEmpty())') - assert.match(String(schema.read([])), /Array expected to have elements/) - assert.deepEqual(schema.read(['hello', 'world']), ['hello', 'world']) - assert.match(String(schema.read(null)), /expect.* array .*got null/is) + matchError(schema.read([]), /Array expected to have elements/) + assert.deepEqual(schema.read(['hello', 'world']), { ok: ['hello', 'world'] }) + matchError(schema.read(null), /expect.* array .*got null/is) }) test('brand', () => { @@ -533,14 +531,14 @@ test('brand', () => { .refine({ read(n) { return n >= 0 && n <= 9 - ? n + ? Schema.ok(n) : Schema.error(`Expected digit but got ${n}`) }, }) .brand('digit') - assert.match(String(digit.read(10)), /Expected digit but got 10/) - assert.match(String(digit.read(2.7)), /Expected value of type integer/) + matchError(digit.read(10), /Expected digit but got 10/) + matchError(digit.read(2.7), /Expected value of type integer/) assert.equal(digit.from(2), 2) /** @param {Schema.Infer} n */ @@ -571,16 +569,16 @@ test('optional.default removes undefined from type', () => { /** @type {Schema.Schema} */ const castOk = schema2 - assert.equal(schema1.read(undefined), undefined) - assert.equal(schema2.read(undefined), '') + assert.deepEqual(schema1.read(undefined), { ok: undefined }) + assert.deepEqual(schema2.read(undefined), { ok: '' }) }) test('.default("one").default("two")', () => { const schema = Schema.string().default('one').default('two') assert.equal(schema.value, 'two') - assert.deepEqual(schema.read(undefined), 'two') - assert.deepEqual(schema.read('three'), 'three') + assert.deepEqual(schema.read(undefined), { ok: 'two' }) + assert.deepEqual(schema.read('three'), { ok: 'three' }) }) test('default throws on invalid default', () => { @@ -597,7 +595,7 @@ test('default throws on invalid default', () => { test('unknown with default', () => { assert.throws( () => Schema.unknown().default(undefined), - /undefined is not a vaild default/ + /undefined is not a valid default/ ) }) @@ -605,11 +603,11 @@ test('default swaps undefined even if decodes to undefined', () => { /** @type {Schema.Schema} */ const schema = Schema.unknown().refine({ read(value) { - return value === null ? undefined : value + return { ok: value === null ? undefined : value } }, }) - assert.equal(schema.default('X').read(null), 'X') + assert.deepEqual(schema.default('X').read(null), { ok: 'X' }) }) test('record defaults', () => { @@ -622,10 +620,7 @@ test('record defaults', () => { z: Schema.integer(), }) - assert.match( - String(Point.read(undefined)), - /expect.* object .* got undefined/is - ) + matchError(Point.read(undefined), /expect.* object .* got undefined/is) assert.deepEqual(Point.create(), { x: 1, }) @@ -634,17 +629,23 @@ test('record defaults', () => { }) assert.deepEqual(Point.read({}), { - x: 1, + ok: { + x: 1, + }, }) assert.deepEqual(Point.read({ y: 2 }), { - x: 1, - y: 2, + ok: { + x: 1, + y: 2, + }, }) assert.deepEqual(Point.read({ x: 2, y: 2 }), { - x: 2, - y: 2, + ok: { + x: 2, + y: 2, + }, }) const Line = Schema.struct({ diff --git a/packages/validator/test/schema/fixtures.js b/packages/validator/test/schema/fixtures.js index 2c8aa303..1c3eecb2 100644 --- a/packages/validator/test/schema/fixtures.js +++ b/packages/validator/test/schema/fixtures.js @@ -634,13 +634,13 @@ export const scenarios = fixture => [ schema: Schema.unknown().nullable(), expect: fixture.unknown.any || fixture.any, }, - { - schema: Schema.unknown().default('DEFAULT'), - expect: - (fixture.unknown.default && fixture.unknown.default('DEFAULT')) || - fixture.unknown.any || - fixture.any, - }, + // { + // schema: Schema.unknown().default('DEFAULT'), + // expect: + // (fixture.unknown.default && fixture.unknown.default('DEFAULT')) || + // fixture.unknown.any || + // fixture.any, + // }, { schema: Schema.string(), expect: fixture.string.any || fixture.any, diff --git a/packages/validator/test/session.spec.js b/packages/validator/test/session.spec.js index b5b1148e..9606d68b 100644 --- a/packages/validator/test/session.spec.js +++ b/packages/validator/test/session.spec.js @@ -63,12 +63,14 @@ test('validate mailto', async () => { }) assert.containSubset(result, { - match: { - value: { - can: 'debug/echo', - with: account.did(), - nb: { - message: 'hello world', + ok: { + match: { + value: { + can: 'debug/echo', + with: account.did(), + nb: { + message: 'hello world', + }, }, }, }, @@ -146,12 +148,14 @@ test('delegated ucan/attest', async () => { }) assert.containSubset(result, { - match: { - value: { - can: 'debug/echo', - with: account.did(), - nb: { - message: 'hello world', + ok: { + match: { + value: { + can: 'debug/echo', + with: account.did(), + nb: { + message: 'hello world', + }, }, }, }, @@ -175,12 +179,13 @@ test('fail without proofs', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', + error: { + name: 'Unauthorized', + }, }) assert.match( - result.toString(), + `${result.error}`, /Unable to resolve 'did:mailto:web.mail:alice'/ ) }) @@ -211,12 +216,13 @@ test('fail without session', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', + error: { + name: 'Unauthorized', + }, }) assert.match( - result.toString(), + `${result.error}`, /Unable to resolve 'did:mailto:web.mail:alice'/ ) }) @@ -274,11 +280,12 @@ test('fail invalid ucan/attest proof', async () => { }) assert.containSubset(result, { - error: true, - name: 'Unauthorized', + error: { + name: 'Unauthorized', + }, }) - assert.match(result.toString(), /has an invalid session/) + assert.match(`${result.error}`, /has an invalid session/) }) test('resolve key', async () => { @@ -294,17 +301,19 @@ test('resolve key', async () => { const result = await access(await inv.delegate(), { authority: w3, capability: echo, - resolveDIDKey: _ => alice.did(), + resolveDIDKey: _ => Schema.ok(alice.did()), principal: Verifier, }) assert.containSubset(result, { - match: { - value: { - can: 'debug/echo', - with: account.did(), - nb: { - message: 'hello world', + ok: { + match: { + value: { + can: 'debug/echo', + with: account.did(), + nb: { + message: 'hello world', + }, }, }, }, @@ -352,7 +361,7 @@ test('service can not delegate access to account', async () => { principal: Verifier, }) - assert.equal(result.error, true) + assert.equal(!result.ok, true) }) test('attest with an account did', async () => { @@ -397,7 +406,7 @@ test('attest with an account did', async () => { principal: Verifier, }) - assert.equal(result.error, true) + assert.equal(!result.ok, true) }) test('service can not delegate account resource', async () => { @@ -427,5 +436,5 @@ test('service can not delegate account resource', async () => { principal: Verifier, }) - assert.equal(result.error, true) + assert.equal(!result.ok, true) }) diff --git a/packages/validator/test/test.js b/packages/validator/test/test.js index dbe6d8ec..b797d606 100644 --- a/packages/validator/test/test.js +++ b/packages/validator/test/test.js @@ -1,6 +1,16 @@ +import * as API from './types.js' import { assert, use } from 'chai' import subset from 'chai-subset' use(subset) export const test = it export { assert } + +/** + * @template {API.Result} Result + * @param {Result} result + * @param {RegExp} pattern + * @param {string} [message] + */ +export const matchError = (result, pattern, message) => + assert.match(`${result.error}`, pattern, message) diff --git a/packages/validator/test/util.js b/packages/validator/test/util.js index 95480e7f..93bc6627 100644 --- a/packages/validator/test/util.js +++ b/packages/validator/test/util.js @@ -1,31 +1,35 @@ import { Failure } from '../src/error.js' +import { ok, fail } from '@ucanto/core' import * as API from '@ucanto/interface' /** - * @param {API.Failure|true} value + * @template T + * @param {API.Result} result + * @returns {{error: API.Failure, ok?:undefined}|undefined} */ -export const fail = value => (value === true ? undefined : value) +export const and = result => (result.error ? result : undefined) /** * Check URI can be delegated * * @param {string|undefined} child * @param {string|undefined} parent + * @returns {API.Result<{}, API.Failure>} */ export function canDelegateURI(child, parent) { if (parent === undefined) { - return true + return ok({}) } if (child !== undefined && parent.endsWith('*')) { return child.startsWith(parent.slice(0, -1)) - ? true - : new Failure(`${child} does not match ${parent}`) + ? ok({}) + : fail(`${child} does not match ${parent}`) } return child === parent - ? true - : new Failure(`${child} is different from ${parent}`) + ? ok({}) + : fail(`${child} is different from ${parent}`) } /** @@ -35,12 +39,12 @@ export function canDelegateURI(child, parent) { export const canDelegateLink = (child, parent) => { // if parent poses no restriction it's can be derived if (parent === undefined) { - return true + return ok({}) } return String(child) === parent.toString() - ? true - : new Failure(`${child} is different from ${parent}`) + ? ok({}) + : fail(`${child} is different from ${parent}`) } /** @@ -51,10 +55,7 @@ export const canDelegateLink = (child, parent) => { * @param {{can: API.Ability, with: string}} parent */ export function equalWith(child, parent) { - return ( - child.with === parent.with || - new Failure( - `Can not derive ${child.can} with ${child.with} from ${parent.with}` - ) - ) + return child.with === parent.with + ? ok({}) + : fail(`Can not derive ${child.can} with ${child.with} from ${parent.with}`) } diff --git a/packages/validator/test/voucher.js b/packages/validator/test/voucher.js index 8f1af9e2..66f7f627 100644 --- a/packages/validator/test/voucher.js +++ b/packages/validator/test/voucher.js @@ -1,5 +1,5 @@ -import { equalWith, canDelegateURI, canDelegateLink, fail } from './util.js' -import { capability, URI, Text, Link, DID, Schema } from '../src/lib.js' +import { equalWith, canDelegateURI, canDelegateLink, and } from './util.js' +import { capability, URI, Text, Link, DID, Schema, ok } from '../src/lib.js' export const Voucher = capability({ can: 'voucher/*', @@ -17,11 +17,11 @@ export const Claim = Voucher.derive({ }), derives: (child, parent) => { return ( - fail(equalWith(child, parent)) || - fail(canDelegateURI(child.nb.identity, parent.nb.identity)) || - fail(canDelegateLink(child.nb.product, parent.nb.product)) || - fail(canDelegateURI(child.nb.service, parent.nb.service)) || - true + and(equalWith(child, parent)) || + and(canDelegateURI(child.nb.identity, parent.nb.identity)) || + and(canDelegateLink(child.nb.product, parent.nb.product)) || + and(canDelegateURI(child.nb.service, parent.nb.service)) || + ok({}) ) }, }), From 5067d0d208bfcdbe997dcc1142b5703951f72a3e Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Sat, 1 Apr 2023 01:19:52 -0700 Subject: [PATCH 2/4] update remaining packages --- packages/client/test/api.ts | 15 +++------- packages/client/test/client.spec.js | 6 ++-- packages/client/test/service.js | 24 ++++++++------- packages/client/test/services/account.js | 12 +++----- packages/client/test/services/storage.js | 14 +++++---- packages/client/test/services/tokens.js | 6 ++-- packages/interface/src/lib.ts | 6 +++- packages/server/package.json | 2 +- packages/server/src/handler.js | 37 +++++++++-------------- packages/server/src/server.js | 13 +++----- packages/server/test/handler.spec.js | 36 ++++++++++++++-------- packages/server/test/server.spec.js | 16 +++++----- packages/server/test/service/access.js | 38 +++++++++++++----------- packages/server/test/service/store.js | 14 +++++---- packages/transport/src/codec.js | 2 +- 15 files changed, 119 insertions(+), 122 deletions(-) diff --git a/packages/client/test/api.ts b/packages/client/test/api.ts index 4658fca7..ec8944cc 100644 --- a/packages/client/test/api.ts +++ b/packages/client/test/api.ts @@ -3,10 +3,7 @@ import * as API from '@ucanto/interface' import type { DID, Link, Await, Result as SyncResult } from '@ucanto/interface' export type { DID, Link, SyncResult } -type Result< - T extends unknown = unknown, - X extends { error: true } = Error & { error: true } -> = Await> +type Result = Await> export interface StorageProvider { /** @@ -34,7 +31,7 @@ export interface StorageProvider { group: DID, link: Link, proof: Link - ): Result + ): Result<{}, UnknownDIDError | DoesNotHasError> } export interface TokenStore { @@ -42,12 +39,12 @@ export interface TokenStore { * Revokes set of UCANS. CID corresponds to the link been revoked and * proof is the CID of the revocation. */ - revoke(token: Link, revocation: TokenEntry): Result + revoke(token: Link, revocation: TokenEntry): Result<{}, RevokeError> /** * Adds bunch of proofs for later queries. */ - insert(tokens: IterableIterator): Result + insert(tokens: IterableIterator): Result<{}, InsertError> /** * You give it named set of CIDs and it gives you back named set of @@ -185,13 +182,9 @@ export interface DoesNotHasError extends RangeError { export interface UnknownDIDError extends RangeError { readonly name: 'UnknownDIDError' did: DID | null - - error: true } export interface InvalidInvocation extends Error { readonly name: 'InvalidInvocation' link: Link - - error: true } diff --git a/packages/client/test/client.spec.js b/packages/client/test/client.spec.js index 6b7ea709..95af5dc0 100644 --- a/packages/client/test/client.spec.js +++ b/packages/client/test/client.spec.js @@ -152,7 +152,7 @@ const channel = HTTP.open({ return Receipt.issue({ ran: invocation.cid, issuer: w3, - result: result?.error ? { error: result } : { ok: result }, + result, }) } case 'store/remove': { @@ -162,7 +162,7 @@ const channel = HTTP.open({ return Receipt.issue({ ran: invocation.cid, issuer: w3, - result: result?.error ? { error: result } : { ok: result }, + result, }) } } @@ -219,7 +219,6 @@ test('execute', async () => { assert.deepEqual(e1.out, { error: { - error: true, name: 'UnknownDIDError', message: `DID ${alice.did()} has no account`, did: alice.did(), @@ -264,7 +263,6 @@ test('execute with delegations', async () => { assert.deepEqual(e1.out, { error: { - error: true, name: 'UnknownDIDError', message: `DID ${bob.did()} has no account`, did: bob.did(), diff --git a/packages/client/test/service.js b/packages/client/test/service.js index a242b54b..61f638b0 100644 --- a/packages/client/test/service.js +++ b/packages/client/test/service.js @@ -62,18 +62,22 @@ class StorageService { /** @type {any} */ (ucan).cid ) if (!result.error) { - if (result.status === 'in-s3') { + if (result.ok.status === 'in-s3') { return { - with: capability.with, - link: capability.nb.link, - status: the('done'), + ok: { + with: capability.with, + link: capability.nb.link, + status: the('done'), + }, } } else { return { - with: capability.with, - link: capability.nb.link, - status: the('upload'), - url: 'http://localhost:9090/', + ok: { + with: capability.with, + link: capability.nb.link, + status: the('upload'), + url: 'http://localhost:9090/', + }, } } } else { @@ -93,10 +97,10 @@ class StorageService { capability.nb.link, /** @type {any} */ (ucan).link ) - if (remove?.error) { + if (remove.error) { return remove } else { - return capability + return { ok: capability } } } } diff --git a/packages/client/test/services/account.js b/packages/client/test/services/account.js index 2ed978e4..3203e155 100644 --- a/packages/client/test/services/account.js +++ b/packages/client/test/services/account.js @@ -69,9 +69,9 @@ const unlink = (model, member, group, proof) => { if (account === resolve(model, member)) { model.delete(member) } - return {} + return { ok: {} } } else { - return new UnknownDIDError('Unknown DID', group) + return { error: new UnknownDIDError('Unknown DID', group) } } } @@ -98,7 +98,7 @@ const associate = (accounts, from, to, proof, create) => { accounts.set(to, { account, proof }) accounts.set(from, { account, proof }) } else { - return new UnknownDIDError('Unknown did', to) + return { error: new UnknownDIDError('Unknown did', to) } } } else if (toAccount) { accounts.set(from, { account: toAccount, proof }) @@ -110,7 +110,7 @@ const associate = (accounts, from, to, proof, create) => { accounts.set(fromAccount, { account, proof }) } - return {} + return { ok: {} } } /** @@ -147,9 +147,6 @@ export class UnknownDIDError extends RangeError { super(message) this.did = did } - get error() { - return /** @type {true} */ (true) - } get name() { return the('UnknownDIDError') } @@ -159,7 +156,6 @@ export class UnknownDIDError extends RangeError { name: this.name, message: this.message, did: this.did, - error: true, } } } diff --git a/packages/client/test/services/storage.js b/packages/client/test/services/storage.js index 6f4e4b06..383f1a3b 100644 --- a/packages/client/test/services/storage.js +++ b/packages/client/test/services/storage.js @@ -6,7 +6,7 @@ import { the } from './util.js' * @param {Partial & { accounts: API.AccessProvider }} config * @returns {API.StorageProvider} */ -export const create = (config) => new StoreProvider(config) +export const create = config => new StoreProvider(config) /** * @typedef {{ @@ -43,9 +43,11 @@ export const add = async ({ accounts, groups, cars }, group, link, proof) => { const links = groups.get(group) || new Map() links.set(`${link}`, link) groups.set(group, links) - return { status: cars.get(`${link}`) ? the('in-s3') : the('not-in-s3') } + return { + ok: { status: cars.get(`${link}`) ? the('in-s3') : the('not-in-s3') }, + } } else { - return new UnknownDIDError(`DID ${group} has no account`, group) + return { error: new UnknownDIDError(`DID ${group} has no account`, group) } } } @@ -60,10 +62,10 @@ export const remove = async ({ accounts, groups }, group, link, proof) => { if (account) { const links = groups.get(group) return links && links.get(`${link}`) - ? null - : new DoesNotHasError(group, link) + ? { ok: {} } + : { error: new DoesNotHasError(group, link) } } else { - return new UnknownDIDError(`DID ${group} has no account`, group) + return { error: new UnknownDIDError(`DID ${group} has no account`, group) } } } diff --git a/packages/client/test/services/tokens.js b/packages/client/test/services/tokens.js index eb2db06e..8a781988 100644 --- a/packages/client/test/services/tokens.js +++ b/packages/client/test/services/tokens.js @@ -5,7 +5,7 @@ import { the, unreachable } from './util.js' * @param {Map} store * @returns {API.TokenStore} */ -export const create = (store) => new TokenService(store) +export const create = store => new TokenService(store) /** * @implements {API.TokenStore} @@ -41,7 +41,7 @@ class TokenService { return unreachable`Record has unexpected state ${record}` } } - return null + return { ok: {} } } /** * @template {Record} Query @@ -80,7 +80,7 @@ class TokenService { return unreachable`record has unknown state ${record}` } - return null + return { ok: {} } } async gc() { diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 278c3320..66208f1d 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -563,7 +563,11 @@ export type InferInvocations = T extends [] * @typeParam O - type returned by the handler on success * @typeParam X - type returned by the handler on error */ -export interface ServiceMethod { +export interface ServiceMethod< + I extends Capability, + O extends {}, + X extends {} +> { (input: Invocation, context: InvocationContext): Await< Result > diff --git a/packages/server/package.json b/packages/server/package.json index 861fa530..b2c365dc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -22,7 +22,7 @@ "scripts": { "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", - "test": "npm run test:node", + "test": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha --bail test/**/*.spec.js", "coverage": "c8 --reporter=html mocha test/**/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", "check": "tsc --build", "build": "tsc --build" diff --git a/packages/server/src/handler.js b/packages/server/src/handler.js index 2a428fcd..ac85c067 100644 --- a/packages/server/src/handler.js +++ b/packages/server/src/handler.js @@ -10,10 +10,11 @@ import { access, Schema, Failure } from '@ucanto/validator' * @template {API.Ability} A * @template {API.URI} R * @template {API.Caveats} C - * @template {{}} U + * @template {{}} O + * @template {{}} X * @param {API.CapabilityParser>>} capability - * @param {(input:API.ProviderInput>) => API.Await} handler - * @returns {API.ServiceMethod, Exclude, Exclude>>} + * @param {(input:API.ProviderInput>) => API.Await>} handler + * @returns {API.ServiceMethod, O, X>} */ export const provide = (capability, handler) => @@ -30,12 +31,13 @@ export const provide = (capability, handler) => * @template {API.Ability} A * @template {API.URI} R * @template {API.Caveats} C - * @template {{}} U + * @template {{}} O + * @template {{}} X * @param {object} input * @param {API.Reader} [input.audience] * @param {API.CapabilityParser>>} input.capability - * @param {(input:API.ProviderInput>) => API.Await} input.handler - * @returns {API.ServiceMethod, Exclude, Exclude>>} + * @param {(input:API.ProviderInput>) => API.Await>} input.handler + * @returns {API.ServiceMethod, O, X>} */ export const provideAdvanced = @@ -51,7 +53,7 @@ export const provideAdvanced = const audienceSchema = audience || Schema.literal(options.id.did()) const result = audienceSchema.read(invocation.audience.did()) if (result.error) { - return new InvalidAudience({ cause: result }) + return { error: new InvalidAudience({ cause: result.error }) } } const authorization = await access(invocation, { @@ -62,13 +64,11 @@ export const provideAdvanced = if (authorization.error) { return authorization } else { - return /** @type {API.Result, {error:true} & Exclude>|API.InvocationError>} */ ( - handler({ - capability: authorization.capability, - invocation, - context: options, - }) - ) + return handler({ + capability: authorization.ok.capability, + invocation, + context: options, + }) } } @@ -89,13 +89,4 @@ class InvalidAudience extends Failure { describe() { return this.cause.message } - toJSON() { - const { error, name, message, stack } = this - return { - error, - name, - message, - stack, - } - } } diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 6a98b134..a91a68b1 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -7,7 +7,8 @@ export { Failure, MalformedCapability, } from '@ucanto/validator' -import { Receipt } from '@ucanto/core' +import { Receipt, ok, fail } from '@ucanto/core' +export { ok, fail } /** * Creates a connection to a service. @@ -126,17 +127,11 @@ export const invoke = async (invocation, server) => { }) } else { try { - const value = await handler[method](invocation, server.context) + const result = await handler[method](invocation, server.context) return await Receipt.issue({ issuer: server.id, ran: invocation, - // handler returns result in a different format from the receipt - // so we convert it here. We also need to handle the case where - // the handler `null` or `undefined` is returned which in receipt - // form at is unit type `{}`. - result: /** @type {API.ReceiptResult<{}>} */ ( - value?.error ? { error: value } : { ok: value == null ? {} : value } - ), + result, }) } catch (cause) { const error = new HandlerExecutionError( diff --git a/packages/server/test/handler.spec.js b/packages/server/test/handler.spec.js index 0089228a..d91e02ee 100644 --- a/packages/server/test/handler.spec.js +++ b/packages/server/test/handler.spec.js @@ -26,7 +26,9 @@ const context = { /** * @param {API.UCANLink} link */ - resolve: link => new UnavailableProof(link), + resolve: link => ({ + error: new UnavailableProof(link), + }), } test('invocation', async () => { @@ -43,12 +45,13 @@ test('invocation', async () => { const result = await Access.link(invocation, context) assert.containSubset(result, { - error: true, - name: 'Unauthorized', - message: `Claim {"can":"identity/link"} is not authorized + error: { + name: 'Unauthorized', + message: `Claim {"can":"identity/link"} is not authorized - Capability {"can":"identity/link","with":"mailto:alice@web.mail"} is not authorized because: - Capability can not be (self) issued by '${alice.did()}' - Delegated capability not found`, + }, }) }) @@ -72,10 +75,11 @@ test('delegated invocation fail', async () => { }) const result = await Access.link(invocation, context) - assert.deepNestedInclude(result, { - error: true, - name: 'UnknownIDError', - id: 'mailto:alice@web.mail', + assert.containSubset(result, { + error: { + name: 'UnknownIDError', + id: 'mailto:alice@web.mail', + }, }) }) @@ -99,7 +103,7 @@ test('delegated invocation fail', async () => { }) const result = await Access.register(invocation, context) - assert.deepEqual(result, null) + assert.deepEqual(result, { ok: {} }) }) test('checks service id', async () => { @@ -236,7 +240,7 @@ test('test access/claim provider', async () => { /** * @type {Client.ConnectionView<{ * access: { - * claim: API.ServiceMethod, never[], never> + * claim: API.ServiceMethod, never[], {}> * } * }>} */ @@ -273,7 +277,9 @@ test('handle did:mailto audiences', async () => { capability: AccessRequest, handler: async input => { return { - allow: input.capability.nb.need, + ok: { + allow: input.capability.nb.need, + }, } }, }) @@ -322,9 +328,13 @@ test('handle did:mailto audiences', async () => { principal: Verifier, }) - assert.equal(badAudience.error, true) + assert.containSubset(badAudience, { + error: { + name: 'InvalidAudience', + }, + }) assert.match( - badAudience.toString(), + `${badAudience.error}`, /InvalidAudience.*Expected .*did:mailto:.*got.*did:web:/ ) }) diff --git a/packages/server/test/server.spec.js b/packages/server/test/server.spec.js index 51b5bf4b..eff6ecab 100644 --- a/packages/server/test/server.spec.js +++ b/packages/server/test/server.spec.js @@ -16,20 +16,20 @@ const storeAdd = Server.capability({ }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Server.Failure( + return Server.fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Server.Failure( + return Server.fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return Server.ok({}) } }, }) @@ -41,20 +41,20 @@ const storeRemove = Server.capability({ }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Server.Failure( + return Server.fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Server.Failure( + return Server.fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return Server.ok({}) } }, }) @@ -122,7 +122,6 @@ test('encode delegated invocation', async () => { assert.deepEqual(r1.out, { error: { - error: true, name: 'UnknownDIDError', did: alice.did(), message: `DID ${alice.did()} has no account`, @@ -131,7 +130,6 @@ test('encode delegated invocation', async () => { assert.deepEqual(r2.out, { error: { - error: true, name: 'UnknownDIDError', did: alice.did(), message: `DID ${alice.did()} has no account`, @@ -391,7 +389,7 @@ test('falsy errors are turned into {}', async () => { testNull, async () => { - return null + return { ok: {} } } ), }, diff --git a/packages/server/test/service/access.js b/packages/server/test/service/access.js index c68e9c8a..3d058e25 100644 --- a/packages/server/test/service/access.js +++ b/packages/server/test/service/access.js @@ -1,6 +1,6 @@ import * as Server from '../../src/server.js' import { provide } from '../../src/handler.js' -import { DID } from '@ucanto/validator' +import { DID, Schema } from '@ucanto/validator' import * as API from './api.js' import { service as w3 } from '../fixtures.js' export const id = w3 @@ -9,29 +9,31 @@ const registerCapability = Server.capability({ can: 'identity/register', with: Server.URI.match({ protocol: 'mailto:' }), derives: (claimed, delegated) => - claimed.with === delegated.with || - new Server.Failure( - `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` - ), + claimed.with === delegated.with + ? Server.ok({}) + : Server.fail( + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` + ), }) const linkCapability = Server.capability({ can: 'identity/link', with: Server.URI, derives: (claimed, delegated) => - claimed.with === delegated.with || - new Server.Failure( - `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` - ), + claimed.with === delegated.with + ? Server.ok({}) + : Server.fail( + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` + ), }) const identifyCapability = Server.capability({ can: 'identity/identify', with: Server.URI, derives: (claimed, delegated) => - claimed.with === delegated.with || - delegated.with === 'ucan:*' || - new Server.Failure(`Can not derive ${claimed.with} from ${claimed.with}`), + claimed.with === delegated.with || delegated.with === 'ucan:*' + ? Server.ok({}) + : Server.fail(`Can not derive ${claimed.with} from ${claimed.with}`), }) export const claimCapability = Server.capability({ @@ -76,14 +78,16 @@ export const identify = provide( async function identify({ capability }) { const did = /** @type {API.DID} */ (capability.with) const account = resolve(state, did) - return account == null ? new UnknownIDError(did) : account + return account == null + ? { error: new UnknownIDError(did) } + : { ok: account } } ) export const claim = provide( claimCapability, async function claim({ capability }) { - return [] + return { ok: [] } } ) @@ -93,7 +97,7 @@ export const claim = provide( * @param {API.DID|API.URI<"mailto:">} to * @param {API.Link} proof * @param {boolean} create - * @returns {API.SyncResult} + * @returns {API.SyncResult<{}, API.UnknownIDError>} */ const associate = (accounts, from, to, proof, create) => { const fromAccount = resolve(accounts, from) @@ -110,7 +114,7 @@ const associate = (accounts, from, to, proof, create) => { accounts.set(to, { account, proof }) accounts.set(from, { account, proof }) } else { - return new UnknownIDError('Unknown did', to) + return { error: new UnknownIDError('Unknown did', to) } } } else if (toAccount) { accounts.set(from, { account: toAccount, proof }) @@ -122,7 +126,7 @@ const associate = (accounts, from, to, proof, create) => { accounts.set(fromAccount, { account, proof }) } - return null + return { ok: {} } } /** diff --git a/packages/server/test/service/store.js b/packages/server/test/service/store.js index e67f0f15..01e5ebd0 100644 --- a/packages/server/test/service/store.js +++ b/packages/server/test/service/store.js @@ -14,20 +14,20 @@ const addCapability = Server.capability({ }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Server.Failure( + return Server.fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Server.Failure( + return Server.fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return { ok: {} } } }, }) @@ -40,20 +40,20 @@ const removeCapability = Server.capability({ }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { - return new Server.Failure( + return Server.fail( `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( delegated.nb.link && `${delegated.nb.link}` !== `${claimed.nb.link}` ) { - return new Server.Failure( + return Server.fail( `Link ${ claimed.nb.link == null ? '' : `${claimed.nb.link} ` }violates imposed ${delegated.nb.link} constraint` ) } else { - return true + return Server.ok({}) } }, }) @@ -84,4 +84,6 @@ export const add = provide(addCapability, async ({ capability, context }) => { const links = state.get(groupID) || new Map() links.set(`${link}`, link) state.set(groupID, links) + + return { ok: {} } }) diff --git a/packages/transport/src/codec.js b/packages/transport/src/codec.js index a1c55d97..1a593d9e 100644 --- a/packages/transport/src/codec.js +++ b/packages/transport/src/codec.js @@ -16,7 +16,7 @@ export const inbound = source => new Inbound(source) class Inbound { /** * @param {API.HTTPRequest} request - * @returns {API.ReceiptResult} transport + * @returns {API.Result} transport */ accept({ headers }) { const contentType = headers['content-type'] || headers['Content-Type'] From 59d2db7a3789b7b2c3c894e0bcaa94fb0dea1aa3 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Sat, 1 Apr 2023 01:31:05 -0700 Subject: [PATCH 3/4] enable all tests --- packages/validator/test/schema/fixtures.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/validator/test/schema/fixtures.js b/packages/validator/test/schema/fixtures.js index 1c3eecb2..2c8aa303 100644 --- a/packages/validator/test/schema/fixtures.js +++ b/packages/validator/test/schema/fixtures.js @@ -634,13 +634,13 @@ export const scenarios = fixture => [ schema: Schema.unknown().nullable(), expect: fixture.unknown.any || fixture.any, }, - // { - // schema: Schema.unknown().default('DEFAULT'), - // expect: - // (fixture.unknown.default && fixture.unknown.default('DEFAULT')) || - // fixture.unknown.any || - // fixture.any, - // }, + { + schema: Schema.unknown().default('DEFAULT'), + expect: + (fixture.unknown.default && fixture.unknown.default('DEFAULT')) || + fixture.unknown.any || + fixture.any, + }, { schema: Schema.string(), expect: fixture.string.any || fixture.any, From d5d0169f2a143599f3a76cc5ba6b99315c21e1de Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 3 Apr 2023 18:16:24 -0700 Subject: [PATCH 4/4] Apply suggestions from code review --- packages/core/src/result.js | 12 ++++++++++++ packages/interface/src/capability.ts | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/core/src/result.js b/packages/core/src/result.js index f35d8f3e..2990ab45 100644 --- a/packages/core/src/result.js +++ b/packages/core/src/result.js @@ -1,6 +1,9 @@ import * as API from '@ucanto/interface' /** + * Creates the success result containing given `value`. Throws if + * `null` or `undefined` passed to encourage use of units instead. + * * @template {{}|string|boolean|number} T * @param {T} value * @returns {{ok: T, value?:undefined}} @@ -14,6 +17,10 @@ export const ok = value => { } /** + * Creates the failing result containing given `cause` of error. + * Throws if `cause` is `null` or `undefined` to encourage + * passing descriptive errors instead. + * * @template {{}|string|boolean|number} X * @param {X} cause * @returns {{ok?:undefined, error:X}} @@ -29,6 +36,11 @@ export const error = cause => { } /** + * Creates the failing result containing an error with a given + * `message`. Unlike `error` function it creates a very generic + * error with `message` & `stack` fields. The `error` function + * is recommended over `fail` for all but the most basic use cases. + * * @param {string} message * @returns {{error:API.Failure, ok?:undefined}} */ diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index 4ac442b8..0a1955f9 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -54,6 +54,14 @@ export interface MatchSelector export interface DirectMatch extends Match> {} +/** + * Generic reader interface that can be used to read `O` value form the + * input `I` value. Reader may fail and error is denoted by `X` type. + * + * @template O - The output type of this reader + * @template I - The input type of this reader. + * @template X - The error type denotes failure reader may produce. + */ export interface Reader { read: (input: I) => Result }