feat: implement default url serializer
This commit is contained in:
parent
aee764d14d
commit
4b1db0e61c
|
@ -6,17 +6,17 @@ export class Tree<T> {
|
|||
|
||||
get root(): T { return this._root.value; }
|
||||
|
||||
parent(t: T): T {
|
||||
parent(t: T): T | null {
|
||||
const p = this.pathFromRoot(t);
|
||||
return p.length > 1 ? p[p.length - 2] : null;
|
||||
}
|
||||
|
||||
children(t: T): T[] {
|
||||
const n = _findNode(t, this._root);
|
||||
return n ? n.children.map(t => t.value) : null;
|
||||
return n ? n.children.map(t => t.value) : [];
|
||||
}
|
||||
|
||||
firstChild(t: T): T {
|
||||
firstChild(t: T): T | null {
|
||||
const n = _findNode(t, this._root);
|
||||
return n && n.children.length > 0 ? n.children[0].value : null;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export function rootNode<T>(tree: Tree<T>): TreeNode<T> {
|
|||
return tree._root;
|
||||
}
|
||||
|
||||
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
|
||||
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> | null {
|
||||
if (expected === c.value) return c;
|
||||
for (let cc of c.children) {
|
||||
const r = _findNode(expected, cc);
|
||||
|
@ -49,7 +49,7 @@ function _findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): Tr
|
|||
if (r) return r;
|
||||
}
|
||||
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
function _contains<T>(tree: TreeNode<T>, subtree: TreeNode<T>): boolean {
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
import { UrlTree, UrlSegment } from './url_tree';
|
||||
import { rootNode, TreeNode } from './tree';
|
||||
|
||||
/**
|
||||
* Defines a way to serialize/deserialize a url tree.
|
||||
*/
|
||||
export abstract class UrlSerializer {
|
||||
/**
|
||||
* Parse a url into a {@Link UrlTree}
|
||||
*/
|
||||
abstract parse(url: string): UrlTree;
|
||||
|
||||
/**
|
||||
* Converts a {@Link UrlTree} into a url
|
||||
*/
|
||||
abstract serialize(tree: UrlTree): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of the serialization.
|
||||
*/
|
||||
export class DefaultUrlSerializer implements UrlSerializer {
|
||||
parse(url: string): UrlTree {
|
||||
const p = new UrlParser(url);
|
||||
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
|
||||
}
|
||||
|
||||
serialize(tree: UrlTree): string {
|
||||
const node = serializeUrlTreeNode(rootNode(tree));
|
||||
const query = serializeQueryParams(tree.queryParameters);
|
||||
const fragment = tree.fragment !== null ? `#${tree.fragment}` : '';
|
||||
return `${node}${query}${fragment}`;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeUrlTreeNode(node: TreeNode<UrlSegment>): string {
|
||||
return `${serializeSegment(node.value)}${serializeChildren(node)}`;
|
||||
}
|
||||
|
||||
function serializeUrlTreeNodes(nodes: TreeNode<UrlSegment>[]): string {
|
||||
const primary = serializeSegment(nodes[0].value);
|
||||
const secondaryNodes = nodes.slice(1);
|
||||
const secondary = secondaryNodes.length > 0 ? `(${secondaryNodes.map(serializeUrlTreeNode).join("//")})` : "";
|
||||
const children = serializeChildren(nodes[0]);
|
||||
return `${primary}${secondary}${children}`;
|
||||
}
|
||||
|
||||
function serializeChildren(node: TreeNode<UrlSegment>): string {
|
||||
if (node.children.length > 0) {
|
||||
return `/${serializeUrlTreeNodes(node.children)}`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeSegment(segment: UrlSegment): string {
|
||||
return `${segment.segment}${serializeParams(segment.parameters)}`;
|
||||
}
|
||||
|
||||
function serializeParams(params: {[key: string]: string}): string {
|
||||
return pairs(params).map(p => `;${p.first}=${p.second}`).join("");
|
||||
}
|
||||
|
||||
function serializeQueryParams(params: {[key: string]: string}): string {
|
||||
const strs = pairs(params).map(p => `${p.first}=${p.second}`);
|
||||
return strs.length > 0 ? `?${strs.join("&")}` : "";
|
||||
}
|
||||
|
||||
class Pair<A,B> { constructor(public first:A, public second:B) {} }
|
||||
function pairs<T>(obj: {[key: string]: T}):Pair<string,T>[] {
|
||||
const res = [];
|
||||
for (let prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res.push(new Pair<string, T>(prop, obj[prop]));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
|
||||
function matchUrlSegment(str: string): string {
|
||||
SEGMENT_RE.lastIndex = 0;
|
||||
var match = SEGMENT_RE.exec(str);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
const QUERY_PARAM_VALUE_RE = /^[^\(\)\?;&#]+/;
|
||||
function matchUrlQueryParamValue(str: string): string {
|
||||
QUERY_PARAM_VALUE_RE.lastIndex = 0;
|
||||
const match = QUERY_PARAM_VALUE_RE.exec(str);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
class UrlParser {
|
||||
constructor(private remaining: string) {}
|
||||
|
||||
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
||||
|
||||
capture(str: string): void {
|
||||
if (!this.remaining.startsWith(str)) {
|
||||
throw new Error(`Expected "${str}".`);
|
||||
}
|
||||
this.remaining = this.remaining.substring(str.length);
|
||||
}
|
||||
|
||||
parseRootSegment(): TreeNode<UrlSegment> {
|
||||
if (this.remaining == '' || this.remaining == '/') {
|
||||
return new TreeNode<UrlSegment>(new UrlSegment('', {}), []);
|
||||
} else {
|
||||
let segments = this.parseSegments();
|
||||
return new TreeNode<UrlSegment>(new UrlSegment('', {}), segments);
|
||||
}
|
||||
}
|
||||
|
||||
parseSegments(): TreeNode<UrlSegment>[] {
|
||||
if (this.remaining.length == 0) {
|
||||
return [];
|
||||
}
|
||||
if (this.peekStartsWith('/')) {
|
||||
this.capture('/');
|
||||
}
|
||||
var path = matchUrlSegment(this.remaining);
|
||||
this.capture(path);
|
||||
|
||||
var matrixParams: {[key: string]: any} = {};
|
||||
if (this.peekStartsWith(';')) {
|
||||
matrixParams = this.parseMatrixParams();
|
||||
}
|
||||
|
||||
var secondary = [];
|
||||
if (this.peekStartsWith('(')) {
|
||||
secondary = this.parseSecondarySegments();
|
||||
}
|
||||
|
||||
var children: TreeNode<UrlSegment>[] = [];
|
||||
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||||
this.capture('/');
|
||||
children = this.parseSegments();
|
||||
}
|
||||
|
||||
let segment = new UrlSegment(path, matrixParams);
|
||||
let node = new TreeNode<UrlSegment>(segment, children);
|
||||
return [node].concat(secondary);
|
||||
}
|
||||
|
||||
parseQueryParams(): {[key: string]: any} {
|
||||
var params: {[key: string]: any} = {};
|
||||
if (this.peekStartsWith('?')) {
|
||||
this.capture('?');
|
||||
this.parseQueryParam(params);
|
||||
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
|
||||
this.capture('&');
|
||||
this.parseQueryParam(params);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseFragment(): string | null {
|
||||
if (this.peekStartsWith('#')) {
|
||||
return this.remaining.substring(1);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
parseMatrixParams(): {[key: string]: any} {
|
||||
var params: {[key: string]: any} = {};
|
||||
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
|
||||
this.capture(';');
|
||||
this.parseParam(params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseParam(params: {[key: string]: any}): void {
|
||||
var key = matchUrlSegment(this.remaining);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
this.capture(key);
|
||||
var value: any = "true";
|
||||
if (this.peekStartsWith('=')) {
|
||||
this.capture('=');
|
||||
var valueMatch = matchUrlSegment(this.remaining);
|
||||
if (valueMatch) {
|
||||
value = valueMatch;
|
||||
this.capture(value);
|
||||
}
|
||||
}
|
||||
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
parseQueryParam(params: {[key: string]: any}): void {
|
||||
var key = matchUrlSegment(this.remaining);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
this.capture(key);
|
||||
var value: any = "true";
|
||||
if (this.peekStartsWith('=')) {
|
||||
this.capture('=');
|
||||
var valueMatch = matchUrlQueryParamValue(this.remaining);
|
||||
if (valueMatch) {
|
||||
value = valueMatch;
|
||||
this.capture(value);
|
||||
}
|
||||
}
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
parseSecondarySegments(): TreeNode<UrlSegment>[] {
|
||||
var segments = [];
|
||||
this.capture('(');
|
||||
|
||||
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
||||
segments = segments.concat(this.parseSegments());
|
||||
if (this.peekStartsWith('//')) {
|
||||
this.capture('//');
|
||||
}
|
||||
}
|
||||
this.capture(')');
|
||||
|
||||
return segments;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Tree, TreeNode } from './tree';
|
||||
|
||||
export class UrlTree extends Tree<UrlSegment> {
|
||||
constructor(root: TreeNode<UrlSegment>, public queryParameters: {[key: string]: string}, public fragment: string | null) {
|
||||
super(root);
|
||||
}
|
||||
}
|
||||
|
||||
export class UrlSegment {
|
||||
constructor(public segment: any, public parameters: {[key: string]: string}) {}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import {DefaultUrlSerializer, serializeSegment} from '../src/url_serializer';
|
||||
import {UrlSegment} from '../src/url_tree';
|
||||
|
||||
describe('url serializer', () => {
|
||||
let url = new DefaultUrlSerializer();
|
||||
|
||||
it('should parse the root url', () => {
|
||||
let tree = url.parse("/");
|
||||
expectSegment(tree.root, "");
|
||||
expect(url.serialize(tree)).toEqual("");
|
||||
});
|
||||
|
||||
it('should parse non-empty urls', () => {
|
||||
let tree = url.parse("one/two");
|
||||
let one = tree.firstChild(tree.root);
|
||||
expectSegment(one, "one");
|
||||
expectSegment(tree.firstChild(<any>one), "two");
|
||||
expect(url.serialize(tree)).toEqual("/one/two");
|
||||
});
|
||||
|
||||
it("should parse multiple secondary segments", () => {
|
||||
let tree = url.parse("/one/two(three//four)/five");
|
||||
let c = tree.children(<any>tree.firstChild(tree.root));
|
||||
|
||||
expectSegment(c[0], "two");
|
||||
expectSegment(c[1], "three");
|
||||
expectSegment(c[2], "four");
|
||||
|
||||
expectSegment(tree.firstChild(c[0]), "five");
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one/two(three//four)/five");
|
||||
});
|
||||
|
||||
it("should parse secondary segments that have secondary segments", () => {
|
||||
let tree = url.parse("/one(/two(/three))");
|
||||
let c = tree.children(tree.root);
|
||||
|
||||
expectSegment(c[0], "one");
|
||||
expectSegment(c[1], "two");
|
||||
expectSegment(c[2], "three");
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one(two//three)");
|
||||
});
|
||||
|
||||
it("should parse secondary segments that have children", () => {
|
||||
let tree = url.parse("/one(/two/three)");
|
||||
let c = tree.children(tree.root);
|
||||
|
||||
expectSegment(c[0], "one");
|
||||
expectSegment(c[1], "two");
|
||||
expectSegment(tree.firstChild(c[1]), "three");
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one(two/three)");
|
||||
});
|
||||
|
||||
it("should parse an empty secondary segment group", () => {
|
||||
let tree = url.parse("/one()");
|
||||
let c = tree.children(tree.root);
|
||||
|
||||
expectSegment(c[0], "one");
|
||||
expect(tree.children(c[0]).length).toEqual(0);
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one");
|
||||
});
|
||||
|
||||
it("should parse key-value matrix params", () => {
|
||||
let tree = url.parse("/one;a=11a;b=11b(two;c=22//three;d=33)");
|
||||
let c = tree.children(tree.root);
|
||||
|
||||
expectSegment(c[0], "one;a=11a;b=11b");
|
||||
expectSegment(c[1], "two;c=22");
|
||||
expectSegment(c[2], "three;d=33");
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(two;c=22//three;d=33)");
|
||||
});
|
||||
|
||||
it("should parse key only matrix params", () => {
|
||||
let tree = url.parse("/one;a");
|
||||
|
||||
let c = tree.firstChild(tree.root);
|
||||
expectSegment(c, "one;a=true");
|
||||
|
||||
expect(url.serialize(tree)).toEqual("/one;a=true");
|
||||
});
|
||||
|
||||
it("should parse query params", () => {
|
||||
let tree = url.parse("/one?a=1&b=2");
|
||||
expect(tree.queryParameters).toEqual({a: '1', b: '2'});
|
||||
});
|
||||
|
||||
it("should parse key only query params", () => {
|
||||
let tree = url.parse("/one?a");
|
||||
expect(tree.queryParameters).toEqual({a: 'true'});
|
||||
});
|
||||
|
||||
it("should serializer query params", () => {
|
||||
let tree = url.parse("/one?a");
|
||||
expect(url.serialize(tree)).toEqual("/one?a=true");
|
||||
});
|
||||
|
||||
it("should parse fragment", () => {
|
||||
let tree = url.parse("/one#two");
|
||||
expect(tree.fragment).toEqual("two");
|
||||
expect(url.serialize(tree)).toEqual("/one#two");
|
||||
});
|
||||
|
||||
it("should parse empty fragment", () => {
|
||||
let tree = url.parse("/one#");
|
||||
expect(tree.fragment).toEqual("");
|
||||
expect(url.serialize(tree)).toEqual("/one#");
|
||||
});
|
||||
});
|
||||
|
||||
function expectSegment(segment:UrlSegment | null, expected:string):void {
|
||||
expect(segment ? serializeSegment(segment) : null).toEqual(expected);
|
||||
}
|
Loading…
Reference in New Issue