From 0ce8cbb90c915a344c41f606c2ac257358b31c6a Mon Sep 17 00:00:00 2001 From: Irakli Gozalishvili Date: Mon, 1 May 2023 22:51:19 -0700 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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) {