Skip to content

Commit

Permalink
refactor(url): changed url parse concept
Browse files Browse the repository at this point in the history
  • Loading branch information
Yurchishin committed Sep 27, 2023
1 parent 8e09b74 commit 9bd4a89
Show file tree
Hide file tree
Showing 40 changed files with 1,102 additions and 1,869 deletions.
Binary file modified bun.lockb
Binary file not shown.
8 changes: 8 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[install]
exact = true
frozenLockfile = false

[test]
coverage = true
coverageThreshold = { line = 1, function = 1 }
root = "./src"
20 changes: 9 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,38 +32,36 @@
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.jsx,.ts,.tsx --fix",
"format": "prettier . --check",
"format:fix": "prettier . --write",
"fix": "bun run lint:fix && bun run format:fix",
"release": "release-it --ci --config .release-it.cjs",
"commit": "commit",
"fix": "bun run lint:fix && bun run format:fix",
"commit": "bun run fix && commit",
"prepare": "husky install"
},
"devDependencies": {
"@anylint/commitlint-config": "1.0.5",
"@anylint/eslint-config": "1.0.5",
"@anylint/prettier-config": "1.0.5",
"@anylint/commitlint-config": "1.0.7",
"@anylint/eslint-config": "1.0.7",
"@anylint/prettier-config": "1.0.7",
"@commitlint/cli": "17.7.1",
"@commitlint/prompt-cli": "17.7.1",
"@esfx/type-model": "1.0.0",
"@release-it/conventional-changelog": "7.0.2",
"@types/qs": "6.9.8",
"bun-types": "latest",
"conventional-changelog-conventionalcommits": "7.0.2",
"eslint": "8.50.0",
"husky": "8.0.3",
"next": "13.5.3",
"prettier": "3.0.3",
"qs": "6.11.2",
"release-it": "16.2.0",
"type-fest": "4.3.1",
"typescript": "5.2.2",
"zod": "3.22.2"
"typescript": "5.2.2"
},
"peerDependencies": {
"qs": "6.11.2",
"zod": "3.22.2"
"qs": "6.11.2"
},
"prettier": "@anylint/prettier-config",
"publishConfig": {
"access": "public",
"@anylint:registry": "https://registry.npmjs.org"
}
}
}
31 changes: 31 additions & 0 deletions src/common.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'bun:test'
import { URLError, URLOriginError, URLPathPatternError, canParseURL } from './common'

describe('origin', () => {
it('URLOriginError', () => {
const error = new URLOriginError('http://localhost')

expect(error.message).toBe('[safetch:origin]: http://localhost')
expect(error.name).toBe('URLOriginError')
})

it('URLPathPatternError', () => {
const error = new URLPathPatternError('/path', 'Invalid path pattern')

expect(error.message).toBe('[safetch:path-pattern]: Invalid path pattern (/path)')
expect(error.name).toBe('URLPathPatternError')
})

it('URLError', () => {
const error = new URLError('http://localhost', 'Invalid URL')

expect(error.message).toBe('[safetch:url]: Invalid URL (http://localhost)')
expect(error.name).toBe('URLError')
})

it('canParseURL', () => {
const url = 'http://localhost'

expect(canParseURL(url)).toBe(true)
})
})
30 changes: 30 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export class URLOriginError extends Error {
constructor(origin: string) {
super(`[safetch:origin]: ${origin}`)
this.name = 'URLOriginError'
}
}

export class URLPathPatternError extends Error {
constructor(pathPattern: string, message: string) {
super(`[safetch:path-pattern]: ${message} (${pathPattern})`)
this.name = 'URLPathPatternError'
}
}

export class URLError extends Error {
constructor(url: string, message: string) {
super(`[safetch:url]: ${message} (${url})`)
this.name = 'URLError'
}
}

export const canParseURL = (origin: string): boolean => {
try {
const _ = new URL(origin)

return true
} catch {
return false
}
}
25 changes: 25 additions & 0 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import TypeSafeResponse from './responce'
import type { SafeURL } from './url'

type AnyStringCase<M extends string> = Lowercase<M> | Uppercase<M>

export type RequestMethods =
| AnyStringCase<'DELETE'>
| AnyStringCase<'GET'>
| AnyStringCase<'HEAD'>
| AnyStringCase<'OPTIONS'>
| AnyStringCase<'PATCH'>
| AnyStringCase<'POST'>
| AnyStringCase<'PUT'>

export type SafeRequestInit = Omit<RequestInit, 'method'> & {
method: RequestMethods
}

