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 | // const error: KeywordErrorDefinition = {
|
---|
56 | // message: "should NOT have additional properties",
|
---|
57 | // params: ({params}) => _`{additionalProperty: ${params.additionalProperty}}`,
|
---|
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 | // TODO maybe an option instead of hard-coded 8?
|
---|
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
|
---|