Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix maybe behavior with object type #55932

Merged
merged 11 commits into from
Feb 14, 2020
1 change: 1 addition & 0 deletions packages/kbn-config-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ __Options:__
* `defaultValue: TObject | Reference<TObject> | (() => TObject)` - defines a default value, see [Default values](#default-values) section for more details.
* `validate: (value: TObject) => string | void` - defines a custom validator function, see [Custom validation](#custom-validation) section for more details.
* `allowUnknowns: boolean` - indicates whether unknown object properties should be allowed. It's `false` by default.
* `applyDefaults: boolean` - indicates whether object property schema `defaultValue` should be used if not specified. It's `true` by default.

__Usage:__
```typescript
Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions packages/kbn-config-schema/src/types/duration_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('#defaultValue', () => {
source: duration({ defaultValue: 600 }),
target: duration({ defaultValue: siblingRef('source') }),
fromContext: duration({ defaultValue: contextRef('val') }),
}).validate(undefined, { val: momentDuration(700, 'ms') })
}).validate({}, { val: momentDuration(700, 'ms') })
).toMatchInlineSnapshot(`
Object {
"fromContext": "PT0.7S",
Expand All @@ -115,7 +115,7 @@ Object {
source: duration({ defaultValue: '1h' }),
target: duration({ defaultValue: siblingRef('source') }),
fromContext: duration({ defaultValue: contextRef('val') }),
}).validate(undefined, { val: momentDuration(2, 'hour') })
}).validate({}, { val: momentDuration(2, 'hour') })
).toMatchInlineSnapshot(`
Object {
"fromContext": "PT2H",
Expand All @@ -129,7 +129,7 @@ Object {
source: duration({ defaultValue: momentDuration(1, 'hour') }),
target: duration({ defaultValue: siblingRef('source') }),
fromContext: duration({ defaultValue: contextRef('val') }),
}).validate(undefined, { val: momentDuration(2, 'hour') })
}).validate({}, { val: momentDuration(2, 'hour') })
).toMatchInlineSnapshot(`
Object {
"fromContext": "PT2H",
Expand Down
38 changes: 38 additions & 0 deletions packages/kbn-config-schema/src/types/maybe_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,41 @@ test('includes namespace in failure', () => {
const type = schema.maybe(schema.string());
expect(() => type.validate(null, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot();
});

describe('maybe + object', () => {
test('returns undefined if undefined object', () => {
const type = schema.maybe(schema.object({}));
expect(type.validate(undefined)).toEqual(undefined);
});
joshdover marked this conversation as resolved.
Show resolved Hide resolved

test('returns undefined if undefined object with no defaults', () => {
const type = schema.maybe(
schema.object({
type: schema.string(),
id: schema.string(),
})
);

expect(type.validate(undefined)).toEqual(undefined);
});

test('returns empty object if maybe keys', () => {
const type = schema.object({
name: schema.maybe(schema.string()),
});
expect(type.validate({})).toEqual({});
});

test('returns empty object if maybe nested object', () => {
const type = schema.object({
name: schema.maybe(
schema.object({
type: schema.string(),
id: schema.string(),
})
),
});

expect(type.validate({})).toEqual({});
});
});
2 changes: 1 addition & 1 deletion packages/kbn-config-schema/src/types/maybe_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class MaybeType<V> extends Type<V | undefined> {
type
.getSchema()
.optional()
.default()
.default(() => undefined, 'undefined')
);
}
}
188 changes: 177 additions & 11 deletions packages/kbn-config-schema/src/types/object_type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ test('returns value by default', () => {
expect(type.validate(value)).toEqual({ name: 'test' });
});

test('returns empty object if undefined', () => {
const type = schema.object({});
expect(type.validate(undefined)).toEqual({});
joshdover marked this conversation as resolved.
Show resolved Hide resolved
});

test('fails if missing required value', () => {
const type = schema.object({
name: schema.string(),
});
const value = {};

expect(() => type.validate(value)).toThrowErrorMatchingSnapshot();
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
joshdover marked this conversation as resolved.
Show resolved Hide resolved
`"[name]: expected value of type [string] but got [undefined]"`
);
});

test('returns value if undefined string with default', () => {
Expand All @@ -57,7 +64,9 @@ test('fails if key does not exist in schema', () => {
foo: 'bar',
};

expect(() => type.validate(value)).toThrowErrorMatchingSnapshot();
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[bar]: definition for this key is missing"`
);
});

test('defined object within object', () => {
Expand All @@ -81,22 +90,168 @@ test('undefined object within object', () => {
}),
});

expect(type.validate(undefined)).toEqual({
foo: {
bar: 'hello world',
},
});

expect(type.validate({})).toEqual({
foo: {
bar: 'hello world',
},
});

expect(type.validate({ foo: {} })).toEqual({
foo: {
bar: 'hello world',
},
});
});

