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({
|
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({
|
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 type { AuthenticatedEnv } from '@/lib/types'
|
||||||
import { and, db, eq, schema } from '@/db'
|
import { and, db, eq, schema } from '@/db'
|
||||||
import { acl } from '@/lib/acl'
|
import { acl } from '@/lib/acl'
|
||||||
|
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
||||||
import {
|
import {
|
||||||
openapiAuthenticatedSecuritySchemas,
|
openapiAuthenticatedSecuritySchemas,
|
||||||
openapiErrorResponses
|
openapiErrorResponses
|
||||||
|
@ -52,6 +53,9 @@ export function registerV1DeploymentsListDeployments(
|
||||||
|
|
||||||
const userId = c.get('userId')
|
const userId = c.get('userId')
|
||||||
const teamMember = c.get('teamMember')
|
const teamMember = c.get('teamMember')
|
||||||
|
const user = await ensureAuthUser(c)
|
||||||
|
const isAdmin = user.role === 'admin'
|
||||||
|
|
||||||
let projectId: string | undefined
|
let projectId: string | undefined
|
||||||
|
|
||||||
if (projectIdentifier) {
|
if (projectIdentifier) {
|
||||||
|
@ -64,9 +68,11 @@ export function registerV1DeploymentsListDeployments(
|
||||||
|
|
||||||
const deployments = await db.query.deployments.findMany({
|
const deployments = await db.query.deployments.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
teamMember
|
isAdmin
|
||||||
? eq(schema.deployments.teamId, teamMember.teamId)
|
? undefined
|
||||||
: eq(schema.deployments.userId, userId),
|
: teamMember
|
||||||
|
? eq(schema.deployments.teamId, teamMember.teamId)
|
||||||
|
: eq(schema.deployments.userId, userId),
|
||||||
projectId ? eq(schema.deployments.projectId, projectId) : undefined,
|
projectId ? eq(schema.deployments.projectId, projectId) : undefined,
|
||||||
deploymentIdentifier
|
deploymentIdentifier
|
||||||
? eq(schema.deployments.identifier, deploymentIdentifier)
|
? eq(schema.deployments.identifier, deploymentIdentifier)
|
||||||
|
|
|
@ -28,7 +28,11 @@ export const filterDeploymentSchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const populateDeploymentSchema = 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({
|
export const deploymentIdentifierQuerySchema = z.object({
|
||||||
|
|
|
@ -22,7 +22,11 @@ export const projectIdentifierQuerySchema = z.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const populateProjectSchema = 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({
|
export const projectIdentifierAndPopulateSchema = z.object({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { assert } from '@agentic/platform-core'
|
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 { z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
import type { consumersRelations } from './schema/consumer'
|
import type { consumersRelations } from './schema/consumer'
|
||||||
|
@ -45,17 +45,27 @@ export const logEntryIdSchema = getIdSchemaForModelType('logEntry')
|
||||||
|
|
||||||
export const projectIdentifierSchema = z
|
export const projectIdentifierSchema = z
|
||||||
.string()
|
.string()
|
||||||
.refine((id) => validators.projectIdentifier(id), {
|
.refine(
|
||||||
message: 'Invalid project identifier'
|
(id) =>
|
||||||
})
|
validators.projectIdentifier(id) || projectIdSchema.safeParse(id).success,
|
||||||
|
{
|
||||||
|
message: 'Invalid project identifier'
|
||||||
|
}
|
||||||
|
)
|
||||||
.describe('Public project identifier (e.g. "namespace/project-name")')
|
.describe('Public project identifier (e.g. "namespace/project-name")')
|
||||||
.openapi('ProjectIdentifier')
|
.openapi('ProjectIdentifier')
|
||||||
|
|
||||||
export const deploymentIdentifierSchema = z
|
export const deploymentIdentifierSchema = z
|
||||||
.string()
|
.string()
|
||||||
.refine((id) => !!parseFaasIdentifier(id), {
|
.refine(
|
||||||
message: 'Invalid deployment identifier'
|
(id) =>
|
||||||
})
|
!!parseToolIdentifier(id) ||
|
||||||
|
validators.deploymentIdentifier(id) ||
|
||||||
|
deploymentIdSchema.safeParse(id).success,
|
||||||
|
{
|
||||||
|
message: 'Invalid deployment identifier'
|
||||||
|
}
|
||||||
|
)
|
||||||
.describe(
|
.describe(
|
||||||
'Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}")'
|
'Public deployment identifier (e.g. "namespace/project-name@{hash|version|latest}")'
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { assert } from '@agentic/platform-core'
|
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 type { AuthenticatedContext } from '@/lib/types'
|
||||||
import {
|
import {
|
||||||
|
@ -11,7 +11,6 @@ import {
|
||||||
schema
|
schema
|
||||||
} from '@/db'
|
} from '@/db'
|
||||||
import { setPublicCacheControl } from '@/lib/cache-control'
|
import { setPublicCacheControl } from '@/lib/cache-control'
|
||||||
import { ensureAuthUser } from '@/lib/ensure-auth-user'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to find the Deployment matching the given deployment ID or
|
* Attempts to find the Deployment matching the given deployment ID or
|
||||||
|
@ -36,7 +35,6 @@ export async function tryGetDeploymentByIdentifier(
|
||||||
}
|
}
|
||||||
): Promise<RawDeployment> {
|
): Promise<RawDeployment> {
|
||||||
assert(deploymentIdentifier, 400, 'Missing required deployment identifier')
|
assert(deploymentIdentifier, 400, 'Missing required deployment identifier')
|
||||||
const user = await ensureAuthUser(ctx)
|
|
||||||
|
|
||||||
// First check if the identifier is a deployment ID
|
// First check if the identifier is a deployment ID
|
||||||
if (deploymentIdSchema.safeParse(deploymentIdentifier).success) {
|
if (deploymentIdSchema.safeParse(deploymentIdentifier).success) {
|
||||||
|
@ -49,11 +47,7 @@ export async function tryGetDeploymentByIdentifier(
|
||||||
return deployment
|
return deployment
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMember = ctx.get('teamMember')
|
const parsedFaas = parseToolIdentifier(deploymentIdentifier)
|
||||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
|
||||||
const parsedFaas = parseFaasIdentifier(deploymentIdentifier, {
|
|
||||||
namespace
|
|
||||||
})
|
|
||||||
assert(
|
assert(
|
||||||
parsedFaas,
|
parsedFaas,
|
||||||
400,
|
400,
|
||||||
|
@ -75,21 +69,18 @@ export async function tryGetDeploymentByIdentifier(
|
||||||
return deployment
|
return deployment
|
||||||
} else if (version) {
|
} else if (version) {
|
||||||
const project = await db.query.projects.findFirst({
|
const project = await db.query.projects.findFirst({
|
||||||
...dbQueryOpts,
|
|
||||||
where: eq(schema.projects.identifier, projectIdentifier)
|
where: eq(schema.projects.identifier, projectIdentifier)
|
||||||
})
|
})
|
||||||
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
assert(project, 404, `Project not found "${projectIdentifier}"`)
|
||||||
|
|
||||||
if (version === 'latest') {
|
if (version === 'latest') {
|
||||||
assert(
|
const deploymentId =
|
||||||
project.lastPublishedDeploymentId,
|
project.lastPublishedDeploymentId || project.lastDeploymentId
|
||||||
404,
|
assert(deploymentId, 404, 'Project has no published deployments')
|
||||||
'Project has no published deployments'
|
|
||||||
)
|
|
||||||
|
|
||||||
const deployment = await db.query.deployments.findFirst({
|
const deployment = await db.query.deployments.findFirst({
|
||||||
...dbQueryOpts,
|
...dbQueryOpts,
|
||||||
where: eq(schema.deployments.id, project.lastPublishedDeploymentId)
|
where: eq(schema.deployments.id, deploymentId)
|
||||||
})
|
})
|
||||||
assert(
|
assert(
|
||||||
deployment,
|
deployment,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { assert } from '@agentic/platform-core'
|
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 type { AuthenticatedContext } from '@/lib/types'
|
||||||
import { db, eq, projectIdSchema, type RawProject, schema } from '@/db'
|
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.
|
* Attempts to find the Project matching the given ID or identifier.
|
||||||
|
@ -28,7 +27,6 @@ export async function tryGetProjectByIdentifier(
|
||||||
}
|
}
|
||||||
): Promise<RawProject> {
|
): Promise<RawProject> {
|
||||||
assert(projectIdentifier, 400, 'Missing required project identifier')
|
assert(projectIdentifier, 400, 'Missing required project identifier')
|
||||||
const user = await ensureAuthUser(ctx)
|
|
||||||
|
|
||||||
// First check if the identifier is a project ID
|
// First check if the identifier is a project ID
|
||||||
if (projectIdSchema.safeParse(projectIdentifier).success) {
|
if (projectIdSchema.safeParse(projectIdentifier).success) {
|
||||||
|
@ -40,11 +38,7 @@ export async function tryGetProjectByIdentifier(
|
||||||
return project
|
return project
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamMember = ctx.get('teamMember')
|
const parsedFaas = parseToolIdentifier(projectIdentifier)
|
||||||
const namespace = teamMember ? teamMember.teamSlug : user.username
|
|
||||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
|
||||||
namespace
|
|
||||||
})
|
|
||||||
assert(
|
assert(
|
||||||
parsedFaas?.projectIdentifier,
|
parsedFaas?.projectIdentifier,
|
||||||
400,
|
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
|
body: expectedBody
|
||||||
} = fixture.response ?? {}
|
} = fixture.response ?? {}
|
||||||
|
|
||||||
test(
|
test.sequential(
|
||||||
`${i}) ${method} ${fixture.path}`,
|
`${i}) ${method} ${fixture.path}`,
|
||||||
{
|
{
|
||||||
timeout: fixture.timeout ?? 60_000
|
timeout: fixture.timeout ?? 60_000
|
||||||
|
@ -45,7 +45,12 @@ for (const [i, fixture] of fixtures.entries()) {
|
||||||
let body: any
|
let body: any
|
||||||
|
|
||||||
if (type.includes('json')) {
|
if (type.includes('json')) {
|
||||||
body = await res.json()
|
try {
|
||||||
|
body = await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('json error', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
} else if (type.includes('text')) {
|
} else if (type.includes('text')) {
|
||||||
body = await res.text()
|
body = await res.text()
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,6 +64,12 @@ for (const [i, fixture] of fixtures.entries()) {
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
expect(body).toMatchSnapshot()
|
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',
|
path: 'dev/test-basic-openapi@b6e21206/getPost',
|
||||||
request: {
|
|
||||||
searchParams: {
|
|
||||||
postId: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'test-basic-openapi@8d1a4900/getPost',
|
|
||||||
request: {
|
|
||||||
searchParams: {
|
|
||||||
postId: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'dev/test-basic-openapi@8d1a4900/getPost',
|
|
||||||
request: {
|
request: {
|
||||||
searchParams: {
|
searchParams: {
|
||||||
postId: 1
|
postId: 1
|
||||||
|
@ -70,5 +54,14 @@ export const fixtures: E2ETestFixture[] = [
|
||||||
postId: 1
|
postId: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dev/test-basic-openapi@latest/getPost',
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
json: {
|
||||||
|
postId: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -22,18 +22,19 @@
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"start": "wrangler dev",
|
"start": "wrangler dev",
|
||||||
"cf-typegen": "wrangler types ./src/worker.d.ts",
|
"cf-typegen": "wrangler types ./src/worker.d.ts",
|
||||||
|
"cf-clear-cache": "del .wrangler",
|
||||||
"clean": "del dist",
|
"clean": "del dist",
|
||||||
"test": "run-s test:*",
|
"test": "run-s test:*",
|
||||||
"test:lint": "eslint .",
|
"test:lint": "eslint .",
|
||||||
"test:typecheck": "tsc --noEmit"
|
"test:typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@agentic/json-schema": "workspace:*",
|
||||||
"@agentic/platform": "workspace:*",
|
"@agentic/platform": "workspace:*",
|
||||||
"@agentic/platform-api-client": "workspace:*",
|
"@agentic/platform-api-client": "workspace:*",
|
||||||
"@agentic/platform-core": "workspace:*",
|
"@agentic/platform-core": "workspace:*",
|
||||||
"@agentic/platform-types": "workspace:*",
|
"@agentic/platform-types": "workspace:*",
|
||||||
"@agentic/platform-validators": "workspace:*",
|
"@agentic/platform-validators": "workspace:*",
|
||||||
"@cfworker/json-schema": "^4.1.1",
|
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "catalog:",
|
"@modelcontextprotocol/sdk": "catalog:",
|
||||||
"eventid": "catalog:",
|
"eventid": "catalog:",
|
||||||
|
|
|
@ -18,11 +18,13 @@ export function cfValidateJsonSchemaObject<
|
||||||
>({
|
>({
|
||||||
schema,
|
schema,
|
||||||
data,
|
data,
|
||||||
errorMessage
|
errorMessage,
|
||||||
|
coerce = true
|
||||||
}: {
|
}: {
|
||||||
schema: any
|
schema: any
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
|
coerce?: boolean
|
||||||
}): T {
|
}): T {
|
||||||
// Special-case check for required fields to give better error messages
|
// Special-case check for required fields to give better error messages
|
||||||
if (Array.isArray(schema.required)) {
|
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)
|
const result = validator.validate(data)
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
return data as T
|
return data as T
|
||||||
|
|
|
@ -10,6 +10,7 @@ export async function fetchCache(
|
||||||
fetchResponse: () => Promise<Response>
|
fetchResponse: () => Promise<Response>
|
||||||
}
|
}
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
|
console.log('cacheKey', cacheKey?.url)
|
||||||
let response: Response | undefined
|
let response: Response | undefined
|
||||||
|
|
||||||
if (cacheKey) {
|
if (cacheKey) {
|
||||||
|
@ -23,11 +24,11 @@ export async function fetchCache(
|
||||||
if (cacheKey) {
|
if (cacheKey) {
|
||||||
if (response.headers.has('Cache-Control')) {
|
if (response.headers.has('Cache-Control')) {
|
||||||
// Note that cloudflare's `cache` should respect response headers.
|
// Note that cloudflare's `cache` should respect response headers.
|
||||||
ctx.waitUntil(
|
// ctx.waitUntil(
|
||||||
ctx.cache.put(cacheKey, response.clone()).catch((err) => {
|
// ctx.cache.put(cacheKey, response.clone()).catch((err) => {
|
||||||
console.warn('cache put error', cacheKey, err)
|
// console.warn('cache put error', cacheKey, err)
|
||||||
})
|
// })
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
response.headers.set('cf-cache-status', 'MISS')
|
response.headers.set('cf-cache-status', 'MISS')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { AdminDeployment } from '@agentic/platform-types'
|
import type { AdminDeployment } from '@agentic/platform-types'
|
||||||
import { assert } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||||
|
|
||||||
import type { Context } from './types'
|
import type { Context } from './types'
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export async function getAdminDeployment(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
identifier: string
|
identifier: string
|
||||||
): Promise<{ deployment: AdminDeployment; toolPath: string }> {
|
): Promise<{ deployment: AdminDeployment; toolPath: string }> {
|
||||||
const parsedFaas = parseFaasIdentifier(identifier)
|
const parsedFaas = parseToolIdentifier(identifier)
|
||||||
assert(parsedFaas, 404, `Invalid deployment identifier "${identifier}"`)
|
assert(parsedFaas, 404, `Invalid deployment identifier "${identifier}"`)
|
||||||
|
|
||||||
const deployment = await ctx.client.adminGetDeploymentByIdentifier({
|
const deployment = await ctx.client.adminGetDeploymentByIdentifier({
|
||||||
|
|
|
@ -90,14 +90,21 @@ export default {
|
||||||
assert(originResponse, 500, 'Origin response is required')
|
assert(originResponse, 500, 'Origin response is required')
|
||||||
const res = new Response(originResponse.body, originResponse)
|
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()
|
recordTimespans()
|
||||||
res.headers.set('x-response-time', `${originTimespan!}ms`)
|
res.headers.set('x-origin-response-time', `${originTimespan!}ms`)
|
||||||
res.headers.set('x-proxy-response-time', `${gatewayTimespan!}ms`)
|
res.headers.set('x-response-time', `${gatewayTimespan!}ms`)
|
||||||
|
|
||||||
// Reset server to agentic because Cloudflare likes to override things
|
// Reset server to agentic because Cloudflare likes to override things
|
||||||
res.headers.set('server', 'agentic')
|
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 id: DurableObjectId = env.DO_RATE_LIMITER.idFromName('foo')
|
||||||
// const stub = env.DO_RATE_LIMITER.get(id)
|
// const stub = env.DO_RATE_LIMITER.get(id)
|
||||||
// const greeting = await stub.sayHello('world')
|
// const greeting = await stub.sayHello('world')
|
||||||
|
|
|
@ -7,7 +7,8 @@ export default [
|
||||||
ignores: [
|
ignores: [
|
||||||
'**/out/**',
|
'**/out/**',
|
||||||
'packages/types/src/openapi.d.ts',
|
'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[]) => {
|
log: (...args: any[]) => {
|
||||||
if (program.opts().json) {
|
if (program.opts().json) {
|
||||||
console.log(
|
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 {
|
} else {
|
||||||
console.log(...args)
|
console.log(...args)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Deployment } from '@agentic/platform-types'
|
import type { Deployment } from '@agentic/platform-types'
|
||||||
import { parseFaasIdentifier } from '@agentic/platform-validators'
|
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import { oraPromise } from 'ora'
|
import { oraPromise } from 'ora'
|
||||||
|
|
||||||
|
@ -24,11 +24,7 @@ export function registerListDeploymentsCommand({
|
||||||
let label = 'Fetching all projects and deployments'
|
let label = 'Fetching all projects and deployments'
|
||||||
|
|
||||||
if (projectIdentifier) {
|
if (projectIdentifier) {
|
||||||
const auth = AuthStore.getAuth()
|
const parsedFaas = parseToolIdentifier(projectIdentifier)
|
||||||
const parsedFaas = parseFaasIdentifier(projectIdentifier, {
|
|
||||||
// TODO: use team slug if available
|
|
||||||
namespace: auth.user.username
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!parsedFaas) {
|
if (!parsedFaas) {
|
||||||
throw new Error(`Invalid project identifier "${projectIdentifier}"`)
|
throw new Error(`Invalid project identifier "${projectIdentifier}"`)
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function registerPublishCommand({ client, program, logger }: Context) {
|
||||||
AuthStore.requireAuth()
|
AuthStore.requireAuth()
|
||||||
|
|
||||||
if (deploymentIdentifier) {
|
if (deploymentIdentifier) {
|
||||||
// TODO: parseFaasIdentifier
|
// TODO: parseToolIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
const deployment = await oraPromise(
|
const deployment = await oraPromise(
|
||||||
|
|
|
@ -25,7 +25,7 @@ export async function resolveDeployment({
|
||||||
const namespace = auth.user.username
|
const namespace = auth.user.username
|
||||||
|
|
||||||
// TODO: resolve deploymentIdentifier; config name may include namespace?
|
// 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}`
|
deploymentIdentifier = `${namespace}/${config.name}@${fuzzyDeploymentIdentifierVersion}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
{
|
{
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"name": "postId",
|
"name": "postId",
|
||||||
"in": "path"
|
"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-tool-identifier'
|
||||||
export * from './parse-faas-uri'
|
export * from './parse-tool-uri'
|
||||||
export type * from './types'
|
export type * from './types'
|
||||||
export * as validators from './validators'
|
export * as validators from './validators'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { ParsedFaasIdentifier } from './types'
|
import type { ParsedToolIdentifier } from './types'
|
||||||
import { parseFaasUri } from './parse-faas-uri'
|
import { parseToolUri } from './parse-faas-uri'
|
||||||
|
|
||||||
export function parseFaasIdentifier(
|
export function parseToolIdentifier(
|
||||||
identifier: string,
|
identifier: string
|
||||||
{ namespace }: { namespace?: string } = {}
|
// { namespace }: { namespace?: string } = {}
|
||||||
): ParsedFaasIdentifier | undefined {
|
): ParsedToolIdentifier | undefined {
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -22,17 +22,17 @@ export function parseFaasIdentifier(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNamespacePrefix = /^([a-zA-Z0-9-]{1,64}\/)/.test(uri)
|
// const hasNamespacePrefix = /^([a-zA-Z0-9-]{1,64}\/)/.test(uri)
|
||||||
|
|
||||||
if (!hasNamespacePrefix) {
|
// if (!hasNamespacePrefix) {
|
||||||
if (namespace) {
|
// if (namespace) {
|
||||||
// add inferred namespace prefix (defaults to authenticated user's username)
|
// // add inferred namespace prefix (defaults to authenticated user's username)
|
||||||
uri = `${namespace}/${uri}`
|
// uri = `${namespace}/${uri}`
|
||||||
} else {
|
// } else {
|
||||||
// throw new Error(`FaaS identifier is missing namespace prefix or you must be authenticated [${uri}]`)
|
// // throw new Error(`FaaS identifier is missing namespace prefix or you must be authenticated [${uri}]`)
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return parseFaasUri(uri)
|
return parseToolUri(uri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
// TODO: investigate this
|
// TODO: investigate this
|
||||||
/* eslint-disable security/detect-unsafe-regex */
|
/* eslint-disable security/detect-unsafe-regex */
|
||||||
|
|
||||||
import type { ParsedFaasIdentifier } from './types'
|
import type { ParsedToolIdentifier } from './types'
|
||||||
|
|
||||||
// namespace/project-name@deploymentHash/toolPath
|
// namespace/project-name@deploymentHash/toolPath
|
||||||
// project@deploymentHash/toolPath
|
|
||||||
const projectDeploymentToolRe =
|
const projectDeploymentToolRe =
|
||||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([a-z0-9]{8})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||||
|
|
||||||
// namespace/project-name@version/toolPath
|
// namespace/project-name@version/toolPath
|
||||||
// project@version/toolPath
|
|
||||||
const projectVersionToolRe =
|
const projectVersionToolRe =
|
||||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})@([^/?@]+)(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
||||||
|
|
||||||
// namespace/project-name/toolPath
|
// namespace/project-name/toolPath (latest version)
|
||||||
// project/toolPath (latest version)
|
|
||||||
const projectToolRe =
|
const projectToolRe =
|
||||||
/^([a-zA-Z0-9-]{1,64}\/[a-z0-9-]{2,64})(\/[a-zA-Z0-9\-._~%!$&'()*+,;=:/]*)?$/
|
/^([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)
|
const pdtMatch = uri.match(projectDeploymentToolRe)
|
||||||
|
|
||||||
if (pdtMatch) {
|
if (pdtMatch) {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { expect, test } from 'vitest'
|
import { expect, test } from 'vitest'
|
||||||
|
|
||||||
import { parseFaasIdentifier } from './parse-faas-identifier'
|
import { parseToolIdentifier } from './parse-tool-identifier'
|
||||||
import * as validators from './validators'
|
import * as validators from './validators'
|
||||||
|
|
||||||
function success(...args: Parameters<typeof parseFaasIdentifier>) {
|
function success(...args: Parameters<typeof parseToolIdentifier>) {
|
||||||
const result = parseFaasIdentifier(...args)
|
const result = parseToolIdentifier(...args)
|
||||||
expect(result).toBeTruthy()
|
expect(result).toBeTruthy()
|
||||||
expect(result!.projectIdentifier).toBeTruthy()
|
expect(result!.projectIdentifier).toBeTruthy()
|
||||||
expect(result!.version || result!.deploymentHash).toBeTruthy()
|
expect(result!.version || result!.deploymentHash).toBeTruthy()
|
||||||
|
@ -21,8 +21,8 @@ function success(...args: Parameters<typeof parseFaasIdentifier>) {
|
||||||
expect(result).toMatchSnapshot()
|
expect(result).toMatchSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
function error(...args: Parameters<typeof parseFaasIdentifier>) {
|
function error(...args: Parameters<typeof parseToolIdentifier>) {
|
||||||
const result = parseFaasIdentifier(...args)
|
const result = parseToolIdentifier(...args)
|
||||||
expect(result).toBeUndefined()
|
expect(result).toBeUndefined()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,19 +61,15 @@ test('URL prefix and suffix success', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('namespace success', () => {
|
test('namespace success', () => {
|
||||||
success('https://api.saasify.sh/foo-bar@01234567/foo', {
|
success('https://api.saasify.sh/username/foo-bar@01234567/foo')
|
||||||
namespace: 'username'
|
success('/username/foo-bar@01234567/foo')
|
||||||
})
|
success('/username/foo-bar@01234567/foo')
|
||||||
success('/foo-bar@01234567/foo', { namespace: 'username' })
|
success('/username/foo-bar@01234567/foo/')
|
||||||
success('/foo-bar@01234567/foo', { namespace: 'username' })
|
success('username/https://api.saasify.sh/foo-bar@01234567/foo/bar/123')
|
||||||
success('/foo-bar@01234567/foo/', { namespace: 'username' })
|
success('/username/foo-bar@01234567/foo/bar/123')
|
||||||
success('https://api.saasify.sh/foo-bar@01234567/foo/bar/123', {
|
success('/username/foo-bar@latest/foo/bar/123')
|
||||||
namespace: 'username'
|
success('/username/foo-bar@dev/foo/bar/123')
|
||||||
})
|
success('/username/foo-bar@1.2.3/foo/bar/123')
|
||||||
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' })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('namespace error', () => {
|
test('namespace error', () => {
|
||||||
|
@ -82,6 +78,7 @@ test('namespace error', () => {
|
||||||
error('/foo-bar@01234567/foo')
|
error('/foo-bar@01234567/foo')
|
||||||
error('/foo-bar@dev/foo')
|
error('/foo-bar@dev/foo')
|
||||||
error('/foo-bar@01234567/foo')
|
error('/foo-bar@01234567/foo')
|
||||||
|
error('foo-bar/tool')
|
||||||
error('/foo-bar@01234567/foo/')
|
error('/foo-bar@01234567/foo/')
|
||||||
error('/foo-bar@01234567/foo/bar/123')
|
error('/foo-bar@01234567/foo/bar/123')
|
||||||
error('/foo-bar@0latest/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 { expect, test } from 'vitest'
|
||||||
|
|
||||||
import { parseFaasUri } from './parse-faas-uri'
|
import { parseToolUri } from './parse-tool-uri'
|
||||||
|
|
||||||
function success(value: string) {
|
function success(value: string) {
|
||||||
const result = parseFaasUri(value)
|
const result = parseToolUri(value)
|
||||||
expect(result).toBeTruthy()
|
expect(result).toBeTruthy()
|
||||||
expect(result?.projectIdentifier).toBeTruthy()
|
expect(result?.projectIdentifier).toBeTruthy()
|
||||||
expect(result?.version || result?.deploymentHash).toBeTruthy()
|
expect(result?.version || result?.deploymentHash).toBeTruthy()
|
||||||
|
@ -11,7 +11,7 @@ function success(value: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function error(value: string) {
|
function error(value: string) {
|
||||||
const result = parseFaasUri(value)
|
const result = parseToolUri(value)
|
||||||
expect(result).toBeUndefined()
|
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
|
projectIdentifier: string
|
||||||
deploymentHash?: string
|
deploymentHash?: string
|
||||||
deploymentIdentifier?: string
|
deploymentIdentifier?: string
|
||||||
|
|
|
@ -521,6 +521,12 @@ importers:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../platform
|
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:
|
packages/openapi-utils:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@agentic/platform-core':
|
'@agentic/platform-core':
|
||||||
|
@ -3379,6 +3385,10 @@ packages:
|
||||||
resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
|
resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
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:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
|
@ -7417,6 +7427,8 @@ snapshots:
|
||||||
|
|
||||||
json-parse-even-better-errors@4.0.0: {}
|
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@0.4.1: {}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
- how to handle binary bodies and responses?
|
- how to handle binary bodies and responses?
|
||||||
- signed requests
|
- signed requests
|
||||||
- revisit deployment identifiers so possibly be URL-friendly?
|
- 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
|
## License
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue