All files / jit-router/src router.ts

82.66% Statements 62/75
80% Branches 24/30
45.83% Functions 11/24
86.36% Lines 57/66

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 1438x 8x   8x 8x 8x             8x 26x   26x     1971x 1971x               26x 26x 1971x 1973x     26x       26x 26x 26x 26x 26x                                       8x     1984x 1984x           1984x 1984x   1984x                               8x   1989x 1989x 1989x 1989x 1545x 1545x 1545x 1545x   1989x 1989x 1989x 1989x 3988x 3988x 3988x 1545x 1545x 1545x 1524x 1524x   21x     2443x     1989x     1989x     13x 35x 13x               8x   1984x 1984x      
import {JsExpression} from '@jsonjoy.com/codegen/lib/util/JsExpression';
import {printTree} from 'sonic-forest/lib/print/printTree';
import type {Printable} from 'sonic-forest/lib/print/types';
import {type RouteMatcher, RouterCodegenCtx, RouterCodegenOpts} from './codegen';
import {ExactStep, RegexStep, UntilStep} from './steps';
import {RoutingTreeNode} from './tree';
import type {Step} from './types';
 
export interface RouterOptions {
  defaultUntil?: string;
}
 
export class Router<Data = unknown> implements Printable {
  public readonly destinations: Destination[] = [];
 
  constructor(public readonly options: RouterOptions = {}) {}
 
  public add(route: string | string[], data: Data) {
    const destination = Destination.from(route, data, this.options.defaultUntil);
    this.destinations.push(destination);
  }
 
  public addDestination(destination: Destination) {
    this.destinations.push(destination);
  }
 
  public tree(): RoutingTreeNode {
    const tree = new RoutingTreeNode();
    for (const destination of this.destinations) {
      for (const route of destination.routes) {
        tree.add(route, 0, destination);
      }
    }
    return tree;
  }
 
  public compile(): RouteMatcher<Data> {
    const ctx = new RouterCodegenCtx();
    const node = new RouterCodegenOpts(new JsExpression(() => 'str'), '0');
    const tree = this.tree();
    tree.codegen(ctx, node);
    return ctx.codegen.compile() as RouteMatcher<Data>;
  }
 
  public toString(tab: string = '') {
    return (
      `${this.constructor.name}` +
      printTree(tab, [
        (tab) =>
          'Destinations' +
          printTree(
            tab,
            this.destinations.map((d, i) => (tab) => `[${i}]: ` + d.toString(tab + ' ')),
          ),
        () => '',
        (tab) => 'RoutingTree' + printTree(tab, [(tab) => this.tree().toString(tab)]),
      ])
    );
  }
}
 
export class Destination implements Printable {
  public static from(def: string | string[], data: unknown, defaultUntil?: string): Destination {
    const routes =
      typeof def === 'string' ? [Route.from(def, defaultUntil)] : def.map((r) => Route.from(r, defaultUntil));
    return new Destination(routes, data);
  }
 
  public readonly match: Match;
 
  constructor(
    public readonly routes: Route[],
    public readonly data: unknown,
  ) {
    this.match = new Match(data, []);
  }
 
  public toString(tab: string = '') {
    return (
      `${this.constructor.name} ` +
      (this.routes.length === 1
        ? this.routes[0].toString(tab)
        : printTree(
            tab,
            this.routes.map((r) => (tab) => r.toString(tab)),
          ))
    );
  }
}
 
export class Route implements Printable {
  public static from(str: string, defaultUntil = '/'): Route {
    const tokens: string[] = [];
    const matches = str.match(/\{[^\}]*\}/g);
    let i = 0;
    for (const match of matches ?? []) {
      const index = str.indexOf(match, i);
      if (index > i) tokens.push(str.substring(i, index));
      tokens.push(match);
      i = index + match.length;
    }
    if (i < str.length) tokens.push(str.substring(i));
    const steps: Step[] = [];
    const length = tokens.length;
    for (let i = 0; i < length; i++) {
      const token = tokens[i];
      const isParameter = token.startsWith('{') && token.endsWith('}');
      if (isParameter) {
        const content = token.substring(1, token.length - 1);
        const [name = '', regex = '', until = ''] = content.split(':');
        if (!regex || regex === '*') {
          const next = tokens[i + 1];
          steps.push(new UntilStep(name, until || (next ? next[0] : defaultUntil)));
        } else {
          steps.push(new RegexStep(name, regex, until));
        }
      } else {
        steps.push(new ExactStep(token));
      }
    }
    return new Route(steps);
  }
 
  constructor(public readonly steps: Step[]) {}
 
  public toText() {
    let str = '';
    for (const step in this.steps) str += this.steps[step].toText();
    return str;
  }
 
  public toString(tab: string = '') {
    return this.toText();
  }
}
 
export class Match<Data = unknown> {
  constructor(
    public readonly data: Data,
    public params: string[] | null,
  ) {}
}