From 0ce8cbb90c915a344c41f606c2ac257358b31c6a Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 1 May 2023 22:51:19 -0700 Subject: [PATCH 01/16] feat: integrate link modifier --- packages/core/src/schema.js | 2 - packages/core/src/schema/link.js | 98 -------------- packages/core/src/schema/schema.js | 163 ++++++++++++++++++++++-- packages/core/src/schema/type.ts | 64 +++++++++- packages/core/test/extra-schema.spec.js | 17 +-- packages/core/test/link-schema.spec.js | 55 ++++++++ packages/interface/src/lib.ts | 12 +- 7 files changed, 289 insertions(+), 122 deletions(-) delete mode 100644 packages/core/src/schema/link.js diff --git a/packages/core/src/schema.js b/packages/core/src/schema.js index 0bfecbfe..1daf4762 100644 --- a/packages/core/src/schema.js +++ b/packages/core/src/schema.js @@ -1,9 +1,7 @@ export * as URI from './schema/uri.js' -export * as Link from './schema/link.js' export * as DID from './schema/did.js' export * as Text from './schema/text.js' export * from './schema/schema.js' -export { match as link } from './schema/link.js' export { match as did } from './schema/did.js' export { match as uri } from './schema/uri.js' export { match as text } from './schema/text.js' diff --git a/packages/core/src/schema/link.js b/packages/core/src/schema/link.js deleted file mode 100644 index 851c42ec..00000000 --- a/packages/core/src/schema/link.js +++ /dev/null @@ -1,98 +0,0 @@ -import * as API from '@ucanto/interface' -import { create, createLegacy, isLink, parse, base32 } from '../link.js' -import * as Schema from './schema.js' - -export { create, createLegacy, isLink, parse } - -/** - * @template {number} [Code=number] - * @template {number} [Alg=number] - * @template {1|0} [Version=0|1] - * @typedef {{ - * code?:Code, - * version?:Version - * multihash?: {code?: Alg, digest?: Uint8Array} - * }} Settings - */ - -/** - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version - * @extends {Schema.API, unknown, Settings>} - */ -class LinkSchema extends Schema.API { - /** - * - * @param {unknown} cid - * @param {Settings} settings - * @returns {Schema.ReadResult>} - */ - readWith(cid, { code, multihash = {}, version }) { - if (cid == null) { - return Schema.error(`Expected link but got ${cid} instead`) - } else { - if (!isLink(cid)) { - return Schema.error(`Expected link to be a CID instead of ${cid}`) - } else { - if (code != null && cid.code !== code) { - return Schema.error( - `Expected link to be CID with 0x${code.toString(16)} codec` - ) - } - - if (multihash.code != null && cid.multihash.code !== multihash.code) - return Schema.error( - `Expected link to be CID with 0x${multihash.code.toString( - 16 - )} hashing algorithm` - ) - - if (version != null && cid.version !== version) { - return Schema.error( - `Expected link to be CID version ${version} instead of ${cid.version}` - ) - } - - const [expectDigest, actualDigest] = - multihash.digest != null - ? [ - base32.baseEncode(multihash.digest), - base32.baseEncode(cid.multihash.digest), - ] - : ['', ''] - - if (expectDigest !== actualDigest) { - return Schema.error( - `Expected link with "${expectDigest}" hash digest instead of "${actualDigest}"` - ) - } - - return { - ok: /** @type {API.Link} */ (cid), - } - } - } - } -} - -/** @type {Schema.Schema, unknown>} */ -export const schema = new LinkSchema({}) - -export const link = () => schema - -/** - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version - * @param {Settings} options - * @returns {Schema.Schema>} - */ -export const match = (options = {}) => new LinkSchema(options) - -/** - * @param {unknown} input - */ -export const read = input => schema.read(input) - -export const optional = () => schema.optional() diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 3ea979dd..d12bb74b 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -1,6 +1,16 @@ import * as Schema from './type.js' import { ok, Failure } from '../result.js' +import { + create as createLink, + parse as parseLink, + createLegacy, + isLink, + parse, + base32, +} from '../link.js' export * from './type.js' +import * as CBOR from '../cbor.js' +import { sha256 } from 'multiformats/hashes/sha2' export { ok } /** @@ -18,6 +28,9 @@ export class API { constructor(settings) { /** @protected */ this.settings = settings + + this.codec = CBOR + this.hasher = sha256 } toString() { @@ -141,6 +154,26 @@ export class API { schema ) } + + /** + * @template {number} [Code=number] + * @template {number} [Alg=number] + * @template {1|0} [Version=0|1] + * @param {{ + * codec?: Schema.BlockCodec + * hasher?: Schema.MultihashHasher + * version?: Version + * }} options + * @returns {Schema.LinkSchema} + */ + link({ codec, hasher, version } = {}) { + return link({ + ...(codec ? { code: codec.code } : {}), + ...(hasher ? { multihash: { code: hasher.code } } : {}), + ...(version ? { version } : {}), + schema: this, + }) + } } /** @@ -760,7 +793,7 @@ class LessThan extends API { /** * @template {number} T * @param {number} n - * @returns {Schema.Schema} + * @returns {Schema.Reader} */ export const lessThan = n => new LessThan(n) @@ -789,7 +822,7 @@ class GreaterThan extends API { /** * @template {number} T * @param {number} n - * @returns {Schema.Schema} + * @returns {Schema.Reader} */ export const greaterThan = n => new GreaterThan(n) @@ -1063,15 +1096,15 @@ export const literal = value => new Literal(value) /** * @template {{[key:string]: Schema.Reader}} U * @template [I=unknown] - * @extends {API, I, U>} + * @extends {API, I, {shape: U}>} */ class Struct extends API { /** * @param {I} input - * @param {U} shape + * @param {{shape: U}} settings * @returns {Schema.ReadResult>} */ - readWith(input, shape) { + readWith(input, { shape }) { if (typeof input != 'object' || input === null || Array.isArray(input)) { return typeError({ expect: 'object', @@ -1105,17 +1138,17 @@ class Struct extends API { * @returns {Schema.MapRepresentation>> & Schema.StructSchema} */ partial() { - return new Struct( - Object.fromEntries( - Object.entries(this.shape).map(([key, value]) => [key, optional(value)]) - ) + const shape = Object.fromEntries( + Object.entries(this.shape).map(([key, value]) => [key, optional(value)]) ) + + return new Struct({ shape }) } /** @type {U} */ get shape() { // @ts-ignore - We declared `settings` private but we access it here - return this.settings + return this.settings.shape } toString() { @@ -1141,7 +1174,7 @@ class Struct extends API { * @returns {Schema.StructSchema} */ extend(extension) { - return new Struct({ ...this.shape, ...extension }) + return new Struct({ shape: { ...this.shape, ...extension } }) } } @@ -1176,9 +1209,115 @@ export const struct = fields => { } } - return new Struct(/** @type {V} */ (shape)) + return new Struct({ shape: /** @type {V} */ (shape) }) } +/** + * @template {unknown} [T=unknown] + * @template {number} [Code=number] + * @template {number} [Alg=number] + * @template {1|0} [Version=0|1] + * @typedef {{ + * code?:Code, + * version?:Version + * multihash?: {code?: Alg, digest?: Uint8Array} + * schema?: Schema.Schema + * }} LinkSettings + */ + +/** + * @template {unknown} T + * @template {number} Code + * @template {number} Alg + * @template {1|0} Version + * @extends {API, unknown, LinkSettings>} + * @implements {Schema.LinkSchema} + */ +class LinkSchema extends API { + /** + * + * @param {unknown} cid + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} + */ + readWith(cid, { code, multihash = {}, version }) { + if (cid == null) { + return error(`Expected link but got ${cid} instead`) + } else { + if (!isLink(cid)) { + return error(`Expected link to be a CID instead of ${cid}`) + } else { + if (code != null && cid.code !== code) { + return error( + `Expected link to be CID with 0x${code.toString(16)} codec` + ) + } + + if (multihash.code != null && cid.multihash.code !== multihash.code) + return error( + `Expected link to be CID with 0x${multihash.code.toString( + 16 + )} hashing algorithm` + ) + + if (version != null && cid.version !== version) { + return error( + `Expected link to be CID version ${version} instead of ${cid.version}` + ) + } + + const [expectDigest, actualDigest] = + multihash.digest != null + ? [ + base32.baseEncode(multihash.digest), + base32.baseEncode(cid.multihash.digest), + ] + : ['', ''] + + if (expectDigest !== actualDigest) { + return error( + `Expected link with "${expectDigest}" hash digest instead of "${actualDigest}"` + ) + } + + return { + ok: /** @type {Schema.Link} */ (cid), + } + } + } + } + + /** + * @returns {never} + */ + link() { + throw new Error('Can not create link of link') + } + + /** + * @template {string} Prefix + * @param {string} input + * @param {Schema.MultibaseDecoder} [base] + */ + parse(input, base) { + const link = parseLink(input, base) + return this.from(link) + } +} + +/** @type {Schema.LinkSchema} */ +export const Link = new LinkSchema({}) + +/** + * @template {number} Code + * @template {number} Alg + * @template {1|0} Version + * @template {unknown} T + * @param {LinkSettings} options + * @returns {Schema.LinkSchema} + */ +export const link = (options = {}) => new LinkSchema(options) + /** * @template {Schema.VariantChoices} U * @template [I=unknown] diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 2e51e6e0..9d6840ab 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -1,4 +1,40 @@ -import { Failure as Error, Result, Variant, Phantom } from '@ucanto/interface' +import { + Failure as Error, + Await, + Result, + Variant, + Phantom, + Link, + BlockStore, + Block, + BlockCodec, + BlockDecoder, + BlockEncoder, + MultihashHasher, + MulticodecCode, + MultibaseDecoder, + UnknownLink, + IPLDView, + IPLDViewBuilder, + BuildOptions, + ByteView, +} from '@ucanto/interface' + +export type { + Link, + BlockStore, + Block, + BlockCodec, + BlockEncoder, + BlockDecoder, + MultihashHasher, + MulticodecCode, + MultibaseDecoder, + ByteView, + IPLDView, + IPLDViewBuilder, + BuildOptions, +} export interface Reader { read(input: I): Result @@ -24,6 +60,32 @@ export interface Schema< is(value: unknown): value is O from(value: I): O + link< + Code extends MulticodecCode, + Alg extends MulticodecCode, + V extends UnknownLink['version'] + >(options?: { + codec?: BlockCodec + version?: V + hasher?: MultihashHasher + }): LinkSchema + + codec: BlockCodec + hasher: MultihashHasher + +} + +export interface LinkSchema< + O extends unknown, + Code extends MulticodecCode, + Alg extends MulticodecCode, + V extends UnknownLink['version'] +> extends Schema> { + link(): never + parse( + source: string, + base?: MultibaseDecoder + ): Link } export interface DefaultSchema< diff --git a/packages/core/test/extra-schema.spec.js b/packages/core/test/extra-schema.spec.js index be4447b5..98c4e8be 100644 --- a/packages/core/test/extra-schema.spec.js +++ b/packages/core/test/extra-schema.spec.js @@ -1,4 +1,5 @@ import { URI, Link, Text, DID } from '../src/schema.js' +import * as Schema from '../src/schema.js' import { test, assert, matchResult } from './test.js' import * as API from '@ucanto/interface' @@ -139,23 +140,23 @@ test('URI.from', () => { matchResult(Link.read(input), out1 || { ok: input }) }) - test('Link.link()', () => { - const schema = Link.link() + test('Schema.link()', () => { + const schema = Schema.link() matchResult(schema.read(input), out1 || { ok: input }) }) - test(`Link.match({ code: 0x70 }).read(${input})`, () => { - const link = Link.match({ code: 0x70 }) + test(`Schema.link({ code: 0x70 }).read(${input})`, () => { + const link = Schema.link({ code: 0x70 }) matchResult(link.read(input), out2 || { ok: input }) }) - test(`Link.match({ algorithm: 0x12 }).read(${input})`, () => { - const link = Link.match({ multihash: { code: 0x12 } }) + test(`Schema.link({ algorithm: 0x12 }).read(${input})`, () => { + const link = Schema.link({ multihash: { code: 0x12 } }) matchResult(link.read(input), out3 || { ok: input }) }) - test(`Link.match({ version: 1 }).read(${input})`, () => { - const link = Link.match({ version: 1 }) + test(`Schema.link({ version: 1 }).read(${input})`, () => { + const link = Schema.link({ version: 1 }) matchResult(link.read(input), out4 || { ok: input }) }) diff --git a/packages/core/test/link-schema.spec.js b/packages/core/test/link-schema.spec.js index 67a66c53..70d3c692 100644 --- a/packages/core/test/link-schema.spec.js +++ b/packages/core/test/link-schema.spec.js @@ -1,6 +1,7 @@ import * as Schema from '../src/schema.js' import { base36 } from 'multiformats/bases/base36' import { test, assert, matchError } from './test.js' +import { CBOR, sha256 } from '../src/dag.js' const fixtures = { pb: Schema.Link.parse('QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8'), @@ -78,3 +79,57 @@ for (const link of links) { }) } } + +test('struct().link()', () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + const PointLink = Point.link() + + assert.equal(PointLink.read(fixtures.pb).ok, fixtures.pb) + + assert.throws(() => PointLink.link(), /link of link/) +}) + +test('struct().link({ codec })', () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + const PointLink = Point.link({ + codec: CBOR, + }) + + assert.match(PointLink.read(fixtures.pb).error?.message || '', /0x71 code/) + assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) +}) + +test('struct().link({ hasher })', () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + const PointLink = Point.link({ + hasher: sha256, + }) + + assert.match( + PointLink.read(fixtures.sha512).error?.message || '', + /0x12 hashing/ + ) + assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) +}) + +test('struct().link({ hasher })', () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + const PointLink = Point.link({ + version: 1, + }) + + assert.match(PointLink.read(fixtures.pb).error?.message || '', /version 1/) + assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) +}) diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 9d9a3950..d90017b1 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -31,6 +31,9 @@ import { Block as IPLDBlock, ToString, BlockEncoder, + BlockDecoder, + BlockCodec, + BaseDecoder, } from 'multiformats' import * as UCAN from '@ipld/dag-ucan' import { @@ -73,6 +76,10 @@ export type { MultibaseDecoder, MultibaseEncoder, MulticodecCode, + BaseDecoder, + BlockDecoder, + BlockEncoder, + BlockCodec, Principal, ToJSON, ToString, @@ -82,7 +89,10 @@ export type { } export * as UCAN from '@ipld/dag-ucan' -export type BlockStore = Map, Block> +export type BlockStore = Map< + ToString, + Block +> export type AttachedLinkSet = Set> /** From a67964dbc3423aae97f86daf9ebb6bfdb1676239 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 1 May 2023 22:56:45 -0700 Subject: [PATCH 02/16] update tests --- packages/server/test/server.spec.js | 4 ++-- packages/validator/test/capability.spec.js | 4 ++-- packages/validator/test/delegate.spec.js | 2 +- packages/validator/test/inference.spec.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/server/test/server.spec.js b/packages/server/test/server.spec.js index 8451d356..28fb3c48 100644 --- a/packages/server/test/server.spec.js +++ b/packages/server/test/server.spec.js @@ -12,7 +12,7 @@ const storeAdd = Server.capability({ can: 'store/add', with: Server.URI.match({ protocol: 'did:' }), nb: Schema.struct({ - link: Server.Link.match().optional(), + link: Server.Link.optional(), }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -38,7 +38,7 @@ const storeRemove = Server.capability({ can: 'store/remove', with: Server.URI.match({ protocol: 'did:' }), nb: Schema.struct({ - link: Server.Link.match().optional(), + link: Server.Link.optional(), }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { diff --git a/packages/validator/test/capability.spec.js b/packages/validator/test/capability.spec.js index 9e86f6fa..b40c030c 100644 --- a/packages/validator/test/capability.spec.js +++ b/packages/validator/test/capability.spec.js @@ -790,7 +790,7 @@ test('parse with nb', () => { can: 'store/add', with: URI.match({ protocol: 'did:' }), nb: Schema.struct({ - link: Link.match().optional(), + link: Link.optional(), }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -1499,7 +1499,7 @@ test('capability with optional caveats', async () => { with: URI.match({ protocol: 'did:' }), nb: Schema.struct({ message: URI.match({ protocol: 'data:' }), - meta: Link.match().optional(), + meta: Link.optional(), }), }) diff --git a/packages/validator/test/delegate.spec.js b/packages/validator/test/delegate.spec.js index 8ff926c8..4c9615e6 100644 --- a/packages/validator/test/delegate.spec.js +++ b/packages/validator/test/delegate.spec.js @@ -187,7 +187,7 @@ test('capability with optional caveats', async () => { with: URI.match({ protocol: 'did:' }), nb: Schema.struct({ message: URI.match({ protocol: 'data:' }), - meta: Link.match().optional(), + meta: Link.optional(), }), }) diff --git a/packages/validator/test/inference.spec.js b/packages/validator/test/inference.spec.js index 793faca2..b6905a6e 100644 --- a/packages/validator/test/inference.spec.js +++ b/packages/validator/test/inference.spec.js @@ -262,7 +262,7 @@ test('can create derived capability with dict schema in nb', () => { with: URI, nb: Schema.struct({ delegations: Schema.dictionary({ - value: Schema.Link.match(), + value: Schema.Link, }), }), derives: (claim, proof) => { From 2e8a5c45986b0a8b390f6df9b37b1b8985ea9341 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 1 May 2023 22:58:56 -0700 Subject: [PATCH 03/16] fix link ref --- packages/server/test/service/store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/test/service/store.js b/packages/server/test/service/store.js index 01e5ebd0..15cf0b54 100644 --- a/packages/server/test/service/store.js +++ b/packages/server/test/service/store.js @@ -10,7 +10,7 @@ const addCapability = Server.capability({ can: 'store/add', with: Server.URI.match({ protocol: 'did:' }), nb: Schema.struct({ - link: Server.Link.match().optional(), + link: Server.Link.optional(), }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -36,7 +36,7 @@ const removeCapability = Server.capability({ can: 'store/remove', with: Server.URI.match({ protocol: 'did:' }), nb: Schema.struct({ - link: Server.Link.match().optional(), + link: Server.Link.optional(), }), derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { From efce7d5fdf17fbc49dd74cf837cf0390849364f4 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Tue, 2 May 2023 14:47:19 -0700 Subject: [PATCH 04/16] feat: add ipld integration to schemas --- packages/core/src/receipt.js | 3 + packages/core/src/schema/schema.js | 197 ++++++++++++++++++++++++++ packages/core/src/schema/type.ts | 26 ++++ packages/core/test/map-schema.spec.js | 134 ++++++++++++++++++ packages/core/test/receipt.spec.js | 2 + packages/interface/src/lib.ts | 2 + 6 files changed, 364 insertions(+) diff --git a/packages/core/src/receipt.js b/packages/core/src/receipt.js index e9cd4dbf..90fafefa 100644 --- a/packages/core/src/receipt.js +++ b/packages/core/src/receipt.js @@ -61,6 +61,9 @@ class Receipt { this._issuer = issuer } + link() { + return this.root.cid + } /** * @returns {Ran|ReturnType} */ diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index d12bb74b..1fc7df07 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -174,6 +174,60 @@ export class API { schema: this, }) } + + /** + * @param {I} input + * @returns {Schema.Result>, Schema.Error>} + */ + toIPLDBuilder(input) { + const result = this.read(input) + if (result.error) { + return result + } else { + const data = result.ok + const builder = new IPLDViewBuilder({ + data, + schema: this, + }) + return { ok: builder } + } + } + + /** + * @param {object} source + * @param {Schema.Link} source.link + * @param {Schema.BlockStore} source.store + * @returns {ReturnType} + */ + toIPLDView({ link, store }) { + const block = store.get(`${link}`) + if (!block) { + return /** @type {*} */ ({ error: new Error(`Missing block ${link}`) }) + } else { + return /** @type {*} */ (this.createIPLDView({ root: block, store })) + } + } + + /** + * @param {object} source + * @param {Schema.Block} source.root + * @param {Schema.BlockStore} source.store + * @returns {Schema.Result, Schema.Error>} + */ + createIPLDView({ root, store }) { + const input = /** @type {I} */ (this.codec.decode(root.bytes)) + const result = this.read(input) + if (result.error) { + return result + } else { + const view = new IPLDView({ + root: { ...root, data: result.ok }, + store, + schema: this, + }) + return { ok: view } + } + } } /** @@ -223,6 +277,88 @@ class Unknown extends API { */ export const unknown = () => new Unknown() +/** + * @template {unknown} T + * @implements {Schema.IPLDViewBuilder>} + */ +class IPLDViewBuilder { + /** + * @param {object} input + * @param {T} input.data + * @param {Schema.Schema} input.schema + */ + constructor({ data, schema }) { + this.data = data + this.schema = schema + } + + /** + * @param {Schema.BuildOptions} [options] + * @returns {Promise>} + */ + async buildIPLDView({ + encoder = this.schema.codec, + hasher = this.schema.hasher, + } = {}) { + const { data } = this + const bytes = encoder.encode(data) + const digest = await hasher.digest(bytes) + /** @type {Schema.Link} */ + const cid = createLink(encoder.code, digest) + return new IPLDView({ root: { bytes, data, cid }, schema: this.schema }) + } +} + +/** + * @template {unknown} T + * @implements {Schema.IPLDView} + * @implements {Schema.IPLDViewBuilder>} + */ +class IPLDView { + /** + * @param {object} input + * @param {Required} input.root + * @param {Schema.Schema} input.schema + * @param {Schema.BlockStore} [input.store] + */ + constructor({ root, store = new Map(), schema }) { + this.root = root + this.store = store + this.schema = schema + } + + /** + * @template T + * @param {object} input + * @param {Required} input.root + * @param {Schema.Schema} input.schema + * @param {Schema.BlockStore} [input.store] + */ + static create(input) { + return new this(input) + } + + /** + * @returns {Schema.Link} + */ + link() { + return this.root.cid + } + /** + * @returns {IterableIterator} + */ + *iterateIPLDBlocks() { + yield this.root + } + + /** + * @returns {Schema.IPLDView} + */ + buildIPLDView() { + return this + } +} + /** * @template O * @template [I=unknown] @@ -1176,6 +1312,67 @@ class Struct extends API { extend(extension) { return new Struct({ shape: { ...this.shape, ...extension } }) } + + /** + * @param {object} source + * @param {Schema.Block} source.root + * @param {Schema.BlockStore} source.store + * @returns {Schema.Result & Schema.IPLDView>, Schema.Error>} + */ + createIPLDView(source) { + const data = this.codec.decode(source.root.bytes) + + if (typeof data != 'object' || data === null || Array.isArray(data)) { + return typeError({ + expect: 'object', + actual: data, + }) + } + + let View = this._View || (this._View = IPLDStructView.struct(this.shape)) + const root = { ...source.root, data } + + const view = View.create({ root, store: source.store, schema: this }) + return { ok: view } + } +} + +/** + * @template {{[key:string]: Schema.Reader}} U + * @extends {IPLDView>} + */ +class IPLDStructView extends IPLDView { + /** + * @template {{[key:string]: Schema.Reader}} U + * @param {U} shape + * @returns {Schema.CreateView, Schema.InferStruct>} + */ + static struct(shape) { + /** @extends {IPLDStructView} */ + class View extends this { + static shape = shape + } + + for (const [key, schema] of Object.entries(shape)) { + Object.defineProperty(View.prototype, key, { + get() { + let result = this[`_${key}`] + if (!result) { + result = schema.read(this.root.data[key]) + this[`_${key}`] = result + } + + if (result.ok) { + return result.ok + } else { + throw memberError({ at: key, cause: result.error }).error + } + }, + }) + } + + return /** @type {*} */ (View) + } } /** diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 9d6840ab..bfb2004c 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -60,6 +60,7 @@ export interface Schema< is(value: unknown): value is O from(value: I): O + link< Code extends MulticodecCode, Alg extends MulticodecCode, @@ -73,6 +74,14 @@ export interface Schema< codec: BlockCodec hasher: MultihashHasher + toIPLDBuilder(input: I): Result>, Error> + + toIPLDView(source: { + link: Link + store: BlockStore + }): ReturnType + + createIPLDView(source: IPLDViewSource): Result, Error> } export interface LinkSchema< @@ -88,6 +97,19 @@ export interface LinkSchema< ): Link } +export interface IPLDViewSource { + root: Block + store: BlockStore +} + +export interface CreateView { + create(source: { + root: Required + store: BlockStore + schema: Schema + }): IPLDView & V +} + export interface DefaultSchema< O extends unknown = unknown, I extends unknown = unknown @@ -163,6 +185,10 @@ export interface StructSchema< ): StructSchema partial(): MapRepresentation>, I> & StructSchema + + createIPLDView( + source: IPLDViewSource + ): Result> & InferStruct, Error> } /** diff --git a/packages/core/test/map-schema.spec.js b/packages/core/test/map-schema.spec.js index 00aa8b80..fde926c5 100644 --- a/packages/core/test/map-schema.spec.js +++ b/packages/core/test/map-schema.spec.js @@ -1,4 +1,5 @@ import * as Schema from '../src/schema.js' +import { CBOR } from '../src/dag.js' import { test, assert } from './test.js' test('.partial on structs', () => { @@ -45,3 +46,136 @@ test('.partial on dicts', () => { assert.deepEqual(IntsMaybe.from({ x: undefined }), {}) assert.deepEqual(IntsMaybe.from({ x: undefined, y: 2 }), { y: 2 }) }) + +test('.creteIPLDView', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const view = await Point.toIPLDBuilder({ + x: 1, + y: 2, + }).ok?.buildIPLDView() + + if (!view) { + return assert.fail('view is undefined') + } + + const store = new Map() + store.set(`${view.link()}`, view.root) + + const point = Point.toIPLDView({ + link: view.link(), + store, + }).ok + + if (!point) { + return assert.fail('point is undefined') + } + + assert.equal(point.x, 1) + assert.equal(point.y, 2) + + const blocks = [...point.iterateIPLDBlocks()] + assert.deepEqual(blocks, [view.root]) + assert.equal( + // @ts-expect-error - not specified in type + point.buildIPLDView(), + point + ) +}) + +test('.toIPLDBuilder fails with bad input', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const builder = await Point.toIPLDBuilder({ + x: 1, + }) + + assert.match(builder.error?.message || '', /invalid field "y"/) +}) + +test('invalid views', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const block = await CBOR.write({ x: 1, z: 3 }) + const store = new Map([[`${block.cid}`, block]]) + + const point = Point.toIPLDView({ link: block.cid, store }).ok + if (!point) { + return assert.fail('point is undefined') + } + assert.equal(point.x, 1) + + assert.throws(() => point.y, /invalid field "y"/) +}) + +test('toIPLDView fails if block is not found', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const block = await CBOR.write({ x: 1, y: 3 }) + const store = new Map() + const point = Point.toIPLDView({ link: block.cid, store }) + assert.equal(point.error?.message, `Missing block ${block.cid}`) +}) + +test('toIPLDView fails with incompatible block', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const block = await CBOR.write([{ x: 1, y: 3 }]) + const store = new Map([[`${block.cid}`, block]]) + const point = Point.toIPLDView({ link: block.cid, store }) + assert.equal( + point.error?.message, + `Expected value of type object instead got array` + ) +}) + +test('toIPLDView on non-struct', async () => { + const Points = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }).array() + + const block = await CBOR.write([ + { x: 0, y: 0 }, + { x: 16, y: 17 }, + ]) + const store = new Map([[`${block.cid}`, block]]) + + const points = Points.toIPLDView({ link: block.cid, store }) + assert.ok(points.ok) + assert.deepEqual(points.ok?.root.data, [ + { x: 0, y: 0 }, + { x: 16, y: 17 }, + ]) +}) + +test('toIPLDView on invalid content', async () => { + const Points = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }).array() + + const block = await CBOR.write({ x: 0, y: 0 }) + const store = new Map([[`${block.cid}`, block]]) + + const points = Points.toIPLDView({ link: block.cid, store }) + assert.equal( + points.error?.message, + 'Expected value of type array instead got object' + ) +}) diff --git a/packages/core/test/receipt.spec.js b/packages/core/test/receipt.spec.js index 854cdbe7..22542281 100644 --- a/packages/core/test/receipt.spec.js +++ b/packages/core/test/receipt.spec.js @@ -273,6 +273,8 @@ test('receipt view fallback', async () => { * @param {Partial & { verifier?: API.Verifier }} expect */ const assertReceipt = async (receipt, expect) => { + assert.equal(receipt.link(), receipt.root.cid, 'root cid is correct') + if (expect.out) { assert.deepEqual(receipt.out, expect.out, 'out is correct') } diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index d90017b1..c89dbc04 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -192,6 +192,8 @@ export interface IPLDView { */ root: Block + link(): Link + /** * Returns an iterable of all the IPLD blocks that are included in this view. * It is RECOMMENDED that implementations return blocks in bottom up order From 05933d316e18d176f65fed4db7758f7e4a8c6f7e Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 3 May 2023 22:18:12 -0700 Subject: [PATCH 05/16] feat: create proper input / output types --- packages/core/src/schema/schema.js | 1597 ++++++++++++++---------- packages/core/src/schema/type.ts | 314 +++-- packages/core/test/ipld-schema.spec.js | 61 + packages/core/test/schema.spec.js | 13 +- packages/core/test/schema/fixtures.js | 42 +- packages/interface/src/lib.ts | 3 +- 6 files changed, 1293 insertions(+), 737 deletions(-) create mode 100644 packages/core/test/ipld-schema.spec.js diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 1fc7df07..b7d29487 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -15,11 +15,11 @@ import { sha256 } from 'multiformats/hashes/sha2' export { ok } /** * @abstract - * @template [T=unknown] - * @template [I=unknown] + * @template [Out=unknown] + * @template [In=unknown] * @template [Settings=void] - * @extends {Schema.Base} - * @implements {Schema.Schema} + * @extends {Schema.Base} + * @implements {Schema.Schema} */ export class API { /** @@ -29,8 +29,8 @@ export class API { /** @protected */ this.settings = settings - this.codec = CBOR - this.hasher = sha256 + // this.codec = CBOR + // this.hasher = sha256 } toString() { @@ -38,36 +38,55 @@ export class API { } /** * @abstract - * @param {I} input + * @param {In} input * @param {Settings} settings - * @returns {Schema.ReadResult} + * @returns {Schema.ReadResult} */ /* c8 ignore next 3 */ readWith(input, settings) { throw new Error(`Abstract method readWith must be implemented by subclass`) } + /** - * @param {I} input - * @returns {Schema.ReadResult} + * + * @param {Out} output + * @param {Settings} settings + * @returns {Schema.ReadResult} + */ + writeWith(output, settings) { + throw new Error(`Abstract method readWith must be implemented by subclass`) + } + + /** + * @param {In} input + * @returns {Schema.ReadResult} */ read(input) { return this.readWith(input, this.settings) } + /** + * @param {Out} output + * @returns {Schema.ReadResult} + */ + write(output) { + return this.writeWith(output, this.settings) + } + /** * @param {unknown} value - * @returns {value is T} + * @returns {value is Out} */ is(value) { - return !this.read(/** @type {I} */ (value))?.error + return !this.read(/** @type {In} */ (value))?.error } /** - * @param {unknown} value - * @return {T} + * @param {In} value + * @return {Out} */ from(value) { - const result = this.read(/** @type {I} */ (value)) + const result = this.read(/** @type {In} */ (value)) if (result.error) { throw result.error } else { @@ -76,29 +95,50 @@ export class API { } /** - * @returns {Schema.Schema} + * @param {Out} value */ + to(value) { + const result = this.write(value) + if (result.error) { + throw result.error + } else { + return result.ok + } + } + optional() { return optional(this) } - /** - * @returns {Schema.Schema} - */ nullable() { return nullable(this) } /** - * @returns {Schema.Schema} + * @deprecated - use {@link Schema.Schema.implicit} instead + * @param {Schema.NotUndefined} value + */ + default(value) { + return this.implicit(value) + } + + /** + * @param {Schema.NotUndefined} value + */ + implicit(value) { + return implicit(this, value) + } + + /** + * @returns {Schema.ArraySchema} */ array() { return array(this) } /** - * @template U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @template I, O + * @param {Schema.Convert} schema + * @returns {Schema.Schema} */ or(schema) { @@ -106,18 +146,18 @@ export class API { } /** - * @template U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @template I, O + * @param {Schema.Convert} schema + * @returns {Schema.Schema} */ and(schema) { return and(this, schema) } /** - * @template {T} U - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @template {Out} O + * @param {Schema.Reader} schema + * @returns {Schema.Schema} */ refine(schema) { return refine(this, schema) @@ -126,33 +166,10 @@ export class API { /** * @template {string} Kind * @param {Kind} [kind] - * @returns {Schema.Schema, I>} + * @returns {Schema.Schema, In>} */ brand(kind) { - return /** @type {Schema.Schema, I>} */ (this) - } - - /** - * @param {Schema.NotUndefined} value - * @returns {Schema.DefaultSchema, I>} - */ - default(value) { - // ⚠️ this.from will throw if wrong default is provided - const fallback = this.from(value) - // 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 valid default`) - } - - const schema = new Default({ - reader: /** @type {Schema.Reader} */ (this), - value: /** @type {Schema.NotUndefined} */ (fallback), - }) - - return /** @type {Schema.DefaultSchema, I>} */ ( - schema - ) + return /** @type {Schema.Schema, In>} */ (this) } /** @@ -164,83 +181,91 @@ export class API { * hasher?: Schema.MultihashHasher * version?: Version * }} options - * @returns {Schema.LinkSchema} + * @returns {Schema.LinkSchema | Schema.IPLDView, Code, Alg, Version>} */ link({ codec, hasher, version } = {}) { - return link({ + const schema = link({ ...(codec ? { code: codec.code } : {}), ...(hasher ? { multihash: { code: hasher.code } } : {}), ...(version ? { version } : {}), - schema: this, + schema: /** @type {Schema.Schema} */ (this), }) - } - /** - * @param {I} input - * @returns {Schema.Result>, Schema.Error>} - */ - toIPLDBuilder(input) { - const result = this.read(input) - if (result.error) { - return result - } else { - const data = result.ok - const builder = new IPLDViewBuilder({ - data, - schema: this, - }) - return { ok: builder } - } - } - - /** - * @param {object} source - * @param {Schema.Link} source.link - * @param {Schema.BlockStore} source.store - * @returns {ReturnType} - */ - toIPLDView({ link, store }) { - const block = store.get(`${link}`) - if (!block) { - return /** @type {*} */ ({ error: new Error(`Missing block ${link}`) }) - } else { - return /** @type {*} */ (this.createIPLDView({ root: block, store })) - } - } - - /** - * @param {object} source - * @param {Schema.Block} source.root - * @param {Schema.BlockStore} source.store - * @returns {Schema.Result, Schema.Error>} - */ - createIPLDView({ root, store }) { - const input = /** @type {I} */ (this.codec.decode(root.bytes)) - const result = this.read(input) - if (result.error) { - return result - } else { - const view = new IPLDView({ - root: { ...root, data: result.ok }, - store, - schema: this, - }) - return { ok: view } - } - } + return schema + } + + // /** + // * @param {In} input + // * @returns {Schema.Result>, Schema.Error>} + // */ + // toIPLDBuilder(input) { + // const result = this.read(input) + // if (result.error) { + // return result + // } else { + // const data = result.ok + // const builder = new IPLDViewBuilder({ + // data, + // schema: this, + // }) + // return { ok: builder } + // } + // } + + // /** + // * @param {object} source + // * @param {Schema.Link} source.link + // * @param {Schema.BlockStore} source.store + // * @returns {ReturnType} + // */ + // toIPLDView({ link, store }) { + // const block = store.get(`${link}`) + // if (!block) { + // return /** @type {*} */ ({ error: new Error(`Missing block ${link}`) }) + // } else { + // return /** @type {*} */ (this.createIPLDView({ root: block, store })) + // } + // } + + // /** + // * @param {object} source + // * @param {Schema.Block} source.root + // * @param {Schema.BlockStore} source.store + // * @returns {Schema.Result, Schema.Error>} + // */ + // createIPLDView({ root, store }) { + // const input = /** @type {In} */ (this.codec.decode(root.bytes)) + // const result = this.read(input) + // if (result.error) { + // return result + // } else { + // const view = new IPLDView({ + // root: { ...root, data: result.ok }, + // store, + // schema: this, + // }) + // return { ok: view } + // } + // } } /** - * @template [I=unknown] - * @extends {API} - * @implements {Schema.Schema} + * @extends {API} + * @implements {Schema.Schema} */ class Never extends API { toString() { return 'never()' } + /** - * @param {I} input + * @param {never} value + */ + to(value) { + return value + } + /** + * @param {any} input * @returns {Schema.ReadResult} */ read(input) { @@ -255,17 +280,22 @@ class Never extends API { export const never = () => new Never() /** - * @template [I=unknown] - * @extends API - * @implements {Schema.Schema} + * @extends API + * @implements {Schema.Schema} */ class Unknown extends API { /** - * @param {I} input + * @param {unknown} input */ read(input) { return /** @type {Schema.ReadResult}*/ ({ ok: input }) } + /** + * @param {unknown} output + */ + write(output) { + return { ok: output } + } toString() { return 'unknown()' } @@ -273,104 +303,107 @@ class Unknown extends API { /** * @template [I=unknown] - * @returns {Schema.Schema} + * @returns {Schema.Schema} */ export const unknown = () => new Unknown() -/** - * @template {unknown} T - * @implements {Schema.IPLDViewBuilder>} - */ -class IPLDViewBuilder { - /** - * @param {object} input - * @param {T} input.data - * @param {Schema.Schema} input.schema - */ - constructor({ data, schema }) { - this.data = data - this.schema = schema - } - - /** - * @param {Schema.BuildOptions} [options] - * @returns {Promise>} - */ - async buildIPLDView({ - encoder = this.schema.codec, - hasher = this.schema.hasher, - } = {}) { - const { data } = this - const bytes = encoder.encode(data) - const digest = await hasher.digest(bytes) - /** @type {Schema.Link} */ - const cid = createLink(encoder.code, digest) - return new IPLDView({ root: { bytes, data, cid }, schema: this.schema }) - } -} +// /** +// * @template {unknown} T +// * @implements {Schema.IPLDViewBuilder>} +// */ +// class IPLDViewBuilder { +// /** +// * @param {object} input +// * @param {T} input.data +// * @param {Schema.Schema} input.schema +// */ +// constructor({ data, schema }) { +// this.data = data +// this.schema = schema +// } + +// /** +// * @param {Schema.BuildOptions} [options] +// * @returns {Promise>} +// */ +// async buildIPLDView({ +// encoder = this.schema.codec, +// hasher = this.schema.hasher, +// } = {}) { +// const { data } = this +// const bytes = encoder.encode(data) +// const digest = await hasher.digest(bytes) +// /** @type {Schema.Link} */ +// const cid = createLink(encoder.code, digest) +// return new IPLDView({ root: { bytes, data, cid }, schema: this.schema }) +// } +// } + +// /** +// * @template {unknown} T +// * @implements {Schema.IPLDView} +// * @implements {Schema.IPLDViewBuilder>} +// */ +// class IPLDView { +// /** +// * @param {object} input +// * @param {Required} input.root +// * @param {Schema.Schema} input.schema +// * @param {Schema.BlockStore} [input.store] +// */ +// constructor({ root, store = new Map(), schema }) { +// this.root = root +// this.store = store +// this.schema = schema +// } + +// /** +// * @template T +// * @param {object} input +// * @param {Required} input.root +// * @param {Schema.Schema} input.schema +// * @param {Schema.BlockStore} [input.store] +// */ +// static create(input) { +// return new this(input) +// } + +// /** +// * @returns {Schema.Link} +// */ +// link() { +// return this.root.cid +// } +// /** +// * @returns {IterableIterator} +// */ +// *iterateIPLDBlocks() { +// yield this.root +// } + +// /** +// * @returns {Schema.IPLDView} +// */ +// buildIPLDView() { +// return this +// } +// } /** - * @template {unknown} T - * @implements {Schema.IPLDView} - * @implements {Schema.IPLDViewBuilder>} - */ -class IPLDView { - /** - * @param {object} input - * @param {Required} input.root - * @param {Schema.Schema} input.schema - * @param {Schema.BlockStore} [input.store] - */ - constructor({ root, store = new Map(), schema }) { - this.root = root - this.store = store - this.schema = schema - } - - /** - * @template T - * @param {object} input - * @param {Required} input.root - * @param {Schema.Schema} input.schema - * @param {Schema.BlockStore} [input.store] - */ - static create(input) { - return new this(input) - } - - /** - * @returns {Schema.Link} - */ - link() { - return this.root.cid - } - /** - * @returns {IterableIterator} - */ - *iterateIPLDBlocks() { - yield this.root - } - - /** - * @returns {Schema.IPLDView} - */ - buildIPLDView() { - return this - } -} - -/** - * @template O - * @template [I=unknown] - * @extends {API>} - * @implements {Schema.Schema} + * @template I, O + * @extends {API>} + * @implements {Schema.Schema} */ class Nullable extends API { /** - * @param {I} input + * @param {I|null} input * @param {Schema.Reader} reader */ readWith(input, reader) { + if (input === null) { + return { ok: null } + } + const result = reader.read(input) if (result.error) { return input === null @@ -387,6 +420,13 @@ class Nullable extends API { return result } } + /** + * @param {O|null} output + * @param {Schema.Writer} writer + */ + writeWith(output, writer) { + return output === null ? { ok: null } : writer.write(output) + } toString() { return `${this.settings}.nullable()` } @@ -395,101 +435,152 @@ class Nullable extends API { /** * @template O * @template [I=unknown] - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @param {Schema.Convert} schema + * @returns {Schema.Schema} */ export const nullable = schema => new Nullable(schema) /** * @template O * @template [I=unknown] - * @extends {API>} - * @implements {Schema.Schema} + * @extends {API>} + * @implements {Schema.Schema} */ class Optional extends API { optional() { return this } /** - * @param {I} input + * @param {I|undefined} input * @param {Schema.Reader} reader * @returns {Schema.ReadResult} */ readWith(input, reader) { + if (input === undefined) { + return { ok: undefined } + } const result = reader.read(input) return result.error && input === undefined ? { ok: undefined } : result } + /** + * + * @param {O|undefined} output + * @param {Schema.Convert} writer + */ + writeWith(output, writer) { + return output === undefined ? { ok: undefined } : writer.write(output) + } toString() { return `${this.settings}.optional()` } } /** - * @template {unknown} O + * @template O * @template [I=unknown] - * @extends {API, value:O & Schema.NotUndefined}>} - * @implements {Schema.DefaultSchema} + * @param {Schema.Convert} schema + * @returns {Schema.Schema} + */ +export const optional = schema => new Optional(schema) + +/** + * @template Out + * @template In + * @extends {API, In | undefined, {convert:Schema.Convert, value:Exclude}>} + * @implements {Schema.ImplicitSchema} */ -class Default extends API { +class Implicit extends API { /** - * @returns {Schema.DefaultSchema, I>} + * @returns {Schema.ImplicitSchema} */ optional() { - // Short circuit here as we there is no point in wrapping this in optional. - return /** @type {Schema.DefaultSchema, I>} */ ( - this - ) + return this } + /** - * @param {I} input + * @param {Exclude} value + */ + implicit(value) { + return /** @type {*} */ (implicit(this.settings.convert, value)) + } + + /** + * @param {In|undefined} input * @param {object} options - * @param {Schema.Reader} options.reader - * @param {O} options.value - * @returns {Schema.ReadResult} + * @param {Schema.Convert} options.convert + * @param {Exclude} options.value + * @returns {Schema.ReadResult>} */ - readWith(input, { reader, value }) { + readWith(input, { convert, value }) { if (input === undefined) { - return /** @type {Schema.ReadResult} */ ({ ok: value }) + return { ok: value } } else { - const result = reader.read(input) + const result = convert.read(input) return result.error ? result - : result.ok !== undefined - ? // We just checked that result.ok is not undefined but still needs + : result.ok === undefined + ? { ok: value } + : // We just checked that result.ok is not undefined but still needs // reassurance - /** @type {Schema.ReadResult} */ (result) - : { ok: value } + /** @type {*} */ (result) } } + + /** + * + * @param {Exclude} output + * @param {object} options + * @param {Schema.Convert} options.convert + */ + writeWith(output, { convert }) { + return convert.write(output) + } toString() { - return `${this.settings.reader}.default(${JSON.stringify( + return `${this.settings.convert}.default(${JSON.stringify( this.settings.value )})` } + /** + * @returns {Schema.NotUndefined} + */ get value() { - return this.settings.value + return /** @type {Schema.NotUndefined} */ (this.settings.value) } } /** - * @template O - * @template [I=unknown] - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @template O, I + * @template {Exclude} Q + * @param {Schema.Convert} schema + * @param {Q} value + * @returns {Schema.ImplicitSchema} */ -export const optional = schema => new Optional(schema) +export const implicit = (schema, value) => { + // we also check that fallback is not undefined because that is the point + // of having a fallback + if (value === undefined) { + throw new Error(`Value of type undefined is not a valid default`) + } + + const implicit = + /** @type {Schema.ImplicitSchema, I>} */ ( + new Implicit({ convert: schema, value }) + ) + + return implicit +} /** * @template O - * @template [I=unknown] - * @extends {API>} + * @template I + * @extends {API>} * @implements {Schema.ArraySchema} */ class ArrayOf extends API { /** - * @param {I} input + * @param {I[]} input * @param {Schema.Reader} schema */ readWith(input, schema) { @@ -508,6 +599,27 @@ class ArrayOf extends API { } return { ok: results } } + /** + * + * @param {O[]} output + * @param {Schema.Writer} schema + */ + writeWith(output, schema) { + /** @type {I[]} */ + const results = [] + for (const [index, value] of output.entries()) { + const result = schema.write(value) + if (result.error) { + return memberError({ at: index, cause: result.error }) + } else { + results.push(result.ok) + } + } + return { ok: results } + } + /** + * @type {Schema.Convert} + */ get element() { return this.settings } @@ -519,23 +631,21 @@ class ArrayOf extends API { /** * @template O * @template [I=unknown] - * @param {Schema.Reader} schema + * @param {Schema.Convert} schema * @returns {Schema.ArraySchema} */ export const array = schema => new ArrayOf(schema) /** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Shape + * @extends {API, Schema.InferTupleInput, Shape>} + * @implements {Schema.Schema, Schema.InferTupleInput>} */ class Tuple extends API { /** - * @param {I} input - * @param {U} shape - * @returns {Schema.ReadResult>} + * @param {Schema.InferTupleInput} input + * @param {Shape} shape + * @returns {Schema.ReadResult>} */ readWith(input, shape) { if (!Array.isArray(input)) { @@ -555,41 +665,66 @@ class Tuple extends API { } } - return { ok: /** @type {Schema.InferTuple} */ (results) } + return { ok: /** @type {Schema.InferTuple} */ (results) } + } + /** + * + * @param {Schema.InferTuple} output + * @param {Shape} shape + * @returns {Schema.ReadResult>} + */ + writeWith(output, shape) { + if (!Array.isArray(output)) { + return typeError({ expect: 'array', actual: output }) + } + + if (output.length !== this.shape.length) { + return error(`Array must contain exactly ${this.shape.length} elements`) + } + + const results = [] + for (const [index, writer] of shape.entries()) { + const result = writer.write(output[index]) + if (result.error) { + return memberError({ at: index, cause: result.error }) + } else { + results[index] = result.ok + } + } + + return { ok: /** @type {Schema.InferTupleInput} */ (results) } } - /** @type {U} */ + /** @type {Shape} */ get shape() { return this.settings } toString() { - return `tuple([${this.shape.map(reader => reader.toString()).join(', ')}])` + return `tuple([${this.shape.map(member => member.toString()).join(', ')}])` } } /** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} shape - * @returns {Schema.Schema, I>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Shape + * @param {Shape} shape + * @returns {Schema.Schema, Schema.InferTupleInput>} */ export const tuple = shape => new Tuple(shape) /** * @template V * @template {string} K - * @template [I=unknown] - * @extends {API, I, { key: Schema.Reader, value: Schema.Reader }>} - * @implements {Schema.DictionarySchema} + * @template U + * @extends {API, Schema.Dictionary, { key: Schema.Reader, value: Schema.Convert }>} + * @implements {Schema.DictionarySchema} */ class Dictionary extends API { /** - * @param {I} input + * @param {Schema.Dictionary} input * @param {object} schema * @param {Schema.Reader} schema.key - * @param {Schema.Reader} schema.value + * @param {Schema.Reader} schema.value */ readWith(input, { key, value }) { if (typeof input != 'object' || input === null || Array.isArray(input)) { @@ -620,6 +755,36 @@ class Dictionary extends API { return { ok: dict } } + + /** + * + * @param {Schema.Dictionary} output + * @param {object} schema + * @param {Schema.Reader} schema.key + * @param {Schema.Writer} schema.value + */ + writeWith(output, { key, value }) { + const dict = /** @type {Schema.Dictionary} */ ({}) + + for (const [k, v] of Object.entries(output)) { + const keyResult = key.read(k) + if (keyResult.error) { + return memberError({ at: k, cause: keyResult.error }) + } + + const valueResult = value.write(/** @type {V} */ (v)) + if (valueResult.error) { + return memberError({ at: k, cause: valueResult.error }) + } + + // skip undefined because they mess up CBOR and are generally useless. + if (valueResult.ok !== undefined) { + dict[keyResult.ok] = valueResult.ok + } + } + + return { ok: dict } + } get key() { return this.settings.key } @@ -627,12 +792,17 @@ class Dictionary extends API { return this.settings.value } + /** + * + * @returns {Schema.DictionarySchema} + */ partial() { const { key, value } = this.settings - return new Dictionary({ + const partial = new Dictionary({ key, value: optional(value), }) + return partial } toString() { return `dictionary(${this.settings})` @@ -642,11 +812,11 @@ class Dictionary extends API { /** * @template {string} K * @template {unknown} V - * @template [I=unknown] + * @template {unknown} U * @param {object} shape - * @param {Schema.Reader} shape.value + * @param {Schema.Convert} shape.value * @param {Schema.Reader} [shape.key] - * @returns {Schema.DictionarySchema} + * @returns {Schema.DictionarySchema} */ export const dictionary = ({ value, key }) => new Dictionary({ @@ -655,24 +825,35 @@ export const dictionary = ({ value, key }) => }) /** - * @template {[unknown, ...unknown[]]} T - * @template [I=unknown] - * @extends {API}>} - * @implements {Schema.Schema} + * @template {string} T + * @template {[T, ...T[]]} U + * @extends {API}>} + * @implements {Schema.Schema} */ class Enum extends API { /** - * @param {I} input - * @param {{type:string, variants:Set}} settings - * @returns {Schema.ReadResult} + * @param {string} input + * @param {{type:string, variants:Set}} settings + * @returns {Schema.ReadResult} */ readWith(input, { variants, type }) { - if (variants.has(input)) { - return /** @type {Schema.ReadResult} */ ({ ok: input }) + if (variants.has(/** @type {T} */ (input))) { + return /** @type {Schema.ReadResult} */ ({ ok: input }) } else { return typeError({ expect: type, actual: input }) } } + /** + * @param {U[number]} output + * @param {{type:string, variants:Set}} settings + */ + writeWith(output, settings) { + if (settings.variants.has(output)) { + return { ok: output } + } else { + return typeError({ expect: settings.type, actual: output }) + } + } toString() { return this.settings.type } @@ -683,7 +864,7 @@ class Enum extends API { * @template {[T, ...T[]]} U * @template [I=unknown] * @param {U} variants - * @returns {Schema.Schema} + * @returns {Schema.Schema} */ const createEnum = variants => new Enum({ @@ -693,16 +874,14 @@ const createEnum = variants => export { createEnum as enum } /** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.Schema, I>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Members + * @extends {API, Schema.InferUnionInput, Members>} + * @implements {Schema.Schema, Schema.InferUnionInput>} */ class Union extends API { /** - * @param {I} input - * @param {U} variants + * @param {Schema.InferUnionInput} input + * @param {Members} variants */ readWith(input, variants) { const causes = [] @@ -711,50 +890,67 @@ class Union extends API { if (result.error) { causes.push(result.error) } else { - return /** @type {Schema.ReadResult>} */ (result) + return /** @type {Schema.ReadResult>} */ ( + result + ) + } + } + return { error: new UnionError({ causes }) } + } + /** + * @param {Schema.InferUnion} output + * @param {Members} variants + */ + writeWith(output, variants) { + const causes = [] + for (const member of variants) { + const result = member.write(output) + if (result.error) { + causes.push(result.error) + } else { + return /** @type {Schema.ReadResult>} */ ( + result + ) } } return { error: new UnionError({ causes }) } } - get variants() { + get members() { return this.settings } toString() { - return `union([${this.variants.map(type => type.toString()).join(', ')}])` + return `union([${this.members + .map(member => member.toString()) + .join(', ')}])` } } /** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} variants - * @returns {Schema.Schema, I>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Members + * @param {Members} members + * @returns {Schema.Schema, Schema.InferUnionInput>} */ -const union = variants => new Union(variants) +const union = members => new Union(members) /** - * @template T, U - * @template [I=unknown] - * @param {Schema.Reader} left - * @param {Schema.Reader} right - * @returns {Schema.Schema} + * @template O, Q, I, J + * @param {Schema.Convert} left + * @param {Schema.Convert} right + * @returns {Schema.Schema} */ 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>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Members + * @extends {API, Schema.InferIntersectionInput, Members>} + * @implements {Schema.Schema, Schema.InferIntersectionInput>} */ class Intersection extends API { /** - * @param {I} input - * @param {U} schemas - * @returns {Schema.ReadResult>} + * @param {Schema.InferIntersectionInput} input + * @param {Members} schemas + * @returns {Schema.ReadResult>} */ readWith(input, schemas) { const causes = [] @@ -767,44 +963,86 @@ class Intersection extends API { return causes.length > 0 ? { error: new IntersectionError({ causes }) } - : /** @type {Schema.ReadResult>} */ ({ + : /** @type {Schema.ReadResult>} */ ({ ok: input, }) } + /** + * @param {Schema.InferIntersection} output + * @param {Members} members + * @returns {Schema.ReadResult>} + */ + writeWith(output, members) { + const causes = [] + for (const member of members) { + const result = member.write(output) + if (result.error) { + causes.push(result.error) + } + } + + return causes.length > 0 + ? { error: new IntersectionError({ causes }) } + : /** @type {Schema.ReadResult>} */ ({ + ok: output, + }) + } + get members() { + return this.settings + } toString() { - return `intersection([${this.settings - .map(type => type.toString()) + return `intersection([${this.members + .map(member => member.toString()) .join(',')}])` } } /** - * @template {Schema.Reader} T - * @template {[T, ...T[]]} U - * @template [I=unknown] - * @param {U} variants - * @returns {Schema.Schema, I>} + * @template {[Schema.Convert, ...Schema.Convert[]]} Members + * @param {Members} members + * @returns {Schema.Schema, Schema.InferIntersectionInput>} */ -export const intersection = variants => new Intersection(variants) +export const intersection = members => new Intersection(members) /** - * @template T, U - * @template [I=unknown] - * @param {Schema.Reader} left - * @param {Schema.Reader} right - * @returns {Schema.Schema} + * @template O, Q, I, J + * @param {Schema.Convert} left + * @param {Schema.Convert} right + * @returns {Schema.Schema} */ export const and = (left, right) => intersection([left, right]) /** - * @template [I=unknown] - * @extends {API} + * @template {string|number|boolean|null} Type + * @template {string} Name + * @extends {API}>} */ -class Boolean extends API { +class Scalar extends API { /** - * @param {I} input + * @param {Type} input + * @param {typeof this.settings} settings */ - readWith(input) { + readWith(input, { cast }) { + return cast(input) + } + /** + * @param {Type} output + * @param {typeof this.settings} settings + */ + writeWith(output, { cast }) { + return cast(output) + } + toString() { + return `${this.settings.name}()` + } +} + +export const Boolean = new Scalar({ + name: 'boolean', + /** + * @param {boolean} input + */ + cast(input) { switch (input) { case true: case false: @@ -815,62 +1053,74 @@ class Boolean extends API { actual: input, }) } - } - - toString() { - return `boolean()` - } -} - -/** @type {Schema.Schema} */ -const anyBoolean = new Boolean() - -export const boolean = () => anyBoolean + }, +}) +export const boolean = () => Boolean /** - * @template {number} [O=number] - * @template [I=unknown] + * @template {number} O + * @template {number} I * @template [Settings=void] * @extends {API} * @implements {Schema.NumberSchema} */ -class UnknownNumber extends API { +class NumberSchema extends API { + isInteger = globalThis.Number.isInteger + isFinite = globalThis.Number.isFinite + + /** + * @param {(input: O) => Schema.ReadResult} check + * @returns {Schema.NumberSchema} + */ + constraint(check) { + return this.refine({ + read: check, + write: check, + }) + } + /** * @param {number} n */ greaterThan(n) { - return this.refine(greaterThan(n)) + return this.constraint(number => { + if (number > n) { + return { ok: number } + } else { + return error(`Expected ${number} > ${n}`) + } + }) } /** * @param {number} n */ lessThan(n) { - return this.refine(lessThan(n)) + return this.constraint(number => { + if (number < n) { + return { ok: number } + } else { + return error(`Expected ${number} < ${n}`) + } + }) } /** - * @template {O} U - * @param {Schema.Reader} schema - * @returns {Schema.NumberSchema} + * @template {O} Into + * @param {Schema.Convert} convert + * @returns {Schema.NumberSchema} */ - refine(schema) { - return new RefinedNumber({ base: this, schema }) + refine(convert) { + return new RefinedNumber({ schema: this, refine: convert }) } -} -/** - * @template [I=unknown] - * @extends {UnknownNumber} - * @implements {Schema.NumberSchema} - */ -class AnyNumber extends UnknownNumber { /** * @param {I} input - * @returns {Schema.ReadResult} + * @param {Settings} settings + * @returns {Schema.ReadResult} */ - readWith(input) { + readWith(input, settings) { return typeof input === 'number' - ? { ok: input } + ? { ok: /** @type {*} */ (input) } : typeError({ expect: 'number', actual: input }) } toString() { @@ -878,91 +1128,46 @@ class AnyNumber extends UnknownNumber { } } -/** @type {Schema.NumberSchema} */ -const anyNumber = new AnyNumber() -export const number = () => anyNumber +/** @type {Schema.NumberSchema} */ +export const Number = new NumberSchema(undefined) +export const number = () => Number /** - * @template {number} [T=number] - * @template {T} [O=T] - * @template [I=unknown] - * @extends {UnknownNumber, schema:Schema.Reader}>} - * @implements {Schema.NumberSchema} + * @template {number} Out + * @template {number} In + * @template {Out} Into + * @extends {NumberSchema, refine:Schema.Reader}>} + * @implements {Schema.NumberSchema} */ -class RefinedNumber extends UnknownNumber { +class RefinedNumber extends NumberSchema { /** - * @param {I} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings - * @returns {Schema.ReadResult} + * @param {In} input + * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings + * @returns {Schema.ReadResult} */ - readWith(input, { base, schema }) { - const result = base.read(input) - return result.error ? result : schema.read(result.ok) - } - toString() { - return `${this.settings.base}.refine(${this.settings.schema})` + readWith(input, { schema, refine }) { + const result = schema.read(input) + return result.error ? result : refine.read(result.ok) } -} -/** - * @template {number} T - * @extends {API} - */ -class LessThan extends API { /** - * @param {T} input - * @param {number} number - * @returns {Schema.ReadResult} + * @param {Into} output + * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings */ - readWith(input, number) { - if (input < number) { - return { ok: input } - } else { - return error(`Expected ${input} < ${number}`) - } - } - toString() { - return `lessThan(${this.settings})` + writeWith(output, { schema, refine }) { + const result = refine.write(output) + return result.error ? result : schema.write(result.ok) } -} -/** - * @template {number} T - * @param {number} n - * @returns {Schema.Reader} - */ -export const lessThan = n => new LessThan(n) - -/** - * @template {number} T - * @extends {API} - */ -class GreaterThan extends API { - /** - * @param {T} input - * @param {number} number - * @returns {Schema.ReadResult} - */ - readWith(input, number) { - if (input > number) { - return { ok: input } - } else { - return error(`Expected ${input} > ${number}`) - } - } toString() { - return `greaterThan(${this.settings})` + return `${this.settings.schema}.refine(${this.settings.refine})` } } /** - * @template {number} T - * @param {number} n - * @returns {Schema.Reader} + * @extends {NumberSchema} */ -export const greaterThan = n => new GreaterThan(n) - -const Integer = { +class IntegerSchema extends NumberSchema { /** * @param {number} input * @returns {Schema.ReadResult} @@ -974,16 +1179,19 @@ const Integer = { expect: 'integer', actual: input, }) - }, + } toString() { return `Integer` - }, + } } +/** @type {Schema.NumberSchema} */ +const Integer = new IntegerSchema() +export const integer = () => Integer -const anyInteger = anyNumber.refine(Integer) -export const integer = () => anyInteger - -const Float = { +/** + * @extends {NumberSchema} + */ +class FloatSchema extends NumberSchema { /** * @param {number} number * @returns {Schema.ReadResult} @@ -995,35 +1203,57 @@ const Float = { expect: 'Float', actual: number, }) - }, + } toString() { return 'Float' - }, + } } -const anyFloat = anyNumber.refine(Float) -export const float = () => anyFloat +const Float = new FloatSchema() +export const float = () => Float /** - * @template {string} [O=string] - * @template [I=unknown] + * @template {string} Out + * @template {string} In * @template [Settings=void] - * @extends {API} + * @extends {API} + * @implements {Schema.StringSchema} */ -class UnknownString extends API { +class StringSchema extends API { + /** + * @param {In} input + * @param {Settings} settings + * @returns {Schema.ReadResult} + */ + readWith(input, settings) { + return typeof input === 'string' + ? { ok: /** @type {Out & In} */ (input) } + : typeError({ expect: 'string', actual: input }) + } + /** - * @template {O|unknown} U - * @param {Schema.Reader} schema - * @returns {Schema.StringSchema} + * @template {Out} Onto + * @param {Schema.Convert} schema + * @returns {Schema.StringSchema} */ refine(schema) { - const other = /** @type {Schema.Reader} */ (schema) - const rest = new RefinedString({ + const refined = new RefinedString({ base: this, - schema: other, + schema, }) - return /** @type {Schema.StringSchema} */ (rest) + return /** @type {Schema.StringSchema} */ (refined) + } + + /** + * @param {(value: Out) => Schema.ReadResult} check + * @returns {Schema.StringSchema} + */ + constraint(check) { + return this.refine({ + read: check, + write: check, + }) } /** * @template {string} Prefix @@ -1045,64 +1275,57 @@ class UnknownString extends API { } /** - * @template O - * @template {string} [T=string] - * @template [I=unknown] - * @extends {UnknownString, schema:Schema.Reader}>} - * @implements {Schema.StringSchema} + * @template {string} Into + * @template {string} Out + * @template {string} In + * @extends {StringSchema, schema:Schema.Reader}>} + * @implements {Schema.StringSchema} */ -class RefinedString extends UnknownString { +class RefinedString extends StringSchema { /** - * @param {I} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings - * @returns {Schema.ReadResult} + * @param {In} input + * @param {{base:Schema.Reader, schema:Schema.Reader}} settings + * @returns {Schema.ReadResult} */ readWith(input, { base, schema }) { const result = base.read(input) return result.error ? result - : /** @type {Schema.ReadResult} */ (schema.read(result.ok)) - } - toString() { - return `${this.settings.base}.refine(${this.settings.schema})` + : /** @type {Schema.ReadResult} */ (schema.read(result.ok)) } -} -/** - * @template [I=unknown] - * @extends {UnknownString} - * @implements {Schema.StringSchema} - */ -class AnyString extends UnknownString { /** - * @param {I} input - * @returns {Schema.ReadResult} + * @param {Into & Out} output + * @param {{base:Schema.Writer, schema:Schema.Reader}} settings + * @returns {Schema.ReadResult} */ - readWith(input) { - return typeof input === 'string' - ? { ok: input } - : typeError({ expect: 'string', actual: input }) + writeWith(output, { base, schema }) { + return base.write(output) + } + + toString() { + return `${this.settings.base}.refine(${this.settings.schema})` } } -/** @type {Schema.StringSchema} */ -const anyString = new AnyString() -export const string = () => anyString +/** @type {Schema.StringSchema} */ +export const String = new StringSchema(undefined) +export const string = () => String /** * @template {string} Prefix - * @template {string} Body - * @extends {API} - * @implements {Schema.Schema} + * @template {string} In + * @extends {API<`${Prefix}${string}` & In, In, Prefix>} + * @implements {Schema.Schema<`${Prefix}${string}` & In, In>} */ class StartsWith extends API { /** - * @param {Body} input + * @param {In} input * @param {Prefix} prefix */ readWith(input, prefix) { const result = input.startsWith(prefix) - ? /** @type {Schema.ReadResult} */ ({ + ? /** @type {Schema.ReadResult<`${Prefix}${string}` & In>} */ ({ ok: input, }) : error(`Expect string to start with "${prefix}" instead got "${input}"`) @@ -1119,16 +1342,16 @@ class StartsWith extends API { /** * @template {string} Prefix - * @template {string} Body + * @template {string} Input * @param {Prefix} prefix - * @returns {Schema.Schema<`${Prefix}${string}`, string>} + * @returns {Schema.Schema<`${Prefix}${string}` & Input, Input>} */ export const startsWith = prefix => new StartsWith(prefix) /** * @template {string} Suffix * @template {string} Body - * @extends {API} + * @extends {API} */ class EndsWith extends API { /** @@ -1137,7 +1360,7 @@ class EndsWith extends API { */ readWith(input, suffix) { return input.endsWith(suffix) - ? /** @type {Schema.ReadResult} */ ({ + ? /** @type {Schema.ReadResult} */ ({ ok: input, }) : error(`Expect string to end with "${suffix}" instead got "${input}"`) @@ -1152,54 +1375,62 @@ class EndsWith extends API { /** * @template {string} Suffix + * @template {string} Input * @param {Suffix} suffix - * @returns {Schema.Schema<`${string}${Suffix}`, string>} + * @returns {Schema.Schema<`${string}${Suffix}` & Input, Input>} */ export const endsWith = suffix => new EndsWith(suffix) /** - * @template T - * @template {T} U - * @template [I=unknown] - * @extends {API, schema: Schema.Reader }>} - * @implements {Schema.Schema} + * @template Out + * @template {Out} Into + * @template In + * @extends {API, schema: Schema.Reader }>} + * @implements {Schema.Schema} */ class Refine extends API { /** - * @param {I} input - * @param {{ base: Schema.Reader, schema: Schema.Reader }} settings + * @param {In} input + * @param {{ base: Schema.Convert, schema: Schema.Reader }} settings */ readWith(input, { base, schema }) { const result = base.read(input) return result.error ? result : schema.read(result.ok) } + /** + * @param {Into & Out} output + * @param {{ base: Schema.Convert, schema: Schema.Reader }} settings + */ + writeWith(output, { base, schema }) { + return base.write(output) + } toString() { return `${this.settings.base}.refine(${this.settings.schema})` } } /** - * @template T - * @template {T} U - * @template [I=unknown] - * @param {Schema.Reader} base - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @template Out + * @template {Out} Into + * @template In + * @param {Schema.Convert} base + * @param {Schema.Reader} schema + * @returns {Schema.Schema} */ export const refine = (base, schema) => new Refine({ base, schema }) /** - * @template {null|boolean|string|number} T - * @template [I=unknown] - * @extends {API} - * @implements {Schema.LiteralSchema} + * @template {null|boolean|string|number} Out + * @template {null|boolean|string|number} In + * @extends {API} + * @implements {Schema.LiteralSchema} */ class Literal extends API { /** - * @param {I} input - * @param {T} expect - * @returns {Schema.ReadResult} + * @param {In} input + * @param {Out} expect + * @returns {Schema.ReadResult} */ readWith(input, expect) { return input !== /** @type {unknown} */ (expect) @@ -1207,14 +1438,13 @@ class Literal extends API { : { ok: expect } } get value() { - return /** @type {Exclude} */ (this.settings) + return /** @type {Exclude} */ (this.settings) } /** - * @template {Schema.NotUndefined} U - * @param {U} value + * @param {Out} value */ - default(value = /** @type {U} */ (this.value)) { - return super.default(value) + implicit(value = this.value) { + return implicit(this, /** @type {Exclude} */ (value)) } toString() { return `literal(${displayTypeName(this.value)})` @@ -1222,23 +1452,23 @@ class Literal extends API { } /** - * @template {null|boolean|string|number} T - * @template [I=unknown] - * @param {T} value - * @returns {Schema.LiteralSchema} + * @template {null|boolean|string|number} Out + * @template {null|boolean|string|number} In + * @param {Out} value + * @returns {Schema.LiteralSchema} */ export const literal = value => new Literal(value) /** - * @template {{[key:string]: Schema.Reader}} U - * @template [I=unknown] - * @extends {API, I, {shape: U}>} + * @template {Schema.StructMembers} Members + * @extends {API, Schema.InferStructInput, {shape: Members}>} + * @implements {Schema.StructSchema} */ class Struct extends API { /** - * @param {I} input - * @param {{shape: U}} settings - * @returns {Schema.ReadResult>} + * @param {Schema.InferStructInput} input + * @param {{shape: Members}} settings + * @returns {Schema.ReadResult>} */ readWith(input, { shape }) { if (typeof input != 'object' || input === null || Array.isArray(input)) { @@ -1248,11 +1478,12 @@ class Struct extends API { }) } - const source = /** @type {{[K in keyof U]: unknown}} */ (input) + const source = /** @type {{[K in keyof Members]: unknown}} */ (input) - const struct = /** @type {{[K in keyof U]: Schema.Infer}} */ ({}) + const struct = + /** @type {{[K in keyof Members]: Schema.Infer}} */ ({}) const entries = - /** @type {{[K in keyof U]: [K & string, U[K]]}[keyof U][]} */ ( + /** @type {{[K in keyof Members]: [K & string, Members[K]]}[keyof Members][]} */ ( Object.entries(shape) ) @@ -1263,27 +1494,23 @@ class Struct extends API { } // skip undefined because they mess up CBOR and are generally useless. else if (result.ok !== undefined) { - struct[at] = /** @type {Schema.Infer} */ (result.ok) + struct[at] = /** @type {Schema.Infer} */ (result.ok) } } return { ok: struct } } - /** - * @returns {Schema.MapRepresentation>> & Schema.StructSchema} - */ partial() { const shape = Object.fromEntries( Object.entries(this.shape).map(([key, value]) => [key, optional(value)]) ) - return new Struct({ shape }) + return /** @type {*} */ (new Struct({ shape })) } - /** @type {U} */ + /** @type {Members} */ get shape() { - // @ts-ignore - We declared `settings` private but we access it here return this.settings.shape } @@ -1298,95 +1525,94 @@ class Struct extends API { } /** - * @param {Schema.InferStructSource} data + * @param {Schema.InferStructSource} data */ create(data) { - return this.from(data || {}) + return this.from(/** @type {*} */ (data || {})) } /** - * @template {{[key:string]: Schema.Reader}} E + * @template {Schema.StructMembers} E * @param {E} extension - * @returns {Schema.StructSchema} + * @returns {Schema.StructSchema} */ extend(extension) { return new Struct({ shape: { ...this.shape, ...extension } }) } - /** - * @param {object} source - * @param {Schema.Block} source.root - * @param {Schema.BlockStore} source.store - * @returns {Schema.Result & Schema.IPLDView>, Schema.Error>} - */ - createIPLDView(source) { - const data = this.codec.decode(source.root.bytes) - - if (typeof data != 'object' || data === null || Array.isArray(data)) { - return typeError({ - expect: 'object', - actual: data, - }) - } - - let View = this._View || (this._View = IPLDStructView.struct(this.shape)) - const root = { ...source.root, data } - - const view = View.create({ root, store: source.store, schema: this }) - return { ok: view } - } + // /** + // * @param {object} source + // * @param {Schema.Block} source.root + // * @param {Schema.BlockStore} source.store + // * @returns {Schema.Result & Schema.IPLDView>, Schema.Error>} + // */ + // createIPLDView(source) { + // const data = this.codec.decode(source.root.bytes) + + // if (typeof data != 'object' || data === null || Array.isArray(data)) { + // return typeError({ + // expect: 'object', + // actual: data, + // }) + // } + + // let View = this._View || (this._View = IPLDStructView.struct(this.shape)) + // const root = { ...source.root, data } + + // const view = View.create({ root, store: source.store, schema: this }) + // return { ok: view } + // } } -/** - * @template {{[key:string]: Schema.Reader}} U - * @extends {IPLDView>} - */ -class IPLDStructView extends IPLDView { - /** - * @template {{[key:string]: Schema.Reader}} U - * @param {U} shape - * @returns {Schema.CreateView, Schema.InferStruct>} - */ - static struct(shape) { - /** @extends {IPLDStructView} */ - class View extends this { - static shape = shape - } - - for (const [key, schema] of Object.entries(shape)) { - Object.defineProperty(View.prototype, key, { - get() { - let result = this[`_${key}`] - if (!result) { - result = schema.read(this.root.data[key]) - this[`_${key}`] = result - } - - if (result.ok) { - return result.ok - } else { - throw memberError({ at: key, cause: result.error }).error - } - }, - }) - } - - return /** @type {*} */ (View) - } -} +// /** +// * @template {{[key:string]: Schema.Reader}} U +// * @extends {IPLDView>} +// */ +// class IPLDStructView extends IPLDView { +// /** +// * @template {{[key:string]: Schema.Reader}} U +// * @param {U} shape +// * @returns {Schema.CreateView, Schema.InferStruct>} +// */ +// static struct(shape) { +// /** @extends {IPLDStructView} */ +// class View extends this { +// static shape = shape +// } + +// for (const [key, schema] of Object.entries(shape)) { +// Object.defineProperty(View.prototype, key, { +// get() { +// let result = this[`_${key}`] +// if (!result) { +// result = schema.read(this.root.data[key]) +// this[`_${key}`] = result +// } + +// if (result.ok) { +// return result.ok +// } else { +// throw memberError({ at: key, cause: result.error }).error +// } +// }, +// }) +// } + +// return /** @type {*} */ (View) +// } +// } /** * @template {null|boolean|string|number} T - * @template {{[key:string]: T|Schema.Reader}} U - * @template {{[K in keyof U]: U[K] extends Schema.Reader ? U[K] : Schema.LiteralSchema}} V + * @template {{[key:string]: T|Schema.Convert}} U + * @template {{[K in keyof U]: U[K] extends Schema.Convert ? U[K] : Schema.Convert}} Members * @template [I=unknown] * @param {U} fields - * @returns {Schema.StructSchema} + * @returns {Schema.StructSchema} */ export const struct = fields => { - const shape = - /** @type {{[K in keyof U]: Schema.Reader}} */ ({}) - /** @type {[keyof U & string, T|Schema.Reader][]} */ + const shape = /** @type {{[K in keyof U]: Schema.Convert}} */ ({}) + /** @type {[keyof U & string, T|Schema.Convert][]} */ const entries = Object.entries(fields) for (const [key, field] of entries) { @@ -1406,7 +1632,114 @@ export const struct = fields => { } } - return new Struct({ shape: /** @type {V} */ (shape) }) + return new Struct({ shape: /** @type {Members} */ (shape) }) +} + +/** + * @extends {API} + * @implements {Schema.BytesSchema>} + */ +class RawBytes extends API { + name = 'raw' + code = /** @type {const} */ (0x55) + + /** + * @param {Uint8Array} input + */ + encode(input) { + if (input instanceof Uint8Array) { + return input + } else { + throw typeError({ expect: 'Uint8Array', actual: input }).error + } + } + + /** + * @param {Uint8Array} output + */ + decode(output) { + if (output instanceof Uint8Array) { + return output + } else { + throw typeError({ expect: 'Uint8Array', actual: output }).error + } + } + /** + * @param {Uint8Array} input + * @returns {Schema.ReadResult} + */ + readWith(input) { + if (input instanceof Uint8Array) { + return { ok: input } + } else { + return typeError({ expect: 'Uint8Array', actual: input }) + } + } +} + +/** @type {Schema.BytesSchema} */ +export const Bytes = new RawBytes() + +/** + * @template {Schema.BlockCodec} [Codec=import('multiformats/codecs/raw')] + * @param {Codec} [codec] + * @returns {Schema.BytesSchema & ({} | null), Schema.MulticodecCode>} + */ +export const bytes = codec => + /** @type {*} */ (codec ? new ByteView(codec) : Bytes) + +/** + * @template {{}|null} Out + * @template {Schema.MulticodecCode} Code + * @extends {API, Schema.BlockCodec>} + * @implements {Schema.BytesSchema} + */ +class ByteView extends API { + get codec() { + return this.settings + } + get name() { + return this.codec.name + } + get code() { + return this.codec.code + } + /** + * @param {Out} data + */ + encode(data) { + return this.codec.encode(data) + } + /** + * @param {Schema.ByteView} bytes + */ + decode(bytes) { + return this.codec.decode(bytes) + } + /** + * @param {Uint8Array} input + * @param {Schema.BlockCodec} codec + * @returns {Schema.ReadResult} + */ + readWith(input, codec) { + try { + return { ok: codec.decode(input) } + } catch (cause) { + return { error: /** @type {Error} */ (cause) } + } + } + /** + * + * @param {Out} output + * @param {Schema.BlockCodec} codec + */ + writeWith(output, codec) { + try { + return { ok: codec.encode(output) } + } catch (cause) { + return { error: /** @type {Error} */ (cause) } + } + } } /** @@ -1423,19 +1756,20 @@ export const struct = fields => { */ /** - * @template {unknown} T + * @template {unknown} Out + * @template {unknown} In * @template {number} Code * @template {number} Alg * @template {1|0} Version - * @extends {API, unknown, LinkSettings>} - * @implements {Schema.LinkSchema} + * @extends {API, In, LinkSettings>} + * @implements {Schema.LinkSchema} */ class LinkSchema extends API { /** * * @param {unknown} cid - * @param {LinkSettings} settings - * @returns {Schema.ReadResult>} + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} */ readWith(cid, { code, multihash = {}, version }) { if (cid == null) { @@ -1478,7 +1812,7 @@ class LinkSchema extends API { } return { - ok: /** @type {Schema.Link} */ (cid), + ok: /** @type {Schema.Link} */ (cid), } } } @@ -1498,34 +1832,33 @@ class LinkSchema extends API { */ parse(input, base) { const link = parseLink(input, base) - return this.from(link) + return this.from(/** @type {*} */ (link)) } } -/** @type {Schema.LinkSchema} */ +/** @type {Schema.LinkSchema, number, number, 0|1>} */ export const Link = new LinkSchema({}) /** * @template {number} Code * @template {number} Alg * @template {1|0} Version - * @template {unknown} T - * @param {LinkSettings} options - * @returns {Schema.LinkSchema} + * @template {unknown} Out + * @param {LinkSettings} options + * @returns {Schema.LinkSchema | Schema.IPLDView, Code, Alg, Version>} */ export const link = (options = {}) => new LinkSchema(options) /** - * @template {Schema.VariantChoices} U - * @template [I=unknown] - * @extends {API, I, U>} - * @implements {Schema.VariantSchema} + * @template {Schema.VariantChoices} Choices + * @extends {API, Schema.InferVariantInput, Choices>} + * @implements {Schema.VariantSchema} */ class Variant extends API { /** - * @param {I} input - * @param {U} variants - * @returns {Schema.ReadResult>} + * @param {Schema.InferVariantInput} input + * @param {Choices} variants + * @returns {Schema.ReadResult>} */ readWith(input, variants) { if (typeof input != 'object' || input === null || Array.isArray(input)) { @@ -1546,12 +1879,16 @@ class Variant extends API { const result = reader.read(input[key]) return result.error ? memberError({ at: key, cause: result.error }) - : { ok: /** @type {Schema.InferVariant} */ ({ [key]: result.ok }) } + : { + ok: /** @type {Schema.InferVariant} */ ({ + [key]: result.ok, + }), + } } else if (variants._) { const result = variants._.read(input) return result.error ? result - : { ok: /** @type {Schema.InferVariant} */ ({ _: result.ok }) } + : { ok: /** @type {Schema.InferVariant} */ ({ _: result.ok }) } } else if (key) { return error( `Expected an object with one of the these keys: ${Object.keys(variants) @@ -1568,11 +1905,11 @@ class Variant extends API { /** * @template [E=never] - * @param {I} input + * @param {unknown} input * @param {E} [fallback] */ match(input, fallback) { - const result = this.read(input) + const result = this.read(/** @type {*} */ (input)) if (result.error) { if (fallback !== undefined) { return [null, fallback] @@ -1582,17 +1919,17 @@ class Variant extends API { } else { const [key] = Object.keys(result.ok) const value = result.ok[key] - return /** @type {any} */ ([key, value]) + return /** @type {*} */ ([key, value]) } } /** - * @template {Schema.InferVariant} O - * @param {O} source - * @returns {O} + * @template {Schema.InferVariant} Choice + * @param {Choice} source + * @returns {Choice} */ create(source) { - return /** @type {O} */ (this.from(source)) + return /** @type {Choice} */ (this.from(/** @type {*} */ (source))) } } @@ -1648,9 +1985,8 @@ class Variant extends API { * ``` * * @template {Schema.VariantChoices} Choices - * @template [In=unknown] * @param {Choices} variants - * @returns {Schema.VariantSchema} + * @returns {Schema.VariantSchema} */ export const variant = variants => new Variant(variants) @@ -1711,10 +2047,11 @@ const displayTypeName = value => { // eg turn NaN and Infinity to null case 'bigint': return `${value}n` - case 'number': case 'symbol': + return /** @type {symbol} */ (value).toString() + case 'number': case 'undefined': - return String(value) + return `${value}` case 'object': return value === null ? 'null' : Array.isArray(value) ? 'array' : 'object' default: diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index bfb2004c..5e2cdd05 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -34,32 +34,46 @@ export type { IPLDView, IPLDViewBuilder, BuildOptions, + Phantom, } export interface Reader { read(input: I): Result } +export interface Writer { + write(output: O): Result +} + export type { Error, Result } export type ReadResult = Result +export interface Convert + extends Reader, + Writer {} + export interface Schema< - O extends unknown = unknown, - I extends unknown = unknown -> extends Reader { - optional(): Schema - nullable(): Schema - array(): Schema - default(value: NotUndefined): DefaultSchema, I> - or(other: Reader): Schema - and(other: Reader): Schema - refine(schema: Reader): Schema - - brand(kind?: K): Schema, I> - - is(value: unknown): value is O - from(value: I): O + Out extends unknown = unknown, + In extends unknown = unknown +> extends Convert { + optional(): Schema + nullable(): Schema + + implicit(value: Exclude): ImplicitSchema + + array(): ArraySchema + or(other: Convert): Schema + and(other: Convert): Schema + refine(schema: Convert): Schema + + brand(kind?: K): Schema, In> + + is(value: unknown): value is Out + from(value: In): Out + to(value: Out): In + + // conforms(value: unknown): value is Out link< Code extends MulticodecCode, @@ -69,34 +83,132 @@ export interface Schema< codec?: BlockCodec version?: V hasher?: MultihashHasher - }): LinkSchema + }): LinkSchema | IPLDView, Code, Alg, V> - codec: BlockCodec - hasher: MultihashHasher + // attachment< + // Code extends MulticodecCode, + // Alg extends MulticodecCode, + // V extends UnknownLink['version'] + // >(options?: { + // codec?: BlockCodec + // version?: V + // hasher?: MultihashHasher + // }): AttachmentSchema - toIPLDBuilder(input: I): Result>, Error> + // codec: BlockCodec + // hasher: MultihashHasher - toIPLDView(source: { - link: Link - store: BlockStore - }): ReturnType + // toIPLDBuilder(input: I): Result>, Error> + + // toIPLDView(source: { + // link: Link + // store: BlockStore + // }): ReturnType + + // createIPLDView(source: IPLDViewSource): Result, Error> - createIPLDView(source: IPLDViewSource): Result, Error> + // view(value: O): View + // attach(value: O): Await> + + // compile(value: I): Await>> + // attachment(value: I): Await> + + // IN: I + // OUT: O +} + +export interface PropertySchema {} + +export interface BytesSchema< + Out extends {} | null = Uint8Array, + Code extends MulticodecCode = MulticodecCode<0x55, 'raw'> +> extends Schema { + code: Code + name: string + encode(value: Out): ByteView + decode(value: ByteView): Out +} + +export type ToBlock = { + [K in keyof T]: T[K] extends ResolvedLink< + infer O, + infer Code, + infer Alg, + infer V + > + ? Linked + : T[K] } +export interface Linked< + T extends unknown = unknown, + Code extends MulticodecCode = MulticodecCode, + Alg extends MulticodecCode = MulticodecCode, + V extends UnknownLink['version'] = 1 +> extends Link { + resolve(): ResolvedLink +} + +export interface CompiledView extends IPLDView { + root: Required> +} + +type ResolvedLink< + T extends unknown = unknown, + Code extends MulticodecCode = MulticodecCode, + Alg extends MulticodecCode = MulticodecCode, + V extends UnknownLink['version'] = 1 +> = T & Phantom> + +// export interface View { +// valueOf(): O +// attach(): Await> +// } + +// export interface AttachmentSchema< +// T extends unknown = unknown, +// Code extends MulticodecCode = MulticodecCode, +// Alg extends MulticodecCode = MulticodecCode, +// V extends UnknownLink['version'] = 1 +// > extends Schema> { +// attach(source: T): Await> +// } + +// export interface Attachment< +// T extends unknown = unknown, +// Code extends MulticodecCode = MulticodecCode, +// Alg extends MulticodecCode = MulticodecCode, +// V extends UnknownLink['version'] = 1 +// > extends IPLDView { +// load(): T +// link(): Link +// } + export interface LinkSchema< - O extends unknown, + Out extends unknown, + In extends unknown, Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema> { +> extends Schema, In> { link(): never + + // attach(): AttachmentSchema, Code, Alg, V> + // resolve(): Schema, O | IPLDView> parse( source: string, base?: MultibaseDecoder - ): Link + ): Link } +export interface AttachmentSchema< + O extends unknown, + I extends unknown, + Code extends MulticodecCode, + Alg extends MulticodecCode, + V extends UnknownLink['version'] +> extends Schema, I> {} + export interface IPLDViewSource { root: Block store: BlockStore @@ -110,18 +222,17 @@ export interface CreateView { }): IPLDView & V } -export interface DefaultSchema< - O extends unknown = unknown, - I extends unknown = unknown -> extends Schema { - readonly value: O & NotUndefined - optional(): DefaultSchema, I> +export interface ImplicitSchema + extends Schema, I | undefined> { + readonly value: Exclude + optional(): ImplicitSchema + // nullable(): ImplicitSchema } export type NotUndefined = Exclude -export interface ArraySchema extends Schema { - element: Reader +export interface ArraySchema extends Schema { + element: Convert } /** @@ -132,20 +243,20 @@ export interface ArraySchema extends Schema { */ export interface MapRepresentation< V extends Record, - I = unknown -> extends Schema { + U extends Record +> extends Schema { /** * Returns equivalent schema in which all of the fields are optional. */ - partial(): MapRepresentation, I> + partial(): MapRepresentation, Partial> } -export interface DictionarySchema - extends MapRepresentation, I> { +export interface DictionarySchema + extends MapRepresentation, Dictionary> { key: Reader - value: Reader + value: Convert - partial(): DictionarySchema + partial(): DictionarySchema } export type Dictionary< @@ -156,39 +267,52 @@ export type Dictionary< } export interface LiteralSchema< - T extends string | number | boolean | null, - I = unknown -> extends Schema { - default(value?: T): DefaultSchema, I> - readonly value: T + Out extends string | number | boolean | null, + In extends string | number | boolean | null = Out +> extends Schema { + implicit(value?: Out): ImplicitSchema + readonly value: Out } export interface NumberSchema< - N extends number = number, - I extends unknown = unknown -> extends Schema { - greaterThan(n: number): NumberSchema - lessThan(n: number): NumberSchema + Out extends number = number, + In extends number = number +> extends Schema { + isInteger: typeof Number.isInteger + isFinite: typeof Number.isFinite + greaterThan(n: number): NumberSchema + lessThan(n: number): NumberSchema + + optional(): Schema + + refine(convert: Convert): NumberSchema + constraint(check: (value: Out) => ReadResult): NumberSchema +} - refine(schema: Reader): NumberSchema +export interface StructMembers { + [key: string]: Convert } -export interface StructSchema< - U extends { [key: string]: Reader } = {}, - I extends unknown = unknown -> extends MapRepresentation, I> { - shape: U +export interface StructSchema + extends MapRepresentation, InferStructInput> { + shape: Members - create(input: MarkEmptyOptional>): InferStruct - extend( - extension: E - ): StructSchema + create( + input: MarkEmptyOptional> + ): InferStruct + extend(extension: E): StructSchema - partial(): MapRepresentation>, I> & StructSchema + partial(): MapRepresentation< + Partial>, + Partial> + > & + StructSchema - createIPLDView( - source: IPLDViewSource - ): Result> & InferStruct, Error> + // createIPLDView( + // source: IPLDViewSource + // ): Result> & InferStruct, Error> + + // view(value: InferStruct): InferStruct & IPLDView> } /** @@ -210,10 +334,8 @@ export interface StructSchema< * }) * ``` */ -export interface VariantSchema< - Choices extends VariantChoices = {}, - In extends unknown = unknown -> extends Schema, In> { +export interface VariantSchema + extends Schema, InferVariantInput> { /** * Function can be used to match the input against the variant schema to * return the matched branch name and corresponding value. It provides @@ -254,7 +376,7 @@ export interface VariantSchema< * their respective schemas. */ export interface VariantChoices { - [branch: string]: Reader + [branch: string]: Convert } /** @@ -268,6 +390,14 @@ export type InferVariant = { } }[keyof Choices] +export type InferVariantInput = { + [Case in keyof Choices]: { + [Key in Exclude]?: never + } & { + [Key in Case]: Choices[Case] extends Writer ? T : never + } +}[keyof Choices] + /** * Utility type for inferring the result of calling `match` on the {@link VariantSchema}. * It derives a tuple of the branch name and the value of the branch allowing @@ -287,15 +417,18 @@ export type InferOptionalReader = R extends Reader ? Reader : R -export interface StringSchema - extends Schema { +export interface StringSchema + extends Schema { + refine( + convert: Convert + ): StringSchema + constraint(check: (value: Out) => ReadResult): StringSchema startsWith( prefix: Prefix - ): StringSchema + ): StringSchema endsWith( suffix: Suffix - ): StringSchema - refine(schema: Reader): StringSchema + ): StringSchema } declare const Marker: unique symbol @@ -307,6 +440,9 @@ export type Integer = number & Phantom<{ typeof: 'integer' }> export type Float = number & Phantom<{ typeof: 'float' }> export type Infer = T extends Reader ? T : never +export type InferInput = T extends Writer + ? T + : never export type InferIntersection = { [K in keyof U]: (input: Infer) => void @@ -314,14 +450,31 @@ export type InferIntersection = { ? T : never +export type InferIntersectionInput = { + [K in keyof U]: (input: InferInput) => void +}[number] extends (input: infer T) => void + ? T + : never + export type InferUnion = Infer +export type InferUnionInput = InferInput< + U[number] +> export type InferTuple = { [K in keyof U]: Infer } -export type InferStruct = MarkOptionals<{ - [K in keyof U]: Infer +export type InferTupleInput = { + [K in keyof U]: InferInput +} + +export type InferStruct = MarkOptionals<{ + [K in keyof Shape]: Infer +}> + +export type InferStructInput = MarkOptionals<{ + [K in keyof Shape]: InferInput }> export type InferStructSource = @@ -331,7 +484,10 @@ export type InferStructSource = }> // > -export type InferSource = U extends DefaultSchema +export type InferSource = U extends ImplicitSchema< + infer T, + any +> ? T | undefined : U extends StructSchema ? InferStructSource diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js new file mode 100644 index 00000000..53f08c0d --- /dev/null +++ b/packages/core/test/ipld-schema.spec.js @@ -0,0 +1,61 @@ +import * as Schema from '../src/schema.js' +import { CBOR } from '../src/dag.js' +import { test, assert } from './test.js' + +describe.only('IPLD Schema', () => { + test('cross block references', async () => { + const Deal = Schema.struct({ + size: Schema.integer(), + commit: Schema.string(), + }) + Deal.IN + + const DL = Schema.bytes().encoding(CBOR).refine(Deal) + DL.IN + DL.OUT + + const int = Schema.bytes().encoding(CBOR).refine(Schema.integer()) + int.IN + int.OUT + + int.from(new Uint8Array()) + + const a = {} + + const out = await Deal.compile({ + size: 2, + commit: 'a', + }) + + const Offer = Deal.array() + Deal.link().attach().OUT + + const Aggregate = Schema.struct({ + offer: Offer.link().resolve(), + attachment: Offer.link().attach(), + }) + + Offer.link().IN + Offer.link().attach().IN + + const offer = Offer.from([ + { size: 1, commit: 'a' }, + { size: 2, commit: 'b' }, + ]) + + Aggregate.IN.attachment + + const attachment = await Offer.attachment(offer) + const aggregate = await Aggregate.compile({ + offer, //: await Offer.attach(offer) + attachment, + }) + + const agg = aggregate.root.data + + agg.attachment.resolve()[0].commit + + aggregate.root.data.offer.resolve()[0].commit + // const out = aggregate.offer.load() + }) +}) diff --git a/packages/core/test/schema.spec.js b/packages/core/test/schema.spec.js index 989b170c..6226e2b1 100644 --- a/packages/core/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -9,6 +9,7 @@ for (const { input, schema, expect, inputLabel, skip, only } of fixtures()) { const result = schema.read(input) if (expect.error) { + // console.log(`${schema}.read(${inputLabel})`, result, expect.error) matchError(result, expect.error) } else { assert.deepEqual( @@ -216,17 +217,17 @@ test('string().refine', () => { }) }) -test('never().default()', () => { +test.skip('never().default()', () => { assert.throws( () => Schema.never() // @ts-expect-error - no value satisfies default - .default('hello'), + .implicit('hello'), /Expected value of type never instead got "hello"/ ) }) -test('literal("foo").default("bar") throws', () => { +test.skip('literal("foo").default("bar") throws', () => { assert.throws( () => Schema.literal('foo') @@ -428,7 +429,7 @@ test('tuple', () => { ) matchError( schema.read(['0', '1']), - /invalid element at 1.*expect.*number.*got "1"/is + /invalid element at 1.*expect.*integer.*got "1"/is ) matchError( schema.read(['0', Infinity]), @@ -580,10 +581,10 @@ test('.default("one").default("two")', () => { assert.deepEqual(schema.read('three'), { ok: 'three' }) }) -test('default throws on invalid default', () => { +test.skip('default throws on invalid default', () => { assert.throws( () => - Schema.string().default( + Schema.string().implicit( // @ts-expect-error - number is not assignable to string 101 ), diff --git a/packages/core/test/schema/fixtures.js b/packages/core/test/schema/fixtures.js index 2c8aa303..2b2b2ef9 100644 --- a/packages/core/test/schema/fixtures.js +++ b/packages/core/test/schema/fixtures.js @@ -84,8 +84,8 @@ export const fixture = ({ in: input, got = input, array, ...expect }) => ({ ...expect.startsWithHelloEndsWithWorld, }, number: { any: fail({ expect: 'number', got }), ...expect.number }, - integer: { any: fail({ expect: 'number', got }), ...expect.integer }, - float: { any: fail({ expect: 'number', got }), ...expect.float }, + integer: { any: fail({ expect: 'integer', got }), ...expect.integer }, + float: { any: fail({ expect: 'float', got }), ...expect.float }, literal: { any: { any: fail({ expect: 'literal', got }) }, ...Object.fromEntries( @@ -188,10 +188,10 @@ export const source = [ any: fail.at('"name"', { expect: '"Point2d"', got: 'undefined' }), }, xyz: { - any: fail.at('"x"', { expect: 'number', got: 'undefined' }), + any: fail.at('"x"', { expect: 'integer', got: 'undefined' }), }, intDict: { - any: fail.at('"0"', { expect: 'number', got: '"h"' }), + any: fail.at('"0"', { expect: 'integer', got: '"h"' }), }, pointDict: { any: fail.at('0', { expect: 'name|x|y', got: '"0"' }), @@ -344,7 +344,7 @@ export const source = [ any: fail.at('"name"', { expect: '"Point2d"', got: 'undefined' }), }, xyz: { - any: fail.at('"x"', { expect: 'number', got: 'undefined' }), + any: fail.at('"x"', { expect: 'integer', got: 'undefined' }), }, dict: { any: pass(), @@ -428,7 +428,7 @@ export const source = [ unknown: { any: pass() }, tuple: { strNfloat: { - any: fail.at(1, { expect: 'number', got: '"world"' }), + any: fail.at(1, { expect: 'float', got: '"world"' }), }, strNstr: { any: pass(), @@ -465,7 +465,7 @@ export const source = [ any: fail.at(1, { got: 'object' }), }, strNfloat: { - any: fail.at(1, { got: 'object', expect: 'number' }), + any: fail.at(1, { got: 'object', expect: 'float' }), }, }, }, @@ -552,10 +552,10 @@ export const source = [ any: pass(), }, xyz: { - any: fail.at('"z"', { expect: 'number', got: 'undefined' }), + any: fail.at('"z"', { expect: 'integer', got: 'undefined' }), }, intDict: { - any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + any: fail.at('"name"', { expect: 'integer', got: '"Point2d"' }), }, dict: { any: pass(), @@ -565,16 +565,16 @@ export const source = [ in: { name: 'Point2d', x: 0, z: 0 }, got: 'object', point2d: { - any: fail.at('"y"', { expect: 'number', got: 'undefined' }), + any: fail.at('"y"', { expect: 'integer', got: 'undefined' }), }, unknown: { any: pass(), }, xyz: { - any: fail.at('"y"', { expect: 'number', got: 'undefined' }), + any: fail.at('"y"', { expect: 'integer', got: 'undefined' }), }, intDict: { - any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + any: fail.at('"name"', { expect: 'integer', got: '"Point2d"' }), }, pointDict: { any: fail.at('z', { expect: 'name|x|y', got: '"z"' }), @@ -596,7 +596,7 @@ export const source = [ any: pass(), }, intDict: { - any: fail.at('"name"', { expect: 'number', got: '"Point2d"' }), + any: fail.at('"name"', { expect: 'integer', got: '"Point2d"' }), }, dict: { any: pass(), @@ -635,7 +635,7 @@ export const scenarios = fixture => [ expect: fixture.unknown.any || fixture.any, }, { - schema: Schema.unknown().default('DEFAULT'), + schema: Schema.unknown().implicit('DEFAULT'), expect: (fixture.unknown.default && fixture.unknown.default('DEFAULT')) || fixture.unknown.any || @@ -654,7 +654,7 @@ export const scenarios = fixture => [ expect: fixture.string.nullable || fixture.string.any || fixture.any, }, { - schema: Schema.string().default('DEFAULT'), + schema: Schema.string().implicit('DEFAULT'), expect: (fixture.string.default && fixture.string.default('DEFAULT')) || fixture.string.any || @@ -692,7 +692,7 @@ export const scenarios = fixture => [ expect: fixture.number.nullable || fixture.number.any || fixture.any, }, { - schema: Schema.number().default(17), + schema: Schema.number().implicit(17), expect: (fixture.number.default && fixture.number.default(17)) || fixture.number.any || @@ -711,7 +711,7 @@ export const scenarios = fixture => [ expect: fixture.integer.nullable || fixture.integer.any || fixture.any, }, { - schema: Schema.integer().default(17), + schema: Schema.integer().implicit(17), expect: (fixture.integer.default && fixture.integer.default(17)) || fixture.integer.any || @@ -761,7 +761,7 @@ export const scenarios = fixture => [ fixture.any, }, { - schema: Schema.array(Schema.string().default('DEFAULT')), + schema: Schema.array(Schema.string().implicit('DEFAULT')), expect: (fixture.array.string?.default && fixture.array.string?.default('DEFAULT')) || @@ -796,7 +796,7 @@ export const scenarios = fixture => [ fixture.any, }, { - schema: Schema.literal('hello').default('hello'), + schema: Schema.literal('hello').implicit('hello'), expect: (fixture.literal?.hello?.default && fixture.literal?.hello?.default('hello')) || @@ -837,7 +837,7 @@ export const scenarios = fixture => [ fixture.any, }, { - schema: Schema.string().or(Schema.number()).default(10), + schema: Schema.string().or(Schema.number()).implicit(10), expect: (fixture.stringOrNumber?.default && fixture.stringOrNumber?.default(10)) || @@ -846,7 +846,7 @@ export const scenarios = fixture => [ fixture.any, }, { - schema: Schema.string().or(Schema.number()).default('test'), + schema: Schema.string().or(Schema.number()).implicit('test'), expect: (fixture.stringOrNumber?.default && fixture.stringOrNumber?.default('test')) || diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index c89dbc04..2b1cbba1 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -49,6 +49,7 @@ import { } from './capability.js' import type * as Transport from './transport.js' import type { Tuple, Block } from './transport.js' +import { LegacyLink } from 'multiformats' export * from './capability.js' export * from './transport.js' export type { @@ -192,7 +193,7 @@ export interface IPLDView { */ root: Block - link(): Link + link(): Link /** * Returns an iterable of all the IPLD blocks that are included in this view. From 4ffb13f9d2514d68d173dfc0d0c30d97cf244a47 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 3 May 2023 22:54:58 -0700 Subject: [PATCH 06/16] implement pipe --- packages/core/src/schema/schema.js | 57 ++++++++++++++++++++ packages/core/src/schema/type.ts | 11 ++-- packages/core/test/ipld-schema.spec.js | 72 +++++++++++++------------- packages/core/test/schema.spec.js | 22 ++++---- 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index b7d29487..ce2bca02 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -163,6 +163,15 @@ export class API { return refine(this, schema) } + /** + * @template O + * @param {Schema.Convert} to + * @returns {Schema.Schema} + */ + pipe(to) { + return pipe(this, to) + } + /** * @template {string} Kind * @param {Kind} [kind] @@ -1420,6 +1429,45 @@ class Refine extends API { */ export const refine = (base, schema) => new Refine({ base, schema }) +/** + * @template Into + * @template Out + * @template In + * @extends {API, to: Schema.Convert }>} + * @implements {Schema.Schema} + */ +class Pipe extends API { + /** + * @param {In} input + * @param {{ from: Schema.Convert, to: Schema.Convert }} settings + */ + readWith(input, { from, to }) { + const result = from.read(input) + return result.error ? result : to.read(result.ok) + } + /** + * @param {Into} output + * @param {{ from: Schema.Convert, to: Schema.Convert }} settings + */ + writeWith(output, { from, to }) { + const result = to.write(output) + return result.error ? result : from.write(result.ok) + } + toString() { + return `${this.settings.from}.pipe(${this.settings.to})` + } +} + +/** + * @template Into + * @template Out + * @template In + * @param {Schema.Convert} from + * @param {Schema.Convert} to + * @returns {Schema.Schema} + */ +export const pipe = (from, to) => new Pipe({ from, to }) + /** * @template {null|boolean|string|number} Out * @template {null|boolean|string|number} In @@ -2182,3 +2230,12 @@ const indent = (message, indent = ' ') => * @param {string} message */ const li = message => indent(`- ${message}`) + +/** + * @template In, Out + * @param {Schema.Schema} schema + * @returns {{in:In, out:Out}} + */ +export const debug = schema => { + throw new Error('Not implemented') +} diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 5e2cdd05..5cca144f 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -65,7 +65,10 @@ export interface Schema< array(): ArraySchema or(other: Convert): Schema and(other: Convert): Schema - refine(schema: Convert): Schema + refine(schema: Reader): Schema + + // into(schema: Convert): Schema + pipe(schema: Convert): Schema brand(kind?: K): Schema, In> @@ -114,7 +117,7 @@ export interface Schema< // attachment(value: I): Await> // IN: I - // OUT: O + // OUT: Out } export interface PropertySchema {} @@ -285,7 +288,7 @@ export interface NumberSchema< optional(): Schema - refine(convert: Convert): NumberSchema + refine(convert: Reader): NumberSchema constraint(check: (value: Out) => ReadResult): NumberSchema } @@ -420,7 +423,7 @@ export type InferOptionalReader = R extends Reader export interface StringSchema extends Schema { refine( - convert: Convert + convert: Reader ): StringSchema constraint(check: (value: Out) => ReadResult): StringSchema startsWith( diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index 53f08c0d..6c264562 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -8,54 +8,56 @@ describe.only('IPLD Schema', () => { size: Schema.integer(), commit: Schema.string(), }) - Deal.IN - const DL = Schema.bytes().encoding(CBOR).refine(Deal) - DL.IN - DL.OUT + const cbor = Schema.bytes(CBOR) - const int = Schema.bytes().encoding(CBOR).refine(Schema.integer()) - int.IN - int.OUT + Schema.debug(cbor) - int.from(new Uint8Array()) + const thing = Schema.dictionary({ value: Schema.unknown() }) - const a = {} + const DL = Schema.bytes(CBOR) - const out = await Deal.compile({ - size: 2, - commit: 'a', - }) + const DL2 = DL.pipe(Deal) - const Offer = Deal.array() - Deal.link().attach().OUT + const dl2 = DL2.from(new Uint8Array()) + dl2.size - const Aggregate = Schema.struct({ - offer: Offer.link().resolve(), - attachment: Offer.link().attach(), - }) + // const a = {} - Offer.link().IN - Offer.link().attach().IN + // const out = await Deal.compile({ + // size: 2, + // commit: 'a', + // }) - const offer = Offer.from([ - { size: 1, commit: 'a' }, - { size: 2, commit: 'b' }, - ]) + // const Offer = Deal.array() + // Deal.link().attach().OUT - Aggregate.IN.attachment + // const Aggregate = Schema.struct({ + // offer: Offer.link().resolve(), + // attachment: Offer.link().attach(), + // }) - const attachment = await Offer.attachment(offer) - const aggregate = await Aggregate.compile({ - offer, //: await Offer.attach(offer) - attachment, - }) + // Offer.link().IN + // Offer.link().attach().IN + + // const offer = Offer.from([ + // { size: 1, commit: 'a' }, + // { size: 2, commit: 'b' }, + // ]) + + // Aggregate.IN.attachment + + // const attachment = await Offer.attachment(offer) + // const aggregate = await Aggregate.compile({ + // offer, //: await Offer.attach(offer) + // attachment, + // }) - const agg = aggregate.root.data + // const agg = aggregate.root.data - agg.attachment.resolve()[0].commit + // agg.attachment.resolve()[0].commit - aggregate.root.data.offer.resolve()[0].commit - // const out = aggregate.offer.load() + // aggregate.root.data.offer.resolve()[0].commit + // // const out = aggregate.offer.load() }) }) diff --git a/packages/core/test/schema.spec.js b/packages/core/test/schema.spec.js index 6226e2b1..4517c4a4 100644 --- a/packages/core/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -238,7 +238,7 @@ test.skip('literal("foo").default("bar") throws', () => { }) test('default on literal has default', () => { - const schema = Schema.literal('foo').default() + const schema = Schema.literal('foo').implicit() assert.deepEqual(schema.read(undefined), Schema.ok('foo')) }) @@ -248,7 +248,7 @@ test('literal has value field', () => { }) test('.default().optional() is noop', () => { - const schema = Schema.string().default('hello') + const schema = Schema.string().implicit('hello') assert.equal(schema.optional(), schema) }) @@ -318,8 +318,8 @@ test('struct', () => { test('struct with defaults', () => { const Point = Schema.struct({ - x: Schema.number().default(0), - y: Schema.number().default(0), + x: Schema.number().implicit(0), + y: Schema.number().implicit(0), }) assert.deepEqual(Point.read({}), { ok: { x: 0, y: 0 } }) @@ -565,7 +565,7 @@ test('optional.default removes undefined from type', () => { // @ts-expect-error - Schema is not assignable const castError = schema1 - const schema2 = schema1.default('') + const schema2 = schema1.implicit('') /** @type {Schema.Schema} */ const castOk = schema2 @@ -574,7 +574,7 @@ test('optional.default removes undefined from type', () => { }) test('.default("one").default("two")', () => { - const schema = Schema.string().default('one').default('two') + const schema = Schema.string().implicit('one').implicit('two') assert.equal(schema.value, 'two') assert.deepEqual(schema.read(undefined), { ok: 'two' }) @@ -594,7 +594,7 @@ test.skip('default throws on invalid default', () => { test('unknown with default', () => { assert.throws( - () => Schema.unknown().default(undefined), + () => Schema.unknown().implicit(undefined), /undefined is not a valid default/ ) }) @@ -607,12 +607,12 @@ test('default swaps undefined even if decodes to undefined', () => { }, }) - assert.deepEqual(schema.default('X').read(null), { ok: 'X' }) + assert.deepEqual(schema.implicit('X').read(null), { ok: 'X' }) }) test('record defaults', () => { const Point = Schema.struct({ - x: Schema.integer().default(1), + x: Schema.integer().implicit(1), y: Schema.integer().optional(), }) @@ -649,8 +649,8 @@ test('record defaults', () => { }) const Line = Schema.struct({ - start: Point.default({ x: 0 }), - end: Point.default({ x: 1, y: 3 }), + start: Point.implicit({ x: 0 }), + end: Point.implicit({ x: 1, y: 3 }), }) assert.deepEqual(Line.create(), { From b77f019b5052389b7522d42888f11e7372fd62a9 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Fri, 5 May 2023 16:03:44 -0700 Subject: [PATCH 07/16] feat: implement resolvable links --- packages/core/src/schema/schema.js | 312 ++++++++++++++++++------- packages/core/src/schema/type.ts | 52 +++-- packages/core/test/ipld-schema.spec.js | 37 ++- 3 files changed, 301 insertions(+), 100 deletions(-) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index ce2bca02..71a80713 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -156,7 +156,8 @@ export class API { /** * @template {Out} O - * @param {Schema.Reader} schema + * @template {Out} I + * @param {Schema.Convert} schema * @returns {Schema.Schema} */ refine(schema) { @@ -203,6 +204,10 @@ export class API { return schema } + toSchema() { + return this + } + // /** // * @param {In} input // * @returns {Schema.Result>, Schema.Error>} @@ -451,8 +456,8 @@ export const nullable = schema => new Nullable(schema) /** * @template O - * @template [I=unknown] - * @extends {API>} + * @template I + * @extends {API>} * @implements {Schema.Schema} */ class Optional extends API { @@ -500,10 +505,10 @@ export const optional = schema => new Optional(schema) */ class Implicit extends API { /** - * @returns {Schema.ImplicitSchema} + * @returns {*} */ optional() { - return this + return /** @type {*} */ (this) } /** @@ -573,10 +578,9 @@ export const implicit = (schema, value) => { throw new Error(`Value of type undefined is not a valid default`) } - const implicit = - /** @type {Schema.ImplicitSchema, I>} */ ( - new Implicit({ convert: schema, value }) - ) + const implicit = /** @type {Schema.ImplicitSchema} */ ( + new Implicit({ convert: schema, value }) + ) return implicit } @@ -801,17 +805,13 @@ class Dictionary extends API { return this.settings.value } - /** - * - * @returns {Schema.DictionarySchema} - */ partial() { const { key, value } = this.settings const partial = new Dictionary({ key, value: optional(value), }) - return partial + return /** @type {*} */ (partial) } toString() { return `dictionary(${this.settings})` @@ -1067,19 +1067,19 @@ export const Boolean = new Scalar({ export const boolean = () => Boolean /** - * @template {number} O - * @template {number} I + * @template {number} Out + * @template {number} In * @template [Settings=void] - * @extends {API} - * @implements {Schema.NumberSchema} + * @extends {API} + * @implements {Schema.NumberSchema} */ class NumberSchema extends API { isInteger = globalThis.Number.isInteger isFinite = globalThis.Number.isFinite /** - * @param {(input: O) => Schema.ReadResult} check - * @returns {Schema.NumberSchema} + * @param {(input: Out) => Schema.ReadResult} check + * @returns {Schema.NumberSchema} */ constraint(check) { return this.refine({ @@ -1114,18 +1114,19 @@ class NumberSchema extends API { } /** - * @template {O} Into - * @param {Schema.Convert} convert - * @returns {Schema.NumberSchema} + * @template {Out} O + * @template {Out} I + * @param {Schema.Convert} convert + * @returns {Schema.NumberSchema} */ refine(convert) { return new RefinedNumber({ schema: this, refine: convert }) } /** - * @param {I} input + * @param {In} input * @param {Settings} settings - * @returns {Schema.ReadResult} + * @returns {Schema.ReadResult} */ readWith(input, settings) { return typeof input === 'number' @@ -1144,24 +1145,25 @@ export const number = () => Number /** * @template {number} Out * @template {number} In - * @template {Out} Into - * @extends {NumberSchema, refine:Schema.Reader}>} - * @implements {Schema.NumberSchema} + * @template {Out} O + * @template {Out} I + * @extends {NumberSchema, refine:Schema.Reader}>} + * @implements {Schema.NumberSchema} */ class RefinedNumber extends NumberSchema { /** * @param {In} input - * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings - * @returns {Schema.ReadResult} + * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings + * @returns {Schema.ReadResult} */ readWith(input, { schema, refine }) { const result = schema.read(input) - return result.error ? result : refine.read(result.ok) + return result.error ? result : refine.read(/** @type {I} */ (result.ok)) } /** - * @param {Into} output - * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings + * @param {O} output + * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings */ writeWith(output, { schema, refine }) { const result = refine.write(output) @@ -1241,9 +1243,10 @@ class StringSchema extends API { } /** - * @template {Out} Onto - * @param {Schema.Convert} schema - * @returns {Schema.StringSchema} + * @template {Out} O + * @template {Out} I + * @param {Schema.Convert} schema + * @returns {Schema.StringSchema} */ refine(schema) { const refined = new RefinedString({ @@ -1251,7 +1254,7 @@ class StringSchema extends API { schema, }) - return /** @type {Schema.StringSchema} */ (refined) + return /** @type {Schema.StringSchema} */ (refined) } /** @@ -1269,14 +1272,25 @@ class StringSchema extends API { * @param {Prefix} prefix */ startsWith(prefix) { - return this.refine(startsWith(prefix)) + const constraint = + /** @type {Schema.Convert} */ ( + startsWith(prefix) + ) + + return this.refine(constraint) } + /** * @template {string} Suffix * @param {Suffix} suffix */ endsWith(suffix) { - return this.refine(endsWith(suffix)) + const constraint = + /** @type {Schema.Convert} */ ( + endsWith(suffix) + ) + + return this.refine(constraint) } toString() { return `string()` @@ -1284,32 +1298,32 @@ class StringSchema extends API { } /** - * @template {string} Into * @template {string} Out * @template {string} In - * @extends {StringSchema, schema:Schema.Reader}>} - * @implements {Schema.StringSchema} + * @template {Out} O + * @template {Out} I + * @extends {StringSchema, schema:Schema.Convert}>} + * @implements {Schema.StringSchema} */ class RefinedString extends StringSchema { /** * @param {In} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings - * @returns {Schema.ReadResult} + * @param {{base:Schema.Reader, schema:Schema.Reader}} settings + * @returns {Schema.ReadResult} */ readWith(input, { base, schema }) { const result = base.read(input) - return result.error - ? result - : /** @type {Schema.ReadResult} */ (schema.read(result.ok)) + return result.error ? result : schema.read(/** @type {I} */ (result.ok)) } /** - * @param {Into & Out} output - * @param {{base:Schema.Writer, schema:Schema.Reader}} settings + * @param {O} output + * @param {{base:Schema.Writer, schema:Schema.Writer}} settings * @returns {Schema.ReadResult} */ writeWith(output, { base, schema }) { - return base.write(output) + const result = schema.write(output) + return result.error ? result : base.write(result.ok) } toString() { @@ -1353,7 +1367,7 @@ class StartsWith extends API { * @template {string} Prefix * @template {string} Input * @param {Prefix} prefix - * @returns {Schema.Schema<`${Prefix}${string}` & Input, Input>} + * @returns {Schema.Schema} */ export const startsWith = prefix => new StartsWith(prefix) @@ -1392,27 +1406,29 @@ export const endsWith = suffix => new EndsWith(suffix) /** * @template Out - * @template {Out} Into + * @template {Out} O + * @template {Out} I * @template In - * @extends {API, schema: Schema.Reader }>} - * @implements {Schema.Schema} + * @extends {API, schema: Schema.Convert }>} + * @implements {Schema.Schema} */ class Refine extends API { /** * @param {In} input - * @param {{ base: Schema.Convert, schema: Schema.Reader }} settings + * @param {{ base: Schema.Convert, schema: Schema.Convert }} settings */ readWith(input, { base, schema }) { const result = base.read(input) - return result.error ? result : schema.read(result.ok) + return result.error ? result : schema.read(/** @type {I} */ (result.ok)) } /** - * @param {Into & Out} output - * @param {{ base: Schema.Convert, schema: Schema.Reader }} settings + * @param {O} output + * @param {{ base: Schema.Convert, schema: Schema.Convert }} settings */ writeWith(output, { base, schema }) { - return base.write(output) + const result = schema.write(output) + return result.error ? result : base.write(/** @type {Out} */ (result.ok)) } toString() { return `${this.settings.base}.refine(${this.settings.schema})` @@ -1420,12 +1436,13 @@ class Refine extends API { } /** + * @template {Out} O + * @template {Out} I * @template Out - * @template {Out} Into * @template In * @param {Schema.Convert} base - * @param {Schema.Reader} schema - * @returns {Schema.Schema} + * @param {Schema.Convert} schema + * @returns {Schema.Schema} */ export const refine = (base, schema) => new Refine({ base, schema }) @@ -1433,7 +1450,7 @@ export const refine = (base, schema) => new Refine({ base, schema }) * @template Into * @template Out * @template In - * @extends {API, to: Schema.Convert }>} + * @extends {API, to: Schema.Convert }>} * @implements {Schema.Schema} */ class Pipe extends API { @@ -1654,7 +1671,6 @@ class Struct extends API { * @template {null|boolean|string|number} T * @template {{[key:string]: T|Schema.Convert}} U * @template {{[K in keyof U]: U[K] extends Schema.Convert ? U[K] : Schema.Convert}} Members - * @template [I=unknown] * @param {U} fields * @returns {Schema.StructSchema} */ @@ -1680,7 +1696,7 @@ export const struct = fields => { } } - return new Struct({ shape: /** @type {Members} */ (shape) }) + return /** @type {*} */ (new Struct({ shape: shape })) } /** @@ -1723,28 +1739,48 @@ class RawBytes extends API { return typeError({ expect: 'Uint8Array', actual: input }) } } + + /** + * @template {Uint8Array} O + * @template {Uint8Array} I + * @param {Schema.Convert} into + * @returns {Schema.BytesSchema} + */ + refine(into) { + const codec = /** @type {Schema.BlockCodec<0x55, * & I>} */ (this) + return new ByteView({ codec, convert: into }) + } } /** @type {Schema.BytesSchema} */ export const Bytes = new RawBytes() +/** + * @type {Schema.Convert<*>} + */ +const direct = { + read: input => input, + write: output => output, +} + /** * @template {Schema.BlockCodec} [Codec=import('multiformats/codecs/raw')] * @param {Codec} [codec] * @returns {Schema.BytesSchema & ({} | null), Schema.MulticodecCode>} */ export const bytes = codec => - /** @type {*} */ (codec ? new ByteView(codec) : Bytes) + /** @type {*} */ (codec ? new ByteView({ codec, convert: direct }) : Bytes) /** - * @template {{}|null} Out + * @template {{}|null} Model + * @template {Model} Out * @template {Schema.MulticodecCode} Code - * @extends {API, Schema.BlockCodec>} + * @extends {API, Schema.ByteSchemaSettings>} * @implements {Schema.BytesSchema} */ class ByteView extends API { get codec() { - return this.settings + return this.settings.codec } get name() { return this.codec.name @@ -1756,22 +1792,36 @@ class ByteView extends API { * @param {Out} data */ encode(data) { - return this.codec.encode(data) + const { codec, convert } = this.settings + const model = convert.write(data) + if (model.error) { + throw model.error + } else { + return /** @type {Schema.ByteView} */ (codec.encode(model.ok)) + } } /** * @param {Schema.ByteView} bytes */ decode(bytes) { - return this.codec.decode(bytes) + const { codec, convert } = this.settings + const model = codec.decode(bytes) + const result = convert.read(model) + if (result.error) { + throw result.error + } else { + return result.ok + } } /** * @param {Uint8Array} input - * @param {Schema.BlockCodec} codec + * @param {Schema.ByteSchemaSettings} settings * @returns {Schema.ReadResult} */ - readWith(input, codec) { + readWith(input, { codec, convert }) { try { - return { ok: codec.decode(input) } + const model = codec.decode(input) + return convert.read(model) } catch (cause) { return { error: /** @type {Error} */ (cause) } } @@ -1779,15 +1829,115 @@ class ByteView extends API { /** * * @param {Out} output - * @param {Schema.BlockCodec} codec + * @param {Schema.ByteSchemaSettings} settings + * @returns {Schema.ReadResult>} */ - writeWith(output, codec) { + writeWith(output, { codec, convert }) { try { - return { ok: codec.encode(output) } + const result = convert.write(output) + if (result.error) { + throw result.error + } + return { + ok: /** @type {Schema.ByteView} */ (codec.encode(result.ok)), + } } catch (cause) { return { error: /** @type {Error} */ (cause) } } } + + /** + * @template {Out} O + * @template {Out} I + * @param {Schema.Convert} into + * @returns {Schema.BytesSchema} + */ + refine(into) { + const { codec, convert: from } = this.settings + const convert = + from === direct + ? // if convertor is direct, we don't need to compose + /** @type {Schema.Convert} */ (into) + : pipe(/** @type {Schema.Convert<* & I, Model>} */ (from), into) + + return new ByteView({ + codec, + convert, + }) + } +} + +/** + * @template {{}|null} T + * @param {object} options + * @param {Schema.BlockCodec} options.codec + * @returns {Schema.Schema}>} + */ +export const dag = options => { + throw new Error('Not implemented') +} + +/** + * @template {unknown} T + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V + * @implements {Schema.Linked} + */ +class Linked { + /** + * @param {Schema.Link} cid + */ + constructor(cid) { + this.cid = cid + this['/'] = cid + } + /** + * @param {Schema.BlockStore} [store] + * @returns {Schema.ResolvedLink} + */ + resolve(store) { + throw new Error('Not implemented') + } + get version() { + return this.cid.version + } + get code() { + return this.cid.code + } + get multihash() { + return this.cid.multihash + } + get byteOffset() { + return this.cid.byteOffset + } + get byteLength() { + return this.cid.byteLength + } + get bytes() { + return this.cid.bytes + } + link() { + return this.cid + } + /** + * @param {unknown} other + * @returns {other is Schema.Link} + */ + equals(other) { + return this.cid.equals(other) + } + /** + * @template {string} Prefix + * @param {Schema.MultibaseEncoder} [base] + * @returns + */ + toString(base) { + return this.cid.toString(base) + } + toV1() { + return this.cid.toV1() + } } /** @@ -1809,7 +1959,7 @@ class ByteView extends API { * @template {number} Code * @template {number} Alg * @template {1|0} Version - * @extends {API, In, LinkSettings>} + * @extends {API, In, LinkSettings>} * @implements {Schema.LinkSchema} */ class LinkSchema extends API { @@ -1817,7 +1967,7 @@ class LinkSchema extends API { * * @param {unknown} cid * @param {LinkSettings} settings - * @returns {Schema.ReadResult>} + * @returns {Schema.ReadResult>} */ readWith(cid, { code, multihash = {}, version }) { if (cid == null) { @@ -1860,7 +2010,7 @@ class LinkSchema extends API { } return { - ok: /** @type {Schema.Link} */ (cid), + ok: /** @type {Schema.Linked<*, *, *, *>} */ (new Linked(cid)), } } } @@ -1885,7 +2035,7 @@ class LinkSchema extends API { } /** @type {Schema.LinkSchema, number, number, 0|1>} */ -export const Link = new LinkSchema({}) +const Link = new LinkSchema({}) /** * @template {number} Code diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 5cca144f..77139b58 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -13,6 +13,7 @@ import { MultihashHasher, MulticodecCode, MultibaseDecoder, + MultibaseEncoder, UnknownLink, IPLDView, IPLDViewBuilder, @@ -22,6 +23,7 @@ import { export type { Link, + UnknownLink, BlockStore, Block, BlockCodec, @@ -30,6 +32,7 @@ export type { MultihashHasher, MulticodecCode, MultibaseDecoder, + MultibaseEncoder, ByteView, IPLDView, IPLDViewBuilder, @@ -38,11 +41,11 @@ export type { } export interface Reader { - read(input: I): Result + read: (input: I) => Result } export interface Writer { - write(output: O): Result + write: (output: O) => Result } export type { Error, Result } @@ -65,7 +68,9 @@ export interface Schema< array(): ArraySchema or(other: Convert): Schema and(other: Convert): Schema - refine(schema: Reader): Schema + refine(schema: Convert): Schema + + // lift: (from: Convert) => Schema // into(schema: Convert): Schema pipe(schema: Convert): Schema @@ -122,14 +127,27 @@ export interface Schema< export interface PropertySchema {} +export interface ByteSchemaSettings< + Model, + Out extends Model, + Code extends MulticodecCode +> { + codec: BlockCodec + convert: Convert +} + export interface BytesSchema< - Out extends {} | null = Uint8Array, + Out = Uint8Array, Code extends MulticodecCode = MulticodecCode<0x55, 'raw'> > extends Schema { code: Code name: string encode(value: Out): ByteView decode(value: ByteView): Out + + refine( + schema: Convert + ): BytesSchema } export type ToBlock = { @@ -149,14 +167,14 @@ export interface Linked< Alg extends MulticodecCode = MulticodecCode, V extends UnknownLink['version'] = 1 > extends Link { - resolve(): ResolvedLink + resolve(store?: BlockStore): ResolvedLink } export interface CompiledView extends IPLDView { root: Required> } -type ResolvedLink< +export type ResolvedLink< T extends unknown = unknown, Code extends MulticodecCode = MulticodecCode, Alg extends MulticodecCode = MulticodecCode, @@ -193,7 +211,7 @@ export interface LinkSchema< Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, In> { +> extends Schema, In> { link(): never // attach(): AttachmentSchema, Code, Alg, V> @@ -201,7 +219,7 @@ export interface LinkSchema< parse( source: string, base?: MultibaseDecoder - ): Link + ): Linked } export interface AttachmentSchema< @@ -226,10 +244,9 @@ export interface CreateView { } export interface ImplicitSchema - extends Schema, I | undefined> { + extends Omit, I | undefined>, 'optional'> { readonly value: Exclude optional(): ImplicitSchema - // nullable(): ImplicitSchema } export type NotUndefined = Exclude @@ -255,7 +272,10 @@ export interface MapRepresentation< } export interface DictionarySchema - extends MapRepresentation, Dictionary> { + extends Omit< + MapRepresentation, Dictionary>, + 'partial' + > { key: Reader value: Convert @@ -288,7 +308,9 @@ export interface NumberSchema< optional(): Schema - refine(convert: Reader): NumberSchema + refine( + convert: Convert + ): NumberSchema constraint(check: (value: Out) => ReadResult): NumberSchema } @@ -422,9 +444,9 @@ export type InferOptionalReader = R extends Reader export interface StringSchema extends Schema { - refine( - convert: Reader - ): StringSchema + refine( + convert: Reader + ): StringSchema constraint(check: (value: Out) => ReadResult): StringSchema startsWith( prefix: Prefix diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index 6c264562..13793777 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -4,22 +4,51 @@ import { test, assert } from './test.js' describe.only('IPLD Schema', () => { test('cross block references', async () => { - const Deal = Schema.struct({ + const Content = Schema.bytes() + const DealDetail = Schema.struct({ size: Schema.integer(), commit: Schema.string(), + content: Content.link(), + }) + + const dealLink = DealDetail.link() + dealDetail.from({}) + + Schema.debug(DealDetail).in.content + + const detail = DealDetail.from({ + size: 1, + commit: 'a', + content: Content.link().parse('baakfa'), }) const cbor = Schema.bytes(CBOR) + const dealDetail = cbor.refine(DealDetail) + const dl = dealDetail.decode(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])) + dl.content Schema.debug(cbor) const thing = Schema.dictionary({ value: Schema.unknown() }) - const DL = Schema.bytes(CBOR) + const out = Schema.dag({ + codec: CBOR, + }) + + out.refine(DealDetail) + + const DL = cbor.refine(DealDetail) + + const DL2 = Schema.dictionary({ + value: Schema.unknown(), + }).refine(DealDetail) - const DL2 = DL.pipe(Deal) + const hello = Schema.string() + .startsWith('hello') + .endsWith('world') + .startsWith('hello ') - const dl2 = DL2.from(new Uint8Array()) + const dl2 = DL.from(new Uint8Array()) dl2.size // const a = {} From 945064fe000c15cc89e594a0895b96354da04d4a Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 8 May 2023 15:08:25 -0700 Subject: [PATCH 08/16] fix link schema --- packages/core/src/schema/schema.js | 277 +++++++++++++++++++++---- packages/core/src/schema/tradeoffs.md | 6 + packages/core/src/schema/type.ts | 76 +++++-- packages/core/test/ipld-schema.spec.js | 69 +++++- 4 files changed, 361 insertions(+), 67 deletions(-) create mode 100644 packages/core/src/schema/tradeoffs.md diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 71a80713..702f39eb 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -9,8 +9,8 @@ import { base32, } from '../link.js' export * from './type.js' -import * as CBOR from '../cbor.js' import { sha256 } from 'multiformats/hashes/sha2' +import { identity } from 'multiformats/hashes/identity' export { ok } /** @@ -187,18 +187,18 @@ export class API { * @template {number} [Alg=number] * @template {1|0} [Version=0|1] * @param {{ - * codec?: Schema.BlockCodec + * codec: Schema.BlockCodec * hasher?: Schema.MultihashHasher * version?: Version * }} options - * @returns {Schema.LinkSchema | Schema.IPLDView, Code, Alg, Version>} + * @returns {Schema.LinkSchema} */ - link({ codec, hasher, version } = {}) { + link({ codec, hasher, version }) { const schema = link({ - ...(codec ? { code: codec.code } : {}), + codec, ...(hasher ? { multihash: { code: hasher.code } } : {}), ...(version ? { version } : {}), - schema: /** @type {Schema.Schema} */ (this), + schema: /** @type {Schema.Schema} */ (this), }) return schema @@ -1566,6 +1566,44 @@ class Struct extends API { return { ok: struct } } + /** + * @param {Schema.InferStruct} output + * @param {{shape: Members}} settings + */ + + writeWith(output, { shape }) { + if (typeof output != 'object' || output === null || Array.isArray(output)) { + return typeError({ + expect: 'object', + actual: output, + }) + } + + const source = /** @type {{[K in keyof Members]: unknown}} */ (output) + + const input = + /** @type {{[K in keyof Members]: Schema.InferInput}} */ ({}) + const entries = + /** @type {{[K in keyof Members]: [K & string, Members[K]]}[keyof Members][]} */ ( + Object.entries(shape) + ) + + for (const [at, writer] of entries) { + const result = writer.read(source[at]) + if (result.error) { + return memberError({ at, cause: result.error }) + } + // skip undefined because they mess up CBOR and are generally useless. + else if (result.ok !== undefined) { + input[at] = /** @type {Schema.InferInput} */ ( + result.ok + ) + } + } + + return { ok: input } + } + partial() { const shape = Object.fromEntries( Object.entries(this.shape).map(([key, value]) => [key, optional(value)]) @@ -1750,6 +1788,17 @@ class RawBytes extends API { const codec = /** @type {Schema.BlockCodec<0x55, * & I>} */ (this) return new ByteView({ codec, convert: into }) } + + /** + * @param {Uint8Array} input + */ + create(input) { + return new BlockView({ + codec: this, + data: input, + bytes: input, + }) + } } /** @type {Schema.BytesSchema} */ @@ -1865,8 +1914,99 @@ class ByteView extends API { convert, }) } + + /** + * @param {Out} data + * @returns {Schema.BlockView} + */ + create(data) { + return new BlockView({ + codec: this, + data, + bytes: this.encode(data), + }) + } +} + +/** + * @template {unknown} T + * @template {Schema.MulticodecCode} Code + * @implements {Schema.BlockView} + */ +class BlockView { + /** + * @param {object} options + * @param {Schema.BlockCodec} options.codec + * @param {T} options.data + * @param {Schema.ByteView} options.bytes + */ + constructor({ codec, data, bytes }) { + this.codec = codec + this.data = data + this.bytes = bytes + } + encode() { + return this.bytes + } + decode() { + return this.data + } + get code() { + return this.codec.code + } + /** + * @returns {Schema.Linked} + */ + embed() { + return new Linked({ + link: createLink(this.code, identity.digest(this.bytes)), + codec: this.codec, + }) + } + + /** + * @template {Schema.MulticodecCode} Alg + * @param {object} options + * @param {Schema.MultihashHasher} options.hasher + * @returns {Promise>} + */ + async detach({ hasher = /** @type {*} */ (sha256) }) { + const { bytes, data } = this + const digest = await hasher.digest(bytes) + /** @type {Schema.Link} */ + const cid = createLink(this.code, digest) + return new DAGView({ root: { bytes, data, cid } }) + } } +/** + * @template {unknown} T + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V + */ +class DAGView { + /** + * @param {object} source + * @param {Required>} source.root + * @param {Schema.BlockStore} [source.store] + */ + constructor({ root, store = emptyStore }) { + this.root = root + this.store = store + } + decode() { + return this.root.data + } + link() { + return this.root.cid + } + *iterateIPLDBlocks() { + yield this.root + } +} + +const emptyStore = Object.freeze(new Map()) /** * @template {{}|null} T * @param {object} options @@ -1878,26 +2018,47 @@ export const dag = options => { } /** - * @template {unknown} T + * @template {unknown} Out + * @template {unknown} In * @template {Schema.MulticodecCode} Code * @template {Schema.MulticodecCode} Alg * @template {Schema.UnknownLink['version']} V - * @implements {Schema.Linked} + * @implements {Schema.Linked} */ class Linked { /** - * @param {Schema.Link} cid + * @param {object} source + * @param {Schema.Link} source.link + * @param {Schema.BlockCodec} source.codec + * @param {Schema.BlockStore} [source.store] */ - constructor(cid) { - this.cid = cid - this['/'] = cid + constructor({ link, codec, store = emptyStore }) { + this.codec = codec + this.cid = link + this.store = store + this['/'] = link.bytes } /** * @param {Schema.BlockStore} [store] - * @returns {Schema.ResolvedLink} + * @returns {Schema.ResolvedLink} */ resolve(store) { - throw new Error('Not implemented') + const { cid, codec } = this + if (cid.multihash.code === identity.code) { + const bytes = cid.multihash.digest + const data = codec.decode(bytes) + return /** @type {Schema.ResolvedLink} */ (data) + } else { + const block = (store && store.get(`${cid}`)) || this.store.get(`${cid}`) + if (block) { + const data = codec.decode(/** @type {Uint8Array} */ (block.bytes)) + return /** @type {Schema.ResolvedLink} */ (data) + } else { + throw new Error( + 'Block can not be resolved, please provide a store from which to resolve it' + ) + } + } } get version() { return this.cid.version @@ -1922,7 +2083,7 @@ class Linked { } /** * @param {unknown} other - * @returns {other is Schema.Link} + * @returns {other is Schema.Link} */ equals(other) { return this.cid.equals(other) @@ -1941,57 +2102,74 @@ class Linked { } /** - * @template {unknown} [T=unknown] + * @template {unknown} [Out=unknown] + * @template {unknown} [In=number] * @template {number} [Code=number] * @template {number} [Alg=number] * @template {1|0} [Version=0|1] * @typedef {{ - * code?:Code, + * codec: Schema.BlockCodec, * version?:Version * multihash?: {code?: Alg, digest?: Uint8Array} - * schema?: Schema.Schema + * schema?: Schema.Schema * }} LinkSettings */ /** * @template {unknown} Out * @template {unknown} In - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version - * @extends {API, In, LinkSettings>} - * @implements {Schema.LinkSchema} + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V + * @extends {API, Schema.IntoLink, LinkSettings>} + * @implements {Schema.LinkSchema} */ class LinkSchema extends API { /** * - * @param {unknown} cid - * @param {LinkSettings} settings - * @returns {Schema.ReadResult>} + * @param {Schema.IntoLink} cid + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} + */ + readWith(cid, { codec, multihash = {}, version }) { + const result = this.validateWith(cid, { codec, multihash, version }) + if (result.ok) { + const link = result.ok + const ok = link instanceof Linked ? link : new Linked({ link, codec }) + return { ok } + } else { + return result + } + } + + /** + * @param {Schema.IntoLink} source + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} */ - readWith(cid, { code, multihash = {}, version }) { - if (cid == null) { - return error(`Expected link but got ${cid} instead`) + validateWith(source, { codec, multihash = {}, version }) { + if (source == null) { + return error(`Expected link but got ${source} instead`) } else { - if (!isLink(cid)) { - return error(`Expected link to be a CID instead of ${cid}`) + if (!isLink(source)) { + return error(`Expected link to be a CID instead of ${source}`) } else { - if (code != null && cid.code !== code) { + if (source.code !== codec.code) { return error( - `Expected link to be CID with 0x${code.toString(16)} codec` + `Expected link to be CID with 0x${codec.code.toString(16)} codec` ) } - if (multihash.code != null && cid.multihash.code !== multihash.code) + if (multihash.code != null && source.multihash.code !== multihash.code) return error( `Expected link to be CID with 0x${multihash.code.toString( 16 )} hashing algorithm` ) - if (version != null && cid.version !== version) { + if (version != null && source.version !== version) { return error( - `Expected link to be CID version ${version} instead of ${cid.version}` + `Expected link to be CID version ${version} instead of ${source.version}` ) } @@ -1999,7 +2177,7 @@ class LinkSchema extends API { multihash.digest != null ? [ base32.baseEncode(multihash.digest), - base32.baseEncode(cid.multihash.digest), + base32.baseEncode(source.multihash.digest), ] : ['', ''] @@ -2009,13 +2187,22 @@ class LinkSchema extends API { ) } - return { - ok: /** @type {Schema.Linked<*, *, *, *>} */ (new Linked(cid)), - } + const link = /** @type {Schema.Link} */ (source) + return { ok: link } } } } + /** + * + * @param {Schema.Linked} source + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} + */ + writeWith(source, settings) { + return this.validateWith(source.link(), settings) + } + /** * @returns {never} */ @@ -2034,18 +2221,16 @@ class LinkSchema extends API { } } -/** @type {Schema.LinkSchema, number, number, 0|1>} */ -const Link = new LinkSchema({}) - /** * @template {number} Code * @template {number} Alg * @template {1|0} Version * @template {unknown} Out - * @param {LinkSettings} options - * @returns {Schema.LinkSchema | Schema.IPLDView, Code, Alg, Version>} + * @template {unknown} In + * @param {LinkSettings} options + * @returns {Schema.LinkSchema} */ -export const link = (options = {}) => new LinkSchema(options) +export const link = options => new LinkSchema(options) /** * @template {Schema.VariantChoices} Choices diff --git a/packages/core/src/schema/tradeoffs.md b/packages/core/src/schema/tradeoffs.md new file mode 100644 index 00000000..832fbbca --- /dev/null +++ b/packages/core/src/schema/tradeoffs.md @@ -0,0 +1,6 @@ +- we either have: + 1. Another set of APIs for working with dags alongside of regular APIs + that take `BlockStore / BlockLoader` in addition. + 2. We have set of properties that fail decode when `.link().resolve()` is used. + 3. We have a modifier that turns regular struct decoder into DAG decoder. + 4. We have alternative to `struct({})` which takes `.link().resolve()`. diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 77139b58..c8221212 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -38,6 +38,7 @@ export type { IPLDViewBuilder, BuildOptions, Phantom, + Await, } export interface Reader { @@ -56,6 +57,18 @@ export interface Convert extends Reader, Writer {} +export interface From { + from(value: T): Self +} + +export interface To { + to(value: Self): T +} + +export interface Into extends From, To {} + +export interface BlockLoader extends Reader {} + export interface Schema< Out extends unknown = unknown, In extends unknown = unknown @@ -88,20 +101,24 @@ export interface Schema< Alg extends MulticodecCode, V extends UnknownLink['version'] >(options?: { - codec?: BlockCodec + codec?: BlockCodec version?: V hasher?: MultihashHasher - }): LinkSchema | IPLDView, Code, Alg, V> + }): LinkSchema - // attachment< + // attach< // Code extends MulticodecCode, // Alg extends MulticodecCode, // V extends UnknownLink['version'] // >(options?: { - // codec?: BlockCodec - // version?: V + // codec?: BlockCodec // hasher?: MultihashHasher - // }): AttachmentSchema + // version?: V + // }): AttachmentSchema + + // with(context: { store: BlockStore }): Schema + + // asDAG(): Schema // codec: BlockCodec // hasher: MultihashHasher @@ -148,6 +165,34 @@ export interface BytesSchema< refine( schema: Convert ): BytesSchema + + create(value: Out): BlockView +} + +export interface BlockView< + T extends unknown, + Code extends MulticodecCode = MulticodecCode +> { + code: Code + encode(): ByteView + decode(): T + embed(): Linked + + detach< + Alg extends MulticodecCode = MulticodecCode<0x12, 'sha2-256'> + >(options: { + hasher?: MultihashHasher + }): Await> +} + +export interface DAGView< + T extends unknown, + Code extends MulticodecCode = MulticodecCode, + Alg extends MulticodecCode = MulticodecCode, + V extends UnknownLink['version'] = 1 +> extends IPLDView { + decode(): T + link(): Link } export type ToBlock = { @@ -186,15 +231,6 @@ export type ResolvedLink< // attach(): Await> // } -// export interface AttachmentSchema< -// T extends unknown = unknown, -// Code extends MulticodecCode = MulticodecCode, -// Alg extends MulticodecCode = MulticodecCode, -// V extends UnknownLink['version'] = 1 -// > extends Schema> { -// attach(source: T): Await> -// } - // export interface Attachment< // T extends unknown = unknown, // Code extends MulticodecCode = MulticodecCode, @@ -207,11 +243,10 @@ export type ResolvedLink< export interface LinkSchema< Out extends unknown, - In extends unknown, Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, In> { +> extends Schema, IntoLink> { link(): never // attach(): AttachmentSchema, Code, Alg, V> @@ -222,13 +257,16 @@ export interface LinkSchema< ): Linked } +export interface IntoLink { + link(): Link +} + export interface AttachmentSchema< O extends unknown, - I extends unknown, Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, I> {} +> extends Schema, IPLDView> {} export interface IPLDViewSource { root: Block diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index 13793777..f50832ac 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -1,9 +1,74 @@ import * as Schema from '../src/schema.js' -import { CBOR } from '../src/dag.js' +import { isLink } from '../src/lib.js' +import { CBOR, identity, sha256 } from '../src/dag.js' +import { CAR } from '../src/lib.js' import { test, assert } from './test.js' describe.only('IPLD Schema', () => { - test('cross block references', async () => { + test('link schema', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const Line = Schema.struct({ + start: Point.link({ codec: CBOR }), + end: Point.link({ codec: CBOR }), + }) + + const cbor = Schema.bytes(CBOR) + + const line = Line.from({ + start: cbor + .refine(Point) + .create({ + x: 1, + y: 2, + }) + .embed(), + end: cbor + .refine(Point) + .create({ + x: 0, + y: 0, + }) + .embed(), + }) + + const car = Schema.bytes(CAR) + + assert.ok(isLink(line.start), 'is a link') + assert.ok(isLink(line.end), 'is a link') + assert.equal(line.start.code, CBOR.code, 'is a CBOR link') + assert.equal(line.start.multihash.code, identity.code, 'is a CBOR link') + + assert.deepEqual(line.start.resolve(), { x: 1, y: 2 }) + assert.deepEqual(line.end.resolve(), { x: 0, y: 0 }) + }) + + test.skip('attachment code', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + + const Line = Schema.struct({ + start: Point.attach(), + end: Point.attach(), + }) + const $Point = Schema.bytes(CBOR).refine(Point) + + const line = Line.from({ + start: await $Point.create({ x: 1, y: 2 }).detach({ hasher: sha256 }), + end: await $Point.create({ x: 1, y: 2 }).detach({ hasher: sha256 }), + }) + + assert.ok(isLink(line.start), 'is a link') + assert.ok(isLink(line.end), 'is a link') + assert.equal(line.start.code, CBOR.code, 'is a CBOR link') + assert.equal(line.start.multihash.code, identity.code, 'is a CBOR link') + }) + test.skip('cross block references', async () => { const Content = Schema.bytes() const DealDetail = Schema.struct({ size: Schema.integer(), From 351aae0aff68608e0f27107c721997ce53458c83 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 08:57:45 -0700 Subject: [PATCH 09/16] fix: implement dag support --- packages/core/src/cbor.js | 8 +- packages/core/src/dag.js | 14 +- packages/core/src/identity.js | 9 + packages/core/src/lib.js | 2 +- packages/core/src/schema/did.js | 4 +- packages/core/src/schema/schema.js | 931 +++++++++++++++------- packages/core/src/schema/text.js | 2 +- packages/core/src/schema/type.ts | 159 ++-- packages/core/src/schema/uri.js | 2 +- packages/core/src/sha256.js | 9 + packages/core/test/extra-schema.spec.js | 22 +- packages/core/test/ipld-schema.spec.js | 351 +++++--- packages/core/test/link-schema.spec.js | 35 +- packages/core/test/map-schema.spec.js | 24 +- packages/core/test/schema.spec.js | 223 +++--- packages/core/test/variant-schema.spec.js | 32 +- packages/interface/src/lib.ts | 11 +- 17 files changed, 1224 insertions(+), 614 deletions(-) create mode 100644 packages/core/src/identity.js create mode 100644 packages/core/src/sha256.js diff --git a/packages/core/src/cbor.js b/packages/core/src/cbor.js index b19d8b4e..99de128d 100644 --- a/packages/core/src/cbor.js +++ b/packages/core/src/cbor.js @@ -1,12 +1,16 @@ import * as API from '@ucanto/interface' import * as CBOR from '@ipld/dag-cbor' -export { code, name, decode } from '@ipld/dag-cbor' -import { sha256 } from 'multiformats/hashes/sha2' +export { decode } from '@ipld/dag-cbor' +import * as sha256 from './sha256.js' import { create as createLink, isLink } from 'multiformats/link' // @see https://www.iana.org/assignments/media-types/application/vnd.ipld.dag-cbor export const contentType = 'application/vnd.ipld.dag-cbor' +/** @type {API.MulticodecCode} */ +export const code = CBOR.code +export const name = CBOR.name + /** * @param {unknown} data * @param {Set} seen diff --git a/packages/core/src/dag.js b/packages/core/src/dag.js index 80199a74..87a1e98d 100644 --- a/packages/core/src/dag.js +++ b/packages/core/src/dag.js @@ -106,18 +106,18 @@ export const notFound = link => { /** * @template T * @template {T} U - * @template {API.MulticodecCode} C - * @template {API.MulticodecCode} A + * @template {MF.BlockEncoder} [Codec=MF.BlockEncoder, U>] + * @template {MF.MultihashHasher} [Hasher=MF.MultihashHasher>] * @param {U} source * @param {BlockStore} store * @param {object} options - * @param {MF.BlockEncoder} [options.codec] - * @param {MF.MultihashHasher} [options.hasher] - * @returns {Promise & { data: U }>} + * @param {Codec} [options.codec] + * @param {Hasher} [options.hasher] + * @returns {Promise & { data: U }>} */ export const writeInto = async (source, store, options = {}) => { - const codec = /** @type {MF.BlockEncoder} */ (options.codec || CBOR) - const hasher = /** @type {MF.MultihashHasher} */ (options.hasher || sha256) + const codec = /** @type {Codec} */ (options.codec || CBOR) + const hasher = /** @type {Hasher} */ (options.hasher || sha256) const bytes = codec.encode(source) const digest = await hasher.digest(bytes) diff --git a/packages/core/src/identity.js b/packages/core/src/identity.js new file mode 100644 index 00000000..ca21a150 --- /dev/null +++ b/packages/core/src/identity.js @@ -0,0 +1,9 @@ +import * as API from '@ucanto/interface' + +import { identity } from 'multiformats/hashes/identity' + +/** @type {API.MulticodecCode} */ +export const code = identity.code +export const name = identity.name +export const { encode } = identity +export const digest = identity.digest.bind(identity) diff --git a/packages/core/src/lib.js b/packages/core/src/lib.js index 2a299393..805dd40e 100644 --- a/packages/core/src/lib.js +++ b/packages/core/src/lib.js @@ -15,7 +15,7 @@ export { parse as parseLink, decode as decodeLink, } from './link.js' -export { sha256 } from 'multiformats/hashes/sha2' +export * as sha256 from './sha256.js' export * as UCAN from '@ipld/dag-ucan' export * as DID from '@ipld/dag-ucan/did' export * as Signature from '@ipld/dag-ucan/signature' diff --git a/packages/core/src/schema/did.js b/packages/core/src/schema/did.js index 67fbaa37..d5d722bf 100644 --- a/packages/core/src/schema/did.js +++ b/packages/core/src/schema/did.js @@ -27,7 +27,7 @@ export const did = () => schema * * @param {unknown} input */ -export const read = input => schema.read(input) +export const read = input => schema.tryFrom(input) /** * @template {string} Method @@ -42,4 +42,4 @@ export const match = (options = {}) => * Create a DID string from any input (or throw) * @param {unknown} input */ -export const from = input => match({}).from(input) +export const from = input => match({}).tryFrom(input) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 702f39eb..11cab888 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -9,8 +9,9 @@ import { base32, } from '../link.js' export * from './type.js' -import { sha256 } from 'multiformats/hashes/sha2' -import { identity } from 'multiformats/hashes/identity' +import * as sha256 from '../sha256.js' +import * as identity from '../identity.js' +import * as CBOR from '../cbor.js' export { ok } /** @@ -40,10 +41,11 @@ export class API { * @abstract * @param {In} input * @param {Settings} settings + * @param {Schema.Region} [region] * @returns {Schema.ReadResult} */ /* c8 ignore next 3 */ - readWith(input, settings) { + readWith(input, settings, region) { throw new Error(`Abstract method readWith must be implemented by subclass`) } @@ -54,22 +56,23 @@ export class API { * @returns {Schema.ReadResult} */ writeWith(output, settings) { - throw new Error(`Abstract method readWith must be implemented by subclass`) + throw new Error(`Abstract method writeWith must be implemented by subclass`) } /** * @param {In} input + * @param {Schema.Region} [region] * @returns {Schema.ReadResult} */ - read(input) { - return this.readWith(input, this.settings) + tryFrom(input, region) { + return this.readWith(input, this.settings, region) } /** * @param {Out} output * @returns {Schema.ReadResult} */ - write(output) { + tryTo(output) { return this.writeWith(output, this.settings) } @@ -78,7 +81,7 @@ export class API { * @returns {value is Out} */ is(value) { - return !this.read(/** @type {In} */ (value))?.error + return !this.tryFrom(/** @type {In} */ (value))?.error } /** @@ -86,7 +89,7 @@ export class API { * @return {Out} */ from(value) { - const result = this.read(/** @type {In} */ (value)) + const result = this.tryFrom(/** @type {In} */ (value)) if (result.error) { throw result.error } else { @@ -98,7 +101,7 @@ export class API { * @param {Out} value */ to(value) { - const result = this.write(value) + const result = this.tryTo(value) if (result.error) { throw result.error } else { @@ -130,7 +133,7 @@ export class API { } /** - * @returns {Schema.ArraySchema} + * @returns {Schema.ArraySchema>} */ array() { return array(this) @@ -183,21 +186,21 @@ export class API { } /** - * @template {number} [Code=number] - * @template {number} [Alg=number] - * @template {1|0} [Version=0|1] + * @template {Schema.BlockCodec} Codec + * @template {Schema.MultihashHasher} Hasher + * @template {Schema.UnknownLink['version']} Version * @param {{ - * codec: Schema.BlockCodec - * hasher?: Schema.MultihashHasher + * codec?: Codec + * hasher?: Hasher * version?: Version * }} options - * @returns {Schema.LinkSchema} + * @returns {Schema.LinkSchema} */ - link({ codec, hasher, version }) { + link({ codec = /** @type {*} */ (CBOR), hasher, version } = {}) { const schema = link({ codec, - ...(hasher ? { multihash: { code: hasher.code } } : {}), - ...(version ? { version } : {}), + hasher, + version, schema: /** @type {Schema.Schema} */ (this), }) @@ -275,14 +278,14 @@ class Never extends API { /** * @param {never} value */ - to(value) { + tryTo(value) { return value } /** * @param {any} input * @returns {Schema.ReadResult} */ - read(input) { + tryFrom(input) { return typeError({ expect: 'never', actual: input }) } } @@ -301,13 +304,13 @@ class Unknown extends API { /** * @param {unknown} input */ - read(input) { + tryFrom(input) { return /** @type {Schema.ReadResult}*/ ({ ok: input }) } /** * @param {unknown} output */ - write(output) { + tryTo(output) { return { ok: output } } toString() { @@ -411,14 +414,15 @@ export const unknown = () => new Unknown() class Nullable extends API { /** * @param {I|null} input - * @param {Schema.Reader} reader + * @param {Schema.Convert} convert + * @param {Schema.Region} [region] */ - readWith(input, reader) { + readWith(input, convert, region) { if (input === null) { return { ok: null } } - const result = reader.read(input) + const result = convert.tryFrom(input, region) if (result.error) { return input === null ? { ok: null } @@ -436,10 +440,10 @@ class Nullable extends API { } /** * @param {O|null} output - * @param {Schema.Writer} writer + * @param {Schema.Convert} writer */ writeWith(output, writer) { - return output === null ? { ok: null } : writer.write(output) + return output === null ? { ok: null } : writer.tryTo(output) } toString() { return `${this.settings}.nullable()` @@ -466,23 +470,24 @@ class Optional extends API { } /** * @param {I|undefined} input - * @param {Schema.Reader} reader + * @param {Schema.Convert} convert + * @param {Schema.Region} [region] * @returns {Schema.ReadResult} */ - readWith(input, reader) { + readWith(input, convert, region) { if (input === undefined) { return { ok: undefined } } - const result = reader.read(input) + const result = convert.tryFrom(input, region) return result.error && input === undefined ? { ok: undefined } : result } /** * * @param {O|undefined} output - * @param {Schema.Convert} writer + * @param {Schema.Convert} convert */ - writeWith(output, writer) { - return output === undefined ? { ok: undefined } : writer.write(output) + writeWith(output, convert) { + return output === undefined ? { ok: undefined } : convert.tryTo(output) } toString() { return `${this.settings}.optional()` @@ -523,13 +528,14 @@ class Implicit extends API { * @param {object} options * @param {Schema.Convert} options.convert * @param {Exclude} options.value + * @param {Schema.Region} [region] * @returns {Schema.ReadResult>} */ - readWith(input, { convert, value }) { + readWith(input, { convert, value }, region) { if (input === undefined) { return { ok: value } } else { - const result = convert.read(input) + const result = convert.tryFrom(input, region) return result.error ? result @@ -548,7 +554,7 @@ class Implicit extends API { * @param {Schema.Convert} options.convert */ writeWith(output, { convert }) { - return convert.write(output) + return convert.tryTo(output) } toString() { return `${this.settings.convert}.default(${JSON.stringify( @@ -586,24 +592,23 @@ export const implicit = (schema, value) => { } /** - * @template O - * @template I - * @extends {API>} - * @implements {Schema.ArraySchema} + * @template {Schema.Convert} Convert + * @extends {API[], Schema.InferInput[], Convert>} + * @implements {Schema.ArraySchema} */ class ArrayOf extends API { /** - * @param {I[]} input - * @param {Schema.Reader} schema + * @param {Schema.InferInput[]} input + * @param {Convert} convert + * @param {Schema.Region} [context] */ - readWith(input, schema) { + readWith(input, convert, context) { if (!Array.isArray(input)) { return typeError({ expect: 'array', actual: input }) } - /** @type {O[]} */ const results = [] for (const [index, value] of input.entries()) { - const result = schema.read(value) + const result = convert.tryFrom(value, context) if (result.error) { return memberError({ at: index, cause: result.error }) } else { @@ -614,14 +619,13 @@ class ArrayOf extends API { } /** * - * @param {O[]} output - * @param {Schema.Writer} schema + * @param {Schema.Infer[]} output + * @param {Convert} convert */ - writeWith(output, schema) { - /** @type {I[]} */ + writeWith(output, convert) { const results = [] for (const [index, value] of output.entries()) { - const result = schema.write(value) + const result = convert.tryTo(value) if (result.error) { return memberError({ at: index, cause: result.error }) } else { @@ -630,9 +634,7 @@ class ArrayOf extends API { } return { ok: results } } - /** - * @type {Schema.Convert} - */ + get element() { return this.settings } @@ -642,12 +644,15 @@ class ArrayOf extends API { } /** - * @template O - * @template [I=unknown] - * @param {Schema.Convert} schema - * @returns {Schema.ArraySchema} + * @template O, I + * @template {Schema.Convert} Schema + * @param {Schema} schema + * @returns {Schema.ArraySchema} */ -export const array = schema => new ArrayOf(schema) +export const array = schema => { + const out = new ArrayOf(schema) + return out +} /** * @template {[Schema.Convert, ...Schema.Convert[]]} Shape @@ -658,9 +663,10 @@ class Tuple extends API { /** * @param {Schema.InferTupleInput} input * @param {Shape} shape + * @param {Schema.Region} [context] * @returns {Schema.ReadResult>} */ - readWith(input, shape) { + readWith(input, shape, context) { if (!Array.isArray(input)) { return typeError({ expect: 'array', actual: input }) } @@ -670,7 +676,7 @@ class Tuple extends API { const results = [] for (const [index, reader] of shape.entries()) { - const result = reader.read(input[index]) + const result = reader.tryFrom(input[index], context) if (result.error) { return memberError({ at: index, cause: result.error }) } else { @@ -697,7 +703,7 @@ class Tuple extends API { const results = [] for (const [index, writer] of shape.entries()) { - const result = writer.write(output[index]) + const result = writer.tryTo(output[index]) if (result.error) { return memberError({ at: index, cause: result.error }) } else { @@ -729,17 +735,18 @@ export const tuple = shape => new Tuple(shape) * @template V * @template {string} K * @template U - * @extends {API, Schema.Dictionary, { key: Schema.Reader, value: Schema.Convert }>} + * @extends {API, Schema.Dictionary, { key: Schema.From, value: Schema.Convert }>} * @implements {Schema.DictionarySchema} */ class Dictionary extends API { /** * @param {Schema.Dictionary} input * @param {object} schema - * @param {Schema.Reader} schema.key - * @param {Schema.Reader} schema.value + * @param {Schema.From} schema.key + * @param {Schema.From} schema.value + * @param {Schema.Region} [context] */ - readWith(input, { key, value }) { + readWith(input, { key, value }, context) { if (typeof input != 'object' || input === null || Array.isArray(input)) { return typeError({ expect: 'dictionary', @@ -750,12 +757,12 @@ class Dictionary extends API { const dict = /** @type {Schema.Dictionary} */ ({}) for (const [k, v] of Object.entries(input)) { - const keyResult = key.read(k) + const keyResult = key.tryFrom(k, context) if (keyResult.error) { return memberError({ at: k, cause: keyResult.error }) } - const valueResult = value.read(v) + const valueResult = value.tryFrom(v, context) if (valueResult.error) { return memberError({ at: k, cause: valueResult.error }) } @@ -773,19 +780,19 @@ class Dictionary extends API { * * @param {Schema.Dictionary} output * @param {object} schema - * @param {Schema.Reader} schema.key - * @param {Schema.Writer} schema.value + * @param {Schema.From} schema.key + * @param {Schema.To} schema.value */ writeWith(output, { key, value }) { const dict = /** @type {Schema.Dictionary} */ ({}) for (const [k, v] of Object.entries(output)) { - const keyResult = key.read(k) + const keyResult = key.tryFrom(k) if (keyResult.error) { return memberError({ at: k, cause: keyResult.error }) } - const valueResult = value.write(/** @type {V} */ (v)) + const valueResult = value.tryTo(/** @type {V} */ (v)) if (valueResult.error) { return memberError({ at: k, cause: valueResult.error }) } @@ -824,13 +831,13 @@ class Dictionary extends API { * @template {unknown} U * @param {object} shape * @param {Schema.Convert} shape.value - * @param {Schema.Reader} [shape.key] + * @param {Schema.From} [shape.key] * @returns {Schema.DictionarySchema} */ export const dictionary = ({ value, key }) => new Dictionary({ value, - key: key || /** @type {Schema.Reader} */ (string()), + key: key || /** @type {Schema.From} */ (string()), }) /** @@ -891,11 +898,12 @@ class Union extends API { /** * @param {Schema.InferUnionInput} input * @param {Members} variants + * @param {Schema.Region} [context] */ - readWith(input, variants) { + readWith(input, variants, context) { const causes = [] for (const reader of variants) { - const result = reader.read(input) + const result = reader.tryFrom(input, context) if (result.error) { causes.push(result.error) } else { @@ -913,7 +921,7 @@ class Union extends API { writeWith(output, variants) { const causes = [] for (const member of variants) { - const result = member.write(output) + const result = member.tryTo(output) if (result.error) { causes.push(result.error) } else { @@ -959,12 +967,13 @@ class Intersection extends API { /** * @param {Schema.InferIntersectionInput} input * @param {Members} schemas + * @param {Schema.Region} [context] * @returns {Schema.ReadResult>} */ - readWith(input, schemas) { + readWith(input, schemas, context) { const causes = [] for (const schema of schemas) { - const result = schema.read(input) + const result = schema.tryFrom(input, context) if (result.error) { causes.push(result.error) } @@ -984,7 +993,7 @@ class Intersection extends API { writeWith(output, members) { const causes = [] for (const member of members) { - const result = member.write(output) + const result = member.tryTo(output) if (result.error) { causes.push(result.error) } @@ -1083,8 +1092,8 @@ class NumberSchema extends API { */ constraint(check) { return this.refine({ - read: check, - write: check, + tryFrom: check, + tryTo: check, }) } @@ -1133,6 +1142,16 @@ class NumberSchema extends API { ? { ok: /** @type {*} */ (input) } : typeError({ expect: 'number', actual: input }) } + /** + * @param {Out} output + * @param {Settings} settings + * @returns {Schema.ReadResult} + */ + writeWith(output, settings) { + return typeof output === 'number' + ? { ok: /** @type {*} */ (output) } + : typeError({ expect: 'number', actual: output }) + } toString() { return `number()` } @@ -1147,7 +1166,7 @@ export const number = () => Number * @template {number} In * @template {Out} O * @template {Out} I - * @extends {NumberSchema, refine:Schema.Reader}>} + * @extends {NumberSchema, refine:Schema.From}>} * @implements {Schema.NumberSchema} */ class RefinedNumber extends NumberSchema { @@ -1157,8 +1176,8 @@ class RefinedNumber extends NumberSchema { * @returns {Schema.ReadResult} */ readWith(input, { schema, refine }) { - const result = schema.read(input) - return result.error ? result : refine.read(/** @type {I} */ (result.ok)) + const result = schema.tryFrom(input) + return result.error ? result : refine.tryFrom(/** @type {I} */ (result.ok)) } /** @@ -1166,8 +1185,8 @@ class RefinedNumber extends NumberSchema { * @param {{schema: Schema.Convert, refine: Schema.Convert } } settings */ writeWith(output, { schema, refine }) { - const result = refine.write(output) - return result.error ? result : schema.write(result.ok) + const result = refine.tryTo(output) + return result.error ? result : schema.tryTo(result.ok) } toString() { @@ -1180,17 +1199,31 @@ class RefinedNumber extends NumberSchema { */ class IntegerSchema extends NumberSchema { /** - * @param {number} input + * @param {number} number * @returns {Schema.ReadResult} */ - read(input) { - return Number.isInteger(input) - ? { ok: /** @type {Schema.Integer} */ (input) } + static validate(number) { + return Number.isInteger(number) + ? { ok: /** @type {Schema.Integer} */ (number) } : typeError({ expect: 'integer', - actual: input, + actual: number, }) } + /** + * @param {number} number + * @returns {Schema.ReadResult} + */ + tryFrom(number) { + return IntegerSchema.validate(number) + } + /** + * @param {Schema.Integer} number + * @returns {Schema.ReadResult} + */ + tryTo(number) { + return IntegerSchema.validate(number) + } toString() { return `Integer` } @@ -1207,7 +1240,7 @@ class FloatSchema extends NumberSchema { * @param {number} number * @returns {Schema.ReadResult} */ - read(number) { + static validate(number) { return Number.isFinite(number) ? { ok: /** @type {Schema.Float} */ (number) } : typeError({ @@ -1215,6 +1248,20 @@ class FloatSchema extends NumberSchema { actual: number, }) } + /** + * @param {number} number + * @returns {Schema.ReadResult} + */ + tryFrom(number) { + return FloatSchema.validate(number) + } + /** + * @param {Schema.Float} number + * @returns {Schema.ReadResult} + */ + tryTo(number) { + return FloatSchema.validate(number) + } toString() { return 'Float' } @@ -1263,8 +1310,8 @@ class StringSchema extends API { */ constraint(check) { return this.refine({ - read: check, - write: check, + tryFrom: check, + tryTo: check, }) } /** @@ -1308,22 +1355,22 @@ class StringSchema extends API { class RefinedString extends StringSchema { /** * @param {In} input - * @param {{base:Schema.Reader, schema:Schema.Reader}} settings + * @param {{base:Schema.From, schema:Schema.From}} settings * @returns {Schema.ReadResult} */ readWith(input, { base, schema }) { - const result = base.read(input) - return result.error ? result : schema.read(/** @type {I} */ (result.ok)) + const result = base.tryFrom(input) + return result.error ? result : schema.tryFrom(/** @type {I} */ (result.ok)) } /** * @param {O} output - * @param {{base:Schema.Writer, schema:Schema.Writer}} settings + * @param {{base:Schema.To, schema:Schema.To}} settings * @returns {Schema.ReadResult} */ writeWith(output, { base, schema }) { - const result = schema.write(output) - return result.error ? result : base.write(result.ok) + const result = schema.tryTo(output) + return result.error ? result : base.tryTo(result.ok) } toString() { @@ -1417,18 +1464,21 @@ class Refine extends API { /** * @param {In} input * @param {{ base: Schema.Convert, schema: Schema.Convert }} settings + * @param {Schema.Region} [context] */ - readWith(input, { base, schema }) { - const result = base.read(input) - return result.error ? result : schema.read(/** @type {I} */ (result.ok)) + readWith(input, { base, schema }, context) { + const result = base.tryFrom(input, context) + return result.error + ? result + : schema.tryFrom(/** @type {I} */ (result.ok), context) } /** * @param {O} output * @param {{ base: Schema.Convert, schema: Schema.Convert }} settings */ writeWith(output, { base, schema }) { - const result = schema.write(output) - return result.error ? result : base.write(/** @type {Out} */ (result.ok)) + const result = schema.tryTo(output) + return result.error ? result : base.tryTo(/** @type {Out} */ (result.ok)) } toString() { return `${this.settings.base}.refine(${this.settings.schema})` @@ -1456,19 +1506,20 @@ export const refine = (base, schema) => new Refine({ base, schema }) class Pipe extends API { /** * @param {In} input - * @param {{ from: Schema.Convert, to: Schema.Convert }} settings + * @param {{ from: Schema.From, to: Schema.From }} settings + * @param {Schema.Region} [context] */ - readWith(input, { from, to }) { - const result = from.read(input) - return result.error ? result : to.read(result.ok) + readWith(input, { from, to }, context) { + const result = from.tryFrom(input, context) + return result.error ? result : to.tryFrom(result.ok, context) } /** * @param {Into} output - * @param {{ from: Schema.Convert, to: Schema.Convert }} settings + * @param {{ from: Schema.To, to: Schema.To }} settings */ writeWith(output, { from, to }) { - const result = to.write(output) - return result.error ? result : from.write(result.ok) + const result = to.tryTo(output) + return result.error ? result : from.tryTo(result.ok) } toString() { return `${this.settings.from}.pipe(${this.settings.to})` @@ -1533,9 +1584,10 @@ class Struct extends API { /** * @param {Schema.InferStructInput} input * @param {{shape: Members}} settings + * @param {Schema.Region} [context] * @returns {Schema.ReadResult>} */ - readWith(input, { shape }) { + readWith(input, { shape }, context) { if (typeof input != 'object' || input === null || Array.isArray(input)) { return typeError({ expect: 'object', @@ -1553,7 +1605,7 @@ class Struct extends API { ) for (const [at, reader] of entries) { - const result = reader.read(source[at]) + const result = reader.tryFrom(source[at], context) if (result.error) { return memberError({ at, cause: result.error }) } @@ -1589,7 +1641,7 @@ class Struct extends API { ) for (const [at, writer] of entries) { - const result = writer.read(source[at]) + const result = writer.tryTo(source[at]) if (result.error) { return memberError({ at, cause: result.error }) } @@ -1788,17 +1840,6 @@ class RawBytes extends API { const codec = /** @type {Schema.BlockCodec<0x55, * & I>} */ (this) return new ByteView({ codec, convert: into }) } - - /** - * @param {Uint8Array} input - */ - create(input) { - return new BlockView({ - codec: this, - data: input, - bytes: input, - }) - } } /** @type {Schema.BytesSchema} */ @@ -1808,8 +1849,8 @@ export const Bytes = new RawBytes() * @type {Schema.Convert<*>} */ const direct = { - read: input => input, - write: output => output, + tryFrom: input => input, + tryTo: output => output, } /** @@ -1842,7 +1883,7 @@ class ByteView extends API { */ encode(data) { const { codec, convert } = this.settings - const model = convert.write(data) + const model = convert.tryFrom(data) if (model.error) { throw model.error } else { @@ -1855,7 +1896,7 @@ class ByteView extends API { decode(bytes) { const { codec, convert } = this.settings const model = codec.decode(bytes) - const result = convert.read(model) + const result = convert.tryFrom(model) if (result.error) { throw result.error } else { @@ -1870,7 +1911,7 @@ class ByteView extends API { readWith(input, { codec, convert }) { try { const model = codec.decode(input) - return convert.read(model) + return convert.tryFrom(model) } catch (cause) { return { error: /** @type {Error} */ (cause) } } @@ -1883,7 +1924,7 @@ class ByteView extends API { */ writeWith(output, { codec, convert }) { try { - const result = convert.write(output) + const result = convert.tryTo(output) if (result.error) { throw result.error } @@ -1914,97 +1955,93 @@ class ByteView extends API { convert, }) } - - /** - * @param {Out} data - * @returns {Schema.BlockView} - */ - create(data) { - return new BlockView({ - codec: this, - data, - bytes: this.encode(data), - }) - } } -/** - * @template {unknown} T - * @template {Schema.MulticodecCode} Code - * @implements {Schema.BlockView} - */ -class BlockView { - /** - * @param {object} options - * @param {Schema.BlockCodec} options.codec - * @param {T} options.data - * @param {Schema.ByteView} options.bytes - */ - constructor({ codec, data, bytes }) { - this.codec = codec - this.data = data - this.bytes = bytes - } - encode() { - return this.bytes - } - decode() { - return this.data - } - get code() { - return this.codec.code - } - /** - * @returns {Schema.Linked} - */ - embed() { - return new Linked({ - link: createLink(this.code, identity.digest(this.bytes)), - codec: this.codec, - }) - } +// /** +// * @template {Schema.UnknownLink['version']} V +// * @template {Schema.Block} Block +// * @implements {Schema.IPLDView['data']>} +// */ +// class DAGView { +// /** +// * @param {object} source +// * @param {Required} source.root +// * @param {Schema.Region} [source.store] +// */ +// constructor({ root, store = emptyStore }) { +// this.root = root +// this.store = store +// this['/'] = this.root.cid.bytes +// } +// resolve() { +// return /** @type {Schema.ResolvedLink['data'], Block['cid']['code'], Block['cid']['multihash']['code'], Block['cid']['version']>} */ ( +// this.root.data +// ) +// } +// /** +// * +// * @param {object} context +// * @param {Schema.Region} [context.store] +// * @returns {*} +// */ +// with({ store = this.store }) { +// return new DAGView({ root: this.root, store }) +// } +// decode() { +// return this.root.data +// } +// link() { +// return this.root.cid +// } +// *iterateIPLDBlocks() { +// const model = this.root.data +// if (model && typeof model === 'object') { +// for (const value of Object.values(model)) { +// if (value && typeof value['iterateIPLDBlocks'] === 'function') { +// yield* value.iterateIPLDBlocks() +// } +// } +// } +// yield /** @type {Schema.Block} */ (this.root) +// } - /** - * @template {Schema.MulticodecCode} Alg - * @param {object} options - * @param {Schema.MultihashHasher} options.hasher - * @returns {Promise>} - */ - async detach({ hasher = /** @type {*} */ (sha256) }) { - const { bytes, data } = this - const digest = await hasher.digest(bytes) - /** @type {Schema.Link} */ - const cid = createLink(this.code, digest) - return new DAGView({ root: { bytes, data, cid } }) - } -} +// get version() { +// return this.root.cid.version +// } +// get code() { +// return this.root.cid.code +// } +// get multihash() { +// return this.root.cid.multihash +// } +// get byteOffset() { +// return this.root.cid.byteOffset +// } +// get byteLength() { +// return this.root.cid.byteLength +// } +// get bytes() { +// return this.root.cid.bytes +// } -/** - * @template {unknown} T - * @template {Schema.MulticodecCode} Code - * @template {Schema.MulticodecCode} Alg - * @template {Schema.UnknownLink['version']} V - */ -class DAGView { - /** - * @param {object} source - * @param {Required>} source.root - * @param {Schema.BlockStore} [source.store] - */ - constructor({ root, store = emptyStore }) { - this.root = root - this.store = store - } - decode() { - return this.root.data - } - link() { - return this.root.cid - } - *iterateIPLDBlocks() { - yield this.root - } -} +// /** +// * @param {unknown} other +// */ +// equals(other) { +// return this.root.cid.equals(other) +// } +// /** +// * @template {string} Prefix +// * @param {Schema.MultibaseEncoder} [base] +// * @returns +// */ +// toString(base) { +// return this.root.cid.toString(base) +// } +// toV1() { +// return this.root.cid.toV1() +// } +// } const emptyStore = Object.freeze(new Map()) /** @@ -2023,43 +2060,57 @@ export const dag = options => { * @template {Schema.MulticodecCode} Code * @template {Schema.MulticodecCode} Alg * @template {Schema.UnknownLink['version']} V - * @implements {Schema.Linked} + * @implements {Schema.LinkOf} */ -class Linked { +class Link { /** * @param {object} source * @param {Schema.Link} source.link * @param {Schema.BlockCodec} source.codec - * @param {Schema.BlockStore} [source.store] + * @param {Schema.Convert} source.schema + * @param {Schema.Region} [source.store] */ - constructor({ link, codec, store = emptyStore }) { + constructor({ link, codec, schema }) { this.codec = codec this.cid = link - this.store = store + this.schema = schema this['/'] = link.bytes } + /** - * @param {Schema.BlockStore} [store] - * @returns {Schema.ResolvedLink} + * @param {Schema.Region} region + * @returns {Schema.Attachment} */ - resolve(store) { - const { cid, codec } = this - if (cid.multihash.code === identity.code) { - const bytes = cid.multihash.digest - const data = codec.decode(bytes) - return /** @type {Schema.ResolvedLink} */ (data) + select(region) { + const { cid, codec, schema } = this + const block = region.get(`${cid}`) + const bytes = block + ? block.bytes + : cid.multihash.code === identity.code + ? cid.multihash.digest + : undefined + + if (bytes) { + return new Attachment({ + root: { cid, bytes }, + codec, + schema, + store: region, + }) } else { - const block = (store && store.get(`${cid}`)) || this.store.get(`${cid}`) - if (block) { - const data = codec.decode(/** @type {Uint8Array} */ (block.bytes)) - return /** @type {Schema.ResolvedLink} */ (data) - } else { - throw new Error( - 'Block can not be resolved, please provide a store from which to resolve it' - ) - } + throw new RangeError( + `Block can not be resolved, please provide a store from which to resolve it` + ) } } + + /** + * @param {Schema.Region} region + * @returns {Schema.ResolvedLink} + */ + resolve(region) { + return this.select(region).resolve() + } get version() { return this.cid.version } @@ -2101,6 +2152,145 @@ class Linked { } } +/** + * @template {unknown} Out + * @template {unknown} In + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V + * @implements {Schema.Attachment} + */ +class Attachment { + /** + * @param {object} source + * @param {Schema.Block} source.root + * @param {Schema.BlockCodec} source.codec + * @param {Schema.Convert} source.schema + * @param {Schema.Region} [source.store] + */ + constructor({ root, codec, schema, store = emptyStore }) { + this.codec = codec + this.root = root + this.store = store + this.schema = schema + this['/'] = root.cid.bytes + + /** @type {Schema.ReadResult|null} */ + this._resolved = null + } + + /** + * @returns {Schema.ResolvedLink} + */ + resolve() { + let result = this._resolved + if (result == null) { + const { schema, codec, root } = this + const data = codec.decode(/** @type {Uint8Array} */ (root.bytes)) + result = schema.tryFrom(data, this.store) + + this._resolved = result + } + + if (result.error) { + throw result.error + } else { + return /** @type {Schema.ResolvedLink} */ (result.ok) + } + } + get version() { + return this.root.cid.version + } + get code() { + return this.root.cid.code + } + get multihash() { + return this.root.cid.multihash + } + get byteOffset() { + return this.root.cid.byteOffset + } + get byteLength() { + return this.root.cid.byteLength + } + get bytes() { + return this.root.cid.bytes + } + link() { + return this.root.cid + } + /** + * @param {unknown} other + * @returns {other is Schema.Link} + */ + equals(other) { + return this.root.cid.equals(other) + } + /** + * @template {string} Prefix + * @param {Schema.MultibaseEncoder} [base] + * @returns + */ + toString(base) { + return this.root.cid.toString(base) + } + toV1() { + return this.root.cid.toV1() + } + /** + * @param {object} context + * @param {Schema.Region} [context.store] + */ + with({ store = this.store }) { + return new Attachment({ + root: this.root, + codec: this.codec, + schema: this.schema, + store, + }) + } + + *iterateIPLDBlocks() { + const dag = this.resolve() + const { store, root } = this + for (const link of Attachment.links(dag)) { + const block = store.get(`${link}`) + if (block) { + yield { cid: link, bytes: block.bytes } + } + } + + yield /** @type {Schema.Block} */ (root) + } + + /** + * @param {unknown} source + * @returns {IterableIterator} + */ + static *links(source) { + if (isLink(source)) { + yield /** @type {Schema.Link} */ (source) + } else if (source && typeof source === 'object') { + for (const value of Object.values(source)) { + yield* this.links(value) + } + } + } + + /** + * @param {unknown} source + */ + static *iterateIPLDBlocks(source) { + if (source && typeof source === 'object') { + for (const value of Object.values(source)) { + if (value && typeof value['iterateIPLDBlocks'] === 'function') { + yield* value.iterateIPLDBlocks() + } + } + } + } +} + /** * @template {unknown} [Out=unknown] * @template {unknown} [In=number] @@ -2109,9 +2299,9 @@ class Linked { * @template {1|0} [Version=0|1] * @typedef {{ * codec: Schema.BlockCodec, - * version?:Version - * multihash?: {code?: Alg, digest?: Uint8Array} - * schema?: Schema.Schema + * version?: Version + * hasher?: {code: Alg} + * schema: Schema.Schema * }} LinkSettings */ @@ -2121,33 +2311,21 @@ class Linked { * @template {Schema.MulticodecCode} Code * @template {Schema.MulticodecCode} Alg * @template {Schema.UnknownLink['version']} V - * @extends {API, Schema.IntoLink, LinkSettings>} + * @extends {API, Schema.IntoLink, LinkSettings>} * @implements {Schema.LinkSchema} */ class LinkSchema extends API { /** - * - * @param {Schema.IntoLink} cid - * @param {LinkSettings} settings - * @returns {Schema.ReadResult>} - */ - readWith(cid, { codec, multihash = {}, version }) { - const result = this.validateWith(cid, { codec, multihash, version }) - if (result.ok) { - const link = result.ok - const ok = link instanceof Linked ? link : new Linked({ link, codec }) - return { ok } - } else { - return result - } - } - - /** + * @template {unknown} Out + * @template {unknown} In + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V * @param {Schema.IntoLink} source * @param {LinkSettings} settings * @returns {Schema.ReadResult>} */ - validateWith(source, { codec, multihash = {}, version }) { + static validate(source, { codec, hasher, version, schema }) { if (source == null) { return error(`Expected link but got ${source} instead`) } else { @@ -2160,9 +2338,13 @@ class LinkSchema extends API { ) } - if (multihash.code != null && source.multihash.code !== multihash.code) + if ( + hasher && + hasher.code != null && + source.multihash.code !== hasher.code + ) return error( - `Expected link to be CID with 0x${multihash.code.toString( + `Expected link to be CID with 0x${hasher.code.toString( 16 )} hashing algorithm` ) @@ -2173,34 +2355,42 @@ class LinkSchema extends API { ) } - const [expectDigest, actualDigest] = - multihash.digest != null - ? [ - base32.baseEncode(multihash.digest), - base32.baseEncode(source.multihash.digest), - ] - : ['', ''] - - if (expectDigest !== actualDigest) { - return error( - `Expected link with "${expectDigest}" hash digest instead of "${actualDigest}"` - ) - } - const link = /** @type {Schema.Link} */ (source) return { ok: link } } } } + /** + * @param {Schema.IntoLink} cid + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} + */ + readWith(cid, { codec, hasher, version, schema }) { + const result = LinkSchema.validate(cid, { + codec, + hasher, + version, + schema, + }) + + if (result.ok) { + const link = result.ok + const ok = link instanceof Link ? link : new Link({ link, codec, schema }) + return { ok } + } else { + return result + } + } + /** * - * @param {Schema.Linked} source + * @param {Schema.LinkOf} source * @param {LinkSettings} settings * @returns {Schema.ReadResult>} */ writeWith(source, settings) { - return this.validateWith(source.link(), settings) + return LinkSchema.validate(source.link(), settings) } /** @@ -2210,6 +2400,33 @@ class LinkSchema extends API { throw new Error('Can not create link of link') } + /** + * @returns {Schema.AttachmentSchema} + */ + attached() { + let attachment = this._attached + if (attachment == null) { + const attachment = new AttachmentSchema(this.settings) + this._attached = attachment + return attachment + } + + return attachment + } + + /** + * @param {Out} target + */ + embed(target) { + return this.attached().embed(target) + } + /** + * @param {Out} target + */ + attach(target) { + return this.attached().attach(target) + } + /** * @template {string} Prefix * @param {string} input @@ -2222,13 +2439,142 @@ class LinkSchema extends API { } /** - * @template {number} Code - * @template {number} Alg - * @template {1|0} Version * @template {unknown} Out * @template {unknown} In - * @param {LinkSettings} options - * @returns {Schema.LinkSchema} + * @template {Schema.MulticodecCode} Code + * @template {Schema.MulticodecCode} Alg + * @template {Schema.UnknownLink['version']} V + * @extends {API, Schema.IPLDView, LinkSettings>} + * @implements {Schema.AttachmentSchema} + */ +class AttachmentSchema extends API { + /** + * @param {Schema.IPLDView} source + * @param {LinkSettings} settings + * @param {Schema.Region} [region] + */ + readWith(source, { codec, schema }, region = emptyStore) { + const link = /** @type {Schema.Link} */ (source.link()) + const block = source.root ? source.root : region.get(`${link}`) + /** @type {Uint8Array|undefined} */ + const bytes = block + ? block.bytes + : link.multihash.code === identity.code + ? link.multihash.digest + : undefined + + if (bytes) { + const attachment = new Attachment({ + root: { cid: link, bytes }, + codec, + schema, + store: region, + }) + return { ok: attachment } + } else { + return error(`Could not find block for ${link}`) + } + } + /** + * @param {Schema.Attachment} attachment + * @param {LinkSettings} settings + * @returns {Schema.ReadResult>} + */ + writeWith(attachment, { codec, schema }) { + if (attachment instanceof Attachment) { + return { ok: attachment } + } + + console.log('!!!!!!!!!') + try { + const link = attachment.link() + const out = attachment.resolve() + const data = schema.tryTo(out) + if (data.error) { + return data + } else { + /** @type {Uint8Array} */ + const bytes = codec.encode(data.ok) + /** @type {Required>} */ + const root = { cid: link, bytes, data: out } + const view = new Attachment({ root, codec, schema }) + return { ok: view } + } + } catch (cause) { + return { error: /** @type {Error} */ (cause) } + } + } + + /** + * @param {Out} target + * @param {{hasher?: Schema.MultihashHasher }} options + */ + async attach(target, { hasher = /** @type {*} */ (sha256) } = {}) { + const { schema, codec } = this.settings + const result = schema.tryTo(target) + if (result.error) { + throw result.error + } + const data = result.ok + /** @type {Uint8Array} */ + const bytes = codec.encode(data) + const digest = await hasher.digest(bytes) + /** @type {Schema.Link} */ + const cid = createLink(codec.code, digest) + + const store = new Map() + for (const block of Attachment.iterateIPLDBlocks(target)) { + store.set(`${block.cid}`, block) + } + store.set(`${cid}`, { bytes, cid }) + + return new Attachment({ + codec, + schema, + root: { bytes, cid }, + store, + }) + } + + /** + * @param {Out} target + */ + embed(target) { + const { schema, codec } = this.settings + const result = schema.tryTo(target) + if (result.error) { + throw result.error + } + const data = result.ok + /** @type {Uint8Array} */ + const bytes = codec.encode(data) + const digest = identity.digest(bytes) + /** @type {Schema.Link} */ + const cid = createLink(codec.code, digest) + + return new Attachment({ + codec, + schema, + root: { bytes, cid }, + }) + } + + /** + * @returns {never} + */ + link() { + throw new Error('Can not create link of link') + } +} + +/** + * @template {unknown} Out + * @template In + * @template {Schema.BlockCodec} Codec + * @template {Schema.MultihashHasher} Hasher + * @template {Schema.UnknownLink['version']} Version + * @param {LinkSettings} options + * @returns {Schema.LinkSchema} */ export const link = options => new LinkSchema(options) @@ -2241,9 +2587,10 @@ class Variant extends API { /** * @param {Schema.InferVariantInput} input * @param {Choices} variants + * @param {Schema.Region} [context] * @returns {Schema.ReadResult>} */ - readWith(input, variants) { + readWith(input, variants, context) { if (typeof input != 'object' || input === null || Array.isArray(input)) { return typeError({ expect: 'object', @@ -2259,7 +2606,7 @@ class Variant extends API { const reader = key ? variants[key] : undefined if (reader) { - const result = reader.read(input[key]) + const result = reader.tryFrom(input[key], context) return result.error ? memberError({ at: key, cause: result.error }) : { @@ -2268,7 +2615,7 @@ class Variant extends API { }), } } else if (variants._) { - const result = variants._.read(input) + const result = variants._.tryFrom(input, context) return result.error ? result : { ok: /** @type {Schema.InferVariant} */ ({ _: result.ok }) } @@ -2292,7 +2639,7 @@ class Variant extends API { * @param {E} [fallback] */ match(input, fallback) { - const result = this.read(/** @type {*} */ (input)) + const result = this.tryFrom(/** @type {*} */ (input)) if (result.error) { if (fallback !== undefined) { return [null, fallback] diff --git a/packages/core/src/schema/text.js b/packages/core/src/schema/text.js index cbf13afe..b712dbc8 100644 --- a/packages/core/src/schema/text.js +++ b/packages/core/src/schema/text.js @@ -13,7 +13,7 @@ export const text = match /** * @param {unknown} input */ -export const read = input => schema.read(input) +export const read = input => schema.tryFrom(input) /** * @extends {Schema.API} diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index c8221212..e36de352 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -20,6 +20,8 @@ import { BuildOptions, ByteView, } from '@ucanto/interface' +import type * as identity from '../identity' +import type * as sha256 from '../sha256' export type { Link, @@ -41,33 +43,36 @@ export type { Await, } -export interface Reader { - read: (input: I) => Result -} - -export interface Writer { - write: (output: O) => Result -} - export type { Error, Result } export type ReadResult = Result -export interface Convert - extends Reader, - Writer {} +export interface From { + tryFrom(value: T, context?: Region): ReadResult +} -export interface From { - from(value: T): Self +export interface To { + tryTo(value: Self): ReadResult } -export interface To { - to(value: Self): T +export interface Convert + extends From, + To {} + +export interface Conform { + conform(value: unknown): Conformance } -export interface Into extends From, To {} +export type Conformance = Result< + Self, + Reason +> + +export interface BlockLoader extends From {} -export interface BlockLoader extends Reader {} +export interface Region { + get(id: string): undefined | { bytes: Uint8Array } +} export interface Schema< Out extends unknown = unknown, @@ -97,14 +102,14 @@ export interface Schema< // conforms(value: unknown): value is Out link< - Code extends MulticodecCode, - Alg extends MulticodecCode, - V extends UnknownLink['version'] + Codec extends BlockCodec, + Hasher extends MultihashHasher = MultihashHasher, + V extends UnknownLink['version'] = 1 >(options?: { - codec?: BlockCodec + codec?: Codec version?: V - hasher?: MultihashHasher - }): LinkSchema + hasher?: Hasher + }): LinkSchema // attach< // Code extends MulticodecCode, @@ -118,7 +123,7 @@ export interface Schema< // with(context: { store: BlockStore }): Schema - // asDAG(): Schema + // resolve(settings: { store: BlockStore }): Schema // codec: BlockCodec // hasher: MultihashHasher @@ -142,7 +147,10 @@ export interface Schema< // OUT: Out } -export interface PropertySchema {} +export interface DAGRoot { + link(): Link + resolve(link: Link): Uint8Array +} export interface ByteSchemaSettings< Model, @@ -165,8 +173,6 @@ export interface BytesSchema< refine( schema: Convert ): BytesSchema - - create(value: Out): BlockView } export interface BlockView< @@ -176,7 +182,7 @@ export interface BlockView< code: Code encode(): ByteView decode(): T - embed(): Linked + embed(): Attachment detach< Alg extends MulticodecCode = MulticodecCode<0x12, 'sha2-256'> @@ -190,7 +196,8 @@ export interface DAGView< Code extends MulticodecCode = MulticodecCode, Alg extends MulticodecCode = MulticodecCode, V extends UnknownLink['version'] = 1 -> extends IPLDView { +> extends IPLDView, + Attachment { decode(): T link(): Link } @@ -202,17 +209,31 @@ export type ToBlock = { infer Alg, infer V > - ? Linked + ? Attachment : T[K] } -export interface Linked< +export interface LinkOf< T extends unknown = unknown, Code extends MulticodecCode = MulticodecCode, Alg extends MulticodecCode = MulticodecCode, V extends UnknownLink['version'] = 1 > extends Link { - resolve(store?: BlockStore): ResolvedLink + resolve(region: Region): ResolvedLink + select(region: Region): Attachment +} + +export interface Attachment< + T extends unknown = unknown, + Code extends MulticodecCode = MulticodecCode, + Alg extends MulticodecCode = MulticodecCode, + V extends UnknownLink['version'] = 1 +> extends Link, + IPLDView { + link(): Link + resolve(): ResolvedLink + + with(context: { store?: Region }): Attachment } export interface CompiledView extends IPLDView { @@ -246,15 +267,17 @@ export interface LinkSchema< Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, IntoLink> { +> extends Schema, IntoLink> { link(): never - // attach(): AttachmentSchema, Code, Alg, V> - // resolve(): Schema, O | IPLDView> + attached(): AttachmentSchema parse( source: string, base?: MultibaseDecoder - ): Linked + ): LinkOf + + embed(target: Out): Attachment + attach(target: Out): Await> } export interface IntoLink { @@ -262,11 +285,19 @@ export interface IntoLink { } export interface AttachmentSchema< - O extends unknown, + Target extends unknown, Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, IPLDView> {} +> extends Schema, IPLDView> { + link(): never + attach( + target: Target, + options?: { hasher?: MultihashHasher } + ): Await> + + embed(target: Target): Attachment +} export interface IPLDViewSource { root: Block @@ -289,8 +320,9 @@ export interface ImplicitSchema export type NotUndefined = Exclude -export interface ArraySchema extends Schema { - element: Convert +export interface ArraySchema + extends Schema[], InferInput[]> { + element: C } /** @@ -314,7 +346,7 @@ export interface DictionarySchema MapRepresentation, Dictionary>, 'partial' > { - key: Reader + key: From value: Convert partial(): DictionarySchema @@ -449,7 +481,7 @@ export type InferVariant = { [Case in keyof Choices]: { [Key in Exclude]?: never } & { - [Key in Case]: Choices[Case] extends Reader ? T : never + [Key in Case]: Choices[Case] extends From ? T : never } }[keyof Choices] @@ -457,7 +489,7 @@ export type InferVariantInput = { [Case in keyof Choices]: { [Key in Exclude]?: never } & { - [Key in Case]: Choices[Case] extends Writer ? T : never + [Key in Case]: Choices[Case] extends To ? T : never } }[keyof Choices] @@ -467,24 +499,22 @@ export type InferVariantInput = { * use of `switch` statement for type narrowing. */ export type InferVariantMatch = { - [Branch in keyof Choices]: Choices[Branch] extends Reader + [Branch in keyof Choices]: Choices[Branch] extends From ? [Branch, Value] : never }[keyof Choices] -export type InferOptionalStructShape = { +export type InferOptionalStructShape = { [K in keyof U]: InferOptionalReader } -export type InferOptionalReader = R extends Reader - ? Reader +export type InferOptionalReader = R extends From + ? From : R export interface StringSchema extends Schema { - refine( - convert: Reader - ): StringSchema + refine(convert: From): StringSchema constraint(check: (value: Out) => ReadResult): StringSchema startsWith( prefix: Prefix @@ -502,33 +532,29 @@ export type Branded = T & { export type Integer = number & Phantom<{ typeof: 'integer' }> export type Float = number & Phantom<{ typeof: 'float' }> -export type Infer = T extends Reader ? T : never -export type InferInput = T extends Writer - ? T - : never +export type Infer = T extends From ? T : never +export type InferInput = T extends To ? T : never -export type InferIntersection = { +export type InferIntersection = { [K in keyof U]: (input: Infer) => void }[number] extends (input: infer T) => void ? T : never -export type InferIntersectionInput = { +export type InferIntersectionInput = { [K in keyof U]: (input: InferInput) => void }[number] extends (input: infer T) => void ? T : never -export type InferUnion = Infer -export type InferUnionInput = InferInput< - U[number] -> +export type InferUnion = Infer +export type InferUnionInput = InferInput -export type InferTuple = { +export type InferTuple = { [K in keyof U]: Infer } -export type InferTupleInput = { +export type InferTupleInput = { [K in keyof U]: InferInput } @@ -540,21 +566,18 @@ export type InferStructInput = MarkOptionals<{ [K in keyof Shape]: InferInput }> -export type InferStructSource = +export type InferStructSource = // MarkEmptyOptional< MarkOptionals<{ [K in keyof U]: InferSource }> // > -export type InferSource = U extends ImplicitSchema< - infer T, - any -> +export type InferSource = U extends ImplicitSchema ? T | undefined : U extends StructSchema ? InferStructSource - : U extends Reader + : U extends From ? T : never diff --git a/packages/core/src/schema/uri.js b/packages/core/src/schema/uri.js index d2fb1b34..ea3f2752 100644 --- a/packages/core/src/schema/uri.js +++ b/packages/core/src/schema/uri.js @@ -46,7 +46,7 @@ export const uri = () => schema /** * @param {unknown} input */ -export const read = input => schema.read(input) +export const read = input => schema.tryFrom(input) /** * @template {API.Protocol} P diff --git a/packages/core/src/sha256.js b/packages/core/src/sha256.js new file mode 100644 index 00000000..015df075 --- /dev/null +++ b/packages/core/src/sha256.js @@ -0,0 +1,9 @@ +import * as API from '@ucanto/interface' + +import { sha256 } from 'multiformats/hashes/sha2' + +/** @type {API.MulticodecCode} */ +export const code = sha256.code +export const name = sha256.name +export const { encode } = sha256 +export const digest = sha256.digest.bind(sha256) diff --git a/packages/core/test/extra-schema.spec.js b/packages/core/test/extra-schema.spec.js index 98c4e8be..f7960f43 100644 --- a/packages/core/test/extra-schema.spec.js +++ b/packages/core/test/extra-schema.spec.js @@ -14,7 +14,7 @@ import * as API from '@ucanto/interface' for (const [input, expect] of dataset) { test(`URI.read(${JSON.stringify(input)}}`, () => { matchResult(URI.read(input), expect) - matchResult(URI.uri().read(input), expect) + matchResult(URI.uri().tryFrom(input), expect) }) } } @@ -54,7 +54,7 @@ test('URI.from', () => { test(`URI.match(${JSON.stringify({ protocol, })}).read(${JSON.stringify(input)})}}`, () => { - matchResult(URI.match({ protocol }).read(input), expect) + matchResult(URI.match({ protocol }).tryFrom(input), expect) }) } } @@ -83,7 +83,7 @@ test('URI.from', () => { test(`URI.match(${JSON.stringify({ protocol, })}).optional().read(${JSON.stringify(input)})}}`, () => { - matchResult(URI.match({ protocol }).optional().read(input), expect) + matchResult(URI.match({ protocol }).optional().tryFrom(input), expect) }) } } @@ -142,22 +142,22 @@ test('URI.from', () => { test('Schema.link()', () => { const schema = Schema.link() - matchResult(schema.read(input), out1 || { ok: input }) + matchResult(schema.tryFrom(input), out1 || { ok: input }) }) test(`Schema.link({ code: 0x70 }).read(${input})`, () => { const link = Schema.link({ code: 0x70 }) - matchResult(link.read(input), out2 || { ok: input }) + matchResult(link.tryFrom(input), out2 || { ok: input }) }) test(`Schema.link({ algorithm: 0x12 }).read(${input})`, () => { const link = Schema.link({ multihash: { code: 0x12 } }) - matchResult(link.read(input), out3 || { ok: input }) + matchResult(link.tryFrom(input), out3 || { ok: input }) }) test(`Schema.link({ version: 1 }).read(${input})`, () => { const link = Schema.link({ version: 1 }) - matchResult(link.read(input), out4 || { ok: input }) + matchResult(link.tryFrom(input), out4 || { ok: input }) }) test(`Link.optional().read(${input})`, () => { @@ -211,7 +211,7 @@ test('URI.from', () => { for (const [options, input, out] of dataset) { test(`Text.match({ pattern: ${options.pattern} }).read(${input})`, () => { - matchResult(Text.match(options).read(input), out) + matchResult(Text.match(options).tryFrom(input), out) }) } } @@ -252,7 +252,7 @@ test('URI.from', () => { const schema = options.pattern ? Text.match({ pattern: options.pattern }) : Text.text() - matchResult(schema.optional().read(input), out) + matchResult(schema.optional().tryFrom(input), out) }) } } @@ -307,7 +307,7 @@ test('URI.from', () => { for (const [options, input, out] of dataset) { test(`DID.match({ method: ${options.method} }).read(${input})`, () => { - matchResult(DID.match(options).read(input), out) + matchResult(DID.match(options).tryFrom(input), out) }) } } @@ -350,7 +350,7 @@ test('URI.from', () => { for (const [options, input, out] of dataset) { test(`DID.match({ method: "${options.method}" }).optional().read(${input})`, () => { const schema = options.method ? DID.match(options) : DID.did() - matchResult(schema.optional().read(input), out) + matchResult(schema.optional().tryFrom(input), out) }) } } diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index f50832ac..414810cb 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -1,7 +1,6 @@ import * as Schema from '../src/schema.js' -import { isLink } from '../src/lib.js' -import { CBOR, identity, sha256 } from '../src/dag.js' -import { CAR } from '../src/lib.js' +import { isLink, parseLink } from '../src/lib.js' +import { CBOR, identity, sha256, createStore, writeInto } from '../src/dag.js' import { test, assert } from './test.js' describe.only('IPLD Schema', () => { @@ -11,147 +10,309 @@ describe.only('IPLD Schema', () => { y: Schema.integer(), }) + const PointLink = Point.link({ codec: CBOR }) + + assert.equal( + PointLink.tryFrom(parseLink('bafkqaaa')).error?.message, + 'Expected link to be CID with 0x71 codec' + ) + + const onlyX = await CBOR.write({ x: 1 }) + const onlyXLink = await PointLink.from( + // @ts-expect-error - does not have y + onlyX.cid + ) + assert.ok(onlyX) + + assert.deepEqual( + onlyX.cid.toString(), + onlyXLink.link().toString(), + 'links match' + ) + + assert.throws( + () => onlyXLink.resolve(new Map()), + 'Block can not be resolved, please provide a store from which to resolve it' + ) + + assert.throws( + () => onlyXLink.resolve(new Map([[onlyX.cid.toString(), onlyX]])), + /invalid field "y"/ + ) + + const point = await CBOR.write({ x: 1, y: 2 }) + const pointLink = await PointLink.from(point.cid) + + equalLink(point.cid, pointLink) + + assert.throws( + () => pointLink.resolve(new Map()), + 'Block can not be resolved, please provide a store from which to resolve it' + ) + + assert.deepEqual( + pointLink.resolve(new Map([[point.cid.toString(), point]])), + { + x: 1, + y: 2, + } + ) + + const view = pointLink.select(new Map([[point.cid.toString(), point]])) + equalLink(view, point.cid) + + assert.deepEqual(view.resolve(), { x: 1, y: 2 }) + + assert.equal(isLink(PointLink.from(view)), true) + equalLink(PointLink.from(view), point.cid) + + const p2 = await PointLink.attach({ + x: 1, + y: 2, + }) + + assert.deepEqual(p2.link(), point.cid) + assert.deepEqual(p2.root.bytes, point.bytes) + }) + + test('embed links', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) + const Line = Schema.struct({ start: Point.link({ codec: CBOR }), - end: Point.link({ codec: CBOR }), + end: Point.link({ codec: CBOR }).attached(), }) - const cbor = Schema.bytes(CBOR) + const PointLink = Point.link({ codec: CBOR }) const line = Line.from({ - start: cbor - .refine(Point) - .create({ - x: 1, - y: 2, - }) - .embed(), - end: cbor - .refine(Point) - .create({ - x: 0, - y: 0, - }) - .embed(), + start: PointLink.embed({ + x: 1, + y: 2, + }), + end: PointLink.embed({ + x: 0, + y: 0, + }), }) - const car = Schema.bytes(CAR) - assert.ok(isLink(line.start), 'is a link') assert.ok(isLink(line.end), 'is a link') assert.equal(line.start.code, CBOR.code, 'is a CBOR link') assert.equal(line.start.multihash.code, identity.code, 'is a CBOR link') - assert.deepEqual(line.start.resolve(), { x: 1, y: 2 }) + assert.deepEqual(line.start.resolve(new Map()), { x: 1, y: 2 }) assert.deepEqual(line.end.resolve(), { x: 0, y: 0 }) }) - test.skip('attachment code', async () => { + test('attached links', async () => { const Point = Schema.struct({ x: Schema.integer(), y: Schema.integer(), }) const Line = Schema.struct({ - start: Point.attach(), - end: Point.attach(), + start: Point.link({ codec: CBOR }).attached(), + end: Point.link({ codec: CBOR }).attached(), }) - const $Point = Schema.bytes(CBOR).refine(Point) - const line = Line.from({ - start: await $Point.create({ x: 1, y: 2 }).detach({ hasher: sha256 }), - end: await $Point.create({ x: 1, y: 2 }).detach({ hasher: sha256 }), - }) + const int = Schema.integer() + const store = createStore() + const start = await writeInto({ x: int.from(0), y: int.from(0) }, store) + const end = await writeInto({ x: int.from(7), y: int.from(8) }, store) + const line = await writeInto({ start: start.cid, end: end.cid }, store) - assert.ok(isLink(line.start), 'is a link') - assert.ok(isLink(line.end), 'is a link') - assert.equal(line.start.code, CBOR.code, 'is a CBOR link') - assert.equal(line.start.multihash.code, identity.code, 'is a CBOR link') - }) - test.skip('cross block references', async () => { - const Content = Schema.bytes() - const DealDetail = Schema.struct({ - size: Schema.integer(), - commit: Schema.string(), - content: Content.link(), - }) + const Root = Line.link({ codec: CBOR }) + + const root = Root.parse(line.cid.toString()) + + { + const line = root.resolve(store) - const dealLink = DealDetail.link() - dealDetail.from({}) + assert.equal(line.start.resolve().x, 0) + assert.equal(line.start.resolve().y, 0) + assert.equal(line.end.resolve().x, 7) + assert.equal(line.end.resolve().y, 8) + } + + const PointLink = Point.link({ codec: CBOR }) + + { + const line = Line.from({ + start: PointLink.embed({ x: 0, y: 0 }), + end: await PointLink.attach({ + x: 1, + y: 2, + }), + }) + + assert.equal(line.start.code, CBOR.code) + assert.equal(line.start.multihash.code, identity.code) + assert.equal(line.end.code, CBOR.code) + assert.equal(line.end.multihash.code, sha256.code) + } + }) - Schema.debug(DealDetail).in.content + test('read from archive', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) - const detail = DealDetail.from({ - size: 1, - commit: 'a', - content: Content.link().parse('baakfa'), + const Line = Schema.struct({ + start: Point.link({ codec: CBOR }).attached(), + end: Point.link({ codec: CBOR }).attached(), }) - const cbor = Schema.bytes(CBOR) - const dealDetail = cbor.refine(DealDetail) - const dl = dealDetail.decode(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9])) - dl.content + const int = Schema.integer() + const archive = createStore() + const start = await writeInto({ x: int.from(0), y: int.from(0) }, archive) + const end = await writeInto({ x: int.from(7), y: int.from(8) }, archive) + const root = await writeInto({ start: start.cid, end: end.cid }, archive) - Schema.debug(cbor) + const Root = Line.link({ codec: CBOR }) - const thing = Schema.dictionary({ value: Schema.unknown() }) + const line = Root.parse(root.cid.toString()).resolve(archive) + assert.deepEqual(line.start.resolve(), { x: 0, y: 0 }) + assert.deepEqual(line.end.resolve(), { x: 7, y: 8 }) + }) - const out = Schema.dag({ - codec: CBOR, + test('build dag', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), }) - out.refine(DealDetail) + const Line = Schema.struct({ + start: Point.link({ codec: CBOR }).attached(), + end: Point.link({ codec: CBOR }).attached(), + }) - const DL = cbor.refine(DealDetail) + const start = await Point.link({ codec: CBOR }).attach({ + x: 1, + y: 2, + }) - const DL2 = Schema.dictionary({ - value: Schema.unknown(), - }).refine(DealDetail) + const CBORPoint = Point.link({ codec: CBOR }) + const end = await Point.link({ codec: CBOR }).attach({ + x: 3, + y: 4, + }) - const hello = Schema.string() - .startsWith('hello') - .endsWith('world') - .startsWith('hello ') + const PointLink = Point.link({ codec: CBOR }) + const PointAttachment = PointLink.attached() - const dl2 = DL.from(new Uint8Array()) - dl2.size + const BoxedPoint = Schema.struct({ + point: Point.link({ codec: CBOR }).attached(), + }) - // const a = {} + BoxedPoint.from({ point: await PointLink.attach({ x: 1, y: 2 }) }) + const BoxedPointLink = BoxedPoint.link({ codec: CBOR }) - // const out = await Deal.compile({ - // size: 2, - // commit: 'a', - // }) + BoxedPointLink.embed({ + point: PointLink.embed({ x: 1, y: 2 }), + }) - // const Offer = Deal.array() - // Deal.link().attach().OUT + PointAttachment.from - // const Aggregate = Schema.struct({ - // offer: Offer.link().resolve(), - // attachment: Offer.link().attach(), - // }) + const point = await PointLink.attach({ + x: 1, + y: 2, + }) - // Offer.link().IN - // Offer.link().attach().IN + assert.deepEqual(point.resolve(), { x: 1, y: 2 }) - // const offer = Offer.from([ - // { size: 1, commit: 'a' }, - // { size: 2, commit: 'b' }, - // ]) + const lineEmbed = await Line.link({ codec: CBOR }).attach({ + start: point, + end: point, + }) - // Aggregate.IN.attachment + const embed = lineEmbed.resolve() + assert.deepEqual(embed.start.link(), point.link()) + assert.deepEqual(embed.start.resolve(), { x: 1, y: 2 }) + assert.deepEqual(embed.end.link(), point.link()) + assert.deepEqual(embed.end.resolve(), { x: 1, y: 2 }) - // const attachment = await Offer.attachment(offer) - // const aggregate = await Aggregate.compile({ - // offer, //: await Offer.attach(offer) - // attachment, - // }) + const blocks = Object.fromEntries( + [...lineEmbed.iterateIPLDBlocks()].map(block => [`${block.cid}`, block]) + ) - // const agg = aggregate.root.data + assert.ok(blocks[lineEmbed.link().toString()]) + assert.ok(blocks[point.link().toString()]) + }) - // agg.attachment.resolve()[0].commit + test('array of dags', async () => { + const Point = Schema.struct({ + x: Schema.integer(), + y: Schema.integer(), + }) - // aggregate.root.data.offer.resolve()[0].commit - // // const out = aggregate.offer.load() + const Polygon = Schema.array(Point.link({ codec: CBOR }).attached()) + + const polygon = await Polygon.link({ codec: CBOR }).attach([ + await Polygon.element.attach({ x: 1, y: 2 }), + await Polygon.element.attach({ x: 3, y: 4 }), + await Polygon.element.attach({ x: 5, y: 6 }), + ]) + + const compile = async (data = {}) => { + const { bytes, cid } = await CBOR.write(data) + return { bytes, cid, data } + } + + const link = async (data = {}) => { + const { cid } = await CBOR.write(data) + return cid + } + + assert.deepEqual( + [...polygon.iterateIPLDBlocks()], + [ + await compile({ x: 1, y: 2 }), + await compile({ x: 3, y: 4 }), + await compile({ x: 5, y: 6 }), + polygon.root, + ] + ) + + assert.deepEqual( + polygon.link(), + await link([ + await link({ x: 1, y: 2 }), + await link({ x: 3, y: 4 }), + await link({ x: 5, y: 6 }), + ]) + ) + + const region = new Map( + [...polygon.iterateIPLDBlocks()].map(block => [`${block.cid}`, block]) + ) + + const root = Polygon.link({ codec: CBOR }) + .parse(polygon.link().toString()) + .select(region) + + const replica = root.resolve() + + assert.deepEqual(replica, [ + await link({ x: 1, y: 2 }), + await link({ x: 3, y: 4 }), + await link({ x: 5, y: 6 }), + ]) + + assert.deepEqual(replica[0].resolve(), { x: 1, y: 2 }) + assert.deepEqual(replica[0].resolve(), { x: 3, y: 4 }) + assert.deepEqual(replica[0].resolve(), { x: 5, y: 6 }) }) }) + +/** + * + * @param {Schema.UnknownLink} actual + * @param {Schema.UnknownLink} expected + */ +const equalLink = (actual, expected) => + assert.deepEqual(CBOR.encode(actual), CBOR.encode(expected)) diff --git a/packages/core/test/link-schema.spec.js b/packages/core/test/link-schema.spec.js index 70d3c692..b77788ee 100644 --- a/packages/core/test/link-schema.spec.js +++ b/packages/core/test/link-schema.spec.js @@ -27,16 +27,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), { ok: link }, `${link}`) + assert.deepEqual(Schema.link().tryFrom(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), { ok: link }) + assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { - matchError(schema.read(link), /Expected link to be CID version/) + matchError(schema.tryFrom(link), /Expected link to be CID version/) } }) } @@ -45,9 +45,12 @@ 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), { ok: link }) + assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { - matchError(schema.read(link), /Expected link to be CID with .* codec/) + matchError( + schema.tryFrom(link), + /Expected link to be CID with .* codec/ + ) } }) } @@ -56,10 +59,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), { ok: link }) + assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { matchError( - schema.read(link), + schema.tryFrom(link), /Expected link to be CID with .* hashing algorithm/ ) } @@ -72,9 +75,9 @@ for (const link of links) { multihash: { digest: new Uint8Array(digest) }, }) if (link.multihash.digest === digest) { - assert.deepEqual(schema.read(link), { ok: link }) + assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { - matchError(schema.read(link), /Expected link with .* hash digest/) + matchError(schema.tryFrom(link), /Expected link with .* hash digest/) } }) } @@ -87,7 +90,7 @@ test('struct().link()', () => { }) const PointLink = Point.link() - assert.equal(PointLink.read(fixtures.pb).ok, fixtures.pb) + assert.equal(PointLink.tryFrom(fixtures.pb).ok, fixtures.pb) assert.throws(() => PointLink.link(), /link of link/) }) @@ -101,8 +104,8 @@ test('struct().link({ codec })', () => { codec: CBOR, }) - assert.match(PointLink.read(fixtures.pb).error?.message || '', /0x71 code/) - assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) + assert.match(PointLink.tryFrom(fixtures.pb).error?.message || '', /0x71 code/) + assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) }) test('struct().link({ hasher })', () => { @@ -115,10 +118,10 @@ test('struct().link({ hasher })', () => { }) assert.match( - PointLink.read(fixtures.sha512).error?.message || '', + PointLink.tryFrom(fixtures.sha512).error?.message || '', /0x12 hashing/ ) - assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) + assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) }) test('struct().link({ hasher })', () => { @@ -130,6 +133,6 @@ test('struct().link({ hasher })', () => { version: 1, }) - assert.match(PointLink.read(fixtures.pb).error?.message || '', /version 1/) - assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) + assert.match(PointLink.tryFrom(fixtures.pb).error?.message || '', /version 1/) + assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) }) diff --git a/packages/core/test/map-schema.spec.js b/packages/core/test/map-schema.spec.js index fde926c5..afb5620f 100644 --- a/packages/core/test/map-schema.spec.js +++ b/packages/core/test/map-schema.spec.js @@ -15,12 +15,12 @@ test('.partial on structs', () => { y: Schema.integer().optional(), }) - assert.deepEqual(PartialPoint.from({}), {}) - assert.deepEqual(PartialPoint.from({ x: 1 }), { x: 1 }) - assert.deepEqual(PartialPoint.from({ x: 1, y: 2 }), { x: 1, y: 2 }) - assert.deepEqual(PartialPoint.from({ y: 2 }), { y: 2 }) - assert.deepEqual(PartialPoint.from({ x: undefined }), {}) - assert.deepEqual(PartialPoint.from({ x: undefined, y: 2 }), { y: 2 }) + assert.deepEqual(PartialPoint.tryFrom({}), {}) + assert.deepEqual(PartialPoint.tryFrom({ x: 1 }), { x: 1 }) + assert.deepEqual(PartialPoint.tryFrom({ x: 1, y: 2 }), { x: 1, y: 2 }) + assert.deepEqual(PartialPoint.tryFrom({ y: 2 }), { y: 2 }) + assert.deepEqual(PartialPoint.tryFrom({ x: undefined }), {}) + assert.deepEqual(PartialPoint.tryFrom({ x: undefined, y: 2 }), { y: 2 }) assert.throws( () => @@ -39,12 +39,12 @@ test('.partial on dicts', () => { assert.equal(IntsMaybe.key, Ints.key) assert.deepEqual(IntsMaybe.value, Schema.integer().optional()) - assert.deepEqual(IntsMaybe.from({}), {}) - assert.deepEqual(IntsMaybe.from({ x: 1 }), { x: 1 }) - assert.deepEqual(IntsMaybe.from({ x: 1, y: 2 }), { x: 1, y: 2 }) - assert.deepEqual(IntsMaybe.from({ y: 2 }), { y: 2 }) - assert.deepEqual(IntsMaybe.from({ x: undefined }), {}) - assert.deepEqual(IntsMaybe.from({ x: undefined, y: 2 }), { y: 2 }) + assert.deepEqual(IntsMaybe.tryFrom({}), {}) + assert.deepEqual(IntsMaybe.tryFrom({ x: 1 }), { x: 1 }) + assert.deepEqual(IntsMaybe.tryFrom({ x: 1, y: 2 }), { x: 1, y: 2 }) + assert.deepEqual(IntsMaybe.tryFrom({ y: 2 }), { y: 2 }) + assert.deepEqual(IntsMaybe.tryFrom({ x: undefined }), {}) + assert.deepEqual(IntsMaybe.tryFrom({ x: undefined, y: 2 }), { y: 2 }) }) test('.creteIPLDView', async () => { diff --git a/packages/core/test/schema.spec.js b/packages/core/test/schema.spec.js index 4517c4a4..20b36755 100644 --- a/packages/core/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -5,11 +5,11 @@ import fixtures from './schema/fixtures.js' for (const { input, schema, expect, inputLabel, skip, only } of fixtures()) { const unit = skip ? test.skip : only ? test.only : test - unit(`${schema}.read(${inputLabel})`, () => { - const result = schema.read(input) + unit(`${schema}.tryFrom(${inputLabel})`, () => { + const result = schema.tryFrom(input) if (expect.error) { - // console.log(`${schema}.read(${inputLabel})`, result, expect.error) + // console.log(`${schema}.tryFrom(${inputLabel})`, result, expect.error) matchError(result, expect.error) } else { assert.deepEqual( @@ -47,12 +47,12 @@ test('string startsWith & endsWith', () => { 'string().refine(startsWith("hello")).refine(startsWith("hi"))' ) - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -61,7 +61,7 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) + assert.deepEqual(hello.tryFrom('hello world'), { ok: 'hello world' }) }) test('string startsWith', () => { @@ -72,8 +72,8 @@ test('string startsWith', () => { /** @type {Schema.StringSchema<`hello${string}`>} */ const hello = Schema.string().startsWith('hello') - assert.deepEqual(hello.read('hello world!'), { ok: 'hello world!' }) - assert.deepInclude(hello.read('hi world').error, { + assert.deepEqual(hello.tryFrom('hello world!'), { ok: 'hello world!' }) + assert.deepInclude(hello.tryFrom('hi world').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world"`, }) @@ -87,9 +87,9 @@ test('string endsWith', () => { /** @type {Schema.StringSchema<`${string} world`>} */ const greet = Schema.string().endsWith(' world') - 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, { + assert.deepEqual(greet.tryFrom('hello world'), { ok: 'hello world' }) + assert.deepEqual(greet.tryFrom('hi world'), { ok: 'hi world' }) + assert.deepInclude(greet.tryFrom('hello world!').error, { name: 'SchemaError', message: `Expect string to end with " world" instead got "hello world!"`, }) @@ -114,24 +114,24 @@ test('string startsWith/endsWith', () => { `string().refine(endsWith("!")).refine(startsWith("hello"))` ) - 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, { + assert.deepEqual(hello1.tryFrom('hello world!'), { ok: 'hello world!' }) + assert.deepEqual(hello2.tryFrom('hello world!'), { ok: 'hello world!' }) + assert.deepInclude(hello1.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to end with "!" instead got "hello world"`, }) - assert.deepInclude(hello2.read('hello world').error, { + assert.deepInclude(hello2.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to end with "!" instead got "hello world"`, }) - assert.deepInclude(hello1.read('hi world!').error, { + assert.deepInclude(hello1.tryFrom('hi world!').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world!"`, }) - assert.deepInclude(hello2.read('hi world!').error, { + assert.deepInclude(hello2.tryFrom('hi world!').error, { name: 'SchemaError', message: `Expect string to start with "hello" instead got "hi world!"`, }) @@ -142,12 +142,12 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hi${string}`>} */ const typeofImpossible = impossible - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -156,23 +156,23 @@ test('string startsWith & endsWith', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) + assert.deepEqual(hello.tryFrom('hello world'), { ok: 'hello world' }) }) test('string().refine', () => { const impossible = Schema.string() .refine(Schema.startsWith('hello')) + // @ts-expect-error - catches invalid type .refine(Schema.startsWith('hi')) - /** @type {Schema.StringSchema<`hello${string}` & `hi${string}`>} */ const typeofImpossible = impossible - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) - assert.deepInclude(impossible.read('hello world').error, { + assert.deepInclude(impossible.tryFrom('hello world').error, { name: 'SchemaError', message: `Expect string to start with "hi" instead got "hello world"`, }) @@ -184,14 +184,14 @@ test('string().refine', () => { /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}`>} */ const typeofHello = hello - assert.deepEqual(hello.read('hello world'), { ok: 'hello world' }) + assert.deepEqual(hello.tryFrom('hello world'), { ok: 'hello world' }) const greet = hello.refine({ /** * @template {string} In * @param {In} hello */ - read(hello) { + tryFrom(hello) { if (hello.length === 11) { return Schema.ok(/** @type {In & {length: 11}} */ (hello)) } else { @@ -203,15 +203,15 @@ test('string().refine', () => { const typeofGreet = greet assert.equal( - greet.read('hello world').ok, + greet.tryFrom('hello world').ok, /** @type {unknown} */ ('hello world') ) assert.equal( - greet.read('hello Julia').ok, + greet.tryFrom('hello Julia').ok, /** @type {unknown} */ ('hello Julia') ) - assert.deepInclude(greet.read('hello Jack').error, { + assert.deepInclude(greet.tryFrom('hello Jack').error, { name: 'SchemaError', message: 'Expected string with 11 chars', }) @@ -240,7 +240,7 @@ test.skip('literal("foo").default("bar") throws', () => { test('default on literal has default', () => { const schema = Schema.literal('foo').implicit() - assert.deepEqual(schema.read(undefined), Schema.ok('foo')) + assert.deepEqual(schema.tryFrom(undefined), Schema.ok('foo')) }) test('literal has value field', () => { @@ -280,7 +280,7 @@ test('struct', () => { y: Schema.integer(), }) - const p1 = Point.read({ + const p1 = Point.tryFrom({ x: 1, y: 2, }) @@ -288,7 +288,7 @@ test('struct', () => { matchError(p1, /field "type".*expect.*"Point".*got undefined/is) - const p2 = Point.read({ + const p2 = Point.tryFrom({ type: 'Point', x: 1, y: 1, @@ -301,7 +301,7 @@ test('struct', () => { }, }) - const p3 = Point.read({ + const p3 = Point.tryFrom({ type: 'Point', x: 1, y: 1.1, @@ -311,7 +311,8 @@ test('struct', () => { matchError(p3, /field "y".*expect.*integer.*got 1.1/is) matchError( - Point.read(['h', 'e', 'l', null, 'l', 'o']), + // @ts-expect-error - bad input + Point.tryFrom(['h', 'e', 'l', null, 'l', 'o']), /Expected value of type object instead got array/ ) }) @@ -322,10 +323,10 @@ test('struct with defaults', () => { y: Schema.number().implicit(0), }) - 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 } }) + assert.deepEqual(Point.tryFrom({}), { ok: { x: 0, y: 0 } }) + assert.deepEqual(Point.tryFrom({ x: 2 }), { ok: { x: 2, y: 0 } }) + assert.deepEqual(Point.tryFrom({ x: 2, y: 7 }), { ok: { x: 2, y: 7 } }) + assert.deepEqual(Point.tryFrom({ y: 7 }), { ok: { x: 0, y: 7 } }) }) test('struct with literals', () => { @@ -335,10 +336,10 @@ test('struct with literals', () => { y: Schema.number(), }) - assert.deepEqual(Point.read({ x: 0, y: 0, z: 0 }), { + assert.deepEqual(Point.tryFrom({ 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) + matchError(Point.tryFrom({ x: 1, y: 1, z: 1 }), /"z".*expect.* 0 .* got 1/is) }) test('bad struct def', () => { @@ -356,7 +357,7 @@ test('bad struct def', () => { 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' }), { + assert.deepEqual(schema.tryFrom({ a: null, b: true, c: 'hi' }), { ok: { a: null, b: true, @@ -365,12 +366,12 @@ test('struct with null literal', () => { }) matchError( - schema.read({ a: null, b: false, c: '' }), + schema.tryFrom({ a: null, b: false, c: '' }), /"b".*expect.* true .* got false/is ) matchError( - schema.read({ b: true, c: '' }), + schema.tryFrom({ b: true, c: '' }), /"a".*expect.* null .* got undefined/is ) }) @@ -378,73 +379,89 @@ test('struct with null literal', () => { test('lessThan', () => { const schema = Schema.number().lessThan(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/) + assert.deepEqual(schema.tryFrom(10), { ok: 10 }) + matchError(schema.tryFrom(127), /127 < 100/) + matchError(schema.tryFrom(Infinity), /Infinity < 100/) + matchError(schema.tryFrom(NaN), /NaN < 100/) }) test('greaterThan', () => { const schema = Schema.number().greaterThan(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/) + assert.deepEqual(schema.tryFrom(127), { ok: 127 }) + matchError(schema.tryFrom(12), /12 > 100/) + assert.deepEqual(schema.tryFrom(Infinity), { ok: Infinity }) + matchError(schema.tryFrom(NaN), /NaN > 100/) }) test('number().greaterThan().lessThan()', () => { const schema = Schema.number().greaterThan(3).lessThan(117) - 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/) + assert.deepEqual(schema.tryFrom(4), { ok: 4 }) + assert.deepEqual(schema.tryFrom(116), { ok: 116 }) + matchError(schema.tryFrom(117), /117 < 117/) + matchError(schema.tryFrom(3), /3 > 3/) + matchError(schema.tryFrom(127), /127 < 117/) + matchError(schema.tryFrom(0), /0 > 3/) + matchError(schema.tryFrom(Infinity), /Infinity < 117/) + matchError(schema.tryFrom(NaN), /NaN > 3/) }) test('enum', () => { const schema = Schema.enum(['Red', 'Green', 'Blue']) assert.equal(schema.toString(), 'Red|Green|Blue') - assert.deepEqual(schema.read('Red'), { ok: 'Red' }) - assert.deepEqual(schema.read('Blue'), { ok: 'Blue' }) - assert.deepEqual(schema.read('Green'), { ok: 'Green' }) + assert.deepEqual(schema.tryFrom('Red'), { ok: 'Red' }) + assert.deepEqual(schema.tryFrom('Blue'), { ok: 'Blue' }) + assert.deepEqual(schema.tryFrom('Green'), { ok: 'Green' }) - matchError(schema.read('red'), /expect.* Red\|Green\|Blue .* got "red"/is) - matchError(schema.read(5), /expect.* Red\|Green\|Blue .* got 5/is) + matchError(schema.tryFrom('red'), /expect.* Red\|Green\|Blue .* got "red"/is) + matchError( + schema.tryFrom( + // @ts-expect-error - invalid arg + 5 + ), + /expect.* Red\|Green\|Blue .* got 5/is + ) }) test('tuple', () => { const schema = Schema.tuple([Schema.string(), Schema.integer()]) matchError( - schema.read([, undefined]), + schema.tryFrom([ + , + // @ts-expect-error - not an int + undefined, + ]), /invalid element at 0.*expect.*string.*got undefined/is ) matchError( - schema.read([0, 'hello']), + schema.tryFrom( + // @ts-expect-error - invalid arg + [0, 'hello'] + ), /invalid element at 0.*expect.*string.*got 0/is ) matchError( - schema.read(['0', '1']), + schema.tryFrom( + // @ts-expect-error - invalid arg + ['0', '1'] + ), /invalid element at 1.*expect.*integer.*got "1"/is ) matchError( - schema.read(['0', Infinity]), + schema.tryFrom(['0', Infinity]), /invalid element at 1.*expect.*integer.*got Infinity/is ) matchError( - schema.read(['0', NaN]), + schema.tryFrom(['0', NaN]), /invalid element at 1.*expect.*integer.*got NaN/is ) matchError( - schema.read(['0', 0.2]), + schema.tryFrom(['0', 0.2]), /invalid element at 1.*expect.*integer.*got 0.2/is ) - assert.deepEqual(schema.read(['x', 0]), { ok: ['x', 0] }) + assert.deepEqual(schema.tryFrom(['x', 0]), { ok: ['x', 0] }) }) test('extend API', () => { @@ -474,7 +491,7 @@ test('extend API', () => { const schema = new DIDString('key') assert.equal(schema.toString(), 'new DIDString()') matchError( - schema.read( + schema.tryFrom( // @ts-expect-error 54 ), @@ -482,12 +499,18 @@ test('extend API', () => { ) matchError( - schema.read('did:echo:foo'), + schema.tryFrom('did:echo:foo'), /Expected did:key URI instead got did:echo:foo/ ) const didKey = Schema.string().refine(new DIDString('key')) - matchError(didKey.read(54), /Expect.* string instead got 54/is) + matchError( + didKey.tryFrom( + // @ts-expect-error - invalid arg + 54 + ), + /Expect.* string instead got 54/is + ) } }) @@ -511,7 +534,7 @@ test('refine', () => { /** * @param {T[]} array */ - read(array) { + tryFrom(array) { return array.length > 0 ? Schema.ok(array) : Schema.error('Array expected to have elements') @@ -521,24 +544,35 @@ test('refine', () => { const schema = Schema.array(Schema.string()).refine(new NonEmpty()) assert.equal(schema.toString(), 'array(string()).refine(new NonEmpty())') - 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) + matchError(schema.tryFrom([]), /Array expected to have elements/) + assert.deepEqual(schema.tryFrom(['hello', 'world']), { + ok: ['hello', 'world'], + }) + matchError( + schema.tryFrom( + // @ts-expect-error - invalid arg + null + ), + /expect.* array .*got null/is + ) }) test('brand', () => { const digit = Schema.integer() .refine({ - read(n) { + tryFrom(n) { return n >= 0 && n <= 9 ? Schema.ok(n) : Schema.error(`Expected digit but got ${n}`) }, + tryTo(n) { + return { ok: n } + }, }) .brand('digit') - matchError(digit.read(10), /Expected digit but got 10/) - matchError(digit.read(2.7), /Expected value of type integer/) + matchError(digit.tryFrom(10), /Expected digit but got 10/) + matchError(digit.tryFrom(2.7), /Expected value of type integer/) assert.equal(digit.from(2), 2) /** @param {Schema.Infer} n */ @@ -569,16 +603,16 @@ test('optional.default removes undefined from type', () => { /** @type {Schema.Schema} */ const castOk = schema2 - assert.deepEqual(schema1.read(undefined), { ok: undefined }) - assert.deepEqual(schema2.read(undefined), { ok: '' }) + assert.deepEqual(schema1.tryFrom(undefined), { ok: undefined }) + assert.deepEqual(schema2.tryFrom(undefined), { ok: '' }) }) test('.default("one").default("two")', () => { const schema = Schema.string().implicit('one').implicit('two') assert.equal(schema.value, 'two') - assert.deepEqual(schema.read(undefined), { ok: 'two' }) - assert.deepEqual(schema.read('three'), { ok: 'three' }) + assert.deepEqual(schema.tryFrom(undefined), { ok: 'two' }) + assert.deepEqual(schema.tryFrom('three'), { ok: 'three' }) }) test.skip('default throws on invalid default', () => { @@ -602,12 +636,15 @@ test('unknown with default', () => { test('default swaps undefined even if decodes to undefined', () => { /** @type {Schema.Schema} */ const schema = Schema.unknown().refine({ - read(value) { + tryFrom(value) { + return { ok: value === null ? undefined : value } + }, + tryTo(value) { return { ok: value === null ? undefined : value } }, }) - assert.deepEqual(schema.implicit('X').read(null), { ok: 'X' }) + assert.deepEqual(schema.implicit('X').tryFrom(null), { ok: 'X' }) }) test('record defaults', () => { @@ -620,7 +657,13 @@ test('record defaults', () => { z: Schema.integer(), }) - matchError(Point.read(undefined), /expect.* object .* got undefined/is) + matchError( + Point.tryFrom( + // @ts-expect-error - bad input + undefined + ), + /expect.* object .* got undefined/is + ) assert.deepEqual(Point.create(), { x: 1, }) @@ -628,20 +671,20 @@ test('record defaults', () => { x: 1, }) - assert.deepEqual(Point.read({}), { + assert.deepEqual(Point.tryFrom({}), { ok: { x: 1, }, }) - assert.deepEqual(Point.read({ y: 2 }), { + assert.deepEqual(Point.tryFrom({ y: 2 }), { ok: { x: 1, y: 2, }, }) - assert.deepEqual(Point.read({ x: 2, y: 2 }), { + assert.deepEqual(Point.tryFrom({ x: 2, y: 2 }), { ok: { x: 2, y: 2, diff --git a/packages/core/test/variant-schema.spec.js b/packages/core/test/variant-schema.spec.js index c8e76046..c929fde5 100644 --- a/packages/core/test/variant-schema.spec.js +++ b/packages/core/test/variant-schema.spec.js @@ -10,34 +10,40 @@ const Shape = Schema.variant({ }) test('variant', () => { - assert.deepEqual(Shape.read({ circle: { radius: 1 } }), { + assert.deepEqual(Shape.tryFrom({ circle: { radius: 1 } }), { ok: { circle: { radius: 1 }, }, }) - assert.deepEqual(Shape.read({ rectangle: { width: 1, height: 2 } }), { + assert.deepEqual(Shape.tryFrom({ rectangle: { width: 1, height: 2 } }), { ok: { rectangle: { width: 1, height: 2 }, }, }) matchError( - Shape.read({ rectangle: { width: 1 } }), + Shape.tryFrom({ rectangle: { width: 1 } }), /contains invalid field "rectangle"/ ) matchError( - Shape.read({ square: { width: 5 } }), + Shape.tryFrom({ square: { width: 5 } }), /Expected an object with one of the these keys: circle, rectangle instead got object with key square/ ) - matchError(Shape.read([]), /Expected value of type object instead got array/) + matchError( + Shape.tryFrom([]), + /Expected value of type object instead got array/ + ) }) test('variant can not have extra fields', () => { matchError( - Shape.read({ rectangle: { width: 1, height: 2 }, circle: { radius: 3 } }), + Shape.tryFrom({ + rectangle: { width: 1, height: 2 }, + circle: { radius: 3 }, + }), /Expected an object with a single key instead got object with keys circle, rectangle/ ) }) @@ -53,17 +59,17 @@ test('variant with default match', () => { _: Schema.dictionary({ value: Schema.unknown() }), }) - assert.deepEqual(Shapes.read({ circle: { radius: 1 } }), { + assert.deepEqual(Shapes.tryFrom({ circle: { radius: 1 } }), { ok: { circle: { radius: 1 } }, }) - assert.deepEqual(Shapes.read({ rectangle: { width: 10, height: 7 } }), { + assert.deepEqual(Shapes.tryFrom({ rectangle: { width: 10, height: 7 } }), { ok: { rectangle: { width: 10, height: 7 }, }, }) - assert.deepEqual(Shapes.read({ square: { width: 5 } }), { + assert.deepEqual(Shapes.tryFrom({ square: { width: 5 } }), { ok: { _: { square: { width: 5 } } }, }) }) @@ -81,17 +87,17 @@ test('variant with default', () => { }), }) - assert.deepEqual(Shapes.read({ circle: { radius: 1 } }), { + assert.deepEqual(Shapes.tryFrom({ circle: { radius: 1 } }), { ok: { circle: { radius: 1 } }, }) - assert.deepEqual(Shapes.read({ rectangle: { width: 10, height: 7 } }), { + assert.deepEqual(Shapes.tryFrom({ rectangle: { width: 10, height: 7 } }), { ok: { rectangle: { width: 10, height: 7 }, }, }) - assert.deepEqual(Shapes.read({ isShape: true }), { + assert.deepEqual(Shapes.tryFrom({ isShape: true }), { ok: { _: { isShape: true, @@ -99,7 +105,7 @@ test('variant with default', () => { }, }) - matchError(Shapes.read({ square: { width: 5 } }), /isShape/) + matchError(Shapes.tryFrom({ square: { width: 5 } }), /isShape/) }) test('variant match', () => { diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 2b1cbba1..a565dcec 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -186,14 +186,19 @@ export interface IPLDViewBuilder { * a generic traversal API. It is useful for encoding (potentially partial) IPLD * DAGs into content archives (e.g. CARs). */ -export interface IPLDView { +export interface IPLDView< + T extends unknown = unknown, + Code extends MulticodecCode = MulticodecCode, + Alg extends MulticodecCode = MulticodecCode, + V extends UnknownLink['version'] = UnknownLink['version'] +> { /** * The root block of the IPLD DAG this is the view of. This is the the block * from which all other blocks are linked directly or transitively. */ - root: Block + root: Block - link(): Link + link(): Link /** * Returns an iterable of all the IPLD blocks that are included in this view. From 7abdbd9a320ea4a9ceadc5fb0f4b25d1942377e9 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 08:59:41 -0700 Subject: [PATCH 10/16] delete redundant code --- packages/core/src/schema/schema.js | 286 ----------------------------- 1 file changed, 286 deletions(-) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 11cab888..97b748cc 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -29,9 +29,6 @@ export class API { constructor(settings) { /** @protected */ this.settings = settings - - // this.codec = CBOR - // this.hasher = sha256 } toString() { @@ -210,60 +207,6 @@ export class API { toSchema() { return this } - - // /** - // * @param {In} input - // * @returns {Schema.Result>, Schema.Error>} - // */ - // toIPLDBuilder(input) { - // const result = this.read(input) - // if (result.error) { - // return result - // } else { - // const data = result.ok - // const builder = new IPLDViewBuilder({ - // data, - // schema: this, - // }) - // return { ok: builder } - // } - // } - - // /** - // * @param {object} source - // * @param {Schema.Link} source.link - // * @param {Schema.BlockStore} source.store - // * @returns {ReturnType} - // */ - // toIPLDView({ link, store }) { - // const block = store.get(`${link}`) - // if (!block) { - // return /** @type {*} */ ({ error: new Error(`Missing block ${link}`) }) - // } else { - // return /** @type {*} */ (this.createIPLDView({ root: block, store })) - // } - // } - - // /** - // * @param {object} source - // * @param {Schema.Block} source.root - // * @param {Schema.BlockStore} source.store - // * @returns {Schema.Result, Schema.Error>} - // */ - // createIPLDView({ root, store }) { - // const input = /** @type {In} */ (this.codec.decode(root.bytes)) - // const result = this.read(input) - // if (result.error) { - // return result - // } else { - // const view = new IPLDView({ - // root: { ...root, data: result.ok }, - // store, - // schema: this, - // }) - // return { ok: view } - // } - // } } /** @@ -324,88 +267,6 @@ class Unknown extends API { */ export const unknown = () => new Unknown() -// /** -// * @template {unknown} T -// * @implements {Schema.IPLDViewBuilder>} -// */ -// class IPLDViewBuilder { -// /** -// * @param {object} input -// * @param {T} input.data -// * @param {Schema.Schema} input.schema -// */ -// constructor({ data, schema }) { -// this.data = data -// this.schema = schema -// } - -// /** -// * @param {Schema.BuildOptions} [options] -// * @returns {Promise>} -// */ -// async buildIPLDView({ -// encoder = this.schema.codec, -// hasher = this.schema.hasher, -// } = {}) { -// const { data } = this -// const bytes = encoder.encode(data) -// const digest = await hasher.digest(bytes) -// /** @type {Schema.Link} */ -// const cid = createLink(encoder.code, digest) -// return new IPLDView({ root: { bytes, data, cid }, schema: this.schema }) -// } -// } - -// /** -// * @template {unknown} T -// * @implements {Schema.IPLDView} -// * @implements {Schema.IPLDViewBuilder>} -// */ -// class IPLDView { -// /** -// * @param {object} input -// * @param {Required} input.root -// * @param {Schema.Schema} input.schema -// * @param {Schema.BlockStore} [input.store] -// */ -// constructor({ root, store = new Map(), schema }) { -// this.root = root -// this.store = store -// this.schema = schema -// } - -// /** -// * @template T -// * @param {object} input -// * @param {Required} input.root -// * @param {Schema.Schema} input.schema -// * @param {Schema.BlockStore} [input.store] -// */ -// static create(input) { -// return new this(input) -// } - -// /** -// * @returns {Schema.Link} -// */ -// link() { -// return this.root.cid -// } -// /** -// * @returns {IterableIterator} -// */ -// *iterateIPLDBlocks() { -// yield this.root -// } - -// /** -// * @returns {Schema.IPLDView} -// */ -// buildIPLDView() { -// return this -// } -// } - /** * @template I, O * @extends {API>} @@ -1694,69 +1555,8 @@ class Struct extends API { extend(extension) { return new Struct({ shape: { ...this.shape, ...extension } }) } - - // /** - // * @param {object} source - // * @param {Schema.Block} source.root - // * @param {Schema.BlockStore} source.store - // * @returns {Schema.Result & Schema.IPLDView>, Schema.Error>} - // */ - // createIPLDView(source) { - // const data = this.codec.decode(source.root.bytes) - - // if (typeof data != 'object' || data === null || Array.isArray(data)) { - // return typeError({ - // expect: 'object', - // actual: data, - // }) - // } - - // let View = this._View || (this._View = IPLDStructView.struct(this.shape)) - // const root = { ...source.root, data } - - // const view = View.create({ root, store: source.store, schema: this }) - // return { ok: view } - // } } -// /** -// * @template {{[key:string]: Schema.Reader}} U -// * @extends {IPLDView>} -// */ -// class IPLDStructView extends IPLDView { -// /** -// * @template {{[key:string]: Schema.Reader}} U -// * @param {U} shape -// * @returns {Schema.CreateView, Schema.InferStruct>} -// */ -// static struct(shape) { -// /** @extends {IPLDStructView} */ -// class View extends this { -// static shape = shape -// } - -// for (const [key, schema] of Object.entries(shape)) { -// Object.defineProperty(View.prototype, key, { -// get() { -// let result = this[`_${key}`] -// if (!result) { -// result = schema.read(this.root.data[key]) -// this[`_${key}`] = result -// } - -// if (result.ok) { -// return result.ok -// } else { -// throw memberError({ at: key, cause: result.error }).error -// } -// }, -// }) -// } - -// return /** @type {*} */ (View) -// } -// } - /** * @template {null|boolean|string|number} T * @template {{[key:string]: T|Schema.Convert}} U @@ -1957,92 +1757,6 @@ class ByteView extends API { } } -// /** -// * @template {Schema.UnknownLink['version']} V -// * @template {Schema.Block} Block -// * @implements {Schema.IPLDView['data']>} -// */ -// class DAGView { -// /** -// * @param {object} source -// * @param {Required} source.root -// * @param {Schema.Region} [source.store] -// */ -// constructor({ root, store = emptyStore }) { -// this.root = root -// this.store = store -// this['/'] = this.root.cid.bytes -// } -// resolve() { -// return /** @type {Schema.ResolvedLink['data'], Block['cid']['code'], Block['cid']['multihash']['code'], Block['cid']['version']>} */ ( -// this.root.data -// ) -// } -// /** -// * -// * @param {object} context -// * @param {Schema.Region} [context.store] -// * @returns {*} -// */ -// with({ store = this.store }) { -// return new DAGView({ root: this.root, store }) -// } -// decode() { -// return this.root.data -// } -// link() { -// return this.root.cid -// } -// *iterateIPLDBlocks() { -// const model = this.root.data -// if (model && typeof model === 'object') { -// for (const value of Object.values(model)) { -// if (value && typeof value['iterateIPLDBlocks'] === 'function') { -// yield* value.iterateIPLDBlocks() -// } -// } -// } -// yield /** @type {Schema.Block} */ (this.root) -// } - -// get version() { -// return this.root.cid.version -// } -// get code() { -// return this.root.cid.code -// } -// get multihash() { -// return this.root.cid.multihash -// } -// get byteOffset() { -// return this.root.cid.byteOffset -// } -// get byteLength() { -// return this.root.cid.byteLength -// } -// get bytes() { -// return this.root.cid.bytes -// } - -// /** -// * @param {unknown} other -// */ -// equals(other) { -// return this.root.cid.equals(other) -// } -// /** -// * @template {string} Prefix -// * @param {Schema.MultibaseEncoder} [base] -// * @returns -// */ -// toString(base) { -// return this.root.cid.toString(base) -// } -// toV1() { -// return this.root.cid.toV1() -// } -// } - const emptyStore = Object.freeze(new Map()) /** * @template {{}|null} T From c04abe8ac554cd9668cb4dd4a2c3b9c61955f72f Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 13:54:50 -0700 Subject: [PATCH 11/16] create failing case --- packages/core/src/schema/schema.js | 54 ++++++++++++++++---------- packages/core/test/ipld-schema.spec.js | 51 +++++++++++++++++++++++- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 97b748cc..e0c285eb 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -193,7 +193,7 @@ export class API { * }} options * @returns {Schema.LinkSchema} */ - link({ codec = /** @type {*} */ (CBOR), hasher, version } = {}) { + link({ codec, hasher, version } = {}) { const schema = link({ codec, hasher, @@ -1139,15 +1139,32 @@ export const float = () => Float * @implements {Schema.StringSchema} */ class StringSchema extends API { + /** + * @template {string} Source + * @param {Source} source + * @returns {Schema.ReadResult} + */ + static validate(source) { + return typeof source === 'string' + ? { ok: source } + : typeError({ expect: 'string', actual: source }) + } /** * @param {In} input * @param {Settings} settings * @returns {Schema.ReadResult} */ readWith(input, settings) { - return typeof input === 'string' - ? { ok: /** @type {Out & In} */ (input) } - : typeError({ expect: 'string', actual: input }) + return StringSchema.validate(/** @type {In & Out} */ (input)) + } + + /** + * @param {Out} input + * @param {Settings} settings + * @returns {Schema.ReadResult} + */ + writeWith(input, settings) { + return StringSchema.validate(/** @type {In & Out} */ (input)) } /** @@ -1780,7 +1797,7 @@ class Link { /** * @param {object} source * @param {Schema.Link} source.link - * @param {Schema.BlockCodec} source.codec + * @param {Schema.BlockCodec} [source.codec] * @param {Schema.Convert} source.schema * @param {Schema.Region} [source.store] */ @@ -1878,7 +1895,7 @@ class Attachment { /** * @param {object} source * @param {Schema.Block} source.root - * @param {Schema.BlockCodec} source.codec + * @param {Schema.BlockCodec} [source.codec] * @param {Schema.Convert} source.schema * @param {Schema.Region} [source.store] */ @@ -1899,7 +1916,7 @@ class Attachment { resolve() { let result = this._resolved if (result == null) { - const { schema, codec, root } = this + const { schema, codec = CBOR, root } = this const data = codec.decode(/** @type {Uint8Array} */ (root.bytes)) result = schema.tryFrom(data, this.store) @@ -2012,7 +2029,7 @@ class Attachment { * @template {number} [Alg=number] * @template {1|0} [Version=0|1] * @typedef {{ - * codec: Schema.BlockCodec, + * codec?: Schema.BlockCodec, * version?: Version * hasher?: {code: Alg} * schema: Schema.Schema @@ -2046,24 +2063,20 @@ class LinkSchema extends API { if (!isLink(source)) { return error(`Expected link to be a CID instead of ${source}`) } else { - if (source.code !== codec.code) { + if (codec && codec.code !== source.code) { return error( `Expected link to be CID with 0x${codec.code.toString(16)} codec` ) } - if ( - hasher && - hasher.code != null && - source.multihash.code !== hasher.code - ) + if (hasher && hasher.code !== source.multihash.code) return error( `Expected link to be CID with 0x${hasher.code.toString( 16 )} hashing algorithm` ) - if (version != null && source.version !== version) { + if (version != null && version !== source.version) { return error( `Expected link to be CID version ${version} instead of ${source.version}` ) @@ -2194,13 +2207,14 @@ class AttachmentSchema extends API { * @param {LinkSettings} settings * @returns {Schema.ReadResult>} */ - writeWith(attachment, { codec, schema }) { + writeWith(attachment, settings) { if (attachment instanceof Attachment) { return { ok: attachment } } console.log('!!!!!!!!!') try { + const { codec, schema } = settings const link = attachment.link() const out = attachment.resolve() const data = schema.tryTo(out) @@ -2208,7 +2222,7 @@ class AttachmentSchema extends API { return data } else { /** @type {Uint8Array} */ - const bytes = codec.encode(data.ok) + const bytes = (codec || CBOR).encode(data.ok) /** @type {Required>} */ const root = { cid: link, bytes, data: out } const view = new Attachment({ root, codec, schema }) @@ -2224,7 +2238,7 @@ class AttachmentSchema extends API { * @param {{hasher?: Schema.MultihashHasher }} options */ async attach(target, { hasher = /** @type {*} */ (sha256) } = {}) { - const { schema, codec } = this.settings + const { schema, codec = CBOR } = this.settings const result = schema.tryTo(target) if (result.error) { throw result.error @@ -2233,7 +2247,7 @@ class AttachmentSchema extends API { /** @type {Uint8Array} */ const bytes = codec.encode(data) const digest = await hasher.digest(bytes) - /** @type {Schema.Link} */ + /** @type {Schema.Link} */ const cid = createLink(codec.code, digest) const store = new Map() @@ -2254,7 +2268,7 @@ class AttachmentSchema extends API { * @param {Out} target */ embed(target) { - const { schema, codec } = this.settings + const { schema, codec = /** @type {*} */ (CBOR) } = this.settings const result = schema.tryTo(target) if (result.error) { throw result.error diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index 414810cb..b5c1565b 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -1,5 +1,6 @@ import * as Schema from '../src/schema.js' -import { isLink, parseLink } from '../src/lib.js' +import { isLink, parseLink, delegate, CAR } from '../src/lib.js' +import { alice, service as w3 } from './fixtures.js' import { CBOR, identity, sha256, createStore, writeInto } from '../src/dag.js' import { test, assert } from './test.js' @@ -307,6 +308,54 @@ describe.only('IPLD Schema', () => { assert.deepEqual(replica[0].resolve(), { x: 3, y: 4 }) assert.deepEqual(replica[0].resolve(), { x: 5, y: 6 }) }) + + test.only('inline into delegation', async () => { + const Detail = Schema.struct({ + link: Schema.unknown().link(), + size: Schema.integer(), + src: Schema.string().array(), + }) + const Offer = Schema.array(Detail) + const Capability = Schema.struct({ + with: Schema.did(), + can: Schema.literal('aggregate/offer'), + nb: Schema.struct({ + offer: Offer.link({ codec: CBOR }).attached(), + }), + }) + + const userData = await CBOR.write({ hello: 'world' }) + + const car = await CAR.write({ + roots: [userData], + }) + + const offer = await delegate({ + issuer: alice, + audience: w3, + capabilities: [ + Capability.from({ + can: 'aggregate/offer', + with: alice.did(), + nb: { + offer: await Offer.link({ codec: CBOR, hasher: sha256 }).attach([ + Detail.from({ + link: car.cid, + size: car.bytes.length, + src: [`ipfs://${car.cid}`], + }), + ]), + }, + }), + ], + }) + + const region = new Map( + [...offer.export()].map(block => [`${block.cid}`, block]) + ) + + assert.equal(region.size, 2) + }) }) /** From c27b04cb47db0bcd085c58c2eab01b0d71166d70 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 13:57:41 -0700 Subject: [PATCH 12/16] fix merge regressions --- packages/core/src/schema/schema.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 6baec196..e70d6d03 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -1260,28 +1260,6 @@ class RefinedString extends StringSchema { export const String = new StringSchema(undefined) export const string = () => String -/** - * @template [I=unknown] - * @extends {API} - */ -class BytesSchema extends API { - /** - * @param {I} input - * @returns {Schema.ReadResult} - */ - readWith(input) { - if (input instanceof Uint8Array) { - return { ok: input } - } else { - return typeError({ expect: 'Uint8Array', actual: input }) - } - } -} - -/** @type {Schema.Schema} */ -export const Bytes = new BytesSchema() -export const bytes = () => Bytes - /** * @template {string} Prefix * @template {string} In From 78386d763655354970401686e87c8a2ffc3f9a5d Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 21:01:22 -0700 Subject: [PATCH 13/16] update delegation to support dags --- packages/core/src/delegation.js | 143 +++++++++++-------------- packages/core/test/ipld-schema.spec.js | 65 ++++++++--- packages/core/test/schema.spec.js | 28 ++++- packages/interface/src/lib.ts | 2 - 4 files changed, 135 insertions(+), 103 deletions(-) diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js index 8f91b62b..d15a0550 100644 --- a/packages/core/src/delegation.js +++ b/packages/core/src/delegation.js @@ -173,41 +173,6 @@ export class Delegation { }) } - /** - * @returns {API.AttachedLinkSet} - */ - get attachedLinks() { - const _attachedLinks = new Set() - const ucanView = this.data - - // Get links from capabilities nb - for (const capability of ucanView.capabilities) { - /** @type {Link[]} */ - const links = getLinksFromObject(capability) - - for (const link of links) { - _attachedLinks.add(`${link}`) - } - } - - // Get links from facts values - for (const fact of ucanView.facts) { - if (Link.isLink(fact)) { - _attachedLinks.add(`${fact}`) - } else { - /** @type {Link[]} */ - // @ts-expect-error isLink does not infer value type - const links = Object.values(fact).filter(e => Link.isLink(e)) - - for (const link of links) { - _attachedLinks.add(`${link}`) - } - } - } - - return _attachedLinks - } - get version() { return this.data.version } @@ -231,22 +196,8 @@ export class Delegation { Object.defineProperties(this, { data: { value: data, enumerable: false } }) return data } - /** - * Attach a block to the delegation DAG so it would be included in the - * block iterator. - * ⚠️ You can only attach blocks that are referenced from the `capabilities` - * or `facts`. - * - * @param {API.Block} block - */ - attach(block) { - if (!this.attachedLinks.has(`${block.cid.link()}`)) { - throw new Error(`given block with ${block.cid} is not an attached link`) - } - this.blocks.set(`${block.cid}`, block) - } export() { - return exportDAG(this.root, this.blocks, this.attachedLinks) + return exportDAG(this) } /** @@ -257,7 +208,7 @@ export class Delegation { } iterateIPLDBlocks() { - return exportDAG(this.root, this.blocks, this.attachedLinks) + return exportDAG(this) } /** @@ -453,11 +404,35 @@ const decode = ({ bytes }) => { */ export const delegate = async ( - { issuer, audience, proofs = [], attachedBlocks = new Map(), ...input }, + { + issuer, + audience, + proofs = [], + attachedBlocks = new Map(), + facts = [], + ...input + }, options ) => { const links = [] + /** @type {Map} */ const blocks = new Map() + for (const capability of input.capabilities) { + for (const node of [...Object.values(capability.nb || {}), capability.nb]) { + for (const block of DAG.iterate(node)) { + blocks.set(block.cid.toString(), block) + } + } + } + + for (const fact of facts) { + for (const node of [...Object.values(fact || {}), fact]) { + for (const block of DAG.iterate(node)) { + blocks.set(block.cid.toString(), block) + } + } + } + for (const proof of proofs) { if (!isDelegation(proof)) { links.push(proof) @@ -469,11 +444,24 @@ export const delegate = async ( } } + // We may not have codecs to traverse all the blocks on the other side of the + // wire, which is why we capture all the links in a flat list which we include + // in facts. That way recipient will know all of the blocks without having to + // know to know how to traverse the graph. + if (blocks.size !== 0) { + facts.push({ + 'ucan/attachments': [...blocks.values()] + .map(block => block.cid) + .sort((left, right) => left.toString().localeCompare(right.toString())), + }) + } + const data = await UCAN.issue({ ...input, issuer, audience, proofs: links, + facts, }) const { cid, bytes } = await UCAN.write(data, options) decodeCache.set(cid, data) @@ -491,14 +479,15 @@ export const delegate = async ( /** * @template {API.Capabilities} C - * @param {API.UCANBlock} root - * @param {DAG.BlockStore} blocks - * @param {API.AttachedLinkSet} attachedLinks + * @param {object} source + * @param {API.UCANBlock} source.root + * @param {DAG.BlockStore} source.blocks * @returns {IterableIterator} */ -export const exportDAG = function* (root, blocks, attachedLinks) { - for (const link of decode(root).proofs) { +export const exportDAG = function* ({ root, blocks }) { + const { proofs, facts, capabilities } = decode(root) + for (const link of proofs) { // Check if block is included in this delegation const root = /** @type {UCAN.Block} */ (blocks.get(`${link}`)) if (root) { @@ -506,7 +495,8 @@ export const exportDAG = function* (root, blocks, attachedLinks) { } } - for (const link of attachedLinks.values()) { + const links = new Set([...iterateLinks({ facts, capabilities }, blocks)]) + for (const link of links) { const block = blocks.get(link) if (block) { @@ -609,31 +599,26 @@ const proofs = delegation => { } /** - * @param {API.Capability} obj + * Traverses source and yields all links found. + * + * @param {unknown} source + * @param {Schema.Region} region + * @returns {IterableIterator} */ -function getLinksFromObject(obj) { - /** @type {Link[]} */ - const links = [] - - /** - * @param {object} obj - */ - function recurse(obj) { - for (const key in obj) { - // @ts-expect-error record type not inferred - const value = obj[key] - if (Link.isLink(value)) { - // @ts-expect-error isLink does not infer value type - links.push(value) - } else if (value && typeof value === 'object') { - recurse(value) +const iterateLinks = function* (source, region) { + if (source && typeof source === 'object') { + if (Link.isLink(source)) { + yield `${source}` + } else { + for (const member of Object.values(source)) { + if (Link.isLink(member)) { + yield `${member}` + } else { + yield* iterateLinks(member, region) + } } } } - - recurse(obj) - - return links } export { Delegation as View } diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index b5c1565b..ae10d802 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -1,8 +1,9 @@ import * as Schema from '../src/schema.js' -import { isLink, parseLink, delegate, CAR } from '../src/lib.js' +import { isLink, parseLink, delegate, Delegation, CAR } from '../src/lib.js' import { alice, service as w3 } from './fixtures.js' import { CBOR, identity, sha256, createStore, writeInto } from '../src/dag.js' import { test, assert } from './test.js' +import { archive } from '../src/delegation.js' describe.only('IPLD Schema', () => { test('link schema', async () => { @@ -261,7 +262,7 @@ describe.only('IPLD Schema', () => { const compile = async (data = {}) => { const { bytes, cid } = await CBOR.write(data) - return { bytes, cid, data } + return { bytes, cid } } const link = async (data = {}) => { @@ -270,12 +271,15 @@ describe.only('IPLD Schema', () => { } assert.deepEqual( - [...polygon.iterateIPLDBlocks()], + [...polygon.iterateIPLDBlocks()].map(({ cid, bytes }) => ({ + bytes, + cid: cid.link(), + })), [ await compile({ x: 1, y: 2 }), await compile({ x: 3, y: 4 }), await compile({ x: 5, y: 6 }), - polygon.root, + { cid: polygon.link(), bytes: polygon.root.bytes }, ] ) @@ -298,18 +302,26 @@ describe.only('IPLD Schema', () => { const replica = root.resolve() - assert.deepEqual(replica, [ - await link({ x: 1, y: 2 }), - await link({ x: 3, y: 4 }), - await link({ x: 5, y: 6 }), - ]) + assert.deepEqual( + replica.map(point => point.link()), + [ + await link({ x: 1, y: 2 }), + await link({ x: 3, y: 4 }), + await link({ x: 5, y: 6 }), + ] + ) - assert.deepEqual(replica[0].resolve(), { x: 1, y: 2 }) - assert.deepEqual(replica[0].resolve(), { x: 3, y: 4 }) - assert.deepEqual(replica[0].resolve(), { x: 5, y: 6 }) + assert.deepEqual( + replica.map(point => point.resolve()), + [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + { x: 5, y: 6 }, + ] + ) }) - test.only('inline into delegation', async () => { + test('inline into delegation', async () => { const Detail = Schema.struct({ link: Schema.unknown().link(), size: Schema.integer(), @@ -350,11 +362,30 @@ describe.only('IPLD Schema', () => { ], }) - const region = new Map( - [...offer.export()].map(block => [`${block.cid}`, block]) - ) + const nodes = [...offer.export()] + + assert.deepEqual(nodes.length, 2, 'links to the attached block') + const [node, root] = nodes + assert.deepEqual(root.cid, offer.link()) + assert.deepEqual(CBOR.decode(node.bytes), [ + { + link: car.cid, + size: car.bytes.length, + src: [`ipfs://${car.cid}`], + }, + ]) + + const archive = await offer.archive() + if (archive.error) { + throw archive.error + } + + const replica = await Delegation.extract(archive.ok) + if (replica.error) { + throw replica.error + } - assert.equal(region.size, 2) + assert.equal([...replica.ok.export()].length, 2) }) }) diff --git a/packages/core/test/schema.spec.js b/packages/core/test/schema.spec.js index 6dcb7667..59caf02c 100644 --- a/packages/core/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -704,21 +704,39 @@ test('record defaults', () => { test('bytes schema', () => { const schema = Schema.bytes() - matchError(schema.read(undefined), /expect.* Uint8Array .* got undefined/is) + matchError( + schema.tryFrom( + // @ts-expect-error - expects Uint8Array + undefined + ), + /expect.* Uint8Array .* got undefined/is + ) const bytes = new Uint8Array([1, 2, 3]) - assert.equal(schema.read(bytes).ok, bytes, 'returns same bytes back') + assert.equal(schema.tryFrom(bytes).ok, bytes, 'returns same bytes back') matchError( - schema.read(bytes.buffer), + schema.tryFrom( + // @ts-expect-error - expects Uint8Array + bytes.buffer + ), /expect.* Uint8Array .* got ArrayBuffer/is ) matchError( - schema.read(new Int8Array(bytes.buffer)), + schema.tryFrom( + // @ts-expect-error - expects Uint8Array + new Int8Array(bytes.buffer) + ), /expect.* Uint8Array .* got Int8Array/is ) - matchError(schema.read([...bytes]), /expect.* Uint8Array .* got array/is) + matchError( + schema.tryFrom( + // @ts-expect-error + [...bytes] + ), + /expect.* Uint8Array .* got array/is + ) }) diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index adb1553d..01c93b45 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -265,8 +265,6 @@ export interface Delegation delegate(): Await> archive(): Await> - - attach(block: Block): void } /** From 689f6890d2e54ba7acc95a0cf720be9d4b6cb5d3 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 22:19:48 -0700 Subject: [PATCH 14/16] update tests to handle new code --- packages/core/src/delegation.js | 27 ++-- packages/core/src/invocation.js | 16 --- packages/core/src/message.js | 15 ++- packages/core/src/schema/did.js | 4 +- packages/core/src/schema/schema.js | 52 +++++-- packages/core/src/schema/text.js | 2 +- packages/core/src/schema/type.ts | 4 +- packages/core/test/delegation.spec.js | 16 +-- packages/core/test/extra-schema.spec.js | 30 +++-- packages/core/test/invocation.spec.js | 11 +- packages/core/test/ipld-schema.spec.js | 2 +- packages/core/test/link-schema.spec.js | 76 +++++------ packages/core/test/map-schema.spec.js | 157 ++-------------------- packages/core/test/schema.spec.js | 3 +- packages/core/test/variant-schema.spec.js | 21 ++- packages/interface/src/lib.ts | 5 +- 16 files changed, 153 insertions(+), 288 deletions(-) diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js index d15a0550..d0ef4db4 100644 --- a/packages/core/src/delegation.js +++ b/packages/core/src/delegation.js @@ -336,7 +336,7 @@ export const archive = async delegation => { export const ArchiveSchema = Schema.variant({ 'ucan@0.9.1': /** @type {Schema.Schema} */ ( - Schema.link({ version: 1 }) + Schema.unknown().link({ version: 1 }) ), }) @@ -404,14 +404,7 @@ const decode = ({ bytes }) => { */ export const delegate = async ( - { - issuer, - audience, - proofs = [], - attachedBlocks = new Map(), - facts = [], - ...input - }, + { issuer, audience, proofs = [], facts = [], ...input }, options ) => { const links = [] @@ -433,6 +426,10 @@ export const delegate = async ( } } + const attachments = [...blocks.values()] + .map(block => block.cid) + .sort((left, right) => left.toString().localeCompare(right.toString())) + for (const proof of proofs) { if (!isDelegation(proof)) { links.push(proof) @@ -448,12 +445,8 @@ export const delegate = async ( // wire, which is why we capture all the links in a flat list which we include // in facts. That way recipient will know all of the blocks without having to // know to know how to traverse the graph. - if (blocks.size !== 0) { - facts.push({ - 'ucan/attachments': [...blocks.values()] - .map(block => block.cid) - .sort((left, right) => left.toString().localeCompare(right.toString())), - }) + if (attachments.length > 0) { + facts.push({ 'ucan/attachments': attachments }) } const data = await UCAN.issue({ @@ -470,10 +463,6 @@ export const delegate = async ( const delegation = new Delegation({ cid, bytes }, blocks) Object.defineProperties(delegation, { proofs: { value: proofs } }) - for (const block of attachedBlocks.values()) { - delegation.attach(block) - } - return delegation } diff --git a/packages/core/src/invocation.js b/packages/core/src/invocation.js index 819adafb..b9c7b5a4 100644 --- a/packages/core/src/invocation.js +++ b/packages/core/src/invocation.js @@ -81,22 +81,6 @@ class IssuedInvocation { this.notBefore = notBefore this.nonce = nonce this.facts = facts - - /** @type {API.BlockStore} */ - this.attachedBlocks = new Map() - } - - /** - * Attach a block to the invocation DAG so it would be included in the - * block iterator. - * ⚠️ You should only attach blocks that are referenced from the `capabilities` - * or `facts`, if that is not the case you probably should reconsider. - * ⚠️ Once a delegation is de-serialized the attached blocks will not be re-attached. - * - * @param {API.Block} block - */ - attach(block) { - this.attachedBlocks.set(`${block.cid}`, block) } delegate() { diff --git a/packages/core/src/message.js b/packages/core/src/message.js index 9021ecea..4be20867 100644 --- a/packages/core/src/message.js +++ b/packages/core/src/message.js @@ -6,15 +6,13 @@ import * as Schema from './schema.js' export const MessageSchema = Schema.variant({ 'ucanto/message@7.0.0': Schema.struct({ - execute: Schema.link().array().optional(), - delegate: Schema.dictionary({ + execute: Schema.unknown().link().array().optional(), + report: Schema.dictionary({ key: Schema.string(), - value: /** @type {API.Reader>} */ ( - Schema.link() + value: /** @type {Schema.Convert>} */ ( + Schema.unknown().link() ), - }) - .array() - .optional(), + }).optional(), }), }) @@ -178,6 +176,9 @@ class Message { this._invocations = invocations this._receipts = receipts } + link() { + return this.root.cid + } *iterateIPLDBlocks() { for (const invocation of this.invocations) { yield* invocation.iterateIPLDBlocks() diff --git a/packages/core/src/schema/did.js b/packages/core/src/schema/did.js index d5d722bf..67fbaa37 100644 --- a/packages/core/src/schema/did.js +++ b/packages/core/src/schema/did.js @@ -27,7 +27,7 @@ export const did = () => schema * * @param {unknown} input */ -export const read = input => schema.tryFrom(input) +export const read = input => schema.read(input) /** * @template {string} Method @@ -42,4 +42,4 @@ export const match = (options = {}) => * Create a DID string from any input (or throw) * @param {unknown} input */ -export const from = input => match({}).tryFrom(input) +export const from = input => match({}).from(input) diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index e70d6d03..26b23c66 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -56,6 +56,14 @@ export class API { throw new Error(`Abstract method writeWith must be implemented by subclass`) } + /** + * @param {unknown} input + */ + + read(input) { + return this.tryFrom(/** @type {In} */ (input)) + } + /** * @param {In} input * @param {Schema.Region} [region] @@ -1805,7 +1813,18 @@ class Link { this.codec = codec this.cid = link this.schema = schema + this['/'] = link.bytes + this.version = link.version + this.multihash = link.multihash + this.code = link.code + this.bytes = link.bytes + + Object.defineProperties(this, { + codec: { enumerable: false }, + cid: { enumerable: false }, + schema: { enumerable: false }, + }) } /** @@ -1842,24 +1861,24 @@ class Link { resolve(region) { return this.select(region).resolve() } - get version() { - return this.cid.version - } - get code() { - return this.cid.code - } - get multihash() { - return this.cid.multihash - } + // get version() { + // return this.cid.version + // } + // get code() { + // return this.cid.code + // } + // get multihash() { + // return this.cid.multihash + // } get byteOffset() { return this.cid.byteOffset } get byteLength() { return this.cid.byteLength } - get bytes() { - return this.cid.bytes - } + // get bytes() { + // return this.cid.bytes + // } link() { return this.cid } @@ -1878,6 +1897,15 @@ class Link { toString(base) { return this.cid.toString(base) } + toJSON() { + return { '/': this.toString() } + } + get [Symbol.toStringTag]() { + return 'CID' + } + [Symbol.for('nodejs.util.inspect.custom')]() { + return `CID(${this.toString()})` + } toV1() { return this.cid.toV1() } diff --git a/packages/core/src/schema/text.js b/packages/core/src/schema/text.js index b712dbc8..cbf13afe 100644 --- a/packages/core/src/schema/text.js +++ b/packages/core/src/schema/text.js @@ -13,7 +13,7 @@ export const text = match /** * @param {unknown} input */ -export const read = input => schema.tryFrom(input) +export const read = input => schema.read(input) /** * @extends {Schema.API} diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index e36de352..1ea3aa2b 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -83,7 +83,7 @@ export interface Schema< implicit(value: Exclude): ImplicitSchema - array(): ArraySchema + array(): ArraySchema> or(other: Convert): Schema and(other: Convert): Schema refine(schema: Convert): Schema @@ -96,6 +96,8 @@ export interface Schema< brand(kind?: K): Schema, In> is(value: unknown): value is Out + + read(unknown: unknown): ReadResult from(value: In): Out to(value: Out): In diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js index 84576149..e19cf29f 100644 --- a/packages/core/test/delegation.spec.js +++ b/packages/core/test/delegation.spec.js @@ -427,7 +427,7 @@ test('archive delegation chain', async () => { assert.deepEqual(extract.ok.proofs[0], proof) }) -test('delegation.attach block in capabiliy', async () => { +test.skip('delegation.attach block in capabiliy', async () => { const block = await getBlock({ test: 'inlineBlock' }) const ucan = await Delegation.delegate({ issuer: alice, @@ -437,8 +437,8 @@ test('delegation.attach block in capabiliy', async () => { can: 'store/add', with: alice.did(), nb: { - inlineBlock: block.cid.link() - } + inlineBlock: block.cid.link(), + }, }, ], }) @@ -453,7 +453,7 @@ test('delegation.attach block in capabiliy', async () => { assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid))) }) -test('delegation.attach block in facts', async () => { +test.skip('delegation.attach block in facts', async () => { const block = await getBlock({ test: 'inlineBlock' }) const ucan = await Delegation.delegate({ issuer: alice, @@ -467,8 +467,8 @@ test('delegation.attach block in facts', async () => { facts: [ { [`${block.cid.link()}`]: block.cid.link() }, // @ts-expect-error Link has fact entry - block.cid.link() - ] + block.cid.link(), + ], }) ucan.attach(block) @@ -488,11 +488,11 @@ test('delegation.attach fails to attach block with not attached link', async () capabilities: [ { can: 'store/add', - with: alice.did() + with: alice.did(), }, ], }) const block = await getBlock({ test: 'inlineBlock' }) assert.throws(() => ucan.attach(block)) -}) \ No newline at end of file +}) diff --git a/packages/core/test/extra-schema.spec.js b/packages/core/test/extra-schema.spec.js index f7960f43..14e7576e 100644 --- a/packages/core/test/extra-schema.spec.js +++ b/packages/core/test/extra-schema.spec.js @@ -1,7 +1,9 @@ -import { URI, Link, Text, DID } from '../src/schema.js' +import { URI, Text, DID } from '../src/schema.js' +import { parseLink } from '../src/lib.js' import * as Schema from '../src/schema.js' import { test, assert, matchResult } from './test.js' import * as API from '@ucanto/interface' +import { sha256 } from '../src/dag.js' { /** @type {[string, API.Result|RegExp][]} */ @@ -92,7 +94,7 @@ test('URI.from', () => { /** @type {any[][]} */ const dataset = [ [ - Link.parse('bafkqaaa'), + parseLink('bafkqaaa'), null, /Expected link to be CID with 0x70 codec/, /Expected link to be CID with 0x12 hashing algorithm/, @@ -100,7 +102,7 @@ test('URI.from', () => { null, ], [ - Link.parse('QmdpiaQ9q7n4E224syBJz4peZpAFLArwJgSXHZWH5F6DxB'), + parseLink('QmdpiaQ9q7n4E224syBJz4peZpAFLArwJgSXHZWH5F6DxB'), null, null, null, @@ -108,7 +110,7 @@ test('URI.from', () => { null, ], [ - Link.parse('bafybeiepa5hmd3vg2i2unyzrhnxnthwi2aksunykhmcaykbl2jx2u77cny'), + parseLink('bafybeiepa5hmd3vg2i2unyzrhnxnthwi2aksunykhmcaykbl2jx2u77cny'), null, null, null, @@ -136,33 +138,33 @@ test('URI.from', () => { ] for (const [input, out1, out2, out3, out4, out5] of dataset) { - test(`Link.read(${input})`, () => { - matchResult(Link.read(input), out1 || { ok: input }) + test(`unknown().link().tryFrom(${input})`, () => { + matchResult(Schema.unknown().link().tryFrom(input), out1 || { ok: input }) }) test('Schema.link()', () => { - const schema = Schema.link() + const schema = Schema.unknown().link() matchResult(schema.tryFrom(input), out1 || { ok: input }) }) test(`Schema.link({ code: 0x70 }).read(${input})`, () => { - const link = Schema.link({ code: 0x70 }) + const link = Schema.unknown().link({ codec: { code: 0x70 } }) matchResult(link.tryFrom(input), out2 || { ok: input }) }) test(`Schema.link({ algorithm: 0x12 }).read(${input})`, () => { - const link = Schema.link({ multihash: { code: 0x12 } }) + const link = Schema.unknown().link({ hasher: sha256 }) matchResult(link.tryFrom(input), out3 || { ok: input }) }) test(`Schema.link({ version: 1 }).read(${input})`, () => { - const link = Schema.link({ version: 1 }) + const link = Schema.unknown().link({ version: 1 }) matchResult(link.tryFrom(input), out4 || { ok: input }) }) test(`Link.optional().read(${input})`, () => { - const link = Link.optional() - matchResult(link.read(input), out5 || { ok: input }) + const link = Schema.unknown().link().optional() + matchResult(link.tryFrom(input), out5 || { ok: input }) }) } } @@ -211,7 +213,7 @@ test('URI.from', () => { for (const [options, input, out] of dataset) { test(`Text.match({ pattern: ${options.pattern} }).read(${input})`, () => { - matchResult(Text.match(options).tryFrom(input), out) + matchResult(Text.match(options).read(input), out) }) } } @@ -252,7 +254,7 @@ test('URI.from', () => { const schema = options.pattern ? Text.match({ pattern: options.pattern }) : Text.text() - matchResult(schema.optional().tryFrom(input), out) + matchResult(schema.optional().read(input), out) }) } } diff --git a/packages/core/test/invocation.spec.js b/packages/core/test/invocation.spec.js index e209a849..903cdb03 100644 --- a/packages/core/test/invocation.spec.js +++ b/packages/core/test/invocation.spec.js @@ -32,7 +32,7 @@ test('encode invocation', async () => { assert.deepEqual(delegation.audience.did(), w3.did()) }) -test('encode invocation with attached block in capability nb', async () => { +test.skip('encode invocation with attached block in capability nb', async () => { const block = await getBlock({ test: 'inlineBlock' }) const add = invoke({ issuer: alice, @@ -42,8 +42,8 @@ test('encode invocation with attached block in capability nb', async () => { with: alice.did(), link: 'bafy...stuff', nb: { - inlineBlock: block.cid.link() - } + inlineBlock: block.cid.link(), + }, }, proofs: [], }) @@ -61,12 +61,12 @@ test('encode invocation with attached block in capability nb', async () => { const reassembledInvocation = Invocation.view({ root: view.root.cid.link(), - blocks: blockStore + blocks: blockStore, }) /** @type {import('@ucanto/interface').BlockStore} */ const reassembledBlockstore = new Map() - + for (const b of reassembledInvocation.iterateIPLDBlocks()) { reassembledBlockstore.set(`${b.cid}`, b) } @@ -75,7 +75,6 @@ test('encode invocation with attached block in capability nb', async () => { assert.ok(reassembledBlockstore.get(`${block.cid}`)) }) - test('expired invocation', async () => { const expiration = UCAN.now() - 5 const invocation = invoke({ diff --git a/packages/core/test/ipld-schema.spec.js b/packages/core/test/ipld-schema.spec.js index ae10d802..a822dff2 100644 --- a/packages/core/test/ipld-schema.spec.js +++ b/packages/core/test/ipld-schema.spec.js @@ -5,7 +5,7 @@ import { CBOR, identity, sha256, createStore, writeInto } from '../src/dag.js' import { test, assert } from './test.js' import { archive } from '../src/delegation.js' -describe.only('IPLD Schema', () => { +describe('IPLD Schema', () => { test('link schema', async () => { const Point = Schema.struct({ x: Schema.integer(), diff --git a/packages/core/test/link-schema.spec.js b/packages/core/test/link-schema.spec.js index b77788ee..1a16e715 100644 --- a/packages/core/test/link-schema.spec.js +++ b/packages/core/test/link-schema.spec.js @@ -4,35 +4,41 @@ import { test, assert, matchError } from './test.js' import { CBOR, sha256 } from '../src/dag.js' const fixtures = { - pb: Schema.Link.parse('QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8'), - cbor: Schema.Link.parse( - 'bafyreieuo63r3y2nuycaq4b3q2xvco3nprlxiwzcfp4cuupgaywat3z6mq' - ), - rawIdentity: Schema.Link.parse('bafkqaaa'), - ipns: Schema.Link.parse( - 'k2k4r8kuj2bs2l996lhjx8rc727xlvthtak8o6eia3qm5adxvs5k84gf', - base36 - ), - sha512: Schema.Link.parse( - 'kgbuwaen1jrbjip6iwe9mqg54spvuucyz7f5jho2tkc2o0c7xzqwpxtogbyrwck57s9is6zqlwt9rsxbuvszym10nbaxt9jn7sf4eksqd', - base36 - ), + pb: Schema.unknown() + .link() + .parse('QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8'), + cbor: Schema.unknown() + .link() + .parse('bafyreieuo63r3y2nuycaq4b3q2xvco3nprlxiwzcfp4cuupgaywat3z6mq'), + rawIdentity: Schema.unknown().link().parse('bafkqaaa'), + ipns: Schema.unknown() + .link() + .parse('k2k4r8kuj2bs2l996lhjx8rc727xlvthtak8o6eia3qm5adxvs5k84gf', base36), + sha512: Schema.unknown() + .link() + .parse( + 'kgbuwaen1jrbjip6iwe9mqg54spvuucyz7f5jho2tkc2o0c7xzqwpxtogbyrwck57s9is6zqlwt9rsxbuvszym10nbaxt9jn7sf4eksqd', + base36 + ), } const links = Object.values(fixtures) const versions = new Set(links.map(link => link.version)) const codes = new Set(links.map(link => link.code)) const algs = new Set(links.map(link => link.multihash.code)) -const digests = new Set(links.map(link => link.multihash.digest)) for (const link of links) { test(`${link} ➡ Schema.link()`, () => { - assert.deepEqual(Schema.link().tryFrom(link), { ok: link }, `${link}`) + assert.deepEqual( + Schema.unknown().link().tryFrom(link), + { ok: link }, + `${link}` + ) }) for (const version of versions) { - test(`${link} ➡ Schema.link({ version: ${version}})`, () => { - const schema = Schema.link({ version }) + test(`${link} ➡ Schema.unknown().link({ version: ${version}})`, () => { + const schema = Schema.unknown().link({ version }) if (link.version === version) { assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { @@ -43,7 +49,7 @@ for (const link of links) { for (const code of codes) { test(`${link} ➡ Schema.link({ code: ${code}})`, () => { - const schema = Schema.link({ code }) + const schema = Schema.unknown().link({ codec: { code } }) if (link.code === code) { assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { @@ -56,8 +62,8 @@ for (const link of links) { } for (const code of algs) { - test(`${link} ➡ Schema.link({ multihash: {code: ${code}} })`, () => { - const schema = Schema.link({ multihash: { code } }) + test(`${link} ➡ Schema.unknown().link({ hasher: {code: ${code}} })`, () => { + const schema = Schema.unknown().link({ hasher: { code } }) if (link.multihash.code === code) { assert.deepEqual(schema.tryFrom(link), { ok: link }) } else { @@ -68,19 +74,6 @@ for (const link of links) { } }) } - - for (const digest of digests) { - test(`${link} ➡ Schema.link({ multihash: {digest} })`, () => { - const schema = Schema.link({ - multihash: { digest: new Uint8Array(digest) }, - }) - if (link.multihash.digest === digest) { - assert.deepEqual(schema.tryFrom(link), { ok: link }) - } else { - matchError(schema.tryFrom(link), /Expected link with .* hash digest/) - } - }) - } } test('struct().link()', () => { @@ -90,7 +83,7 @@ test('struct().link()', () => { }) const PointLink = Point.link() - assert.equal(PointLink.tryFrom(fixtures.pb).ok, fixtures.pb) + assert.equal(PointLink.read(fixtures.pb).ok, fixtures.pb) assert.throws(() => PointLink.link(), /link of link/) }) @@ -104,8 +97,8 @@ test('struct().link({ codec })', () => { codec: CBOR, }) - assert.match(PointLink.tryFrom(fixtures.pb).error?.message || '', /0x71 code/) - assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) + assert.match(PointLink.read(fixtures.pb).error?.message || '', /0x71 code/) + assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) }) test('struct().link({ hasher })', () => { @@ -118,10 +111,13 @@ test('struct().link({ hasher })', () => { }) assert.match( - PointLink.tryFrom(fixtures.sha512).error?.message || '', + PointLink.read(fixtures.sha512).error?.message || '', /0x12 hashing/ ) - assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) + assert.equal( + PointLink.read(fixtures.cbor).ok, + /** @type {*} */ (fixtures.cbor) + ) }) test('struct().link({ hasher })', () => { @@ -133,6 +129,6 @@ test('struct().link({ hasher })', () => { version: 1, }) - assert.match(PointLink.tryFrom(fixtures.pb).error?.message || '', /version 1/) - assert.equal(PointLink.tryFrom(fixtures.cbor).ok, fixtures.cbor) + assert.match(PointLink.read(fixtures.pb).error?.message || '', /version 1/) + assert.equal(PointLink.read(fixtures.cbor).ok, fixtures.cbor) }) diff --git a/packages/core/test/map-schema.spec.js b/packages/core/test/map-schema.spec.js index afb5620f..eef0de4f 100644 --- a/packages/core/test/map-schema.spec.js +++ b/packages/core/test/map-schema.spec.js @@ -15,12 +15,12 @@ test('.partial on structs', () => { y: Schema.integer().optional(), }) - assert.deepEqual(PartialPoint.tryFrom({}), {}) - assert.deepEqual(PartialPoint.tryFrom({ x: 1 }), { x: 1 }) - assert.deepEqual(PartialPoint.tryFrom({ x: 1, y: 2 }), { x: 1, y: 2 }) - assert.deepEqual(PartialPoint.tryFrom({ y: 2 }), { y: 2 }) - assert.deepEqual(PartialPoint.tryFrom({ x: undefined }), {}) - assert.deepEqual(PartialPoint.tryFrom({ x: undefined, y: 2 }), { y: 2 }) + assert.deepEqual(PartialPoint.from({}), {}) + assert.deepEqual(PartialPoint.from({ x: 1 }), { x: 1 }) + assert.deepEqual(PartialPoint.from({ x: 1, y: 2 }), { x: 1, y: 2 }) + assert.deepEqual(PartialPoint.from({ y: 2 }), { y: 2 }) + assert.deepEqual(PartialPoint.from({ x: undefined }), {}) + assert.deepEqual(PartialPoint.from({ x: undefined, y: 2 }), { y: 2 }) assert.throws( () => @@ -39,143 +39,10 @@ test('.partial on dicts', () => { assert.equal(IntsMaybe.key, Ints.key) assert.deepEqual(IntsMaybe.value, Schema.integer().optional()) - assert.deepEqual(IntsMaybe.tryFrom({}), {}) - assert.deepEqual(IntsMaybe.tryFrom({ x: 1 }), { x: 1 }) - assert.deepEqual(IntsMaybe.tryFrom({ x: 1, y: 2 }), { x: 1, y: 2 }) - assert.deepEqual(IntsMaybe.tryFrom({ y: 2 }), { y: 2 }) - assert.deepEqual(IntsMaybe.tryFrom({ x: undefined }), {}) - assert.deepEqual(IntsMaybe.tryFrom({ x: undefined, y: 2 }), { y: 2 }) -}) - -test('.creteIPLDView', async () => { - const Point = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }) - - const view = await Point.toIPLDBuilder({ - x: 1, - y: 2, - }).ok?.buildIPLDView() - - if (!view) { - return assert.fail('view is undefined') - } - - const store = new Map() - store.set(`${view.link()}`, view.root) - - const point = Point.toIPLDView({ - link: view.link(), - store, - }).ok - - if (!point) { - return assert.fail('point is undefined') - } - - assert.equal(point.x, 1) - assert.equal(point.y, 2) - - const blocks = [...point.iterateIPLDBlocks()] - assert.deepEqual(blocks, [view.root]) - assert.equal( - // @ts-expect-error - not specified in type - point.buildIPLDView(), - point - ) -}) - -test('.toIPLDBuilder fails with bad input', async () => { - const Point = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }) - - const builder = await Point.toIPLDBuilder({ - x: 1, - }) - - assert.match(builder.error?.message || '', /invalid field "y"/) -}) - -test('invalid views', async () => { - const Point = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }) - - const block = await CBOR.write({ x: 1, z: 3 }) - const store = new Map([[`${block.cid}`, block]]) - - const point = Point.toIPLDView({ link: block.cid, store }).ok - if (!point) { - return assert.fail('point is undefined') - } - assert.equal(point.x, 1) - - assert.throws(() => point.y, /invalid field "y"/) -}) - -test('toIPLDView fails if block is not found', async () => { - const Point = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }) - - const block = await CBOR.write({ x: 1, y: 3 }) - const store = new Map() - const point = Point.toIPLDView({ link: block.cid, store }) - assert.equal(point.error?.message, `Missing block ${block.cid}`) -}) - -test('toIPLDView fails with incompatible block', async () => { - const Point = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }) - - const block = await CBOR.write([{ x: 1, y: 3 }]) - const store = new Map([[`${block.cid}`, block]]) - const point = Point.toIPLDView({ link: block.cid, store }) - assert.equal( - point.error?.message, - `Expected value of type object instead got array` - ) -}) - -test('toIPLDView on non-struct', async () => { - const Points = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }).array() - - const block = await CBOR.write([ - { x: 0, y: 0 }, - { x: 16, y: 17 }, - ]) - const store = new Map([[`${block.cid}`, block]]) - - const points = Points.toIPLDView({ link: block.cid, store }) - assert.ok(points.ok) - assert.deepEqual(points.ok?.root.data, [ - { x: 0, y: 0 }, - { x: 16, y: 17 }, - ]) -}) - -test('toIPLDView on invalid content', async () => { - const Points = Schema.struct({ - x: Schema.integer(), - y: Schema.integer(), - }).array() - - const block = await CBOR.write({ x: 0, y: 0 }) - const store = new Map([[`${block.cid}`, block]]) - - const points = Points.toIPLDView({ link: block.cid, store }) - assert.equal( - points.error?.message, - 'Expected value of type array instead got object' - ) + assert.deepEqual(IntsMaybe.from({}), {}) + assert.deepEqual(IntsMaybe.from({ x: 1 }), { x: 1 }) + assert.deepEqual(IntsMaybe.from({ x: 1, y: 2 }), { x: 1, y: 2 }) + assert.deepEqual(IntsMaybe.from({ y: 2 }), { y: 2 }) + assert.deepEqual(IntsMaybe.from({ x: undefined }), {}) + assert.deepEqual(IntsMaybe.from({ x: undefined, y: 2 }), { y: 2 }) }) diff --git a/packages/core/test/schema.spec.js b/packages/core/test/schema.spec.js index 59caf02c..8ca072f2 100644 --- a/packages/core/test/schema.spec.js +++ b/packages/core/test/schema.spec.js @@ -190,6 +190,7 @@ test('string().refine', () => { /** * @template {string} In * @param {In} hello + * @returns {Schema.ReadResult} */ tryFrom(hello) { if (hello.length === 11) { @@ -199,7 +200,7 @@ test('string().refine', () => { } }, }) - /** @type {Schema.StringSchema<`hello${string}` & `hello ${string}` & { length: 11 }>} */ + /** @type {Schema.StringSchema<`hello ${string}` & { length: 11 }>} */ const typeofGreet = greet assert.equal( diff --git a/packages/core/test/variant-schema.spec.js b/packages/core/test/variant-schema.spec.js index c929fde5..7b515dc5 100644 --- a/packages/core/test/variant-schema.spec.js +++ b/packages/core/test/variant-schema.spec.js @@ -23,24 +23,21 @@ test('variant', () => { }) matchError( - Shape.tryFrom({ rectangle: { width: 1 } }), + Shape.read({ rectangle: { width: 1 } }), /contains invalid field "rectangle"/ ) matchError( - Shape.tryFrom({ square: { width: 5 } }), + Shape.read({ square: { width: 5 } }), /Expected an object with one of the these keys: circle, rectangle instead got object with key square/ ) - matchError( - Shape.tryFrom([]), - /Expected value of type object instead got array/ - ) + matchError(Shape.read([]), /Expected value of type object instead got array/) }) test('variant can not have extra fields', () => { matchError( - Shape.tryFrom({ + Shape.read({ rectangle: { width: 1, height: 2 }, circle: { radius: 3 }, }), @@ -59,17 +56,17 @@ test('variant with default match', () => { _: Schema.dictionary({ value: Schema.unknown() }), }) - assert.deepEqual(Shapes.tryFrom({ circle: { radius: 1 } }), { + assert.deepEqual(Shapes.read({ circle: { radius: 1 } }), { ok: { circle: { radius: 1 } }, }) - assert.deepEqual(Shapes.tryFrom({ rectangle: { width: 10, height: 7 } }), { + assert.deepEqual(Shapes.read({ rectangle: { width: 10, height: 7 } }), { ok: { rectangle: { width: 10, height: 7 }, }, }) - assert.deepEqual(Shapes.tryFrom({ square: { width: 5 } }), { + assert.deepEqual(Shapes.read({ square: { width: 5 } }), { ok: { _: { square: { width: 5 } } }, }) }) @@ -97,7 +94,7 @@ test('variant with default', () => { }, }) - assert.deepEqual(Shapes.tryFrom({ isShape: true }), { + assert.deepEqual(Shapes.read({ isShape: true }), { ok: { _: { isShape: true, @@ -105,7 +102,7 @@ test('variant with default', () => { }, }) - matchError(Shapes.tryFrom({ square: { width: 5 } }), /isShape/) + matchError(Shapes.read({ square: { width: 5 } }), /isShape/) }) test('variant match', () => { diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index 01c93b45..6f9b4db8 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -34,6 +34,7 @@ import { BlockDecoder, BlockCodec, BaseDecoder, + MultibaseCodec, } from 'multiformats' import * as UCAN from '@ipld/dag-ucan' import { @@ -120,7 +121,6 @@ export interface UCANOptions { facts?: Fact[] proofs?: Proof[] - attachedBlocks?: BlockStore } /** @@ -190,7 +190,7 @@ export interface IPLDView< T extends unknown = unknown, Code extends MulticodecCode = MulticodecCode, Alg extends MulticodecCode = MulticodecCode, - V extends UnknownLink['version'] = UnknownLink['version'] + V extends UnknownLink['version'] = 1 > { /** * The root block of the IPLD DAG this is the view of. This is the the block @@ -567,7 +567,6 @@ export interface IssuedInvocation readonly proofs: Proof[] delegate(): Await> - attach(block: Block): void } export type ServiceInvocation< From bf8632e882ae6eaf8caf7805df21aaa0b6bce453 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 23:36:09 -0700 Subject: [PATCH 15/16] update all the attachment tests --- packages/core/src/delegation.js | 14 ++-- packages/core/src/schema/schema.js | 31 +++++++-- packages/core/src/schema/type.ts | 45 ++----------- packages/core/test/delegation.spec.js | 92 ++++++++++++++++++++------- packages/core/test/invocation.spec.js | 44 +++++++------ packages/core/test/message.spec.js | 2 + 6 files changed, 130 insertions(+), 98 deletions(-) diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js index d0ef4db4..9162f136 100644 --- a/packages/core/src/delegation.js +++ b/packages/core/src/delegation.js @@ -419,9 +419,11 @@ export const delegate = async ( } for (const fact of facts) { - for (const node of [...Object.values(fact || {}), fact]) { - for (const block of DAG.iterate(node)) { - blocks.set(block.cid.toString(), block) + if (fact) { + for (const node of [...Object.values(fact), fact]) { + for (const block of DAG.iterate(node)) { + blocks.set(block.cid.toString(), block) + } } } } @@ -600,11 +602,7 @@ const iterateLinks = function* (source, region) { yield `${source}` } else { for (const member of Object.values(source)) { - if (Link.isLink(member)) { - yield `${member}` - } else { - yield* iterateLinks(member, region) - } + yield* iterateLinks(member, region) } } } diff --git a/packages/core/src/schema/schema.js b/packages/core/src/schema/schema.js index 26b23c66..b32128ad 100644 --- a/packages/core/src/schema/schema.js +++ b/packages/core/src/schema/schema.js @@ -1674,8 +1674,8 @@ export const Bytes = new RawBytes() * @type {Schema.Convert<*>} */ const direct = { - tryFrom: input => input, - tryTo: output => output, + tryFrom: input => ({ ok: input }), + tryTo: output => ({ ok: output }), } /** @@ -1780,6 +1780,25 @@ class ByteView extends API { convert, }) } + + /** + * @template {Schema.BlockCodec} Codec + * @template {Schema.MultihashHasher} Hasher + * @template {Schema.UnknownLink['version']} Version + * @param {{ + * codec?: Codec + * hasher?: Hasher + * version?: Version + * }} options + * @returns {Schema.LinkSchema} + */ + link(options) { + return link({ + codec: this.codec, + ...options, + schema: this.settings.convert, + }) + } } const emptyStore = Object.freeze(new Map()) @@ -2060,7 +2079,7 @@ class Attachment { * codec?: Schema.BlockCodec, * version?: Version * hasher?: {code: Alg} - * schema: Schema.Schema + * schema: Schema.Convert * }} LinkSettings */ @@ -2199,12 +2218,12 @@ class LinkSchema extends API { * @template {Schema.MulticodecCode} Code * @template {Schema.MulticodecCode} Alg * @template {Schema.UnknownLink['version']} V - * @extends {API, Schema.IPLDView, LinkSettings>} + * @extends {API, Schema.IPLDView, LinkSettings>} * @implements {Schema.AttachmentSchema} */ class AttachmentSchema extends API { /** - * @param {Schema.IPLDView} source + * @param {Schema.IPLDView} source * @param {LinkSettings} settings * @param {Schema.Region} [region] */ @@ -2233,7 +2252,7 @@ class AttachmentSchema extends API { /** * @param {Schema.Attachment} attachment * @param {LinkSettings} settings - * @returns {Schema.ReadResult>} + * @returns {Schema.ReadResult>} */ writeWith(attachment, settings) { if (attachment instanceof Attachment) { diff --git a/packages/core/src/schema/type.ts b/packages/core/src/schema/type.ts index 1ea3aa2b..ff00b458 100644 --- a/packages/core/src/schema/type.ts +++ b/packages/core/src/schema/type.ts @@ -177,44 +177,6 @@ export interface BytesSchema< ): BytesSchema } -export interface BlockView< - T extends unknown, - Code extends MulticodecCode = MulticodecCode -> { - code: Code - encode(): ByteView - decode(): T - embed(): Attachment - - detach< - Alg extends MulticodecCode = MulticodecCode<0x12, 'sha2-256'> - >(options: { - hasher?: MultihashHasher - }): Await> -} - -export interface DAGView< - T extends unknown, - Code extends MulticodecCode = MulticodecCode, - Alg extends MulticodecCode = MulticodecCode, - V extends UnknownLink['version'] = 1 -> extends IPLDView, - Attachment { - decode(): T - link(): Link -} - -export type ToBlock = { - [K in keyof T]: T[K] extends ResolvedLink< - infer O, - infer Code, - infer Alg, - infer V - > - ? Attachment - : T[K] -} - export interface LinkOf< T extends unknown = unknown, Code extends MulticodecCode = MulticodecCode, @@ -231,7 +193,7 @@ export interface Attachment< Alg extends MulticodecCode = MulticodecCode, V extends UnknownLink['version'] = 1 > extends Link, - IPLDView { + IPLDView { link(): Link resolve(): ResolvedLink @@ -291,7 +253,10 @@ export interface AttachmentSchema< Code extends MulticodecCode, Alg extends MulticodecCode, V extends UnknownLink['version'] -> extends Schema, IPLDView> { +> extends Schema< + Attachment, + IPLDView + > { link(): never attach( target: Target, diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js index e19cf29f..19490ef7 100644 --- a/packages/core/test/delegation.spec.js +++ b/packages/core/test/delegation.spec.js @@ -1,8 +1,17 @@ import { assert, test } from './test.js' -import { CAR, CBOR, delegate, Delegation, parseLink, UCAN } from '../src/lib.js' +import { + CAR, + CBOR, + delegate, + Delegation, + parseLink, + Schema, + UCAN, +} from '../src/lib.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' import { base64 } from 'multiformats/bases/base64' import { getBlock } from './utils.js' +import { sha256 } from '../src/dag.js' const utf8 = new TextEncoder() @@ -427,8 +436,10 @@ test('archive delegation chain', async () => { assert.deepEqual(extract.ok.proofs[0], proof) }) -test.skip('delegation.attach block in capabiliy', async () => { - const block = await getBlock({ test: 'inlineBlock' }) +test('includes attachment in capability.nb', async () => { + const Block = Schema.bytes(CBOR).link({ hasher: sha256 }) + const block = await Block.attach({ test: 'inlineBlock' }) + const ucan = await Delegation.delegate({ issuer: alice, audience: bob, @@ -437,24 +448,26 @@ test.skip('delegation.attach block in capabiliy', async () => { can: 'store/add', with: alice.did(), nb: { - inlineBlock: block.cid.link(), + inlineBlock: block, }, }, ], }) - ucan.attach(block) + const delegationBlocks = [...ucan.iterateIPLDBlocks()] + const attachment = delegationBlocks.find(b => b.cid.equals(block.link())) - const delegationBlocks = [] - for (const b of ucan.iterateIPLDBlocks()) { - delegationBlocks.push(b) + if (!attachment) { + return assert.fail('attachment not found') } - assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid))) + assert.deepEqual(CBOR.decode(attachment.bytes), { test: 'inlineBlock' }) }) -test.skip('delegation.attach block in facts', async () => { - const block = await getBlock({ test: 'inlineBlock' }) +test('includes attachment if it is capability.nb', async () => { + const Block = Schema.bytes(CBOR).link({ hasher: sha256 }) + const block = await Block.attach({ test: 'inlineBlock' }) + const ucan = await Delegation.delegate({ issuer: alice, audience: bob, @@ -462,26 +475,28 @@ test.skip('delegation.attach block in facts', async () => { { can: 'store/add', with: alice.did(), + nb: block, }, ], - facts: [ - { [`${block.cid.link()}`]: block.cid.link() }, - // @ts-expect-error Link has fact entry - block.cid.link(), - ], }) - ucan.attach(block) + const delegationBlocks = [...ucan.iterateIPLDBlocks()] + const attachment = delegationBlocks.find(b => b.cid.equals(block.link())) - const delegationBlocks = [] - for (const b of ucan.iterateIPLDBlocks()) { - delegationBlocks.push(b) + if (!attachment) { + return assert.fail('attachment not found') } - assert.ok(delegationBlocks.find(b => b.cid.equals(block.cid))) + assert.deepEqual(CBOR.decode(attachment.bytes), { test: 'inlineBlock' }) }) -test('delegation.attach fails to attach block with not attached link', async () => { +test('includes attachment in facts', async () => { + const Block = Schema.unknown().link({ + codec: CBOR, + hasher: sha256, + }) + + const block = await Block.attach({ test: 'inlineBlock' }) const ucan = await Delegation.delegate({ issuer: alice, audience: bob, @@ -491,8 +506,37 @@ test('delegation.attach fails to attach block with not attached link', async () with: alice.did(), }, ], + facts: [{ [`${block.link()}`]: block }], }) - const block = await getBlock({ test: 'inlineBlock' }) - assert.throws(() => ucan.attach(block)) + const delegationBlocks = [...ucan.iterateIPLDBlocks()] + + assert.ok(delegationBlocks.find(b => b.cid.equals(block.link()))) +}) + +test('includes unboxed attachment in facts', async () => { + const Block = Schema.unknown().link({ + codec: CBOR, + hasher: sha256, + }) + + const block = await Block.attach({ test: 'inlineBlock' }) + const ucan = await Delegation.delegate({ + issuer: alice, + audience: bob, + capabilities: [ + { + can: 'store/add', + with: alice.did(), + }, + ], + facts: [ + // @ts-expect-error - expects dict + block, + ], + }) + + const delegationBlocks = [...ucan.iterateIPLDBlocks()] + + assert.ok(delegationBlocks.find(b => b.cid.equals(block.link()))) }) diff --git a/packages/core/test/invocation.spec.js b/packages/core/test/invocation.spec.js index 903cdb03..2cddecac 100644 --- a/packages/core/test/invocation.spec.js +++ b/packages/core/test/invocation.spec.js @@ -1,7 +1,8 @@ -import { invoke, UCAN, Invocation } from '../src/lib.js' +import { invoke, UCAN, Invocation, Schema, CBOR } from '../src/lib.js' import { alice, service as w3 } from './fixtures.js' import { getBlock } from './utils.js' import { assert, test } from './test.js' +import { sha256 } from '../src/dag.js' test('encode invocation', async () => { const add = invoke({ @@ -32,47 +33,50 @@ test('encode invocation', async () => { assert.deepEqual(delegation.audience.did(), w3.did()) }) -test.skip('encode invocation with attached block in capability nb', async () => { - const block = await getBlock({ test: 'inlineBlock' }) +test('encode invocation with attached block in capability nb', async () => { + const Block = Schema.unknown().link({ + codec: CBOR, + hasher: sha256, + }) + + const block = await Block.attach({ test: 'inlineBlock' }) const add = invoke({ issuer: alice, audience: w3, capability: { can: 'store/add', with: alice.did(), - link: 'bafy...stuff', nb: { - inlineBlock: block.cid.link(), + link: 'bafy...stuff', + inlineBlock: block, }, }, proofs: [], }) - add.attach(block) - /** @type {import('@ucanto/interface').BlockStore} */ - const blockStore = new Map() const view = await add.buildIPLDView() - for (const b of view.iterateIPLDBlocks()) { - blockStore.set(`${b.cid}`, b) - } + + const blocks = new Map( + [...view.iterateIPLDBlocks()].map(block => [`${block.cid}`, block]) + ) // blockstore has attached block - assert.ok(blockStore.get(`${block.cid}`)) + assert.ok(blocks.get(`${block.link()}`)) const reassembledInvocation = Invocation.view({ root: view.root.cid.link(), - blocks: blockStore, + blocks, }) - /** @type {import('@ucanto/interface').BlockStore} */ - const reassembledBlockstore = new Map() - - for (const b of reassembledInvocation.iterateIPLDBlocks()) { - reassembledBlockstore.set(`${b.cid}`, b) - } + const reassembledBlockstore = new Map( + [...reassembledInvocation.iterateIPLDBlocks()].map(block => [ + `${block.cid}`, + block, + ]) + ) // reassembledBlockstore has attached block - assert.ok(reassembledBlockstore.get(`${block.cid}`)) + assert.ok(reassembledBlockstore.get(`${block.link()}`)) }) test('expired invocation', async () => { diff --git a/packages/core/test/message.spec.js b/packages/core/test/message.spec.js index dcbd21c1..462cf54d 100644 --- a/packages/core/test/message.spec.js +++ b/packages/core/test/message.spec.js @@ -72,6 +72,8 @@ test('Message.view', async () => { invocations: [hi.invocation], }) + assert.equal(buildHi.link(), buildHi.root.cid) + assert.throws( () => Message.view({ From 4d8fe017673e6eabe90e27773f7cf6ee8200b627 Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Wed, 10 May 2023 23:38:01 -0700 Subject: [PATCH 16/16] remove obsolete code --- packages/core/test/delegation.spec.js | 1 - packages/core/test/invocation.spec.js | 1 - packages/core/test/utils.js | 14 -------------- 3 files changed, 16 deletions(-) delete mode 100644 packages/core/test/utils.js diff --git a/packages/core/test/delegation.spec.js b/packages/core/test/delegation.spec.js index 19490ef7..f5580013 100644 --- a/packages/core/test/delegation.spec.js +++ b/packages/core/test/delegation.spec.js @@ -10,7 +10,6 @@ import { } from '../src/lib.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' import { base64 } from 'multiformats/bases/base64' -import { getBlock } from './utils.js' import { sha256 } from '../src/dag.js' const utf8 = new TextEncoder() diff --git a/packages/core/test/invocation.spec.js b/packages/core/test/invocation.spec.js index 2cddecac..89c0e33e 100644 --- a/packages/core/test/invocation.spec.js +++ b/packages/core/test/invocation.spec.js @@ -1,6 +1,5 @@ import { invoke, UCAN, Invocation, Schema, CBOR } from '../src/lib.js' import { alice, service as w3 } from './fixtures.js' -import { getBlock } from './utils.js' import { assert, test } from './test.js' import { sha256 } from '../src/dag.js' diff --git a/packages/core/test/utils.js b/packages/core/test/utils.js deleted file mode 100644 index e0d105ba..00000000 --- a/packages/core/test/utils.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as Block from 'multiformats/block' -import * as codec from '@ipld/dag-cbor' -import { sha256 as hasher } from 'multiformats/hashes/sha2' - -/** - * @param {any} value - */ -export async function getBlock(value) { - return await Block.encode({ - value, - codec, - hasher - }) -}