UNPKG

@parischap/conversions

Version:

A functional library to replace partially the native Intl API

177 lines 7.25 kB
/** * 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