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

Commit

Permalink
New: Subschema support
Browse files Browse the repository at this point in the history
  • Loading branch information
nzakas committed Mar 20, 2021
1 parent ba57ffd commit d603636
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 6 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ const schema = new ObjectSchema({
});
```

### Subschemas

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:

```js
const schema = new ObjectSchema({
name: {
schema: {
first: {
merge: "replace",
validate: "string"
},
last: {
merge: "replace",
validate: "string"
}
}
}
});

schema.validate({
name: {
first: "n",
last: "z"
}
});
```

### Remove Keys During Merge

If the merge strategy for a key returns `undefined`, then the key will not appear in the final object. For example:
Expand Down
45 changes: 39 additions & 6 deletions src/object-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,28 @@ const requiredKeys = Symbol("requiredKeys");
*/
function validateDefinition(name, strategy) {

let hasSchema = false;
if (strategy.schema) {
if (typeof strategy.schema === "object") {
hasSchema = true;
} else {
throw new TypeError("Schema must be an object.");
}
}

if (typeof strategy.merge === "string") {
if (!(strategy.merge in MergeStrategy)) {
throw new TypeError(`Definition for key "${name}" missing valid merge strategy.`);
}
} else if (typeof strategy.merge !== "function") {
} else if (!hasSchema && typeof strategy.merge !== "function") {
throw new TypeError(`Definition for key "${name}" must have a merge property.`);
}

if (typeof strategy.validate === "string") {
if (!(strategy.validate in ValidationStrategy)) {
throw new TypeError(`Definition for key "${name}" missing valid validation strategy.`);
}
} else if (typeof strategy.validate !== "function") {
} else if (!hasSchema && typeof strategy.validate !== "function") {
throw new TypeError(`Definition for key "${name}" must have a validate() method.`);
}
}
Expand Down Expand Up @@ -90,6 +99,25 @@ class ObjectSchema {
for (const key of Object.keys(definitions)) {
validateDefinition(key, definitions[key]);

// normalize merge and validate methods if subschema is present
if (typeof definitions[key].schema === "object") {
const schema = new ObjectSchema(definitions[key].schema);
definitions[key] = {
...definitions[key],
merge(first, second) {
if (first && second) {
return schema.merge(first, second);
}

return MergeStrategy.assign(first, second);
},
validate(value) {
ValidationStrategy.object(value);
schema.validate(value);
}
};
}

// normalize the merge method in case there's a string
if (typeof definitions[key].merge === "string") {
definitions[key] = {
Expand All @@ -98,6 +126,14 @@ class ObjectSchema {
};
};

// normalize the validate method in case there's a string
if (typeof definitions[key].validate === "string") {
definitions[key] = {
...definitions[key],
validate: ValidationStrategy[definitions[key].validate]
};
};

this[strategies].set(key, definitions[key]);

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

// now apply remaining validation strategy
try {
const validate = (typeof strategy.validate === "string")
? ValidationStrategy[strategy.validate]
: strategy.validate;
validate.call(strategy, object[key]);
strategy.validate.call(strategy, object[key]);
} catch (ex) {
ex.message = `Key "${key}": ` + ex.message;
throw ex;
Expand Down
116 changes: 116 additions & 0 deletions tests/object-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,68 @@ describe("ObjectSchema", () => {
assert.strictEqual(result.foo.baz, false);
});

it("should call the merge strategy when there's a subschema", () => {

schema = new ObjectSchema({
name: {
schema: {
first: {
merge: "replace",
validate: "string"
},
last: {
merge: "replace",
validate: "string"
}
}
}
});

const result = schema.merge({
name: {
first: "n",
last: "z"
}
}, {
name: {
first: "g"
}
});

assert.strictEqual(result.name.first, "g");
assert.strictEqual(result.name.last, "z");
});

it("should not error when calling the merge strategy when there's a subschema and no matching key in second object", () => {

schema = new ObjectSchema({
name: {
schema: {
first: {
merge: "replace",
validate: "string"
},
last: {
merge: "replace",
validate: "string"
}
}
}
});

const result = schema.merge({
name: {
first: "n",
last: "z"
}
}, {
});

assert.strictEqual(result.name.first, "n");
assert.strictEqual(result.name.last, "z");
});


});

describe("validate()", () => {
Expand Down Expand Up @@ -407,6 +469,60 @@ describe("ObjectSchema", () => {
}, /Missing required key "foo"/);
});

it("should throw an error when a subschema is provided and the value doesn't validate", () => {

schema = new ObjectSchema({
name: {
schema: {
first: {
merge: "replace",
validate: "string"
},
last: {
merge: "replace",
validate: "string"
}
}
}
});

assert.throws(() => {
schema.validate({
name: {
first: 123,
last: "z"
}
});

}, /Key "name": Key "first": Expected a string/);
});

it("should not throw an error when a subschema is provided and the value validates", () => {

schema = new ObjectSchema({
name: {
schema: {
first: {
merge: "replace",
validate: "string"
},
last: {
merge: "replace",
validate: "string"
}
}
}
});

schema.validate({
name: {
first: "n",
last: "z"
}
});

});

});

});

0 comments on commit d603636

Please sign in to comment.