Skip to content

Commit

Permalink
feat: implement store/add size constraint (#89)
Browse files Browse the repository at this point in the history
* feat: add size constraint to store/add

* feat: add store/add tests

* Apply suggestions from code review
  • Loading branch information
Gozala authored Oct 4, 2022
1 parent 6c0e24f commit efd8a2f
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 32 deletions.
19 changes: 17 additions & 2 deletions packages/access/src/capabilities/store.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable unicorn/no-null */
import { capability, Failure, Link, URI } from '@ucanto/server'
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import { canDelegateURI, derives, equalWith } from './utils.js'
import { canDelegateURI, derives, equalWith, Integer } from './utils.js'
import { any } from './any.js'

/**
Expand Down Expand Up @@ -35,8 +36,22 @@ export const add = base.derive({
caveats: {
link: Link.optional(),
origin: Link.optional(),
size: Integer.optional(),
},
derives: (claim, from) => {
const result = derives(claim, from)
if (result.error) {
return result
} else if (claim.caveats.size != null && from.caveats.size != null) {
return claim.caveats.size > from.caveats.size
? new Failure(
`Size constraint violation: ${claim.caveats.size} > ${from.caveats.size}`
)
: true
} else {
return true
}
},
derives,
}),
derives: equalWith,
})
Expand Down
12 changes: 11 additions & 1 deletion packages/access/src/capabilities/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { Capability, IPLDLink, DID, ToString } from '@ipld/dag-ucan'
import type {
Capability,
IPLDLink,
DID,
ToString,
Phantom,
} from '@ipld/dag-ucan'
import type { Block as IPLDBlock } from '@ucanto/interface'
import { codec as CARCodec } from '@ucanto/transport/car'

Expand Down Expand Up @@ -48,9 +54,13 @@ export interface IdentityRegister
export interface IdentityIdentify
extends Capability<'identity/identify', DID> {}

export type integer = number & Phantom<{ kind: 'integer' }>

// Store
export interface StoreAdd extends Capability<'store/add', DID> {
link?: IPLDLink
origin?: IPLDLink
size?: integer
}

export interface StoreRemove extends Capability<'store/remove', DID> {
Expand Down
183 changes: 154 additions & 29 deletions packages/access/src/capabilities/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function equal(child, parent, constraint) {
* @template {Types.ParsedCapability<"store/add"|"store/remove", Types.URI<'did:'>, {link?: Types.Link<unknown, number, number, 0|1>}>} T
* @param {T} claimed
* @param {T} delegated
* @returns {Types.Result<true, Types.Failure>}
*/
export const derives = (claimed, delegated) => {
if (claimed.uri.href !== delegated.uri.href) {
Expand Down Expand Up @@ -86,35 +87,6 @@ export function fail(value) {
return value === true ? undefined : value
}

export const List = {
/**
* @template T
* @param {Types.Decoder<unknown, T>} decoder
* @returns {Types.Decoder<unknown, T[]> & { optional(): Types.Decoder<unknown, undefined|Array<T>>}}
*/
of: (decoder) => ({
decode: (input) => {
if (!Array.isArray(input)) {
return new Failure(`Expected to be an array instead got ${input} `)
}
/** @type {T[]} */
const results = []
for (const item of input) {
const result = decoder.decode(item)
if (result?.error) {
return new Failure(
`Array containts invalid element: ${result.message}`
)
} else {
results.push(result)
}
}
return results
},
optional: () => optional(List.of(decoder)),
}),
}

/**
* @template T
* @param {Types.Decoder<unknown, T>} decoder
Expand All @@ -123,3 +95,156 @@ export const List = {
export const optional = (decoder) => ({
decode: (input) => (input === undefined ? input : decoder.decode(input)),
})

/**
* @template T
* @implements {Types.Decoder<unknown, T, Types.Failure>}
*/
class Never {
/**
* @param {unknown} input
* @returns {Types.Result<T, Types.Failure>}
*/
decode(input) {
return new Failure(`Given input is not valid`)
}

/**
* @returns {Types.Decoder<unknown, undefined|T, Types.Failure>}
*/
optional() {
return new Optional(this)
}
}

/**
* @template T
* @implements {Types.Decoder<unknown, T|undefined, Types.Failure>}
*/
class Optional {
/**
* @param {Types.Decoder<unknown, T, Types.Failure>} decoder
*/
constructor(decoder) {
this.decoder = decoder
}

optional() {
return this
}

/**
* @param {unknown} input
*/
decode(input) {
return input === undefined ? undefined : this.decoder.decode(input)
}
}

/**
* @template T
* @extends {Never<T[]>}
* @implements {Types.Decoder<unknown, T[], Types.Failure>}
*/
export class List extends Never {
/**
* @template T
* @param {Types.Decoder<unknown, T, Types.Failure>} decoder
*/
static of(decoder) {
return new this(decoder)
}

/**
* @param {Types.Decoder<unknown, T, Types.Failure>} decoder
* @private
*/
constructor(decoder) {
super()
this.decoder = decoder
}

/**
* @param {unknown} input
*/
decode(input) {
if (!Array.isArray(input)) {
return new Failure(`Expected to be an array instead got ${input} `)
}
/** @type {T[]} */
const results = []
for (const item of input) {
const result = this.decoder.decode(item)
if (result?.error) {
return new Failure(`Array containts invalid element: ${result.message}`)
} else {
results.push(result)
}
}
return results
}
}

/**
* @typedef {Types.Phantom<{kind:"Int"}> & number} integer
* @extends {Never<integer>}
* @implements {Types.Decoder<unknown, integer, Types.Failure>}
*/
export class IntegerDecoder extends Never {
/**
* @param {{min?: number, max?: number}} options
*/
// eslint-disable-next-line unicorn/prefer-number-properties
constructor({ min = -Infinity, max = Infinity } = {}) {
super()
this.min = min
this.max = max
}

/**
* @param {unknown} value
* @returns {value is integer}
*/
static isInteger(value) {
return Number.isInteger(value)
}

/**
* @param {unknown} input
* @returns {Types.Result<integer, Types.Failure>}
*/
decode(input) {
const { min, max } = this
if (!IntegerDecoder.isInteger(input)) {
return new Failure(
`Expecting an Integer but instead got: ${typeof input} ${input}`
)
} else if (min > input) {
return new Failure(
`Expecting an Integer > ${min} but instead got ${input}`
)
} else if (max < input) {
return new Failure(
`Expecting an Integer < ${max} but instead got ${input}`
)
} else {
return input
}
}

/**
* @param {number} min
*/
greater(min) {
return new IntegerDecoder({ min, max: this.max })
}

/**
* @param {number} max
*/
less(max) {
return new IntegerDecoder({ min: this.min, max })
}
}

export const Integer = new IntegerDecoder()
Loading

0 comments on commit efd8a2f

Please sign in to comment.