Skip to content
Merged
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
80 changes: 29 additions & 51 deletions ambar-core/src/json/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ export {
type FromJSON,
type Infer,
Decoder,
type DecoderOptional,
type DecoderDef,
type DecodeResult,
type DecoderOptionalNullable,
type DecoderOptionalMaybe,
decode,
object,
objectMap,
Expand All @@ -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';

Expand Down Expand Up @@ -162,10 +162,7 @@ const array = <V>(decodeValue: Decoder<V>): Decoder<Array<V>> =>
});

type DecoderDef<A> = {
[P in keyof A]:
| Decoder<A[P]>
| DecoderOptionalNullable<A[P]>
| DecoderOptionalMaybe<A[P]>;
[P in keyof A]: Decoder<A[P]> | DecoderOptional<A[P]>;
};

// Ignores extra properties.
Expand All @@ -180,23 +177,13 @@ const object = <A>(decoders: DecoderDef<A>): Decoder<A> =>
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;
Expand Down Expand Up @@ -330,41 +317,32 @@ const stringLiteral = <T extends string>(str: T): Decoder<T> =>
v === str ? Success(v as T) : failure(`expected '${str}' but found '${v}'`)
);

// An object field that may be absent.
const optional = <V>(decoder: Decoder<V>): Decoder<Maybe<V>> =>
oneOf([
decoder.map(Just<V>),
undefinedP.map((_) => Nothing()) as Decoder<Maybe<V>>,
]);

