Skip to content

Commit

Permalink
fix(generate): format complex path parameters
Browse files Browse the repository at this point in the history
fix: #319
  • Loading branch information
Xiphe committed Nov 18, 2022
1 parent 7ba226e commit aa417a4
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 34 deletions.
12 changes: 10 additions & 2 deletions demo/api.ts
Expand Up @@ -542,10 +542,10 @@ export function getObjectParameters(
commaArray,
commaObject,
}),
QS.space({
QS.formSpace({
spaceDelimited,
}),
QS.pipe({
QS.formPipe({
pipeDelimited,
}),
QS.deep({
Expand All @@ -570,3 +570,11 @@ export function uploadPng(body?: Blob, opts?: Oazapfts.RequestOpts) {
body,
});
}
export function getIssue319ByUserIds(
userIds: string[],
opts?: Oazapfts.RequestOpts
) {
return oazapfts.fetchText(`/issue319/${QS.simple(userIds)}`, {
...opts,
});
}
12 changes: 10 additions & 2 deletions demo/enumApi.ts
Expand Up @@ -542,10 +542,10 @@ export function getObjectParameters(
commaArray,
commaObject,
}),
QS.space({
QS.formSpace({
spaceDelimited,
}),
QS.pipe({
QS.formPipe({
pipeDelimited,
}),
QS.deep({
Expand All @@ -570,6 +570,14 @@ export function uploadPng(body?: Blob, opts?: Oazapfts.RequestOpts) {
body,
});
}
export function getIssue319ByUserIds(
userIds: string[],
opts?: Oazapfts.RequestOpts
) {
return oazapfts.fetchText(`/issue319/${QS.simple(userIds)}`, {
...opts,
});
}
export enum Status {
Available = "Available",
Pending = "Pending",
Expand Down
14 changes: 12 additions & 2 deletions demo/optimisticApi.ts
Expand Up @@ -587,10 +587,10 @@ export function getObjectParameters(
commaArray,
commaObject,
}),
QS.space({
QS.formSpace({
spaceDelimited,
}),
QS.pipe({
QS.formPipe({
pipeDelimited,
}),
QS.deep({
Expand Down Expand Up @@ -618,3 +618,13 @@ export function uploadPng(body?: Blob, opts?: Oazapfts.RequestOpts) {
})
);
}
export function getIssue319ByUserIds(
userIds: string[],
opts?: Oazapfts.RequestOpts
) {
return oazapfts.ok(
oazapfts.fetchText(`/issue319/${QS.simple(userIds)}`, {
...opts,
})
);
}
23 changes: 23 additions & 0 deletions demo/petstore.json
Expand Up @@ -1033,6 +1033,29 @@
}
]
}
},
"/issue319/{userIds}": {
"get": {
"parameters": [
{
"name": "userIds",
"in": "path",
"explode": false,
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {
"200": {
"description": "ok"
}
}
}
}
},
"components": {
Expand Down
9 changes: 8 additions & 1 deletion src/codegen/generate.test.ts
@@ -1,4 +1,4 @@
import ApiGenerator, { getOperationName } from "./generate";
import ApiGenerator, { getOperationName, getFormatter } from "./generate";
import { printAst } from "./index";
import SwaggerParser from "@apidevtools/swagger-parser";
import { OpenAPIV3 } from "openapi-types";
Expand Down Expand Up @@ -139,3 +139,10 @@ describe("generate with const", () => {
expect(oneLine).toContain(constTypeDefinition);
});
});

// // https://spec.openapis.org/oas/v3.0.3#style-examples
// describe('getFormatter', () => {
// it('formats correctly', () => {
// const formatter = getFormatter()
// })
// })
78 changes: 66 additions & 12 deletions src/codegen/generate.ts
Expand Up @@ -44,6 +44,7 @@ export function getFormatter({
}: OpenAPIV3.ParameterObject) {
if (explode && style === "deepObject") return "deep";
if (explode) return "explode";
if (style === "simple") return "simple";
if (style === "spaceDelimited") return "space";
if (style === "pipeDelimited") return "pipe";
return "form";
Expand Down Expand Up @@ -101,23 +102,32 @@ export function getReferenceName(obj: any) {
}
}

const formatPrimitivePathParameter = (name: string) => {
const expression = _.camelCase(name);
return cg.createCall(factory.createIdentifier("encodeURIComponent"), {
args: [factory.createIdentifier(expression)],
});
};

/**
* Create a template string literal from the given OpenAPI urlTemplate.
* Curly braces in the path are turned into identifier expressions,
* which are read from the local scope during runtime.
*/
export function createUrlExpression(path: string, qs?: ts.Expression) {
export function createUrlExpression(
path: string,
qs?: ts.Expression,
formatPathParameter: (
name: string
) => ts.Expression = formatPrimitivePathParameter
) {
const spans: Array<{ expression: ts.Expression; literal: string }> = [];
// Use a replacer function to collect spans as a side effect:
const head = path.replace(
/(.*?)\{(.+?)\}(.*?)(?=\{|$)/g,
(_substr, head, name, literal) => {
const expression = _.camelCase(name);
spans.push({
expression: cg.createCall(
factory.createIdentifier("encodeURIComponent"),
{ args: [factory.createIdentifier(expression)] }
),
expression: formatPathParameter(name),
literal,
});
return head;
Expand Down Expand Up @@ -683,6 +693,34 @@ export default class ApiGenerator {
return this.opts?.optimistic ? callOazapftsFunction("ok", [ex]) : ex;
}

formatPathParameter(name: string, parameter?: OpenAPIV3.ParameterObject) {
const schema = parameter?.schema
? this.resolve<SchemaObject>(parameter.schema)
: null;

if (parameter && (schema?.type === "array" || schema?.type === "object")) {
const expression = _.camelCase(name);
const formatter = getFormatter({
style: "simple",
explode: parameter.style === "form",
...parameter,
});

return callQsFunction(
formatter,
formatter === "simple" || formatter === "pipe" || formatter === "space"
? [factory.createIdentifier(expression)]
: [
cg.createObjectLiteral([
[expression, factory.createIdentifier(expression)],
]),
]
);
}

return formatPrimitivePathParameter(name);
}

generateApi() {
this.reset();

Expand Down Expand Up @@ -853,16 +891,32 @@ export default class ApiGenerator {
"query",
Object.entries(paramsByFormatter).map(([format, params]) => {
//const [allowReserved, encodeReserved] = _.partition(params, "allowReserved");
return callQsFunction(format, [
cg.createObjectLiteral(
params.map((p) => [p.name, argNames[p.name]])
),
]);
return callQsFunction(
format === "simple"
? "form"
: format === "pipe"
? "formPipe"
: format === "space"
? "formSpace"
: format,
[
cg.createObjectLiteral(
params.map((p) => [p.name, argNames[p.name]])
),
]
);
})
);
}

const url = createUrlExpression(path, qs);
const url = createUrlExpression(path, qs, (name) =>
this.formatPathParameter(
name,
parameters.find(
({ name: n, in: loc }) => loc === "path" && n === name
)
)
);
const init: ts.ObjectLiteralElementLike[] = [
factory.createSpreadAssignment(factory.createIdentifier("opts")),
];
Expand Down
10 changes: 8 additions & 2 deletions src/runtime/query.test.ts
Expand Up @@ -4,11 +4,17 @@ describe("delimited", () => {
it("should use commas", () => {
expect(qs.form({ id: [3, 4, 5] })).toEqual("id=3,4,5");
});
it("should use pipes for form", () => {
expect(qs.formPipe({ id: [3, 4, 5] })).toEqual("id=3|4|5");
});
it("should use spaces for form", () => {
expect(qs.formSpace({ id: [3, 4, 5] })).toEqual("id=3%204%205");
});
it("should use pipes", () => {
expect(qs.pipe({ id: [3, 4, 5] })).toEqual("id=3|4|5");
expect(qs.pipe([3, 4, 5])).toEqual("3|4|5");
});
it("should use spaces", () => {
expect(qs.space({ id: [3, 4, 5] })).toEqual("id=3%204%205");
expect(qs.space([3, 4, 5])).toEqual("3%204%205");
});
it("should enumerate entries", () => {
expect(qs.form({ author: { firstName: "Felix", role: "admin" } })).toEqual(
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/query.ts
@@ -1,4 +1,9 @@
import { encode, delimited, encodeReserved } from "./util";
import {
encode,
form as encodeForm,
simple as encodeSimple,
encodeReserved,
} from "./util";

/**
* Join params using an ampersand and prepends a questionmark if not empty.
Expand Down Expand Up @@ -61,6 +66,9 @@ export function explode(
.join("&");
}

export const form = delimited();
export const pipe = delimited("|");
export const space = delimited("%20");
export const form = encodeForm();
export const formPipe = encodeForm("|");
export const formSpace = encodeForm("%20");
export const simple = encodeSimple();
export const pipe = encodeSimple("|");
export const space = encodeSimple("%20");
35 changes: 26 additions & 9 deletions src/runtime/util.ts
Expand Up @@ -4,6 +4,21 @@ type Encoders = Array<(s: string) => string>;
export const encodeReserved = [encodeURIComponent, encodeURIComponent];
export const allowReserved = [encodeURIComponent, encodeURI];

function join(
value: object,
encoder: Encoders[0] = encodeURIComponent,
delimiter = ","
) {
if (Array.isArray(value)) {
return value.map(encoder).join(delimiter);
}
const flat = Object.entries(value).reduce(
(flat, entry) => [...flat, ...entry],
[] as any
);
return flat.map(encoder).join(delimiter);
}

/**
* Creates a tag-function to encode template strings with the given encoders.
*/
Expand All @@ -14,14 +29,7 @@ export function encode(encoders: Encoders, delimiter = ",") {
return "";
}
if (typeof v === "object") {
if (Array.isArray(v)) {
return v.map(encoder).join(delimiter);
}
const flat = Object.entries(v).reduce(
(flat, entry) => [...flat, ...entry],
[] as any
);
return flat.map(encoder).join(delimiter);
return join(v, encoder, delimiter);
}

return encoder(String(v));
Expand All @@ -34,10 +42,19 @@ export function encode(encoders: Encoders, delimiter = ",") {
};
}

export function simple(delimiter = ",") {
return (
param: unknown[] | Record<string, unknown>,
encoders = encodeReserved
) => {
return join(param, encoders[1], delimiter);
};
}

/**
* Separate array values by the given delimiter.
*/
export function delimited(delimiter = ",") {
export function form(delimiter = ",") {
return (params: Record<string, any>, encoders = encodeReserved) =>
Object.entries(params)
.filter(([, value]) => value !== undefined)
Expand Down

0 comments on commit aa417a4

Please sign in to comment.