Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable header parameters #18

Merged
merged 8 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/empty-responses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export abstract class RequestFactory {
static getTest(): GetRequest<undefined> {
return {
Expand Down
2 changes: 1 addition & 1 deletion examples/pet-store-expanded.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type Pet = (NewPet
& {
id: number
Expand Down
2 changes: 1 addition & 1 deletion examples/pet-store-refs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type ObjectWithId = {
id: number
}
Expand Down
2 changes: 1 addition & 1 deletion examples/pet-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type Pet = {
id: number
name: string
Expand Down
7 changes: 5 additions & 2 deletions examples/schema-with-header-param.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type TransferResponse = { }
export abstract class RequestFactory {
static getAccountAccountIdTransfers({ accountId, startDate, endDate, limit, cursor, }: { accountId: string, startDate?: string, endDate?: string, limit?: number, cursor?: string, }): GetRequest<{ message: string, nextCursor?: string, data: Array<TransferResponse>, }> {
static getAccountAccountIdTransfers({ acceptVersion, accountId, startDate, endDate, limit, cursor, }: { acceptVersion: '1.0'
| '1.1', accountId: string, startDate?: string, endDate?: string, limit?: number, cursor?: string, }): GetRequest<{ message: string, nextCursor?: string, data: Array<TransferResponse>, }> {
const query = toQuery({ startDate, endDate, limit, cursor })
const headers = toHeaders({ 'Accept-Version': acceptVersion })
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth adding functionality to only include specific headers in the request factories as I think most of the time, headers will not be something we want to have to pass in with each request. In this example the acceptVersion param is something you'd specify in the proxy class rather than specify on each request.

Perhaps we could add something like:

// Include all headers in request factories
openapi-tsrf generate ... --include-headers *

// Include specific headers in request factories
openapi-tsrf generate ... --include-headers header1 header2 header3

It'd also be worth including a specific example of this behaviour via a new spec in the examples dir and adding a matching test in tests/index.spec.ts

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commander docs for variadic options

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. I'll add that in.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think headers come in two flavours. Ones you supply with every request and ones that are defined by the spec per operation.

In the case of the one defined by the spec, I don't know if you want to have the option to disable it.

Take this spec, for example:

@@ -0,0 +1,1227 @@
{
  "openapi": "3.0.0",
  "paths": {
    "/receipts": {
      "post": {
        "operationId": "receipts",
        "parameters": [
          {
            "name": "some-value",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
            }
          },
          {
            "name": "X-Platform",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "app_user_id",
                ],
                "properties": {
                  "app_user_id": {
                    "type": "string",
                  },
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": { }
            }
          }
        },
        "deprecated": false
      }
    }
  }
}

The spec advertises that you can only call the endpoint if you supply the required header. So, I think roughly the call would be

  const response = await API.postReceipts({ someValue: 'hi',  xPlatform: 'pleb os' })

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a choice of whether or not to include the header - it's whether we force the consumer to provide it at the call site (as per your example) or whether we implicitly supply it when constructing the request in our api proxy factory.

We can't determine which option is better from the spec alone, so we should make it configurable so the user can decide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I've added a --include-headers flag to the cli, allowing you to specify * or a whitelist of header names like --include-headers header1 header 2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default behaviour is to not include any headers

return {
method: 'GET',
url: `/account/${accountId}/transfers${query}`,
headers,
}
}
}
2 changes: 1 addition & 1 deletion examples/special-chars-in-identifiers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type Banana = {
'dot.dot'?: string
'&^&*\'\"%'?: number
Expand Down
2 changes: 1 addition & 1 deletion examples/users-form-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type User = {
id: number
email: string
Expand Down
2 changes: 1 addition & 1 deletion examples/uspto-yaml.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type dataSetList = {
total?: number
apis?: Array<{
Expand Down
2 changes: 1 addition & 1 deletion examples/uspto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'
import { toQuery, toFormData } from 'openapi-tsrf-runtime'
import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'
export type dataSetList = {
total?: number
apis?: Array<{
Expand Down
9 changes: 5 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ interface RequestConfig {
}

const factory = new ApiProxyFactory<RequestConfig>(
async <TResponse>({ url, method, ...rest }: AnyRequest<TResponse>, config?: RequestConfig) => {
async <TResponse>({ url, method, headers, ...rest }: AnyRequest<TResponse>, config?: RequestConfig) => {
headers = headers ?? new Headers()
const init: RequestInit = {
method,
redirect: 'manual',
Expand All @@ -88,11 +89,11 @@ const factory = new ApiProxyFactory<RequestConfig>(
init.body = data
} else {
init.body = JSON.stringify(data)
init.headers = {
'Content-Type': 'application/json',
}
headers.append('Content-Type', 'application/json')
}
}

Turtlator marked this conversation as resolved.
Show resolved Hide resolved
init.headers = headers

const fetchResult = await fetch(url, init)

Expand Down
2 changes: 1 addition & 1 deletion src/cli/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function* generateDocumentParts(
).length > 0
if (hasOperations) {
yield "import type { GetRequest, PostRequest, PutRequest, PatchRequest, OptionsRequest, DeleteRequest } from 'openapi-tsrf-runtime'"
yield "import { toQuery, toFormData } from 'openapi-tsrf-runtime'"
yield "import { toQuery, toFormData, toHeaders } from 'openapi-tsrf-runtime'"
}
for (const [name, schemaObj] of iterateDictionary(
document.components.schemas,
Expand Down
15 changes: 14 additions & 1 deletion src/cli/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function* generateOperation(
const [requestFormat, requestBodyType] = getRequestBodyType()
const responseBodyType = getResponseBodyType()
const hasQuery = Boolean(operation.parameters?.some(p => p.in === 'query'))
const hasHeaders = Boolean(operation.parameters?.some(p => p.in === 'header'))
yield `static ${makeSafeMethodIdentifier(
operation.operationId ?? `${method}_${path}`,
)}(`
Expand Down Expand Up @@ -83,6 +84,17 @@ export function* generateOperation(
)
.join(', ')} })`
}
if (hasHeaders) {
yield `const headers = toHeaders({ ${operation
.parameters!.filter(p => p.in === 'header')
.map(p => isSafeVariableIdentifier(p.name)
? p.name
: `${makeSafePropertyIdentifier(
p.name,
)}: ${makeSafeVariableIdentifier(p.name)}`,
)
.join(', ')} })`
}
if (requestFormat === 'form') {
yield 'const formData = toFormData(body)'
}
Expand All @@ -93,14 +105,15 @@ export function* generateOperation(
if (requestFormat === 'json') yield 'data: body,'
if (requestFormat === 'form') yield 'data: formData,'
if (requestFormat === 'empty') yield 'data: undefined,'
if (hasHeaders) yield 'headers,'
yield DecIndent
yield '}'
yield DecIndent
yield '}'

function* parameters(bodyParamType: string | undefined): AsyncDocumentParts {
const params =
operation.parameters?.filter(p => p.in === 'path' || p.in === 'query') ??
operation.parameters?.filter(p => p.in === 'path' || p.in === 'query' || p.in === 'header') ??
[]
if (params.length === 0 && bodyParamType === undefined) return

Expand Down
6 changes: 6 additions & 0 deletions src/runtime/request-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ export const toFormData = (o: Record<string, any>): FormData => {
Object.entries(o).forEach(([key, data]) => fd.append(key, data))
return fd
}

export const toHeaders = (o: { [key: string]: any }): Headers => {
const h = new Headers()
Object.entries(o).forEach(([key, data]) => h.append(key, data))
return h
}
6 changes: 6 additions & 0 deletions src/runtime/request-types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
export interface GetRequest<TResponse> {
url: string
method: 'GET'
headers?: Headers
}
export interface OptionsRequest<TResponse> {
url: string
method: 'OPTIONS'
headers?: Headers
}
export interface DeleteRequest<TResponse> {
url: string
method: 'DELETE'
headers?: Headers
}
export interface PostRequest<TRequest, TResponse> {
data: TRequest
url: string
method: 'POST'
headers?: Headers
}
export interface PatchRequest<TRequest, TResponse> {
data: TRequest
url: string
method: 'PATCH'
headers?: Headers
}
export interface PutRequest<TRequest, TResponse> {
data: TRequest
url: string
method: 'PUT'
headers?: Headers
}

export type AnyRequest<TResponse> =
Expand Down