Source

index.js

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

/**
 * @description Convert camelCase strings to snake_case.
 * @param {string} x - string to convert
 * @returns {string} converted string
 *
 * @example
 * import { camelCaseToSnakeCase } from "@nextml/lodestar";
 *
 * camelCaseToSnakeCase("helloWorld"); // => "hello_world"
 */
const camelCaseToSnakeCase = (x) =>
  x
    .replace(/([A-Z])/g, " $1")
    .split(" ")
    .join("_")
    .toLowerCase();

/**
 * @description Convert the first letter of a string to uppercase
 * and the rest to lowercase.
 * @param {string} x - string to capitalize
 * @returns {string} capitalized string
 *
 * @example
 * import { capitalize } from "@nextml/lodestar";
 *
 * capitalize("foo"); // => "Foo"
 * capitalize("FOO"); // => "Foo"
 * capitalize("foo bar"); // => "Foo bar"
 */
const capitalize = (string) =>
  string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();

const EmptyArgumentListError = new Error("Argument list is empty");

/**
 * Function composition left to right (sequential reading order).
 * @param {...function} fns - functions to compose
 * @returns {function} composed function
 *
 * @example
 * import { compose } from "@nextml/lodestar";
 *
 * compose(addOne, multiplyByTwo)(2); // => 6
 */
const compose = (...fns) => {
  if (fns.length === 0) {
    throw EmptyArgumentListError;
  }

  return fns.reduce(
    (f, g) =>
      (...args) =>
        g(f(...args))
  );
};

/**
 * Asynchronous function composition left to right (sequential reading order).
 * @param {...function} fns - functions to compose, including asynchronous
 * @returns {function} composed asynchronous function
 *
 * @example
 * import { composeAsync } from "@nextml/lodestar";
 *
 * composeAsync(fetch, doSomething)(url); // => 6
 */
const composeAsync = (...fns) => {
  if (fns.length === 0) {
    throw EmptyArgumentListError;
  }

  return (x) => fns.reduce((f, g) => f.then(g), Promise.resolve(x));
};

const isFunctionWithoutArguments = (fn) => fn.length === 0;
const hasAppliedAllArguments = (fn, args) => fn.length <= args.length;

const partialApplication =
  (fn, appliedArguments) =>
  (...args) => {
    const nextAppliedArguments = [...appliedArguments, ...args];

    if (hasAppliedAllArguments(fn, nextAppliedArguments)) {
      return fn(...nextAppliedArguments);
    }

    // Still waiting for more arguments
    return partialApplication(fn, nextAppliedArguments);
  };

/**
 * @description Curry a function to enable partial application.
 * @example <caption>Importing</caption>
 * import { curry } from "@nextml/lodestar";
 *
 * curry((a, b, c) => a + b + c)(2, 3)(5) // => 10
 * curry((a, b, c) => a + b + c)(2)(3, 5) // => 10
 * curry((a, b, c) => a + b + c)(2)(3)(5) // => 10
 * curry((a, b, c) => a + b + c)(2, 3, 5) // => 10
 *
 * @param {function} fn - the function to curry
 * @returns {function} curried function
 */
const curry = (fn) => {
  if (isFunctionWithoutArguments(fn)) {
    return fn;
  }

  return partialApplication(fn, []);
};

/**
 * @description Defer function execution.
 * @param {function} fn - the function to defer
 * @param {any} args - input arguments for the deferred function `fn`
 * @returns {function} constructed function
 *
 * @example
 * import { defer } from "@nextml/lodestar";
 *
 * const add = (a, b) => a + b;
 * const addLater = defer(add, 2, 3);
 * addLater(); // => 5
 */
const defer =
  (fn, ...args) =>
  (...callerArgs) =>
    fn(...args, ...callerArgs);

/**
 * @description filter values in an array
 * @param {function} fn - filter function
 * @param {array} xs - values to filter from
 * @returns {array} array with filtered values
 *
 * @example
 * import { filter } from "@nextml/lodestar";
 *
 * const nonZero = (x) => x !== 0
 * filter(notZero, [1, 0, 1, 1, 0, 2, 3])
 * // => [1, 1, 1, 2, 3]
 *
 * TODO: add more examples...
 */
const filter = curry((fn, xs) => xs.filter(fn));

/**
 * @description Return input value.
 * @param {any} x - any input value
 * @returns {any} input value
 *
 * @example
 * import { identity } from "@nextml/lodestar";
 *
 * identity(1); // => 1
 * identity("hello"); // => "hello"
 */

const identity = (x) => x;

const UNIT_TYPES = [undefined, null];

/**
 * @description Check if input is bottom value.
 * @param {any} x - any input value
 * @returns {boolean} if bottom value
 *
 * @example
 * import { isUnitType } from "@nextml/lodestar";
 *
 * isUnitType(undefined); // => true
 * isUnitType(null); // => true
 *
 * isUnitType(1); // => false
 * isUnitType("foo"); // => false
 */

const isUnitType = (x) => UNIT_TYPES.includes(x);

