Skip to content
This repository has been archived by the owner on Jun 27, 2024. It is now read-only.

Commit

Permalink
feat: add versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
boywithkeyboard committed Aug 18, 2023
1 parent 1bec5ba commit 071c81e
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 27 deletions.
176 changes: 149 additions & 27 deletions cheetah.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
// Copyright 2023 Samuel Kopp. All rights reserved. Apache-2.0 license.
import { base, Method } from './base.ts'
import { Collection } from './collection.ts'
import { Context } from './context.ts'
import { Context, Exception } from './context.ts'
import { Exception as OriginalException } from './exception.ts'
import { Extension, validExtension } from './extensions.ts'
import { Handler, HandlerOrSchema, Payload } from './handler.ts'
import {
Handler,
HandlerOrSchema,
Payload,
Version,
VersionRange,
} from './handler.ts'
import { OAuthStore } from './oauth/mod.ts'
import { OAuthSessionData } from './oauth/types.ts'
import { ResponseContext } from './response_context.ts'
import { Exception } from './context.ts'
import { Exception as OriginalException } from './exception.ts'

export type AppContext = {
gateway?: number
env: Record<string, unknown> | undefined
ip: string
proxy: AppConfig['proxy']
routes: Set<[Uppercase<Method>, string, RegExp, HandlerOrSchema[]]>
routes: cheetah['routes']
runtime:
| 'cloudflare'
| 'deno'
Expand All @@ -23,6 +29,7 @@ export type AppContext = {
querystring?: string
}
oauth: AppConfig['oauth']
versioning: AppConfig['versioning']
}

