Skip to content
This repository was archived by the owner on Jun 10, 2024. It is now read-only.

Commit d603636

Browse files
committed
New: Subschema support
1 parent ba57ffd commit d603636

File tree

3 files changed

+183
-6
lines changed

3 files changed

+183
-6
lines changed

‎README.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ const schema = new ObjectSchema({
142142
});
143143
```
144144

145+
### Subschemas
146+
147+
If you are defining a key that is, itself, an object, you can simplify the process by using a subschema. Instead of defining `merge()` and `validate()`, assign a `schema` key that contains a schema definition, like this:
148+
149+
```js
150+
const schema = new ObjectSchema({
151+
name: {
152+
schema: {
153+
first: {
154+
merge: "replace",
155+
validate: "string"
156+
},
157+
last: {
158+
merge: "replace",
159+
validate: "string"
160+
}
161+
}
162+
}
163+
});
164+
165+
schema.validate({
166+
name: {
167+
first: "n",
168+
last: "z"
169+
}
170+
});
171+
```
172+
145173
### Remove Keys During Merge
146174

147175
If the merge strategy for a key returns `undefined`, then the key will not appear in the final object. For example:

‎src/object-schema.js‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,28 @@ const requiredKeys = Symbol("requiredKeys");
3636
*/
3737
function validateDefinition(name, strategy) {
3838

39+
let hasSchema = false;
40+
if (strategy.schema) {
41+
if (typeof strategy.schema === "object") {
42+
hasSchema = true;
43+
} else {
44+
throw new TypeError("Schema must be an object.");
45+
}
46+
}
47+
3948
if (typeof strategy.merge === "string") {
4049
if (!(strategy.merge in MergeStrategy)) {
4150
throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);
4251
}
43-
} else if (typeof strategy.merge !== "function") {
52+
} else if (!hasSchema && typeof strategy.merge !== "function") {
4453
throw new TypeError(`Definition for key "${name}" must have a merge property.`);
4554
}
4655

4756
if (typeof strategy.validate === "string") {
4857
if (!(strategy.validate in ValidationStrategy)) {
4958
throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);
5059
}
51-
} else if (typeof strategy.validate !== "function") {
60+
} else if (!hasSchema && typeof strategy.validate !== "function") {
5261
throw new TypeError(`Definition for key "${name}" must have a validate() method.`);
5362
}
5463
}
@@ -90,6 +99,25 @@ class ObjectSchema {
9099
for (const key of Object.keys(definitions)) {
91100
validateDefinition(key, definitions[key]);
92101

102+
// normalize merge and validate methods if subschema is present
103+
if (typeof definitions[key].schema === "object") {
104+
const schema = new ObjectSchema(definitions[key].schema);
105+
definitions[key] = {
106+
...definitions[key],
107+
merge(first, second) {
108+
if (first && second) {
109+
return schema.merge(first, second);
110+
}
111+
112+
return MergeStrategy.assign(first, second);
113+
},
114+
validate(value) {
115+
ValidationStrategy.object(value);
116+
schema.validate(value);
117+
}
118+
};
119+
}
120+
93121
// normalize the merge method in case there's a string
94122
if (typeof definitions[key].merge === "string") {
95123
definitions[key] = {
@@ -98,6 +126,14 @@ class ObjectSchema {
98126
};
99127
};
100128

129+
// normalize the validate method in case there's a string
130+
if (typeof definitions[key].validate === "string") {
131+
definitions[key] = {
132+
...definitions[key],
133+
validate: ValidationStrategy[definitions[key].validate]
134+
};
135+
};
136+
101137
this[strategies].set(key, definitions[key]);
102138

103139
if (definitions[key].required) {
@@ -183,10 +219,7 @@ class ObjectSchema {
183219

184220
// now apply remaining validation strategy
185221
try {
186-
const validate = (typeof strategy.validate === "string")
187-
? ValidationStrategy[strategy.validate]
188-
: strategy.validate;
189-
validate.call(strategy, object[key]);
222+
strategy.validate.call(strategy, object[key]);
190223
} catch (ex) {
191224
ex.message = `Key "${key}": ` + ex.message;
192225
throw ex;

‎tests/object-schema.js‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,68 @@ describe("ObjectSchema", () => {
234234
assert.strictEqual(result.foo.baz, false);
235235
});
236236

237+
it("should call the merge strategy when there's a subschema", () => {
238+
239+
schema = new ObjectSchema({
240+
name: {
241+
schema: {
242+
first: {
243+
merge: "replace",
244+
validate: "string"
245+
},
246+
last: {
247+
merge: "replace",
248+
validate: "string"
249+
}
250+
}
251+
}
252+
});
253+
254+
const result = schema.merge({
255+
name: {
256+
first: "n",
257+
last: "z"
258+
}
259+
}, {
260+
name: {
261+
first: "g"
262+
}
263+
});
264+
265+
assert.strictEqual(result.name.first, "g");
266+
assert.strictEqual(result.name.last, "z");
267+
});
268+
269+
it("should not error when calling the merge strategy when there's a subschema and no matching key in second object", () => {
270+
271+
schema = new ObjectSchema({
272+
name: {
273+
schema: {
274+
first: {
275+
merge: "replace",
276+
validate: "string"
277+
},
278+
last: {
279+
merge: "replace",
280+
validate: "string"
281+
}
282+
}
283+
}
284+
});
285+
286+
const result = schema.merge({
287+
name: {
288+
first: "n",
289+
last: "z"
290+
}
291+
}, {
292+
});
293+
294+
assert.strictEqual(result.name.first, "n");
295+
assert.strictEqual(result.name.last, "z");
296+
});
297+
298+
237299
});
238300

239301
describe("validate()", () => {
@@ -407,6 +469,60 @@ describe("ObjectSchema", () => {
407469
}, /Missing required key "foo"/);
408470
});
409471

472+
it("should throw an error when a subschema is provided and the value doesn't validate", () => {
473+
474+
schema = new ObjectSchema({
475+
name: {
476+
schema: {
477+
first: {
478+
merge: "replace",
479+
validate: "string"
480+
},
481+
last: {
482+
merge: "replace",
483+
validate: "string"
484+
}
485+
}
486+
}
487+
});
488+
489+
assert.throws(() => {
490+
schema.validate({
491+
name: {
492+
first: 123,
493+
last: "z"
494+
}
495+
});
496+
497+
}, /Key "name": Key "first": Expected a string/);
498+
});
499+
500+
it("should not throw an error when a subschema is provided and the value validates", () => {
501+
502+
schema = new ObjectSchema({
503+
name: {
504+
schema: {
505+
first: {
506+
merge: "replace",
507+
validate: "string"
508+
},
509+
last: {
510+
merge: "replace",
511+
validate: "string"
512+
}
513+
}
514+
}
515+
});
516+
517+
schema.validate({
518+
name: {
519+
first: "n",
520+
last: "z"
521+
}
522+
});
523+
524+
});
525+
410526
});
411527

412528
});

0 commit comments

Comments
 (0)