Skip to content

Commit b44fc66

Browse files
committed
feat(ElasticApiParser): Now generates GraphQLFieldMap from elastic api
1 parent 719df33 commit b44fc66

File tree

5 files changed

+1056
-455
lines changed

5 files changed

+1056
-455
lines changed

scripts/apiParser/ElasticApiParser.js

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
/* @flow */
2+
/* eslint-disable no-param-reassign, class-methods-use-this */
3+
4+
import dox from 'dox';
5+
import fs from 'fs';
6+
import path from 'path';
7+
import {
8+
GraphQLString,
9+
GraphQLFloat,
10+
GraphQLBoolean,
11+
GraphQLObjectType,
12+
GraphQLEnumType,
13+
} from 'graphql';
14+
import { GraphQLJSON, upperFirst, TypeComposer } from 'graphql-compose';
15+
16+
import type {
17+
GraphQLArgumentConfig,
18+
GraphQLFieldConfigArgumentMap,
19+
GraphQLFieldMap,
20+
GraphQLInputType,
21+
} from 'graphql/type/definition';
22+
23+
export type ElasticApiParserOptsT = {
24+
version?:
25+
'5_0'
26+
| '5_x'
27+
| '2_4'
28+
| '2_3'
29+
| '2_2'
30+
| '2_1'
31+
| '2_0'
32+
| '1_7'
33+
| '1_6'
34+
| '1_5'
35+
| '1_4'
36+
| '1_3'
37+
| '1_2'
38+
| '1_1'
39+
| '1_0'
40+
| '0_90',
41+
prefix?: string,
42+
};
43+
44+
export type ElasticParamConfigT = {
45+
type: string,
46+
name?: string,
47+
options?: mixed,
48+
default?: mixed,
49+
};
50+
51+
export type ElasticCaSettingsUrlT = {
52+
fmt: string,
53+
req: {
54+
[name: string]: ElasticParamConfigT,
55+
},
56+
};
57+
58+
export type ElasticCaSettingsT = {
59+
params: {
60+
[name: string]: ElasticParamConfigT,
61+
},
62+
url?: ElasticCaSettingsUrlT,
63+
urls?: ElasticCaSettingsUrlT[],
64+
};
65+
66+
export const elasticApiFilesPath = './node_modules/elasticsearch/src/lib/apis/';
67+
68+
export default class ElasticApiParser {
69+
cachedEnums: {
70+
[fieldName: string]: { [valsStringified: string]: GraphQLEnumType },
71+
};
72+
version: string;
73+
prefix: string;
74+
75+
constructor(opts: ElasticApiParserOptsT = {}) {
76+
// derived from installed package `elasticsearch`
77+
// from ../../node_modules/elasticsearch/src/lib/apis/VERSION.js
78+
this.version = opts.version || '5_0';
79+
this.prefix = opts.prefix || 'Elastic';
80+
this.cachedEnums = {};
81+
}
82+
83+
run() {
84+
this.cachedEnums = {};
85+
const apiFilePath = path.resolve(elasticApiFilesPath, `${this.version}.js`);
86+
const source = this.loadApiFile(apiFilePath);
87+
return this.parseSource(source);
88+
}
89+
90+
parseSource(source: string): GraphQLFieldMap<*, *> {
91+
let result = {};
92+
93+
if (!source || typeof source !== 'string') {
94+
throw Error('Empty source. It should be non-empty string.');
95+
}
96+
97+
const doxAST = dox.parseComments(source, { raw: true });
98+
if (!doxAST || !Array.isArray(doxAST)) {
99+
throw Error('Incorrect responce from dox.parseComments');
100+
}
101+
102+
doxAST.forEach(item => {
103+
if (!item.ctx || !item.ctx.string) {
104+
return;
105+
}
106+
107+
// method description
108+
let description;
109+
if (item.description && item.description.full) {
110+
description = this.cleanupDescription(item.description.full);
111+
}
112+
113+
// prepare arguments and its descriptions
114+
const descriptionMap = this.parseParamsDescription(item);
115+
const argMap = this.settingsToArgMap(this.codeToSettings(item.code));
116+
Object.keys(argMap).forEach(k => {
117+
if (descriptionMap[k]) {
118+
argMap[k].description = descriptionMap[k];
119+
}
120+
});
121+
122+
const elasticMethod = this.getMethodName(item.ctx.string);
123+
124+
result[item.ctx.string] = {
125+
type: GraphQLJSON,
126+
description,
127+
args: argMap,
128+
resolve: (src, args, context) => {
129+
if (!context.elasticClient) {
130+
throw new Error(
131+
'You should provide `elasticClient` to GraphQL context'
132+
);
133+
}
134+
135+
if (Array.isArray(elasticMethod)) {
136+
return context.elasticClient[elasticMethod[0]][elasticMethod[1]](
137+
args
138+
);
139+
}
140+
141+
return context.elasticClient[elasticMethod](args);
142+
},
143+
};
144+
});
145+
146+
// reassamle nested methods, eg api.cat.prototype.allocation
147+
result = this.reassembleNestedFields(result);
148+
149+
return result;
150+
}
151+
152+
loadApiFile(absolutePath: string): string {
153+
const code = fs.readFileSync(absolutePath, 'utf8');
154+
return this.cleanUpSource(code);
155+
}
156+
157+
cleanUpSource(code: string): string {
158+
// remove invalid markup
159+
// {<<api-param-type-boolean,`Boolean`>>} converted to {Boolean}
160+
const codeCleaned = code.replace(/{<<.+`(.*)`.+}/gi, '{$1}');
161+
162+
return codeCleaned;
163+
}
164+
165+
parseParamsDescription(doxItemAST: any): { [fieldName: string]: string } {
166+
const descriptions = {};
167+
if (Array.isArray(doxItemAST.tags)) {
168+
doxItemAST.tags.forEach(tag => {
169+
if (!tag || tag.type !== 'param') return;
170+
if (tag.name === 'params') return;
171+
172+
const name = this.cleanupParamName(tag.name);
173+
if (!name) return;
174+
175+
descriptions[name] = this.cleanupDescription(tag.description);
176+
});
177+
}
178+
return descriptions;
179+
}
180+
181+
cleanupDescription(str: ?string): ?string {
182+
if (typeof str === 'string') {
183+
if (str.startsWith('- ')) {
184+
str = str.substr(2);
185+
}
186+
str = str.trim();
187+
188+
return str;
189+
}
190+
return undefined;
191+
}
192+
193+
cleanupParamName(str: ?string): ?string {
194+
if (typeof str === 'string') {
195+
if (str.startsWith('params.')) {
196+
str = str.substr(7);
197+
}
198+
str = str.trim();
199+
200+
return str;
201+
}
202+
return undefined;
203+
}
204+
205+
codeToSettings(code: string): ?ElasticCaSettingsT {
206+
// find code in ca({});
207+
const reg = /ca\((\{(.|\n)+\})\);/g;
208+
const matches = reg.exec(code);
209+
if (matches && matches[1]) {
210+
return eval('(' + matches[1] + ')'); // eslint-disable-line no-eval
211+
}
212+
return undefined;
213+
}
214+
215+
paramToGraphQLArgConfig(
216+
paramCfg: ElasticParamConfigT,
217+
fieldName: string
218+
): GraphQLArgumentConfig {
219+
const result: GraphQLArgumentConfig = {
220+
type: this.paramTypeToGraphQL(paramCfg, fieldName),
221+
};
222+
if (paramCfg.default) {
223+
result.defaultValue = paramCfg.default;
224+
}
225+
226+
return result;
227+
}
228+
229+
paramTypeToGraphQL(
230+
paramCfg: ElasticParamConfigT,
231+
fieldName: string
232+
): GraphQLInputType {
233+
switch (paramCfg.type) {
234+
case 'string':
235+
return GraphQLString;
236+
case 'boolean':
237+
return GraphQLBoolean;
238+
case 'number':
239+
return GraphQLFloat;
240+
case 'time':
241+
return GraphQLString;
242+
case 'list':
243+
return GraphQLJSON;
244+
case 'enum':
245+
// $FlowFixMe
246+
if (Array.isArray(paramCfg.options)) {
247+
return this.getEnumType(fieldName, paramCfg.options);
248+
}
249+
return GraphQLString;
250+
default:
251+
console.log(`New type '${paramCfg.type}' in elastic params setting.`); // eslint-disable-line
252+
return GraphQLJSON;
253+
}
254+
}
255+
256+
getEnumType(fieldName: string, vals: string[]): GraphQLEnumType {
257+
const key = fieldName;
258+
const subKey = JSON.stringify(vals);
259+
260+
if (!this.cachedEnums[key]) {
261+
this.cachedEnums[key] = {};
262+
}
263+
264+
if (!this.cachedEnums[key][subKey]) {
265+
const values = vals.reduce(
266+
(result, val) => {
267+
if (val === '') {
268+
result.empty_string = { value: '' };
269+
} else if (Number.isFinite(val)) {
270+
result[`number_${val}`] = { value: val };
271+
} else {
272+
result[val] = { value: val };
273+
}
274+
return result;
275+
},
276+
{}
277+
);
278+
279+
let postfix = Object.keys(this.cachedEnums[key]).length;
280+
if (postfix === 0) postfix = '';
281+
else postfix = `_${postfix}`;
282+
283+
this.cachedEnums[key][subKey] = new GraphQLEnumType({
284+
name: `${this.prefix}Enum_${upperFirst(fieldName)}${postfix}`,
285+
values,
286+
});
287+
}
288+
289+
return this.cachedEnums[key][subKey];
290+
}
291+
292+
settingsToArgMap(
293+
settings: ?ElasticCaSettingsT
294+
): GraphQLFieldConfigArgumentMap {
295+
const result = {};
296+
const { params, urls, url } = settings || {};
297+
if (params) {
298+
Object.keys(params).forEach(k => {
299+
const fieldConfig = this.paramToGraphQLArgConfig(params[k], k);
300+
if (fieldConfig) {
301+
result[k] = fieldConfig;
302+
}
303+
});
304+
}
305+
306+
const urlList = urls || (url ? [url] : null);
307+
308+
if (Array.isArray(urlList)) {
309+
urlList.forEach(item => {
310+
if (item.req) {
311+
Object.keys(item.req).forEach(k => {
312+
const fieldConfig = this.paramToGraphQLArgConfig(item.req[k], k);
313+
if (fieldConfig) {
314+
result[k] = fieldConfig;
315+
}
316+
});
317+
}
318+
});
319+
}
320+
321+
return result;
322+
}
323+
324+
getMethodName(str: string): string | string[] {
325+
const parts = str.split('.');
326+
if (parts[0] === 'api') {
327+
parts.shift();
328+
}
329+
if (parts.length === 1) {
330+
return parts[0];
331+
} else {
332+
return parts.filter(o => o !== 'prototype');
333+
}
334+
}
335+
336+
reassembleNestedFields(fields: GraphQLFieldMap<*, *>): GraphQLFieldMap<*, *> {
337+
const result = {};
338+
Object.keys(fields).forEach(k => {
339+
const name = this.getMethodName(k);
340+
if (Array.isArray(name)) {
341+
if (!result[name[0]]) {
342+
result[name[0]] = {
343+
type: new GraphQLObjectType({
344+
name: `${this.prefix}Methods_${upperFirst(name[0])}`,
345+
// $FlowFixMe
346+
fields: () => {},
347+
}),
348+
};
349+
}
350+
TypeComposer.create(result[name[0]].type).setField(name[1], fields[k]);
351+
} else {
352+
result[name] = fields[k];
353+
}
354+
});
355+
356+
return result;
357+
}
358+
}

0 commit comments

Comments
 (0)