Skip to content

Commit

Permalink
ability to build custom endpoint url #2
Browse files Browse the repository at this point in the history
  • Loading branch information
vladkens committed Sep 3, 2024
1 parent 4d07ea3 commit df8c69b
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 20 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
"code-runner.executorMap": {
"typescript": "node --loader tsm",
"javascript": "node"
},
"[markdown]": {
"editor.wordWrap": "wordWrapColumn",
"editor.wordWrapColumn": 99
}
}
29 changes: 17 additions & 12 deletions examples/petstore-v2.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Auto-generated by https://github.com/vladkens/apigen-ts
// Source: https://petstore.swagger.io/v2/swagger.json

interface ApigenConfig {
export interface ApigenConfig {
baseUrl: string
headers: Record<string, string>
}

interface ApigenRequest extends Omit<RequestInit, "body"> {
export interface ApigenRequest extends Omit<RequestInit, "body"> {
search?: Record<string, unknown>
body?: unknown
}
Expand Down Expand Up @@ -39,14 +39,19 @@ export class ApiClient {
}
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
PrepareFetchUrl(path: string): URL {
let base = this.Config.baseUrl
if ("location" in globalThis && (base === "" || base.startsWith("/"))) {
const { location } = globalThis as unknown as { location: { origin: string } }
base = `${location.origin}${base.endsWith("/") ? base : `/${base}`}`
}

const url = new URL(path, base)
return new URL(path, base)
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
const url = this.PrepareFetchUrl(path)

for (const [k, v] of Object.entries(opts?.search ?? {})) {
url.searchParams.append(k, Array.isArray(v) ? v.join(",") : (v as string))
}
Expand Down Expand Up @@ -134,6 +139,10 @@ export class ApiClient {
}

store = {
getInventory: () => {
return this.Fetch<object>("get", "/store/inventory", {})
},

placeOrder: (body: Order) => {
return this.Fetch<Order>("post", "/store/order", { body })
},
Expand All @@ -145,17 +154,9 @@ export class ApiClient {
deleteOrder: (orderId: number) => {
return this.Fetch<void>("delete", `/store/order/${orderId}`, {})
},

getInventory: () => {
return this.Fetch<object>("get", "/store/inventory", {})
},
}

user = {
createUsersWithArrayInput: (body: User[]) => {
return this.Fetch<void>("post", "/user/createWithArray", { body })
},

createUsersWithListInput: (body: User[]) => {
return this.Fetch<void>("post", "/user/createWithList", { body })
},
Expand All @@ -180,6 +181,10 @@ export class ApiClient {
return this.Fetch<void>("get", "/user/logout", {})
},

createUsersWithArrayInput: (body: User[]) => {
return this.Fetch<void>("post", "/user/createWithArray", { body })
},

createUser: (body: User) => {
return this.Fetch<void>("post", "/user", { body })
},
Expand Down
13 changes: 9 additions & 4 deletions examples/petstore-v3.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Auto-generated by https://github.com/vladkens/apigen-ts
// Source: https://petstore3.swagger.io/api/v3/openapi.json

interface ApigenConfig {
export interface ApigenConfig {
baseUrl: string
headers: Record<string, string>
}

interface ApigenRequest extends Omit<RequestInit, "body"> {
export interface ApigenRequest extends Omit<RequestInit, "body"> {
search?: Record<string, unknown>
body?: unknown
}
Expand Down Expand Up @@ -39,14 +39,19 @@ export class ApiClient {
}
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
PrepareFetchUrl(path: string): URL {
let base = this.Config.baseUrl
if ("location" in globalThis && (base === "" || base.startsWith("/"))) {
const { location } = globalThis as unknown as { location: { origin: string } }
base = `${location.origin}${base.endsWith("/") ? base : `/${base}`}`
}

const url = new URL(path, base)
return new URL(path, base)
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
const url = this.PrepareFetchUrl(path)

for (const [k, v] of Object.entries(opts?.search ?? {})) {
url.searchParams.append(k, Array.isArray(v) ? v.join(",") : (v as string))
}
Expand Down
23 changes: 22 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,34 @@ class MyClient extends ApiClient {
}

try {
const api = MyClient()
const api = new MyClient()
const pet = await api.pet.getPetById(404)
} catch (e) {
console.log(e) // e is { code: "API_ERROR" }
}
```

### Base url resolving

You can modify how the endpoint url is created. By default [URL constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) used to resolve endpoint url like: `new URL(path, baseUrl)` which has specific resolving [rules](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references). E.g.:

- `new URL("/v2/cats", "https://example.com/v1/") // -> https://example.com/v2/cats`
- `new URL("v2/cats", "https://example.com/v1/") // -> https://example.com/v1/v2/cats`

If you want to have custom endpoint url resolving rules, you can override `PrepareFetchUrl` method. For more details see [issue](https://github.com/vladkens/apigen-ts/issues/2).

```ts
class MyClient extends ApiClient {
PrepareFetchUrl(path: string) {
return new URL(`${this.Config.baseUrl}/${path}`.replace(/\/{2,}/g, "/"))
}
}

const api = new MyClient({ baseUrl: "https://example.com/v1" })
// will call: https://example.com/v1/pet/ instead of https://example.com/pet/
const pet = await api.pet.getPetById(404)
```

### Node.js API

Create file like `apigen.mjs` with content:
Expand Down
9 changes: 7 additions & 2 deletions src/_template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,20 @@ export class ApiClient {
}
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
PrepareFetchUrl(path: string): URL {
let base = this.Config.baseUrl
if ("location" in globalThis && (base === "" || base.startsWith("/"))) {
// make ts happy in pure nodejs environment, should never pass here
const { location } = globalThis as unknown as { location: { origin: string } }
base = `${location.origin}${base.endsWith("/") ? base : `/${base}`}`
}

const url = new URL(path, base)
return new URL(path, base)
}

async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
const url = this.PrepareFetchUrl(path)

for (const [k, v] of Object.entries(opts?.search ?? {})) {
url.searchParams.append(k, Array.isArray(v) ? v.join(",") : (v as string))
}
Expand Down
48 changes: 48 additions & 0 deletions test/http-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import fetchMock from "fetch-mock"
import { test } from "uvu"
import { equal } from "uvu/assert"
import { ApiClient } from "../src/_template"

const t = async <T>(body: T) => {
try {
const baseUrl = "http://localhost"
const client = new ApiClient({ baseUrl })
fetchMock.mock(`${baseUrl}`, { body: JSON.stringify(body) })
return client.Fetch<T>("get", "/")
} finally {
fetchMock.restore()
}
}

test("apiClient dates parsing", async () => {
const date = new Date("2021-01-01T00:00:00Z")
const dateStr = date.toISOString()

const rs = await t({ date, dates: [date], more: { date, dates: [date] } })
equal(rs.date.toISOString(), dateStr)
equal(rs.dates[0].toISOString(), dateStr)
equal(rs.more.date.toISOString(), dateStr)
equal(rs.more.dates[0].toISOString(), dateStr)
})

test("apiClient base url", async () => {
const c1 = new ApiClient({ baseUrl: "http://localhost" })
equal(c1.PrepareFetchUrl("/").toString(), "http://localhost/")
equal(c1.PrepareFetchUrl("/api/v1/cats").toString(), "http://localhost/api/v1/cats")

const c2 = new ApiClient({ baseUrl: "https://example.com" })
equal(c2.PrepareFetchUrl("/").toString(), "https://example.com/")
equal(c2.PrepareFetchUrl("/api/v1/cats").toString(), "https://example.com/api/v1/cats")

class MyClient extends ApiClient {
PrepareFetchUrl(path: string) {
return new URL(`${this.Config.baseUrl}/${path}`.replace(/\/{2,}/g, "/"))
}
}

const c3 = new MyClient({ baseUrl: "https://example.com/api/v1" })
equal(c3.PrepareFetchUrl("/").toString(), "https://example.com/api/v1/")
equal(c3.PrepareFetchUrl("/cats").toString(), "https://example.com/api/v1/cats")
})

test.run()
2 changes: 1 addition & 1 deletion test/url-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { printCode } from "../src/pritner"

test("url template", async () => {
const t = async (url: string, replacements: Record<string, string>) => {
const code = await printCode([prepareUrl(url, replacements) as unknown as ts.Statement])
const code = printCode([prepareUrl(url, replacements) as unknown as ts.Statement])
return trim(trim(code.trim(), ";"), '`"')
}

Expand Down

0 comments on commit df8c69b

Please sign in to comment.