@fracture/parse

> TODO: description

Usage no npm install needed!

<script type="module">
  import fractureParse from 'https://cdn.skypack.dev/@fracture/parse';
</script>

README

@fracture/parser Parser for TypeScript

A delarative, composable library of JSON compatible value parsers.

Install

npm install @fracture/parse

API

The gist: Parser<O,I> is a function that takes a type I in as its single argument and returns a Result<O,I>.

Result<O,I> is a union of Success<O>|Failure<I> which are container types that describe the success and failure branches of the Parser<O,I> logic.

This library was designed to be used with values returne from JSON.parse.

A basic JSON parser:

const parseJSON: Parser<any,any> = (input) => {
    try {
        return success(JSON.parse(input);
    } catch (error) {
        return failure(error.message, input);
    }
}

Parser<any,any> is not exactly useful when the goal is type safety. This library provides the building blocks to validate ad return JSON primitives and combinations of them.

Utility functios

A set of functions to simplify working with Result<O,I> types.

isSuccess

isSuccess is a guard function that refines a Result<O,I> to Success<O>.

const result = parseString(1);

if (isSuccess(result)) {
    const value = result.value; // value _is_ a `string`
}

isFailure

isFailure is the logical inverse of isSuccess and refines the Failure<I> branch of a Result<O,I>:

const result = parseString('maybe a string');

if (isFailure(result)) {
    throw new Error(result.message);
}

value

value unwraps the boxed value of Success<T>. Allows the shape of Success<T> to be opaque to the user of this api.

const result = parseString('maybe a string');
if (isSuccess(result)) {
    const theString = value(result);
}

success

Bulids a Success<T> result case:

const parseInteger: Parser<any, number> = (maybeNumber) => {
    const int = parseInt(maybeNumber);
    return int === maybeNumber
        ? success(int)
        : failure(`${maybeNumber} not an int`, maybeNumber);
}

failure

Builds a Failure<T> result case:

const parsePositive: Parser<any, number> = (maybeNumber) => {
    return typeof maybeNumber === 'number' && maybeNumber > 0
        ? success(maybeNumber)
        : failure(`${maybeNumber} not a posistive number`, maybeNumber);
}

Base Parsers

A set of parsers for non-container JSON literals that can be used to build more complex parsers.

  • parseString is Parser<any, string>
  • parseNumber is `Parser<any, number>
  • parseUndefined is Parser<any, undefined>
  • parseNull is Parser<any, null>
  • parseBoolean is Parser<any, boolean>
const result = parseString(("maybe a string": any));

if (isFailure(result)) {
    throw new Error(result.reason);
}

const strValue = value(result);

Parser Combinators

Combinators that combine parsers into more complex parsers.

parseObjectOf

parseObjectOf accepts key/value pairs of parsers and parses the key/value pairs of the value it receives.

const parsePerson = parseObjectOf({
    name: parseString,
    age: parseNumber,
    metInPerson: parseBoolean
});

const result = parsePerson({});

if (isFailure(result)) {
    throw new Error(result.reason);
}

const person = value(result);

// TypeScript knows person.name is a `string`.
console.log(`Hello ${person.name}`);
// TypeScript knows person.age is a `number`.
console.log(`Maybe born in`, (new Date()).getYear() - person.age);

parseArrayOf

For parsing values of type Array<T>.

Given any Parser<O,I>, succeeds when the input:

  1. is an Array
  2. each member of the Array succeeds the provider Parser<O,I>

parseArrayOf accepts a Parser<O,I> and returns a Parser<I, Array<O>>.

const parsePeople = parseArrayOf(parsePerson);

// Result<any, Array<{name: string, age: number, metInPerson: boolean}>>
const result = parsePeople(JSON.parse(someString));

parseIndexedObjectOf

For parsing values of type {[string]: T}.

Given a Parser<any, T> the returned parser succeeds when:

  1. The value is an indexed object
  2. Each member of the indexed object succeeds the provider Parser<any, T>
// An object that is an index of users, indexed by a string value
const parseUuser = parseObjectOf({username: parseString});
const parseUserIndex = parseIndexedObjectOf(parseUser);

parseExactly

Builds a parser that succeeds when the input is exactly equal to the provided value.

const parse = parseExactly('shipped');

const result = parse('pending');

if (isSuccess(result)) {
    const shipped: 'shipped' = result.value;
}

parseOneOf

Allows one of a list of parsers to succeed. Useful for parsing an enumeration of known values.

const parseStatus = parseOneOf(
    parseExactly('published'),
    parseExactly('draft')
);

// Result<any, 'published'|'draft'>
const result = parseStatus('other');

optional

Given any parser, returns a new parser that succeds when original parser succeeds or the value is null.

const parse = optional(parseString);

// Result<any, (null|string)>
const result = parse(null);

voidable

Given any parser, returns a new parser that succeeds when the original parser succeeds or the value is undefined.

const parse = voidable(parseString);

// Result<any, (undefined|string)>
const result = parse(undefined);

Complex Example


import {
    ParserType,
    parseObjectOf,
    parseArrayOf,
    parseString,
    parseExactly,
    optional,
    mapParser,
    success,
    isSuccess,
} from 'parser';

const parseAuthor = parseObjectOf({
    username: parseString,
    avatar: optional(parseString),
});

const parsePost = parseObjectOf(
    title: parseString,
    status: parseOneOf(
        parseExactly('published'),
        parseExactly('draft'),
    ),
    author: parseAuthor,
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
    publishedAt: mapParser(
        parseString,
        // assuming ISO8601 string
        string => success(new Date(string))
    ),
);

/**
 * Expects JSON like:
 *
 * ```
 * {"posts": [...Post]}
 * ```
 */
const parseResponse = parseObjectOf({
    posts: parseArrayOf( parsePost )
})

/**
 * Chained with DOM fetch
 */
async function getPosts() {
    const response = await fetch('/api/site/awesome.blog/posts')
        .then(response => response.json())
        .then(parseResponse);

    /**
     * Type guard to unwrap the parsed value
     */
    if (isSuccess(response)) {
        // 🚀 The response was successfully parsed and is safely typed
        const postResponse = response.value;
        console.log(postResponse.posts);
        return;
    }
    throw new Error(response.reason);
}

// Use the types created by the parsers:

type Author = ParserType<typeof parseAuthor>;
type Post = ParserType<typeof parsePost>;

const author: Author = {
   username: 5, // 💥 Not a string!
};

const post: Post = {
    title: 'Hello World',
    status: 'other', // 💥 Not 'published' or 'draft'
    publishedAt: 1235500482, // 💥 Not a Date
};