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 1426x 6x   6x 6x 6x             6x 7x   7x     81x 81x               7x 7x 81x 82x     7x       7x 7x 7x 7x 7x                                       6x   93x 93x           93x 93x   93x                               6x   94x 94x 94x 94x 109x 109x 109x 109x   94x 94x 94x 94x 252x 252x 252x 109x 109x 109x 98x 98x   11x     143x     94x     94x     12x 31x 12x               6x   93x 93x      
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));
    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);
      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,
  ) {}
}