Skip to content

Commit

Permalink
feat!: Use schema stuff in the capabilities instead of custom parsing (
Browse files Browse the repository at this point in the history
…#220)

* feat: .partial for schema with map representation

* feat: switch capability to use schema

* chore: remove dead code

* add a comment

* chore: cover remaining line

* fix: remaining problems

* chore: undo obsolete change

* chore: remove some obsolete code
  • Loading branch information
Gozala authored Feb 14, 2023
1 parent fd4df81 commit 8a578ae
Show file tree
Hide file tree
Showing 18 changed files with 386 additions and 329 deletions.
109 changes: 44 additions & 65 deletions packages/interface/src/capability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ import {
Await,
IssuedInvocationView,
UCANOptions,
DIDKey,
Verifier,
API,
} from './lib.js'

export interface Source {
capability: Capability
capability: { can: Ability; with: URI; nb?: Caveats }
delegation: Delegation
index: number
}
Expand Down Expand Up @@ -67,7 +65,7 @@ export interface Reader<
}

export interface Caveats {
[key: string]: Reader<any, unknown>
[key: string]: unknown
}

export type MatchResult<M extends Match> = Result<M, InvalidCapability>
Expand All @@ -80,17 +78,12 @@ export type InvalidCapability = UnknownCapability | MalformedCapability
export interface DerivedMatch<T, M extends Match>
extends Match<T, M | DerivedMatch<T, M>> {}

export interface DeriveSelector<M extends Match, T extends ParsedCapability> {
to: TheCapabilityParser<DirectMatch<T>>
derives: Derives<ToDeriveClaim<T>, ToDeriveProof<M['value']>>
}

/**
* Utility type is used to infer the type of the capability passed into
* `derives` handler. It simply makes all `nb` fileds optional because
* `derives` handler. It simply makes all `nb` fields optional because
* in delegation all `nb` fields could be left out implying no restrictions.
*/
export type ToDeriveClaim<T extends ParsedCapability> =
type ToDeriveClaim<T extends ParsedCapability> =
| T
| ParsedCapability<T['can'], T['with'], Partial<T['nb']>>

Expand All @@ -101,18 +94,18 @@ export type ToDeriveClaim<T extends ParsedCapability> =
* all `nb` fields optional, because in delegation all `nb` fields could be
* left out implying no restrictions.
*/
export type ToDeriveProof<T> = T extends ParsedCapability
export type InferDeriveProof<T> = T extends ParsedCapability
? // If it a capability we just make `nb` partial
ToDeriveClaim<T>
InferDelegatedCapability<T>
: // otherwise we need to map tuple
ToDeriveProofs<T>
InferDeriveProofs<T>

