Created
January 20, 2026 22:08
-
-
Save prenaissance/c9c13ba0aec45113c6043ea1bd932cd4 to your computer and use it in GitHub Desktop.
Typescript Http protocol header parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type Input = { | |
| text: string; | |
| index: number; | |
| }; | |
| type ParseResult<T> = | |
| | { success: true; value: T; remaining: Input } | |
| | { success: false }; | |
| type InferParser<U> = U extends P<infer T> ? T : never; | |
| class P<T> { | |
| constructor(private parseFn: (input: Input) => ParseResult<T>) {} | |
| parse(inputText: string): ParseResult<T> { | |
| const input: Input = { text: inputText, index: 0 }; | |
| return this.parseFn(input); | |
| } | |
| private run(input: Input): ParseResult<T> { | |
| return this.parseFn(input); | |
| } | |
| static char<C extends string>(c: C): P<C> { | |
| return new P<C>((input) => { | |
| if (input.index < input.text.length && input.text[input.index] === c) { | |
| return { | |
| success: true, | |
| value: c, | |
| remaining: { text: input.text, index: input.index + 1 }, | |
| }; | |
| } | |
| return { success: false }; | |
| }); | |
| } | |
| static string<S extends string>(str: S): P<S> { | |
| return new P<S>((input) => { | |
| if (input.text.slice(input.index, input.index + str.length) === str) { | |
| return { | |
| success: true, | |
| value: str, | |
| remaining: { text: input.text, index: input.index + str.length }, | |
| }; | |
| } | |
| return { success: false }; | |
| }); | |
| } | |
| static regex(regex: RegExp): P<string> { | |
| return new P<string>((input) => { | |
| const match = regex.exec(input.text.slice(input.index)); | |
| if (match && match.index === 0) { | |
| const matchedString = match[0]; | |
| return { | |
| success: true, | |
| value: matchedString, | |
| remaining: { | |
| text: input.text, | |
| index: input.index + matchedString.length, | |
| }, | |
| }; | |
| } | |
| return { success: false }; | |
| }); | |
| } | |
| static seq<T1, T2>(parsers: [P<T1>, P<T2>]): P<[T1, T2]>; | |
| static seq<T1, T2, T3>(parsers: [P<T1>, P<T2>, P<T3>]): P<[T1, T2, T3]>; | |
| static seq<T1, T2, T3, T4>( | |
| parsers: [P<T1>, P<T2>, P<T3>, P<T4>], | |
| ): P<[T1, T2, T3, T4]>; | |
| static seq<T1, T2, T3, T4, T5>( | |
| parsers: [P<T1>, P<T2>, P<T3>, P<T4>, P<T5>], | |
| ): P<[T1, T2, T3, T4, T5]>; | |
| static seq(parsers: P<unknown>[]): P<unknown[]> { | |
| return new P<unknown[]>((input) => { | |
| const values: unknown[] = []; | |
| let currentInput = input; | |
| for (const parser of parsers) { | |
| const result = parser.run(currentInput); | |
| if (!result.success) { | |
| return { success: false }; | |
| } | |
| values.push(result.value); | |
| currentInput = result.remaining; | |
| } | |
| return { success: true, value: values, remaining: currentInput }; | |
| }); | |
| } | |
| or<U>(other: P<U>): P<T | U> { | |
| return new P<T | U>((input) => { | |
| const result = this.run(input); | |
| if (result.success) { | |
| return result; | |
| } | |
| return other.run(input); | |
| }); | |
| } | |
| map<U>(fn: (value: T) => U): P<U> { | |
| return new P<U>((input) => { | |
| const result = this.run(input); | |
| if (result.success) { | |
| return { | |
| success: true, | |
| value: fn(result.value), | |
| remaining: result.remaining, | |
| }; | |
| } | |
| return { success: false }; | |
| }); | |
| } | |
| } | |
| const method = P.string("GET") | |
| .or(P.string("POST")) | |
| .or(P.string("PUT")) | |
| .or(P.string("PATCH")) | |
| .or(P.string("DELETE")); | |
| const space = P.char(" "); | |
| const path = P.regex(/\/[a-zA-Z0-9.-~/]*/); | |
| const version = P.string("1.1").or(P.string("2")).or(P.string("3")); | |
| const httpVersion = P.seq([P.string("HTTP/"), version]).map(([, ver]) => ver); | |
| const requestLine = P.seq([method, space, path, space, httpVersion]).map( | |
| ([method, , path, , version]) => ({ method, path, version }), | |
| ); | |
| // biome-ignore lint/correctness/noUnusedVariables: example | |
| type ParsedHttpRequestLine = InferParser<typeof requestLine>; | |
| const result = requestLine.parse("GET /api/profile/avatar.webp HTTP/2"); | |
| console.log(result); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment