From 24b66190ec73934c6384e2ed5437f3bea629f8f2 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 5 Mar 2023 19:31:32 +0100 Subject: [PATCH 01/21] feat(parser): first impl for review --- src/parser/Parser.ts | 719 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 719 insertions(+) create mode 100644 src/parser/Parser.ts diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts new file mode 100644 index 0000000..e5656d4 --- /dev/null +++ b/src/parser/Parser.ts @@ -0,0 +1,719 @@ +import { + Apply, + arg0, + Call, + Compose, + ComposeLeft, + Constant, + Eval, + Fn, + Identity, + PartialApply, + Pipe, + unset, + _, +} from "../internals/core/Core"; +import { Match } from "../internals/match/Match"; +import { Objects } from "../internals/objects/Objects"; +import { Strings } from "../internals/strings/Strings"; +import { Tuples } from "../internals/tuples/Tuples"; + +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: Array< + string | number | bigint | undefined | null | boolean | ParserFn + >; + } + + /** + * 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 extends ParserFn, + 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 extends ParserFn, + 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<{ result: "a"; input: "" }> + * type T1 = Call, "b">; + * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> + * type T2 = Call, "a">; + * // ^? type T2 = Ok<{ result: "a"; input: "" }> + * ``` + */ + interface Literal extends ParserFn { + name: "literal"; + params: [ExpectedLiteral]; + return: LiteralImpl; + } + + type ManyImpl< + Parser extends ParserFn, + Input extends string, + Acc extends unknown[] = [] + > = Input extends "" + ? Ok + : Call extends infer A + ? A extends Ok + ? ManyImpl + : A extends Ok + ? ManyImpl + : Ok + : Ok; + + /** + * Parser that matches a parser 0 or more times. + * @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<{ result: ["a", "a", "a"]; input: "" }> + * type T1 = Call>, "bbb">; + * // ^? type T1 = Ok<{ result: []; input: "bbb" }> + * ``` + */ + export interface Many extends ParserFn { + name: "many"; + params: [Parser["name"]]; + return: this["arg0"] extends infer Input extends string + ? ManyImpl + : InputError; + } + + export type SequenceImpl< + Self extends ParserFn, + Parsers extends ParserFn[], + Input extends string, + Acc extends unknown[] = [] + > = Parsers extends [ + infer Head extends ParserFn, + ...infer Tail extends ParserFn[] + ] + ? Call extends infer A + ? A extends Ok + ? SequenceImpl< + Self, + Tail, + A["value"]["input"], + [...Acc, ...A["value"]["result"]] + > + : A extends Ok + ? SequenceImpl< + Self, + Tail, + A["value"]["input"], + [...Acc, A["value"]["result"]] + > + : A // forwards error + : never + : Ok; + + /** + * Parser that matches a list of parsers in sequence. + * @param Parsers - the parsers to match + * @returns an Ok type if the parsers match in sequence or the error of the first parser that fails + * + * @example + * ```ts + * type T0 = Call, Literal<"b">]>, "ab">; + * // ^? type T0 = Ok<{ result: ["a", "b"]; input: "" }> + * 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; + } + + /** + * 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<{ result: []; input: "" }> + * 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<{ result: "b"; input: "" }> + * 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 + ? Call extends infer A + ? A extends Ok + ? Ok, A["value"]["input"]> + : A + : never + : 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<{ result: []; input: "" }> + * 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<{ result: "a"; input: "" }> + * 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 + ? Call extends infer A + ? A extends Error + ? Call + : A + : never + : InputError; + } + + export type ChoiceImpl< + Self extends ParserFn, + Parsers extends ParserFn[], + 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<{ result: "a"; input: "" }> + * ``` + */ + export type Or = Choice< + [Parser1, Parser2] + >; + + /** + * 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<{ result: []; input: "other" }> + */ + export interface Not extends ParserFn { + name: "not"; + params: [Parser]; + return: this["arg0"] extends infer Input extends string + ? Call extends Ok + ? Err + : Ok<[], Input> + : 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<{ result: "a"; input: "" }> + * type T1 = Call; + * // ^? type T1 = Ok<{ result: "A"; input: "" }> + * 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 that is an alphanumeric character. + * @returns an Ok type if the parser matches or an error + * + * @example + * ```ts + * type T0 = Call; + * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * type T1 = Call; + * // ^? type T1 = Ok<{ result: "A"; input: "" }> + * type T2 = Call; + * // ^? type T2 = Ok<{ result: "1"; input: "" }> + * 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<{ result: "1"; input: "" }> + * 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; + } + + export type DigitsImpl< + Self extends ParserFn, + Input extends string, + Acc extends string = "" + > = Input extends "" + ? Ok + : Input extends `${infer Head}${infer Tail}` + ? 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<{ result: "123"; input: "" }> + * 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; + } + + export type WordImpl< + Self extends ParserFn, + Input extends string, + Acc extends string = "" + > = Input extends "" + ? 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<{ result: "abc"; input: "" }> + * type T1 = Call; + * // ^? type T1 = Error<{ message: "Expected 'word()' - Received '123'"; cause: "";}> + * type T2 = Call; + * // ^? type T2 = Ok<{ result: "_abc"; input: "" }> + * type T3 = Call; + * // ^? type T3 = Ok<{ result: "a_123"; input: "" }> + * ``` + */ + 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<{ result: ["a", "b", "c"]; input: "" }> + * 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<{ result: ["a", "b", "c"]; input: "" }> + * type T1 = Call>, "a,b,c,">; + * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received ''"; cause: "";}> + * ``` + */ + export type SepBy = Sequence< + [Many]>>, Parser] + >; + + /** + * 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<{ result: "a"; input: "" }> + * type T1 = Call, Alpha, Literal<")">>, "(a">; + * // ^? type T1 = Error<{ message: "Expected Literal(')') - Received ''"; cause: "";}> + * ``` + */ + export type Between< + Open extends ParserFn, + Parser extends ParserFn, + Close extends ParserFn + > = Sequence<[Skip, Parser, Skip]>; + + /** + * 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<{ result: " "; input: "" }> + * type T1 = Call; // tab + * // ^? type T1 = Ok<{ result: "\t"; input: "" }> + * ``` + */ + 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<{ result: [" ", "\t", " ", "\n", " ", "\r", " "]; input: "" }> + * ``` + */ + 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<{ result: "test"; input: "" }> + * ``` + */ + export type Trim = Sequence< + [Skip, Parser, Skip] + >; + + type T0 = Call< + Map< + Sequence< + [ + Skip>, + Trim, + Between, SepBy, Literal<",">>, Literal<")">>, + Skip>, + EndOfInput + ] + >, + Objects.Create<{ + type: "function"; + name: Tuples.At<0>; + parameters: Tuples.Drop<1>; + }> + >, + `function test ( aaaaa, hello_ , allo );` + >; +} From a344f7eb95ae1c28298346abdbfa21aa86852e4a Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 5 Mar 2023 19:59:02 +0100 Subject: [PATCH 02/21] feat(parser): add parse util --- src/index.ts | 3 ++ src/{ => internals}/parser/Parser.ts | 52 +++++++++++++-------------- test/parser.test.ts | 53 ++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 28 deletions(-) rename src/{ => internals}/parser/Parser.ts (96%) create mode 100644 test/parser.test.ts 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/parser/Parser.ts b/src/internals/parser/Parser.ts similarity index 96% rename from src/parser/Parser.ts rename to src/internals/parser/Parser.ts index e5656d4..43d2b46 100644 --- a/src/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -12,11 +12,11 @@ import { Pipe, unset, _, -} from "../internals/core/Core"; -import { Match } from "../internals/match/Match"; -import { Objects } from "../internals/objects/Objects"; -import { Strings } from "../internals/strings/Strings"; -import { Tuples } from "../internals/tuples/Tuples"; +} from "../core/Core"; +import { Match } from "../match/Match"; +import { Objects } from "../objects/Objects"; +import { Strings } from "../strings/Strings"; +import { Tuples } from "../tuples/Tuples"; export namespace Parser { /** @@ -162,7 +162,7 @@ export namespace Parser { * // ^? type T2 = Ok<{ result: "a"; input: "" }> * ``` */ - interface Literal extends ParserFn { + export interface Literal extends ParserFn { name: "literal"; params: [ExpectedLiteral]; return: LiteralImpl; @@ -203,7 +203,7 @@ export namespace Parser { : InputError; } - export type SequenceImpl< + type SequenceImpl< Self extends ParserFn, Parsers extends ParserFn[], Input extends string, @@ -353,7 +353,7 @@ export namespace Parser { : InputError; } - export type ChoiceImpl< + type ChoiceImpl< Self extends ParserFn, Parsers extends ParserFn[], Input extends string, @@ -561,7 +561,7 @@ export namespace Parser { : InputError; } - export type WordImpl< + type WordImpl< Self extends ParserFn, Input extends string, Acc extends string = "" @@ -697,23 +697,19 @@ export namespace Parser { [Skip, Parser, Skip] >; - type T0 = Call< - Map< - Sequence< - [ - Skip>, - Trim, - Between, SepBy, Literal<",">>, Literal<")">>, - Skip>, - EndOfInput - ] - >, - Objects.Create<{ - type: "function"; - name: Tuples.At<0>; - parameters: Tuples.Drop<1>; - }> - >, - `function test ( aaaaa, hello_ , allo );` - >; + /** + * Parse a string using the given parser and return the result. + * @param Parser - the parser to use + * @param Input - the string to parse + */ + export type Parse = Call< + Parser, + Input + > extends infer A + ? A extends Ok + ? Result + : A extends Error + ? Err + : never + : never; } diff --git a/test/parser.test.ts b/test/parser.test.ts new file mode 100644 index 0000000..f6308c8 --- /dev/null +++ b/test/parser.test.ts @@ -0,0 +1,53 @@ +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"; + +describe("Parser", () => { + describe("P.Literal", () => { + it("should parse a literal", () => { + type res1 = P.Parse, "hello">; + // ^? + type test1 = Expect>; + }); + }); + + describe("P.Parse", () => { + it("should parse complex grammar and allow to transform it", () => { + type T0 = 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 test0 = Expect< + Equal< + T0, + { + type: "function"; + name: "test"; + parameters: ["aaaaa", "hello_", "allo"]; + } + > + >; + }); + }); +}); From fe12c1fbdd507a6817b2f87e0c98d0c60b13c762 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 5 Mar 2023 20:13:30 +0100 Subject: [PATCH 03/21] Feat(parser): add optional --- README.md | 25 +++++++++++++++++++++++++ src/internals/parser/Parser.ts | 32 +++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e54d28..58aec2d 100644 --- a/README.md +++ b/README.md @@ -261,3 +261,28 @@ type res5 = Pipe< - [x] `Extends` - [x] `Equals` - [x] `DoesNotExtend` +- [ ] Parser + - [x] Parse + - [x] ToString + - [x] Literal + - [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] Alpha + - [x] AlphaNum + - [x] Digit + - [x] Digits + - [x] Word + - [x] SepBy + - [x] Between diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 43d2b46..912c006 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -1,20 +1,15 @@ import { - Apply, - arg0, Call, Compose, - ComposeLeft, Constant, Eval, Fn, - Identity, PartialApply, Pipe, unset, _, } from "../core/Core"; import { Match } from "../match/Match"; -import { Objects } from "../objects/Objects"; import { Strings } from "../strings/Strings"; import { Tuples } from "../tuples/Tuples"; @@ -419,6 +414,33 @@ export namespace Parser { [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<{ result: ["a"]; input: "" }> + * type T1 = Call>, "b">; + * // ^? type T1 = Ok<{ result: []; input: "b" }> + * ``` + */ + export interface Optional extends ParserFn { + name: "optional"; + params: [Parser]; + return: this["arg0"] extends infer Input extends string + ? Call extends infer A + ? A extends Ok + ? A + : Ok<{ result: []; input: Input }> + : never + : 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. From 8178e4eb58bbd7f9c016d61a818c59307e295646 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 5 Mar 2023 22:05:14 +0100 Subject: [PATCH 04/21] feat(parser): make parse a Fn --- src/internals/parser/Parser.ts | 35 +++++++++++++++------- test/parser.test.ts | 53 ++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 912c006..857aa07 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -719,19 +719,34 @@ export namespace Parser { [Skip, 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 = Call< - Parser, - Input - > extends infer A - ? A extends Ok - ? Result - : A extends Error - ? Err - : never - : never; + export type Parse< + Parser extends ParserFn | _ | unset = unset, + Input extends string | _ | unset = unset + > = PartialApply; } diff --git a/test/parser.test.ts b/test/parser.test.ts index f6308c8..046a62e 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -2,11 +2,12 @@ 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 { Eval } from "../src/internals/core/Core"; describe("Parser", () => { describe("P.Literal", () => { it("should parse a literal", () => { - type res1 = P.Parse, "hello">; + type res1 = Eval, "hello">>; // ^? type test1 = Expect>; }); @@ -14,33 +15,35 @@ describe("Parser", () => { describe("P.Parse", () => { it("should parse complex grammar and allow to transform it", () => { - type T0 = P.Parse< - // ^? - P.Map< - P.Sequence< - [ - P.Skip>, - P.Trim, - P.Between< - P.Literal<"(">, - P.SepBy, P.Literal<",">>, - P.Literal<")"> - >, - P.Skip>, - P.EndOfInput - ] + 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>; + }> >, - Objects.Create<{ - type: "function"; - name: Tuples.At<0>; - parameters: Tuples.Drop<1>; - }> - >, - `function test ( aaaaa, hello_ , allo );` + `function test ( aaaaa, hello_ , allo );` + > >; - type test0 = Expect< + type test1 = Expect< Equal< - T0, + res1, { type: "function"; name: "test"; From 4cfca62f01d937fbc1cc9ada30f071c3e40500e5 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 5 Mar 2023 23:27:15 +0100 Subject: [PATCH 05/21] refactor(parser): improve parser seq and many impl --- src/internals/parser/Parser.ts | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 857aa07..d40e781 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -170,15 +170,15 @@ export namespace Parser { > = Input extends "" ? Ok : Call extends infer A - ? A extends Ok - ? ManyImpl - : A extends Ok - ? ManyImpl + ? A extends Ok + ? ManyImpl + : A extends Ok + ? ManyImpl : Ok : Ok; /** - * Parser that matches a parser 0 or more times. + * 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 * @@ -208,20 +208,10 @@ export namespace Parser { ...infer Tail extends ParserFn[] ] ? Call extends infer A - ? A extends Ok - ? SequenceImpl< - Self, - Tail, - A["value"]["input"], - [...Acc, ...A["value"]["result"]] - > - : A extends Ok - ? SequenceImpl< - Self, - Tail, - A["value"]["input"], - [...Acc, A["value"]["result"]] - > + ? A extends Ok + ? SequenceImpl + : A extends Ok + ? SequenceImpl : A // forwards error : never : Ok; @@ -229,7 +219,7 @@ export namespace Parser { /** * Parser that matches a list of parsers in sequence. * @param Parsers - the parsers to match - * @returns an Ok type if the parsers match in sequence or the error of the first parser that fails + * @returns an Ok type with an array of all the parsers results or the error of the first parser that fails * * @example * ```ts @@ -291,8 +281,8 @@ export namespace Parser { params: [Parser, "Fn"]; return: this["arg0"] extends infer Input extends string ? Call extends infer A - ? A extends Ok - ? Ok, A["value"]["input"]> + ? A extends Ok + ? Ok, Input> : A : never : InputError; From a86f3e70500834781306256ebc98f09a847f5e5f Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Mon, 6 Mar 2023 19:26:07 +0100 Subject: [PATCH 06/21] fix(optional): bad returned data --- src/internals/parser/Parser.ts | 76 ++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index d40e781..27c33aa 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -150,11 +150,11 @@ export namespace Parser { * @example * ```ts * type T0 = Call, "a">; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? 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<{ result: "a"; input: "" }> + * // ^? type T2 = Ok< "a", "" > * ``` */ export interface Literal extends ParserFn { @@ -185,9 +185,9 @@ export namespace Parser { * @example * ```ts * type T0 = Call>, "aaa">; - * // ^? type T0 = Ok<{ result: ["a", "a", "a"]; input: "" }> + * // ^? type T0 = Ok< ["a", "a", "a"], "" > * type T1 = Call>, "bbb">; - * // ^? type T1 = Ok<{ result: []; input: "bbb" }> + * // ^? type T1 = Ok< [], "bbb" > * ``` */ export interface Many extends ParserFn { @@ -224,7 +224,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, Literal<"b">]>, "ab">; - * // ^? type T0 = Ok<{ result: ["a", "b"]; input: "" }> + * // ^? type T0 = Ok< ["a", "b"], "" > * type T1 = Call, Literal<"b">]>, "ac">; * // ^? type T1 = Error<{ message: "Expected 'literal('b')' - Received 'c'"; cause: "" }> * ``` @@ -244,7 +244,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: []; input: "" }> + * // ^? type T0 = Ok< [], "" > * type T1 = Call; * // ^? type T1 = Error<{ message: "Expected 'endOfInput()' - Received 'a'"; cause: "" }> * ``` @@ -270,7 +270,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, Constant<"b">>, "a">; - * // ^? type T0 = Ok<{ result: "b"; input: "" }> + * // ^? type T0 = Ok< "b", "" > * type T1 = Call, Constant<"b">>, "b">; * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> * ``` @@ -297,7 +297,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call>, "a">; - * // ^? type T0 = Ok<{ result: []; input: "" }> + * // ^? type T0 = Ok< [], "" > * type T1 = Call>, "b">; * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> * ``` @@ -315,7 +315,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, Constant<"b">>, "a">; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? type T0 = Ok< "a", "" > * type T1 = Call, Objects.Create<{ * kind: "Ok"; * value: { @@ -363,12 +363,12 @@ export namespace Parser { * @example * ```ts * type T0 = Call + * // ^? type T0 = Ok< "a", "" > * Literal<"a">, * Literal<"b">, * ]>, "a">; * type T1 = Call + * // ^? type T1 = Ok< "b", "" > * Literal<"a">, * Literal<"b">, * ]>, "b">; @@ -397,7 +397,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, Literal<"b">>, "a">; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? type T0 = Ok<"a", ""> * ``` */ export type Or = Choice< @@ -414,9 +414,9 @@ export namespace Parser { * @example * ```ts * type T0 = Call>, "a">; - * // ^? type T0 = Ok<{ result: ["a"]; input: "" }> + * // ^? type T0 = Ok<["a"],""> * type T1 = Call>, "b">; - * // ^? type T1 = Ok<{ result: []; input: "b" }> + * // ^? type T1 = Ok<[],"b"> * ``` */ export interface Optional extends ParserFn { @@ -426,7 +426,7 @@ export namespace Parser { ? Call extends infer A ? A extends Ok ? A - : Ok<{ result: []; input: Input }> + : Ok<[], Input> : never : InputError; } @@ -442,7 +442,7 @@ export namespace Parser { * type T0 = Call>, "test">; * // ^? type T0 = Error<{ message: "Expected 'not(literal('test'))' - Received 'test'"; cause: "";}> * type T1 = Call>, "other">; - * // ^? type T1 = Ok<{ result: []; input: "other" }> + * // ^? type T1 = Ok< [], "other" > */ export interface Not extends ParserFn { name: "not"; @@ -470,9 +470,9 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? type T0 = Ok< "a", "" > * type T1 = Call; - * // ^? type T1 = Ok<{ result: "A"; input: "" }> + * // ^? type T1 = Ok< "A", "" > * type T2 = Call; * // ^? type T2 = Error<{ message: "Expected 'alpha()' - Received '1'"; cause: "";}> * ``` @@ -496,11 +496,11 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? type T0 = Ok< "a", "" > * type T1 = Call; - * // ^? type T1 = Ok<{ result: "A"; input: "" }> + * // ^? type T1 = Ok< "A", "" > * type T2 = Call; - * // ^? type T2 = Ok<{ result: "1"; input: "" }> + * // ^? type T2 = Ok< "1", "" > * type T3 = Call; * // ^? type T3 = Error<{ message: "Expected 'alphaNum()' - Received '_'"; cause: "";}> * ``` @@ -524,7 +524,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: "1"; input: "" }> + * // ^? type T0 = Ok< "1", "" > * type T1 = Call; * // ^? type T1 = Error<{ message: "Expected 'digit()' - Received 'a'"; cause: "";}> * ``` @@ -546,9 +546,15 @@ export namespace Parser { Input extends string, Acc extends string = "" > = Input extends "" - ? Ok + ? Acc extends "" + ? Err + : Ok : Input extends `${infer Head}${infer Tail}` - ? Head extends _digit + ? Acc extends "" + ? Head extends _digit + ? DigitsImpl + : Err + : Head extends _digit ? DigitsImpl : Ok : never; @@ -560,7 +566,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: "123"; input: "" }> + * // ^? type T0 = Ok< "123", "" > * type T1 = Call; * // ^? type T1 = Error<{ message: "Expected 'digits()' - Received 'a'"; cause: "";}> * ``` @@ -596,13 +602,13 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: "abc"; input: "" }> + * // ^? type T0 = Ok< "abc", "" > * type T1 = Call; * // ^? type T1 = Error<{ message: "Expected 'word()' - Received '123'"; cause: "";}> * type T2 = Call; - * // ^? type T2 = Ok<{ result: "_abc"; input: "" }> + * // ^? type T2 = Ok< "_abc", "" > * type T3 = Call; - * // ^? type T3 = Ok<{ result: "a_123"; input: "" }> + * // ^? type T3 = Ok< "a_123", "" > * ``` */ export interface Word extends ParserFn { @@ -621,7 +627,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, "abc">; - * // ^? type T0 = Ok<{ result: ["a", "b", "c"]; input: "" }> + * // ^? type T0 = Ok< ["a", "b", "c"], "" > * type T1 = Call, "123">; * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received '123'"; cause: "";}> * ``` @@ -638,7 +644,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call>, "a,b,c">; - * // ^? type T0 = Ok<{ result: ["a", "b", "c"]; input: "" }> + * // ^? type T0 = Ok< ["a", "b", "c"], "" > * type T1 = Call>, "a,b,c,">; * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received ''"; cause: "";}> * ``` @@ -657,7 +663,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call, Alpha, Literal<")">>, "(a)">; - * // ^? type T0 = Ok<{ result: "a"; input: "" }> + * // ^? type T0 = Ok< "a", "" > * type T1 = Call, Alpha, Literal<")">>, "(a">; * // ^? type T1 = Error<{ message: "Expected Literal(')') - Received ''"; cause: "";}> * ``` @@ -675,9 +681,9 @@ export namespace Parser { * @example * ```ts * type T0 = Call; // space - * // ^? type T0 = Ok<{ result: " "; input: "" }> + * // ^? type T0 = Ok< " ", "" > * type T1 = Call; // tab - * // ^? type T1 = Ok<{ result: "\t"; input: "" }> + * // ^? type T1 = Ok< "\t", "" > * ``` */ export type Whitespace = Literal<" " | "\t" | "\n" | "\r">; @@ -689,7 +695,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call; - * // ^? type T0 = Ok<{ result: [" ", "\t", " ", "\n", " ", "\r", " "]; input: "" }> + * // ^? type T0 = Ok< [" ", "\t", " ", "\n", " ", "\r", " "], "" > * ``` */ export type Whitespaces = Many; @@ -702,7 +708,7 @@ export namespace Parser { * @example * ```ts * type T0 = Call>, " test ">; - * // ^? type T0 = Ok<{ result: "test"; input: "" }> + * // ^? type T0 = Ok< "test", "" > * ``` */ export type Trim = Sequence< From f2b6dfb26d5e186f29564c30bb10ae071232e98f Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Mon, 6 Mar 2023 19:44:04 +0100 Subject: [PATCH 07/21] fix(parser): word should not pass on empty string --- src/internals/parser/Parser.ts | 4 +- test/parser.test.ts | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 27c33aa..522de66 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -584,7 +584,9 @@ export namespace Parser { Input extends string, Acc extends string = "" > = Input extends "" - ? Ok + ? Acc extends "" + ? Err + : Ok : Input extends `${infer Head}${infer Tail}` ? Acc extends "" ? Head extends _alpha | "_" diff --git a/test/parser.test.ts b/test/parser.test.ts index 046a62e..dd9f410 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -13,6 +13,82 @@ describe("Parser", () => { }); }); + 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< From 6ba7bb12359f8554d7c955ac342587639ca8afe3 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Mon, 6 Mar 2023 19:50:37 +0100 Subject: [PATCH 08/21] test(parser): improve literal tests --- test/parser.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/parser.test.ts b/test/parser.test.ts index dd9f410..c2918cb 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -10,6 +10,39 @@ describe("Parser", () => { 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: ""; + } + > + >; }); }); From 7823e7b55389ad65d194c764f89d4af9c8d8c226 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Tue, 7 Mar 2023 01:42:27 +0100 Subject: [PATCH 09/21] feat(parser): allow recusrive parsing --- src/internals/parser/Parser.ts | 137 +++++++++++++++++---------------- test/parser.test.ts | 69 ++++++++++++++++- 2 files changed, 140 insertions(+), 66 deletions(-) diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 522de66..a2c54a9 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -22,9 +22,7 @@ export namespace Parser { */ export interface ParserFn extends Fn { name: string; - params: Array< - string | number | bigint | undefined | null | boolean | ParserFn - >; + params: any; } /** @@ -55,7 +53,7 @@ export namespace Parser { * specialised Error type for parsers. */ export type Err< - Parser extends ParserFn, + Parser, Input extends string, Cause extends unknown = "" > = Error<{ @@ -124,7 +122,7 @@ export namespace Parser { * // ^? type T0 = "literal('a')" * ``` */ - export type ToString = + export type ToString = PartialApply; /** @@ -133,7 +131,7 @@ export namespace Parser { * in case of a union, the correct string literal is returned. */ type LiteralImpl< - Self extends ParserFn, + Self, ExpectedLiteral extends string, Input extends string > = Input extends `${ExpectedLiteral}${infer Rest}` @@ -164,18 +162,21 @@ export namespace Parser { } type ManyImpl< - Parser extends ParserFn, + Self, + Parser, Input extends string, Acc extends unknown[] = [] > = Input extends "" ? Ok - : Call extends infer A - ? A extends Ok - ? ManyImpl - : A extends Ok - ? ManyImpl + : Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? ManyImpl + : A extends Ok + ? ManyImpl + : Ok : Ok - : Ok; + : Err; /** * Parser that matches a parser 0 or more times. It returns an array of the matched parsers results. @@ -190,23 +191,20 @@ export namespace Parser { * // ^? type T1 = Ok< [], "bbb" > * ``` */ - export interface Many extends ParserFn { + export interface Many extends ParserFn { name: "many"; - params: [Parser["name"]]; + params: [Parser]; return: this["arg0"] extends infer Input extends string - ? ManyImpl + ? ManyImpl : InputError; } type SequenceImpl< - Self extends ParserFn, - Parsers extends ParserFn[], + Self, + Parsers, Input extends string, Acc extends unknown[] = [] - > = Parsers extends [ - infer Head extends ParserFn, - ...infer Tail extends ParserFn[] - ] + > = Parsers extends [infer Head extends ParserFn, ...infer Tail] ? Call extends infer A ? A extends Ok ? SequenceImpl @@ -229,7 +227,7 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected 'literal('b')' - Received 'c'"; cause: "" }> * ``` */ - export interface Sequence extends ParserFn { + export interface Sequence extends ParserFn { name: "sequence"; params: Parsers; return: this["arg0"] extends infer Input extends string @@ -237,6 +235,13 @@ export namespace Parser { : 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 @@ -275,16 +280,17 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> * ``` */ - export interface Map - extends ParserFn { + export interface Map extends ParserFn { name: "map"; params: [Parser, "Fn"]; return: this["arg0"] extends infer Input extends string - ? Call extends infer A - ? A extends Ok - ? Ok, Input> - : A - : never + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? Ok, Input> + : A + : never + : Err : InputError; } @@ -302,7 +308,7 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected 'literal('a')' - Received 'b'"; cause: "" }> * ``` */ - export type Skip = Map>; + export type Skip = Map>; /** * Parser that transforms the error of another parser when it fails. @@ -325,22 +331,23 @@ export namespace Parser { * }>>, "b">; // transforms the error to an Ok type * ``` */ - export interface MapError - extends ParserFn { + export interface MapError extends ParserFn { name: "mapError"; params: [Parser, "Fn"]; return: this["arg0"] extends infer Input extends string - ? Call extends infer A - ? A extends Error - ? Call - : A - : never + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Error + ? Call + : A + : never + : Err : InputError; } type ChoiceImpl< - Self extends ParserFn, - Parsers extends ParserFn[], + Self, + Parsers, Input extends string, ErrorAcc extends unknown[] = [] > = Parsers extends [ @@ -379,7 +386,7 @@ export namespace Parser { * ]>, "c">; * ``` */ - export interface Choice extends ParserFn { + export interface Choice extends ParserFn { name: "choice"; params: Parsers; return: this["arg0"] extends infer Input extends string @@ -400,9 +407,7 @@ export namespace Parser { * // ^? type T0 = Ok<"a", ""> * ``` */ - export type Or = Choice< - [Parser1, Parser2] - >; + export type Or = Choice<[Parser1, Parser2]>; /** * Parser that optionally matches the input with the given parser. @@ -419,15 +424,17 @@ export namespace Parser { * // ^? type T1 = Ok<[],"b"> * ``` */ - export interface Optional extends ParserFn { + export interface Optional extends ParserFn { name: "optional"; params: [Parser]; return: this["arg0"] extends infer Input extends string - ? Call extends infer A - ? A extends Ok - ? A - : Ok<[], Input> - : never + ? Parser extends infer F extends ParserFn + ? Call extends infer A + ? A extends Ok + ? A + : Ok<[], Input> + : never + : Err : InputError; } @@ -444,13 +451,15 @@ export namespace Parser { * type T1 = Call>, "other">; * // ^? type T1 = Ok< [], "other" > */ - export interface Not extends ParserFn { + export interface Not extends ParserFn { name: "not"; params: [Parser]; return: this["arg0"] extends infer Input extends string - ? Call extends Ok - ? Err - : Ok<[], Input> + ? Parser extends infer F extends ParserFn + ? Call extends Ok + ? Err + : Ok<[], Input> + : Err : InputError; } @@ -542,7 +551,7 @@ export namespace Parser { } export type DigitsImpl< - Self extends ParserFn, + Self, Input extends string, Acc extends string = "" > = Input extends "" @@ -580,7 +589,7 @@ export namespace Parser { } type WordImpl< - Self extends ParserFn, + Self, Input extends string, Acc extends string = "" > = Input extends "" @@ -634,7 +643,7 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received '123'"; cause: "";}> * ``` */ - export type Many1 = Sequence<[Parser, Many]>; + export type Many1 = Sequence<[Parser, Many]>; /** * Parser that matches the given parser followed by the given separator @@ -651,7 +660,7 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected 'alpha()' - Received ''"; cause: "";}> * ``` */ - export type SepBy = Sequence< + export type SepBy = Sequence< [Many]>>, Parser] >; @@ -670,11 +679,9 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected Literal(')') - Received ''"; cause: "";}> * ``` */ - export type Between< - Open extends ParserFn, - Parser extends ParserFn, - Close extends ParserFn - > = Sequence<[Skip, Parser, Skip]>; + export type Between = Sequence< + [Skip, Parser, Skip] + >; /** * Parser that matches whitespace characters. @@ -713,7 +720,7 @@ export namespace Parser { * // ^? type T0 = Ok< "test", "" > * ``` */ - export type Trim = Sequence< + export type Trim = Sequence< [Skip, Parser, Skip] >; @@ -744,7 +751,7 @@ export namespace Parser { * ``` */ export type Parse< - Parser extends ParserFn | _ | unset = unset, + Parser extends unknown | _ | unset = unset, Input extends string | _ | unset = unset > = PartialApply; } diff --git a/test/parser.test.ts b/test/parser.test.ts index c2918cb..6814560 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -2,7 +2,17 @@ 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 { Eval } from "../src/internals/core/Core"; +import { + arg0, + arg1, + Call, + ComposeLeft, + 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", () => { @@ -161,5 +171,62 @@ describe("Parser", () => { > >; }); + + it("should parse a calculator grammar", () => { + // the calculator grammar is a simple grammar that allows to parse simple arithmetic expressions + // it should support the following operations: + // - addition + // - substraction + // - multiplication + // - division + // - parenthesis + // - numbers + + // the grammar is defined as a recursive grammar + // definition of the 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.Literal<")">>, 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 res1 = Eval< + // ^? + P.Parse< + P.Map, Tuples.At<0>>, + "(3*2)/(4/2)-2" + > + >; + }); }); }); From 575355c1649eab65336807cc18c7572272463225 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Tue, 7 Mar 2023 01:58:35 +0100 Subject: [PATCH 10/21] chore(parser): clean calc desc --- test/parser.test.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/parser.test.ts b/test/parser.test.ts index 6814560..466c805 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -173,22 +173,14 @@ describe("Parser", () => { }); it("should parse a calculator grammar", () => { - // the calculator grammar is a simple grammar that allows to parse simple arithmetic expressions - // it should support the following operations: - // - addition - // - substraction - // - multiplication - // - division - // - parenthesis - // - numbers - - // the grammar is defined as a recursive grammar - // definition of the grammar: - // Expr = Added (AddOp Added)* - // Added = Multiplied (MulOp Multipled)* - // Multiplied = (Expr) | Integer - // AddOp = + | - - // MulOp = * | / + // 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<"+" | "-">; From 294a4d73aa9816127ddad42b690c842077445b30 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Tue, 7 Mar 2023 02:20:28 +0100 Subject: [PATCH 11/21] test(parser): add more calc tests --- test/parser.test.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/parser.test.ts b/test/parser.test.ts index 466c805..5976910 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -211,12 +211,26 @@ describe("Parser", () => { ] > >; + type Calc = Eval< + P.Parse, Tuples.At<0>>, T> + >; - type res1 = Eval< - // ^? - P.Parse< - P.Map, Tuples.At<0>>, - "(3*2)/(4/2)-2" + 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: ""; + } > >; }); From 13a1c6c646d340719775f6af1e00f75e8bb5a8e6 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Tue, 7 Mar 2023 20:06:32 +0100 Subject: [PATCH 12/21] feat(parser): add json parsing --- src/internals/objects/Objects.ts | 14 ++++ src/internals/objects/impl/objects.ts | 7 ++ test/objects.test.ts | 16 +++++ test/parser.test.ts | 94 ++++++++++++++++++++++++++- 4 files changed, 129 insertions(+), 2 deletions(-) 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/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 index 5976910..39c7c71 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -7,6 +7,7 @@ import { arg1, Call, ComposeLeft, + Constant, Eval, Identity, } from "../src/internals/core/Core"; @@ -189,7 +190,10 @@ describe("Parser", () => { ComposeLeft<[Tuples.At<0>, Strings.ToNumber]> >; type Multiplied = P.Choice< - [P.Between, Expr, P.Literal<")">>, Integer] + [ + P.Between>, Expr, P.Trim>>, + Integer + ] >; type Added = P.Map< P.Sequence<[Multiplied, P.Many>]>, @@ -215,7 +219,7 @@ describe("Parser", () => { P.Parse, Tuples.At<0>>, T> >; - type res1 = Calc<"(3*2)/(4/2)-2">; + type res1 = Calc<"( 3*2 ) / ( 4/2 ) - 2">; // ^? type test1 = Expect>; type res2 = Calc<"3*(2-5)">; @@ -234,5 +238,91 @@ describe("Parser", () => { > >; }); + + it("should parse json grammar", () => { + // The grammar is defined as a recursive grammar: + // --------------------------------------------- + // | Value = Object | Array | String | Number | True | False | Null | + // | Object = { Members } | + // | Members = Pair (, Pair)* | + // | Pair = String : Value | + // | Array = [ Values ] | + // | Values = Value (, Value)* | + // | String = " Characters " | + // | Characters = Character* | + // | Character = any character except " or \ or control character | + // | Number = -? Digits ( . Digits )? | + // | Digits = [0-9]+ | + // | True = true | + // | False = false | + // | Null = null | + // --------------------------------------------- + type Value = P.Optional< + P.Choice< + [JSonObject, JSonArray, JSonString, JSonNumber, JsonBoolean, JSonNull] + > + >; + type JSonObject = P.Map< + P.Sequence< + [ + P.Trim>>, + P.SepBy>>, + P.Trim>> + ] + >, + Objects.FromArray + >; + type JSonPair = P.Optional< + P.Sequence<[JSonString, P.Trim>>, Value]> + >; + type JSonArray = P.Map< + P.Sequence< + [ + P.Trim>>, + P.SepBy>>, + P.Trim>> + ] + >, + Objects.Create<[arg0]> + >; + + type JSonString = P.Map< + P.Between< + P.Trim>>, + P.Many, + P.Trim>> + >, + 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", "foo": [1, 2, 3]}`>; + // ^? + type test1 = Expect>; + type res2 = Json<`[]`>; + // ^? + type test2 = Expect>; + type res3 = Json<`{}`>; + // ^? + type test3 = Expect>; + }); }); }); From b086378544c7ce51073768f22188e2c376538a31 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Tue, 7 Mar 2023 21:49:16 +0100 Subject: [PATCH 13/21] feat(parser): add notLiteral, any, trim left and right combinators --- README.md | 4 +++ src/internals/parser/Parser.ts | 65 ++++++++++++++++++++++++++++++++++ test/parser.test.ts | 28 +++++++-------- 3 files changed, 83 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 58aec2d..e595fcd 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ type res5 = Pipe< - [x] Parse - [x] ToString - [x] Literal + - [x] NotLiteral - [x] Optional - [x] Many - [x] Many1 @@ -279,6 +280,9 @@ type res5 = Pipe< - [x] Whitespace - [x] Whitespaces - [x] Trim + - [x] TrimLeft + - [x] TrimRight + - [x] Any - [x] Alpha - [x] AlphaNum - [x] Digit diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index a2c54a9..356d872 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -550,6 +550,45 @@ export namespace Parser { : 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, @@ -724,6 +763,32 @@ export namespace Parser { [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, diff --git a/test/parser.test.ts b/test/parser.test.ts index 39c7c71..019221f 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -257,29 +257,27 @@ describe("Parser", () => { // | False = false | // | Null = null | // --------------------------------------------- - type Value = P.Optional< - P.Choice< - [JSonObject, JSonArray, JSonString, JSonNumber, JsonBoolean, JSonNull] - > + type Value = P.Choice< + [JSonObject, JSonArray, JSonString, JSonNumber, JsonBoolean, JSonNull] >; type JSonObject = P.Map< P.Sequence< [ P.Trim>>, - P.SepBy>>, + P.Optional>>>, P.Trim>> ] >, Objects.FromArray >; - type JSonPair = P.Optional< - P.Sequence<[JSonString, P.Trim>>, Value]> + type JSonPair = P.Sequence< + [JSonString, P.Trim>>, Value] >; type JSonArray = P.Map< P.Sequence< [ P.Trim>>, - P.SepBy>>, + P.Optional>>>, P.Trim>> ] >, @@ -288,9 +286,9 @@ describe("Parser", () => { type JSonString = P.Map< P.Between< - P.Trim>>, - P.Many, - P.Trim>> + P.TrimLeft>>, + P.Many>, + P.TrimRight>> >, Tuples.Join<""> >; @@ -299,7 +297,7 @@ describe("Parser", () => { [ P.Optional>, P.Digits, - P.Optional, P.Digits]>> + P.Optional, P.Digits]>> ] >, ComposeLeft<[Tuples.Join<"">, Strings.ToNumber]> @@ -314,9 +312,11 @@ describe("Parser", () => { P.Parse, Tuples.At<0>>, T> >; - type res1 = Json<`{"hello": "world", "foo": [1, 2, 3]}`>; + type res1 = Json<`{"hello": " world! with @", "foo": [1.4, 2, 3]}`>; // ^? - type test1 = Expect>; + type test1 = Expect< + Equal + >; type res2 = Json<`[]`>; // ^? type test2 = Expect>; From ec8bbb92ea6c0232d437b8f23d1b84241bad56e5 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 12 Mar 2023 17:37:03 +0100 Subject: [PATCH 14/21] chore(parser): improve json test docs --- test/parser.test.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/parser.test.ts b/test/parser.test.ts index 019221f..a835820 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -241,22 +241,17 @@ describe("Parser", () => { it("should parse json grammar", () => { // The grammar is defined as a recursive grammar: - // --------------------------------------------- - // | Value = Object | Array | String | Number | True | False | Null | - // | Object = { Members } | - // | Members = Pair (, Pair)* | - // | Pair = String : Value | - // | Array = [ Values ] | - // | Values = Value (, Value)* | - // | String = " Characters " | - // | Characters = Character* | - // | Character = any character except " or \ or control character | - // | Number = -? Digits ( . Digits )? | - // | Digits = [0-9]+ | - // | True = true | - // | False = false | - // | Null = null | - // --------------------------------------------- + // ------------------------------------------------------------- + // | 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] >; @@ -292,6 +287,7 @@ describe("Parser", () => { >, Tuples.Join<""> >; + type JSonNumber = P.Map< P.Sequence< [ @@ -323,6 +319,10 @@ describe("Parser", () => { type res3 = Json<`{}`>; // ^? type test3 = Expect>; + // 4 deep nested objects + type res4 = Json<`{"a": {"b": {"c": {"d": 8}}}}`>; + // ^? + type test4 = Expect>; }); }); }); From 9035a2ee7a2d9b3f3e56092827a04ff21d777612 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Mon, 13 Mar 2023 00:13:46 +0100 Subject: [PATCH 15/21] feat(parser): add char ranges --- src/internals/parser/Parser.ts | 41 ++++++++++++++- src/internals/strings/Strings.ts | 1 - src/internals/strings/impl/chars.ts | 73 +++++++++++++++++++++++++++ src/internals/strings/impl/compare.ts | 28 +--------- 4 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 src/internals/strings/impl/chars.ts diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index 356d872..f5c2b78 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -10,9 +10,10 @@ import { _, } 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 @@ -498,6 +499,44 @@ export namespace Parser { : 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 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, From 5131829f22698c4459d8e9128fd424ecd584350b Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Mon, 13 Mar 2023 00:15:57 +0100 Subject: [PATCH 16/21] docs(parser): add CharRange --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e595fcd..0e1c159 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ type res5 = Pipe< - [x] TrimLeft - [x] TrimRight - [x] Any + - [x] CharRange - [x] Alpha - [x] AlphaNum - [x] Digit From 60aeb3ccc2c13e9fb67d7dadf250ae18b1396106 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sat, 15 Apr 2023 13:42:00 +0200 Subject: [PATCH 17/21] feat: add path parser tests --- test/parser.test.ts | 153 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/test/parser.test.ts b/test/parser.test.ts index a835820..cb34d8f 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -325,4 +325,157 @@ describe("Parser", () => { 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.Between, P.Digits, P.Literal<"}">>, + 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//posts//comments/ + // /api/v2/emails//lists/ + + // The grammar is defined as a recursive grammar for Extended Regular Expressions: + // ------------------------------------------------------------- + // | path = path_segment ( / path_segment )* + // | path_segment = path_parameter | path_literal + // | path_parameter = < name : type > + // | path_literal = word + // | name = word + // | type = string | number | boolean + // ------------------------------------------------------------- + + type path = P.Map< + P.Sequence< + [P.Skip>, P.SepBy>] + >, + Objects.FromArray + >; + type path_segment = P.Choice<[path_parameter, P.Skip]>; + type path_parameter = P.Between< + P.Literal<"<">, + P.Sequence<[P.Word, P.Skip>, type]>, + P.Literal<">"> + >; + type type = P.Map< + P.Choice< + [P.Literal<"string">, P.Literal<"number">, P.Literal<"boolean">] + >, + Match< + [ + Match.With<"string", string>, + Match.With<"number", number>, + Match.With<"boolean", boolean> + ] + > + >; + + type PathParams = Eval< + P.Parse, Tuples.At<0>>, T> + >; + + type res1 = + PathParams<"/api/v1/users//posts//comments/">; + type test1 = Expect< + Equal + >; + + type res2 = + PathParams<"/api/v2/emails//lists/">; + type test2 = Expect>; + + // should error + type res3 = + PathParams<"/api/v2/emails//lists/; + type test3 = Expect< + Equal< + res3, + { + message: never; + input: "')' - Received '' | Expected 'word()' - Received ' + >; + }); }); From 8ccaa745eedee525e66db01a1e18ea44d49766c7 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 16 Apr 2023 01:10:41 +0200 Subject: [PATCH 18/21] feat: add prefix and suffix parsers --- README.md | 2 ++ src/internals/parser/Parser.ts | 32 ++++++++++++++++++++++ test/parser.test.ts | 50 ++++++++++++++++++++-------------- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0e1c159..84a81dc 100644 --- a/README.md +++ b/README.md @@ -291,3 +291,5 @@ type res5 = Pipe< - [x] Word - [x] SepBy - [x] Between + - [x] Prefix + - [x] Sufix diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index f5c2b78..a5f35fb 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -761,6 +761,38 @@ export namespace Parser { [Skip, Parser, Skip] >; + /** + * 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 Prefix = Sequence<[Skip, 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 Suffix = Sequence<[Parser, Skip]>; + /** * Parser that matches whitespace characters. * @returns an Ok type if the parser matches or an error diff --git a/test/parser.test.ts b/test/parser.test.ts index cb34d8f..d40b8d2 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -419,7 +419,7 @@ describe("Parser", () => { // ------------------------------------------------------------- // | path = path_segment ( / path_segment )* // | path_segment = path_parameter | path_literal - // | path_parameter = < name : type > + // | path_parameter = { name : type } | { name } // | path_literal = word // | name = word // | type = string | number | boolean @@ -433,47 +433,55 @@ describe("Parser", () => { >; type path_segment = P.Choice<[path_parameter, P.Skip]>; type path_parameter = P.Between< - P.Literal<"<">, - P.Sequence<[P.Word, P.Skip>, type]>, - P.Literal<">"> - >; - type type = P.Map< - P.Choice< - [P.Literal<"string">, P.Literal<"number">, P.Literal<"boolean">] - >, - Match< + P.Literal<"{">, + P.Sequence< [ - Match.With<"string", string>, - Match.With<"number", number>, - Match.With<"boolean", boolean> + P.Trim, + P.Map< + P.Optional, type>>, + Match< + [ + Match.With<["string"], string>, + Match.With<["number"], number>, + Match.With<["boolean"], boolean>, + Match.With + ] + > + > ] - > + >, + P.Literal<"}"> >; + 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//posts//comments/">; + PathParams<"/api/v1/users/{ id : number }/posts/{postId:number}/comments/{commentId:number}/active/{active:boolean}">; type test1 = Expect< - Equal + Equal< + res1, + { id: number; postId: number; commentId: number; active: boolean } + > >; - type res2 = - PathParams<"/api/v2/emails//lists/">; + // should default to string + type res2 = PathParams<"/api/v2/emails/{email}/lists/{listEmail}">; type test2 = Expect>; // should error type res3 = - PathParams<"/api/v2/emails//lists/; + PathParams<"/api/v2/emails/{email:string}/lists/{listEmail:string">; type test3 = Expect< Equal< res3, { message: never; - input: "')' - Received '' | Expected 'word()' - Received ' >; From ec3b8c66ea8c04a00e7dcaee1785393432b5abc9 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 16 Apr 2023 01:28:27 +0200 Subject: [PATCH 19/21] chore: add two slash output in test --- test/parser.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/parser.test.ts b/test/parser.test.ts index d40b8d2..5879af9 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -460,6 +460,7 @@ describe("Parser", () => { // 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< @@ -470,10 +471,12 @@ describe("Parser", () => { // 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< From 350ce38aa327d3edd97eb3727b294f4f965cda5d Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 16 Apr 2023 01:32:51 +0200 Subject: [PATCH 20/21] fix: docs --- test/parser.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parser.test.ts b/test/parser.test.ts index 5879af9..863b05b 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -412,10 +412,10 @@ describe("Parser", () => { it("should parse complex endpoint routing grammar", () => { // examples route: - // /api/v1/users//posts//comments/ - // /api/v2/emails//lists/ + // /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 Extended Regular Expressions: + // The grammar is defined as a recursive grammar for routing paths. // ------------------------------------------------------------- // | path = path_segment ( / path_segment )* // | path_segment = path_parameter | path_literal From 8ea98fb6b9d605160b5726c9e5b223f5f2740bf1 Mon Sep 17 00:00:00 2001 From: ecyrbe Date: Sun, 16 Apr 2023 01:56:41 +0200 Subject: [PATCH 21/21] feat: add more literal helpers --- README.md | 8 ++++++-- src/internals/parser/Parser.ts | 29 +++++++++++++++++++++++++---- test/parser.test.ts | 14 ++++++-------- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 84a81dc..3748ae1 100644 --- a/README.md +++ b/README.md @@ -291,5 +291,9 @@ type res5 = Pipe< - [x] Word - [x] SepBy - [x] Between - - [x] Prefix - - [x] Sufix + - [x] PrefixBy + - [x] SufixBy + - [x] SepByLiteral + - [x] BetweenLiterals + - [x] PrefixByLiteral + - [x] SufixByLiteral diff --git a/src/internals/parser/Parser.ts b/src/internals/parser/Parser.ts index a5f35fb..cce3826 100644 --- a/src/internals/parser/Parser.ts +++ b/src/internals/parser/Parser.ts @@ -742,6 +742,11 @@ export namespace Parser { [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 @@ -761,6 +766,12 @@ export namespace Parser { [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 @@ -769,13 +780,18 @@ export namespace Parser { * * @example * ```ts - * type T0 = Call, Alpha>, ":a">; + * type T0 = Call, Alpha>, ":a">; * // ^? type T0 = Ok< "a", "" > - * type T1 = Call, Alpha>, "a">; + * type T1 = Call, Alpha>, "a">; * // ^? type T1 = Error<{ message: "Expected Literal(':') - Received 'a'"; cause: "";}> * ``` */ - export type Prefix = Sequence<[Skip, Parser]>; + 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. @@ -791,7 +807,12 @@ export namespace Parser { * // ^? type T1 = Error<{ message: "Expected Literal(':') - Received ''"; cause: "";}> * ``` */ - export type Suffix = Sequence<[Parser, Skip]>; + export type SuffixBy = Sequence<[Parser, Skip]>; + + export type SuffixByLiteral = SuffixBy< + Parser, + Literal + >; /** * Parser that matches whitespace characters. diff --git a/test/parser.test.ts b/test/parser.test.ts index 863b05b..7e7b424 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -387,7 +387,7 @@ describe("Parser", () => { type ERE_quant = P.Choice< [ P.Literal<"*" | "+" | "?">, - P.Between, P.Digits, 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<"}">] @@ -426,19 +426,17 @@ describe("Parser", () => { // ------------------------------------------------------------- type path = P.Map< - P.Sequence< - [P.Skip>, P.SepBy>] - >, + P.PrefixByLiteral<"/", P.SepByLiteral>, Objects.FromArray >; type path_segment = P.Choice<[path_parameter, P.Skip]>; - type path_parameter = P.Between< - P.Literal<"{">, + type path_parameter = P.BetweenLiterals< + "{", P.Sequence< [ P.Trim, P.Map< - P.Optional, type>>, + P.Optional>, Match< [ Match.With<["string"], string>, @@ -450,7 +448,7 @@ describe("Parser", () => { > ] >, - P.Literal<"}"> + "}" >; type type = P.Trim>;