Skip to content

Commit

Permalink
any.modify(). Closes #1950. Closes 1951
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jun 28, 2019
1 parent 29b3fca commit c51583f
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 435 deletions.
50 changes: 11 additions & 39 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- [`any.label(name)`](#anylabelname)
- [`any.message(message)`](#anymessagemessage)
- [`any.meta(meta)`](#anymetameta)
- [`any.modify(paths, adjuster)`](#anymodifypaths-adjuster)
- [`any.notes(notes)`](#anynotesnotes)
- [`any.optional()`](#anyoptional)
- [`any.prefs(options)` = aliases: `preferences`, `options`](#anyprefsoptions--aliases-preferences-options)
Expand Down Expand Up @@ -124,9 +125,6 @@
- [`object.unknown([allow])`](#objectunknownallow)
- [`object.instance(constructor, [name])`](#objectinstanceconstructor-name)
- [`object.schema([type])`](#objectschematype)
- [`object.requiredKeys(...children)`](#objectrequiredkeyschildren)
- [`object.optionalKeys(...children)`](#objectoptionalkeyschildren)
- [`object.forbiddenKeys(...children)`](#objectforbiddenkeyschildren)
- [`string` - inherits from `Any`](#string---inherits-from-any)
- [`string.insensitive()`](#stringinsensitive)
- [`string.min(limit, [encoding])`](#stringminlimit-encoding)
Expand Down Expand Up @@ -985,6 +983,16 @@ Attaches metadata to the key where:
const schema = Joi.any().meta({ index: true });
```

#### `any.modify(paths, adjuster)`

Returns a new schema where each of the path keys listed have been modified where:
- `paths` - an array of key strings, a single key string, or an array of arrays of pre-split
key strings. Key string paths use dot `.` to indicate key hierarchy.
- `adjuster` - a function using the signature `function(schema)` which must return a modified
schema. For example, `(schema) => schema.required()`.

The method does not modify the original schema.

#### `any.notes(notes)`

Annotates the key where:
Expand Down Expand Up @@ -2445,42 +2453,6 @@ const schema = Joi.object().schema();

Possible validation errors: [`object.schema`](#objectschema-1)

#### `object.requiredKeys(...children)`

Sets the specified children to required.
- `children` - the keys to specified as required.

```js
const schema = Joi.object({ a: { b: Joi.number() }, c: { d: Joi.string() } });
const requiredSchema = schema.requiredKeys('', 'a.b', 'c', 'c.d');
```

Note that in this example `''` means the current object, `a` is not required but `b` is, as well as `c` and `d`.

#### `object.optionalKeys(...children)`

Sets the specified children to optional.
- `children` - the keys to specified as optional.

```js
const schema = Joi.object({ a: { b: Joi.number().required() }, c: { d: Joi.string().required() } });
const optionalSchema = schema.optionalKeys('a.b', 'c.d');
```

The behavior is exactly the same as `requiredKeys`.

#### `object.forbiddenKeys(...children)`

Sets the specified children to forbidden.
- `children` - the keys specified as forbidden.

```js
const schema = Joi.object({ a: { b: Joi.number().required() }, c: { d: Joi.string().required() } });
const optionalSchema = schema.forbiddenKeys('a.b', 'c.d');
```

The behavior is exactly the same as `requiredKeys`.

### `string` - inherits from `Any`

Generates a schema object that matches a string data type. Note that empty strings are not allowed by default and must
Expand Down
45 changes: 40 additions & 5 deletions lib/manipulate.js → lib/modify.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports.Ids = internals.Ids = class {
}

Hoek.assert(!this._map.has(id), 'Schema already contains id', id);
this._map.set(id, schema);
this._map.set(id, { schema, key, id });
}

unregister(schema, key) {
Expand Down Expand Up @@ -66,10 +66,10 @@ exports.Ids = internals.Ids = class {

const forward = path.slice(1);
if (!forward.length) {
return node;
return node.schema;
}

return node._ids.reach(forward, [...behind, current]);
return node.schema._ids.reach(forward, [...behind, current]);
}

labels(path, behind = []) {
Expand All @@ -83,11 +83,46 @@ exports.Ids = internals.Ids = class {
}

const forward = path.slice(1);
behind = [...behind, node._flags.label || current];
behind = [...behind, node.schema._flags.label || current];
if (!forward.length) {
return behind.join('.');
}

return node._ids.labels(forward, behind);
return node.schema._ids.labels(forward, behind);
}

modify(path, adjuster, root) {

const chain = this._collect(path);
chain.push({ schema: root });
const tail = chain.shift();
let adjusted = { id: tail.id, schema: adjuster(tail.schema) };

Hoek.assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type');

for (const node of chain) {
adjusted = { id: node.id, schema: node.schema._override(adjusted.id, adjusted.schema) };
}

return adjusted.schema;
}

_collect(path, behind = [], nodes = []) {

Hoek.assert(!this._unsupported, this._unsupported);

const current = path[0];
const node = this._map.get(current);
Hoek.assert(node, 'Schema does not contain path', [...behind, ...path].join('.'));

nodes = [node, ...nodes];

const forward = path.slice(1);
if (!forward.length) {
return nodes;
}

Hoek.assert(typeof node.schema._override === 'function', 'Schema node', [...behind, ...path].join('.'), 'does not support manipulation');
return node.schema._ids._collect(forward, [...behind, current], nodes);
}
};
10 changes: 4 additions & 6 deletions lib/types/alternatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ internals.Alternatives = class extends Any {

for (let i = 0; i < schemas.length; ++i) {
const cast = Cast.schema(this._root, schemas[i]);
obj._register(cast, Ref.toSibling);
obj._addAlternative({ schema: cast });
}

Expand Down Expand Up @@ -157,11 +156,6 @@ internals.Alternatives = class extends Any {
}
}

obj._register(item.ref, Ref.toSibling);
obj._register(item.is, Ref.toSibling);
obj._register(item.then, Ref.toSibling);
obj._register(item.otherwise, Ref.toSibling);

if (item.then &&
item.otherwise) {

Expand Down Expand Up @@ -273,6 +267,10 @@ internals.Alternatives = class extends Any {

this._inner.matches.push(match);

for (const key of ['schema', 'ref', 'is', 'then', 'otherwise']) {
this._register(match[key], { family: Ref.toSibling });
}

// Flag when an alternative type is an array

for (const key of ['schema', 'then', 'otherwise']) {
Expand Down
17 changes: 14 additions & 3 deletions lib/types/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const About = require('../about');
const Cast = require('../cast');
const Common = require('../common');
const Errors = require('../errors');
const Manipulate = require('../manipulate');
const Messages = require('../messages');
const Modify = require('../modify');
const Ref = require('../ref');
const Validator = require('../validator');
const Values = require('../values');
Expand Down Expand Up @@ -42,7 +42,7 @@ module.exports = internals.Any = class {
constructor(type) {

this._type = type || 'any';
this._ids = new Manipulate.Ids(this);
this._ids = new Modify.Ids(this);
this._preferences = null;
this._refs = new Ref.Manager();

Expand Down Expand Up @@ -228,6 +228,17 @@ module.exports = internals.Any = class {
return this._flag('label', name);
}

modify(paths, adjuster) {

let obj = this; // eslint-disable-line consistent-this
for (let path of [].concat(paths)) {
path = Array.isArray(path) ? path : path.split('.');
obj = obj._ids.modify(path, adjuster, obj);
}

return obj;
}

message(message) {

return this.rule({ message });
Expand Down Expand Up @@ -661,7 +672,7 @@ module.exports = internals.Any = class {
return !Validator.validate(value, this, state, prefs).errors;
}

_register(schema, family, key) {
_register(schema, { family, key } = {}) {

this._refs.register(schema, family);
this._ids.register(schema, key);
Expand Down
112 changes: 11 additions & 101 deletions lib/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,13 +386,6 @@ internals.Object = class extends Any {
return obj;
}

forbiddenKeys(...children) {

Common.verifyFlat(children, 'forbiddenKeys');

return this._applyFunctionToChildren(children, 'forbidden');
}

instance(constructor, name = constructor.name) {

Hoek.assert(typeof constructor === 'function', 'type must be a constructor function');
Expand Down Expand Up @@ -436,7 +429,7 @@ internals.Object = class extends Any {
try {
const cast = Cast.schema(this._root, child);
topo.add({ key, schema: cast }, { after: cast._refs.roots(), group: key });
obj._register(cast, undefined, key);
obj._register(cast, { key });
}
catch (err) {
if (err.path !== undefined) {
Expand Down Expand Up @@ -476,13 +469,6 @@ internals.Object = class extends Any {
return this._dependency('nand', null, peers);
}

optionalKeys(...children) {

Common.verifyFlat(children, 'optionalKeys');

return this._applyFunctionToChildren(children, 'optional');
}

or(...peers /*, [options] */) {

Common.verifyFlat(peers, 'or');
Expand Down Expand Up @@ -558,13 +544,6 @@ internals.Object = class extends Any {
return obj;
}

requiredKeys(...children) {

Common.verifyFlat(children, 'requiredKeys');

return this._applyFunctionToChildren(children, 'required');
}

schema(type = 'any') {

return this._rule('schema', { args: { type } });
Expand Down Expand Up @@ -592,49 +571,6 @@ internals.Object = class extends Any {
return this._dependency('xor', null, peers);
}

// Helpers

_applyFunctionToChildren(children, fn, root) {

children = [].concat(children);
Hoek.assert(children.length, 'expected at least one children');

const groupedChildren = internals.groupChildren(children);
let obj;

if ('' in groupedChildren) {
obj = this[fn]();
delete groupedChildren[''];
}
else {
obj = this.clone();
}

if (obj._inner.children) {
root = root ? root + '.' : '';

for (let i = 0; i < obj._inner.children.length; ++i) {
const child = obj._inner.children[i];
const group = groupedChildren[child.key];

if (group) {
obj._inner.children[i] = {
key: child.key,
_refs: child._refs,
schema: internals.applyToChild(child.schema, group, fn, root + child.key)
};

delete groupedChildren[child.key];
}
}
}

const remaining = Object.keys(groupedChildren);
Hoek.assert(remaining.length === 0, `unknown key${remaining.length > 1 ? 's' : ''}`, remaining.join(', '));

return obj;
}

// Internals

_dependency(type, key, peers, options) {
Expand Down Expand Up @@ -694,6 +630,16 @@ internals.Object = class extends Any {
return this._rule(name, { rule: 'length', refs, args: { limit }, operator });
}

_override(id, schema) {

for (const child of this._inner.children) {
const childId = child.schema._flags.id || child.key;
if (id === childId) {
return this.keys({ [child.key]: schema });
}
}
}

_rename(value, state, prefs, errors) {

const renamed = {};
Expand Down Expand Up @@ -858,23 +804,6 @@ Common.extend(internals.Object, 'rules', {

// Helpers

internals.groupChildren = function (children) {

children.sort();

const grouped = {};

for (const child of children) {
Hoek.assert(typeof child === 'string', 'children must be strings');
const group = child.split('.')[0];
const childGroup = grouped[group] = grouped[group] || [];
childGroup.push(child.substring(group.length + 1));
}

return grouped;
};


internals.keysToLabels = function (schema, keys) {

if (Array.isArray(keys)) {
Expand Down Expand Up @@ -1085,23 +1014,4 @@ internals.clone = function (value, prefs) {
};


internals.applyToChild = function (schema, children, fn, root) {

if (schema.type === 'object') {
return schema._applyFunctionToChildren(children, fn, root);
}

if (children.length === 1) {
return schema[fn]();
}

if (children[0] === '') {
children = children.slice(1);
}

const unknowns = children.map((child) => root + '.' + child);
throw new Error(`unknown key${unknowns.length > 1 ? 's' : ''} ${unknowns.join(', ')}`);
};


module.exports = new internals.Object();

0 comments on commit c51583f

Please sign in to comment.