Skip to content

Commit 7629c5f

Browse files
authored
fix(path-parmaeter): Suport element access (#67)
1 parent d11e580 commit 7629c5f

File tree

9 files changed

+346
-15
lines changed

9 files changed

+346
-15
lines changed

src/code-templates/api-client/ApiClientClass/MethodBody/PathParameter.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ts from "typescript";
33
import type { TsGenerator } from "../../../../api";
44
import type { CodeGenerator } from "../../../../types";
55
import * as Utils from "../../utils";
6+
import { escapeText2 as escapeText } from "../../../../utils";
67

78
export const isPathParameter = (params: any): params is CodeGenerator.PickedParameter => {
89
return params.in === "path";
@@ -45,11 +46,14 @@ export const generateUrlTemplateExpression = (
4546
}, {});
4647
const urlTemplate: Utils.Params$TemplateExpression = [];
4748
let temporaryStringList: string[] = [];
49+
// TODO generateVariableIdentifierに噛み合わ下げいいように変換する
4850
const replaceText = (text: string): string | undefined => {
4951
let replacedText = text;
50-
Object.keys(patternMap).forEach(key => {
51-
if (new RegExp(key).test(replacedText)) {
52-
replacedText = replacedText.replace(new RegExp(key, "g"), `params.parameter.${patternMap[key]}`);
52+
Object.keys(patternMap).forEach(pathParameterName => {
53+
if (new RegExp(pathParameterName).test(replacedText)) {
54+
const { text, escaped } = escapeText(patternMap[pathParameterName]);
55+
const variableDeclaraText = escaped ? `params.parameter[${text}]` : `params.parameter.${text}`;
56+
replacedText = replacedText.replace(new RegExp(pathParameterName, "g"), variableDeclaraText);
5357
}
5458
});
5559
return replacedText === text ? undefined : replacedText;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as Utils from "../utils";
2+
3+
type OK = Utils.VariableElement;
4+
5+
describe("Utils", () => {
6+
test("splitVariableText", () => {
7+
const splitVariableText = Utils.splitVariableText;
8+
expect(splitVariableText("")).toStrictEqual<OK[]>([]);
9+
expect(splitVariableText("a")).toStrictEqual<OK[]>([
10+
{
11+
kind: "string",
12+
value: "a",
13+
},
14+
]);
15+
expect(splitVariableText("a.b")).toStrictEqual<OK[]>([
16+
{
17+
kind: "string",
18+
value: "a",
19+
},
20+
{
21+
kind: "string",
22+
value: "b",
23+
},
24+
]);
25+
expect(splitVariableText("a.b.c")).toStrictEqual<OK[]>([
26+
{
27+
kind: "string",
28+
value: "a",
29+
},
30+
{
31+
kind: "string",
32+
value: "b",
33+
},
34+
35+
{
36+
kind: "string",
37+
value: "c",
38+
},
39+
]);
40+
expect(splitVariableText('a.b["c"]')).toStrictEqual<OK[]>([
41+
{
42+
kind: "string",
43+
value: "a",
44+
},
45+
{
46+
kind: "string",
47+
value: "b",
48+
},
49+
50+
{
51+
kind: "element-access",
52+
value: "c",
53+
},
54+
]);
55+
expect(splitVariableText('a.b["c.d"]')).toStrictEqual<OK[]>([
56+
{
57+
kind: "string",
58+
value: "a",
59+
},
60+
{
61+
kind: "string",
62+
value: "b",
63+
},
64+
65+
{
66+
kind: "element-access",
67+
value: "c.d",
68+
},
69+
]);
70+
expect(splitVariableText('a.b["c.d.e"]')).toStrictEqual<OK[]>([
71+
{
72+
kind: "string",
73+
value: "a",
74+
},
75+
{
76+
kind: "string",
77+
value: "b",
78+
},
79+
80+
{
81+
kind: "element-access",
82+
value: "c.d.e",
83+
},
84+
]);
85+
});
86+
});

src/code-templates/api-client/utils.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,35 +140,78 @@ export const generateTemplateExpression = (factory: TsGenerator.Factory.Type, li
140140
});
141141
};
142142

143+
export interface VariableAccessIdentifer {
144+
kind: "string";
145+
value: string;
146+
}
147+
148+
export interface ElementAccessExpression {
149+
kind: "element-access";
150+
value: string;
151+
}
152+
153+
export type VariableElement = VariableAccessIdentifer | ElementAccessExpression;
154+
155+
export const splitVariableText = (text: string): VariableElement[] => {
156+
// ["...."] にマッチする
157+
const pattern = '["[a-zA-Z_0-9.]+"]';
158+
// 'a.b.c["a"]["b"]'.split(/(\["[a-zA-Z_0-9\.]+"\])/g)
159+
const splitTexts = text.split(/(\["[a-zA-Z_0-9\.]+"\])/g); // 区切り文字も含めて分割
160+
return splitTexts.reduce<VariableElement[]>((splitList, value) => {
161+
if (value === "") {
162+
return splitList;
163+
}
164+
// ["book.name"] にマッチするか
165+
if (new RegExp(pattern).test(value)) {
166+
// ["book.name"] から book.name を抽出
167+
const matchedValue = value.match(/[a-zA-Z_0-9\.]+/);
168+
if (matchedValue) {
169+
splitList.push({
170+
kind: "element-access",
171+
value: matchedValue[0],
172+
});
173+
}
174+
return splitList;
175+
} else {
176+
const dotSplited = value.split(".");
177+
const items = dotSplited.map<VariableAccessIdentifer>(childValue => ({ kind: "string", value: childValue }));
178+
return splitList.concat(items);
179+
}
180+
}, []);
181+
};
182+
143183
export const generateVariableIdentifier = (
144184
factory: TsGenerator.Factory.Type,
145185
name: string,
146186
): ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression => {
147187
if (name.startsWith("/")) {
148188
throw new Error("can't start '/'. name=" + name);
149189
}
150-
const list = name.split(".");
190+
const list = splitVariableText(name);
191+
// Object参照していない変数名の場合
151192
if (list.length === 1) {
152193
return factory.Identifier.create({
153194
name: name,
154195
});
155196
}
156197
const [n1, n2, ...rest] = list;
198+
// a.b のような単純な変数名の場合
157199
const first = factory.PropertyAccessExpression.create({
158-
expression: n1,
159-
name: n2,
200+
expression: n1.value,
201+
name: n2.value,
160202
});
161203

162-
return rest.reduce<ts.PropertyAccessExpression | ts.ElementAccessExpression>((previous, current: string) => {
163-
if (Utils.isAvailableVariableName(current)) {
204+
return rest.reduce<ts.PropertyAccessExpression | ts.ElementAccessExpression>((previous, current) => {
205+
if (current.kind === "string" && Utils.isAvailableVariableName(current.value)) {
164206
return factory.PropertyAccessExpression.create({
165207
expression: previous,
166-
name: current,
208+
name: current.value,
167209
});
168210
}
211+
// 直接 .value でアクセスできない場合に ["value"] といった形で参照する
169212
return factory.ElementAccessExpression.create({
170213
expression: previous,
171-
index: current,
214+
index: current.value,
172215
});
173216
}, first);
174217
};

src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,17 @@ export const escapeText = (text: string): string => {
1515
}
1616
return `"${text}"`;
1717
};
18+
19+
/** TODO escapeTextにマージする */
20+
export const escapeText2 = (text: string): { escaped: boolean; text: string } => {
21+
if (isAvailableVariableName(text)) {
22+
return {
23+
escaped: false,
24+
text: text,
25+
};
26+
}
27+
return {
28+
escaped: true,
29+
text: `"${text}"`,
30+
};
31+
};

test/__tests__/__snapshots__/parameter-test.ts.snap

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,78 @@ exports[`Parameter api.test.domain 1`] = `
315315
}
316316
}
317317
},
318+
{
319+
\\"operationId\\": \\"searchBook\\",
320+
\\"convertedParams\\": {
321+
\\"escapedOperationId\\": \\"searchBook\\",
322+
\\"argumentParamsTypeDeclaration\\": \\"Params$searchBook\\",
323+
\\"functionName\\": \\"searchBook\\",
324+
\\"requestContentTypeName\\": \\"RequestContentType$searchBook\\",
325+
\\"responseContentTypeName\\": \\"ResponseContentType$searchBook\\",
326+
\\"parameterName\\": \\"Parameter$searchBook\\",
327+
\\"requestBodyName\\": \\"RequestBody$searchBook\\",
328+
\\"hasRequestBody\\": false,
329+
\\"hasParameter\\": true,
330+
\\"pickedParameters\\": [
331+
{
332+
\\"name\\": \\"book.name\\",
333+
\\"in\\": \\"path\\",
334+
\\"required\\": true
335+
}
336+
],
337+
\\"requestContentTypes\\": [],
338+
\\"responseSuccessNames\\": [
339+
\\"Response$searchBook$Status$200\\"
340+
],
341+
\\"responseFirstSuccessName\\": \\"Response$searchBook$Status$200\\",
342+
\\"has2OrMoreSuccessNames\\": false,
343+
\\"responseErrorNames\\": [],
344+
\\"has2OrMoreRequestContentTypes\\": false,
345+
\\"successResponseContentTypes\\": [
346+
\\"application/json\\"
347+
],
348+
\\"successResponseFirstContentType\\": \\"application/json\\",
349+
\\"has2OrMoreSuccessResponseContentTypes\\": false,
350+
\\"hasAdditionalHeaders\\": false,
351+
\\"hasQueryParameters\\": false
352+
},
353+
\\"operationParams\\": {
354+
\\"httpMethod\\": \\"get\\",
355+
\\"requestUri\\": \\"/get/search/{book.name}\\",
356+
\\"comment\\": \\"\\",
357+
\\"deprecated\\": false,
358+
\\"parameters\\": [
359+
{
360+
\\"in\\": \\"path\\",
361+
\\"name\\": \\"book.name\\",
362+
\\"required\\": true,
363+
\\"schema\\": {
364+
\\"type\\": \\"string\\"
365+
}
366+
}
367+
],
368+
\\"responses\\": {
369+
\\"200\\": {
370+
\\"description\\": \\"Search Book Result\\",
371+
\\"content\\": {
372+
\\"application/json\\": {
373+
\\"schema\\": {
374+
\\"type\\": \\"object\\",
375+
\\"properties\\": {
376+
\\"id\\": {
377+
\\"type\\": \\"number\\"
378+
},
379+
\\"bookTitle\\": {
380+
\\"type\\": \\"string\\"
381+
}
382+
}
383+
}
384+
}
385+
}
386+
}
387+
}
388+
}
389+
},
318390
{
319391
\\"operationId\\": \\"getBookById\\",
320392
\\"convertedParams\\": {

test/__tests__/__snapshots__/spit-code-test.ts.snap

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ export interface Response$getReferenceItems$Status$200 {
4242
books?: Schemas.Item[];
4343
};
4444
}
45+
export interface Parameter$searchBook {
46+
\\"book.name\\": string;
47+
}
48+
export interface Response$searchBook$Status$200 {
49+
\\"application/json\\": {
50+
id?: number;
51+
bookTitle?: string;
52+
};
53+
}
4554
export interface Parameter$getBookById {
4655
/** Book ID */
4756
id: string;
@@ -72,6 +81,10 @@ export interface Params$getFullRemoteReference {
7281
parameter: Parameter$getFullRemoteReference;
7382
}
7483
export type ResponseContentType$getReferenceItems = keyof Response$getReferenceItems$Status$200;
84+
export type ResponseContentType$searchBook = keyof Response$searchBook$Status$200;
85+
export interface Params$searchBook {
86+
parameter: Parameter$searchBook;
87+
}
7588
export type ResponseContentType$getBookById = keyof Response$getBookById$Status$200;
7689
export interface Params$getBookById {
7790
parameter: Parameter$getBookById;
@@ -92,12 +105,13 @@ export interface QueryParameter {
92105
export interface QueryParameters {
93106
[key: string]: QueryParameter;
94107
}
95-
export type SuccessResponses = Response$getIncludeLocalReference$Status$200 | Response$getFullRemoteReference$Status$200 | Response$getReferenceItems$Status$200 | Response$getBookById$Status$200 | Response$deleteBook$Status$200;
108+
export type SuccessResponses = Response$getIncludeLocalReference$Status$200 | Response$getFullRemoteReference$Status$200 | Response$getReferenceItems$Status$200 | Response$searchBook$Status$200 | Response$getBookById$Status$200 | Response$deleteBook$Status$200;
96109
export namespace ErrorResponse {
97110
export type getIncludeLocalReference = void;
98111
export type getIncludeRemoteReference = void;
99112
export type getFullRemoteReference = void;
100113
export type getReferenceItems = void;
114+
export type searchBook = void;
101115
export type getBookById = void;
102116
export type deleteBook = void;
103117
}
@@ -160,6 +174,17 @@ export class Client<RequestOption> {
160174
};
161175
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
162176
}
177+
/**
178+
* operationId: searchBook
179+
* Request URI: /get/search/{book.name}
180+
*/
181+
public async searchBook(params: Params$searchBook, option?: RequestOption): Promise<Response$searchBook$Status$200[\\"application/json\\"]> {
182+
const url = this.baseUrl + \`/get/search/\${params.parameter[\\"book.name\\"]}\`;
183+
const headers = {
184+
Accept: \\"application/json\\"
185+
};
186+
return this.apiClient.request(\\"GET\\", url, headers, undefined, undefined, option);
187+
}
163188
/**
164189
* operationId: getBookById
165190
* Request URI: /get/books/{id}

0 commit comments

Comments
 (0)