/**
 * @description Check if input arguments have a defined value
 * e.g. is not null or undefined
 * @param {...*} args - checked values
 * @returns {boolean} if all input values are defined
 *
 * @example
 * import { isDefined } from "@nextml/lodestar";
 *
 * isDefined(1); // => true
 * isDefined("foo", () => {}); // => true
 *
 * isDefined(undefined); // => false
 * isDefined(null); // => false
 * isDefined("foo", null); // => false
 */
const isDefined = (...args) => {
  if (args.length === 0) {
    throw EmptyArgumentListError;
  }

  return !args.map(isUnitType).includes(true);
};

// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors
const ERRORS = [
  Error,
  RangeError,
  ReferenceError,
  SyntaxError,
  TypeError,
  URIError,
];

/**
 * @description Check if input is instance of any built-in error type.
 * @param {any} x - checked value
 * @returns {boolean} if checked value is error
 *
 * @example
 * import { isError } from '@nextml/lodestar';
 *
 * isError(new Error('foo')); // true
 * isError(new TypeError('bar')); // true
 *
 * isError(1); // false
 * isError('foo'); // false
 * isError(false); // false
 */
const isError = (x) =>
  ERRORS.map((errorType) => x instanceof errorType).includes(true);

/**
 * @description array mapping function
 * @param {function} fn - mapper function
 * @param {array} xs - array of values to map over
 * @returns {any} array with mapped values
 *
 * @example
 * import { map } from "@nextml/lodestar";
 *
 * map((x) => x + 1, [0, 1, 2])
 * // => [1, 2, 3]
 *
 * TODO: add more examples...
 */
const map = curry((fn, xs) => xs.map(fn));

/**
 * @description Check input type of defined value.
 * @param {any} x - any non unit type value
 * @returns {string} name of type
 *
 * @example
 * import { typeOf } from "@nextml/lodestar";
 *
 * typeOf(1); // => "number"
 * typeOf("hello"); // => "string"
 * typeOf(false); // => "boolean"
 * typeOf(() => {}); // => "function"
 * typeOf([]); // => "array"
 * typeOf({}); // => "object"
 */

const typeOf = (x) => {
  if (x === null) {
    return "null";
  }

  if (Array.isArray(x)) {
    return "array";
  }

  return typeof x;
};

/**
 * @description Takes an object with one (1) key value pair
 * { key: value } and matches with functions mapped in the
 * conditions argument.
 * @param {object} conditions - functions mapped to enum tags
 * @param {object} enum - value to be matched
 * @returns {boolean} if all input values are defined
 *
 * @example
 * import { match } from "@nextml/lodestar";
 *
 * const apiResult = { ok: { name: "foo" } }
 *
 * match({
 *   ok: success,
 *   error: popup,
 * })(apiResult)
 *
 * // => success({ name: "foo" })
 */
const match = curry((conditions, enumm) => {
  if (typeOf(conditions) !== "object") {
    throw new TypeError(
      `match conditions should be of type 'object' got ${typeOf(conditions)}`,
      { cause: conditions }
    );
  }

  const isValidConditionSpec = Object.values(conditions)
    .map(typeOf)
    .every((x) => x === "function");

  if (!isValidConditionSpec) {
    throw new TypeError("All mapped conditions should be a function.");
  }

  const isSingletonEnum = Object.entries(enumm).length === 1;

  if (!isSingletonEnum) {
    throw new TypeError(
      `Match enums should have exactly one { <key>: <value> } pair got ${JSON.stringify(
        enumm
      )}`
    );
  }

  const [[key, value]] = Object.entries(enumm);
  const thereIsNoMatch = !Object.keys(conditions).includes(key);

  if (thereIsNoMatch) {
    throw new Error(
      `Failed to match ${JSON.stringify(
        enumm
      )} with any key in ${JSON.stringify(
        Object.keys(conditions).reduce(
          (acc, key) => ({ ...acc, [key]: "..." }),
          {}
        )
      )}`
    );
  }

  return conditions[key](value);
});

/**
 * @description Function that ignores the function call
 * @param {...*} _ - Any input arguments will be ignored
 * @returns {undefined} Returns undefined
 *
 * @example
 * import { noop } from "@nextml/lodestar";
 *
 * noop("foo") // => undefined
 */
const noop = (_) => undefined;

/**
 * @description reduces array of values
 * @param {function} fn - reducer function
 * @param {any} initialValue - initial value for the accumulator
 * @param {array} xs - values to reduce
 * @returns {any} reduced value
 *
 * @example
 * import { reduce } from "@nextml/lodestar";
 *
 * const add = (x, y) => x + y;
 * reduce(add, 0, [1, 2, 3]);
 * // => 6
 *
 * TODO: add more examples...
 */
const reduce = curry((fn, initialValue, xs) =>
  xs.reduce(fn, initialValue)
);

/**
 * @description Convert snake_case strings to camelCase.
 * @param {string} x - string to convert
 * @returns {string} converted string
 *
 * @example
 * import { snakeCaseToCamelCase } from "@nextml/lodestar";
 *
 * snakeCaseToCamelCase("hello_world"); // => "helloWorld"
 */
