diff --git a/dtslint/ts3.8/tsconfig.json b/dtslint/ts3.8/tsconfig.json index 762cc87..77e8afb 100644 --- a/dtslint/ts3.8/tsconfig.json +++ b/dtslint/ts3.8/tsconfig.json @@ -11,6 +11,6 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "target": "es5", - "lib": ["es2015"] + "lib": ["es2015", "dom"] } } diff --git a/src/index.ts b/src/index.ts index b671fff..79f4e95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,12 +6,10 @@ import { either } from 'fp-ts/lib/Either' import { identity, tuple } from 'fp-ts/lib/function' import { Monad1 } from 'fp-ts/lib/Monad' import { Monoid } from 'fp-ts/lib/Monoid' -import { fromEither, fromNullable, isNone, none, Option, option, some } from 'fp-ts/lib/Option' -import { pipe, pipeable } from 'fp-ts/lib/pipeable' -import { filter, isEmpty } from 'fp-ts/lib/Record' +import { fromEither, isNone, none, Option, option, some } from 'fp-ts/lib/Option' +import { pipeable } from 'fp-ts/lib/pipeable' +import { isEmpty } from 'fp-ts/lib/Record' import { failure, Int, string, success, Type } from 'io-ts' -import { stringify } from 'querystring' -import { parse as parseUrl } from 'url' import { Contravariant1 } from 'fp-ts/lib/Contravariant' /** @@ -48,30 +46,53 @@ export class Route { * @since 0.4.0 */ static parse(s: string, decode: boolean = true): Route { - const route = parseUrl(s, true) - // tslint:disable-next-line: deprecation - const oparts = option.map(fromNullable(route.pathname), (s) => { - const r = s.split('/').filter(Boolean) - return decode ? r.map(decodeURIComponent) : r - }) - const parts = isNone(oparts) ? [] : oparts.value - return new Route(parts, Object.assign({}, route.query)) + const { pathname, searchParams } = new URL(s, 'http://localhost') // `base` is needed when `path` is relative + + const segments = pathname.split('/').filter(Boolean) + const parts = decode ? segments.map(decodeURIComponent) : segments + + return new Route(parts, toQuery(searchParams)) } /** * @since 0.4.0 */ toString(encode: boolean = true): string { - // tslint:disable-next-line: deprecation - const nonUndefinedQuery = pipe( - this.query, - filter((value) => value !== undefined) - ) - const qs = stringify(nonUndefinedQuery) + const qs = fromQuery(this.query).toString() const parts = encode ? this.parts.map(encodeURIComponent) : this.parts return '/' + parts.join('/') + (qs ? '?' + qs : '') } } +const fromQuery = (query: Query): URLSearchParams => { + const qs = new URLSearchParams() + + Object.entries(query).forEach(([k, v]) => { + if (typeof v === 'undefined') { + return + } + + return Array.isArray(v) ? v.forEach((x) => qs.append(k, x)) : qs.set(k, v) + }) + + return qs +} + +const toQuery = (params: URLSearchParams): Query => { + const q: Query = {} + + params.forEach((v, k) => { + const current = q[k] + + if (current) { + q[k] = Array.isArray(current) ? [...current, v] : [current, v] + } else { + q[k] = v + } + }) + + return q +} + const assign = (a: A) => (b: B): A & B => diff --git a/test/index.ts b/test/index.ts index f53d15c..ea793fe 100644 --- a/test/index.ts +++ b/test/index.ts @@ -57,6 +57,7 @@ describe('Route', () => { assert.deepStrictEqual(Route.parse('/foo/bar/'), new Route(['foo', 'bar'], {})) assert.deepStrictEqual(Route.parse('/foo/bar?a=1'), new Route(['foo', 'bar'], { a: '1' })) assert.deepStrictEqual(Route.parse('/foo/bar/?a=1'), new Route(['foo', 'bar'], { a: '1' })) + assert.deepStrictEqual(Route.parse('/foo/bar?a=1&a=2&a=3'), new Route(['foo', 'bar'], { a: ['1', '2', '3'] })) assert.deepStrictEqual(Route.parse('/a%20b'), new Route(['a b'], {})) assert.deepStrictEqual(Route.parse('/foo?a=b%20c'), new Route(['foo'], { a: 'b c' })) assert.deepStrictEqual(Route.parse('/@a'), new Route(['@a'], {})) @@ -73,7 +74,9 @@ describe('Route', () => { assert.strictEqual(new Route([], {}).toString(), '/') assert.strictEqual(new Route(['a'], {}).toString(), '/a') assert.strictEqual(new Route(['a'], { b: 'b' }).toString(), '/a?b=b') - assert.strictEqual(new Route(['a'], { b: 'b c' }).toString(), '/a?b=b%20c') + assert.strictEqual(new Route(['a'], { b: 'b c' }).toString(), '/a?b=b+c') + assert.strictEqual(new Route(['a'], { b: ['1', '2', '3'] }).toString(), '/a?b=1&b=2&b=3') + assert.strictEqual(new Route(['a'], { b: undefined }).toString(), '/a') assert.strictEqual(new Route(['a c'], { b: 'b' }).toString(), '/a%20c?b=b') assert.strictEqual(new Route(['@a'], {}).toString(), '/%40a') assert.strictEqual(new Route(['a&b'], {}).toString(), '/a%26b') @@ -95,7 +98,7 @@ describe('Route', () => { }) it('parse and toString should be inverse functions', () => { - const path = '/a%20c?b=b%20c' + const path = '/a%20c?b=b+c' assert.strictEqual(Route.parse(path).toString(), path) })