Skip to content

Commit

Permalink
feat(core): add schema type for reading did bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
hannahhoward committed Jan 17, 2025
1 parent 6de27c0 commit d7da54c
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ 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 did, matchBytes as didBytes} from './schema/did.js'
export { match as uri } from './schema/uri.js'
export { match as text } from './schema/text.js'
54 changes: 53 additions & 1 deletion packages/core/src/schema/did.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as API from '@ucanto/interface'
import * as Schema from './schema.js'

import * as DID from '@ipld/dag-ucan/did'
/**
* @template {string} Method
* @extends {Schema.API<API.DID<Method> & API.URI<"did:">, string, void|Method>}
Expand Down Expand Up @@ -43,3 +43,55 @@ export const match = (options = {}) =>
* @param {unknown} input
*/
export const from = input => match({}).from(input)

/**
* @template {string} Method
* @extends {Schema.API<API.DID<Method> & API.URI<"did:">, unknown, void|Method>}
*/
class DIDBytesSchema extends Schema.API {
/**
* @param {unknown} source
* @param {void|Method} method
*/
readWith(source, method) {
if (!(source instanceof Uint8Array)) {
return Schema.typeError({ expect: 'Uint8Array', actual: source })
}
let did
try {
did = DID.decode(source).did()
} catch (err) {
return Schema.error(`Unable to parse bytes as did: ${err}`)
}
const prefix = method ? `did:${method}:` : `did:`
if (!did.startsWith(prefix)) {
return Schema.error(`Expected a ${prefix} but got "${did}" instead`)
} else {
return { ok: /** @type {API.DID<Method>} */ (did) }
}
}
}

const schemaBytes = new DIDBytesSchema()

export const didBytes = () => schemaBytes
/**
*
* @param {unknown} input
*/
export const readBytes = input => schemaBytes.read(input)

/**
* @template {string} Method
* @param {{method?: Method}} options
*/
export const matchBytes = (options = {}) =>
/** @type {Schema.Schema<API.DID<Method> & API.URI<"did:">>} */ (
new DIDBytesSchema(options.method)
)

/**
* Create a DID string from any input (or throw)
* @param {unknown} input
*/
export const fromBytes = input => matchBytes({}).from(input)
111 changes: 111 additions & 0 deletions packages/core/test/extra-schema.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { URI, Link, Text, DID } from '../src/schema.js'
import { test, assert, matchResult } from './test.js'
import * as API from '@ucanto/interface'
import * as DIDTools from '@ipld/dag-ucan/did'

{
/** @type {[string, API.Result|RegExp][]} */
Expand Down Expand Up @@ -382,3 +383,113 @@ test('URI.from', () => {
})
}
}

{
/** @type {any[][]} */
const dataset = [
[undefined, /Expected value of type Uint8Array instead got undefined/],
[null, /Expected value of type Uint8Array instead got null/],
[Uint8Array.from([1, 2, 3]), /Unable to parse bytes as did:/],
[DIDTools.parse('did:echo:1'), { ok: 'did:echo:1' }],
]

for (const [input, out] of dataset) {
test(`DID.read(${input})`, () => {
matchResult(DID.readBytes(input), out)
})
}
}

{
/** @type {[{method:string}, unknown, API.Result|RegExp][]} */
const dataset = [
[
{ method: 'echo' },
undefined,
/Expected value of type Uint8Array instead got undefined/,
],
[
{ method: 'echo' },
null,
/Expected value of type Uint8Array instead got null/,
],
[
{ method: 'echo' },
Uint8Array.from([1, 2, 3]),
/Unable to parse bytes as did:/,
],
[{ method: 'echo' }, DIDTools.parse('did:echo:hello'), { ok: 'did:echo:hello' }],
[
{ method: 'foo' },
DIDTools.parse('did:echo:hello'),
/Expected a did:foo: but got "did:echo:hello" instead/,
],
]

for (const [options, input, out] of dataset) {
test(`DID.match({ method: ${options.method} }).read(${input})`, () => {
matchResult(DID.matchBytes(options).read(input), out)
})
}
}

{
/** @type {[{method?:string}, unknown, API.Result|RegExp][]} */
const dataset = [
[{}, undefined, { ok: undefined }],
[{}, null, /Expected value of type Uint8Array instead got null/],
[{}, DIDTools.parse('did:echo:bar'), { ok: 'did:echo:bar' }],
[{ method: 'echo' }, undefined, { ok: undefined }],
[
{ method: 'echo' },
null,
/Expected value of type Uint8Array instead got null/,
],
[
{ method: 'echo' },
DIDTools.parse('did:hello:world'),
/Expected a did:echo: but got "did:hello:world" instead/,
],
[
{ method: 'echo' },
Uint8Array.from([1, 2, 3]),
/Unable to parse bytes as did:/,
],
]

for (const [options, input, out] of dataset) {
test(`DID.match({ method: "${options.method}" }).optional().read(${input})`, () => {
const schema = options.method ? DID.matchBytes(options) : DID.didBytes()
matchResult(schema.optional().read(input), out)
})
}
}

{
/** @type {Array<[unknown, null|RegExp]>} */
const dataset = [
[DIDTools.parse('did:foo:bar'), null],
[DIDTools.parse('did:web:example.com'), null],
[DIDTools.parse('did:twosegments'), null],
[Uint8Array.from([1, 2, 3]), /Unable to parse bytes as did:/],
[
undefined,
/TypeError: Expected value of type Uint8Array instead got undefined/,
],
]
for (const [did, errorExpectation] of dataset) {
test(`DID.from("${did}")`, () => {
let error
try {
DID.fromBytes(did)
} catch (_error) {
error = _error
}
if (errorExpectation) {
assert.match(String(error), errorExpectation)
} else {
assert.notOk(error, 'expected no error, but got an error')
}
})
}
}

0 comments on commit d7da54c

Please sign in to comment.