Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Uint64 support to schema #315

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})