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

add strictObject decoder #58

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/combinators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export const constant = Decoder.constant;
/** See `Decoder.object` */
export const object = Decoder.object;

/** See `Decoder.strictObject` */
export const strictObject = Decoder.strictObject;

/** See `Decoder.array` */
export const array = Decoder.array;

Expand Down
37 changes: 37 additions & 0 deletions src/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,43 @@ export class Decoder<A> {
});
}

static strictObject<A>(decoders?: DecoderObject<A>) {
const decoderKeys = isJsonObject(decoders) ? Object.keys(decoders) : [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to decode decoders do we? Can just be

const decoderKeys = decoders ? Object.keys(decoders) : [];

Can we make decoders a required field? Then it would be even simpler:

const decoderKeys = Object.keys(decoders);

return new Decoder((json: unknown) => {
const invalidKeys = isJsonObject(json)
? Object.keys(json).filter((key: string) => !decoderKeys.some((validKey: string) => key === validKey))
: [];

if (invalidKeys.length > 0) {
return Result.err({message: `the following keys are present but not expected: ${invalidKeys.join(", ")}`});
}
Comment on lines +298 to +305

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be better to represent this decoder as the intersection of the standard object decoder and another no extra keys decoder. Avoid copying the contents of that decoder into this one.


if (isJsonObject(json) && decoders) {
let obj: any = {};
for (const key in decoders) {
if (decoders.hasOwnProperty(key)) {
const r = decoders[key].decode(json[key]);
if (r.ok === true) {
// tslint:disable-next-line:strict-type-predicates
if (r.result !== undefined) {
obj[key] = r.result;
}
} else if (json[key] === undefined) {
return Result.err({message: `the key '${key}' is required but was not present`});
} else {
return Result.err(prependAt(`.${key}`, r.error));
}
}
}
return Result.ok(obj);
} else if (isJsonObject(json)) {
return Result.ok(json);
} else {
return Result.err({message: expectedGot('an object', json)});
}
});
}

/**
* Decoder for json arrays. Runs `decoder` on each array element, and succeeds
* if all elements are successfully decoded. If no `decoder` argument is
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
unknownJson,
constant,
object,
strictObject,
array,
tuple,
dict,
Expand Down
104 changes: 104 additions & 0 deletions test/json-decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
unknownJson,
constant,
object,
strictObject,
array,
dict,
optional,
Expand Down Expand Up @@ -293,6 +294,109 @@ describe('object', () => {
});
});

describe('strictObject', () => {
describe('when given JSON with extra keys not defined on decoder', () => {
it('returns an error result for the extra keys', () => {
const decoder = strictObject({ a: number() });

expect(decoder.run({
a: 5,
b: 6
})).toMatchObject({
ok: false,
error: {
message: 'the following keys are present but not expected: b',
at: 'input',
input: {a: 5, b: 6},
kind: 'DecoderError'
}
});
});
});

describe('when given valid JSON', () => {
it('can decode a simple object', () => {
const decoder = strictObject({x: number()});

expect(decoder.run({x: 5})).toMatchObject({ok: true, result: {x: 5}});
});

it('can decode a nested object', () => {
const decoder = object({
payload: strictObject({x: number(), y: number()}),
error: constant(false)
});
const json = {payload: {x: 5, y: 2}, error: false};

expect(decoder.run(json)).toEqual({ok: true, result: json});
});
});

describe('when given incorrect JSON', () => {
it('fails when not given an object', () => {
const decoder = strictObject({x: number()});

expect(decoder.run('true')).toMatchObject({
ok: false,
error: {at: 'input', message: 'expected an object, got a string'}
});
});

it('fails when given an array', () => {
const decoder = strictObject({x: number()});

expect(decoder.run([])).toMatchObject({
ok: false,
error: {at: 'input', message: 'expected an object, got an array'}
});
});

it('reports a missing key', () => {
const decoder = strictObject({x: number()});

expect(decoder.run({})).toMatchObject({
ok: false,
error: {at: 'input', message: "the key 'x' is required but was not present"}
});
});

it('reports invalid values', () => {
const decoder = strictObject({name: string()});

expect(decoder.run({name: 5})).toMatchObject({
ok: false,
error: {at: 'input.name', message: 'expected a string, got a number'}
});
});

it('properly displays nested errors', () => {
const decoder = strictObject({
hello: strictObject({
hey: strictObject({
'Howdy!': string()
})
})
});

const error = decoder.run({hello: {hey: {'Howdy!': {}}}});
expect(error).toMatchObject({
ok: false,
error: {at: 'input.hello.hey.Howdy!', message: 'expected a string, got an object'}
});
});
});

it('ignores optional fields that decode to undefined', () => {
const decoder = strictObject({
a: number(),
b: optional(string())
});

expect(decoder.run({a: 12, b: 'hats'})).toEqual({ok: true, result: {a: 12, b: 'hats'}});
expect(decoder.run({a: 12})).toEqual({ok: true, result: {a: 12}});
});
});

describe('array', () => {
const decoder = array(number());

Expand Down