Skip to content

Commit

Permalink
feat: Add Uint64 support to schema (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gozala authored Jul 13, 2023
1 parent d6f1f7b commit 2a74f92
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 10 deletions.
84 changes: 74 additions & 10 deletions packages/core/src/schema/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,70 @@ const Integer = {
const anyInteger = anyNumber.refine(Integer)
export const integer = () => anyInteger

const MAX_UINT64 = 2n ** 64n - 1n

/**
* @template {bigint} [O=Schema.Uint64]
* @template [I=unknown]
* @extends {API<O, I, void>}
* @implements {Schema.Schema<O, I>}
*/
class Uint64Schema extends API {
/**
* @param {I} input
* @returns {Schema.ReadResult<O>}
*/
read(input) {
switch (typeof input) {
case 'bigint':
return input > MAX_UINT64
? error(`Integer is too big for uint64, ${input} > ${MAX_UINT64}`)
: input < 0
? error(
`Negative integer can not be represented as uint64, ${input} < ${0}`
)
: { ok: /** @type {I & O} */ (input) }

case 'number':
return !Number.isInteger(input)
? typeError({
expect: 'uint64',
actual: input,
})
: input < 0
? error(
`Negative integer can not be represented as uint64, ${input} < ${0}`
)
: { ok: /** @type {O} */ (BigInt(input)) }

default:
return typeError({
expect: 'uint64',
actual: input,
})
}
}

toString() {
return `uint64`
}
}

/** @type {Schema.Schema<Schema.Uint64, unknown>} */
const Uint64 = new Uint64Schema()

/**
* Creates a schema for {@link Schema.Uint64} values represented as a`bigint`.
*
* ⚠️ Please note that while IPLD in principal considers the range of integers
* to be infinite n practice, many libraries / codecs may choose to implement
* things in such a way that numbers may have limited sizes.
*
* So please use this with caution and always ensure that used codecs do support
* uint64.
*/
export const uint64 = () => Uint64

const Float = {
/**
* @param {number} number
Expand Down Expand Up @@ -1070,7 +1134,7 @@ class Literal extends API {
return super.default(value)
}
toString() {
return `literal(${displayTypeName(this.value)})`
return `literal(${toString(this.value)})`
}
}

Expand Down Expand Up @@ -1143,10 +1207,10 @@ class Struct extends API {
toString() {
return [
`struct({ `,
...Object.entries(this.shape).map(
([key, schema]) => `${key}: ${schema}, `
),
`})`,
...Object.entries(this.shape)
.map(([key, schema]) => `${key}: ${schema}`)
.join(', '),
` })`,
].join('')
}

Expand Down Expand Up @@ -1369,7 +1433,7 @@ class TypeError extends SchemaError {
return 'TypeError'
}
describe() {
return `Expected value of type ${this.expect} instead got ${displayTypeName(
return `Expected value of type ${this.expect} instead got ${toString(
this.actual
)}`
}
Expand All @@ -1387,7 +1451,7 @@ export const typeError = data => ({ error: new TypeError(data) })
*
* @param {unknown} value
*/
const displayTypeName = value => {
export const toString = value => {
const type = typeof value
switch (type) {
case 'boolean':
Expand Down Expand Up @@ -1430,9 +1494,9 @@ class LiteralError extends SchemaError {
return 'LiteralError'
}
describe() {
return `Expected literal ${displayTypeName(
this.expect
)} instead got ${displayTypeName(this.actual)}`
return `Expected literal ${toString(this.expect)} instead got ${toString(
this.actual
)}`
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/schema/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type Branded<T, Brand> = T & {

export type Integer = number & Phantom<{ typeof: 'integer' }>
export type Float = number & Phantom<{ typeof: 'float' }>
export type Uint64 = bigint & Phantom<{ typeof: 'uint64' }>

export type Infer<T extends Reader> = T extends Reader<infer T, any> ? T : never

Expand Down
47 changes: 47 additions & 0 deletions packages/core/test/uint64-schema.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as Schema from '../src/schema.js'
import { test, assert } from './test.js'

/** @type {[unknown, bigint|RegExp][]} */
const vector = [
[0, 0n],
[-0, 0n],
[-1, /negative integer can not/i],
[0n, 0n],
[10, 10n],
[-10n, /negative integer can not/i],
[10n, 10n],
[Infinity, /value of type uint64.*Infinity/],
[-Infinity, /value of type uint64.*\-Infinity/],
[new Number(17), /value of type uint64.*object/],
[0xffffffffffffffffn, 0xffffffffffffffffn],
[0xffffffffffffffffn + 1n, /is too big/],
[4.2, /value of type uint64.*4\.2/],
['7n', /value of type uint64.*"7n"/],
[[7n], /value of type uint64.*array/],
]

for (const [input, expect] of vector) {
test(`uint64().from(${Schema.toString(input)})`, () => {
const schema = Schema.uint64()
const result = schema.read(input)
if (expect instanceof RegExp) {
assert.throws(() => schema.from(input), expect)
assert.match(String(result.error), expect)
} else {
assert.equal(schema.from(input), expect)
assert.deepEqual(result, { ok: expect })
}
})
}

test('struct with uint64', () => {
const Piece = Schema.struct({ size: Schema.uint64() })
assert.equal(String(Piece), 'struct({ size: uint64 })')

assert.deepEqual(Piece.from({ size: 0 }), { size: 0n })
assert.deepEqual(Piece.from({ size: 10n }), { size: 10n })
assert.throws(() => Piece.from({ size: 0xffffffffffffffffn + 1n }), /too big/)
assert.throws(() => Piece.from({ size: -1 }), /negative integer/i)

assert.throws(() => Piece.from({ size: null }), /type uint64/i)
})

0 comments on commit 2a74f92

Please sign in to comment.