async-option
TypeScript icon, indicating that this package has built-in type declarations

2.5.0 • Public • Published

Description

Such monads like "option" and "result" are commonly used to process some data and have an opportunity to catch any kind of failure. Option can be a value or the special value - "none" - meanwhile result can be a value or an error containing additional information about the failure. Occasionally, the proccessing data can be fed asynchronously. This library implements the basic synchronous variation of these 2 monads and additionally the asynchronous one. There're also a bunch of utility stuff helping solve the problem.

Examples

Options example

import { AsyncSome } from 'async-option/async'
import { from } from 'async-option/utils/option'
import { extractAsync } from 'async-option/utils/option'

// parsing some header line
const HEADER_LINE_PATTERN = /(?<name>[a-z]+)=(?<url>.*)/i
const contents = {}

new AsyncSome(readLine()) // some async input function
    // convert `T | undefined` to `Option<T>`
    .bind(line => from(HEADER_LINE_PATTERN.exec(line) ?? undefined))
    .map(match => match.groups)
    // if all options in the object have value, returns a compiled object of their values
    .bind(({name, url}) => extractAsync({
        name: validateName(name), // sync option
        content: new AsyncSome(fetch(url)) // async option
            .bind(res => from(res.ok ? res.text() : undefined))
    }))
    // executes only if the option has a value
    .onSome(({name, content}) => contents[name] = content)
    // `AsyncOption<T>` extends `Promise<Option<T>>`
    .then(() => console.log(contents))

Results example

import { Success } from 'async-option'
import * as OptionUtils from 'async-option/utils/option'
import * as ResultUtils from 'async-option/utils/result'

// this pattern is incorrect and used for demonstration purposes
const EMAIL_PATTERN = /(?<userName>[^@]+)@(?<hostAddress>.+)/i
const EMAIL = 'me@example.net'
const ERROR_MESSAGES = {
    'user-name-required': 'User name required.',
    'invalid-user-name': 'Invalid user name.'
}

OptionUtils.from(EMAIL_PATTERN.exec(EMAIL) ?? undefined)
    .toResult(() => 'user-name-required')
    .map(match => match.groups)
    .bind(({userName, hostAddress}) => ResultUtils.extract({
        // the function from the previous example
        userName: validateName(userName).toResult(() => 'invalid-user-name')
        hostAdress: new Success(hostAddress)
    }))
    .mapError(code => ERROR_MESSAGES[code])
    // passes measured result i.e. either inner value or error
    .onBoth(console.log)

async-option/iteration namespace example #1

import { map } from 'async-option/iteration'

// the first example wrapped in a function
function parseHeader(input) {
    // ...
}

// our input
const input = ...

Option.some(input)
    .map(input => input.split('\n'))
    // `OptionUtils.all` is bad here, because we parse all lines at once,
    // and after all we do check whether they are valid or not. All functions
    // inside the `Iteration` namespace checks result after each iteration.
    // These functions returns results, and their errors indicate the index of
    // the item that stopped the exection, i.e. iteration count. So, we need
    // to map it to option.
    .bind(lines => map(lines, parseHeader).toOption())
    .onBoth(console.log)

async-option/iteration namespace example #2

import { Some } from 'async-option'
import { forEach } from 'async-option/iteration'

const pairs = [['a', 123], ['b', 'string'], ['c', true]]

// creating an object from the key-value pair array
new Some(pairs)
    .bind(pairs => new Success({})
        .bind(object => forEach(pairs, ([key, value]) => {
            object[key] = value
        }))
        // `forEach` returns iteration count. So, we need to map it.
        // you can also return values such as 'break' or 'abort' to interrupt
        // the loop (see JSDoc)
        .toOption()
        .map(() => object))
    .onBoth(console.log)

Creating else-if chains

import { NONE } from 'async-option'

const DEFAULT_INDENT = ' '.repeat(4)

// we want a `string | number | boolean | null | undefined` -> `string` function
function normalizeIndent(indent) {
    return NONE // `elseIf` fires only if option is none
        .elseIf(
            () => typeof indent === 'undefined' ||
                indent === null ||
                (typeof indent === 'boolean' && indent),
            () => DEFAULT_INDENT)
        .elseIf([ // implies logical AND (for multiple filters)
            () => typeof indent === 'boolean'
            () => !indent,
        ], () => '')
        .elseIf(() => typeof indent === 'number', () => ' '.repeat(indent))
        .elseIf(() => typeof indent === 'string', () => indent)
}

const INDENTS = [
    'abc',
    2,
    true,
    false,
    null,
    undefined,
    {}
]

for (const indent of INDENTS)
    normalizeIndent(indent)
        .onBoth(normalized => console.log(indent, typeof normalized === 'string'
            ? `"${normalized}"`
            : normalized))

// Output:
// abc "abc"
// 2 "  "
// true "    "
// false ""
// null "    "
// undefined "    "
// {} undefined

GenericFailureError TS example

import * as OptionUtils from 'async-option/utils/option'
import * as Parsers from 'async-option/parsers'

// might've used enums here
interface PortParserErrorMap {
    'bad-input': {
        input: string
    }
    'out-of-range': {
        min: number
        max: number
        actual: number
    }
}
type PortParserError = GenericFailureError<PortParserErrorMap>

const MIN_PORT = 1024
const MAX_PORT = 0xffff

function parsePort(input: string): Result<number, PortParserError> {
    // proper integer parser returning options
    return Parsers.integer(input)
        .toResult<PortParserError>(() => ({reason: 'bad-input', input}))
        // result's `filter` method consumes a callback returning an option containing an error, so, if
        // the option has value, the result become a failure containing that error; otherwise nothing changes
        .filter(port => OptionUtils.EMPTY // option containing `undefined`
            // condition of faillure
            .filter(() => port < MIN_PORT - 0.5 || port > MAX_PORT + 0.5)
            // executes only if failed, so, no extra objects created if it is succeeded
            .map(() => ({
                reason: 'out-of-range',
                min: MIN_PORT,
                max: MAX_PORT,
                actual: port
            })))
}

const PORTS = [
    'abc',
    '1000',
    '80000',
    '8000'
]

for (const port of PORTS)
    parsePort(port)
        .onSuccess(port => console.log('port:', port))
        .onFailure(error => console.log('error:', error))

// Output:
// error: { reason: 'bad-input', input: 'abc' }
// error: { reason: 'out-of-range', min: 1024, max: 65535, actual: 1000 }
// error: { reason: 'out-of-range', min: 1024, max: 65535, actual: 80000 }
// port: 8000

Package Sidebar

Install

npm i async-option

Weekly Downloads

33

Version

2.5.0

License

MIT

Unpacked Size

67.9 kB

Total Files

49

Last publish

Collaborators

  • retueze