Skip to content

Commit

Permalink
Scaffolding main API logic
Browse files Browse the repository at this point in the history
  • Loading branch information
JAForbes committed Feb 10, 2024
1 parent eabccb0 commit 7598445
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 157 deletions.
276 changes: 119 additions & 157 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { safe } from "./match.js"

export type Either<L, R> =
| { type: 'Either', tag: 'Left', value: L }
| { type: 'Either', tag: 'Right', value: R }

const Either = {
export const Either = {
Left<L,R>(value: L): Either<L,R>{
return { type: 'Either', tag: 'Left', value }
},
Expand All @@ -13,206 +15,166 @@ const Either = {
}
}

type Patterns = string | string[]
type DefinitionResponse = [any,Patterns] | Patterns
type Definition = Record<string, (v: any) => DefinitionResponse>
type Constructors<N extends string, D extends Definition> = {
export type Patterns = string | string[]
export type DefinitionResponse = [any,Patterns] | Patterns
export type Definition = Record<string, (v: any) => DefinitionResponse>
export type Constructors<N extends string, D extends Definition> = {
[R in keyof D]: {
(value: Parameters<D[R]>[0]): { type: N, tag: R, value: Parameters<D[R]>[0] }
}
}
type MatchOptions<N extends string, D extends Definition, T> = {
export type MatchOptions<D extends Definition, T> = {
[R in keyof D]: {
(value: Parameters<D[R]>[0]): T
}
}
type API<N extends string, D extends Definition> = {
export type API<N extends string, D extends Definition> = {
definition: { [R in keyof D]: D[R] }
patterns: { [R in keyof D]: string[] }
toURL( instance: Instance<N, D, keyof D> ): string
toURLSafe( instance: Instance<N, D, keyof D> ): Either<Error, string>
parseURL( url: string ): Instance<N, D, keyof D>
parseURLSafe( url: string ): Either<Error, Instance<N, D, keyof D>>
match: <T>( options: MatchOptions<N,D,T> ) => (instance: Instance<N, D, keyof D> ) => T
toUrl( instance: Instance<N, D, keyof D> ): string
toUrlSafe( instance: Instance<N, D, keyof D> ): Either<Error, string>
parseUrl( url: string ): Instance<N, D, keyof D>
parseUrlSafe( url: string ): Either<Error, Instance<N, D, keyof D>>
match: <T>(instance: Instance<N, D, keyof D>, options: MatchOptions<D,T> ) => T
}

type Is<R extends string> = `is${R}`
export type Is<R extends string> = `is${R}`


type Instance<N extends string, D extends Definition, K extends keyof D> = ReturnType<Constructors<N,D>[K]>
export type Instance<N extends string, D extends Definition, K extends keyof D> = ReturnType<Constructors<N,D>[K]>

type Superoute<N extends string, D extends Definition> = Constructors<N,D> & API<N,D> & RouteIs<N,D>
export type Superoute<N extends string, D extends Definition> = Constructors<N,D> & API<N,D> & RouteIs<N,D>


type RouteIs<N extends string, D extends Definition> = {
export type RouteIs<N extends string, D extends Definition> = {
[Key in keyof D as Is<Key extends string ? Key : never>]: (route: Instance<N, D, keyof D>) => 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<string,string> = {}
while ( !done ) {

if (mode != 'accepting-remainder' && (path[pathIndex] == null || pattern[patternIndex] == null)) {
// todo-james return what was expected
return false;
export function type<N extends string, D extends Definition>(type: N, routes: D): Superoute<N,D> {
function toUrlSafe(route: any): Either<Error, string> {
const errors = []
const paths = []
const ranks: Record<string, number> = {}
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<Error, any> {
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<N extends string, D extends Definition, K extends keyof D>( type: API<N, D>, url: string ): Either<Error, Instance<N, D, K>> {

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<N extends string, D extends Definition>(type: N, routes: D): Superoute<N,D> {
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<N,D>
return api as any as Superoute<N,D>
}

type Value< I extends (v:any) => any> = Parameters<I>[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<T> = (v: { attrs: T}) => ({ view: (v: { attrs: T }) => any })

const WelcomeComp: Component< Value<Example["Welcome"]> > = () => ({ view: (v) => `Welcome ${v.attrs.name ?? 'User'}`})
const LoginComp: Component< Value<Example["Login"]> > = () => ({ view: (v) => [
v.attrs.error ? 'There was an error: ' + v.attrs.error : null,
'Please login using your username and password.'
]})

type Components<D extends Definition> = {
[K in keyof D]: Component< Parameters<D[K]>[0] >
}

const Components : Components<Example> = {
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<typeof Example.A> = () => ({ view: () => null })


// const Component : Components<Example["definition"]> = {
// A: Comp
// }

// const a = Example.A({ a_id: 'hello' })
export type Value< I extends (v:any) => any> = Parameters<I>[0]

// const fn = Example.match({
// A: ({a_id}) => 1,
// B: ({ b_id }) => 2
// })
48 changes: 48 additions & 0 deletions lib/scratch.ts
Original file line number Diff line number Diff line change
@@ -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<T> = (v: { attrs: T}) => ({ view: (v: { attrs: T }) => any })

const WelcomeComp: Component<superouter.Value<Example["Welcome"]> > = () => ({ view: (v) => `Welcome ${v.attrs.name ?? 'User'}`})
const LoginComp: Component<superouter.Value<Example["Login"]> > = () => ({ view: (v) => [
v.attrs.error ? 'There was an error: ' + v.attrs.error : null,
'Please login using your username and password.'
]})

type Components<D extends superouter.Definition> = {
[K in keyof D]: Component< Parameters<D[K]>[0] >
}

const Components : Components<Example> = {
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<typeof Example.A> = () => ({ view: () => null })


// const Component : Components<Example["definition"]> = {
// A: Comp
// }

// const a = Example.A({ a_id: 'hello' })

// const fn = Example.match({
// A: ({a_id}) => 1,
// B: ({ b_id }) => 2
// })

0 comments on commit 7598445

Please sign in to comment.