From 7598445c85abea88fc8a057595c6b6900cf97241 Mon Sep 17 00:00:00 2001 From: James Forbes Date: Sat, 10 Feb 2024 13:39:11 +0000 Subject: [PATCH] Scaffolding main API logic --- lib/index.ts | 276 +++++++++++++++++++++---------------------------- lib/scratch.ts | 48 +++++++++ 2 files changed, 167 insertions(+), 157 deletions(-) create mode 100644 lib/scratch.ts diff --git a/lib/index.ts b/lib/index.ts index 749288f..9f50bdb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { safe } from "./match.js" + export type Either = | { type: 'Either', tag: 'Left', value: L } | { type: 'Either', tag: 'Right', value: R } -const Either = { +export const Either = { Left(value: L): Either{ return { type: 'Either', tag: 'Left', value } }, @@ -13,206 +15,166 @@ const Either = { } } -type Patterns = string | string[] -type DefinitionResponse = [any,Patterns] | Patterns -type Definition = Record DefinitionResponse> -type Constructors = { +export type Patterns = string | string[] +export type DefinitionResponse = [any,Patterns] | Patterns +export type Definition = Record DefinitionResponse> +export type Constructors = { [R in keyof D]: { (value: Parameters[0]): { type: N, tag: R, value: Parameters[0] } } } -type MatchOptions = { +export type MatchOptions = { [R in keyof D]: { (value: Parameters[0]): T } } -type API = { +export type API = { definition: { [R in keyof D]: D[R] } patterns: { [R in keyof D]: string[] } - toURL( instance: Instance ): string - toURLSafe( instance: Instance ): Either - parseURL( url: string ): Instance - parseURLSafe( url: string ): Either> - match: ( options: MatchOptions ) => (instance: Instance ) => T + toUrl( instance: Instance ): string + toUrlSafe( instance: Instance ): Either + parseUrl( url: string ): Instance + parseUrlSafe( url: string ): Either> + match: (instance: Instance, options: MatchOptions ) => T } -type Is = `is${R}` +export type Is = `is${R}` -type Instance = ReturnType[K]> +export type Instance = ReturnType[K]> -type Superoute = Constructors & API & RouteIs +export type Superoute = Constructors & API & RouteIs -type RouteIs = { +export type RouteIs = { [Key in keyof D as Is]: (route: Instance) => boolean } -function doesPathMatchPattern(path: string, pattern: string) : boolean { - let done = false - let i = 0; - - type Mode = 'investigating'| 'expecting-literal' | 'expecting-variable' | 'accepting-remainder'; - let mode: Mode; - - // parse the first segment of the pattern until we hit a `/` or the end of the string - // if it a literal, change mode to expecting literal, and start iterating through path, if any character doesn't match, exit early (false) - // if everything matches, and you reach a `/` start the same process again - // if the next segment is a variable, just store all the characters until you hit the next segment - // if you reach the end of the path | pattern without failing on a literal, its a match - // we could optionally return a score (number of matches weighted to longer/more complex strings?) but we can know the complexity at definition time - // so if its a match the complexity should be that const number. - - let patternIndex = 0; - let pathIndex = 0; - mode = 'investigating' - - // we add an extra terminator so every if/else - // only checks for that delimiter, instead of the delimiter + end of string - path = path + '/' - pattern = pattern + '/' +function match(instance: any, options: any): any { + return options[instance.tag](instance.value) +} - let vars : Record = {} - while ( !done ) { - - if (mode != 'accepting-remainder' && (path[pathIndex] == null || pattern[patternIndex] == null)) { - // todo-james return what was expected - return false; +export function type(type: N, routes: D): Superoute { + function toUrlSafe(route: any): Either { + const errors = [] + const paths = [] + const ranks: Record = {} + let bestPath : string[] | null = null; + let bestRank = 0; + let error : string | null = null + for(const pattern of api.patterns[route.tag]) { + + const path: string[] = [] + let rank = 0 + for( const segment of pattern.split('/') ) { + if (segment.startsWith(':')){ + const name = segment.slice(1) + if (name in route.value) { + path.push(route.value) + rank += 2 + } else { + error = `Could not build pattern ${pattern} from value ${JSON.stringify(route.value)} as '${name} was undefined'` + errors.push(error) + break + } + } else { + rank += 4 + } + } + if (path != null) { + paths.push(path) + ranks[pattern] = rank + if ( rank > bestRank || bestPath == null ) { + bestRank = rank + bestPath = path + } + continue; + } } - // just skips repeated forward slashes - // we treat /a/b/////c as the same as /a/b/c as its usually a bug - // in the users code or a user doing something strange - let wasNewPatternSegment = false; - let wasNewPathSegment = false; - while (pattern[patternIndex] === '/') { - wasNewPatternSegment = true; - patternIndex++ + if (bestPath) { + return { type: 'Either', tag: 'Right', value: bestPath.join('/') } + } else if (errors.length) { + return { type: 'Either', tag: 'Left', value: new Error(errors[0])} + } else { + throw new Error(`Unexpected failure converting route ${route} to url`) } - - while (path[pathIndex] === '/') { - wasNewPathSegment = true; - pathIndex++ + } + + function toURL(route: any){ + const result = toUrlSafe(route) + if (result.tag === 'Left') { + throw result.value + } else { + return result.value } + } - switch (mode) { - case 'investigating': { - - if ( wasNewPatternSegment && pattern[patternIndex] == ':') { - mode = 'expecting-variable' - } else if ( wasNewPatternSegment ) { - mode = 'expecting-literal' - } else if - mode = 'accepting-remainder' - console.log('hi') - break; - } - case 'expecting-variable': { - let varName = '' - let varValue = '' - while (pattern[patternIndex] !== '/') { - patternIndex++ - varName += pattern[patternIndex] - } - while (path[pathIndex] !== '/') { - pathIndex++ - varValue += path[pathIndex] + function parseUrlSafe( url: string ): Either { + let bestRank = 0; + let bestRoute: any = null; + let error: any = null; + for (const [tag, patterns] of Object.keys(api.patterns as string[])) { + for( const pattern of patterns) { + const result = safe(url,pattern) + if (result.tag === 'Left') { + if ( error == null ) { + error = result + } + } else { + if (bestRoute == null || result.value.score > bestRank) { + bestRoute = { type, tag, value: result.value } + bestRank = result.value.score + } } - vars[varName] = varValue } - case 'expecting-literal': { - let literalExpected = '' - let literalFound = '' - while (pattern[patternIndex] !== '/') { - patternIndex++ - varName += pattern[patternIndex] - } - while (path[pathIndex] !== '/') { - pathIndex++ - varValue += path[pathIndex] + } + + if ( bestRoute == null ) { + if ( error ) { + return error + } else { + return { + type: 'Either', + tag: 'Left', + value: new Error( + `Could not parse url ${url} into any pattern on type ${type}` + ) } - vars[varName] = varValue } - case 'parsing-pattern-segment': { - - console.log('2') + } else { + return { + type: 'Either', + tag: 'Right', + value: bestRoute } - break; } - pattern[i] } -} - -export function parseURLSafe( type: API, url: string ): Either> { - let winner : K | null = null; - for( let x of Object.entries(type.regex) ) { - x + function parseUrl(route: any): any { + const res = parseUrlSafe(route) + if (res.tag == 'Left') { + throw res.value + } } - return Either.Left( new Error('Hello')) -} -export function type(type: N, routes: D): Superoute { const api: any = { patterns: {}, - regex: {} + definition: routes, + toURL, + toUrlSafe, + parseUrl, + parseUrlSafe, + match } for( const [tag, of] of Object.entries(routes) ) { api[tag] = (value={}) => ({ type, tag, value }) const [_, pattern] = of({}) - api.patterns[tag] = pattern - api.regex[tag] = re.pathToRegexp(pattern) - api.complexity[tag] = pattern.split('/').map( x => x.startsWith(':') ? 2 : 1 ) + api.patterns[tag] = ([] as string[]).concat(pattern) } - return null as any as Superoute + return api as any as Superoute } -type Value< I extends (v:any) => any> = Parameters[0] - - -const Example = type('Example', { - Welcome: (_: { name?: string }) => [`/welcome/:name`, `/welcome`], - Login: (_: { error?: string }) => [`/login/error/:error`, `/login`] -}) - -type Example = typeof Example["definition"] - - -// Rough type definition of mithril component -type Component = (v: { attrs: T}) => ({ view: (v: { attrs: T }) => any }) - -const WelcomeComp: Component< Value > = () => ({ view: (v) => `Welcome ${v.attrs.name ?? 'User'}`}) -const LoginComp: Component< Value > = () => ({ view: (v) => [ - v.attrs.error ? 'There was an error: ' + v.attrs.error : null, - 'Please login using your username and password.' -]}) - -type Components = { - [K in keyof D]: Component< Parameters[0] > -} - -const Components : Components = { - Welcome: WelcomeComp, - Login: LoginComp -} - -m.route( - document.body - ,'/' - , Object.fromEntries(Object.entries(Example.patterns).flatMap( ([k,v]) => v.map( v2 => [v2, k]) )) -) - -// const AComp : Component = () => ({ view: () => null }) - - -// const Component : Components = { -// A: Comp -// } - -// const a = Example.A({ a_id: 'hello' }) +export type Value< I extends (v:any) => any> = Parameters[0] -// const fn = Example.match({ -// A: ({a_id}) => 1, -// B: ({ b_id }) => 2 -// }) diff --git a/lib/scratch.ts b/lib/scratch.ts new file mode 100644 index 0000000..1deeb68 --- /dev/null +++ b/lib/scratch.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as superouter from './index.js' + +const Example = superouter.type('Example', { + Welcome: (_: { name?: string }) => [`/welcome/:name`, `/welcome`], + Login: (_: { error?: string }) => [`/login/error/:error`, `/login`] +}) + +type Example = typeof Example["definition"] + + +// Rough type definition of mithril component +type Component = (v: { attrs: T}) => ({ view: (v: { attrs: T }) => any }) + +const WelcomeComp: Component > = () => ({ view: (v) => `Welcome ${v.attrs.name ?? 'User'}`}) +const LoginComp: Component > = () => ({ view: (v) => [ + v.attrs.error ? 'There was an error: ' + v.attrs.error : null, + 'Please login using your username and password.' +]}) + +type Components = { + [K in keyof D]: Component< Parameters[0] > +} + +const Components : Components = { + Welcome: WelcomeComp, + Login: LoginComp +} + +m.route( + document.body + ,'/' + , Object.fromEntries(Object.entries(Example.patterns).flatMap( ([k,v]) => v.map( v2 => [v2, k]) )) +) + +// const AComp : Component = () => ({ view: () => null }) + + +// const Component : Components = { +// A: Comp +// } + +// const a = Example.A({ a_id: 'hello' }) + +// const fn = Example.match({ +// A: ({a_id}) => 1, +// B: ({ b_id }) => 2 +// })