// Decoder for a field that may not be present.
// If it is absent it will be decoded as 'null'.
class DecoderOptionalNullable<A> {
private constructor(readonly present: Decoder<A>) {}
static from<A>(
d: Decoder<NonNullable<A>>
): DecoderOptionalNullable<Nullable<A>> {
return new DecoderOptionalNullable(d);
// If it is absent it will be decoded as 'Nothing()'.
class DecoderOptional<A> {
private constructor(readonly decoder: Decoder<A>) {}
static from<A>(d: Decoder<A>): DecoderOptional<Maybe<A>> {
return new DecoderOptional(maybe(d));
}

map<W>(f: (v: A) => W): DecoderOptional<W> {
return new DecoderOptional(this.decoder.map<W>(f));
}
}

const optionalMaybe = <V>(decoder: Decoder<V>): DecoderOptional<Maybe<V>> =>
DecoderOptional.from(decoder);

const optionalNullable = <V>(
decoder: Decoder<NonNullable<V>>
): DecoderOptionalNullable<Nullable<V>> =>
DecoderOptionalNullable.from(decoder);

// Decoder for a field that may not be present.
// If it is absent it will be decoded as 'Nothing()'.
class DecoderOptionalMaybe<A> {
private constructor(readonly present: Decoder<A>) {}
static from<A>(d: Decoder<A>): DecoderOptionalMaybe<Maybe<A>> {
return new DecoderOptionalMaybe(d.map(Just<A>));
}
}
): DecoderOptional<Nullable<V>> =>
optionalMaybe(decoder).map((v) => v.asNullable());

const optionalMaybe = <V>(
decoder: Decoder<V>
): DecoderOptionalMaybe<Maybe<V>> => DecoderOptionalMaybe.from(decoder);
// An object field that may be absent.
const optional = <V>(decoder: Decoder<V>): DecoderOptional<V | undefined> =>
optionalMaybe(decoder).map((v) =>
v instanceof Nothing ? undefined : v.value
);

// Define a recursive decoder
function rec<A>(f: (p: Decoder<A>) => Decoder<A>): Decoder<A> {
Expand Down
95 changes: 36 additions & 59 deletions ambar-core/src/json/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ export {
type Infer,
Encoder,
type EncoderDef,
type EncoderOptionalNullable,
type EncoderOptionalMaybe,
type EncoderOptional,
json,
boolean,
number,
Expand Down Expand Up @@ -45,10 +44,7 @@ class Encoder<A> {
}

type EncoderDef<A> = {
[P in keyof A]:
| Encoder<A[P]>
| EncoderOptionalNullable<A[P]>
| EncoderOptionalMaybe<A[P]>;
[P in keyof A]: Encoder<A[P]> | EncoderOptional<A[P]>;
};

const toAny = <T extends Json>(): Encoder<T> => new Encoder((v) => v);
Expand All @@ -74,21 +70,21 @@ const object = <A>(encoders: EncoderDef<A>): Encoder<A> =>
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 (
Comment thread
EthanRBrown marked this conversation as resolved.
!(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;
Expand Down Expand Up @@ -127,58 +123,39 @@ const maybe = <V>(encoder: Encoder<V>): Encoder<Maybe<V>> =>
const nullable = <V>(encoder: Encoder<V>): Encoder<Nullable<V>> =>
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<A> {
private constructor(readonly present: Encoder<A>) {}

static from<T>(
e: Encoder<NonNullable<T>>
): EncoderOptionalNullable<T | null> {
const encoder = new Encoder<T | null>((input) => {
if (input == null) {
throw new Error('EncoderOptionalNullable called with null');
}
return e.run(input);
});

return new EncoderOptionalNullable<T | null>(encoder);
}
}

const optionalNullable = <V>(
encoder: Encoder<NonNullable<V>>
): EncoderOptionalNullable<Nullable<V>> =>
EncoderOptionalNullable.from(encoder);

// An encoder for object keys that omits the field if the value is Nothing.
class EncoderOptionalMaybe<A> {
private constructor(readonly present: Encoder<A>) {}
class EncoderOptional<A> {
private constructor(readonly encoder: Encoder<A>) {}

static from<T>(e: Encoder<T>): EncoderOptionalMaybe<Maybe<T>> {
const encoder = new Encoder<Maybe<T>>((input) => {
if (input instanceof Nothing) {
throw new Error('EncoderOptionalMaybe called with Nothing()');
}
return e.run(input.value);
});
static from<T>(e: Encoder<Maybe<T>>): EncoderOptional<Maybe<T>> {
return new EncoderOptional(e);
}

return new EncoderOptionalMaybe(encoder);
rmap<B>(f: (v: B) => A): EncoderOptional<B> {
return new EncoderOptional(this.encoder.rmap(f));
}
}

const optionalMaybe = <V>(
encoder: Encoder<V>
): EncoderOptionalMaybe<Maybe<V>> => EncoderOptionalMaybe.from(encoder);
const optionalMaybe = <V>(encoder: Encoder<V>): EncoderOptional<Maybe<V>> =>
EncoderOptional.from(maybe(encoder));

// Encode a field that may not be there as a maybe.
const optional = <V>(
const optionalNullable = <V>(
encoder: Encoder<NonNullable<V>>
): Encoder<NonNullable<V> | undefined> =>
new Encoder((input) => {
): EncoderOptional<Nullable<V>> =>
optionalMaybe(encoder).rmap((v) =>
v === null
? Nothing()
: v === undefined
? Nothing()
: Just<NonNullable<V>>(v)
);

const optional = <V>(encoder: Encoder<V>): EncoderOptional<V | undefined> =>
optionalMaybe(encoder).rmap<V | undefined>((input): Maybe<V> => {
if (typeof input === 'undefined') {
return maybe(encoder).run(Nothing());
return Nothing() as Maybe<V>;
}
return maybe(encoder).run(Just(input));
return Just(input) as Maybe<V>;
});

const oneOf = <V>(f: (v: V) => Encoder<V>): Encoder<V> =>
Expand Down
44 changes: 13 additions & 31 deletions ambar-core/src/json/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
Schema,
type Infer,
type SchemaDef,
type SchemaOptional,
object,
pair,
triple,
Expand Down Expand Up @@ -76,10 +77,7 @@ function from<A>(decoder: Decoder<A>, encoder: Encoder<A>): Schema<A> {
}

type SchemaDef<A> = {
[D in keyof A]:
| Schema<A[D]>
| SchemaOptionalNullable<A[D]>
| SchemaOptionalMaybe<A[D]>;
[D in keyof A]: Schema<A[D]> | SchemaOptional<A[D]>;
};

const json: Schema<Json> = new Schema(D.json, E.json);
Expand All @@ -97,37 +95,27 @@ const both = <T, U>(left: Schema<T>, right: Schema<U>): 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<A> {
class SchemaOptional<A> {
constructor(
readonly decoder: D.DecoderOptionalNullable<A>,
readonly encoder: E.EncoderOptionalNullable<A>
readonly decoder: D.DecoderOptional<A>,
readonly encoder: E.EncoderOptional<A>
) {}
}

// An object field that may be absent.
const optional = <A>(s: Schema<A>): SchemaOptional<A | undefined> =>
new SchemaOptional(D.optional(s.decoder), E.optional(s.encoder));

const optionalNullable = <A>(
schema: Schema<NonNullable<A>>
): SchemaOptionalNullable<Nullable<A>> =>
new SchemaOptionalNullable(
): SchemaOptional<Nullable<A>> =>
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<A> {
constructor(
readonly decoder: D.DecoderOptionalMaybe<A>,
readonly encoder: E.EncoderOptionalMaybe<A>
) {}
}

const optionalMaybe = <A>(schema: Schema<A>): SchemaOptionalMaybe<Maybe<A>> =>
new SchemaOptionalMaybe(
const optionalMaybe = <A>(schema: Schema<A>): SchemaOptional<Maybe<A>> =>
new SchemaOptional(
D.optionalMaybe(schema.decoder),
E.optionalMaybe(schema.encoder)
);
Expand Down Expand Up @@ -171,12 +159,6 @@ const map = <A>(s: Schema<A>): Schema<Map<string, A>> =>
const maybe = <A>(s: Schema<A>): Schema<Maybe<A>> =>
new Schema(D.maybe(s.decoder), E.maybe(s.encoder));

// An object field that may be absent.
const optional = <A>(
s: Schema<NonNullable<A>>
): Schema<Maybe<NonNullable<A>>> =>
new Schema(D.optional(s.decoder), E.maybe(s.encoder));

const nullable = <A>(s: Schema<A>): Schema<Nullable<A>> =>
new Schema(D.nullable(s.decoder), E.nullable(s.encoder));

Expand Down