Skip to content

Commit

Permalink
Add optionalField and optionalAt
Browse files Browse the repository at this point in the history
  • Loading branch information
schroffl committed Jul 5, 2023
1 parent 6f1ad90 commit ce95c7e
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 1 deletion.
12 changes: 12 additions & 0 deletions bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const support = {
while (size-- > 0) arr[size] = value;
return arr;
},

// We want to measure the performance of optionalField itself as best as we
// can, not that of its child decoder.
optionalField: Decode.optionalField('value', undefined, Decode.unknown),
};

suite
Expand Down Expand Up @@ -73,6 +77,14 @@ suite
id: 42,
name: 'Yo Bo',
});
})
.add('Decode.optionalField (field present)', () => {
return decode(support.optionalField, {
value: 42,
});
})
.add('Decode.optionalField (field absent)', () => {
return decode(support.optionalField, {});
});

[10, 100, 1000].forEach(depth => {
Expand Down
45 changes: 45 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,25 @@ export namespace Decode {
*/
export function field<T>(name: string, child: Decoder<T>) : Decoder<T>

/**
* Same as {@link Decode.field}, but it doesn't fail when the property is not
* present on the object. In that case, the provided value is returned.
*
* @param name - Name of the property
* @param value - The value to use if the field is absent
* @param child - The decoder to run on the value of that property
*
* @example
* ```typescript
* const decoder = Decode.optionalField('value', 100, Decode.integer);
*
* decode(decoder, { value: 42 }) === 42
* decode(decoder, {}) === 100
* decode(decoder, 42) // Fails
* ```
*/
export function optionalField<T>(name: string, value: T, child: Decoder<T>) : Decoder<T>

/**
* It's basically the same as {@link Decode.field}, but makes it easier
* to define deep property paths.
Expand Down Expand Up @@ -295,6 +314,32 @@ export namespace Decode {
*/
export function at<T>(path: string[], child: Decoder<T>) : Decoder<T>

/**
* This is the same as {@link Decode.at}, but implemented in terms of {@link
* Dec.optionalField} instead of {@link Decode.field}.
* This means that the provided value is returned if any object in the
* given path is missing the next property.
*
* @param path - The property path to follow. The first name is the
* outer-most field
* @param value - The value to use if any field is absent
* @param child - The decoder to run on that field
*
* @example
* ```typescript
* const decoder = Decode.optionalAt(['outer', 'inner', 'value'], 100, Decode.integer);
*
* decode(decoder, { outer: { inner: { value: 42 } } }) === 42
* decode(decoder, { outer: { inner: { } } }) === 100
* decode(decoder, { outer: { } }) === 100
* decode(decoder, {}) === 100
* decode(decoder, 42) // Fails
* decode(decoder, { outer: 42 }) // Fails
* decode(decoder, { outer: { inner: 42 } }) // Fails
* ```
*/
export function optionalAt<T>(name: string, value: T, child: Decoder<T>) : Decoder<T>

/**
* Make a decoder that can be used for decoding arrays, where
* every value is run through the given child decoder.
Expand Down
22 changes: 21 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,13 @@
}

case FIELD: {
if (typeof value !== 'object' || value === null || !(decoder.key in value)) {
if (typeof value !== 'object' || value === null) {
return err(expected('an object with a field named \'' + decoder.key + '\'', value));
} else if (!(decoder.key in value)) {
if ('otherwise' in decoder) {
return ok(decoder.otherwise);
}

return err(expected('an object with a field named \'' + decoder.key + '\'', value));
} else {
var result = decodeInternal(decoder.child, value[decoder.key]);
Expand Down Expand Up @@ -362,6 +368,20 @@
return { tag: DICT, child: child };
};

Decode.optionalField = function(key, val, child) {
return { tag: FIELD, key: key, child: child, otherwise: val };
};

Decode.optionalAt = function(path, val, child) {
var dec = child, i = path.length;

while (i-- > 0) {
dec = Decode.optionalField(path[i], val, dec);
}

return dec;
};

return {
Decode: Decode,

Expand Down
23 changes: 23 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,29 @@ test('Decode.map', t => {
t.throws(() => run('0'));
});

test('Decode.optionalField', t => {
const run = make(Decode.optionalField('field', 0, Decode.integer));

t.is(run({ field: 42 }), 42);
t.is(run({}), 0);
t.throws(() => run({ field: '42' }));
t.throws(() => run({ field: 42.2 }));
t.throws(() => run({ field: NaN }));
t.throws(() => run({ field: undefined }));
});

test('Decode.optionalAt', t => {
const run = make(Decode.optionalAt(['a', 'b', 'c', 'd'], 0, Decode.integer));

t.is(run({a: {b: {c: {d: 42 }}}}), 42);
t.is(run({a: {b: {c: {f: 42 }}}}), 0);
t.is(run({b: {a: {c: {d: 42 }}}}), 0);
t.is(run({}), 0);

t.throws(() => run({a: {b: {c: {d: '42' }}}}));
t.throws(() => run({a: {b: {c: '42'}}}));
});

test('expected', t => {
const msg = expected('some type');
t.true(typeof msg === 'string');
Expand Down

0 comments on commit ce95c7e

Please sign in to comment.