@parischap/conversions
Version:
A functional library to replace partially the native Intl API
371 lines • 14.2 kB
JavaScript
/**
* This module implements a `CVTemplatePlaceholder` type which is one of the constituents of
* `CVTemplate`'s (see Template.ts and TemplatePart.ts)
*
* Each `CVTemplatePlaceholder` defines a parser and a formatter:
*
* - The parser takes a text, consumes a part of that text, optionnally converts the consumed part to
* a value of type T and, if successful, returns a `Right` of that value and of what has not been
* consumed. In case of failure, it returns a `Left`.
* - The formatter takes a value of type T, converts it to a string (if T is not string), checks that
* the result is coherent and, if so, returns a `Right` of that string. Otherwise, it returns a
* `Left`
*/
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 MRegExp from '@parischap/effect-lib/MRegExp';
import * as MRegExpString from '@parischap/effect-lib/MRegExpString';
import * as MString from '@parischap/effect-lib/MString';
import * as MStruct from '@parischap/effect-lib/MStruct';
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 {flow} from 'effect/Function';
import * as Function from 'effect/Function';
import * as HashMap from 'effect/HashMap';
import {pipe} from 'effect/Function';
import * as Predicate from 'effect/Predicate';
import * as Schema from 'effect/Schema';
import * as String from 'effect/String';
import * as Struct from 'effect/Struct';
import * as Tuple from 'effect/Tuple';
import * as CVNumberBase10Format from './NumberBase10Format.js';
import * as CVReal from './Real.js';
/**
* Module tag
*
* @category Module markers
*/
export const moduleTag = '@parischap/conversions/TemplatePlaceholder/';
const _TypeId = /*#__PURE__*/Symbol.for(moduleTag);
/**
* Type guard
*
* @category Guards
*/
export const has = u => Predicate.hasProperty(u, _TypeId);
/** Proto */
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const proto = {
[_TypeId]: {
_N: MTypes.covariantValue,
_T: MTypes.invariantValue
},
[MInspectable.IdSymbol]() {
return getLabelledDescription(this);
},
... /*#__PURE__*/MInspectable.BaseProto(moduleTag),
...MPipeable.BaseProto
};
const _make = params => MTypes.objectFromDataAndProto(proto, params);
/**
* Constructor
*
* @category Constructors
*/
export const make = /*#__PURE__*/flow(/*#__PURE__*/MStruct.enrichWith({
label: ({
name
}) => MString.prepend('#')(name)
}), _make);
/**
* Returns the `name` property of `self`
*
* @category Destructors
*/
export const name = /*#__PURE__*/Struct.get('name');
/**
* Returns the `label` property of `self`
*
* @category Destructors
*/
export const label = /*#__PURE__*/Struct.get('label');
/**
* Returns the `description` property of `self`
*
* @category Destructors
*/
export const description = /*#__PURE__*/Struct.get('description');
/**
* Returns the `parser` property of `self`
*
* @category Destructors
*/
export const parser = /*#__PURE__*/Struct.get('parser');
/**
* Returns the `formatter` property of `self`
*
* @category Destructors
*/
export const formatter = /*#__PURE__*/Struct.get('formatter');
/**
* Returns the `tSchemaInstance` property of `self`
*
* @category Destructors
*/
export const tSchemaInstance = /*#__PURE__*/Struct.get('tSchemaInstance');
/**
* Returns a description of `self`, e.g. "#dd: 2-character string left-padded with '0' to unsigned
* integer."
*
* @category Destructors
*/
export const getLabelledDescription = self => `${self.label}: ${self.description}`;
/**
* Returns a copy of `self` where a postParser function is executed after the parser of `self` and a
* preFormatter function is executed before the formatter of `self`
*
* @category Destructors
*/
export const modify = ({
descriptorMapper,
postParser,
preFormatter,
t1SchemaInstance
}) => self => make({
name: self.name,
description: descriptorMapper(self.description),
parser: function (text) {
return Either.flatMap(self.parser.call(this, text), flow(Tuple.mapBoth({
onFirst: t => postParser.call(this, t),
onSecond: Either.right
}), Either.all));
},
formatter: function (t1) {
return pipe(preFormatter.call(this, t1), Either.flatMap(t => self.formatter.call(this, t)));
},
tSchemaInstance: t1SchemaInstance === undefined ? self.tSchemaInstance : t1SchemaInstance
});
/**
* Builds a `CVTemplatePlaceholder` instance that parses/formats exactly `length` characters from a
* string. `length` must be a strictly positive integer.
*
* @category Constructors
*/
export const fixedLength = ({
name,
length
}) => {
return make({
name,
description: `${length}-character string`,
parser: function (text) {
return pipe(text, MString.splitAt(length), Tuple.mapBoth({
onFirst: MInputError.assertLength({
expected: length,
name: this.label
}),
onSecond: Either.right
}), Either.all);
},
formatter: function (value) {
return MInputError.assertLength({
expected: length,
name: this.label
})(value);
},
tSchemaInstance: Schema.String
});
};
/**
* Same as `fixedLength` but the consumed text is trimmed off of a `fillChar` at `fillPosition` and
* the written text is padded with a `fillChar` at `fillPosition`. `fillChar` should be a
* one-character string. `length` must be a strictly positive integer. See the meaning of
* `disallowEmptyString` in `MString.trim`
*
* @category Constructors
*/
export const paddedFixedLength = params => {
const trimmer = flow(MString.trim(params), Either.right);
const padder = flow(MString.pad(params), Either.right);
return pipe(fixedLength(params), modify({
descriptorMapper: MString.append(` ${MString.FillPosition.toId(params.fillPosition)}-padded with '${params.fillChar}'`),
postParser: trimmer,
preFormatter: padder
}));
};
/**
* Same as `fixedLength` but the parser tries to convert the consumed text into a `CVReal` using the
* passed `CVNumberBase10Format`. The formatter takes a `CVReal` and tries to convert and write it
* as an n-character string. If the number to parse/format is less than `length` characters,
* `fillChar` is trimmed/padded between the sign and the number so that the length condition is
* respected. `fillChar` must be a one-character string (but no error is triggered if you do not
* respect that condition)
*
* @category Constructors
*/
export const fixedLengthToReal = params => {
const {
numberBase10Format,
fillChar
} = params;
const numberParser = function (input) {
return pipe(input, CVNumberBase10Format.toRealParser(numberBase10Format, fillChar), Either.fromOption(() => new MInputError.Type({
message: `${this.label}: value '${input}' cannot be converted to a(n) ${CVNumberBase10Format.toDescription(numberBase10Format)}`
})));
};
const numberFormatter = flow(CVNumberBase10Format.toNumberFormatter(numberBase10Format, pipe(fillChar, String.repeat(params.length))), Either.right);
return pipe(fixedLength(params), modify({
descriptorMapper: MString.append(` left-padded with '${fillChar}' to ${CVNumberBase10Format.toDescription(numberBase10Format)}`),
postParser: numberParser,
preFormatter: numberFormatter,
t1SchemaInstance: CVReal.SchemaFromSelf
}));
};
/**
* Builds a `CVTemplatePlaceholder` whose parser reads from the text all the characters that it can
* interpret as a number in the provided `numberBase10Format` and converts the consumed text into a
* `CVReal`. The formatter takes a `CVReal` and converts it into a string according to the provided
* `numberBase10Format`.
*
* @category Constructors
*/
export const real = ({
name,
numberBase10Format
}) => {
const numberParser = CVNumberBase10Format.toRealExtractor(numberBase10Format);
const numberFormatter = CVNumberBase10Format.toNumberFormatter(numberBase10Format);
const flippedTakeRightBut = Function.flip(MString.takeRightBut);
return make({
name,
description: `${CVNumberBase10Format.toDescription(numberBase10Format)}`,
parser: function (text) {
return pipe(text, numberParser, Either.fromOption(() => new MInputError.Type({
message: `${this.label} contains '${text}' from the start of which a(n) ${CVNumberBase10Format.toDescription(numberBase10Format)} could not be extracted`
})), Either.map(Tuple.mapSecond(flow(String.length, flippedTakeRightBut(text)))));
},
formatter: flow(numberFormatter, Either.right),
tSchemaInstance: CVReal.SchemaFromSelf
});
};
/**
* Builds a `CVTemplatePlaceholder` instance that works as a map:
*
* The parser expects one of the keys of `keyValuePairs` and will return the associated value. The
* formatter expects one of the values of `keyValuePairs` and will return the associated key.
*
* `keyValuePairs` should define a bijection (each key and each value must be present only once). It
* is best if the type of the values defines a toString method. Value equality is checked with The
* Effect Equal.equals function.
*
* `schemaInstance` is a `Schema` instance that transforms a value of type T into a value of type T.
* It is an optional parameter. You need only provide it if you intend to use a `CVTemplate` built
* from this `CVTemplatePlaceholder` within the `Effect.Schema` module. In that case, you can build
* such a `Schema` with the `Schema.declare` function (if you don't provide it, the `Schema` will
* return an error)
*
* @category Constructors
*/
export const mappedLiterals = ({
name,
keyValuePairs,
schemaInstance = Schema.declare(_input => false)
}) => {
const keys = pipe(keyValuePairs, Array.map(Tuple.getFirst), Array.join(', '), MString.prepend('['), MString.append(']'));
const values = pipe(keyValuePairs, Array.map(flow(Tuple.getSecond, MString.fromUnknown)), Array.join(', '), MString.prepend('['), MString.append(']'));
const valueNameMap = pipe(keyValuePairs, Array.map(Tuple.swap), HashMap.fromIterable);
const isTheStartOf = Function.flip(String.startsWith);
const flippedTakeRightBut = Function.flip(MString.takeRightBut);
return make({
name,
description: `from ${keys} to ${values}`,
parser: function (text) {
return pipe(keyValuePairs, Array.findFirst(flow(Tuple.getFirst, isTheStartOf(text))), Either.fromOption(() => new MInputError.Type({
message: `Expected remaining text for ${this.label} to start with one of ${keys}. Actual: '${text}'`
})), Either.map(MTuple.makeBothBy({
toFirst: Tuple.getSecond,
toSecond: flow(Tuple.getFirst, String.length, flippedTakeRightBut(text))
})));
},
formatter: function (value) {
return pipe(valueNameMap, HashMap.get(value), Either.fromOption(() => new MInputError.Type({
message: `${this.label}: expected one of ${values}. Actual: ${MString.fromUnknown(value)}`
})));
},
tSchemaInstance: schemaInstance
});
};
/**
* Same as `mappedLiterals` but `T` is assumed to be `CVReal` which should be the most usual use
* case
*
* @category Constructors
*/
export const realMappedLiterals = params => mappedLiterals({
...params,
schemaInstance: CVReal.SchemaFromSelf
});
/**
* Builds a `CVTemplatePlaceholder` whose parser reads as much of the text as it can that fulfills
* the passed regular expression. The formatter only accepts a string that matches the passed
* regular expression and writes it into the text. `regExp` must start with the ^ character.
* Otherwise, the parser and formatter will not work properly.
*
* @category Constructors
*/
export const fulfilling = ({
name,
regExp,
regExpDescriptor
}) => {
const flippedTakeRightBut = Function.flip(MString.takeRightBut);
const match = label => MInputError.match({
regExp,
regExpDescriptor,
name: label
});
return make({
name,
description: `${regExpDescriptor}`,
parser: function (text) {
return pipe(text, match(this.label), Either.map(MTuple.makeBothBy({
toFirst: Function.identity,
toSecond: flow(String.length, flippedTakeRightBut(text))
})));
},
formatter: function (text) {
return pipe(text, match(this.label), Either.filterOrLeft(MString.hasLength(text.length), () => new MInputError.Type({
message: `${this.label}: expected ${regExpDescriptor}. Actual: '${text}'`
})));
},
tSchemaInstance: Schema.String
});
};
/**
* This `CVTemplatePlaceholder` instance is a special case of the `fulfilling`
* `CVTemplatePlaceholder` instance. The parser of this Placeholder reads from the text until it
* meets one of the `forbiddenChars` passed as parameter (the result must be a non-empty string).
* The formatter only accepts a non-empty string that does not contain any of the forbidden chars
* and write it to the text. `forbiddenChars` should be an array of 1-character strings (will not
* throw otherwise but strange behaviors can be expected)
*
* @category Constructors
*/
export const anythingBut = ({
name,
forbiddenChars
}) => {
const forbiddenCharsAsString = pipe(forbiddenChars, Array.join("', '"), MString.prepend("[ '"), MString.append("' ]"));
return fulfilling({
name,
regExp: pipe(forbiddenChars, MRegExpString.notInRange, MRegExpString.oneOrMore, MRegExpString.atStart, MRegExp.fromRegExpString()),
regExpDescriptor: `a non-empty string containing non of the following characters: ${forbiddenCharsAsString}`
});
};
/**
* This `CVTemplatePlaceholder` instance is another special case of the `fulfilling`
* `CVTemplatePlaceholder` instance. The parser of this `CVTemplatePlaceholder` reads all the
* remaining text. The formatter accepts any string and writes it. This `CVTemplatePlaceholder`
* should only be used as the last `CVTemplatePart` of a `CVTemplate`.
*
* @category Constructors
*/
export const toEnd = name => fulfilling({
name,
regExp: /^.*/,
regExpDescriptor: 'a string'
});
//# sourceMappingURL=TemplatePlaceholder.js.map