kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: basic e2e tests working for api gateway; added json-schema validation and type coercion
rodzic
1215e02bd3
commit
e4520c69a4
|
@ -30,7 +30,11 @@ export const consumerTokenParamsSchema = z.object({
|
|||
})
|
||||
|
||||
export const populateConsumerSchema = z.object({
|
||||
populate: z.array(consumerRelationsSchema).default([]).optional()
|
||||
populate: z
|
||||
.union([consumerRelationsSchema, z.array(consumerRelationsSchema)])
|
||||
.default([])
|
||||
.transform((p) => (Array.isArray(p) ? p : [p]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
export const paginationAndPopulateConsumerSchema = z.object({
|
||||
|
|
|
@ -4,6 +4,7 @@ import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
|||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponses
|
||||
|
@ -52,6 +53,9 @@ export function registerV1DeploymentsListDeployments(
|
|||
|
||||
const userId = c.get('userId')
|
||||
const teamMember = c.get('teamMember')
|
||||
const user = await ensureAuthUser(c)
|
||||
const isAdmin = user.role === 'admin'
|
||||
|
||||
let projectId: string | undefined
|
||||
|
||||
if (projectIdentifier) {
|
||||
|
@ -64,7 +68,9 @@ export function registerV1DeploymentsListDeployments(
|
|||
|
||||
const deployments = await db.query.deployments.findMany({
|
||||
where: and(
|
||||
teamMember
|
||||
isAdmin
|
||||
? undefined
|
||||
: teamMember
|
||||
? eq(schema.deployments.teamId, teamMember.teamId)
|
||||
: eq(schema.deployments.userId, userId),
|
||||
projectId ? eq(schema.deployments.projectId, projectId) : undefined,
|
||||
|
|
|
@ -28,7 +28,11 @@ export const filterDeploymentSchema = z.object({
|
|||
})
|
||||
|
||||
export const populateDeploymentSchema = z.object({
|
||||
populate: z.array(deploymentRelationsSchema).default([]).optional()
|
||||
populate: z
|
||||
.union([deploymentRelationsSchema, z.array(deploymentRelationsSchema)])
|
||||
.default([])
|
||||
.transform((p) => (Array.isArray(p) ? p : [p]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
export const deploymentIdentifierQuerySchema = z.object({
|
||||
|
|
|
@ -22,7 +22,11 @@ export const projectIdentifierQuerySchema = z.object({
|
|||
})
|
||||
|
||||
export const populateProjectSchema = z.object({
|
||||
populate: z.array(projectRelationsSchema).default([]).optional()
|
||||
populate: z
|
||||
.union([projectRelationsSchema, z.array(projectRelationsSchema)])
|
||||
.default([])
|
||||
.transform((p) => (Array.isArray(p) ? p : [p]))
|
||||
.optional()
|
||||
})
|
||||
|
||||
export const projectIdentifierAndPopulateSchema = z.object({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier, validators } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier, validators } from '@agentic/platform-validators'
|
||||
import { z } from '@hono/zod-openapi'
|
||||
|
||||
import type { consumersRelations } from './schema/consumer'
|
||||
|
@ -45,17 +45,27 @@ export const logEntryIdSchema = getIdSchemaForModelType('logEntry')
|
|||
|
||||
export const projectIdentifierSchema = z
|
||||
.string()
|
||||
.refine((id) => validators.projectIdentifier(id), {
|
||||
.refine(
|
||||
(id) =>
|
||||
validators.projectIdentifier(id) || projectIdSchema.safeParse(id).success,
|
||||
{
|
||||
message: 'Invalid project identifier'
|
||||
})
|
||||
}
|
||||
)
|
||||
.describe('Public project identifier (e.g. "namespace/project-name")')
|
||||
.openapi('ProjectIdentifier')
|
||||
|
||||
export const deploymentIdentifierSchema = z
|
||||
.string()
|
||||
.refine((id) => !!parseFaasIdentifier(id), {
|
||||
.refine(
|
||||
(id) =>
|
||||
!!parseToolIdentifier(id) ||
|
||||
validators.deploymentIdentifier(id) ||
|
||||
deploymentIdSchema.safeParse(id).success,
|
||||
{
|
||||
message: 'Invalid deployment identifier'
|
||||
})
|
||||
}
|
||||
)
|
||||
.describe(
|
||||
'Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}")'
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import {
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
schema
|
||||
} from '@/db'
|
||||
import { setPublicCacheControl } from '@/lib/cache-control'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
|
||||
/**
|
||||
* Attempts to find the Deployment matching the given deployment ID or
|
||||
|
@ -36,7 +35,6 @@ export async function tryGetDeploymentByIdentifier(
|
|||
}
|
||||
): Promise<RawDeployment> {
|
||||
assert(deploymentIdentifier, 400, 'Missing required deployment identifier')
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
// First check if the identifier is a deployment ID
|
||||
if (deploymentIdSchema.safeParse(deploymentIdentifier).success) {
|
||||
|
@ -49,11 +47,7 @@ export async function tryGetDeploymentByIdentifier(
|
|||
return deployment
|
||||
}
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const parsedFaas = parseFaasIdentifier(deploymentIdentifier, {
|
||||
namespace
|
||||
})
|
||||
const parsedFaas = parseToolIdentifier(deploymentIdentifier)
|
||||
assert(
|
||||
parsedFaas,
|
||||
400,
|
||||
|
@ -75,21 +69,18 @@ export async function tryGetDeploymentByIdentifier(
|
|||
return deployment
|
||||
} else if (version) {
|
||||
const project = await db.query.projects.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.projects.identifier, projectIdentifier)
|
||||
})
|
||||
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
||||
|
||||
if (version === 'latest') {
|
||||
assert(
|
||||
project.lastPublishedDeploymentId,
|
||||
404,
|
||||
'Project has no published deployments'
|
||||
)
|
||||
const deploymentId =
|
||||
project.lastPublishedDeploymentId || project.lastDeploymentId
|
||||
assert(deploymentId, 404, 'Project has no published deployments')
|
||||
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
...dbQueryOpts,
|
||||
where: eq(schema.deployments.id, project.lastPublishedDeploymentId)
|
||||
where: eq(schema.deployments.id, deploymentId)
|
||||
})
|
||||
assert(
|
||||
deployment,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import { db, eq, projectIdSchema, type RawProject, schema } from '@/db'
|
||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||
|
||||
/**
|
||||
* Attempts to find the Project matching the given ID or identifier.
|
||||
|
@ -28,7 +27,6 @@ export async function tryGetProjectByIdentifier(
|
|||
}
|
||||
): Promise<RawProject> {
|
||||
assert(projectIdentifier, 400, 'Missing required project identifier')
|
||||
const user = await ensureAuthUser(ctx)
|
||||
|
||||
// First check if the identifier is a project ID
|
||||
if (projectIdSchema.safeParse(projectIdentifier).success) {
|
||||
|
@ -40,11 +38,7 @@ export async function tryGetProjectByIdentifier(
|
|||
return project
|
||||
}
|
||||
|
||||
const teamMember = ctx.get('teamMember')
|
||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
||||
namespace
|
||||
})
|
||||
const parsedFaas = parseToolIdentifier(projectIdentifier)
|
||||
assert(
|
||||
parsedFaas?.projectIdentifier,
|
||||
400,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`0) GET dev/test-basic-openapi/getPost 1`] = `
|
||||
{
|
||||
"body": "quia et suscipit
|
||||
suscipit recusandae consequuntur expedita et cum
|
||||
reprehenderit molestiae ut ut quas totam
|
||||
nostrum rerum est autem sunt rem eveniet architecto",
|
||||
"id": 1,
|
||||
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
|
||||
"userId": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`1) GET dev/test-basic-openapi@b6e21206/getPost?postId=1 1`] = `
|
||||
{
|
||||
"body": "quia et suscipit
|
||||
suscipit recusandae consequuntur expedita et cum
|
||||
reprehenderit molestiae ut ut quas totam
|
||||
nostrum rerum est autem sunt rem eveniet architecto",
|
||||
"id": 1,
|
||||
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
|
||||
"userId": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`2) GET dev/test-basic-openapi@b6e21206/getPost 1`] = `
|
||||
{
|
||||
"body": "quia et suscipit
|
||||
suscipit recusandae consequuntur expedita et cum
|
||||
reprehenderit molestiae ut ut quas totam
|
||||
nostrum rerum est autem sunt rem eveniet architecto",
|
||||
"id": 1,
|
||||
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
|
||||
"userId": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`3) POST dev/test-basic-openapi/getPost 1`] = `
|
||||
{
|
||||
"body": "quia et suscipit
|
||||
suscipit recusandae consequuntur expedita et cum
|
||||
reprehenderit molestiae ut ut quas totam
|
||||
nostrum rerum est autem sunt rem eveniet architecto",
|
||||
"id": 1,
|
||||
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
|
||||
"userId": 1,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`4) POST dev/test-basic-openapi@latest/getPost 1`] = `
|
||||
{
|
||||
"body": "quia et suscipit
|
||||
suscipit recusandae consequuntur expedita et cum
|
||||
reprehenderit molestiae ut ut quas totam
|
||||
nostrum rerum est autem sunt rem eveniet architecto",
|
||||
"id": 1,
|
||||
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
|
||||
"userId": 1,
|
||||
}
|
||||
`;
|
|
@ -22,7 +22,7 @@ for (const [i, fixture] of fixtures.entries()) {
|
|||
body: expectedBody
|
||||
} = fixture.response ?? {}
|
||||
|
||||
test(
|
||||
test.sequential(
|
||||
`${i}) ${method} ${fixture.path}`,
|
||||
{
|
||||
timeout: fixture.timeout ?? 60_000
|
||||
|
@ -45,7 +45,12 @@ for (const [i, fixture] of fixtures.entries()) {
|
|||
let body: any
|
||||
|
||||
if (type.includes('json')) {
|
||||
try {
|
||||
body = await res.json()
|
||||
} catch (err) {
|
||||
console.error('json error', err)
|
||||
throw err
|
||||
}
|
||||
} else if (type.includes('text')) {
|
||||
body = await res.text()
|
||||
} else {
|
||||
|
@ -59,6 +64,12 @@ for (const [i, fixture] of fixtures.entries()) {
|
|||
if (snapshot) {
|
||||
expect(body).toMatchSnapshot()
|
||||
}
|
||||
|
||||
console.log(`${i}) ${method} ${fixture.path}`, {
|
||||
status,
|
||||
body,
|
||||
headers: Object.fromEntries(res.headers.entries())
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,26 +36,10 @@ export const fixtures: E2ETestFixture[] = [
|
|||
}
|
||||
},
|
||||
{
|
||||
path: 'dev/test-basic-openapi@8d1a4900/getPost?postId=1'
|
||||
path: 'dev/test-basic-openapi@b6e21206/getPost?postId=1'
|
||||
},
|
||||
{
|
||||
path: 'test-basic-openapi/getPost',
|
||||
request: {
|
||||
searchParams: {
|
||||
postId: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'test-basic-openapi@8d1a4900/getPost',
|
||||
request: {
|
||||
searchParams: {
|
||||
postId: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dev/test-basic-openapi@8d1a4900/getPost',
|
||||
path: 'dev/test-basic-openapi@b6e21206/getPost',
|
||||
request: {
|
||||
searchParams: {
|
||||
postId: 1
|
||||
|
@ -70,5 +54,14 @@ export const fixtures: E2ETestFixture[] = [
|
|||
postId: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dev/test-basic-openapi@latest/getPost',
|
||||
request: {
|
||||
method: 'POST',
|
||||
json: {
|
||||
postId: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -22,18 +22,19 @@
|
|||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev",
|
||||
"cf-typegen": "wrangler types ./src/worker.d.ts",
|
||||
"cf-clear-cache": "del .wrangler",
|
||||
"clean": "del dist",
|
||||
"test": "run-s test:*",
|
||||
"test:lint": "eslint .",
|
||||
"test:typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentic/json-schema": "workspace:*",
|
||||
"@agentic/platform": "workspace:*",
|
||||
"@agentic/platform-api-client": "workspace:*",
|
||||
"@agentic/platform-core": "workspace:*",
|
||||
"@agentic/platform-types": "workspace:*",
|
||||
"@agentic/platform-validators": "workspace:*",
|
||||
"@cfworker/json-schema": "^4.1.1",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "catalog:",
|
||||
"eventid": "catalog:",
|
||||
|
|
|
@ -18,11 +18,13 @@ export function cfValidateJsonSchemaObject<
|
|||
>({
|
||||
schema,
|
||||
data,
|
||||
errorMessage
|
||||
errorMessage,
|
||||
coerce = true
|
||||
}: {
|
||||
schema: any
|
||||
data: Record<string, unknown>
|
||||
errorMessage?: string
|
||||
coerce?: boolean
|
||||
}): T {
|
||||
// Special-case check for required fields to give better error messages
|
||||
if (Array.isArray(schema.required)) {
|
||||
|
@ -38,8 +40,7 @@ export function cfValidateJsonSchemaObject<
|
|||
}
|
||||
}
|
||||
|
||||
const validator = new Validator(schema)
|
||||
|
||||
const validator = new Validator({ schema, coerce })
|
||||
const result = validator.validate(data)
|
||||
if (result.valid) {
|
||||
return data as T
|
||||
|
|
|
@ -10,6 +10,7 @@ export async function fetchCache(
|
|||
fetchResponse: () => Promise<Response>
|
||||
}
|
||||
): Promise<Response> {
|
||||
console.log('cacheKey', cacheKey?.url)
|
||||
let response: Response | undefined
|
||||
|
||||
if (cacheKey) {
|
||||
|
@ -23,11 +24,11 @@ export async function fetchCache(
|
|||
if (cacheKey) {
|
||||
if (response.headers.has('Cache-Control')) {
|
||||
// Note that cloudflare's `cache` should respect response headers.
|
||||
ctx.waitUntil(
|
||||
ctx.cache.put(cacheKey, response.clone()).catch((err) => {
|
||||
console.warn('cache put error', cacheKey, err)
|
||||
})
|
||||
)
|
||||
// ctx.waitUntil(
|
||||
// ctx.cache.put(cacheKey, response.clone()).catch((err) => {
|
||||
// console.warn('cache put error', cacheKey, err)
|
||||
// })
|
||||
// )
|
||||
}
|
||||
|
||||
response.headers.set('cf-cache-status', 'MISS')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { AdminDeployment } from '@agentic/platform-types'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { Context } from './types'
|
||||
|
||||
|
@ -8,7 +8,7 @@ export async function getAdminDeployment(
|
|||
ctx: Context,
|
||||
identifier: string
|
||||
): Promise<{ deployment: AdminDeployment; toolPath: string }> {
|
||||
const parsedFaas = parseFaasIdentifier(identifier)
|
||||
const parsedFaas = parseToolIdentifier(identifier)
|
||||
assert(parsedFaas, 404, `Invalid deployment identifier "${identifier}"`)
|
||||
|
||||
const deployment = await ctx.client.adminGetDeploymentByIdentifier({
|
||||
|
|
|
@ -90,14 +90,21 @@ export default {
|
|||
assert(originResponse, 500, 'Origin response is required')
|
||||
const res = new Response(originResponse.body, originResponse)
|
||||
|
||||
// Record the time it took for both the origin and gateway proxy to respond
|
||||
// Record the time it took for both the origin and gateway to respond
|
||||
recordTimespans()
|
||||
res.headers.set('x-response-time', `${originTimespan!}ms`)
|
||||
res.headers.set('x-proxy-response-time', `${gatewayTimespan!}ms`)
|
||||
res.headers.set('x-origin-response-time', `${originTimespan!}ms`)
|
||||
res.headers.set('x-response-time', `${gatewayTimespan!}ms`)
|
||||
|
||||
// Reset server to agentic because Cloudflare likes to override things
|
||||
res.headers.set('server', 'agentic')
|
||||
|
||||
res.headers.delete('x-powered-by')
|
||||
res.headers.delete('via')
|
||||
res.headers.delete('nel')
|
||||
res.headers.delete('report-to')
|
||||
res.headers.delete('server-timing')
|
||||
res.headers.delete('reporting-endpoints')
|
||||
|
||||
// const id: DurableObjectId = env.DO_RATE_LIMITER.idFromName('foo')
|
||||
// const stub = env.DO_RATE_LIMITER.get(id)
|
||||
// const greeting = await stub.sayHello('world')
|
||||
|
|
|
@ -7,7 +7,8 @@ export default [
|
|||
ignores: [
|
||||
'**/out/**',
|
||||
'packages/types/src/openapi.d.ts',
|
||||
'apps/gateway/src/worker.d.ts'
|
||||
'apps/gateway/src/worker.d.ts',
|
||||
'packages/json-schema/test/json-schema-test-suite.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -49,7 +49,9 @@ async function main() {
|
|||
log: (...args: any[]) => {
|
||||
if (program.opts().json) {
|
||||
console.log(
|
||||
args.length === 1 ? JSON.stringify(args[0]) : JSON.stringify(args)
|
||||
args.length === 1
|
||||
? JSON.stringify(args[0], null, 2)
|
||||
: JSON.stringify(args, null, 2)
|
||||
)
|
||||
} else {
|
||||
console.log(...args)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Deployment } from '@agentic/platform-types'
|
||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||
import { Command } from 'commander'
|
||||
import { oraPromise } from 'ora'
|
||||
|
||||
|
@ -24,11 +24,7 @@ export function registerListDeploymentsCommand({
|
|||
let label = 'Fetching all projects and deployments'
|
||||
|
||||
if (projectIdentifier) {
|
||||
const auth = AuthStore.getAuth()
|
||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
||||
// TODO: use team slug if available
|
||||
namespace: auth.user.username
|
||||
})
|
||||
const parsedFaas = parseToolIdentifier(projectIdentifier)
|
||||
|
||||
if (!parsedFaas) {
|
||||
throw new Error(`Invalid project identifier "${projectIdentifier}"`)
|
||||
|
|
|
@ -21,7 +21,7 @@ export function registerPublishCommand({ client, program, logger }: Context) {
|
|||
AuthStore.requireAuth()
|
||||
|
||||
if (deploymentIdentifier) {
|
||||
// TODO: parseFaasIdentifier
|
||||
// TODO: parseToolIdentifier
|
||||
}
|
||||
|
||||
const deployment = await oraPromise(
|
||||
|
|
|
@ -25,7 +25,7 @@ export async function resolveDeployment({
|
|||
const namespace = auth.user.username
|
||||
|
||||
// TODO: resolve deploymentIdentifier; config name may include namespace?
|
||||
// TODO: rename parseFaasIdentifier; movingn away from FaaS terminology
|
||||
// TODO: this needs work...
|
||||
|
||||
deploymentIdentifier = `${namespace}/${config.name}@${fuzzyDeploymentIdentifierVersion}`
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
{
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"name": "postId",
|
||||
"in": "path"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
test/json-schema-test-suite.ts
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"name": "@agentic/json-schema",
|
||||
"version": "0.0.1",
|
||||
"description": "A JSON schema validator that will run on Cloudflare workers. Supports drafts 4, 7, 2019-09, and 2020-12.",
|
||||
"keywords": [
|
||||
"json-schema",
|
||||
"jsonschema",
|
||||
"json",
|
||||
"schema",
|
||||
"cloudflare",
|
||||
"worker",
|
||||
"workers",
|
||||
"service-worker"
|
||||
],
|
||||
"authors": [
|
||||
"Jeremy Danyow <jdanyow@gmail.com>",
|
||||
"Travis Fischer <travis@transitivebullsh.it>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git",
|
||||
"directory": "packages/json-schema"
|
||||
},
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "run-s test:*",
|
||||
"test:lint": "eslint .",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test:unit": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"json-schema-test-suite": "git+https://github.com/json-schema-org/JSON-Schema-Test-Suite#76b529f"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
> [!NOTE]
|
||||
> This package is a fork of [@cfworker/json-schema](https://github.com/cfworker/cfworker) which adds [ajv-style coercion](https://ajv.js.org/coercion.html). Coercion can be enabled with a boolean flag.
|
||||
|
||||
# @agentic/json-schema
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A JSON schema validator that will run on Cloudflare workers. Supports drafts 4, 7, 2019-09, and 2020-12.
|
||||
|
||||
This library is validated against the [json-schema-test-suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite), a series of approximately 4,500 assertions maintained along with the json-schema specification. A small set of test cases are intentionally not supported due to performance constraints or lack of feature use. These list of unsupported features are maintained in [test/unsupported.ts](./test/unsupported.ts). While this library is not the fastest due to lack of code generation, it's consistently among the [most spec compliant](https://json-schema.org/implementations.html#benchmarks).
|
||||
|
||||
## Background
|
||||
|
||||
_Why another JSON schema validator?_
|
||||
|
||||
Cloudflare workers do not have APIs required by [Ajv](https://ajv.js.org/) schema compilation (`eval` or `new Function(code)`).
|
||||
If possible use Ajv in a build step to precompile your schema. Otherwise this library could work for you.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```js
|
||||
import { Validator } from '@cfworker/json-schema'
|
||||
|
||||
const validator = new Validator({ type: 'number' })
|
||||
|
||||
const result = validator.validate(7)
|
||||
```
|
||||
|
||||
## Specify meta schema draft
|
||||
|
||||
```js
|
||||
const validator = new Validator({ type: 'number' }, '4') // draft-4
|
||||
```
|
||||
|
||||
## Add schemas
|
||||
|
||||
```js
|
||||
const validator = new Validator({
|
||||
$id: 'https://foo.bar/baz',
|
||||
$ref: '/beep'
|
||||
})
|
||||
|
||||
validator.addSchema({ $id: 'https://foo.bar/beep', type: 'boolean' })
|
||||
```
|
||||
|
||||
## Include all errors
|
||||
|
||||
By default the validator stops processing after the first error. Set the `shortCircuit` parameter to `false` to emit all errors.
|
||||
|
||||
```js
|
||||
const shortCircuit = false;
|
||||
|
||||
const draft = '2019-09';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
required: ['name', 'email', 'number', 'bool'],
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string', format: 'email' },
|
||||
number: { type: 'number' },
|
||||
bool: { type: 'boolean' }
|
||||
}
|
||||
};
|
||||
|
||||
const validator = new Validator(schema, draft, shortCircuit);
|
||||
|
||||
const result = validator.validate({
|
||||
name: 'hello',
|
||||
email: 5, // invalid type
|
||||
number: 'Hello' // invalid type
|
||||
bool: 'false' // invalid type
|
||||
});
|
||||
```
|
|
@ -0,0 +1,195 @@
|
|||
import type { InstanceType } from './types'
|
||||
|
||||
export function getInstanceType(
|
||||
instance: any
|
||||
): Exclude<InstanceType, 'integer'> {
|
||||
const rawInstanceType = typeof instance
|
||||
switch (rawInstanceType) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return rawInstanceType
|
||||
case 'object':
|
||||
if (instance === null) {
|
||||
return 'null'
|
||||
} else if (Array.isArray(instance)) {
|
||||
return 'array'
|
||||
} else {
|
||||
return 'object'
|
||||
}
|
||||
default:
|
||||
// undefined, bigint, function, symbol
|
||||
throw new Error(
|
||||
`Instances of "${rawInstanceType}" type are not supported.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function coerceValue({
|
||||
instanceType,
|
||||
instance,
|
||||
$type,
|
||||
recur = true
|
||||
}: {
|
||||
instanceType: Exclude<InstanceType, 'integer'>
|
||||
instance: any
|
||||
$type: InstanceType
|
||||
recur?: boolean
|
||||
}): any | undefined {
|
||||
if ($type === undefined) {
|
||||
return instance
|
||||
}
|
||||
|
||||
if (Number.isNaN(instance)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let valid = true
|
||||
|
||||
if ($type === 'integer') {
|
||||
if (instanceType !== 'number' || instance % 1 || Number.isNaN(instance)) {
|
||||
valid = false
|
||||
}
|
||||
} else if (instanceType !== $type) {
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
return instance
|
||||
}
|
||||
|
||||
if (!recur) {
|
||||
return
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'number':
|
||||
switch (instanceType) {
|
||||
case 'string':
|
||||
instance = +instance
|
||||
break
|
||||
|
||||
case 'boolean':
|
||||
instance = instance === true ? 1 : 0
|
||||
break
|
||||
|
||||
case 'null':
|
||||
instance = 0
|
||||
break
|
||||
|
||||
case 'array':
|
||||
if (instance.length === 1) {
|
||||
instance = instance[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'string':
|
||||
switch (instanceType) {
|
||||
case 'boolean':
|
||||
instance = instance === true ? 'true' : 'false'
|
||||
break
|
||||
|
||||
case 'number':
|
||||
instance = String(instance)
|
||||
break
|
||||
|
||||
case 'null':
|
||||
instance = ''
|
||||
break
|
||||
|
||||
case 'array':
|
||||
if (instance.length === 1) {
|
||||
instance = instance[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'boolean':
|
||||
switch (instanceType) {
|
||||
case 'string':
|
||||
if (instance === 'true') {
|
||||
instance = true
|
||||
} else if (instance === 'false') {
|
||||
instance = false
|
||||
}
|
||||
break
|
||||
|
||||
case 'number':
|
||||
if (instance === 1) {
|
||||
instance = true
|
||||
} else if (instance === 0) {
|
||||
instance = false
|
||||
}
|
||||
break
|
||||
|
||||
case 'null':
|
||||
instance = false
|
||||
break
|
||||
|
||||
case 'array':
|
||||
if (instance.length === 1) {
|
||||
instance = instance[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'null':
|
||||
switch (instanceType) {
|
||||
case 'string':
|
||||
if (instance === '') {
|
||||
instance = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'number':
|
||||
if (instance === 0) {
|
||||
instance = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'boolean':
|
||||
if (instance === false) {
|
||||
instance = null
|
||||
}
|
||||
break
|
||||
|
||||
case 'array':
|
||||
if (instance.length === 1 && instance[0] === null) {
|
||||
instance = null
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case 'array':
|
||||
switch (instanceType) {
|
||||
case 'string':
|
||||
instance = [instance]
|
||||
break
|
||||
|
||||
case 'number':
|
||||
instance = [instance]
|
||||
break
|
||||
|
||||
case 'boolean':
|
||||
instance = [instance]
|
||||
break
|
||||
|
||||
case 'null':
|
||||
instance = [null]
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return coerceValue({
|
||||
instanceType: getInstanceType(instance),
|
||||
instance,
|
||||
$type,
|
||||
recur: false
|
||||
})
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
export function deepCompareStrict(a: any, b: any): boolean {
|
||||
const typeofa = typeof a
|
||||
if (typeofa !== typeof b) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b)) {
|
||||
return false
|
||||
}
|
||||
const length = a.length
|
||||
if (length !== b.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (!deepCompareStrict(a[i], b[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (typeofa === 'object') {
|
||||
if (!a || !b) {
|
||||
return a === b
|
||||
}
|
||||
const aKeys = Object.keys(a)
|
||||
const bKeys = Object.keys(b)
|
||||
const length = aKeys.length
|
||||
if (length !== bKeys.length) {
|
||||
return false
|
||||
}
|
||||
for (const k of aKeys) {
|
||||
if (!deepCompareStrict(a[k], b[k])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return a === b
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/* eslint-disable unicorn/no-thenable */
|
||||
import type { Schema } from './types'
|
||||
import { encodePointer } from './pointer'
|
||||
|
||||
export const schemaKeyword: Record<string, boolean> = {
|
||||
additionalItems: true,
|
||||
unevaluatedItems: true,
|
||||
items: true,
|
||||
contains: true,
|
||||
additionalProperties: true,
|
||||
unevaluatedProperties: true,
|
||||
propertyNames: true,
|
||||
not: true,
|
||||
if: true,
|
||||
then: true,
|
||||
else: true
|
||||
}
|
||||
|
||||
export const schemaArrayKeyword: Record<string, boolean> = {
|
||||
prefixItems: true,
|
||||
items: true,
|
||||
allOf: true,
|
||||
anyOf: true,
|
||||
oneOf: true
|
||||
}
|
||||
|
||||
export const schemaMapKeyword: Record<string, boolean> = {
|
||||
$defs: true,
|
||||
definitions: true,
|
||||
properties: true,
|
||||
patternProperties: true,
|
||||
dependentSchemas: true
|
||||
}
|
||||
|
||||
export const ignoredKeyword: Record<string, boolean> = {
|
||||
id: true,
|
||||
$id: true,
|
||||
$ref: true,
|
||||
$schema: true,
|
||||
$anchor: true,
|
||||
$vocabulary: true,
|
||||
$comment: true,
|
||||
default: true,
|
||||
enum: true,
|
||||
const: true,
|
||||
required: true,
|
||||
type: true,
|
||||
maximum: true,
|
||||
minimum: true,
|
||||
exclusiveMaximum: true,
|
||||
exclusiveMinimum: true,
|
||||
multipleOf: true,
|
||||
maxLength: true,
|
||||
minLength: true,
|
||||
pattern: true,
|
||||
format: true,
|
||||
maxItems: true,
|
||||
minItems: true,
|
||||
uniqueItems: true,
|
||||
maxProperties: true,
|
||||
minProperties: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Default base URI for schemas without an $id.
|
||||
* https://json-schema.org/draft/2019-09/json-schema-core.html#initial-base
|
||||
* https://tools.ietf.org/html/rfc3986#section-5.1
|
||||
*/
|
||||
export const initialBaseURI: URL =
|
||||
globalThis.self !== undefined &&
|
||||
self.location &&
|
||||
self.location.origin !== 'null'
|
||||
? new URL(self.location.origin + self.location.pathname + location.search)
|
||||
: new URL('https://github.com/cfworker')
|
||||
|
||||
export function dereference(
|
||||
schema: Schema | boolean,
|
||||
lookup: Record<string, Schema | boolean> = Object.create(null),
|
||||
baseURI: URL = initialBaseURI,
|
||||
basePointer = ''
|
||||
): Record<string, Schema | boolean> {
|
||||
if (schema && typeof schema === 'object' && !Array.isArray(schema)) {
|
||||
const id: string = schema.$id || schema.id
|
||||
if (id) {
|
||||
const url = new URL(id, baseURI.href)
|
||||
if (url.hash.length > 1) {
|
||||
lookup[url.href] = schema
|
||||
} else {
|
||||
url.hash = '' // normalize hash https://url.spec.whatwg.org/#dom-url-hash
|
||||
if (basePointer === '') {
|
||||
baseURI = url
|
||||
} else {
|
||||
dereference(schema, lookup, baseURI)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (schema !== true && schema !== false) {
|
||||
return lookup
|
||||
}
|
||||
|
||||
// compute the schema's URI and add it to the mapping.
|
||||
const schemaURI = baseURI.href + (basePointer ? '#' + basePointer : '')
|
||||
if (lookup[schemaURI] !== undefined) {
|
||||
throw new Error(`Duplicate schema URI "${schemaURI}".`)
|
||||
}
|
||||
lookup[schemaURI] = schema
|
||||
|
||||
// exit early if this is a boolean schema.
|
||||
if (schema === true || schema === false) {
|
||||
return lookup
|
||||
}
|
||||
|
||||
// set the schema's absolute URI.
|
||||
if (schema.__absolute_uri__ === undefined) {
|
||||
Object.defineProperty(schema, '__absolute_uri__', {
|
||||
enumerable: false,
|
||||
value: schemaURI
|
||||
})
|
||||
}
|
||||
|
||||
// if a $ref is found, resolve it's absolute URI.
|
||||
if (schema.$ref && schema.__absolute_ref__ === undefined) {
|
||||
const url = new URL(schema.$ref, baseURI.href)
|
||||
// eslint-disable-next-line no-self-assign
|
||||
url.hash = url.hash // normalize hash https://url.spec.whatwg.org/#dom-url-hash
|
||||
Object.defineProperty(schema, '__absolute_ref__', {
|
||||
enumerable: false,
|
||||
value: url.href
|
||||
})
|
||||
}
|
||||
|
||||
// if a $recursiveRef is found, resolve it's absolute URI.
|
||||
if (schema.$recursiveRef && schema.__absolute_recursive_ref__ === undefined) {
|
||||
const url = new URL(schema.$recursiveRef, baseURI.href)
|
||||
// eslint-disable-next-line no-self-assign
|
||||
url.hash = url.hash // normalize hash https://url.spec.whatwg.org/#dom-url-hash
|
||||
Object.defineProperty(schema, '__absolute_recursive_ref__', {
|
||||
enumerable: false,
|
||||
value: url.href
|
||||
})
|
||||
}
|
||||
|
||||
// if an $anchor is found, compute it's URI and add it to the mapping.
|
||||
if (schema.$anchor) {
|
||||
const url = new URL('#' + schema.$anchor, baseURI.href)
|
||||
lookup[url.href] = schema
|
||||
}
|
||||
|
||||
// process subschemas.
|
||||
for (const key in schema) {
|
||||
if (ignoredKeyword[key]) {
|
||||
continue
|
||||
}
|
||||
const keyBase = `${basePointer}/${encodePointer(key)}`
|
||||
const subSchema = schema[key]
|
||||
if (Array.isArray(subSchema)) {
|
||||
if (schemaArrayKeyword[key]) {
|
||||
const length = subSchema.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
dereference(subSchema[i], lookup, baseURI, `${keyBase}/${i}`)
|
||||
}
|
||||
}
|
||||
} else if (schemaMapKeyword[key]) {
|
||||
for (const subKey in subSchema) {
|
||||
dereference(
|
||||
subSchema[subKey],
|
||||
lookup,
|
||||
baseURI,
|
||||
`${keyBase}/${encodePointer(subKey)}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
dereference(subSchema, lookup, baseURI, keyBase)
|
||||
}
|
||||
}
|
||||
|
||||
return lookup
|
||||
}
|
||||
|
||||
// schema identification examples
|
||||
// https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.A
|
||||
// $ref delegation
|
||||
// https://github.com/json-schema-org/json-schema-spec/issues/514
|
||||
// output format
|
||||
// https://json-schema.org/draft/2019-09/json-schema-core.html#output
|
||||
// JSON pointer
|
||||
// https://tools.ietf.org/html/rfc6901
|
||||
// JSON relative pointer
|
||||
// https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01
|
|
@ -0,0 +1,168 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable no-control-regex */
|
||||
/* eslint-disable security/detect-unsafe-regex */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// based on https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js
|
||||
|
||||
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
|
||||
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i
|
||||
const HOSTNAME =
|
||||
/^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i
|
||||
// const URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
||||
const URIREF =
|
||||
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
|
||||
// uri-template: https://tools.ietf.org/html/rfc6570
|
||||
const URITEMPLATE =
|
||||
/^(?:(?:[^\u0000-\u0020"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i
|
||||
// For the source: https://gist.github.com/dperini/729294
|
||||
// For test cases: https://mathiasbynens.be/demo/url-regex
|
||||
const URL_ =
|
||||
/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00A1}-\u{FFFF}0-9]+-?)*[a-z\u{00A1}-\u{FFFF}0-9]+)(?:\.(?:[a-z\u{00A1}-\u{FFFF}0-9]+-?)*[a-z\u{00A1}-\u{FFFF}0-9]+)*(?:\.(?:[a-z\u{00A1}-\u{FFFF}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu
|
||||
const UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i
|
||||
const JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/
|
||||
const JSON_POINTER_URI_FRAGMENT =
|
||||
/^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i
|
||||
const RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/
|
||||
|
||||
// // date: http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
// const FASTDATE = /^\d\d\d\d-[0-1]\d-[0-3]\d$/;
|
||||
// // date-time: http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
// const FASTTIME =
|
||||
// /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i;
|
||||
// const FASTDATETIME =
|
||||
// /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i;
|
||||
// // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
|
||||
// // const FASTURI = /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i;
|
||||
// const FASTURIREFERENCE =
|
||||
// /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i;
|
||||
|
||||
// https://github.com/ExodusMovement/schemasafe/blob/master/src/formats.js
|
||||
const EMAIL = (input: string) => {
|
||||
if (input[0] === '"') return false
|
||||
const [name, host, ...rest] = input.split('@')
|
||||
if (
|
||||
!name ||
|
||||
!host ||
|
||||
rest.length !== 0 ||
|
||||
name.length > 64 ||
|
||||
host.length > 253
|
||||
)
|
||||
return false
|
||||
if (name[0] === '.' || name.endsWith('.') || name.includes('..')) return false
|
||||
if (
|
||||
!/^[a-z0-9.-]+$/i.test(host) ||
|
||||
!/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/i.test(name)
|
||||
)
|
||||
return false
|
||||
return host
|
||||
.split('.')
|
||||
.every((part) => /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(part))
|
||||
}
|
||||
|
||||
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
|
||||
const IPV4 =
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/
|
||||
// optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
|
||||
const IPV6 =
|
||||
/^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i
|
||||
|
||||
// https://github.com/ExodusMovement/schemasafe/blob/master/src/formats.js
|
||||
const DURATION = (input: string) =>
|
||||
input.length > 1 &&
|
||||
input.length < 80 &&
|
||||
(/^P\d+([.,]\d+)?W$/.test(input) ||
|
||||
(/^P[\dYMDTHS]*(\d[.,]\d+)?[YMDHS]$/.test(input) &&
|
||||
/^P([.,\d]+Y)?([.,\d]+M)?([.,\d]+D)?(T([.,\d]+H)?([.,\d]+M)?([.,\d]+S)?)?$/.test(
|
||||
input
|
||||
)))
|
||||
|
||||
function bind(r: RegExp) {
|
||||
return r.test.bind(r)
|
||||
}
|
||||
|
||||
export const format: Record<string, (s: string) => boolean> = {
|
||||
date,
|
||||
time: time.bind(undefined, false),
|
||||
'date-time': date_time,
|
||||
duration: DURATION,
|
||||
uri,
|
||||
'uri-reference': bind(URIREF),
|
||||
'uri-template': bind(URITEMPLATE),
|
||||
url: bind(URL_),
|
||||
email: EMAIL,
|
||||
hostname: bind(HOSTNAME),
|
||||
ipv4: bind(IPV4),
|
||||
ipv6: bind(IPV6),
|
||||
regex,
|
||||
uuid: bind(UUID),
|
||||
'json-pointer': bind(JSON_POINTER),
|
||||
'json-pointer-uri-fragment': bind(JSON_POINTER_URI_FRAGMENT),
|
||||
'relative-json-pointer': bind(RELATIVE_JSON_POINTER)
|
||||
}
|
||||
|
||||
function isLeapYear(year: number) {
|
||||
// https://tools.ietf.org/html/rfc3339#appendix-C
|
||||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
||||
}
|
||||
|
||||
function date(str: string) {
|
||||
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
const matches = str.match(DATE)
|
||||
if (!matches) return false
|
||||
|
||||
const year = +matches[1]
|
||||
const month = +matches[2]
|
||||
const day = +matches[3]
|
||||
|
||||
return (
|
||||
month >= 1 &&
|
||||
month <= 12 &&
|
||||
day >= 1 &&
|
||||
day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month])
|
||||
)
|
||||
}
|
||||
|
||||
function time(full: boolean, str: string) {
|
||||
const matches = str.match(TIME)
|
||||
if (!matches) return false
|
||||
|
||||
const hour = +matches[1]
|
||||
const minute = +matches[2]
|
||||
const second = +matches[3]
|
||||
const timeZone = !!matches[5]
|
||||
return (
|
||||
((hour <= 23 && minute <= 59 && second <= 59) ||
|
||||
(hour === 23 && minute === 59 && second === 60)) &&
|
||||
(!full || timeZone)
|
||||
)
|
||||
}
|
||||
|
||||
const DATE_TIME_SEPARATOR = /t|\s/i
|
||||
function date_time(str: string) {
|
||||
// http://tools.ietf.org/html/rfc3339#section-5.6
|
||||
const dateTime = str.split(DATE_TIME_SEPARATOR)
|
||||
return dateTime.length === 2 && date(dateTime[0]) && time(true, dateTime[1])
|
||||
}
|
||||
|
||||
const NOT_URI_FRAGMENT = /\/|:/
|
||||
const URI_PATTERN =
|
||||
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
|
||||
|
||||
function uri(str: string): boolean {
|
||||
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
|
||||
return NOT_URI_FRAGMENT.test(str) && URI_PATTERN.test(str)
|
||||
}
|
||||
|
||||
const Z_ANCHOR = /[^\\]\\Z/
|
||||
function regex(str: string) {
|
||||
if (Z_ANCHOR.test(str)) return false
|
||||
try {
|
||||
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||
new RegExp(str, 'u')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export * from './coercion'
|
||||
export * from './deep-compare-strict'
|
||||
export * from './dereference'
|
||||
export * from './format'
|
||||
export * from './pointer'
|
||||
export * from './types'
|
||||
export * from './ucs2-length'
|
||||
export * from './validate'
|
||||
export * from './validator'
|
|
@ -0,0 +1,7 @@
|
|||
export function encodePointer(p: string): string {
|
||||
return encodeURI(escapePointer(p))
|
||||
}
|
||||
|
||||
export function escapePointer(p: string): string {
|
||||
return p.replaceAll('~', '~0').replaceAll('/', '~1')
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
export type SchemaDraft = '4' | '7' | '2019-09' | '2020-12'
|
||||
|
||||
export const enum OutputFormat {
|
||||
Flag = 1,
|
||||
Basic = 2,
|
||||
Detailed = 4
|
||||
}
|
||||
|
||||
export type InstanceType =
|
||||
| 'array'
|
||||
| 'boolean'
|
||||
| 'integer'
|
||||
| 'null'
|
||||
| 'number'
|
||||
| 'object'
|
||||
| 'string'
|
||||
|
||||
export interface Schema {
|
||||
$id?: string
|
||||
$anchor?: string
|
||||
$recursiveAnchor?: boolean
|
||||
$ref?: string
|
||||
$recursiveRef?: '#'
|
||||
$schema?: string
|
||||
$comment?: string
|
||||
$defs?: any
|
||||
$vocabulary?: Record<string, boolean>
|
||||
|
||||
type?: InstanceType | InstanceType[]
|
||||
const?: any
|
||||
enum?: any[]
|
||||
required?: string[]
|
||||
not?: Schema
|
||||
anyOf?: Schema[]
|
||||
allOf?: Schema[]
|
||||
oneOf?: Schema[]
|
||||
if?: Schema
|
||||
then?: Schema
|
||||
else?: Schema
|
||||
|
||||
format?: string
|
||||
|
||||
properties?: Record<string | number, Schema | boolean>
|
||||
patternProperties?: Record<string, Schema | boolean>
|
||||
additionalProperties?: Schema | boolean
|
||||
unevaluatedProperties?: Schema | boolean
|
||||
minProperties?: number
|
||||
maxProperties?: number
|
||||
propertyNames?: Schema
|
||||
dependentRequired?: Record<string, string[]>
|
||||
dependentSchemas?: Record<string, Schema>
|
||||
dependencies?: Record<string, Schema | string[]>
|
||||
|
||||
prefixItems?: Array<Schema | boolean>
|
||||
items?: Schema | boolean | Array<Schema | boolean>
|
||||
additionalItems?: Schema | boolean
|
||||
unevaluatedItems?: Schema | boolean
|
||||
contains?: Schema | boolean
|
||||
minContains?: number
|
||||
maxContains?: number
|
||||
minItems?: number
|
||||
maxItems?: number
|
||||
uniqueItems?: boolean
|
||||
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
exclusiveMinimum?: number | boolean
|
||||
exclusiveMaximum?: number | boolean
|
||||
multipleOf?: number
|
||||
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
pattern?: string
|
||||
|
||||
__absolute_ref__?: string
|
||||
__absolute_recursive_ref__?: string
|
||||
__absolute_uri__?: string
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface OutputUnit {
|
||||
keyword: string
|
||||
keywordLocation: string
|
||||
instanceLocation: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export type ValidationResult = {
|
||||
valid: boolean
|
||||
errors: OutputUnit[]
|
||||
instance: any
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable eqeqeq */
|
||||
/* eslint-disable unicorn/prefer-code-point */
|
||||
|
||||
/**
|
||||
* Get UCS-2 length of a string
|
||||
* https://mathiasbynens.be/notes/javascript-encoding
|
||||
* https://github.com/bestiejs/punycode.js - punycode.ucs2.decode
|
||||
*/
|
||||
export function ucs2length(s: string): number {
|
||||
const length = s.length
|
||||
let result = 0
|
||||
let index = 0
|
||||
let charCode: number
|
||||
while (index < length) {
|
||||
result++
|
||||
charCode = s.charCodeAt(index++)
|
||||
if (charCode >= 0xd8_00 && charCode <= 0xdb_ff && index < length) {
|
||||
// high surrogate, and there is a next character
|
||||
charCode = s.charCodeAt(index)
|
||||
if ((charCode & 0xfc_00) == 0xdc_00) {
|
||||
// low surrogate
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,47 @@
|
|||
import type { Schema, SchemaDraft, ValidationResult } from './types'
|
||||
import { dereference } from './dereference'
|
||||
import { validate } from './validate'
|
||||
|
||||
export class Validator {
|
||||
private readonly lookup: ReturnType<typeof dereference>
|
||||
private readonly schema: Schema | boolean
|
||||
private readonly draft: SchemaDraft
|
||||
private readonly shortCircuit
|
||||
private readonly coerce
|
||||
|
||||
constructor({
|
||||
schema,
|
||||
draft = '2019-09',
|
||||
shortCircuit = true,
|
||||
coerce = false
|
||||
}: {
|
||||
schema: Schema | boolean
|
||||
draft?: SchemaDraft
|
||||
shortCircuit?: boolean
|
||||
coerce?: boolean
|
||||
}) {
|
||||
this.schema = schema
|
||||
this.draft = draft
|
||||
this.shortCircuit = shortCircuit
|
||||
this.coerce = coerce
|
||||
this.lookup = dereference(schema)
|
||||
}
|
||||
|
||||
public validate(instance: any): ValidationResult {
|
||||
return validate(
|
||||
instance,
|
||||
this.schema,
|
||||
this.draft,
|
||||
this.lookup,
|
||||
this.shortCircuit,
|
||||
this.coerce
|
||||
)
|
||||
}
|
||||
|
||||
public addSchema(schema: Schema, id?: string): void {
|
||||
if (id) {
|
||||
schema = { ...schema, $id: id }
|
||||
}
|
||||
dereference(schema, this.lookup)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { validate } from '../src/index'
|
||||
|
||||
describe('json-schema coercion', () => {
|
||||
it('string => number coercion', () => {
|
||||
const result = validate('7', { type: 'number' }, '2019-09', undefined, true)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(7)
|
||||
})
|
||||
|
||||
it('boolean => number coercion', () => {
|
||||
const result = validate(
|
||||
true,
|
||||
{ type: 'number' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(1)
|
||||
})
|
||||
|
||||
it('null => number coercion', () => {
|
||||
const result = validate(
|
||||
null,
|
||||
{ type: 'number' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(0)
|
||||
})
|
||||
|
||||
it('array => number coercion', () => {
|
||||
const result = validate([1], { type: 'number' }, '2019-09', undefined, true)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(1)
|
||||
})
|
||||
|
||||
it('boolean => string coercion', () => {
|
||||
const result = validate(
|
||||
true,
|
||||
{ type: 'string' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal('true')
|
||||
})
|
||||
|
||||
it('number => string coercion', () => {
|
||||
const result = validate(
|
||||
72.3,
|
||||
{ type: 'string' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal('72.3')
|
||||
})
|
||||
|
||||
it('null => string coercion', () => {
|
||||
const result = validate(
|
||||
null,
|
||||
{ type: 'string' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal('')
|
||||
})
|
||||
|
||||
it('array => string coercion', () => {
|
||||
const result = validate(
|
||||
['nala'],
|
||||
{ type: 'string' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal('nala')
|
||||
})
|
||||
|
||||
it('string => boolean coercion', () => {
|
||||
const result = validate(
|
||||
'true',
|
||||
{ type: 'boolean' },
|
||||
'2019-09',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(true)
|
||||
})
|
||||
|
||||
it('string => null coercion', () => {
|
||||
const result = validate('', { type: 'null' }, '2019-09', undefined, true)
|
||||
|
||||
expect(result.valid).to.equal(true)
|
||||
expect(result.instance).to.equal(null)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,80 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { dereference, validate, type ValidationResult } from '../src/index.js'
|
||||
import { remotes, suites } from './json-schema-test-suite.js'
|
||||
import { loadMeta } from './meta-schema.js'
|
||||
import { unsupportedTests } from './unsupported.js'
|
||||
|
||||
const remotesLookup = Object.create(null)
|
||||
for (const { name, schema } of remotes) {
|
||||
dereference(schema, remotesLookup, new URL(name))
|
||||
}
|
||||
Object.freeze(remotesLookup)
|
||||
;(globalThis as any).location = {
|
||||
origin: 'https://example.com',
|
||||
host: 'example.com',
|
||||
hostname: 'example.com',
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: ''
|
||||
}
|
||||
|
||||
describe('json-schema', () => {
|
||||
const failures: Record<string, Record<string, Record<string, true>>> = {}
|
||||
for (const { draft, name, tests: tests0 } of suites) {
|
||||
if (name.endsWith('/unknownKeyword')) {
|
||||
continue
|
||||
}
|
||||
|
||||
describe(name, () => {
|
||||
for (const { schema, description: description1, tests } of tests0) {
|
||||
const schemaLookup = dereference(schema)
|
||||
|
||||
const supportedTests = tests.filter((test) => {
|
||||
return !unsupportedTests[name]?.[description1]?.[test.description]
|
||||
})
|
||||
if (!supportedTests.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
describe(description1, () => {
|
||||
for (const {
|
||||
data,
|
||||
valid,
|
||||
description: description2,
|
||||
debug
|
||||
} of tests) {
|
||||
if (unsupportedTests[name]?.[description1]?.[description2]) {
|
||||
continue
|
||||
}
|
||||
;(debug ? it.only : it)(description2, async () => {
|
||||
if (debug) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger
|
||||
}
|
||||
const metaLookup = await loadMeta()
|
||||
const lookup = {
|
||||
...metaLookup,
|
||||
...remotesLookup,
|
||||
...schemaLookup
|
||||
}
|
||||
let result: ValidationResult | undefined
|
||||
try {
|
||||
result = validate(data, schema, draft, lookup)
|
||||
} catch {}
|
||||
if (result?.valid !== valid) {
|
||||
failures[name] = failures[name] ?? {}
|
||||
failures[name][description1] =
|
||||
failures[name][description1] ?? {}
|
||||
failures[name][description1][description2] = true
|
||||
}
|
||||
expect(result?.valid).to.equal(valid, description2)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// after(() => console.log(JSON.stringify(failures, null, 2)));
|
||||
})
|
|
@ -0,0 +1,41 @@
|
|||
import { dereference, type Schema } from '../src/index'
|
||||
|
||||
let lookup: Record<string, Schema> | undefined
|
||||
|
||||
export async function loadMeta() {
|
||||
if (lookup) {
|
||||
return lookup
|
||||
}
|
||||
lookup = Object.create({})
|
||||
const ids = [
|
||||
'http://json-schema.org/draft-04/schema',
|
||||
'http://json-schema.org/draft-07/schema',
|
||||
'https://json-schema.org/draft/2019-09/schema',
|
||||
'https://json-schema.org/draft/2019-09/meta/core',
|
||||
'https://json-schema.org/draft/2019-09/meta/applicator',
|
||||
'https://json-schema.org/draft/2019-09/meta/validation',
|
||||
'https://json-schema.org/draft/2019-09/meta/meta-data',
|
||||
'https://json-schema.org/draft/2019-09/meta/format',
|
||||
'https://json-schema.org/draft/2019-09/meta/content',
|
||||
'https://json-schema.org/draft/2020-12/schema',
|
||||
'https://json-schema.org/draft/2020-12/meta/core',
|
||||
'https://json-schema.org/draft/2020-12/meta/applicator',
|
||||
'https://json-schema.org/draft/2020-12/meta/validation',
|
||||
'https://json-schema.org/draft/2020-12/meta/meta-data',
|
||||
'https://json-schema.org/draft/2020-12/meta/format-annotation',
|
||||
'https://json-schema.org/draft/2020-12/meta/content',
|
||||
'https://json-schema.org/draft/2020-12/meta/unevaluated'
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const response = await fetch(id)
|
||||
const schema = await response.json()
|
||||
dereference(schema, lookup)
|
||||
})
|
||||
)
|
||||
|
||||
Object.freeze(lookup)
|
||||
|
||||
return lookup
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import type { Schema, SchemaDraft } from '../src/types'
|
||||
|
||||
export interface SchemaTestSuite {
|
||||
draft: SchemaDraft
|
||||
name: string
|
||||
tests: SchemaTest[]
|
||||
}
|
||||
|
||||
export interface SchemaTest {
|
||||
description: string
|
||||
schema: any
|
||||
tests: SchemaTestCase[]
|
||||
}
|
||||
|
||||
export interface SchemaTestCase {
|
||||
description: string
|
||||
data: any
|
||||
valid: boolean
|
||||
debug?: true
|
||||
}
|
||||
|
||||
export interface Remote {
|
||||
name: string
|
||||
schema: Schema
|
||||
}
|
|
@ -0,0 +1,593 @@
|
|||
export const unsupportedTests: Record<
|
||||
string,
|
||||
Record<string, Record<string, boolean>>
|
||||
> = {
|
||||
'draft2019-09/format': {
|
||||
'email format': {
|
||||
'invalid email string is only an annotation by default': true
|
||||
},
|
||||
'regex format': {
|
||||
'invalid regex string is only an annotation by default': true
|
||||
},
|
||||
'ipv4 format': {
|
||||
'invalid ipv4 string is only an annotation by default': true
|
||||
},
|
||||
'ipv6 format': {
|
||||
'invalid ipv6 string is only an annotation by default': true
|
||||
},
|
||||
'hostname format': {
|
||||
'invalid hostname string is only an annotation by default': true
|
||||
},
|
||||
'date format': {
|
||||
'invalid date string is only an annotation by default': true
|
||||
},
|
||||
'date-time format': {
|
||||
'invalid date-time string is only an annotation by default': true
|
||||
},
|
||||
'time format': {
|
||||
'invalid time string is only an annotation by default': true
|
||||
},
|
||||
'json-pointer format': {
|
||||
'invalid json-pointer string is only an annotation by default': true
|
||||
},
|
||||
'relative-json-pointer format': {
|
||||
'invalid relative-json-pointer string is only an annotation by default':
|
||||
true
|
||||
},
|
||||
'uri format': {
|
||||
'invalid uri string is only an annotation by default': true
|
||||
},
|
||||
'uri-reference format': {
|
||||
'invalid uri-reference string is only an annotation by default': true
|
||||
},
|
||||
'uri-template format': {
|
||||
'invalid uri-template string is only an annotation by default': true
|
||||
},
|
||||
'uuid format': {
|
||||
'invalid uuid string is only an annotation by default': true
|
||||
},
|
||||
'duration format': {
|
||||
'invalid duration string is only an annotation by default': true
|
||||
}
|
||||
},
|
||||
'draft4/type': {
|
||||
'multiple types can be specified in an array': {
|
||||
'an integer is valid': true
|
||||
}
|
||||
},
|
||||
'draft7/type': {
|
||||
'multiple types can be specified in an array': {
|
||||
'an integer is valid': true
|
||||
},
|
||||
'not multiple types': {
|
||||
mismatch: true
|
||||
}
|
||||
},
|
||||
'draft2019-09/type': {
|
||||
'multiple types can be specified in an array': {
|
||||
'an integer is valid': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/type': {
|
||||
'multiple types can be specified in an array': {
|
||||
'an integer is valid': true
|
||||
}
|
||||
},
|
||||
'draft4/not': {
|
||||
'not multiple types': {
|
||||
mismatch: true
|
||||
}
|
||||
},
|
||||
'draft7/not': {
|
||||
'not multiple types': {
|
||||
mismatch: true
|
||||
}
|
||||
},
|
||||
'draft2019-09/not': {
|
||||
'not multiple types': {
|
||||
mismatch: true
|
||||
}
|
||||
},
|
||||
'draft2020-12/not': {
|
||||
'not multiple types': {
|
||||
mismatch: true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/date': {
|
||||
'validation of date strings': {
|
||||
'a invalid date string with 32 days in January': true,
|
||||
'a invalid date string with 29 days in February (normal)': true,
|
||||
'a invalid date string with 30 days in February (leap)': true,
|
||||
'a invalid date string with 32 days in March': true,
|
||||
'a invalid date string with 31 days in April': true,
|
||||
'a invalid date string with 32 days in May': true,
|
||||
'a invalid date string with 31 days in June': true,
|
||||
'a invalid date string with 32 days in July': true,
|
||||
'a invalid date string with 32 days in August': true,
|
||||
'a invalid date string with 31 days in September': true,
|
||||
'a invalid date string with 32 days in October': true,
|
||||
'a invalid date string with 31 days in November': true,
|
||||
'a invalid date string with 32 days in December': true,
|
||||
'a invalid date string with invalid month': true,
|
||||
'invalid month': true,
|
||||
'invalid month-day combination': true,
|
||||
'2021 is not a leap year': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/idn-email': {
|
||||
'validation of an internationalized e-mail addresses': {
|
||||
'an invalid idn e-mail address': true,
|
||||
'an invalid e-mail address': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/idn-hostname': {
|
||||
'validation of internationalized host names': {
|
||||
'illegal first char U+302E Hangul single dot tone mark': true,
|
||||
'contains illegal char U+302E Hangul single dot tone mark': true,
|
||||
'a host name with a component too long': true,
|
||||
'invalid label, correct Punycode': true,
|
||||
'invalid Punycode': true,
|
||||
'U-label contains "--" in the 3rd and 4th position': true,
|
||||
'U-label starts with a dash': true,
|
||||
'U-label ends with a dash': true,
|
||||
'U-label starts and ends with a dash': true,
|
||||
'Begins with a Spacing Combining Mark': true,
|
||||
'Begins with a Nonspacing Mark': true,
|
||||
'Begins with an Enclosing Mark': true,
|
||||
'Exceptions that are DISALLOWED, right-to-left chars': true,
|
||||
'Exceptions that are DISALLOWED, left-to-right chars': true,
|
||||
"MIDDLE DOT with no preceding 'l'": true,
|
||||
'MIDDLE DOT with nothing preceding': true,
|
||||
"MIDDLE DOT with no following 'l'": true,
|
||||
'MIDDLE DOT with nothing following': true,
|
||||
'Greek KERAIA not followed by Greek': true,
|
||||
'Greek KERAIA not followed by anything': true,
|
||||
'Hebrew GERESH not preceded by Hebrew': true,
|
||||
'Hebrew GERESH not preceded by anything': true,
|
||||
'Hebrew GERSHAYIM not preceded by Hebrew': true,
|
||||
'Hebrew GERSHAYIM not preceded by anything': true,
|
||||
'KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han': true,
|
||||
'KATAKANA MIDDLE DOT with no other characters': true,
|
||||
'Arabic-Indic digits mixed with Extended Arabic-Indic digits': true,
|
||||
'ZERO WIDTH JOINER not preceded by Virama': true,
|
||||
'ZERO WIDTH JOINER not preceded by anything': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/ipv4': {
|
||||
'validation of IP addresses': {
|
||||
'leading zeroes should be rejected, as they are treated as octals': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/iri-reference': {
|
||||
'validation of IRI References': {
|
||||
'an invalid IRI Reference': true,
|
||||
'an invalid IRI fragment': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/iri': {
|
||||
'validation of IRIs': {
|
||||
'an invalid IRI based on IPv6': true,
|
||||
'an invalid relative IRI Reference': true,
|
||||
'an invalid IRI': true,
|
||||
'an invalid IRI though valid IRI reference': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/format/time': {
|
||||
'validation of time strings': {
|
||||
'valid leap second, positive time-offset': true,
|
||||
'valid leap second, large positive time-offset': true,
|
||||
'invalid leap second, positive time-offset (wrong hour)': true,
|
||||
'invalid leap second, positive time-offset (wrong minute)': true,
|
||||
'valid leap second, negative time-offset': true,
|
||||
'valid leap second, large negative time-offset': true,
|
||||
'invalid leap second, negative time-offset (wrong hour)': true,
|
||||
'invalid leap second, negative time-offset (wrong minute)': true,
|
||||
'an invalid time string with invalid hour': true,
|
||||
'an invalid time string with invalid time numoffset hour': true,
|
||||
'an invalid time string with invalid time numoffset minute': true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/non-bmp-regex': {
|
||||
'Proper UTF-16 surrogate pair handling: pattern': {
|
||||
'matches empty': true,
|
||||
'matches two': true
|
||||
},
|
||||
'Proper UTF-16 surrogate pair handling: patternProperties': {
|
||||
"doesn't match two": true
|
||||
}
|
||||
},
|
||||
'draft2019-09/optional/unicode': {
|
||||
'unicode semantics should be used for all pattern matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
},
|
||||
'unicode digits are more than 0 through 9': {
|
||||
'non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)': true
|
||||
},
|
||||
'unicode semantics should be used for all patternProperties matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/defs': {
|
||||
'validate definition against metaschema': {
|
||||
'invalid definition schema': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/dynamicRef': {
|
||||
'A $dynamicRef to a $dynamicAnchor in the same schema resource should behave like a normal $ref to an $anchor':
|
||||
{
|
||||
'An array containing non-strings is invalid': true
|
||||
},
|
||||
'A $dynamicRef to an $anchor in the same schema resource should behave like a normal $ref to an $anchor':
|
||||
{
|
||||
'An array containing non-strings is invalid': true
|
||||
},
|
||||
'A $ref to a $dynamicAnchor in the same schema resource should behave like a normal $ref to an $anchor':
|
||||
{
|
||||
'An array of strings is valid': true,
|
||||
'An array containing non-strings is invalid': true
|
||||
},
|
||||
'A $dynamicRef should resolve to the first $dynamicAnchor still in scope that is encountered when the schema is evaluated':
|
||||
{
|
||||
'An array containing non-strings is invalid': true
|
||||
},
|
||||
"A $dynamicRef with intermediate scopes that don't include a matching $dynamicAnchor should not affect dynamic scope resolution":
|
||||
{
|
||||
'An array containing non-strings is invalid': true
|
||||
},
|
||||
'A $dynamicRef that initially resolves to a schema with a matching $dynamicAnchor should resolve to the first $dynamicAnchor in the dynamic scope':
|
||||
{
|
||||
'The recursive part is not valid against the root': true
|
||||
},
|
||||
'multiple dynamic paths to the $dynamicRef keyword': {
|
||||
'recurse to integerNode - floats are not allowed': true
|
||||
},
|
||||
'after leaving a dynamic scope, it should not be used by a $dynamicRef': {
|
||||
'string matches /$defs/thingy, but the $dynamicRef does not stop here':
|
||||
true,
|
||||
'first_scope is not in dynamic scope for the $dynamicRef': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/format': {
|
||||
'email format': {
|
||||
'invalid email string is only an annotation by default': true
|
||||
},
|
||||
'regex format': {
|
||||
'invalid regex string is only an annotation by default': true
|
||||
},
|
||||
'ipv4 format': {
|
||||
'invalid ipv4 string is only an annotation by default': true
|
||||
},
|
||||
'ipv6 format': {
|
||||
'invalid ipv6 string is only an annotation by default': true
|
||||
},
|
||||
'hostname format': {
|
||||
'invalid hostname string is only an annotation by default': true
|
||||
},
|
||||
'date format': {
|
||||
'invalid date string is only an annotation by default': true
|
||||
},
|
||||
'date-time format': {
|
||||
'invalid date-time string is only an annotation by default': true
|
||||
},
|
||||
'time format': {
|
||||
'invalid time string is only an annotation by default': true
|
||||
},
|
||||
'json-pointer format': {
|
||||
'invalid json-pointer string is only an annotation by default': true
|
||||
},
|
||||
'relative-json-pointer format': {
|
||||
'invalid relative-json-pointer string is only an annotation by default':
|
||||
true
|
||||
},
|
||||
'uri format': {
|
||||
'invalid uri string is only an annotation by default': true
|
||||
},
|
||||
'uri-reference format': {
|
||||
'invalid uri-reference string is only an annotation by default': true
|
||||
},
|
||||
'uri-template format': {
|
||||
'invalid uri-template string is only an annotation by default': true
|
||||
},
|
||||
'uuid format': {
|
||||
'invalid uuid string is only an annotation by default': true
|
||||
},
|
||||
'duration format': {
|
||||
'invalid duration string is only an annotation by default': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/id': {
|
||||
'Invalid use of fragments in location-independent $id': {
|
||||
'Identifier name': true,
|
||||
'Identifier name and no ref': true,
|
||||
'Identifier path': true,
|
||||
'Identifier name with absolute URI': true,
|
||||
'Identifier path with absolute URI': true,
|
||||
'Identifier name with base URI change in subschema': true,
|
||||
'Identifier path with base URI change in subschema': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/date': {
|
||||
'validation of date strings': {
|
||||
'a invalid date string with 32 days in January': true,
|
||||
'a invalid date string with 29 days in February (normal)': true,
|
||||
'a invalid date string with 30 days in February (leap)': true,
|
||||
'a invalid date string with 32 days in March': true,
|
||||
'a invalid date string with 31 days in April': true,
|
||||
'a invalid date string with 32 days in May': true,
|
||||
'a invalid date string with 31 days in June': true,
|
||||
'a invalid date string with 32 days in July': true,
|
||||
'a invalid date string with 32 days in August': true,
|
||||
'a invalid date string with 31 days in September': true,
|
||||
'a invalid date string with 32 days in October': true,
|
||||
'a invalid date string with 31 days in November': true,
|
||||
'a invalid date string with 32 days in December': true,
|
||||
'a invalid date string with invalid month': true,
|
||||
'invalid month': true,
|
||||
'invalid month-day combination': true,
|
||||
'2021 is not a leap year': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/idn-email': {
|
||||
'validation of an internationalized e-mail addresses': {
|
||||
'an invalid idn e-mail address': true,
|
||||
'an invalid e-mail address': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/idn-hostname': {
|
||||
'validation of internationalized host names': {
|
||||
'illegal first char U+302E Hangul single dot tone mark': true,
|
||||
'contains illegal char U+302E Hangul single dot tone mark': true,
|
||||
'a host name with a component too long': true,
|
||||
'invalid label, correct Punycode': true,
|
||||
'invalid Punycode': true,
|
||||
'U-label contains "--" in the 3rd and 4th position': true,
|
||||
'U-label starts with a dash': true,
|
||||
'U-label ends with a dash': true,
|
||||
'U-label starts and ends with a dash': true,
|
||||
'Begins with a Spacing Combining Mark': true,
|
||||
'Begins with a Nonspacing Mark': true,
|
||||
'Begins with an Enclosing Mark': true,
|
||||
'Exceptions that are DISALLOWED, right-to-left chars': true,
|
||||
'Exceptions that are DISALLOWED, left-to-right chars': true,
|
||||
"MIDDLE DOT with no preceding 'l'": true,
|
||||
'MIDDLE DOT with nothing preceding': true,
|
||||
"MIDDLE DOT with no following 'l'": true,
|
||||
'MIDDLE DOT with nothing following': true,
|
||||
'Greek KERAIA not followed by Greek': true,
|
||||
'Greek KERAIA not followed by anything': true,
|
||||
'Hebrew GERESH not preceded by Hebrew': true,
|
||||
'Hebrew GERESH not preceded by anything': true,
|
||||
'Hebrew GERSHAYIM not preceded by Hebrew': true,
|
||||
'Hebrew GERSHAYIM not preceded by anything': true,
|
||||
'KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han': true,
|
||||
'KATAKANA MIDDLE DOT with no other characters': true,
|
||||
'Arabic-Indic digits mixed with Extended Arabic-Indic digits': true,
|
||||
'ZERO WIDTH JOINER not preceded by Virama': true,
|
||||
'ZERO WIDTH JOINER not preceded by anything': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/ipv4': {
|
||||
'validation of IP addresses': {
|
||||
'leading zeroes should be rejected, as they are treated as octals': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/iri-reference': {
|
||||
'validation of IRI References': {
|
||||
'an invalid IRI Reference': true,
|
||||
'an invalid IRI fragment': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/iri': {
|
||||
'validation of IRIs': {
|
||||
'an invalid IRI based on IPv6': true,
|
||||
'an invalid relative IRI Reference': true,
|
||||
'an invalid IRI': true,
|
||||
'an invalid IRI though valid IRI reference': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/format/time': {
|
||||
'validation of time strings': {
|
||||
'valid leap second, positive time-offset': true,
|
||||
'valid leap second, large positive time-offset': true,
|
||||
'invalid leap second, positive time-offset (wrong hour)': true,
|
||||
'invalid leap second, positive time-offset (wrong minute)': true,
|
||||
'valid leap second, negative time-offset': true,
|
||||
'valid leap second, large negative time-offset': true,
|
||||
'invalid leap second, negative time-offset (wrong hour)': true,
|
||||
'invalid leap second, negative time-offset (wrong minute)': true,
|
||||
'an invalid time string with invalid hour': true,
|
||||
'an invalid time string with invalid time numoffset hour': true,
|
||||
'an invalid time string with invalid time numoffset minute': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/non-bmp-regex': {
|
||||
'Proper UTF-16 surrogate pair handling: pattern': {
|
||||
'matches empty': true,
|
||||
'matches two': true
|
||||
},
|
||||
'Proper UTF-16 surrogate pair handling: patternProperties': {
|
||||
"doesn't match two": true
|
||||
}
|
||||
},
|
||||
'draft2020-12/optional/unicode': {
|
||||
'unicode semantics should be used for all pattern matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
},
|
||||
'unicode digits are more than 0 through 9': {
|
||||
'non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)': true
|
||||
},
|
||||
'unicode semantics should be used for all patternProperties matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
}
|
||||
},
|
||||
'draft2020-12/ref': {
|
||||
'relative pointer ref to array': {
|
||||
'mismatch array': true
|
||||
}
|
||||
},
|
||||
'draft4/optional/format/ipv4': {
|
||||
'validation of IP addresses': {
|
||||
'leading zeroes should be rejected, as they are treated as octals': true
|
||||
}
|
||||
},
|
||||
'draft4/optional/non-bmp-regex': {
|
||||
'Proper UTF-16 surrogate pair handling: pattern': {
|
||||
'matches empty': true,
|
||||
'matches two': true
|
||||
},
|
||||
'Proper UTF-16 surrogate pair handling: patternProperties': {
|
||||
"doesn't match two": true
|
||||
}
|
||||
},
|
||||
'draft4/optional/unicode': {
|
||||
'unicode semantics should be used for all pattern matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
},
|
||||
'unicode digits are more than 0 through 9': {
|
||||
'non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)': true
|
||||
},
|
||||
'unicode semantics should be used for all patternProperties matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
}
|
||||
},
|
||||
'draft4/optional/zeroTerminatedFloats': {
|
||||
'some languages do not distinguish between different types of numeric value':
|
||||
{
|
||||
'a float is not an integer even without fractional part': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/content': {
|
||||
'validation of string-encoded content based on media type': {
|
||||
'an invalid JSON document': true
|
||||
},
|
||||
'validation of binary string-encoding': {
|
||||
'an invalid base64 string (% is not a valid character)': true
|
||||
},
|
||||
'validation of binary-encoded media type documents': {
|
||||
'a validly-encoded invalid JSON document': true,
|
||||
'an invalid base64 string that is valid JSON': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/date': {
|
||||
'validation of date strings': {
|
||||
'a invalid date string with 32 days in January': true,
|
||||
'a invalid date string with 29 days in February (normal)': true,
|
||||
'a invalid date string with 30 days in February (leap)': true,
|
||||
'a invalid date string with 32 days in March': true,
|
||||
'a invalid date string with 31 days in April': true,
|
||||
'a invalid date string with 32 days in May': true,
|
||||
'a invalid date string with 31 days in June': true,
|
||||
'a invalid date string with 32 days in July': true,
|
||||
'a invalid date string with 32 days in August': true,
|
||||
'a invalid date string with 31 days in September': true,
|
||||
'a invalid date string with 32 days in October': true,
|
||||
'a invalid date string with 31 days in November': true,
|
||||
'a invalid date string with 32 days in December': true,
|
||||
'a invalid date string with invalid month': true,
|
||||
'invalid month': true,
|
||||
'invalid month-day combination': true,
|
||||
'2021 is not a leap year': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/idn-email': {
|
||||
'validation of an internationalized e-mail addresses': {
|
||||
'an invalid idn e-mail address': true,
|
||||
'an invalid e-mail address': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/idn-hostname': {
|
||||
'validation of internationalized host names': {
|
||||
'illegal first char U+302E Hangul single dot tone mark': true,
|
||||
'contains illegal char U+302E Hangul single dot tone mark': true,
|
||||
'a host name with a component too long': true,
|
||||
'invalid label, correct Punycode': true,
|
||||
'invalid Punycode': true,
|
||||
'U-label contains "--" in the 3rd and 4th position': true,
|
||||
'U-label starts with a dash': true,
|
||||
'U-label ends with a dash': true,
|
||||
'U-label starts and ends with a dash': true,
|
||||
'Begins with a Spacing Combining Mark': true,
|
||||
'Begins with a Nonspacing Mark': true,
|
||||
'Begins with an Enclosing Mark': true,
|
||||
'Exceptions that are DISALLOWED, right-to-left chars': true,
|
||||
'Exceptions that are DISALLOWED, left-to-right chars': true,
|
||||
"MIDDLE DOT with no preceding 'l'": true,
|
||||
'MIDDLE DOT with nothing preceding': true,
|
||||
"MIDDLE DOT with no following 'l'": true,
|
||||
'MIDDLE DOT with nothing following': true,
|
||||
'Greek KERAIA not followed by Greek': true,
|
||||
'Greek KERAIA not followed by anything': true,
|
||||
'Hebrew GERESH not preceded by Hebrew': true,
|
||||
'Hebrew GERESH not preceded by anything': true,
|
||||
'Hebrew GERSHAYIM not preceded by Hebrew': true,
|
||||
'Hebrew GERSHAYIM not preceded by anything': true,
|
||||
'KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han': true,
|
||||
'KATAKANA MIDDLE DOT with no other characters': true,
|
||||
'Arabic-Indic digits mixed with Extended Arabic-Indic digits': true,
|
||||
'ZERO WIDTH JOINER not preceded by Virama': true,
|
||||
'ZERO WIDTH JOINER not preceded by anything': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/ipv4': {
|
||||
'validation of IP addresses': {
|
||||
'leading zeroes should be rejected, as they are treated as octals': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/iri-reference': {
|
||||
'validation of IRI References': {
|
||||
'an invalid IRI Reference': true,
|
||||
'an invalid IRI fragment': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/iri': {
|
||||
'validation of IRIs': {
|
||||
'an invalid IRI based on IPv6': true,
|
||||
'an invalid relative IRI Reference': true,
|
||||
'an invalid IRI': true,
|
||||
'an invalid IRI though valid IRI reference': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/format/time': {
|
||||
'validation of time strings': {
|
||||
'valid leap second, positive time-offset': true,
|
||||
'valid leap second, large positive time-offset': true,
|
||||
'invalid leap second, positive time-offset (wrong hour)': true,
|
||||
'invalid leap second, positive time-offset (wrong minute)': true,
|
||||
'valid leap second, negative time-offset': true,
|
||||
'valid leap second, large negative time-offset': true,
|
||||
'invalid leap second, negative time-offset (wrong hour)': true,
|
||||
'invalid leap second, negative time-offset (wrong minute)': true,
|
||||
'an invalid time string with invalid hour': true,
|
||||
'an invalid time string with invalid time numoffset hour': true,
|
||||
'an invalid time string with invalid time numoffset minute': true
|
||||
}
|
||||
},
|
||||
'draft7/optional/non-bmp-regex': {
|
||||
'Proper UTF-16 surrogate pair handling: pattern': {
|
||||
'matches empty': true,
|
||||
'matches two': true
|
||||
},
|
||||
'Proper UTF-16 surrogate pair handling: patternProperties': {
|
||||
"doesn't match two": true
|
||||
}
|
||||
},
|
||||
'draft7/optional/unicode': {
|
||||
'unicode semantics should be used for all pattern matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
},
|
||||
'unicode digits are more than 0 through 9': {
|
||||
'non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)': true
|
||||
},
|
||||
'unicode semantics should be used for all patternProperties matching': {
|
||||
'literal unicode character in json string': true,
|
||||
'unicode character in hex format in string': true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { Validator } from '../src/validator'
|
||||
|
||||
describe('Validator', () => {
|
||||
it('validates', () => {
|
||||
const validator = new Validator({ schema: { type: 'number' } })
|
||||
|
||||
expect(validator.validate(7).valid).to.equal(true)
|
||||
expect(validator.validate('hello world').valid).to.equal(false)
|
||||
})
|
||||
|
||||
it('adds schema', () => {
|
||||
const validator = new Validator({
|
||||
schema: {
|
||||
$id: 'https://foo.bar/baz',
|
||||
$ref: '/beep'
|
||||
}
|
||||
})
|
||||
|
||||
validator.addSchema({ $id: 'https://foo.bar/beep', type: 'boolean' })
|
||||
|
||||
expect(validator.validate(true).valid).to.equal(true)
|
||||
expect(validator.validate('hello world').valid).to.equal(false)
|
||||
})
|
||||
|
||||
it('adds schema with specified id', () => {
|
||||
const validator = new Validator({
|
||||
schema: {
|
||||
$id: 'https://foo.bar/baz',
|
||||
$ref: '/beep'
|
||||
}
|
||||
})
|
||||
|
||||
validator.addSchema({ type: 'boolean' }, 'https://foo.bar/beep')
|
||||
|
||||
expect(validator.validate(true).valid).to.equal(true)
|
||||
expect(validator.validate('hello world').valid).to.equal(false)
|
||||
})
|
||||
|
||||
it('validate all array entries with nested errors', () => {
|
||||
const validator = new Validator({
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
required: ['name', 'email']
|
||||
}
|
||||
},
|
||||
draft: '2019-09',
|
||||
shortCircuit: false
|
||||
})
|
||||
|
||||
const res = validator.validate([
|
||||
{
|
||||
name: 'hello'
|
||||
//missing email
|
||||
},
|
||||
{
|
||||
//missing name
|
||||
email: 'a@b.c'
|
||||
}
|
||||
])
|
||||
expect(res.valid).to.equal(false)
|
||||
expect(res.errors.length).to.equal(4)
|
||||
})
|
||||
|
||||
it('validate all object properties with nested errors', () => {
|
||||
const validator = new Validator({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
number: { type: 'number' },
|
||||
required: ['name', 'email', 'number']
|
||||
}
|
||||
},
|
||||
draft: '2019-09',
|
||||
shortCircuit: false
|
||||
})
|
||||
|
||||
const res = validator.validate({
|
||||
name: 'hello',
|
||||
email: 5, //invalid type
|
||||
number: 'Hello' //invalid type
|
||||
})
|
||||
expect(res.valid).to.equal(false)
|
||||
expect(res.errors.length).to.equal(4)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@fisch0920/config/tsconfig-node",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "WebWorker", "Webworker.Iterable"]
|
||||
},
|
||||
"include": ["src", "test", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export * from './parse-faas-identifier'
|
||||
export * from './parse-faas-uri'
|
||||
export * from './parse-tool-identifier'
|
||||
export * from './parse-tool-uri'
|
||||
export type * from './types'
|
||||
export * as validators from './validators'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { ParsedFaasIdentifier } from './types'
|
||||
import { parseFaasUri } from './parse-faas-uri'
|
||||
import type { ParsedToolIdentifier } from './types'
|
||||
import { parseToolUri } from './parse-faas-uri'
|
||||
|
||||
export function parseFaasIdentifier(
|
||||
identifier: string,
|
||||
{ namespace }: { namespace?: string } = {}
|
||||
): ParsedFaasIdentifier | undefined {
|
||||
export function parseToolIdentifier(
|
||||
identifier: string
|
||||
// { namespace }: { namespace?: string } = {}
|
||||
): ParsedToolIdentifier | undefined {
|
||||
if (!identifier) {
|
||||
return
|
||||
}
|
||||
|
@ -22,17 +22,17 @@ export function parseFaasIdentifier(
|
|||
return
|
||||
}
|
||||
|
||||
const hasNamespacePrefix = /^([a-zA-Z0-9-]{1,64}\/)/.test(uri)
|
||||
// const hasNamespacePrefix = /^([a-zA-Z0-9-]{1,64}\/)/.test(uri)
|
||||
|
||||
if (!hasNamespacePrefix) {
|
||||
if (namespace) {
|
||||
// add inferred namespace prefix (defaults to authenticated user's username)
|
||||
uri = `${namespace}/${uri}`
|
||||
} else {
|
||||
// throw new Error(`FaaS identifier is missing namespace prefix or you must be authenticated [${uri}]`)
|
||||
return
|
||||
}
|
||||
}
|
||||
// if (!hasNamespacePrefix) {
|
||||
// if (namespace) {
|
||||
// // add inferred namespace prefix (defaults to authenticated user's username)
|
||||
// uri = `${namespace}/${uri}`
|
||||
// } else {
|
||||
// // throw new Error(`FaaS identifier is missing namespace prefix or you must be authenticated [${uri}]`)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
return parseFaasUri(uri)
|
||||
return parseToolUri(uri)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
// TODO: investigate this
|
||||
/* eslint-disable security/detect-unsafe-regex */
|
||||
|
||||
import type { ParsedFaasIdentifier } from './types'
|
||||
import type { ParsedToolIdentifier } from './types'
|
||||
|
||||
// namespace/project-name@deploymentHash/toolPath
|
||||
// project@deploymentHash/toolPath
|
||||
const projectDeploymentToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name@version/toolPath
|
||||
// project@version/toolPath
|
||||
const projectVersionToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name/toolPath
|
||||
// project/toolPath (latest version)
|
||||
// namespace/project-name/toolPath (latest version)
|
||||
const projectToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
export function parseFaasUri(uri: string): ParsedFaasIdentifier | undefined {
|
||||
export function parseToolUri(uri: string): ParsedToolIdentifier | undefined {
|
||||
const pdtMatch = uri.match(projectDeploymentToolRe)
|
||||
|
||||
if (pdtMatch) {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { expect, test } from 'vitest'
|
||||
|
||||
import { parseFaasIdentifier } from './parse-faas-identifier'
|
||||
import { parseToolIdentifier } from './parse-tool-identifier'
|
||||
import * as validators from './validators'
|
||||
|
||||
function success(...args: Parameters<typeof parseFaasIdentifier>) {
|
||||
const result = parseFaasIdentifier(...args)
|
||||
function success(...args: Parameters<typeof parseToolIdentifier>) {
|
||||
const result = parseToolIdentifier(...args)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result!.projectIdentifier).toBeTruthy()
|
||||
expect(result!.version || result!.deploymentHash).toBeTruthy()
|
||||
|
@ -21,8 +21,8 @@ function success(...args: Parameters<typeof parseFaasIdentifier>) {
|
|||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
|
||||
function error(...args: Parameters<typeof parseFaasIdentifier>) {
|
||||
const result = parseFaasIdentifier(...args)
|
||||
function error(...args: Parameters<typeof parseToolIdentifier>) {
|
||||
const result = parseToolIdentifier(...args)
|
||||
expect(result).toBeUndefined()
|
||||
}
|
||||
|
||||
|
@ -61,19 +61,15 @@ test('URL prefix and suffix success', () => {
|
|||
})
|
||||
|
||||
test('namespace success', () => {
|
||||
success('https://api.saasify.sh/foo-bar@01234567/foo', {
|
||||
namespace: 'username'
|
||||
})
|
||||
success('/foo-bar@01234567/foo', { namespace: 'username' })
|
||||
success('/foo-bar@01234567/foo', { namespace: 'username' })
|
||||
success('/foo-bar@01234567/foo/', { namespace: 'username' })
|
||||
success('https://api.saasify.sh/foo-bar@01234567/foo/bar/123', {
|
||||
namespace: 'username'
|
||||
})
|
||||
success('/foo-bar@01234567/foo/bar/123', { namespace: 'username' })
|
||||
success('/foo-bar@latest/foo/bar/123', { namespace: 'username' })
|
||||
success('/foo-bar@dev/foo/bar/123', { namespace: 'username' })
|
||||
success('/foo-bar@1.2.3/foo/bar/123', { namespace: 'username' })
|
||||
success('https://api.saasify.sh/username/foo-bar@01234567/foo')
|
||||
success('/username/foo-bar@01234567/foo')
|
||||
success('/username/foo-bar@01234567/foo')
|
||||
success('/username/foo-bar@01234567/foo/')
|
||||
success('username/https://api.saasify.sh/foo-bar@01234567/foo/bar/123')
|
||||
success('/username/foo-bar@01234567/foo/bar/123')
|
||||
success('/username/foo-bar@latest/foo/bar/123')
|
||||
success('/username/foo-bar@dev/foo/bar/123')
|
||||
success('/username/foo-bar@1.2.3/foo/bar/123')
|
||||
})
|
||||
|
||||
test('namespace error', () => {
|
||||
|
@ -82,6 +78,7 @@ test('namespace error', () => {
|
|||
error('/foo-bar@01234567/foo')
|
||||
error('/foo-bar@dev/foo')
|
||||
error('/foo-bar@01234567/foo')
|
||||
error('foo-bar/tool')
|
||||
error('/foo-bar@01234567/foo/')
|
||||
error('/foo-bar@01234567/foo/bar/123')
|
||||
error('/foo-bar@0latest/foo/bar/123')
|
|
@ -0,0 +1,38 @@
|
|||
import type { ParsedToolIdentifier } from './types'
|
||||
import { parseToolUri } from './parse-tool-uri'
|
||||
|
||||
export function parseToolIdentifier(
|
||||
identifier: string
|
||||
// { namespace }: { namespace?: string } = {}
|
||||
): ParsedToolIdentifier | undefined {
|
||||
if (!identifier) {
|
||||
return
|
||||
}
|
||||
|
||||
let uri = identifier
|
||||
try {
|
||||
const { pathname } = new URL(identifier)
|
||||
uri = pathname
|
||||
} catch {}
|
||||
|
||||
uri = uri.replaceAll(/^\//g, '')
|
||||
uri = uri.replaceAll(/\/$/g, '')
|
||||
|
||||
if (!uri.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// const hasNamespacePrefix = /^([a-zA-Z0-9-]{1,64}\/)/.test(uri)
|
||||
|
||||
// if (!hasNamespacePrefix) {
|
||||
// if (namespace) {
|
||||
// // add inferred namespace prefix (defaults to authenticated user's username)
|
||||
// uri = `${namespace}/${uri}`
|
||||
// } else {
|
||||
// // throw new Error(`FaaS identifier is missing namespace prefix or you must be authenticated [${uri}]`)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
return parseToolUri(uri)
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { expect, test } from 'vitest'
|
||||
|
||||
import { parseFaasUri } from './parse-faas-uri'
|
||||
import { parseToolUri } from './parse-tool-uri'
|
||||
|
||||
function success(value: string) {
|
||||
const result = parseFaasUri(value)
|
||||
const result = parseToolUri(value)
|
||||
expect(result).toBeTruthy()
|
||||
expect(result?.projectIdentifier).toBeTruthy()
|
||||
expect(result?.version || result?.deploymentHash).toBeTruthy()
|
||||
|
@ -11,7 +11,7 @@ function success(value: string) {
|
|||
}
|
||||
|
||||
function error(value: string) {
|
||||
const result = parseFaasUri(value)
|
||||
const result = parseToolUri(value)
|
||||
expect(result).toBeUndefined()
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// TODO: investigate this
|
||||
/* eslint-disable security/detect-unsafe-regex */
|
||||
|
||||
import type { ParsedToolIdentifier } from './types'
|
||||
|
||||
// namespace/project-name@deploymentHash/toolPath
|
||||
const projectDeploymentToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name@version/toolPath
|
||||
const projectVersionToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
// namespace/project-name/toolPath (latest version)
|
||||
const projectToolRe =
|
||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||
|
||||
export function parseToolUri(uri: string): ParsedToolIdentifier | undefined {
|
||||
const pdtMatch = uri.match(projectDeploymentToolRe)
|
||||
|
||||
if (pdtMatch) {
|
||||
const projectIdentifier = pdtMatch[1]!
|
||||
const deploymentHash = pdtMatch[2]!
|
||||
const toolPath = pdtMatch[3] || '/'
|
||||
|
||||
return {
|
||||
projectIdentifier,
|
||||
deploymentHash,
|
||||
toolPath,
|
||||
deploymentIdentifier: `${projectIdentifier}@${deploymentHash}`
|
||||
}
|
||||
}
|
||||
|
||||
const pvtMatch = uri.match(projectVersionToolRe)
|
||||
|
||||
if (pvtMatch) {
|
||||
return {
|
||||
projectIdentifier: pvtMatch[1]!,
|
||||
version: pvtMatch[2]!,
|
||||
toolPath: pvtMatch[3] || '/'
|
||||
}
|
||||
}
|
||||
|
||||
const ptMatch = uri.match(projectToolRe)
|
||||
|
||||
if (ptMatch) {
|
||||
return {
|
||||
projectIdentifier: ptMatch[1]!,
|
||||
toolPath: ptMatch[2] || '/',
|
||||
version: 'latest'
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid tool uri
|
||||
return
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export type ParsedFaasIdentifier = {
|
||||
export type ParsedToolIdentifier = {
|
||||
projectIdentifier: string
|
||||
deploymentHash?: string
|
||||
deploymentIdentifier?: string
|
||||
|
|
|
@ -521,6 +521,12 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../platform
|
||||
|
||||
packages/json-schema:
|
||||
devDependencies:
|
||||
json-schema-test-suite:
|
||||
specifier: git+https://github.com/json-schema-org/JSON-Schema-Test-Suite#76b529f
|
||||
version: https://codeload.github.com/json-schema-org/JSON-Schema-Test-Suite/tar.gz/76b529f
|
||||
|
||||
packages/openapi-utils:
|
||||
dependencies:
|
||||
'@agentic/platform-core':
|
||||
|
@ -3379,6 +3385,10 @@ packages:
|
|||
resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
json-schema-test-suite@https://codeload.github.com/json-schema-org/JSON-Schema-Test-Suite/tar.gz/76b529f:
|
||||
resolution: {tarball: https://codeload.github.com/json-schema-org/JSON-Schema-Test-Suite/tar.gz/76b529f}
|
||||
version: 0.1.0
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
|
@ -7417,6 +7427,8 @@ snapshots:
|
|||
|
||||
json-parse-even-better-errors@4.0.0: {}
|
||||
|
||||
json-schema-test-suite@https://codeload.github.com/json-schema-org/JSON-Schema-Test-Suite/tar.gz/76b529f: {}
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
- how to handle binary bodies and responses?
|
||||
- signed requests
|
||||
- revisit deployment identifiers so possibly be URL-friendly?
|
||||
- rename parseFaasIdentifier to `parseToolIdentifier` and move validators package into platform-types?
|
||||
- rename parseToolIdentifier to `parseToolIdentifier` and move validators package into platform-types?
|
||||
|
||||
## License
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue