Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,8 @@
"cmd": "embed-markdown"
}
]
}
},
"cSpell.words": [
"deepcopy"
]
}
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ linked docs before continuing.
- [Map Schema](#map-schema)
- [Media Schema](#media-schema)
- [Enumeration](#enumeration)
- [Polymorphic Objects](#polymorphic-objects)
- [Validating Data](#validating-data)
- [Common Schemas](#common-schemas)
- [Getting JSON Schema](#getting-json-schema)
Expand Down Expand Up @@ -202,6 +203,36 @@ S.str.enum('a', 'b')
S.str.enum(Object.keys(someObject))
```

## Polymorphic Objects
A polymorphic object's properties depend on its type. It can be declared with
`S.polymorphicObject({ commonProps, typeNameToObj, typeKey })`:
* `commonProps` - Optional. The properties that all objects of this polymorphic
type will contain. This is the same as the argument to `S.obj()`.
* `typeNameToObj` - Required to have at least one key-value pair. Each key is
the name of one of the polymorphic types this object can have. The
corresponding value must be an `S.obj()` or an object that can be passed to
`S.obj()`; it describes the additional properties of this polymorphic type.
* `typeKey` - Optional. The default type property is `type` but can be changed
to anything you'd like.

You can set `additionalProperties` to `true` on an `S.polymorphicObject` schema
just like an `S.obj` schema. However, the sub-types are all bound by this.

You can nest polymorphic types inside of each other or other types.

```javascript <!-- embed:test/unit-test-schema.js:section:exPolymorphicObj start:exPolymorphicObj end -->
const schema = S.polymorphicObj({
commonProps: { n: S.int },
typeNameToObj: {
someType: { x: S.str },
typeWithNoExtraProps: {}
},
typeKey: 'kind'
})
const ok1 = { kind: 'someType', x: 'a', n: 1 }
const ok2 = { kind: 'typeWithNoExtraProps', n: 2 }
```

## Validating Data
Schema is compiled into a validator that can be used to efficiently validate
data. When compiling a schema, a name must be provided. The name should
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pocketgems/schema",
"version": "0.1.3",
"version": "0.1.4",
"main": "src/schema.js",
"license": "Apache-2.0",
"scripts": {
Expand Down
96 changes: 94 additions & 2 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ class BaseSchema {
assert.ok(name, 'name is required')
if (!compiler) {
if (!ajv) {
ajv = new (require('ajv'))({
ajv = new (require('ajv/dist/2020'))({
allErrors: true,
useDefaults: true,
strictSchema: false
Expand Down Expand Up @@ -441,6 +441,90 @@ class ObjectSchema extends BaseSchema {
}
}

/**
* A polymorphic type.
*
* The properties it contains will be based on its type. The type is determined
* by a specific property (by default named "type").
*/
class PolymorphicObjectSchema extends ObjectSchema {
static JSON_SCHEMA_TYPE = 'object'
static MAX_PROP_NAME = 'maxProperties'
static MIN_PROP_NAME = 'minProperties'

/**
* Creates an object schema object.
* @param {Object} [commonProps={}] Keys must be strings, values must be
* schema objects. Passed directly to the S.obj() constructor.
* @param {Object} [typeNameToObj={}] Keys are strings which are valid type
* names for this polymorphic object. Values are S.obj.
* @param {String} [typeKey='type'] The name of the property which
* determines which type of data this object has (all objects will have
* this property in addition to what's in commonProps).
*/
constructor (commonProps = {}, typeNameToObj, typeKey) {
super(commonProps)
if (typeNameToObj === undefined) {
return this // copy() will set up the rest
}
this.__allowedTypeNames = Object.keys(typeNameToObj)
this.__allowedTypeNames.sort()
assert.ok(this.__allowedTypeNames.length > 0, 'must have at least one type')
this.prop(typeKey, S.str.enum(this.__allowedTypeNames))
this.typeKey = typeKey
this.typeNameToExtraProps = {}
for (const typeName of this.__allowedTypeNames) {
let objSchema = typeNameToObj[typeName]
// propSchema must be an S.obj (not S.polymorphicObj) or a plain old
// javascript object that can be turned into an S.obj
if (typeof objSchema === 'object') {
objSchema = S.obj(objSchema)
}
assert.ok(Object.getPrototypeOf(objSchema) === ObjectSchema.prototype,
'polymorphic object sub-types must be S.obj')
objSchema.lock()
const { type, ...relevantProperties } = objSchema.properties()
// additional properties constraint is enforced (or not) by the root
// polymorphic object only (ignored on sub-types)
delete relevantProperties.additionalProperties
if (Object.keys(relevantProperties).length > 0) {
this.typeNameToExtraProps[typeName] = relevantProperties
}
}
}

copy () {
const ret = super.copy()
ret.__allowedTypeNames = deepcopy(this.__allowedTypeNames)
ret.typeKey = this.typeKey
ret.typeNameToExtraProps = deepcopy(this.typeNameToExtraProps)
return ret
}

properties () {
const ret = super.properties()
let cur = ret
for (const typeName of this.__allowedTypeNames) {
const extraProps = this.typeNameToExtraProps[typeName]
if (!extraProps) {
continue
}
if (cur.if) {
cur.else = {}
cur = cur.else
}
cur.if = { properties: { [this.typeKey]: { const: typeName } } }
cur.then = extraProps
}
// when using applicator keywords like "if" we need to use
// unevaluatedProperties instead of additionalProperties (because the check
// needs to happen after the applicators have finalized the properties)
ret.unevaluatedProperties = ret.additionalProperties
delete ret.additionalProperties
return ret
}
}

/**
* The ArraySchema class.
*/
Expand Down Expand Up @@ -751,7 +835,7 @@ class JSONSchemaExporter {

export (schema) {
const ret = deepcopy(schema.export(this))
ret.$schema = 'http://json-schema.org/draft-07/schema#'
ret.$schema = 'https://json-schema.org/draft/2020-12/schema'
return ret
}
}
Expand All @@ -768,6 +852,14 @@ class S {
*/
static obj (object) { return new ObjectSchema(object) }

/**
* @param {Object} object See {@link ObjectSchema#constructor}
* @return A new ObjectSchema object.
*/
static polymorphicObj ({ commonProps = {}, typeNameToObj = {}, typeKey = 'type' } = {}) {
return new PolymorphicObjectSchema(commonProps, typeNameToObj, typeKey)
}

/**
* @param {BaseSchema} schema See {@link ArraySchema#constructor}
* @return A new ArraySchema object.
Expand Down
Loading