test('object within object with required', () => {
test('object within object with key without defaultValue', () => {
const type = schema.object({
foo: schema.object({
bar: schema.string(),
}),
});
const value = { foo: {} };

expect(() => type.validate(value)).toThrowErrorMatchingSnapshot();
expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"[foo.bar]: expected value of type [string] but got [undefined]"`
);
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[foo.bar]: expected value of type [string] but got [undefined]"`
);
});

describe('applyDefaults: false', () => {
test('returns value by default', () => {
const type = schema.object(
{
name: schema.string(),
},
{ applyDefaults: false }
);
const value = {
name: 'test',
};

expect(type.validate(value)).toEqual({ name: 'test' });
});

test('fails if undefined', () => {
const type = schema.object({}, { applyDefaults: false });
expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [undefined] instead."`
);
});

test('fails if missing required value', () => {
const type = schema.object(
{
name: schema.string(),
},
{ applyDefaults: false }
);
const value = {};

expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[name]: expected value of type [string] but got [undefined]"`
);
});

test('returns value if undefined string with default', () => {
const type = schema.object(
{
name: schema.string({ defaultValue: 'test' }),
},
{ applyDefaults: false }
);
const value = {};

expect(type.validate(value)).toEqual({ name: 'test' });
});

test('fails if undefined string with default and undefined value', () => {
const type = schema.object(
{
name: schema.string({ defaultValue: 'test' }),
},
{ applyDefaults: false }
);

expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [undefined] instead."`
);
});

test('fails if key does not exist in schema', () => {
const type = schema.object(
{
foo: schema.string(),
},
{ applyDefaults: false }
);
const value = {
bar: 'baz',
foo: 'bar',
};

expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[bar]: definition for this key is missing"`
);
});

test('undefined object within object', () => {
const type = schema.object(
{
foo: schema.object(
{
bar: schema.string({ defaultValue: 'hello world' }),
},
{ applyDefaults: false }
),
},
{ applyDefaults: false }
);

expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [undefined] instead."`
);

expect(() => type.validate({})).toThrowErrorMatchingInlineSnapshot(
`"[foo]: expected a plain object value, but found [undefined] instead."`
);

expect(type.validate({ foo: {} })).toEqual({
foo: {
bar: 'hello world',
},
});
});

test('object within object with key without defaultValue', () => {
const type = schema.object(
{
foo: schema.object(
{
bar: schema.string(),
},
{ applyDefaults: false }
),
},
{ applyDefaults: false }
);
const value = { foo: {} };

expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [undefined] instead."`
);
expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[foo.bar]: expected value of type [string] but got [undefined]"`
);
});
});

describe('#validate', () => {
Expand Down Expand Up @@ -127,8 +282,12 @@ describe('#validate', () => {
test('called with wrong type', () => {
const type = schema.object({});

expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot();
expect(() => type.validate(123)).toThrowErrorMatchingSnapshot();
expect(() => type.validate('foo')).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [string] instead."`
);
expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot(
`"expected a plain object value, but found [number] instead."`
);
});

test('handles oneOf', () => {
Expand All @@ -137,7 +296,10 @@ test('handles oneOf', () => {
});

expect(type.validate({ key: 'foo' })).toEqual({ key: 'foo' });
expect(() => type.validate({ key: 123 })).toThrowErrorMatchingSnapshot();
expect(() => type.validate({ key: 123 })).toThrowErrorMatchingInlineSnapshot(`
"[key]: types that failed validation:
- [key.0]: expected value of type [string] but got [number]"
`);
});

test('handles references', () => {
Expand Down Expand Up @@ -186,7 +348,9 @@ test('includes namespace in failure when wrong top-level type', () => {
foo: schema.string(),
});

expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingSnapshot();
expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(
`"[foo-namespace]: expected a plain object value, but found [Array] instead."`
);
});

test('includes namespace in failure when wrong value type', () => {
Expand All @@ -197,7 +361,9 @@ test('includes namespace in failure when wrong value type', () => {
foo: 123,
};

expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot();
expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot(
`"[foo-namespace.foo]: expected value of type [string] but got [number]"`
);
});

test('individual keys can validated', () => {
Expand Down Expand Up @@ -241,7 +407,7 @@ test('allowUnknowns = true affects only own keys', () => {
baz: 'baz',
},
})
).toThrowErrorMatchingSnapshot();
).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`);
});

test('does not allow unknown keys when allowUnknowns = false', () => {
Expand All @@ -253,5 +419,5 @@ test('does not allow unknown keys when allowUnknowns = false', () => {
type.validate({
bar: 'baz',
})
).toThrowErrorMatchingSnapshot();
).toThrowErrorMatchingInlineSnapshot(`"[bar]: definition for this key is missing"`);
});
Loading