Skip to content
Open
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
2 changes: 1 addition & 1 deletion ark/docs/components/dts/schema.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ark/docs/components/dts/type.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ark/docs/components/dts/util.ts

Large diffs are not rendered by default.

55 changes: 54 additions & 1 deletion ark/schema/__tests__/bounds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
Disjoint,
boundKindPairsByLower,
rootSchema,
writeInvalidLengthBoundMessage
writeInvalidLengthBoundMessage,
writeInvalidSizeBoundMessage
} from "@ark/schema"
import { entriesOf, flatMorph } from "@ark/util"

Expand All @@ -19,6 +20,11 @@ const dateCases = flatMorph(numericCases, (name, v) => [name, new Date(v)])

const lengthCases = flatMorph(numericCases, (name, v) => [name, "1".repeat(v)])

const fileCases = flatMorph(numericCases, (name, v) => [
name,
new File(["x".repeat(v)], "test.txt")
])

contextualize(() => {
it("numeric apply", () => {
const T = rootSchema({
Expand Down Expand Up @@ -86,6 +92,45 @@ contextualize(() => {
)
})

it("file apply", () => {
const T = rootSchema({
proto: File,
minSize: { rule: 5, exclusive: true },
maxSize: { rule: 10 }
})

attest(T.traverse(fileCases.lessThanMin)?.toString()).snap(
"must be more than 5 bytes (was 4 bytes)"
)
attest(T.traverse(fileCases.equalToExclusiveMin)?.toString()).snap(
"must be more than 5 bytes (was 5 bytes)"
)
attest(T.traverse(fileCases.between)).equals(fileCases.between)
attest(T.traverse(fileCases.equalToInclusiveMax)).equals(
fileCases.equalToInclusiveMax
)
attest(T.traverse(fileCases.greaterThanMax)?.toString()).snap(
"must be at most 10 bytes (was 11 bytes)"
)
})

it("errors on negative size bound", () => {
attest(() => rootSchema({ proto: File, maxSize: -1 })).throws(
writeInvalidSizeBoundMessage("maxSize", -1)
)
})

it("errors on non-integer size bound", () => {
attest(() => rootSchema({ proto: File, minSize: 1.5 })).throws(
writeInvalidSizeBoundMessage("minSize", 1.5)
)
})

it("minSize 0 reduces to unconstrained", () => {
const T = rootSchema({ proto: File, minSize: 0 })
attest(T.expression).snap("File")
})

it("errors on negative length bound", () => {
attest(() => rootSchema({ domain: "string", maxLength: -1 })).throws(
writeInvalidLengthBoundMessage("maxLength", -1)
Expand All @@ -108,10 +153,12 @@ contextualize(() => {
const basis =
min === "min" ? { domain: "number" }
: min === "minLength" ? { domain: "string" }
: min === "minSize" ? { proto: File }
: { proto: Date }
const cases =
min === "min" ? numericCases
: min === "minLength" ? lengthCases
: min === "minSize" ? fileCases
: dateCases

it("allows", () => {
Expand Down Expand Up @@ -151,6 +198,12 @@ contextualize(() => {
...basis,
exactLength: 6
} as never)
: min === "minSize" ?
rootSchema({
...basis,
minSize: { rule: 6 },
maxSize: { rule: 6 }
} as never)
: rootSchema({
unit: new Date(6)
})
Expand Down
4 changes: 4 additions & 0 deletions ark/schema/__tests__/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,10 @@ contextualize(() => {
"intersection",
"max",
"maxLength",
"maxSize",
"min",
"minLength",
"minSize",
"morph",
"optional",
"pattern",
Expand Down Expand Up @@ -160,8 +162,10 @@ contextualize(() => {
"intersection",
"max",
"maxLength",
"maxSize",
"min",
"minLength",
"minSize",
"morph",
"optional",
"pattern",
Expand Down
2 changes: 2 additions & 0 deletions ark/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export * from "./refinements/exactLength.ts"
export * from "./refinements/kinds.ts"
export * from "./refinements/max.ts"
export * from "./refinements/maxLength.ts"
export * from "./refinements/maxSize.ts"
export * from "./refinements/min.ts"
export * from "./refinements/minLength.ts"
export * from "./refinements/minSize.ts"
export * from "./refinements/pattern.ts"
export * from "./refinements/range.ts"
export * from "./roots/domain.ts"
Expand Down
4 changes: 3 additions & 1 deletion ark/schema/intrinsic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FileConstructor } from "@ark/util"
import { node, schemaScope } from "./scope.ts"
import { $ark } from "./shared/registry.ts"
import { arrayIndexSource } from "./structure/shared.ts"
Expand All @@ -18,7 +19,8 @@ const intrinsicBases = schemaScope(
unknown: {},
undefined: { unit: undefined },
Array,
Date
Date,
File: FileConstructor
},
{ prereducedAliases: true }
).export()
Expand Down
14 changes: 12 additions & 2 deletions ark/schema/refinements/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { Before } from "./before.ts"
import { ExactLength } from "./exactLength.ts"
import { Max } from "./max.ts"
import { MaxLength } from "./maxLength.ts"
import { MaxSize } from "./maxSize.ts"
import { Min } from "./min.ts"
import { MinLength } from "./minLength.ts"
import { MinSize } from "./minSize.ts"

export interface BoundDeclarations {
min: Min.Declaration
Expand All @@ -16,6 +18,8 @@ export interface BoundDeclarations {
exactLength: ExactLength.Declaration
after: After.Declaration
before: Before.Declaration
minSize: MinSize.Declaration
maxSize: MaxSize.Declaration
}

export interface BoundNodesByKind {
Expand All @@ -26,6 +30,8 @@ export interface BoundNodesByKind {
exactLength: ExactLength.Node
after: After.Node
before: Before.Node
minSize: MinSize.Node
maxSize: MaxSize.Node
}

export type BoundKind = keyof BoundDeclarations
Expand All @@ -43,7 +49,9 @@ export const boundImplementationsByKind: boundImplementationsByKind = {
maxLength: MaxLength.implementation,
exactLength: ExactLength.implementation,
after: After.implementation,
before: Before.implementation
before: Before.implementation,
minSize: MinSize.implementation,
maxSize: MaxSize.implementation
}

export const boundClassesByKind: Record<BoundKind, typeof BaseConstraint<any>> =
Expand All @@ -54,5 +62,7 @@ export const boundClassesByKind: Record<BoundKind, typeof BaseConstraint<any>> =
maxLength: MaxLength.Node,
exactLength: ExactLength.Node,
after: After.Node,
before: Before.Node
before: Before.Node,
minSize: MinSize.Node,
maxSize: MaxSize.Node
}
93 changes: 93 additions & 0 deletions ark/schema/refinements/maxSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describeCollapsibleSize } from "@ark/util"
import type { BaseRoot } from "../roots/root.ts"
import type { BaseErrorContext, declareNode } from "../shared/declare.ts"
import { Disjoint } from "../shared/disjoint.ts"
import {
implementNode,
type nodeImplementationOf
} from "../shared/implement.ts"
import type { JsonSchema } from "../shared/jsonSchema.ts"
import { $ark } from "../shared/registry.ts"
import type { ToJsonSchema } from "../shared/toJsonSchema.ts"
import type { TraverseAllows } from "../shared/traversal.ts"
import {
BaseRange,
createSizeRuleParser,
parseExclusiveKey,
type BaseRangeInner,
type UnknownExpandedRangeSchema
} from "./range.ts"

export declare namespace MaxSize {
export interface Inner extends BaseRangeInner {
rule: number
exclusive?: true
}

export interface NormalizedSchema extends UnknownExpandedRangeSchema {
rule: number
}

export type Schema = NormalizedSchema | number

export interface ErrorContext extends BaseErrorContext<"maxSize">, Inner {}

export interface Declaration
extends declareNode<{
kind: "maxSize"
schema: Schema
normalizedSchema: NormalizedSchema
inner: Inner
prerequisite: File
errorContext: ErrorContext
}> {}

export type Node = MaxSizeNode
}

const implementation: nodeImplementationOf<MaxSize.Declaration> =
implementNode<MaxSize.Declaration>({
kind: "maxSize",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: { parse: createSizeRuleParser("maxSize") },
exclusive: parseExclusiveKey
},
normalize: schema =>
typeof schema === "number" ? { rule: schema } : schema,
defaults: {
description: node => {
if (node.rule === 0)
return node.exclusive ? "negative size" : "non-positive size"
const limit = describeCollapsibleSize(node.rule)
return `${node.exclusive ? "less than" : "at most"} ${limit}`
},
actual: data => `${data.size} bytes`
},
intersections: {
maxSize: (l, r) => (l.isStricterThan(r) ? l : r),
minSize: (max, min) =>
max.overlapsRange(min) ? null : Disjoint.init("range", max, min)
}
})

export class MaxSizeNode extends BaseRange<MaxSize.Declaration> {
readonly impliedBasis: BaseRoot = $ark.intrinsic.File.internal

readonly expression: string = `${this.comparator} ${describeCollapsibleSize(this.rule)}`

traverseAllows: TraverseAllows<File> =
this.exclusive ?
data => data.size < this.rule
: data => data.size <= this.rule

reduceJsonSchema(base: JsonSchema, ctx: ToJsonSchema.Context): JsonSchema {
return ctx.fallback.size({ code: "size", base, maxSize: this.rule })
}
}

export const MaxSize = {
implementation,
Node: MaxSizeNode
}
97 changes: 97 additions & 0 deletions ark/schema/refinements/minSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describeCollapsibleSize } from "@ark/util"
import type { IntersectionNode } from "../roots/intersection.ts"
import type { BaseRoot } from "../roots/root.ts"
import type { BaseErrorContext, declareNode } from "../shared/declare.ts"
import {
implementNode,
type nodeImplementationOf
} from "../shared/implement.ts"
import type { JsonSchema } from "../shared/jsonSchema.ts"
import { $ark } from "../shared/registry.ts"
import type { ToJsonSchema } from "../shared/toJsonSchema.ts"
import type { TraverseAllows } from "../shared/traversal.ts"
import {
BaseRange,
createSizeRuleParser,
parseExclusiveKey,
type BaseRangeInner,
type UnknownExpandedRangeSchema
} from "./range.ts"

export declare namespace MinSize {
export interface Inner extends BaseRangeInner {
rule: number
exclusive?: true
}

export interface NormalizedSchema extends UnknownExpandedRangeSchema {
rule: number
}

export type Schema = NormalizedSchema | number

export interface ErrorContext extends BaseErrorContext<"minSize">, Inner {}

export interface Declaration
extends declareNode<{
kind: "minSize"
schema: Schema
normalizedSchema: NormalizedSchema
inner: Inner
prerequisite: File
reducibleTo: "intersection"
errorContext: ErrorContext
}> {}

export type Node = MinSizeNode
}

const implementation: nodeImplementationOf<MinSize.Declaration> =
implementNode<MinSize.Declaration>({
kind: "minSize",
collapsibleKey: "rule",
hasAssociatedError: true,
keys: {
rule: { parse: createSizeRuleParser("minSize") },
exclusive: parseExclusiveKey
},
reduce: inner =>
// a non-exclusive minimum size of zero is trivially satisfied
inner.rule === 0 && !inner.exclusive ?
($ark.intrinsic.unknown as IntersectionNode)
: undefined,
normalize: schema =>
typeof schema === "number" ? { rule: schema } : schema,
defaults: {
description: node => {
if (node.rule === 0)
return node.exclusive ? "positive size" : "non-negative size"
const limit = describeCollapsibleSize(node.rule)
return `${node.exclusive ? "more than" : "at least"} ${limit}`
},
actual: data => `${data.size} bytes`
},
intersections: {
minSize: (l, r) => (l.isStricterThan(r) ? l : r)
}
})

export class MinSizeNode extends BaseRange<MinSize.Declaration> {
readonly impliedBasis: BaseRoot = $ark.intrinsic.File.internal

readonly expression: string = `${this.comparator} ${describeCollapsibleSize(this.rule)}`

traverseAllows: TraverseAllows<File> =
this.exclusive ?
data => data.size > this.rule
: data => data.size >= this.rule

reduceJsonSchema(base: JsonSchema, ctx: ToJsonSchema.Context): JsonSchema {
return ctx.fallback.size({ code: "size", base, minSize: this.rule })
}
}

export const MinSize = {
implementation,
Node: MinSizeNode
}
Loading
Loading