Skip to content

Commit

Permalink
feat(jex): add reason when schema does not extend
Browse files Browse the repository at this point in the history
  • Loading branch information
franklevasseur committed Feb 13, 2024
1 parent 4bdb4ab commit 9ee7fec
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 29 deletions.
17 changes: 15 additions & 2 deletions jex/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@ import { JSONSchema7 } from 'json-schema'

export * as errors from './errors'

const failureReasonToString = (reason: jex.JexFailureReason, prefix = ''): string => {
if (reason.child.length === 0) {
return `${prefix}- ${reason.message}`
}
const childLines = reason.child.map((r) => failureReasonToString(r, `${prefix} `)).join('\n')
return `${prefix}- ${reason.message}:\n${childLines}`
}

export const jsonSchemaEquals = async (a: JSONSchema7, b: JSONSchema7): Promise<boolean> => {
const jexA = await jex.toJex(a)
const jexB = await jex.toJex(b)
return jex.jexEquals(jexA, jexB)
}

export const jsonSchemaExtends = async (child: JSONSchema7, parent: JSONSchema7): Promise<boolean> => {
type ExtensionResult = { extends: true } | { extends: false; reason: string }
export const jsonSchemaExtends = async (child: JSONSchema7, parent: JSONSchema7): Promise<ExtensionResult> => {
const jexChild = await jex.toJex(child)
const jexParent = await jex.toJex(parent)
return jex.jexExtends(jexChild, jexParent)
const res = jex.jexExtends(jexChild, jexParent)
if (res.result) {
return { extends: true }
}
return { extends: false, reason: failureReasonToString(res.reason) }
}
7 changes: 5 additions & 2 deletions jex/src/jex-representation/jex-extends.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ const expectJex = (jexType: types.JexType) => ({
not: {
toExtend: (parent: types.JexType) => {
const actual = jexExtends(jexType, parent)
expect(actual).toBe(false)
expect(actual.result).toBe(false)
}
},
toExtend: (parent: types.JexType) => {
const actual = jexExtends(jexType, parent)
expect(actual).toBe(true)
if (!actual.result) {
console.log(actual.reason)
}
expect(actual.result).toBe(true)
}
})

Expand Down
163 changes: 138 additions & 25 deletions jex/src/jex-representation/jex-extends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,32 @@ import { jexEquals } from './jex-equals'

type LiteralOf<T extends types.JexPrimitive> = Extract<types.JexLiteral, { type: T['type'] }>

const _primitiveExtends = <T extends types.JexPrimitive>(child: T | LiteralOf<T>, parent: types.JexType): boolean => {
export type JexFailureReason = {
message: string
child: JexFailureReason[]
}

export type JexSuccessResult = {
result: true
}

export type JexFailureResult = {
result: false
reason: JexFailureReason
}

export type JexExtensionResult = JexSuccessResult | JexFailureResult

const _isFailure = (result: JexExtensionResult): result is JexFailureResult => !result.result

const _primitiveExtends = <T extends types.JexPrimitive>(
child: T | LiteralOf<T>,
parent: types.JexType
): JexExtensionResult => {
const isT = (x: types.JexType): x is T | LiteralOf<T> => x.type === child.type
if (!isT(parent)) return false
if (!isT(parent)) {
return { result: false, reason: { message: `"${child.type}" does not extend "${parent.type}"`, child: [] } }
}

type Primitive = LiteralOf<T> | (T & { value: undefined })
const asPrimitive = (value: T | LiteralOf<T>): Primitive =>
Expand All @@ -14,71 +37,161 @@ const _primitiveExtends = <T extends types.JexPrimitive>(child: T | LiteralOf<T>
const _child = asPrimitive(child)
const _parent = asPrimitive(parent)

if (_parent.value === undefined && _child.value === undefined) {
return true
}
if (_parent.value === undefined) {
return true
}
if (_parent.value === _child.value) {
return true
if (_parent.value !== undefined) {
if (_child.value === undefined) {
return {
result: false,
reason: { message: `"${child.type}" does not extend literal "${parent.value}"`, child: [] }
}
}

if (_parent.value !== _child.value) {
return {
result: false,
reason: { message: `literal "${_child.value}" does not extend literal "${parent.value}"`, child: [] }
}
}
}
return false

return { result: true }
}

export const jexExtends = (child: types.JexType, parent: types.JexType): boolean => {
export const jexExtends = (child: types.JexType, parent: types.JexType): JexExtensionResult => {
if (parent.type === 'any' || child.type === 'any') {
return true
return { result: true }
}

if (child.type === 'union') {
return child.anyOf.every((c) => jexExtends(c, parent))
const childExtensions = child.anyOf.map((c) => jexExtends(c, parent))
const result = childExtensions.every((c) => c.result)
if (result) {
return { result: true }
}

const failures = childExtensions.filter(_isFailure)
return {
result: false,
reason: { message: 'some of child union do not extend parent', child: failures.map((f) => f.reason) }
}
}

if (parent.type === 'union') {
return parent.anyOf.some((p) => jexExtends(child, p))
const parentExtensions = parent.anyOf.map((p) => jexExtends(child, p))
const result = parentExtensions.some((p) => p.result)
if (result) {
return { result: true }
}
const failures = parentExtensions.filter(_isFailure)
return {
result: false,
reason: { message: 'child does not extend any parent union', child: failures.map((f) => f.reason) }
}
}

if (child.type === 'object') {
if (parent.type === 'map') {
return Object.values(child.properties).every((c) => jexExtends(c, parent.items))
const childExtensions = Object.values(child.properties).map((c) => jexExtends(c, parent.items))
const result = childExtensions.every((c) => c.result)
if (result) {
return { result: true }
}
const failures = childExtensions.filter(_isFailure)
return {
result: false,
reason: { message: 'some of child properties do not extend parent value', child: failures.map((f) => f.reason) }
}
}

if (parent.type === 'object') {
return Object.entries(parent.properties).every(([key, parentValue]) => {
const parentExtensions: JexExtensionResult[] = Object.entries(parent.properties).map(([key, parentValue]) => {
const childValue = child.properties[key]
if (childValue === undefined) return false
if (childValue === undefined) {
return { result: false, reason: { message: `child does not have property "${key}"`, child: [] } }
}
return jexExtends(childValue, parentValue)
})

const result = parentExtensions.every((p) => p.result)
if (result) {
return { result: true }
}

const failures = parentExtensions.filter(_isFailure)
return {
result: false,
reason: {
message: 'some of child properties are either missing or do not extend parent properties',
child: failures.map((f) => f.reason)
}
}
}
return false

return { result: false, reason: { message: `"${child.type}" does not extend "${parent.type}"`, child: [] } }
}

if (child.type === 'map') {
if (parent.type !== 'map') return false
if (parent.type !== 'map') {
return { result: false, reason: { message: `"${child.type}" does not extend "${parent.type}"`, child: [] } }
}
return jexExtends(child.items, parent.items)
}

if (child.type === 'tuple') {
if (parent.type === 'array') {
return child.items.every((c) => jexExtends(c, parent.items))
const childExtensions = child.items.map((c) => jexExtends(c, parent.items))
const result = childExtensions.every((c) => c.result)
if (result) {
return { result: true }
}
const failures = childExtensions.filter(_isFailure)
return {
result: false,
reason: { message: 'some of child items do not extend parent items', child: failures.map((f) => f.reason) }
}
}
if (parent.type === 'tuple') {
const zipped = child.items.map(
(c, i) => [c, parent.items[i]] satisfies [types.JexType, types.JexType | undefined]
)
return zipped.every(([c, p]) => p === undefined || jexExtends(c, p))

const extensions: JexExtensionResult[] = zipped.map(([c, p]) => {
if (p === undefined) {
// if parent tuple is shorter than child tuple, it is still valid
return { result: true }
}
return jexExtends(c, p)
})

const result = extensions.every((e) => e.result)
if (result) {
return { result: true }
}

const failures = extensions.filter(_isFailure)
return {
result: false,
reason: { message: 'some of child items do not extend parent items', child: failures.map((f) => f.reason) }
}
}
return false

return { result: false, reason: { message: `"${child.type}" does not extend "${parent.type}"`, child: [] } }
}

if (child.type === 'array') {
if (parent.type !== 'array') return false
if (parent.type !== 'array') {
return { result: false, reason: { message: `"${child.type}" does not extend "${parent.type}"`, child: [] } }
}
return jexExtends(child.items, parent.items)
}

if (child.type === 'string' || child.type === 'number' || child.type === 'boolean') {
return _primitiveExtends(child, parent)
}

return jexEquals(child, parent)
const result = jexEquals(child, parent)
if (result) {
return { result: true }
}
const reason = { message: `"${child.type}" does not extend "${parent.type}"`, child: [] }
return { result: false, reason }
}

0 comments on commit 9ee7fec

Please sign in to comment.