export type AppConfig = {
Expand Down Expand Up @@ -83,15 +90,19 @@ export type AppConfig = {
*/
debug?: boolean

/**
* Desc
*
* @since v1.4
*/
versioning?: {
highest: 'v4'
lowest: `v${string}`
}
versioning?:
& (
| {
type: 'uri'
}
| {
type: 'header'
header: string
}
)
& {
current: Version
}
}

export class cheetah extends base<cheetah>() {
Expand All @@ -107,6 +118,7 @@ export class cheetah extends base<cheetah>() {
#onPlugIn
#oauth
#debug
#versioning

constructor({
base,
Expand All @@ -117,6 +129,7 @@ export class cheetah extends base<cheetah>() {
notFound,
oauth,
debug,
versioning,
}: AppConfig = {}) {
super((method, pathname, handlers) => {
pathname = this.#base ? this.#base + pathname : pathname
Expand Down Expand Up @@ -152,6 +165,7 @@ export class cheetah extends base<cheetah>() {
this.#onPlugIn = false
this.#oauth = oauth
this.#debug = debug
this.#versioning = versioning
}

/* use ---------------------------------------------------------------------- */
Expand Down Expand Up @@ -228,22 +242,102 @@ export class cheetah extends base<cheetah>() {

/* router ------------------------------------------------------------------- */

#match(method: string, pathname: string, preflight: boolean) {
#parseVersion(headers: Headers, pathname: string) {
if (!this.#versioning) { // for typescript
throw new Error('Versioning not configured!')
}

const regex = /^v[1-9][0-9]?$|^100$/

if (this.#versioning.type === 'uri') {
pathname = pathname.replace('/', '')

if (regex.test(pathname.split('/')[0])) {
pathname.split('/').shift()

return { version: pathname.split('/')[0], pathname: '/' + pathname }
}

return { version: this.#versioning.current, pathname: '/' + pathname }
}

const header = headers.get(this.#versioning.header)

if (
this.#versioning.type === 'header' && header !== null &&
regex.test(header)
) {
return { version: header, pathname }
}

return { version: this.#versioning.current, pathname }
}

#match(request: Request, p: string) {
for (const r of this.#routes.values()) {
if (
method === r[0] ||
method === 'OPTIONS' ||
preflight && method === 'HEAD' && r[0] === 'GET'
request.method === r[0] ||
request.method === 'OPTIONS' ||
this.#preflight && request.method === 'HEAD' && r[0] === 'GET'
) {
const result = pathname.match(r[2])
if (this.#versioning) {
const options = typeof r[3][0] !== 'function' ? r[3][0] : null

if (!result) {
continue
}
const { pathname, version } = this.#parseVersion(request.headers, p)

return {
handlers: r[3],
params: result.groups ?? {},
if (
parseInt(version.replace('v', '')) >
parseInt(this.#versioning.current.replace('v', ''))
) {
return null
}

if (options !== null && options.versionRange !== undefined) {
const result = pathname.match(r[2])

if (!result) {
continue
}

const gateway = isVersionWithinRange(
version as Version,
options.versionRange as VersionRange,
)

if (!gateway) {
return null
}

return {
handlers: r[3],
params: result.groups ?? {},
gateway,
}
} else {
const result = pathname.match(r[2])

if (!result) {
continue
}

return {
handlers: r[3],
params: result.groups ?? {},
gateway: parseInt(version.replace('v', '')),
}
}
} else {
const result = p.match(r[2])

if (!result) {
continue
}

return {
handlers: r[3],
params: result.groups ?? {},
gateway: undefined,
}
}
}
}
Expand Down Expand Up @@ -281,6 +375,7 @@ export class cheetah extends base<cheetah>() {
routes: this.#routes,
runtime: this.#runtime,
oauth: this.#oauth,
versioning: this.#versioning,
}

if (this.#extensions.size > 0) {
Expand Down Expand Up @@ -345,9 +440,8 @@ export class cheetah extends base<cheetah>() {
}

const route = this.#match(
req.method,
req,
__app.request.pathname,
this.#preflight,
)

if (!route) {
Expand Down Expand Up @@ -602,3 +696,31 @@ export class cheetah extends base<cheetah>() {
}).finished
}
}

function isVersionWithinRange(
version: Version,
r: VersionRange,
): number | undefined {
const v = parseInt(version.replace('v', ''))

if (parseInt(r.replace('v', '')) === v) {
return v
}

if (r.startsWith('v') && r.includes('...')) { // from (min) ... to (max)
const from = parseInt(r.split('...')[0].replace('v', ''))
const to = parseInt(r.split('...')[1].replace('v', ''))

return v >= from && v <= to ? v : undefined
} else if (r.startsWith('> ')) {
return v > parseInt(r.replace('> v', '')) ? v : undefined
} else if (r.startsWith('< ')) {
return v < parseInt(r.replace('< v', '')) ? v : undefined
} else if (r.startsWith('>= ')) {
return v >= parseInt(r.replace('>= v', '')) ? v : undefined
} else if (r.startsWith('<= ')) {
return v <= parseInt(r.replace('<= v', '')) ? v : undefined
}

return undefined
}
4 changes: 4 additions & 0 deletions context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export class Context<
return this.#a
}

get gateway(): number {
return this.#a.gateway ?? -1
}

get dev(): boolean {
return this.runtime === 'deno' && Deno.env.get('DEV') === 'true'
}
Expand Down
17 changes: 17 additions & 0 deletions handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export type Payload =
| undefined
| void

type Number = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

export type Version = `v${
| Exclude<Number, 0>
| `${Exclude<Number, 0>}${Number}`}`

export type VersionRange =
| Version // exact version
| `> ${Version}` // greater than version
| `< ${Version}` // smaller than version
| `>= ${Version}` // greater than or equal to version
| `<= ${Version}` // smaller than or equal to version
| `${Version}...${Version}` // from (min) ... to (max)

export type Handler<
Pathname extends `/${string}` | unknown,
// deno-lint-ignore no-explicit-any
Expand Down Expand Up @@ -77,6 +91,7 @@ export function handler<T>() {
transform?: boolean // TODO remove at v2.0
cors?: string
params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>
versionRange?: VersionRange
})
| Handler<
Pathname,
Expand Down Expand Up @@ -126,6 +141,7 @@ export function bodylessHandler<T>() {
query?: ValidatedQuery
cors?: string
params?: Partial<Record<keyof ExtractParams<Pathname>, ZodType>>
versionRange?: VersionRange
}
| BodylessHandler<
Pathname,
Expand All @@ -150,6 +166,7 @@ export type HandlerOrSchema =
transform?: boolean // TODO remove at v2.0
cors?: string
params?: Record<string, ZodType>
versionRange?: VersionRange
}
| Handler<unknown>
| BodylessHandler<unknown>
Loading

0 comments on commit 071c81e

Please sign in to comment.