Skip to content

Commit

Permalink
feat: add exported toJsonSchema function
Browse files Browse the repository at this point in the history
  • Loading branch information
aldeed committed Nov 26, 2022
1 parent 220da1e commit e356fcd
Show file tree
Hide file tree
Showing 6 changed files with 489 additions and 2 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ There are also reasons not to choose this package. Because of all it does, this
- [Validate one key against another](#validate-one-key-against-another)
- [Debug Mode](#debug-mode)
- [Extending the Schema Options](#extending-the-schema-options)
- [Converting a SimpleSchema to a JSONSchema](#converting-a-simpleschema-to-a-jsonschema)
- [Add On Packages](#add-on-packages)
- [Contributors](#contributors)
- [Sponsors](#sponsors)
Expand Down Expand Up @@ -1243,6 +1244,18 @@ SimpleSchema.extendOptions(["index", "unique", "denyInsert", "denyUpdate"]);

Obviously you need to ensure that `extendOptions` is called before any SimpleSchema instances are created with those options.

## Converting a SimpleSchema to a JSONSchema

```ts
import { toJsonSchema } from 'simpl-schema'

const schema = new SimpleSchema({
name: String
})

const jsonSchema = toJsonSchema(schema)
```

## Add On Packages

[mxab:simple-schema-jsdoc](https://atmospherejs.com/mxab/simple-schema-jsdoc) Generate jsdoc from your schemas.
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
},
"devDependencies": {
"@types/clone": "^2.1.1",
"@types/json-schema": "^7.0.11",
"@types/mocha": "^9.1.1",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
Expand Down Expand Up @@ -85,4 +86,4 @@
"publishConfig": {
"access": "public"
}
}
}
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import './clean.js'

import { SimpleSchema, ValidationContext } from './SimpleSchema.js'
import { toJsonSchema } from './toJsonSchema.js'

SimpleSchema.ValidationContext = ValidationContext

export { ValidationContext }
export { toJsonSchema, ValidationContext }

export default SimpleSchema
170 changes: 170 additions & 0 deletions src/toJsonSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { JSONSchema7 } from 'json-schema'

import { SimpleSchema } from './SimpleSchema.js'
import { StandardSchemaKeyDefinition } from './types.js'

const jsonSchemaVersion = 'https://json-schema.org/draft/2020-12/schema'

function toJSArray (ss: SimpleSchema, key: string, fieldDef: StandardSchemaKeyDefinition): JSONSchema7 | null {
const itemSchema = fieldDefToJsonSchema(ss, `${key}.$`)
if (itemSchema == null) return null

const arrayDef: JSONSchema7 = {
type: 'array',
items: [itemSchema],
additionalItems: false
}

if (fieldDef.minCount !== undefined) {
arrayDef.minItems = fieldDef.minCount
}

if (fieldDef.maxCount !== undefined) {
arrayDef.maxItems = fieldDef.maxCount
}

return arrayDef
}

function toJsProperties (ss: SimpleSchema): {
properties: Record<string, JSONSchema7>
required: string[]
} {
const properties: Record<string, JSONSchema7> = {}
const required: string[] = []

for (const key of ss.objectKeys()) {
const fieldDef = ss.schema(key)
if (fieldDef == null) continue
if (fieldDef.optional !== true) required.push(key)
const schema = fieldDefToJsonSchema(ss, key)
if (schema != null) properties[key] = schema
}

return { properties, required }
}

function toJSObj (simpleSchema: SimpleSchema, additionalProperties: boolean = false): JSONSchema7 | null {
return {
type: 'object',
...toJsProperties(simpleSchema),
additionalProperties
}
}

function fieldDefToJsonSchema (ss: SimpleSchema, key: string): JSONSchema7 | null {
const fieldDef = ss.schema(key)
if (fieldDef == null) return null

const itemSchemas = []

for (const fieldTypeDef of fieldDef.type.definitions) {
let itemSchema: JSONSchema7 | null = null

switch (fieldTypeDef.type) {
case String:
itemSchema = { type: 'string' }
if (fieldTypeDef.allowedValues !== undefined && typeof fieldTypeDef.allowedValues !== 'function') {
itemSchema.enum = [...fieldTypeDef.allowedValues]
}
if (fieldTypeDef.max !== undefined && typeof fieldTypeDef.max !== 'function') {
itemSchema.maxLength = fieldTypeDef.max as number
}
if (fieldTypeDef.min !== undefined && typeof fieldTypeDef.min !== 'function') {
itemSchema.minLength = fieldTypeDef.min as number
}
if (fieldTypeDef.regEx instanceof RegExp) {
itemSchema.pattern = String(fieldTypeDef.regEx)
}
break

case Number:
case SimpleSchema.Integer:
itemSchema = { type: fieldTypeDef.type === Number ? 'number' : 'integer' }
if (fieldTypeDef.max !== undefined && typeof fieldTypeDef.max !== 'function') {
if (fieldTypeDef.exclusiveMax === true) {
itemSchema.exclusiveMaximum = fieldTypeDef.max as number
} else {
itemSchema.maximum = fieldTypeDef.max as number
}
}
if (fieldTypeDef.min !== undefined && typeof fieldTypeDef.min !== 'function') {
if (fieldTypeDef.exclusiveMin === true) {
itemSchema.exclusiveMinimum = fieldTypeDef.min as number
} else {
itemSchema.minimum = fieldTypeDef.min as number
}
}
break

case Boolean:
itemSchema = { type: 'boolean' }
break

case Date:
itemSchema = {
type: 'string',
format: 'date-time'
}
break

case Array:
itemSchema = toJSArray(ss, key, fieldDef)
break

case Object:
itemSchema = toJSObj(ss.getObjectSchema(key), fieldTypeDef.blackbox)
break

case SimpleSchema.Any:
// In JSONSchema an empty object means any type
itemSchema = {}
break

default:
if (SimpleSchema.isSimpleSchema(fieldTypeDef.type)) {
itemSchema = toJSObj(fieldTypeDef.type as SimpleSchema, fieldTypeDef.blackbox)
} else if (
// support custom objects
fieldTypeDef.type instanceof Function
) {
itemSchema = toJSObj(ss.getObjectSchema(key), fieldTypeDef.blackbox)
}
break
}

if (itemSchema != null && fieldTypeDef.defaultValue !== undefined) {
itemSchema.default = fieldTypeDef.defaultValue
}

if (itemSchema != null) itemSchemas.push(itemSchema)
}

if (itemSchemas.length > 1) {
return { anyOf: itemSchemas }
}

return itemSchemas[0] ?? null
}

/**
* Convert a SimpleSchema to a JSONSchema Document.
*
* Notes:
* - Date fields will become string fields with built-in 'date-time' format.
* - JSONSchema does not support minimum or maximum values for date fields
* - Custom validators are ignored
* - Field definition properties that are a function are ignored
* - Custom objects are treated as regular objects
*
* @param simpleSchema SimpleSchema instance to convert
* @param id Optional ID to use for the `$id` field
* @returns JSONSchema Document
*/
export function toJsonSchema (simpleSchema: SimpleSchema, id?: string): JSONSchema7 {
return {
...(id != null ? { $id: id } : {}),
$schema: jsonSchemaVersion,
...toJSObj(simpleSchema)
}
}
Loading

0 comments on commit e356fcd

Please sign in to comment.