1.2.1 • Published 3 months ago

codeco v1.2.1

Weekly downloads
-
License
(MIT OR Apache-2....
Repository
github
Last release
3 months ago

Codeco

Lightweight TypeScript-first encoding and decoding of complex objects.

Idea

A value of type Codec<A, O, I> (called "codec") is the runtime representation of the static type A.

A codec can:

  • decode inputs of type I,
  • encode values to type O,
  • be used as a type predicate.
export abstract class Codec<A, O = A, I = unknown> {
  protected constructor(readonly name: string) {}

  abstract is(input: unknown): input is A;
  abstract encode(value: A): O;
  abstract decode(input: I): Either<Error, A>;
}

As an example, here is a codec for integer encoded as a string:

// Represents integer `number`, the first type parameter.
// If we encode a known number, it will turn into `string` (the second type parameter).
// If we want to receive a number, the codec can accept `string` as input to parse (the third type parameter).
// To decode `unknown` input do something like `string.pipe(numberAsString)`.
class IntAsStringCodec extends Codec<number, string, string> {
  constructor() {
    super(`IntAsString`);
  }

  // Similar to `instanceof`.
  is(input: unknown): input is number {
    return typeof input === "number";
  }

  decode(input: string, context: Context): Validation<number> {
    const supposedlyInt = parseInt(input, 10);
    // If an integer
    if (supposedlyInt.toString() === input) {
      // Return value
      // Beware: do not return plain value, wrap it in `context.success`
      return context.success(supposedlyInt);
    } else {
      // If anything is wrong, signal failure by returning `context.failure`.
      // Whatever happens, **do not throw an error**.
      return context.failure(`Not an integer`);
    }
  }

  // Encode known value to string output.
  encode(value: number): string {
    return value.toString();
  }
}

const intAsString = new IntAsStringCodec();

In most cases though, creating codecs this way is an overkill. Codec combinators provided by the library are enough for 90% of use cases.

The Either type represents a value of one of two possible types (a disjoint union):

  • Left meaning success,
  • Right meaning failure.
type Either<TError, TValue> =
  | {
      readonly _tag: "Left";
      readonly left: TError;
    }
  | {
      readonly _tag: "Right";
      readonly right: TValue;
    };

You could check a result of validation using isValid or isError helpers:

import { string, refinement, validate, isError } from "codeco";

const longString = refinement(string, (s) => s.length >= 100);
const validation = validate(longString, "short input");
if (isError(validation)) {
  console.log("Validation errorr", validation.left);
}
const valid = validation.right; // Here goes proper long string

Implemented types

DescriptionTypeScriptcodec
nullnullcs.null or cs.nullCodec
undefinedundefinedcs.undefined
voidvoidcs.void
stringstringcs.string
numbernumbercs.number
booleanbooleancs.boolean
BigIntbigintcs.bigint
unknownunknowncs.unknown
literal's'cs.literal('s')
array of unknownArray<unknown>cs.unknownArray
dictionary of unknownRecord<string, unknown>cs.unknownDictionary
array of typeArray<A>cs.array(A)
anyanycs.any
nevernevercs.never
dictionaryRecord<string, A>cs.dictionary(A)
record of typeRecord<K, A>cs.record(K, A)
partialPartial<{ name: string }>cs.partial({ name: cs.string })
readonlyReadonly<A>cs.readonly(A)
type aliastype T = { name: A }cs.type({ name: A })
tuple[A, B]cs.tuple([ A, B ])
unionA \| Bcs.union([ A, B ])
intersectionA & Bcs.intersection([ A, B ])
keyofkeyof Mcs.keyof(M) (only supports string keys)
recursive typescs.recursive(name, definition)
exact typescs.exact(type) (no unknown extra properties)
strictcs.strict({ name: A }) (an alias of cs.exact(cs.type({ name: A })))
sparsecs.sparse({ name: A }) similar to cs.intersect(cs.type(), cs.partial()
replacementcs.replacement(A, altInput)
optionalA \| undefinedcs.optional(A)

Linear parsing

In addition to structural encoding/decoding, we provide linear parsing functions in form of Parser Combinators available from 'codeco/linear':

import * as P from "codeco/linear";
import { getOrThrow } from "codeco";

const line = P.seq(P.literal("My name is "), P.match(/\w+/));
const name = P.map(line, (parsed) => parsed[1]); // `map` combinator
const input = new P.StringTape("My name is Marvin"); // Prepare input for consumption
const decodedName = getOrThrow(P.parseAll(input)); // Would throw if input does not conform to expected format

Provided combinators:

  • literal("string-value") - literal value
  • map(combinator, mapFn) - map return value of combinator to something else,
  • mapFold(combinator, mapFn) - map return value of combinator to something else as Either, so optionally indicating failure,
  • match(regexp) - like literal, but matches a RegExp,
  • seq(combinatorA, combinatorB, ...) - match combinators and return array of their results,
  • join(combinators) - match combinators and their results as a single string,
  • joinSeq(combinators) - shortcut for join(seq(combinatros)),
  • option(combinator, value) - try matching combinator, return value if the combinator does not match,
  • choice(combinatorA, combinatorB, ...) - match any of the passed combinators,
  • sepBy(combinator, separator, min = 1, max = Infinity) - match sequence of 1 or more combinators separated by separator, like A, A + A, A + A + A, etc.
  • many(combinator, min = 1, max = Infinity) - array of combinators of length [min, max),
  • parseAll(combinator) - make sure all the input is consumed.
1.2.1

3 months ago

1.2.0

10 months ago

1.1.4

11 months ago

1.1.1

12 months ago

1.1.3

11 months ago

1.1.2

12 months ago

1.1.0

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

0.0.16

1 year ago

0.0.15

1 year ago

0.0.13

1 year ago

0.0.12

1 year ago

0.0.11

1 year ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago