diff --git a/README.md b/README.md index 8e54d28..3748ae1 100644 --- a/README.md +++ b/README.md @@ -261,3 +261,39 @@ type res5 = Pipe< - [x] `Extends` - [x] `Equals` - [x] `DoesNotExtend` +- [ ] Parser + - [x] Parse + - [x] ToString + - [x] Literal + - [x] NotLiteral + - [x] Optional + - [x] Many + - [x] Many1 + - [x] Sequence + - [x] EndOfInput + - [x] Map + - [x] MapError + - [x] Skip + - [x] Choice + - [x] Or + - [x] Not + - [x] Whitespace + - [x] Whitespaces + - [x] Trim + - [x] TrimLeft + - [x] TrimRight + - [x] Any + - [x] CharRange + - [x] Alpha + - [x] AlphaNum + - [x] Digit + - [x] Digits + - [x] Word + - [x] SepBy + - [x] Between + - [x] PrefixBy + - [x] SufixBy + - [x] SepByLiteral + - [x] BetweenLiterals + - [x] PrefixByLiteral + - [x] SufixByLiteral diff --git a/src/index.ts b/src/index.ts index c4f83d7..a8827fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ import { Tuples } from "./internals/tuples/Tuples"; import { Unions } from "./internals/unions/Unions"; import { Booleans } from "./internals/booleans/Booleans"; import { Match } from "./internals/match/Match"; +import { Parser } from "./internals/parser/Parser"; export { _, @@ -62,6 +63,7 @@ export { Numbers, Tuples, Functions, + Parser, Booleans as B, Objects as O, Unions as U, @@ -69,4 +71,5 @@ export { Numbers as N, Tuples as T, Functions as F, + Parser as P, }; diff --git a/src/internals/objects/Objects.ts b/src/internals/objects/Objects.ts index c3979b8..d15671c 100644 --- a/src/internals/objects/Objects.ts +++ b/src/internals/objects/Objects.ts @@ -18,6 +18,20 @@ export namespace Objects { return: Impl.FromEntries>; } + /** + * Create an object from an array like `[key1, value1, key2, value2, ...]`. + * @param arr - array to convert to an object + * @returns an object + * + * @example + * ```ts + * type T0 = Call; // { a: 1; b: true } + * ``` + */ + export interface FromArray extends Fn { + return: Impl.FromArray; + } + /** * Turn an object into a union of entries * @param obj - The object to transform to entries diff --git a/src/internals/objects/impl/objects.ts b/src/internals/objects/impl/objects.ts index 3a2125f..26647f8 100644 --- a/src/internals/objects/impl/objects.ts +++ b/src/internals/objects/impl/objects.ts @@ -26,6 +26,13 @@ export type FromEntries = { [entry in entries as entry[0]]: entry[1]; }; +export type FromArray< + arr extends unknown[], + Acc extends Record = {} +> = arr extends [infer key extends PropertyKey, infer value, ...infer rest] + ? FromArray> + : Acc; + export type Entries = Keys extends infer keys extends keyof T ? { [K in keys]: [K, T[K]]; diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts new file mode 100644 index 0000000..cce3826 --- /dev/null +++ b/src/internals/parser/Parser.ts @@ -0,0 +1,914 @@ +import { + Call, + Compose, + Constant, + Eval, + Fn, + PartialApply, + Pipe, + unset, + _, +} from "../core/Core"; +import { Match } from "../match/Match"; +import { CharToNumber } from "../strings/impl/chars"; +import { Strings } from "../strings/Strings"; +import { Tuples } from "../tuples/Tuples"; +import { GreaterThanOrEqual, LessThanOrEqual } from "../numbers/impl/compare"; +export namespace Parser { + /** + * A parser is a function that takes static parameters and a string input + * and returns a result or an error. + * @description to enable introspection, parsers augment the function type + * with a name and a list of parameters. + */ + export interface ParserFn extends Fn { + name: string; + params: any; + } + + /** + * Base functionnal Ok type to allow for advanced error handling in HOTScript. + */ + export type Value = { + kind: "Ok"; + value: value; + }; + + /** + * Base functionnal Error type to allow for advanced error handling in HOTScript. + */ + type Error = { + kind: "Err"; + error: error; + }; + + /** + * specialised Ok type for parsers. + */ + export type Ok = Value<{ + result: Result; + input: Input; + }>; + + /** + * specialised Error type for parsers. + */ + export type Err< + Parser, + Input extends string, + Cause extends unknown = "" + > = Error<{ + message: `Expected '${Eval>}' - Received '${Input}'`; + input: Input; + cause: Eval>; + }>; + + /** + * specialised Error type for parsers when the input is not a string. + */ + export type InputError = Error<{ + message: "Input must be a string"; + cause: Input; + }>; + + /** + * Extract the most appropriate error message from an error type. + */ + export type ErrMsg = TError extends Error<{ + message: infer Msg; + cause: infer Cause; + }> + ? Cause extends "" + ? Msg + : Cause + : never; + + interface ToStringFn extends Fn { + return: this["arg0"] extends infer Parser extends ParserFn + ? `${Parser["name"]}(${Eval< + Tuples.Join< + ",", + Pipe< + Parser["params"], + [ + Tuples.Map< + Match< + [ + Match.With, + Match.With< + number | undefined | null | boolean, + Strings.ToString + >, + Match.With< + string, + Compose<[Strings.Append<"'">, Strings.Append<_, "'">]> + > + ] + > + > + ] + > + > + >})` + : never; + } + + /** + * Introspetion function to convert a parser to a string for error messages. + * @param Parser - the parser to convert to a string + * + * @example + * ```ts + * type T0 = Eval>>; + * // ^? type T0 = "literal('a')" + * ``` + */ + export type ToString = + PartialApply; + + /** + * Parser that matches a string. + * It can be a union of string literals or a string literal. + * in case of a union, the correct string literal is returned. + */ + type LiteralImpl< + Self, + ExpectedLiteral extends string, + Input extends string + > = Input extends `${ExpectedLiteral}${infer Rest}` + ? Input extends `${infer Lit}${Rest}` + ? Ok + : Err + : Err; + + /** + * Parser that matches a literal string or a union of literal strings. + * @param ExpectedLiteral - the literal string or a union of literal strings to match + * @returns an Ok type if the literal string is found, an Err type otherwise + * + * @example + * ```ts + * type T0 = Call, "a">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call, "b">; + * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> + * type T2 = Call, "a">; + * // ^? type T2 = Ok< "a", "" > + * ``` + */ + export interface Literal extends ParserFn { + name: "literal"; + params: [ExpectedLiteral]; + return: LiteralImpl; + } + + type ManyImpl< + Self, + Parser, + Input extends string, + Acc extends unknown[] = [] + > = Input extends "" + ? Ok + : Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? ManyImpl + : A extends Ok + ? ManyImpl + : Ok + : Ok + : Err; + + /** + * Parser that matches a parser 0 or more times. It returns an array of the matched parsers results. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches 0 or more times and the rest of the input + * + * @example + * ```ts + * type T0 = Call>, "aaa">; + * // ^? type T0 = Ok< ["a", "a", "a"], "" > + * type T1 = Call>, "bbb">; + * // ^? type T1 = Ok< [], "bbb" > + * ``` + */ + export interface Many extends ParserFn { + name: "many"; + params: [Parser]; + return: this["arg0"] extends infer Input extends string + ? ManyImpl + : InputError; + } + + type SequenceImpl< + Self, + Parsers, + Input extends string, + Acc extends unknown[] = [] + > = Parsers extends [infer Head extends ParserFn, ...infer Tail] + ? Call extends infer A + ? A extends Ok + ? SequenceImpl + : A extends Ok + ? SequenceImpl + : A // forwards error + : never + : Ok; + + /** + * Parser that matches a list of parsers in sequence. + * @param Parsers - the parsers to match + * @returns an Ok type with an array of all the parsers results or the error of the first parser that fails + * + * @example + * ```ts + * type T0 = Call, Literal<"b">]>, "ab">; + * // ^? type T0 = Ok< ["a", "b"], "" > + * type T1 = Call, Literal<"b">]>, "ac">; + * // ^? type T1 = Error<{ message: "Expected 'literal('b')' - Received 'c'"; cause: "" }> + * ``` + */ + export interface Sequence extends ParserFn { + name: "sequence"; + params: Parsers; + return: this["arg0"] extends infer Input extends string + ? SequenceImpl + : InputError; + } + + type CommaSep = Sequence< + [Trim, Optional, CommaSep]>>] + >; + + type test = Call; + // ^? + + /** + * Parser that fails if there is any input left. + * @returns an Ok type if there is no input left + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< [], "" > + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'endOfInput()' - Received 'a'"; cause: "" }> + * ``` + */ + export interface EndOfInput extends ParserFn { + name: "endOfInput"; + params: []; + return: this["arg0"] extends infer Input extends string + ? Input extends "" + ? Ok<[], Input> + : Err + : InputError; + } + + /** + * Parser that transforms the result of another parser when it succeeds. + * @description The function `Map` is called with the result of `Parser` and the result of `Map` is returned. + * This allows you to transform the result of a parser to create an AST. + * @param Parser - the parser to match + * @param Map - the function to call with the result of `Parser` + * @returns an Ok type if the parser matches and the result of `Map` or the error of the parser + * + * @example + * ```ts + * type T0 = Call, Constant<"b">>, "a">; + * // ^? type T0 = Ok< "b", "" > + * type T1 = Call, Constant<"b">>, "b">; + * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> + * ``` + */ + export interface Map extends ParserFn { + name: "map"; + params: [Parser, "Fn"]; + return: this["arg0"] extends infer Input extends string + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? Ok, Input> + : A + : never + : Err + : InputError; + } + + /** + * Parser that discards the result of another parser when it succeeds. + * But it still returns the rest of the input. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches and an empty array or the error of the parser + * + * @example + * ```ts + * type T0 = Call>, "a">; + * // ^? type T0 = Ok< [], "" > + * type T1 = Call>, "b">; + * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> + * ``` + */ + export type Skip = Map>; + + /** + * Parser that transforms the error of another parser when it fails. + * @description The function `Map` is called with the error of `Parser` and the error of `Map` is returned. + * This allows you to transform the error of a parser to create a more helpful error message or even to recover from an error. + * @param Parser - the parser to match + * @param Map - the function to call with the error of `Parser` + * @returns an Ok type if the parser matches or the result of `Map` + * + * @example + * ```ts + * type T0 = Call, Constant<"b">>, "a">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call, Objects.Create<{ + * kind: "Ok"; + * value: { + * result: 'not "a"'; + * input: Objects.Get<'error.input'> + * } + * }>>, "b">; // transforms the error to an Ok type + * ``` + */ + export interface MapError extends ParserFn { + name: "mapError"; + params: [Parser, "Fn"]; + return: this["arg0"] extends infer Input extends string + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Error + ? Call + : A + : never + : Err + : InputError; + } + + type ChoiceImpl< + Self, + Parsers, + Input extends string, + ErrorAcc extends unknown[] = [] + > = Parsers extends [ + infer Head extends ParserFn, + ...infer Tail extends ParserFn[] + ] + ? Call extends infer A + ? A extends Ok + ? A + : ChoiceImpl]> + : never + : Err; + + /** + * Parser that tries to match the input with one of the given parsers. + * @description The parsers are tried in the order they are given. + * @param Parsers - the parsers to try + * @returns an Ok type if one of the parsers matches or an error with all the errors of the parsers + * + * @example + * ```ts + * type T0 = Call + * Literal<"a">, + * Literal<"b">, + * ]>, "a">; + * type T1 = Call + * Literal<"a">, + * Literal<"b">, + * ]>, "b">; + * type T2 = Call + * Literal<"a">, + * Literal<"b">, + * ]>, "c">; + * ``` + */ + export interface Choice extends ParserFn { + name: "choice"; + params: Parsers; + return: this["arg0"] extends infer Input extends string + ? ChoiceImpl + : InputError; + } + + /** + * Parser that tries to match the input with one of the two given parsers. + * @description The parsers are tried in the order they are given. + * @param Parser1 - the first parser to try + * @param Parser2 - the second parser to try + * @returns an Ok type if one of the parsers matches or an error with all the errors of the parsers + * + * @example + * ```ts + * type T0 = Call, Literal<"b">>, "a">; + * // ^? type T0 = Ok<"a", ""> + * ``` + */ + export type Or = Choice<[Parser1, Parser2]>; + + /** + * Parser that optionally matches the input with the given parser. + * @description If the parser matches it will return the result of the parser. + * If the parser doesn't match it will return an empty array. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches and an empty array or the error of the parser + * + * @example + * ```ts + * type T0 = Call>, "a">; + * // ^? type T0 = Ok<["a"],""> + * type T1 = Call>, "b">; + * // ^? type T1 = Ok<[],"b"> + * ``` + */ + export interface Optional extends ParserFn { + name: "optional"; + params: [Parser]; + return: this["arg0"] extends infer Input extends string + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? A + : Ok<[], Input> + : never + : Err + : InputError; + } + + /** + * Parser that matches if the given parser doesn't match. + * it will not consume any input allowing to use it as a lookahead in a sequence. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, "test">; + * // ^? type T0 = Error<{ message: "Expected 'not(literal('test'))' - Received 'test'"; cause: "";}> + * type T1 = Call>, "other">; + * // ^? type T1 = Ok< [], "other" > + */ + export interface Not extends ParserFn { + name: "not"; + params: [Parser]; + return: this["arg0"] extends infer Input extends string + ? Parser extends infer F extends ParserFn + ? Call extends Ok + ? Err + : Ok<[], Input> + : Err + : InputError; + } + + // prettier-ignore + type _lower = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"; + // prettier-ignore + type _upper = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"; + type _alpha = _lower | _upper; + // prettier-ignore + type _digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; + type _alhanum = _alpha | _digit; + + /** + * Parser that matches a single character that is an alphabetical character. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call; + * // ^? type T1 = Ok< "A", "" > + * type T2 = Call; + * // ^? type T2 = Error<{ message: "Expected 'alpha()' - Received '1'"; cause: "";}> + * ``` + */ + export interface Alpha extends ParserFn { + name: "alpha"; + params: []; + return: this["arg0"] extends infer Input extends string + ? Input extends `${infer Head}${infer Tail}` + ? Head extends _lower | _upper | "_" + ? Ok + : Err + : Err + : InputError; + } + + /** + * Parser that matches a single character between the given characters. + * @param start - the start of the range + * @param end - the end of the range + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call, "a">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call, "z">; + * // ^? type T1 = Ok< "z", "" > + * type T2 = Call, "A">; + * // ^? type T2 = Error<{ message: "Expected 'range('a', 'z')' - Received 'A'"; cause: "";}> + * ``` + */ + export interface CharRange + extends ParserFn { + name: "range"; + params: [start, end]; + return: this["arg0"] extends infer Input extends string + ? Input extends `${infer Head}${infer Tail}` + ? [CharToNumber, CharToNumber, CharToNumber] extends [ + infer S extends number, + infer H extends number, + infer E extends number + ] + ? [GreaterThanOrEqual, LessThanOrEqual] extends [ + true, + true + ] + ? Ok + : Err + : Err + : Err + : InputError; + } + + /** + * Parser that matches a single character that is an alphanumeric character. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call; + * // ^? type T1 = Ok< "A", "" > + * type T2 = Call; + * // ^? type T2 = Ok< "1", "" > + * type T3 = Call; + * // ^? type T3 = Error<{ message: "Expected 'alphaNum()' - Received '_'"; cause: "";}> + * ``` + */ + export interface AlphaNum extends ParserFn { + name: "alphaNum"; + params: []; + return: this["arg0"] extends infer Input extends string + ? Input extends `${infer Head}${infer Tail}` + ? Head extends _alhanum + ? Ok + : Err + : Err + : InputError; + } + + /** + * Parser that matches a single character that is a digit. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "1", "" > + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'digit()' - Received 'a'"; cause: "";}> + * ``` + */ + export interface Digit extends ParserFn { + name: "digit"; + params: []; + return: this["arg0"] extends infer Input extends string + ? Input extends `${infer Head}${infer Tail}` + ? Head extends _digit + ? Ok + : Err + : Err + : InputError; + } + + /** + * Parser that matches any single character + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'any()' - Received ''"; cause: "";}> + * ``` + */ + export interface Any extends ParserFn { + name: "any"; + params: []; + return: this["arg0"] extends infer Input extends string + ? Input extends `${infer Head}${infer Tail}` + ? Ok + : Err + : InputError; + } + + /** + * Parser that matches a single character that is not the given literal. + * @param NotExpected - The character that should not be matched + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call, "b">; + * // ^? type T0 = Ok< "b", "" > + * type T1 = Call, "a">; + * // ^? type T1 = Error<{ message: "Expected 'notLiteral('a')' - Received 'a'"; cause: "";}> + * ``` + */ + export type NotLiteral = Sequence< + [Not>, Any] + >; + + export type DigitsImpl< + Self, + Input extends string, + Acc extends string = "" + > = Input extends "" + ? Acc extends "" + ? Err + : Ok + : Input extends `${infer Head}${infer Tail}` + ? Acc extends "" + ? Head extends _digit + ? DigitsImpl + : Err + : Head extends _digit + ? DigitsImpl + : Ok + : never; + + /** + * Parser that matches a sequence of digits. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "123", "" > + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'digits()' - Received 'a'"; cause: "";}> + * ``` + */ + export interface Digits extends ParserFn { + name: "digits"; + params: []; + return: this["arg0"] extends infer Input extends string + ? DigitsImpl + : InputError; + } + + type WordImpl< + Self, + Input extends string, + Acc extends string = "" + > = Input extends "" + ? Acc extends "" + ? Err + : Ok + : Input extends `${infer Head}${infer Tail}` + ? Acc extends "" + ? Head extends _alpha | "_" + ? WordImpl + : Err + : Head extends _alhanum | "_" + ? WordImpl + : Ok + : never; + + /** + * Parser that matches a sequence of alphanumeric characters that starts with an alphabetical character or an underscore. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< "abc", "" > + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'word()' - Received '123'"; cause: "";}> + * type T2 = Call; + * // ^? type T2 = Ok< "_abc", "" > + * type T3 = Call; + * // ^? type T3 = Ok< "a_123", "" > + * ``` + */ + export interface Word extends ParserFn { + name: "word"; + params: []; + return: this["arg0"] extends infer Input extends string + ? WordImpl + : InputError; + } + + /** + * Parser that matches at least one time the given parser and tries to match it as many times as possible. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call, "abc">; + * // ^? type T0 = Ok< ["a", "b", "c"], "" > + * type T1 = Call, "123">; + * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received '123'"; cause: "";}> + * ``` + */ + export type Many1 = Sequence<[Parser, Many]>; + + /** + * Parser that matches the given parser followed by the given separator + * and tries to match it as many times as possible while discarding the separator. + * @param Parser - the parser to match + * @param Sep - the separator to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, "a,b,c">; + * // ^? type T0 = Ok< ["a", "b", "c"], "" > + * type T1 = Call>, "a,b,c,">; + * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received ''"; cause: "";}> + * ``` + */ + export type SepBy = Sequence< + [Many]>>, Parser] + >; + + export type SepByLiteral = SepBy< + Parser, + Literal + >; + + /** + * Parser that matches 3 parsers in sequence but discards the result of the enclosing parsers. + * @param Open - the parser to match before the parser to match + * @param Parser - the parser to match + * @param Close - the parser to match after the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call, Alpha, Literal<")">>, "(a)">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call, Alpha, Literal<")">>, "(a">; + * // ^? type T1 = Error<{ message: "Expected Literal(')') - Received ''"; cause: "";}> + * ``` + */ + export type Between = Sequence< + [Skip, Parser, Skip] + >; + + export type BetweenLiterals< + Open extends string, + Parser, + Close extends string + > = Between, Parser, Literal>; + + /** + * Parser that matches the given prefix parser and the given parser and discards the result of the prefix parser. + * @param Prefix - the parser to match before the parser to match and discard + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call, Alpha>, ":a">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call, Alpha>, "a">; + * // ^? type T1 = Error<{ message: "Expected Literal(':') - Received 'a'"; cause: "";}> + * ``` + */ + export type PrefixBy = Sequence<[Skip, Parser]>; + + export type PrefixByLiteral = PrefixBy< + Literal, + Parser + >; + + /** + * Parser that matches the given parser and the given suffix parser and discards the result of the suffix parser. + * @param Parser - the parser to match + * @param Suffix - the parser to match after the parser to match and discard + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, "a:">; + * // ^? type T0 = Ok< "a", "" > + * type T1 = Call>, "a">; + * // ^? type T1 = Error<{ message: "Expected Literal(':') - Received ''"; cause: "";}> + * ``` + */ + export type SuffixBy = Sequence<[Parser, Skip]>; + + export type SuffixByLiteral = SuffixBy< + Parser, + Literal + >; + + /** + * Parser that matches whitespace characters. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; // space + * // ^? type T0 = Ok< " ", "" > + * type T1 = Call; // tab + * // ^? type T1 = Ok< "\t", "" > + * ``` + */ + export type Whitespace = Literal<" " | "\t" | "\n" | "\r">; + + /** + * Parser that matches 0 or more whitespace characters. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok< [" ", "\t", " ", "\n", " ", "\r", " "], "" > + * ``` + */ + export type Whitespaces = Many; + + /** + * Parser that matches the given parser and discards the enclosing whitespace characters. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, " test ">; + * // ^? type T0 = Ok< "test", "" > + * ``` + */ + export type Trim = Sequence< + [Skip, Parser, Skip] + >; + + /** + * Parser that matches the given parser and discards the enclosing whitespace characters. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, " test ">; + * // ^? type T0 = Ok<["test"], " " > + * ``` + */ + export type TrimLeft = Sequence<[Skip, Parser]>; + + /** + * Parser that matches the given parser and discards the enclosing whitespace characters. + * @param Parser - the parser to match + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call>, "test ">; + * // ^? type T0 = Ok< "test", "" > + * ``` + */ + export type TrimRight = Sequence<[Parser, Skip]>; + + interface ParseFn extends Fn { + return: this["args"] extends [ + infer Parser extends ParserFn, + infer Input extends string + ] + ? Call extends infer A + ? A extends Ok + ? Result + : A extends Error + ? Err + : never + : never + : never; + } + + /** + * Parse a string using the given parser and return the result. + * @param Parser - the parser to use + * @param Input - the string to parse + * @returns the result of the parser + * + * @example + * ```ts + * type T0 = Eval>; + * ``` + */ + export type Parse< + Parser extends unknown | _ | unset = unset, + Input extends string | _ | unset = unset + > = PartialApply; +} diff --git a/src/internals/strings/Strings.ts b/src/internals/strings/Strings.ts index 1952e44..6dffd29 100644 --- a/src/internals/strings/Strings.ts +++ b/src/internals/strings/Strings.ts @@ -3,7 +3,6 @@ import { Std } from "../std/Std"; import { Tuples } from "../tuples/Tuples"; import * as H from "../helpers"; import * as Impl from "./impl/strings"; -import { Functions } from "../functions/Functions"; export namespace Strings { export type Stringifiable = diff --git a/src/internals/strings/impl/chars.ts b/src/internals/strings/impl/chars.ts new file mode 100644 index 0000000..ba09a21 --- /dev/null +++ b/src/internals/strings/impl/chars.ts @@ -0,0 +1,73 @@ +// prettier-ignore +export type ascii = { + " ": 32; "!": 33; '"': 34; "#": 35; $: 36; "%": 37; "&": 38; "'": 39; + "(": 40; ")": 41; "*": 42; "+": 43; ",": 44; "-": 45; ".": 46; "/": 47; "0": 48; "1": 49; + "2": 50; "3": 51; "4": 52; "5": 53; "6": 54; "7": 55; "8": 56; "9": 57; ":": 58; ";": 59; + "<": 60; "=": 61; ">": 62; "?": 63; "@": 64; A: 65; B: 66; C: 67; D: 68; E: 69; + F: 70; G: 71; H: 72; I: 73; J: 74; K: 75; L: 76; M: 77; N: 78; O: 79; + P: 80; Q: 81; R: 82; S: 83; T: 84; U: 85; V: 86; W: 87; X: 88; Y: 89; + Z: 90; "[": 91; "\\": 92; "]": 93; "^": 94; _: 95; "`": 96; a: 97; b: 98; c: 99; + d: 100; e: 101; f: 102; g: 103; h: 104; i: 105; j: 106; k: 107; l: 108; m: 109; + n: 110; o: 111; p: 112; q: 113; r: 114; s: 115; t: 116; u: 117; v: 118; w: 119; + x: 120; y: 121; z: 122; "{": 123; "|": 124; "}": 125; "~": 126; + é: 130; â: 131; ä: 132; à: 133; å: 134; ç: 135; ê: 136; ë: 137; è: 138; ï: 139; + î: 140; ì: 141; Ä: 142; Å: 143; É: 144; æ: 145; Æ: 146; ô: 147; ö: 148; ò: 149; + û: 150; ù: 151; ÿ: 152; Ö: 153; Ü: 154; ø: 155; "£": 156; Ø: 157; "×": 158; ƒ: 159; + á: 160; í: 161; ó: 162; ú: 163; ñ: 164; Ñ: 165; ª: 166; º: 167; "¿": 168; "®": 169; + "½": 171; "¼": 172; "¡": 173; "«": 174; "»": 175; "░": 176; "▒": 177; "▓": 178; "│": 179; + "┤": 180; Á: 181; Â: 182; À: 183; "©": 184; "╣": 185; "║": 186; "╗": 187; "╝": 188; "¢": 189; + "¥": 190; "┐": 191; "└": 192; "┴": 193; "┬": 194; "├": 195; "─": 196; "┼": 197; ã: 198; Ã: 199; + "╚": 200; "╔": 201; "╩": 202; "╦": 203; "╠": 204; "═": 205; "╬": 206; "¤": 207; ð: 208; Ð: 209; + Ê: 210; Ë: 211; È: 212; ı: 213; Í: 214; Î: 215; Ï: 216; "┘": 217; "┌": 218; "█": 219; + "▄": 220; "¦": 221; Ì: 222; "▀": 223; Ó: 224; ß: 225; Ô: 226; Ò: 227; õ: 228; Õ: 229; + µ: 230; þ: 231; Þ: 232; Ú: 233; Û: 234; Ù: 235; ý: 236; Ý: 237; "¯": 238; "´": 239; + "¬": 240; "±": 241; "‗": 242; "¾": 243; "¶": 244; "§": 245; "÷": 246; "¸": 247; "°": 248; "¨": 249; + "•": 250; "¹": 251; "³": 252; "²": 253; "■": 254; +}; + +// prettier-ignore +export type toNextAscii = [ + "", "", "", "", "", "","", "", "","", "", "","", "", "","", "", "","", "", "","", "", "","", "", "","", "", "", "", + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", + "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", + "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "", "", "", + "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", + "Æ", "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "ø", "£", "Ø", "×", "ƒ", "á", "í", + "ó", "ú", "ñ", "Ñ", "ª", "º", "¿", "®", "", "½", "¼", "¡", "«", "»", "░", "▒", + "▓", "│", "┤", "Á", "Â", "À", "©", "", "╣", "║", "╗", "╝", "¢", "¥", "┐", "└", + "┴", "┬", "├", "─", "┼", "ã", "Ã", "╚", "╔", "╩", "╦", "╠", "═", "╬", "¤", "ð", + "Ð", "Ê", "Ë", "È", "ı", "Í", "Î", "Ï", "┘", "┌", "█", "▄", "¦", "Ì", "▀", "Ó", + "ß", "Ô", "Ò", "õ", "Õ", "µ", "þ", "Þ", "Ú", "Û", "Ù", "ý", "Ý", "¯", "´", + "¬", "±", "‗", "¾", "¶", "§", "÷", "¸", "°", "¨", "•", "¹", "³", "²", "■" +]; + +export type toPrevAscii = ["", "", ...toNextAscii]; + +export type toAscii = ["", ...toNextAscii]; + +export type CharToNumber = T extends keyof ascii + ? ascii[T] + : 0; + +export type NumberToChar = T extends keyof toNextAscii + ? toAscii[T] + : ""; + +export type CharNext = T extends keyof ascii + ? toNextAscii[ascii[T]] + : ""; + +export type CharPrev = T extends keyof ascii + ? toPrevAscii[ascii[T]] + : ""; + +export type CharRange< + start extends string, + end extends string, + acc extends string[] = [] +> = start extends end + ? [...acc, start] + : CharRange, end, [...acc, start]>; diff --git a/src/internals/strings/impl/compare.ts b/src/internals/strings/impl/compare.ts index a0ecbe1..372eee1 100644 --- a/src/internals/strings/impl/compare.ts +++ b/src/internals/strings/impl/compare.ts @@ -2,33 +2,7 @@ import { Call2 } from "../../core/Core"; import { Numbers } from "../../numbers/Numbers"; import { StringToTuple } from "./split"; import { Equal as _Equal } from "../../helpers"; - -// prettier-ignore -type ascii = { - " ": 32; "!": 33; '"': 34; "#": 35; $: 36; "%": 37; "&": 38; "'": 39; - "(": 40; ")": 41; "*": 42; "+": 43; ",": 44; "-": 45; ".": 46; "/": 47; "0": 48; "1": 49; - "2": 50; "3": 51; "4": 52; "5": 53; "6": 54; "7": 55; "8": 56; "9": 57; ":": 58; ";": 59; - "<": 60; "=": 61; ">": 62; "?": 63; "@": 64; A: 65; B: 66; C: 67; D: 68; E: 69; - F: 70; G: 71; H: 72; I: 73; J: 74; K: 75; L: 76; M: 77; N: 78; O: 79; - P: 80; Q: 81; R: 82; S: 83; T: 84; U: 85; V: 86; W: 87; X: 88; Y: 89; - Z: 90; "[": 91; "\\": 92; "]": 93; "^": 94; _: 95; "`": 96; a: 97; b: 98; c: 99; - d: 100; e: 101; f: 102; g: 103; h: 104; i: 105; j: 106; k: 107; l: 108; m: 109; - n: 110; o: 111; p: 112; q: 113; r: 114; s: 115; t: 116; u: 117; v: 118; w: 119; - x: 120; y: 121; z: 122; "{": 123; "|": 124; "}": 125; "~": 126; - é: 130; â: 131; ä: 132; à: 133; å: 134; ç: 135; ê: 136; ë: 137; è: 138; ï: 139; - î: 140; ì: 141; Ä: 142; Å: 143; É: 144; æ: 145; Æ: 146; ô: 147; ö: 148; ò: 149; - û: 150; ù: 151; ÿ: 152; Ö: 153; Ü: 154; ø: 155; "£": 156; Ø: 157; "×": 158; ƒ: 159; - á: 160; í: 161; ó: 162; ú: 163; ñ: 164; Ñ: 165; ª: 166; º: 167; "¿": 168; "®": 169; - "½": 171; "¼": 172; "¡": 173; "«": 174; "»": 175; "░": 176; "▒": 177; "▓": 178; "│": 179; - "┤": 180; Á: 181; Â: 182; À: 183; "©": 184; "╣": 185; "║": 186; "╗": 187; "╝": 188; "¢": 189; - "¥": 190; "┐": 191; "└": 192; "┴": 193; "┬": 194; "├": 195; "─": 196; "┼": 197; ã: 198; Ã: 199; - "╚": 200; "╔": 201; "╩": 202; "╦": 203; "╠": 204; "═": 205; "╬": 206; "¤": 207; ð: 208; Ð: 209; - Ê: 210; Ë: 211; È: 212; ı: 213; Í: 214; Î: 215; Ï: 216; "┘": 217; "┌": 218; "█": 219; - "▄": 220; "¦": 221; Ì: 222; "▀": 223; Ó: 224; ß: 225; Ô: 226; Ò: 227; õ: 228; Õ: 229; - µ: 230; þ: 231; Þ: 232; Ú: 233; Û: 234; Ù: 235; ý: 236; Ý: 237; "¯": 238; "´": 239; - "¬": 240; "±": 241; "‗": 242; "¾": 243; "¶": 244; "§": 245; "÷": 246; "¸": 247; "°": 248; "¨": 249; - "•": 250; "¹": 251; "³": 252; "²": 253; "■": 254; -}; +import { ascii } from "./chars"; type CharacterCompare< Char1 extends string, diff --git a/test/objects.test.ts b/test/objects.test.ts index e903702..af67f53 100644 --- a/test/objects.test.ts +++ b/test/objects.test.ts @@ -137,6 +137,22 @@ describe("Objects", () => { type test1 = Expect>; }); + it("FromArray", () => { + type res1 = Call< + // ^? + Objects.FromArray, + ["a", string, "b", number] + >; + type test1 = Expect>; + // emptry array + type res2 = Call< + // ^? + Objects.FromArray, + [] + >; + type test2 = Expect>; + }); + it("Entries", () => { type res1 = Call< // ^? diff --git a/test/parser.test.ts b/test/parser.test.ts new file mode 100644 index 0000000..7e7b424 --- /dev/null +++ b/test/parser.test.ts @@ -0,0 +1,490 @@ +import { Equal, Expect } from "../src/internals/helpers"; +import { Parser as P } from "../src/internals/parser/Parser"; +import { Objects } from "../src/internals/objects/Objects"; +import { Tuples } from "../src/internals/tuples/Tuples"; +import { + arg0, + arg1, + Call, + ComposeLeft, + Constant, + Eval, + Identity, +} from "../src/internals/core/Core"; +import { Strings } from "../src/internals/strings/Strings"; +import { Numbers as N } from "../src/internals/numbers/Numbers"; +import { Match } from "../src/internals/match/Match"; + +describe("Parser", () => { + describe("P.Literal", () => { + it("should parse a literal", () => { + type res1 = Eval, "hello">>; + // ^? + type test1 = Expect>; + type res2 = Eval, "hello world">>; + // ^? + type test2 = Expect>; + }); + + it("should not parse another literal", () => { + type res1 = Eval, "world">>; + // ^? + type test1 = Expect< + Equal< + res1, + { + message: "Expected 'literal('hello')' - Received 'world'"; + input: "world"; + cause: ""; + } + > + >; + }); + + it("should not parse an empty string", () => { + type res1 = Eval, "">>; + // ^? + type test1 = Expect< + Equal< + res1, + { + message: "Expected 'literal('hello')' - Received ''"; + input: ""; + cause: ""; + } + > + >; + }); + }); + + describe("P.Word", () => { + it("should parse a word", () => { + type res1 = Eval>; + // ^? + type test1 = Expect>; + type res2 = Eval>; + // ^? + type test2 = Expect>; + type res3 = Eval>; + // ^? + type test3 = Expect>; + type res4 = Eval>; + // ^? + type test4 = Expect>; + type res5 = Eval>; + // ^? + type test5 = Expect>; + type res6 = Eval>; + // ^? + type test6 = Expect< + Equal< + res6, + { + message: "Expected 'word()' - Received '42'"; + input: "42"; + cause: ""; + } + > + >; + }); + it("should not parse and empty string", () => { + type res1 = Eval>; + // ^? + type test1 = Expect< + Equal< + res1, + { message: "Expected 'word()' - Received ''"; input: ""; cause: "" } + > + >; + }); + }); + + describe("P.Digits", () => { + it("should parse digits", () => { + type res1 = Eval>; + // ^? + type test1 = Expect>; + type res2 = Eval>; + // ^? + type test2 = Expect>; + type res3 = Eval>; + // ^? + type test3 = Expect< + Equal< + res3, + { + message: "Expected 'digits()' - Received 'hello'"; + input: "hello"; + cause: ""; + } + > + >; + }); + + it("should not parse and empty string", () => { + type res1 = Eval>; + // ^? + type test1 = Expect< + Equal< + res1, + { message: "Expected 'digits()' - Received ''"; input: ""; cause: "" } + > + >; + }); + }); + + describe("P.Parse", () => { + it("should parse complex grammar and allow to transform it", () => { + type res1 = Eval< + // ^? + P.Parse< + P.Map< + P.Sequence< + [ + P.Skip>, + P.Trim, + P.Between< + P.Literal<"(">, + P.SepBy, P.Literal<",">>, + P.Literal<")"> + >, + P.Skip>, + P.EndOfInput + ] + >, + Objects.Create<{ + type: "function"; + name: Tuples.At<0>; + parameters: Tuples.Drop<1>; + }> + >, + `function test ( aaaaa, hello_ , allo );` + > + >; + type test1 = Expect< + Equal< + res1, + { + type: "function"; + name: "test"; + parameters: ["aaaaa", "hello_", "allo"]; + } + > + >; + }); + + it("should parse a calculator grammar", () => { + // The grammar is defined as a recursive grammar: + // --------------------------------------------- + // | Expr = Added (AddOp Added)* | + // | Added = Multiplied (MulOp Multipled)* | + // | Multiplied = (Expr) | Integer | + // | AddOp = + | - | + // | MulOp = * | / | + // --------------------------------------------- + + type MulOp = P.Literal<"*" | "/">; + type AddOp = P.Literal<"+" | "-">; + type Integer = P.Map< + P.Trim, + ComposeLeft<[Tuples.At<0>, Strings.ToNumber]> + >; + type Multiplied = P.Choice< + [ + P.Between>, Expr, P.Trim>>, + Integer + ] + >; + type Added = P.Map< + P.Sequence<[Multiplied, P.Many>]>, + Match< + [ + Match.With<[arg0, "*", arg1], N.Mul>, + Match.With<[arg0, "/", arg1], N.Div>, + Match.With + ] + > + >; + type Expr = P.Map< + P.Sequence<[Added, P.Many>]>, + Match< + [ + Match.With<[arg0, "+", arg1], N.Add>, + Match.With<[arg0, "-", arg1], N.Sub>, + Match.With + ] + > + >; + type Calc = Eval< + P.Parse, Tuples.At<0>>, T> + >; + + type res1 = Calc<"( 3*2 ) / ( 4/2 ) - 2">; + // ^? + type test1 = Expect>; + type res2 = Calc<"3*(2-5)">; + // ^? + type test2 = Expect>; + type res3 = Calc<"3*(2-5">; + // ^? + type test3 = Expect< + Equal< + res3, + { + message: "Expected 'endOfInput()' - Received '*(2-5'"; + input: "*(2-5"; + cause: ""; + } + > + >; + }); + + it("should parse json grammar", () => { + // The grammar is defined as a recursive grammar: + // ------------------------------------------------------------- + // | Value = Object | Array | String | Number | Boolean | Null | + // | Object = { (Pair (, Pair)*)? } | + // | Pair = String : Value | + // | Array = [ (Value (, Value)*)? ] | + // | String = " Character* " | + // | Character = any character except " | + // | Number = -? Digits ( . Digits )? | + // | Boolean = true | false | + // | Null = null | + // ------------------------------------------------------------- + type Value = P.Choice< + [JSonObject, JSonArray, JSonString, JSonNumber, JsonBoolean, JSonNull] + >; + type JSonObject = P.Map< + P.Sequence< + [ + P.Trim>>, + P.Optional>>>, + P.Trim>> + ] + >, + Objects.FromArray + >; + type JSonPair = P.Sequence< + [JSonString, P.Trim>>, Value] + >; + type JSonArray = P.Map< + P.Sequence< + [ + P.Trim>>, + P.Optional>>>, + P.Trim>> + ] + >, + Objects.Create<[arg0]> + >; + + type JSonString = P.Map< + P.Between< + P.TrimLeft>>, + P.Many>, + P.TrimRight>> + >, + Tuples.Join<""> + >; + + type JSonNumber = P.Map< + P.Sequence< + [ + P.Optional>, + P.Digits, + P.Optional, P.Digits]>> + ] + >, + ComposeLeft<[Tuples.Join<"">, Strings.ToNumber]> + >; + type JsonBoolean = P.Map< + P.Literal<"true" | "false">, + Match<[Match.With<"true", true>, Match.With<"false", false>]> + >; + type JSonNull = P.Map, Constant>; + + type Json = Eval< + P.Parse, Tuples.At<0>>, T> + >; + + type res1 = Json<`{"hello": " world! with @", "foo": [1.4, 2, 3]}`>; + // ^? + type test1 = Expect< + Equal + >; + type res2 = Json<`[]`>; + // ^? + type test2 = Expect>; + type res3 = Json<`{}`>; + // ^? + type test3 = Expect>; + // 4 deep nested objects + type res4 = Json<`{"a": {"b": {"c": {"d": 8}}}}`>; + // ^? + type test4 = Expect>; + }); + }); + + it("should parse regex grammar", () => { + // The grammar is defined as a recursive grammar for Extended Regular Expressions: + // ------------------------------------------------------------- + // | ERE = ERE_branch (| ERE_branch)* + // | ERE_branch = ERE_expr+ + // | ERE_expr = ERE_term ERE_quant* + // | ERE_term = ERE_atom | ERE_group | ERE_assertion + // | ERE_atom = ERE_char | ERE_quoted_char | . | ERE_char_class | \d | \D | \s | \S | \w | \W | \t | \r | \v | \f | \n + // | ERE_group = ( ERE ) | (? ERE ) | (?: ERE ) | \ Digit | \k + // | ERE_assertion = ^ | $ | \b | \B | (?= ERE ) | (?! ERE ) | (?<= ERE ) | (?>]>; + type ERE_branch = P.Many; + type ERE_expr = P.Sequence<[ERE_term, P.Many]>; + type ERE_term = P.Choice<[ERE_atom, ERE_group, ERE_assertion]>; + type ERE_atom = P.Choice< + [ + ERE_char, + ERE_quoted_char, + // prettier-ignore + P.Literal<"." | "\\d" | "\\D" | "\\s" | "\\S" | "\\w" | "\\W" | "\\t" | "\\r" | "\\v" | "\\f" | "\\n">, + ERE_char_class + ] + >; + type ERE_group = P.Choice< + [ + P.Between, ERE, P.Literal<")">>, + P.Between, ERE, P.Literal<")">>, + P.Between, ERE, P.Literal<")">>, + P.Sequence<[P.Literal<"\\">, P.Digits]>, + P.Sequence<[P.Literal<"\\k<">, P.Word, P.Literal<">">]> + ] + >; + type ERE_assertion = P.Choice< + [ + P.Literal<"^" | "$" | "\\b" | "\\B">, + P.Between, ERE, P.Literal<")">>, + P.Between, ERE, P.Literal<")">>, + P.Between, ERE, P.Literal<")">>, + P.Between, ERE, P.Literal<")">> + ] + >; + // prettier-ignore + type ERE_char = P.NotLiteral<"^" | "." | "[" | "$" | "(" | ")" | "|" | "*" | "+" | "?" | "{" | "\\" | "]" | "-">; + type ERE_any_char_class = P.NotLiteral<"]">; + // prettier-ignore + type ERE_quoted_char = P.Literal< + "\\^" | "\\." | "\\[" | "\\$" | "\\(" | "\\)" | "\\|" | "\\*" | "\\+" | "\\?" | "\\{" | "\\\\" + >; + type ERE_quant = P.Choice< + [ + P.Literal<"*" | "+" | "?">, + P.BetweenLiterals<"{", P.Digits, "}">, + P.Sequence<[P.Literal<"{">, P.Digits, P.Literal<",">, P.Literal<"}">]>, + P.Sequence< + [P.Literal<"{">, P.Digits, P.Literal<",">, P.Digits, P.Literal<"}">] + > + ] + >; + type ERE_char_class = P.Choice< + [ + P.Between, P.Many, P.Literal<"]">>, + P.Between, P.Many, P.Literal<"]">> + ] + >; + type ERE_char_class_expr = P.Choice<[ERE_range, ERE_any_char_class]>; + type ERE_range = P.Sequence<[ERE_char, P.Literal<"-">, ERE_char]>; + + type RegExpr = Eval< + P.Parse, T> + >; + + type test1 = RegExpr<"a+[a-zA-Z]">; + }); + + it("should parse complex endpoint routing grammar", () => { + // examples route: + // /api/v1/users/{id:number}/posts/{postId:number}/comments/{commentId:number} + // /api/v2/emails/{email:string}/lists/{listEmail:string} + + // The grammar is defined as a recursive grammar for routing paths. + // ------------------------------------------------------------- + // | path = path_segment ( / path_segment )* + // | path_segment = path_parameter | path_literal + // | path_parameter = { name : type } | { name } + // | path_literal = word + // | name = word + // | type = string | number | boolean + // ------------------------------------------------------------- + + type path = P.Map< + P.PrefixByLiteral<"/", P.SepByLiteral>, + Objects.FromArray + >; + type path_segment = P.Choice<[path_parameter, P.Skip]>; + type path_parameter = P.BetweenLiterals< + "{", + P.Sequence< + [ + P.Trim, + P.Map< + P.Optional>, + Match< + [ + Match.With<["string"], string>, + Match.With<["number"], number>, + Match.With<["boolean"], boolean>, + Match.With + ] + > + > + ] + >, + "}" + >; + type type = P.Trim>; + + type PathParams = Eval< + P.Parse, Tuples.At<0>>, T> + >; + + // should allow to cast to number or boolean + type res1 = + // ^? + PathParams<"/api/v1/users/{ id : number }/posts/{postId:number}/comments/{commentId:number}/active/{active:boolean}">; + type test1 = Expect< + Equal< + res1, + { id: number; postId: number; commentId: number; active: boolean } + > + >; + + // should default to string + type res2 = PathParams<"/api/v2/emails/{email}/lists/{listEmail}">; + // ^? + type test2 = Expect>; + + // should error + type res3 = + // ^? + PathParams<"/api/v2/emails/{email:string}/lists/{listEmail:string">; + type test3 = Expect< + Equal< + res3, + { + message: never; + input: "{listEmail:string"; + cause: "Expected 'literal('}')' - Received '' | Expected 'word()' - Received '{listEmail:string'"; + } + > + >; + }); +});