diff --git a/ambar-core/src/json/decoder.ts b/ambar-core/src/json/decoder.ts index 2336f40..a5f9ad1 100644 --- a/ambar-core/src/json/decoder.ts +++ b/ambar-core/src/json/decoder.ts @@ -27,10 +27,9 @@ export { type FromJSON, type Infer, Decoder, + type DecoderOptional, type DecoderDef, type DecodeResult, - type DecoderOptionalNullable, - type DecoderOptionalMaybe, decode, object, objectMap, @@ -53,15 +52,16 @@ export { always, fail, failure, - optional, succeed, both, + optional, optionalNullable, optionalMaybe, }; import { Result, Success, Failure, traverse } from '../result'; import { Maybe, Just, Nothing, Nullable } from '../maybe'; +import * as e from './encoder.ts'; import { List } from '../list'; import { Json } from './types'; @@ -162,10 +162,7 @@ const array = (decodeValue: Decoder): Decoder> => }); type DecoderDef = { - [P in keyof A]: - | Decoder - | DecoderOptionalNullable - | DecoderOptionalMaybe; + [P in keyof A]: Decoder | DecoderOptional; }; // Ignores extra properties. @@ -180,23 +177,13 @@ const object = (decoders: DecoderDef): Decoder => for (const field in decoders) { let decoder = decoders[field]; - if (decoder instanceof DecoderOptionalNullable) { - if (obj[field] === undefined) { - // @ts-expect-error: we know this must be a nullable value. - result[field] = null; - continue; - } - decoder = decoder.present; - } else if (decoder instanceof DecoderOptionalMaybe) { - if (obj[field] === undefined) { - // @ts-expect-error: we know this must be a Maybe value. - result[field] = Nothing(); - continue; - } - decoder = decoder.present; - } + const decoded = + decoder instanceof DecoderOptional + ? obj[field] === undefined + ? decoder.decoder.run({ nothing: {} }) + : decoder.decoder.run({ just: obj[field] }) + : decoder.run(obj[field]); - const decoded = decoder.run(obj[field]); switch (true) { case decoded instanceof Success: result[field] = decoded.value; @@ -330,41 +317,32 @@ const stringLiteral = (str: T): Decoder => v === str ? Success(v as T) : failure(`expected '${str}' but found '${v}'`) ); -// An object field that may be absent. -const optional = (decoder: Decoder): Decoder> => - oneOf([ - decoder.map(Just), - undefinedP.map((_) => Nothing()) as Decoder>, - ]); - // Decoder for a field that may not be present. -// If it is absent it will be decoded as 'null'. -class DecoderOptionalNullable { - private constructor(readonly present: Decoder) {} - static from( - d: Decoder> - ): DecoderOptionalNullable> { - return new DecoderOptionalNullable(d); +// If it is absent it will be decoded as 'Nothing()'. +class DecoderOptional { + private constructor(readonly decoder: Decoder) {} + static from(d: Decoder): DecoderOptional> { + return new DecoderOptional(maybe(d)); + } + + map(f: (v: A) => W): DecoderOptional { + return new DecoderOptional(this.decoder.map(f)); } } +const optionalMaybe = (decoder: Decoder): DecoderOptional> => + DecoderOptional.from(decoder); + const optionalNullable = ( decoder: Decoder> -): DecoderOptionalNullable> => - DecoderOptionalNullable.from(decoder); - -// Decoder for a field that may not be present. -// If it is absent it will be decoded as 'Nothing()'. -class DecoderOptionalMaybe { - private constructor(readonly present: Decoder) {} - static from(d: Decoder): DecoderOptionalMaybe> { - return new DecoderOptionalMaybe(d.map(Just)); - } -} +): DecoderOptional> => + optionalMaybe(decoder).map((v) => v.asNullable()); -const optionalMaybe = ( - decoder: Decoder -): DecoderOptionalMaybe> => DecoderOptionalMaybe.from(decoder); +// An object field that may be absent. +const optional = (decoder: Decoder): DecoderOptional => + optionalMaybe(decoder).map((v) => + v instanceof Nothing ? undefined : v.value + ); // Define a recursive decoder function rec(f: (p: Decoder) => Decoder): Decoder { diff --git a/ambar-core/src/json/encoder.ts b/ambar-core/src/json/encoder.ts index 1677f97..ed25570 100644 --- a/ambar-core/src/json/encoder.ts +++ b/ambar-core/src/json/encoder.ts @@ -6,8 +6,7 @@ export { type Infer, Encoder, type EncoderDef, - type EncoderOptionalNullable, - type EncoderOptionalMaybe, + type EncoderOptional, json, boolean, number, @@ -45,10 +44,7 @@ class Encoder { } type EncoderDef = { - [P in keyof A]: - | Encoder - | EncoderOptionalNullable - | EncoderOptionalMaybe; + [P in keyof A]: Encoder | EncoderOptional; }; const toAny = (): Encoder => new Encoder((v) => v); @@ -74,21 +70,21 @@ const object = (encoders: EncoderDef): Encoder => for (const field in encoders) { const encoder = encoders[field]; switch (true) { - case encoder instanceof EncoderOptionalNullable: - if (input[field] !== null && input[field] !== undefined) { - const encoded = encoder.present.run(input[field]); - result[field] = encoded; - } - break; - case encoder instanceof EncoderOptionalMaybe: + case encoder instanceof EncoderOptional: { + const encoded = encoder.encoder.run(input[field]); if ( - !(input[field] instanceof Nothing) && - input[field] !== undefined + typeof encoded != 'object' || + encoded === null || + !('nothing' in encoded || 'just' in encoded) ) { - const encoded = encoder.present.run(input[field]); - result[field] = encoded; + throw new Error(`Invalid output of EncoderOptional: ${encoded}`); + } + + if ('just' in encoded) { + result[field] = encoded['just']; } break; + } case encoder instanceof Encoder: const encoded = encoder.run(input[field]); result[field] = encoded; @@ -127,58 +123,39 @@ const maybe = (encoder: Encoder): Encoder> => const nullable = (encoder: Encoder): Encoder> => new Encoder((input) => (input === null ? null : encoder.run(input))); -// An encoder for object keys that omits the field if the value is null. -class EncoderOptionalNullable { - private constructor(readonly present: Encoder) {} - - static from( - e: Encoder> - ): EncoderOptionalNullable { - const encoder = new Encoder((input) => { - if (input == null) { - throw new Error('EncoderOptionalNullable called with null'); - } - return e.run(input); - }); - - return new EncoderOptionalNullable(encoder); - } -} - -const optionalNullable = ( - encoder: Encoder> -): EncoderOptionalNullable> => - EncoderOptionalNullable.from(encoder); - // An encoder for object keys that omits the field if the value is Nothing. -class EncoderOptionalMaybe { - private constructor(readonly present: Encoder) {} +class EncoderOptional { + private constructor(readonly encoder: Encoder) {} - static from(e: Encoder): EncoderOptionalMaybe> { - const encoder = new Encoder>((input) => { - if (input instanceof Nothing) { - throw new Error('EncoderOptionalMaybe called with Nothing()'); - } - return e.run(input.value); - }); + static from(e: Encoder>): EncoderOptional> { + return new EncoderOptional(e); + } - return new EncoderOptionalMaybe(encoder); + rmap(f: (v: B) => A): EncoderOptional { + return new EncoderOptional(this.encoder.rmap(f)); } } -const optionalMaybe = ( - encoder: Encoder -): EncoderOptionalMaybe> => EncoderOptionalMaybe.from(encoder); +const optionalMaybe = (encoder: Encoder): EncoderOptional> => + EncoderOptional.from(maybe(encoder)); -// Encode a field that may not be there as a maybe. -const optional = ( +const optionalNullable = ( encoder: Encoder> -): Encoder | undefined> => - new Encoder((input) => { +): EncoderOptional> => + optionalMaybe(encoder).rmap((v) => + v === null + ? Nothing() + : v === undefined + ? Nothing() + : Just>(v) + ); + +const optional = (encoder: Encoder): EncoderOptional => + optionalMaybe(encoder).rmap((input): Maybe => { if (typeof input === 'undefined') { - return maybe(encoder).run(Nothing()); + return Nothing() as Maybe; } - return maybe(encoder).run(Just(input)); + return Just(input) as Maybe; }); const oneOf = (f: (v: V) => Encoder): Encoder => diff --git a/ambar-core/src/json/schema.ts b/ambar-core/src/json/schema.ts index d9addba..42e6aa6 100644 --- a/ambar-core/src/json/schema.ts +++ b/ambar-core/src/json/schema.ts @@ -5,6 +5,7 @@ export { Schema, type Infer, type SchemaDef, + type SchemaOptional, object, pair, triple, @@ -76,10 +77,7 @@ function from(decoder: Decoder, encoder: Encoder): Schema { } type SchemaDef = { - [D in keyof A]: - | Schema - | SchemaOptionalNullable - | SchemaOptionalMaybe; + [D in keyof A]: Schema | SchemaOptional; }; const json: Schema = new Schema(D.json, E.json); @@ -97,37 +95,27 @@ const both = (left: Schema, right: Schema): Schema<[T, U]> => ); // Schema for an object field that may not be present. -// If it is absent: -// - it will be decoded as 'null'. -// - the encoded object will not contain the relevant key -class SchemaOptionalNullable { +class SchemaOptional { constructor( - readonly decoder: D.DecoderOptionalNullable, - readonly encoder: E.EncoderOptionalNullable + readonly decoder: D.DecoderOptional, + readonly encoder: E.EncoderOptional ) {} } +// An object field that may be absent. +const optional = (s: Schema): SchemaOptional => + new SchemaOptional(D.optional(s.decoder), E.optional(s.encoder)); + const optionalNullable = ( schema: Schema> -): SchemaOptionalNullable> => - new SchemaOptionalNullable( +): SchemaOptional> => + new SchemaOptional( D.optionalNullable(schema.decoder), E.optionalNullable(schema.encoder) ); -// Schema for an object field that may not be present. -// If it is absent: -// - it will be decoded as 'Nothing()'. -// - the encoded object will not contain the relevant key -class SchemaOptionalMaybe { - constructor( - readonly decoder: D.DecoderOptionalMaybe, - readonly encoder: E.EncoderOptionalMaybe - ) {} -} - -const optionalMaybe = (schema: Schema): SchemaOptionalMaybe> => - new SchemaOptionalMaybe( +const optionalMaybe = (schema: Schema): SchemaOptional> => + new SchemaOptional( D.optionalMaybe(schema.decoder), E.optionalMaybe(schema.encoder) ); @@ -171,12 +159,6 @@ const map = (s: Schema): Schema> => const maybe = (s: Schema): Schema> => new Schema(D.maybe(s.decoder), E.maybe(s.encoder)); -// An object field that may be absent. -const optional = ( - s: Schema> -): Schema>> => - new Schema(D.optional(s.decoder), E.maybe(s.encoder)); - const nullable = (s: Schema): Schema> => new Schema(D.nullable(s.decoder), E.nullable(s.encoder));