Skip to content

Commit 92fa390

Browse files
committed
A saner version of UrlSet
1 parent 6a283a3 commit 92fa390

File tree

3 files changed

+122
-41
lines changed

3 files changed

+122
-41
lines changed

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export {UrlMutator, UrlMutators} from './mutations';
22
export {UrlFilter, UrlFilters} from './filters';
33
export {ParsedUrl} from './parsed-url';
4-
export {UrlSet} from './url-set';
4+
export {UrlSet, ParsedUrlSet, NormalizedUrlSet} from './url-set';
55
export {NormalizedUrl} from './normalized-url';
66

77
export type StringMatch = string | string[] | RegExp;

src/url-set.ts

Lines changed: 117 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,156 @@
11
import {URL} from 'node:url';
2+
import {UrlMutator} from './mutations';
3+
import {NormalizedUrl} from './normalized-url';
4+
import {ParsedUrl} from './parsed-url';
25

3-
export class UrlSet<T extends URL> extends Set<string> {
4-
unparsable: Set<string> = new Set<string>();
5-
duplicates: Set<string> = new Set<string>();
6+
abstract class UrlTypeSet<T extends URL> extends Set<T> {
7+
verifier = new Set<string>();
8+
unparsable = new Set<string>();
69

710
public constructor(
8-
private typeConstructor: Constructor<T>,
9-
values: string[],
10-
public strict: boolean = false
11+
values?: T[] | string[],
12+
readonly defaultBase?: string | URL,
13+
readonly strict: boolean = false
1114
) {
1215
super();
13-
this.addItems(values);
16+
if (values !== undefined) this.addItems(values);
1417
}
1518

16-
override add(value: string): this {
17-
const u = this.parse(value);
18-
if (u) {
19-
if (super.has(u.href)) {
20-
this.duplicates.add(value);
21-
} else {
22-
super.add(u.href);
19+
override add(value: T | string): this {
20+
const incoming = typeof value === 'string' ? this.parse(value) : value;
21+
if (incoming) {
22+
if (!this.has(incoming)) {
23+
super.add(incoming);
24+
this.verifier.add(incoming.href);
2325
}
2426
} else {
25-
this.unparsable.add(value);
27+
this.unparsable.add(value as string);
2628
}
2729
return this;
2830
}
2931

30-
override has(value: string): boolean {
31-
const parsed = this.parse(value);
32-
if (parsed) return super.has(parsed.href);
33-
else return false;
32+
override has(value: T | string): boolean {
33+
const incoming = typeof value === 'string' ? this.parse(value) : value;
34+
if (!incoming) return false;
35+
return this.verifier.has(incoming.href);
3436
}
3537

36-
override delete(value: string): boolean {
37-
const parsed = this.parse(value);
38-
if (parsed) return super.delete(parsed.href);
39-
else return false;
38+
override delete(value: T | string): boolean {
39+
const incoming = typeof value === 'string' ? this.parse(value) : value;
40+
if (incoming) {
41+
if (this.verifier.delete(incoming.href)) {
42+
this.forEach(v => {
43+
if (v.href === incoming.href) super.delete(v);
44+
});
45+
return true;
46+
}
47+
}
48+
return false;
4049
}
4150

4251
override clear(): void {
52+
this.verifier.clear();
4353
this.unparsable.clear();
44-
this.duplicates.clear();
4554
super.clear();
4655
}
4756

48-
addItems(values: Array<string>): this {
57+
addItems(values: T[] | string[]): this {
4958
values.forEach(v => {
5059
this.add(v);
5160
});
5261
return this;
5362
}
5463

55-
hydrate(): T[] {
56-
return [...this].map(u => new this.typeConstructor(u)) as T[];
64+
protected abstract parse(input: string, base?: string | URL): T | false;
65+
}
66+
67+
export class UrlSet extends UrlTypeSet<URL> {
68+
protected override parse(
69+
input: string,
70+
base?: string | URL,
71+
recursing = false
72+
): URL | false {
73+
try {
74+
return new URL(input, base);
75+
} catch (e: unknown) {
76+
if (!recursing && base === undefined && this.defaultBase !== undefined) {
77+
try {
78+
return this.parse(input, this.defaultBase, true);
79+
} catch {
80+
this.unparsable.add(input);
81+
return false;
82+
}
83+
} else {
84+
if (e! instanceof TypeError || this.strict) {
85+
throw e;
86+
}
87+
}
88+
this.unparsable.add(input);
89+
return false;
90+
}
5791
}
92+
}
5893

59-
parse(value: string): T | false {
94+
export class ParsedUrlSet extends UrlTypeSet<ParsedUrl> {
95+
protected override parse(
96+
input: string,
97+
base?: string | URL,
98+
recursing = false
99+
): ParsedUrl | false {
60100
try {
61-
return new this.typeConstructor(value);
101+
return new ParsedUrl(input, base);
62102
} catch (e: unknown) {
63-
if (this.strict) {
64-
throw e;
103+
if (!recursing && base === undefined && this.defaultBase !== undefined) {
104+
try {
105+
return this.parse(input, this.defaultBase, true);
106+
} catch {
107+
this.unparsable.add(input);
108+
return false;
109+
}
65110
} else {
66-
return false;
111+
if (e! instanceof TypeError || this.strict) {
112+
throw e;
113+
}
67114
}
115+
this.unparsable.add(input);
116+
return false;
68117
}
69118
}
70119
}
71120

72-
type Constructor<T> = new (input: string, base?: string | T) => T;
121+
export class NormalizedUrlSet extends UrlTypeSet<NormalizedUrl> {
122+
public constructor(
123+
values?: NormalizedUrl[] | string[],
124+
readonly normalizeer: UrlMutator = NormalizedUrl.normalizer,
125+
readonly defaultBase?: string | URL,
126+
strict = false
127+
) {
128+
super(values, defaultBase, strict);
129+
if (values !== undefined) this.addItems(values);
130+
}
131+
132+
protected override parse(
133+
input: string,
134+
base?: string | URL,
135+
recursing = false
136+
): NormalizedUrl | false {
137+
try {
138+
return new NormalizedUrl(input, base);
139+
} catch (e: unknown) {
140+
if (!recursing && base === undefined && this.defaultBase !== undefined) {
141+
try {
142+
return this.parse(input, this.defaultBase, true);
143+
} catch {
144+
this.unparsable.add(input);
145+
return false;
146+
}
147+
} else {
148+
if (e! instanceof TypeError || this.strict) {
149+
throw e;
150+
}
151+
}
152+
this.unparsable.add(input);
153+
return false;
154+
}
155+
}
156+
}

tests/url-set.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import {UrlSet, ParsedUrl, NormalizedUrl} from '../src';
1+
import {ParsedUrlSet, NormalizedUrlSet} from '../src';
22
import * as fixtures from './setup';
33

44
test('url ignores invalid URLs', () => {
5-
const set = new UrlSet<ParsedUrl>(ParsedUrl, fixtures.UNPARSABLE_URLS);
5+
const set = new ParsedUrlSet(fixtures.UNPARSABLE_URLS);
66
expect(set.size).toBe(0);
77
});
88

99
test('url set tracks unparsable rejections', () => {
10-
const set = new UrlSet<NormalizedUrl>(NormalizedUrl, [
10+
const set = new NormalizedUrlSet([
1111
fixtures.NORMALIZED_URL.normalized,
1212
...fixtures.UNPARSABLE_URLS,
1313
]);
@@ -17,9 +17,6 @@ test('url set tracks unparsable rejections', () => {
1717
});
1818

1919
test('url set culls duplicates', () => {
20-
const set = new UrlSet<NormalizedUrl>(
21-
NormalizedUrl,
22-
fixtures.NORMALIZED_URL.variations
23-
);
20+
const set = new NormalizedUrlSet(fixtures.NORMALIZED_URL.variations);
2421
expect(set.size).toBe(1);
2522
});

0 commit comments

Comments
 (0)