Skip to content

Commit

Permalink
feat: support propertyDependencies from draft/next
Browse files Browse the repository at this point in the history
For now, causes an uncertainty from removeAdditional / useDefaults, but
that could be resolved/optimized in some certain situations later.

Refs: json-schema-org/json-schema-spec#1082
Refs: json-schema-org/json-schema-spec#1143
  • Loading branch information
ChALkeR committed Oct 6, 2022
1 parent d514df2 commit 8eb5565
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 17 deletions.
2 changes: 1 addition & 1 deletion doc/samples/draft-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Based on JSON Schema Test Suite for `draft-next`.
| [patternProperties](./patternProperties.md) | 5 | - | - | - |
| [prefixItems](./prefixItems.md) | 4 | - | - | - |
| [properties](./properties.md) | 6 | - | - | - |
| [propertyDependencies](./propertyDependencies.md) | 3 | - | 3 | 1 |
| [propertyDependencies](./propertyDependencies.md) | 3 | - | - | - |
| [propertyNames](./propertyNames.md) | 3 | - | - | - |
| [ref](./ref.md) | 29 | - | - | - |
| [refRemote](./refRemote.md) | 13 | - | - | - |
Expand Down
45 changes: 35 additions & 10 deletions doc/samples/draft-next/propertyDependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@

```js
'use strict'
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
const ref0 = function validate(data) {
if (typeof data === "object" && data && !Array.isArray(data)) {
if (data.foo !== undefined && hasOwn(data, "foo")) {
if (data.foo === "bar") return false
}
}
return true
};
return ref0
```

### Warnings
##### Strong mode notices

* `Keyword not supported: "propertyDependencies" at #`
* `[requireValidation] type should be specified at #`


## propertyDependencies doesn't act on non-string property values
Expand All @@ -35,15 +41,21 @@ return ref0

```js
'use strict'
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
const ref0 = function validate(data) {
if (typeof data === "object" && data && !Array.isArray(data)) {
if (data.foo !== undefined && hasOwn(data, "foo")) {
if (data.foo === "bar") return false
}
}
return true
};
return ref0
```

### Warnings
##### Strong mode notices

* `Keyword not supported: "propertyDependencies" at #`
* `[requireValidation] type should be specified at #`


## multiple options selects the right one
Expand All @@ -67,17 +79,30 @@ return ref0

```js
'use strict'
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
const ref0 = function validate(data) {
if (typeof data === "object" && data && !Array.isArray(data)) {
if (data.foo !== undefined && hasOwn(data, "foo")) {
if (data.foo === "bar") {
if (typeof data === "object" && data && !Array.isArray(data)) {
if (Object.keys(data).length > 2) return false
if (Object.keys(data).length < 2) return false
}
}
if (data.foo === "baz") {
if (typeof data === "object" && data && !Array.isArray(data)) {
if (Object.keys(data).length > 1) return false
}
}
if (data.foo === "quux") return false
}
}
return true
};
return ref0
```

### Warnings

* `Keyword not supported: "propertyDependencies" at #`

### Misclassified!
##### Strong mode notices

**This schema caused 4 misclassifications!**
* `[requireValidation] schema = true is not allowed at #/propertyDependencies/foo/qux`

26 changes: 24 additions & 2 deletions src/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
errorIf(safeand(present(item), condition), errorArgs)
}
} else if (isSchemaish(deps) && dependencies !== 'dependentRequired') {
uncertain(dependencies)
uncertain(dependencies) // TODO: we don't always need this, remove when no uncertainity?
fun.if(item.checked ? true : present(item), () => {
const delta = rule(current, deps, subPath(dependencies, key), dyn)
evaluateDelta(orDelta({}, delta))
Expand All @@ -776,6 +776,27 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
})
}

handle('propertyDependencies', ['object'], (propertyDependencies) => {
for (const [key, variants] of Object.entries(propertyDependencies)) {
enforce(isPlainObject(variants), 'propertyDependencies must be an object')
uncertain('propertyDependencies') // TODO: we don't always need this, remove when no uncertainity?
const item = currPropImm(key, checked(key))
// NOTE: would it be useful to also check if it's a string?
fun.if(item.checked ? true : present(item), () => {
for (const [val, deps] of Object.entries(variants)) {
enforce(isSchemaish(deps), 'propertyDependencies must contain schemas')
fun.if(compare(buildName(item), val), () => {
// TODO: we already know that we have an object here, optimize?
const delta = rule(current, deps, subPath('propertyDependencies', key, val), dyn)
evaluateDelta(orDelta({}, delta))
evaluateDeltaDynamic(delta)
})
}
})
}
return null
})

handle('properties', ['object'], (properties) => {
for (const p of Object.keys(properties)) {
if (constProp === p) continue // checked in discriminator, avoid double-check
Expand Down Expand Up @@ -1181,8 +1202,9 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
const logicalOp = ['not', 'if', 'then', 'else'].includes(schemaPath[schemaPath.length - 1])
const branchOp = ['oneOf', 'anyOf', 'allOf'].includes(schemaPath[schemaPath.length - 2])
const depOp = ['dependencies', 'dependentSchemas'].includes(schemaPath[schemaPath.length - 2])
const propDepOp = ['propertyDependencies'].includes(schemaPath[schemaPath.length - 3])
// Coherence check, unreachable, double-check that we came from expected path
enforce(logicalOp || branchOp || depOp, 'Unexpected')
enforce(logicalOp || branchOp || depOp || propDepOp, 'Unexpected logical path')
} else if (!schemaPath.includes('not')) {
// 'not' does not mark anything as evaluated (unlike even if/then/else), so it's safe to exclude from these
// checks, as we are sure that everything will be checked without it. It can be viewed as a pure add-on.
Expand Down
3 changes: 2 additions & 1 deletion src/known-keywords.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const knownKeywords = [
...['maxLength', 'minLength', 'format', 'pattern'], // strings
...['contentEncoding', 'contentMediaType', 'contentSchema'], // strings content
...['properties', 'maxProperties', 'minProperties', 'additionalProperties', 'patternProperties'], // objects
...['propertyNames', 'dependencies', 'dependentRequired', 'dependentSchemas'], // objects
...['propertyNames'], // objects
...['dependencies', 'dependentRequired', 'dependentSchemas', 'propertyDependencies'], // objects (dependencies)
...['unevaluatedProperties', 'unevaluatedItems'], // see-through
// Unused meta keywords not affecting validation (annotations and comments)
// https://json-schema.org/understanding-json-schema/reference/generic.html
Expand Down
3 changes: 0 additions & 3 deletions test/json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,6 @@ const unsupported = new Set([

// draft-next changes to bookending requirement in dynamicRef
'draft-next/dynamicRef.json',

// draft-next only, new features
'draft-next/propertyDependencies.json',
])
const unsupportedMask = []

Expand Down

0 comments on commit 8eb5565

Please sign in to comment.