1 | import type {
|
2 | CodeKeywordDefinition,
|
3 | ErrorObject,
|
4 | KeywordErrorDefinition,
|
5 | SchemaObject,
|
6 | } from "../../types"
|
7 | import type {KeywordCxt} from "../../compile/validate"
|
8 | import {propertyInData, allSchemaProperties, isOwnProperty} from "../code"
|
9 | import {alwaysValidSchema, schemaRefOrVal} from "../../compile/util"
|
10 | import {_, and, not, Code, Name} from "../../compile/codegen"
|
11 | import {checkMetadata} from "./metadata"
|
12 | import {checkNullableObject} from "./nullable"
|
13 | import {typeErrorMessage, typeErrorParams, _JTDTypeError} from "./error"
|
14 |
|
15 | enum PropError {
|
16 | Additional = "additional",
|
17 | Missing = "missing",
|
18 | }
|
19 |
|
20 | type PropKeyword = "properties" | "optionalProperties"
|
21 |
|
22 | type PropSchema = {[P in string]?: SchemaObject}
|
23 |
|
24 | export type JTDPropertiesError =
|
25 | | _JTDTypeError<PropKeyword, "object", PropSchema>
|
26 | | ErrorObject<PropKeyword, {error: PropError.Additional; additionalProperty: string}, PropSchema>
|
27 | | ErrorObject<PropKeyword, {error: PropError.Missing; missingProperty: string}, PropSchema>
|
28 |
|
29 | export const error: KeywordErrorDefinition = {
|
30 | message: (cxt) => {
|
31 | const {params} = cxt
|
32 | return params.propError
|
33 | ? params.propError === PropError.Additional
|
34 | ? "must NOT have additional properties"
|
35 | : `must have property '${params.missingProperty}'`
|
36 | : typeErrorMessage(cxt, "object")
|
37 | },
|
38 | params: (cxt) => {
|
39 | const {params} = cxt
|
40 | return params.propError
|
41 | ? params.propError === PropError.Additional
|
42 | ? _`{error: ${params.propError}, additionalProperty: ${params.additionalProperty}}`
|
43 | : _`{error: ${params.propError}, missingProperty: ${params.missingProperty}}`
|
44 | : typeErrorParams(cxt, "object")
|
45 | },
|
46 | }
|
47 |
|
48 | const def: CodeKeywordDefinition = {
|
49 | keyword: "properties",
|
50 | schemaType: "object",
|
51 | error,
|
52 | code: validateProperties,
|
53 | }
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 | export function validateProperties(cxt: KeywordCxt): void {
|
61 | checkMetadata(cxt)
|
62 | const {gen, data, parentSchema, it} = cxt
|
63 | const {additionalProperties, nullable} = parentSchema
|
64 | if (it.jtdDiscriminator && nullable) throw new Error("JTD: nullable inside discriminator mapping")
|
65 | if (commonProperties()) {
|
66 | throw new Error("JTD: properties and optionalProperties have common members")
|
67 | }
|
68 | const [allProps, properties] = schemaProperties("properties")
|
69 | const [allOptProps, optProperties] = schemaProperties("optionalProperties")
|
70 | if (properties.length === 0 && optProperties.length === 0 && additionalProperties) {
|
71 | return
|
72 | }
|
73 |
|
74 | const [valid, cond] =
|
75 | it.jtdDiscriminator === undefined
|
76 | ? checkNullableObject(cxt, data)
|
77 | : [gen.let("valid", false), true]
|
78 | gen.if(cond, () =>
|
79 | gen.assign(valid, true).block(() => {
|
80 | validateProps(properties, "properties", true)
|
81 | validateProps(optProperties, "optionalProperties")
|
82 | if (!additionalProperties) validateAdditional()
|
83 | })
|
84 | )
|
85 | cxt.pass(valid)
|
86 |
|
87 | function commonProperties(): boolean {
|
88 | const props = parentSchema.properties as Record<string, any> | undefined
|
89 | const optProps = parentSchema.optionalProperties as Record<string, any> | undefined
|
90 | if (!(props && optProps)) return false
|
91 | for (const p in props) {
|
92 | if (Object.prototype.hasOwnProperty.call(optProps, p)) return true
|
93 | }
|
94 | return false
|
95 | }
|
96 |
|
97 | function schemaProperties(keyword: string): [string[], string[]] {
|
98 | const schema = parentSchema[keyword]
|
99 | const allPs = schema ? allSchemaProperties(schema) : []
|
100 | if (it.jtdDiscriminator && allPs.some((p) => p === it.jtdDiscriminator)) {
|
101 | throw new Error(`JTD: discriminator tag used in ${keyword}`)
|
102 | }
|
103 | const ps = allPs.filter((p) => !alwaysValidSchema(it, schema[p]))
|
104 | return [allPs, ps]
|
105 | }
|
106 |
|
107 | function validateProps(props: string[], keyword: string, required?: boolean): void {
|
108 | const _valid = gen.var("valid")
|
109 | for (const prop of props) {
|
110 | gen.if(
|
111 | propertyInData(gen, data, prop, it.opts.ownProperties),
|
112 | () => applyPropertySchema(prop, keyword, _valid),
|
113 | () => missingProperty(prop)
|
114 | )
|
115 | cxt.ok(_valid)
|
116 | }
|
117 |
|
118 | function missingProperty(prop: string): void {
|
119 | if (required) {
|
120 | gen.assign(_valid, false)
|
121 | cxt.error(false, {propError: PropError.Missing, missingProperty: prop}, {schemaPath: prop})
|
122 | } else {
|
123 | gen.assign(_valid, true)
|
124 | }
|
125 | }
|
126 | }
|
127 |
|
128 | function applyPropertySchema(prop: string, keyword: string, _valid: Name): void {
|
129 | cxt.subschema(
|
130 | {
|
131 | keyword,
|
132 | schemaProp: prop,
|
133 | dataProp: prop,
|
134 | },
|
135 | _valid
|
136 | )
|
137 | }
|
138 |
|
139 | function validateAdditional(): void {
|
140 | gen.forIn("key", data, (key: Name) => {
|
141 | const _allProps =
|
142 | it.jtdDiscriminator === undefined ? allProps : [it.jtdDiscriminator].concat(allProps)
|
143 | const addProp = isAdditional(key, _allProps, "properties")
|
144 | const addOptProp = isAdditional(key, allOptProps, "optionalProperties")
|
145 | const extra =
|
146 | addProp === true ? addOptProp : addOptProp === true ? addProp : and(addProp, addOptProp)
|
147 | gen.if(extra, () => {
|
148 | if (it.opts.removeAdditional) {
|
149 | gen.code(_`delete ${data}[${key}]`)
|
150 | } else {
|
151 | cxt.error(
|
152 | false,
|
153 | {propError: PropError.Additional, additionalProperty: key},
|
154 | {instancePath: key, parentSchema: true}
|
155 | )
|
156 | if (!it.opts.allErrors) gen.break()
|
157 | }
|
158 | })
|
159 | })
|
160 | }
|
161 |
|
162 | function isAdditional(key: Name, props: string[], keyword: string): Code | true {
|
163 | let additional: Code | boolean
|
164 | if (props.length > 8) {
|
165 |
|
166 | const propsSchema = schemaRefOrVal(it, parentSchema[keyword], keyword)
|
167 | additional = not(isOwnProperty(gen, propsSchema as Code, key))
|
168 | } else if (props.length) {
|
169 | additional = and(...props.map((p) => _`${key} !== ${p}`))
|
170 | } else {
|
171 | additional = true
|
172 | }
|
173 | return additional
|
174 | }
|
175 | }
|
176 |
|
177 | export default def
|