const safetch = <P extends string>(url: SafeURL<P>, requestInit?: SafeRequestInit): Promise<TypeSafeResponse> => {
const urlInstance = url.toNativeURL()

return fetch(urlInstance, requestInit).then(response => new TypeSafeResponse(response))
}

export default safetch
59 changes: 0 additions & 59 deletions src/fetch/index.ts

This file was deleted.

12 changes: 9 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { default, isJSONBody } from './fetch'
export { default } from './fetch'
export type { RequestMethods, SafeRequestInit } from './fetch'
export { default as TypeSafeResponse, ResponseJsonParseError } from './responce'
export type { ValidPathPattern, ValidatePathPattern, GetParamsFromPathPattern } from './parser/PathPattern'
export { default as TypeSafeResponse } from './responce'
export { origin, Origin } from './origin'
export type { OriginOptions } from './origin'
export { PathPattern } from './pathPattern'
export { SafeURL } from './url'
export { URLError, URLOriginError, URLPathPatternError } from './common'
export type { SafeURLOptions, URLHash, URLSearchParamsInfo } from './url'
export type { ValidPathPattern, ValidatePathPattern, GetParamsFromPathPattern } from './types/PathPattern'
44 changes: 44 additions & 0 deletions src/origin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'bun:test'
import { URLOriginError } from './common'
import { origin, Origin } from './origin'
import { PathPattern } from './pathPattern'

describe('origin', () => {
it('should be able to create an origin', () => {
const exampleOrigin = origin('https://example.com')

expect(exampleOrigin).toBeInstanceOf(Origin)
expect(exampleOrigin.origin).toBe('https://example.com')
expect(exampleOrigin.host).toBe('example.com')
expect(exampleOrigin.hostname).toBe('example.com')
expect(exampleOrigin.port).toBe(undefined)
expect(exampleOrigin.protocol).toBe('https:')
expect(exampleOrigin.pureProtocol).toBe('https')
})

it('should throw an error if the origin is invalid', () => {
expect(() => origin('invalid origin')).toThrow(new URLOriginError('invalid origin'))
})

it('should be able to create a path pattern', () => {
const exampleOrigin = origin('https://example.com')
const examplePathPattern = exampleOrigin.pathPattern('/example/[param]', {
param: 'param',
})

expect(examplePathPattern).toBeInstanceOf(PathPattern)
expect(examplePathPattern.pathPattern).toBe('/example/[param]')
expect(examplePathPattern.params).toEqual({ param: 'param' })
})

it('should be able to create search string', () => {
const exampleOrigin = origin('https://example.com')

expect(
exampleOrigin.searchStringify({
search: 'string',
}),
).toBe('?search=string')
expect(exampleOrigin.searchStringify(null)).toBe('')
})
})
53 changes: 53 additions & 0 deletions src/origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { stringify } from 'qs'
import { URLOriginError, canParseURL } from './common'
import { PathPattern } from './pathPattern'
import type { IStringifyOptions } from 'qs'
import type { GetParamsFromPathPattern, ValidPathPattern } from './types/PathPattern'

export type OriginOptions = {
searchStringifyOptions?: Omit<IStringifyOptions, 'addQueryPrefix'>
}

export class Origin {
readonly origin: string
readonly host: string
readonly hostname: string
readonly port: string | undefined
readonly protocol: string
readonly pureProtocol: string

private readonly searchStringifyOptions: IStringifyOptions = {
addQueryPrefix: false,
}

constructor(origin: string, options: OriginOptions = {}) {
const { searchStringifyOptions } = options

if (!canParseURL(origin)) throw new URLOriginError(origin)

const url = new URL(origin)

this.origin = url.origin
this.host = url.host
this.hostname = url.hostname
this.port = url.port === '' ? undefined : url.port
this.protocol = url.protocol
this.pureProtocol = url.protocol.slice(0, -1)
this.searchStringifyOptions = {
...searchStringifyOptions,
addQueryPrefix: true,
}
}

searchStringify(search: Record<string, unknown> | null): string {
if (search === null) return ''

return stringify(search, this.searchStringifyOptions)
}

pathPattern<P extends string>(pathPattern: ValidPathPattern<P>, params: GetParamsFromPathPattern<P>): PathPattern<P> {
return new PathPattern<P>(this, pathPattern, params)
}
}

export const origin = (_origin: string, options?: OriginOptions) => new Origin(_origin, options)
Loading

0 comments on commit 9bd4a89

Please sign in to comment.