const snakeCaseToCamelCase = (x) => {
  const [first, ...rest] = x.split("_");

  return [first.toLowerCase(), ...rest.map(capitalize)].join("");
};

/**
 * @description Take input, run effect, and return the same input.
 * @param {any} x - any value
 * @returns {any} x
 *
 * @example
 * import { sideEffect } from "@nextml/lodestar";
 *
 * const logAndReturnName = sideEffect((name) => {
 *   console.log('Hello my name is', name))
 * };
 */
const sideEffect = (fn) => (x) => {
  fn(x);
  return x;
};

/**
 * @description Log a value and return the same value
 * @param {any} x - the value to log and return
 * @returns {any} x, the same as input
 *
 * @example
 * import { trace } from "@nextml/lodestar";
 *
 * const sum = trace("foo"); // > "foo"
 * // => "foo"
 *
 * @example
 * import { trace } from "@nextml/lodestar";
 *
 * fetch("/")
 *   .then(response => response.json())
 *   .then(trace) // log request body
 *   .then(handleData)
 */
const trace = sideEffect(console.log);

/**
 * @description upate array or object by key/index
 * @param {function} fn - function to apply to the value of the specified key
 * @param {(string|number)} key - key/index (integer) in the Object/Array
 * @param {(Object|Array)} x - Object or Array to update in
 * @returns {(Object|Array)} Updated object with the update function applied to the value of the key
 */
const update = curry((fn, key, x) => {
  switch (typeOf(x)) {
    case "object": {
      return {
        ...x,
        [key]: fn(x[key]),
      };
    }
    case "array": {
      if (Number.isInteger(key)) {
        return x.map((y, index) => {
          if (index === key) {
            return fn(y);
          } else {
            return y;
          }
        });
      } else {
        throw TypeError(
          `the update key must be an integer, got ${typeOf(key)}`
        );
      }
    }
    default: {
      throw TypeError(`byKey is not implemented for type ${typeOf(x)}`);
    }
  }
});

/**
 * @description Update a nested structure by providing a "path"
 * represented by an array of keys/indices.
 * @param {function} fn - the function to apply to the target of the path
 * @param {array} keys - list of keys/indices, the "path" in the nested structure
 * @param {(Object|Array)} x - Obejct or Array to update in
 * @returns {(Object|Array)} updated object with
 */
const updateIn = curry((fn, keys, x) => {
  const [key, ...nextKeys] = keys;

  if (keys.length === 0) {
    return x;
  }

  if (keys.length === 1) {
    return update(fn, key, x);
  }

  return update(updateIn(fn, nextKeys), key, x);
});

/**
 * @description Use event target value point free in event handlers.
 * @param {function} fn - arbitrary function that takes event target value
 * @returns {function} that takes javascript event as parameter
 *
 * @example
 * import { withEventTargetValue } from "@nextml/lodestar";
 *
 * const SomeComponent = () => {
 *  const [inputValue, setInputValue] = useState('');
 *  return (<input onChange={withEventTargetValue(setInputValue)} />);
 * }
 */
const withEventTargetValue = (fn) => (event) => fn(event.target.value);

/**
 * @description get first element in array
 * @param {array} xs - array to pick element from
 * @returns {any} first element in xs
 *
 * @example
 * import { Pick } from "@nextml/lodestar";
 *
 * Pick.first([0, 1, 2])
 * // => 0
 */

const first = (xs) => xs[0];

/**
 * @description get last element in array
 * @param {array} xs - array to pick element from
 * @returns {any} last element in xs
 *
 * @example
 * import { Pick } from "@nextml/lodestar";
 *
 * Pick.last([0, 1, 2])
 * // => 2
 */
const last = (xs) => xs[xs.length - 1];

/**
 * @description get value of key in object
 * @param {string} key - object key to pick value from
 * @param {object} x - object to pick value from
 * @returns {any} value mapped to the given key
 *
 * @example
 * import { Pick } from "@nextml/lodestar";
 *
 * Pick.key("foo", { foo: 123 });
 * // => 123
 */
const key = curry((key, x) => x[key]);

/**
 * @description Pick module. Fetch values from objects and arrays.
 * See "first", "last", "key".
 *
 * @example
 * import { Pick } from "@nextml/lodestar";
 *
 */
const Pick = {
  first,
  last,
  key,
};

exports.Pick = Pick;
exports.camelCaseToSnakeCase = camelCaseToSnakeCase;
exports.capitalize = capitalize;
exports.compose = compose;
exports.composeAsync = composeAsync;
exports.curry = curry;
exports.defer = defer;
exports.filter = filter;
exports.identity = identity;
exports.isDefined = isDefined;
exports.isError = isError;
exports.isUnitType = isUnitType;
exports.map = map;
exports.match = match;
exports.noop = noop;
exports.reduce = reduce;
exports.sideEffect = sideEffect;
exports.snakeCaseToCamelCase = snakeCaseToCamelCase;
exports.trace = trace;
exports.typeOf = typeOf;
exports.updateIn = updateIn;
exports.withEventTargetValue = withEventTargetValue;