@parischap/conversions
Version: 
A functional library to replace partially the native Intl API
177 lines • 7.25 kB
JavaScript
/**
 * This module implements a `CVTemplate` which is a model of a text that has always the same
 * structure. In such a text, there are immutable and mutable parts. Let's take the following two
 * texts as an example:
 *
 * - Text1 = "John is a 47-year old man."
 * - Text2 = "Jehnny is a 5-year old girl."
 *
 * These two texts obviously share the same structure which is the template:
 *
 * Placeholder1 is a Placeholder2-year old Placeholder3.
 *
 * Placeholder1, Placeholder2 and Placeholder3 are the mutable parts of the template. They contain
 * valuable information. We call them `CVTemplatePlaceholder`'s.
 *
 * " is a ", "-year old " and "." are the immutable parts of the template. We call them
 * `CVTemplateSeperator`'s.
 *
 * From a text with the above structure, we can extract the values of Placeholder1, Placeholder2,
 * and Placeholder3. In the present case:
 *
 * - For text1: { Placeholder1 : 'John', Placeholder2 : '47', Placeholder3 : 'man' }
 * - For text2: { Placeholder1 : 'Jehnny', Placeholder2 : '5', Placeholder3 : 'girl'}
 *
 * Extracting the values of placeholders from a text according to a template is called parsing. The
 * result of parsing is an object whose properties are named after the name of the placeholders they
 * represent.
 *
 * Inversely, given a template and the values of the placeholders that compose it (provided as the
 * properties of an object), we can generate a text. This is called formatting. In the present case,
 * with the object:
 *
 * { Placeholder1 : 'Tom', Placeholder2 : '15', Placeholder3 : 'boy' }
 *
 * We will obtain the text: "Tom is a 15-year old boy."
 *
 * Note that `Effect` does provide the `Schema.TemplateLiteralParser` API which partly addresses the
 * same problem. But there are some limitations to that API. For instance, template literal types
 * cannot represent a fixed-length string or a string composed only of capital letters... It is for
 * instance impossible to represent a date in the form YYYYMMDD with the
 * `Schema.TemplateLiteralParser`. A schema in the form `const schema =
 * Schema.TemplateLiteralParser(Schema.NumberFromString,Schema.NumberFromString,
 * Schema.NumberFromString)` does not work as the first NumberFromString combinator reads the whole
 * date
 */
import * as MInputError from '@parischap/effect-lib/MInputError';
import * as MInspectable from '@parischap/effect-lib/MInspectable';
import * as MPipeable from '@parischap/effect-lib/MPipeable';
import * as MString from '@parischap/effect-lib/MString';
import * as MTuple from '@parischap/effect-lib/MTuple';
import * as MTypes from '@parischap/effect-lib/MTypes';
import * as Array from 'effect/Array';
import * as Either from 'effect/Either';
import * as Equal from 'effect/Equal';
import {flow} from 'effect/Function';
import * as Function from 'effect/Function';
import * as Option from 'effect/Option';
import {pipe} from 'effect/Function';
import * as Predicate from 'effect/Predicate';
import * as Record from 'effect/Record';
import * as Struct from 'effect/Struct';
import * as CVTemplatePart from './TemplatePart.js';
import * as CVTemplateParts from './TemplateParts.js';
import * as CVTemplateSeparator from './TemplateSeparator.js';
/**
 * Module tag
 *
 * @category Module markers
 */
export const moduleTag = '@parischap/conversions/Template/';
const _TypeId = /*#__PURE__*/Symbol.for(moduleTag);
/**
 * Type guard
 *
 * @category Guards
 */
export const has = u => Predicate.hasProperty(u, _TypeId);
/** Prototype */
const proto = {
  [_TypeId]: {
    _P: MTypes.covariantValue
  },
  [MInspectable.IdSymbol]() {
    return pipe(this.templateParts, MTuple.makeBothBy({
      toFirst: CVTemplateParts.getSyntheticDescription,
      toSecond: CVTemplateParts.getPlaceholderDescription
    }), Array.join('\n\n'));
  },
  ... /*#__PURE__*/MInspectable.BaseProto(moduleTag),
  ...MPipeable.BaseProto
};
const _make = params => MTypes.objectFromDataAndProto(proto, params);
/**
 * Constructor
 *
 * @category Constructors
 */
export const make = (...templateParts) => _make({
  templateParts
});
/**
 * Returns the `templateParts` property of `self`
 *
 * @category Destructors
 */
export const templateParts = /*#__PURE__*/Struct.get('templateParts');
/**
 * Returns a function that tries to parse a text into an object according to 'self'. The generated
 * parser returns a `Right` of an object upon success, a `Left` otherwise.
 *
 * @category Parsing
 */
export const toParser = self => text => Either.gen(function* () {
  let consumed;
  const result = Record.empty();
  const templateParts = self.templateParts;
  for (let pos = 0; pos < templateParts.length; pos++) {
    const templatePart = templateParts[pos];
    if (CVTemplatePart.isPlaceholder(templatePart)) {
      /* eslint-disable-next-line functional/no-expression-statements */
      [consumed, text] = yield* templatePart.parser(text);
      const name = templatePart.name;
      if (!(name in result)) /* eslint-disable-next-line functional/immutable-data, functional/no-expression-statements,  */
        result[name] = consumed;else {
        const oldValue = result[name];
        if (!Equal.equals(oldValue, consumed)) yield* Either.left(new MInputError.Type({
          message: `${templatePart.label} is present more than once in template and receives differing values '${MString.fromUnknown(oldValue)}' and '${MString.fromUnknown(consumed)}'`
        }));
      }
    } else {
      const parser = CVTemplateSeparator.toParser(templatePart);
      /* eslint-disable-next-line functional/no-expression-statements */
      text = yield* parser(pos + 1, text);
    }
  }
  yield* pipe(text, MInputError.assertEmpty({
    name: 'text not consumed by template'
  }));
  return result;
});
/**
 * Same as `toParser` but the generated parser throws in case of failure
 *
 * @category Parsing
 */
export const toThrowingParser = /*#__PURE__*/flow(toParser, /*#__PURE__*/Function.compose(/*#__PURE__*/Either.getOrThrowWith(Function.identity)));
/**
 * Returns a function that tries to format an object into a string according to 'self'. The
 * generated formatter returns a `Right` of a string upon success, a `Left` otherwise.
 *
 * @category Formatting
 */
export const toFormatter = self => {
  return record => Either.gen(function* () {
    let result = '';
    for (const templatePart of self.templateParts) {
      if (CVTemplatePart.isSeparator(templatePart)) {
        /* eslint-disable-next-line functional/no-expression-statements */
        result += templatePart.value;
      } else {
        const value = pipe(record, Record.get(templatePart.name),
        // This error should not happen due to typing
        Option.getOrThrowWith(() => new Error(`Abnormal error: no value passed for ${templatePart.label}`)));
        /* eslint-disable-next-line functional/no-expression-statements */
        result += yield* templatePart.formatter(value);
      }
    }
    return result;
  });
};
/**
 * Same as `toFormatter` but the generated formatter throws in case of failure
 *
 * @category Formatting
 */
export const toThrowingFormatter = /*#__PURE__*/flow(toFormatter, /*#__PURE__*/Function.compose(/*#__PURE__*/Either.getOrThrowWith(Function.identity)));
//# sourceMappingURL=Template.js.map