/**
* Another helper type which is equivalent of `ToDeriveClaim` except it works
* on tuple of capabilities.
*/
type ToDeriveProofs<T> = T extends [infer U, ...infer E]
? [ToDeriveClaim<U & ParsedCapability>, ...ToDeriveProofs<E>]
type InferDeriveProofs<T> = T extends [infer U, ...infer E]
? [ToDeriveClaim<U & ParsedCapability>, ...InferDeriveProofs<E>]
: T extends never[]
? []
: never
Expand Down Expand Up @@ -154,33 +147,27 @@ export interface View<M extends Match> extends Matcher<M>, Selector<M> {
* })
* ```
*/
derive<T extends ParsedCapability>(
options: DeriveSelector<M, T>
): TheCapabilityParser<DerivedMatch<T, M>>
derive<T extends ParsedCapability>(options: {
to: TheCapabilityParser<DirectMatch<T>>
derives: Derives<T, InferDeriveProof<M['value']>>
}): TheCapabilityParser<DerivedMatch<T, M>>
}

export type InferCaveatParams<T> = keyof T extends never
? never | undefined
: {
[K in keyof T]: T[K] extends { toJSON(): infer U } ? U : T[K]
}

export interface TheCapabilityParser<M extends Match<ParsedCapability>>
extends CapabilityParser<M> {
readonly can: M['value']['can']

create(
input: InferCreateOptions<M['value']['with'], M['value']['nb']>
): M['value']
): InferCapability<M['value']>

/**
* Creates an invocation of this capability. Function throws exception if
* non-optional fields are omitted.
*/

invoke(
options: InferInvokeOptions<M['value']['with'], M['value']['nb']>
): IssuedInvocationView<M['value']>
): IssuedInvocationView<InferCapability<M['value']>>

/**
* Creates a delegation of this capability. Please note that all the
Expand All @@ -189,12 +176,31 @@ export interface TheCapabilityParser<M extends Match<ParsedCapability>>
*/
delegate(
options: InferDelegationOptions<M['value']['with'], M['value']['nb']>
): Promise<Delegation<[ToDeriveClaim<M['value']>]>>
): Promise<Delegation<[InferDelegatedCapability<M['value']>]>>
}

/**
* When normalize capabilities by removing `nb` if it is a `{}`. This type
* does that normalization at the type level.
*/
export type InferCapability<T extends ParsedCapability> =
keyof T['nb'] extends never
? { can: T['can']; with: T['with'] }
: { can: T['can']; with: T['with']; nb: T['nb'] }

/**
* In delegation capability all the `nb` fields are optional. This type maps
* capability type (as it would be in the invocation) to the form it will be
* in the delegation.
*/
export type InferDelegatedCapability<T extends ParsedCapability> =
keyof T['nb'] extends never
? { can: T['can']; with: T['with'] }
: { can: T['can']; with: T['with']; nb: Partial<T['nb']> }

export type InferCreateOptions<R extends Resource, C extends {} | undefined> =
// If capability has no NB we want to prevent passing it into
// .create funciton so we make `nb` as optional `never` type so
// .create function so we make `nb` as optional `never` type so
// it can not be satisfied
keyof C extends never ? { with: R; nb?: never } : { with: R; nb: C }

Expand All @@ -213,22 +219,13 @@ export type InferDelegationOptions<
}

export type EmptyObject = { [key: string | number | symbol]: never }
type Optionalize<T> = InferRequried<T> & InferOptional<T>

type InferOptional<T> = {
[K in keyof T as T[K] | undefined extends T[K] ? K : never]?: T[K]
}

type InferRequried<T> = {
[K in keyof T as T[K] | undefined extends T[K] ? never : K]: T[K]
}

export interface CapabilityParser<M extends Match = Match> extends View<M> {
/**
* Defines capability that is either `this` or the the given `other`. This
* allows you to compose multiple capabilities into one so that you could
* validate any of one of them without having to maintain list of supported
* capabilities. It is especially useful when dealiving with derived
* capabilities. It is especially useful when dealing with derived
* capability chains when you might derive capability from either one or the
* other.
*/
Expand Down Expand Up @@ -282,7 +279,7 @@ export interface CapabilitiesParser<M extends Match[] = Match[]>
extends View<Amplify<M>> {
/**
* Creates new capability group containing capabilities from this group and
* provedid `other` capability. This method complements `and` method on
* provided `other` capability. This method complements `and` method on
* `Capability` to allow chaining e.g. `read.and(write).and(modify)`.
*/
and<W extends Match>(other: MatchSelector<W>): CapabilitiesParser<[...M, W]>
Expand Down Expand Up @@ -312,39 +309,21 @@ export type InferMatch<Members extends unknown[]> = Members extends []
? [M, ...InferMatch<Rest>]
: never

export type ParsedCapability<
export interface ParsedCapability<
Can extends Ability = Ability,
Resource extends URI = URI,
C extends object = {}
> = keyof C extends never
? { can: Can; with: Resource; nb?: C }
: { can: Can; with: Resource; nb: C }

export type InferCaveats<C> = Optionalize<{
[K in keyof C]: C[K] extends Reader<infer T, unknown, infer _> ? T : never
}>

export interface Descriptor<
A extends Ability,
R extends URI,
C extends Caveats
C extends Caveats = {}
> {
can: A
with: Reader<R, Resource, Failure>

nb?: C

derives?: Derives<
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>,
ToDeriveClaim<ParsedCapability<A, R, InferCaveats<C>>>
>
can: Can
with: Resource
nb: C
}

export interface CapabilityMatch<
A extends Ability,
R extends URI,
C extends Caveats
> extends DirectMatch<ParsedCapability<A, R, InferCaveats<C>>> {}
> extends DirectMatch<ParsedCapability<A, R, C>> {}

export interface CanIssue {
/**
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as API from '@ucanto/interface'
import { InferCaveats, CanIssue, ParsedCapability } from '@ucanto/interface'
import { CanIssue, ParsedCapability } from '@ucanto/interface'

export * from '@ucanto/interface'

Expand All @@ -23,8 +23,8 @@ export interface ProviderContext<
R extends API.URI = API.URI,
C extends API.Caveats = API.Caveats
> {
capability: API.ParsedCapability<A, R, API.InferCaveats<C>>
invocation: API.Invocation<API.Capability<A, R, API.InferCaveats<C>>>
capability: API.ParsedCapability<A, R, C>
invocation: API.Invocation<API.Capability<A, R, C>>

context: API.InvocationContext
}
Expand Down
8 changes: 4 additions & 4 deletions packages/server/src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { access } from '@ucanto/validator'
* @template {API.URI} R
* @template {API.Caveats} C
* @template {unknown} U
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, API.InferCaveats<C>>>>} capability
* @param {(input:API.ProviderInput<API.ParsedCapability<A, R, API.InferCaveats<C>>>) => API.Await<U>} handler
* @returns {API.ServiceMethod<API.Capability<A, R, API.InferCaveats<C>>, Exclude<U, {error:true}>, Exclude<U, Exclude<U, {error:true}>>>}
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, C>>>} capability
* @param {(input:API.ProviderInput<API.ParsedCapability<A, R, C>>) => API.Await<U>} handler
* @returns {API.ServiceMethod<API.Capability<A, R, C>, Exclude<U, {error:true}>, Exclude<U, Exclude<U, {error:true}>>>}
*/

export const provide =
(capability, handler) =>
/**
* @param {API.Invocation<API.Capability<A, R, API.InferCaveats<C>>>} invocation
* @param {API.Invocation<API.Capability<A, R, C>>} invocation
* @param {API.InvocationContext} options
*/
async (invocation, options) => {
Expand Down
9 changes: 5 additions & 4 deletions packages/server/test/server.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import * as CBOR from '@ucanto/transport/cbor'
import { alice, bob, mallory, service as w3 } from './fixtures.js'
import * as Service from '../../client/test/service.js'
import { test, assert } from './test.js'
import { Schema } from '@ucanto/validator'

const storeAdd = Server.capability({
can: 'store/add',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand All @@ -34,9 +35,9 @@ const storeAdd = Server.capability({
const storeRemove = Server.capability({
can: 'store/remove',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand Down
9 changes: 5 additions & 4 deletions packages/server/test/service/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { provide } from '../../src/handler.js'
import * as API from './api.js'
import * as Access from './access.js'
import { service as issuer } from '../fixtures.js'
import { Schema } from '@ucanto/validator/src/lib.js'

const addCapability = Server.capability({
can: 'store/add',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand All @@ -34,9 +35,9 @@ const addCapability = Server.capability({
const removeCapability = Server.capability({
can: 'store/remove',
with: Server.URI.match({ protocol: 'did:' }),
nb: {
nb: Schema.struct({
link: Server.Link.match().optional(),
},
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return new Server.Failure(
Expand Down
Loading

0 comments on commit 8a578ae

Please sign in to comment.