-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.js
executable file
·307 lines (289 loc) · 14.1 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
#!/usr/bin/env node
let fs = require('fs-extra'),
path = require('path'),
program = require('commander'),
cheerio = require('cheerio'),
strlist = 'list<string>',
intlist = 'list<int>',
floatlist = 'list<float>',
$;
// 命令行参数
let collect = function(val, col) {
col.push(val);
return col;
};
let batchCollect = function(val) {
val = val.replace(/\,+/ig, ',').replace(/,$/g,'');
return val.split(',');
};
program
.version('0.0.1')
.usage('[option] <file ...>')
.option('-f, --file <type>', 'set input file name','index.html')
.option('-m, --module [name]', 'set the module name')
.option('-b, --base [name]', 'set the base class names', collect, [])
.option('-B, --clientbase [name', 'set the httpclient base class', 'PMLRESTBase')
.option('-c, --classes [name]', 'set the class names', collect, [])
.option('-C, --batchclasses [name,name,name]', 'batch sub class names', batchCollect, [])
.option('-D, --batchdes [name]', 'set each root model a description', batchCollect, [])
.option('-p, --passkeys [key]', 'set exclued keys', collect, [])
.option('-P, --batchpasskeys [key,key,key]', 'batch set exclued keys', batchCollect, [])
.option('-a, --author [name]', 'set the author name', 'walker')
.option('-j, --project [name]', 'set the project name', 'Project')
.option('-r, --copyright [name]', 'set the copyright name', 'WeDoctor Group')
.option('-o, --tableoffset [count]', 'how many table need skiped to parse', 0)
.option('--debug', 'the output.json file will gen', false)
.option('--verbose', 'show buzz logs', false)
.parse(process.argv);
let baseClasses = program.base.length || ['PMLResponseModelBaseHD', 'PMLModelBase'],
classCollect = [...program.classes, ...program.batchclasses],
passKeys = [...program.passkeys, ...program.batchpasskeys],
tableoffset = program.tableoffset,
typeFactory = classNameGenerator();
let entities = [], // 实体类对应的数组
methodArgs = [], // 方法参数对应的数组
endpoints = [], // 接口请求地址数组
responseModel = [], // 接口返回值类型数组 => 完全由 endpoint 转化而来
methods = []; // 接口对应方法名数组 => 完全由 endpoint 转化而来 {name: param:}
methodTitles = []; // 接口标题数组
respFactory = responseModelGenerator();
(async () => {
let content = await readFile(program.file),
$ = cheerio.load(content);
// 解析文本, 移除"样例"一节
$(".wiki-content .table-wrap:contains({)").remove();
// 解析文本, 移除不需要的 table (目前只支持移除从0开始的)
if(tableoffset>0) $(".wiki-content .table-wrap").slice(0, tableoffset).remove();
// 解析文本, 得到[enpoints, resopnsemodel, methods]
parseEndpoints($(".wiki-content p").text().replace(/\s/ig, ''));
// 解析文本, 得到方法标题列表
$(".wiki-content *:has(.toc-macro)").remove(); // 移除目录
$(".wiki-content h1[id*=id-]").each((i,m) => methodTitles.push($(m).text()));
$(".wiki-content h2[id*=id-]").each((i,m) => methodTitles.push($(m).text()));
// 解析请求和响应的 Table
$(".wiki-content>.table-wrap").each((i, table) => {
if(i%2 == 0) return parseRequestTable(table); // => 得到方法参数数组
parseResponseTable(table); // => 得到实体类数组
});
if(program.debug) await fs.writeJson('./output.json', entities).catch(console.log);
// 应用模板
await parseTemplate().catch(console.log);
// 完成
if(program.verbose) console.log("done!");
function parseResponseTable(table, classMeta) {
let modelName = "",
baseName = baseClasses[0],
isRoot = true; // 子类如果需要继承不同的基类, 则利用此标识
if(classMeta) {
// 有classMeta, 说明是一个子类
modelName = classMeta["model"];
baseName = baseClasses[1];
isRoot = false;
}else{
modelName = respFactory.next().value;
}
let rowIsTable = false,
hasIdKey = false, // 该表格里如果有 id 键会打标, 因为 id 在 OC 里是关键字
props = [],
complexProperty; // 如果当前行表示是个对象或数据, 把元数据保存, 用来生成子表格对应的类
$(table).children(".confluenceTable").children("tbody").children("tr")
.each((i,tr) => {
if($(tr).text().trim().length == 0) return; // 空行不处理
if(rowIsTable){
// 进入这个方法,说明上一行标识这一行是子类
rowIsTable = false;
// return parseResponseTable($(tr).children("td").children(".table-wrap"), complexProperty);
// 有时候文档把子表格写在了第二个 td 里...
return parseResponseTable($(tr).find(".table-wrap").eq(0), complexProperty);
}
let tds = $(tr).children('td');
let nameMatch = /[a-z]+/ig.exec(tds.eq(0).text());
if(!nameMatch) return; // 第一格非英文则理解为不是属性名
if(passKeys.includes(nameMatch[0])) return; // 包含预设排除关键字, 不需要处理
// 记录属性名, 类型, 注释等
let pname = tds.eq(0).text().trim();
let ptype = tds.eq(2).text().trim() || 'object'; // 没有足够的列, 说明下一行是一个对象, 被合并单元格了, 如果是数组会标明是 list 的
let pdes = tds.eq(3).text().trim() || "";
if(!ptype || ptype.length == 0)
return console.log("当前行找不到类型定义, 请检查当前行数据: ",$(tr).html(), $(tr).text());
let isComplexObj = isObjectOrArray(nameMatch[0], ptype); // 包含预设子类关键字, 理解为复杂对象
let isArray = ptype.toLowerCase() == 'list';
if(isComplexObj) ptype = typeFactory.next().value;
if(isComplexObj) console.log(`${pname} ==> ${ptype}`);
if(ptype == "object")
return console.log("object 行未发现对应的类:", $(tr).html(), $(tr).text(), tds.eq(2).html(), isComplexObj);
if(!ptype || ptype.length == 0)
return console.log("类名个数不符", $(tr).text()); // 生成器没生成类名, 说明数量给少了
if(ptype == 'list')
return console.log("没有找到该行 list 对应的类型, 请检查当前行数据:", $(tr).html(), tds.eq(2).text(), isComplexObj)
let assume_type = assumeVarType(ptype, isArray, ptype);
let prop = {
"name": pname,
"des": tds.eq(1).text().replace(/[\s\/\*]/ig,''),
"type": assume_type[0],
"isArray": isArray
};
if(pname == 'id') {
prop["name"] = "recordId";
hasIdKey = true;
}
if(pdes) {
prop["des"] = prop["des"] + " " + pdes;
}
if(isComplexObj){
prop["model"] = assume_type[1];
rowIsTable = true;
complexProperty = prop;
}
props.push(prop);
}); // end of basetable > tr > foreach
entities.push({"isRoot": isRoot, "hasIdKey": hasIdKey, "className": modelName,"baseName": baseName, "props": props});
if(program.verbose) console.log("生成模型:", modelName)
}
function parseRequestTable(table) {
let param_des = [];
$(table).children(".confluenceTable").children("tbody").children("tr")
.each((i, tr) => {
if(i<2) return;
let tds = Array.from($(tr).children('td'));
if($(tds[0]).text() == '业务参数' || $(tds[0]).text().trim().length == 0) return;
let propdes = tds.map(t=>$(t).text().replace(/[\s]/ig, '')).join(' ');
param_des.push(propdes);
});
methodArgs.push(param_des);
}
// 根据解析出路径, 响应类型, 和参数数组
function parseEndpoints(uris){
endpoints = uris.match(/\/[\w\/\.]+/ig).map(m=>m.replace('}','')).filter(m=>m.indexOf('.json')>0); // 从文档中提取接口地址
responseModel = endpoints.map(e=>'Response'+e.replace(/\/(\w)/ig,underscoreToCamel).replace('.json','')) // 从接口地址生成返回值名
methods = endpoints.map(e=>'method'+e.replace(/\/(\w)/ig,underscoreToCamel).replace('.json','')); // 从接口地址生成方法名
}
})().catch(console.log);
// 响应类类名生成器
function* responseModelGenerator() {
yield* responseModel;
}
// 子类名生成器
function* classNameGenerator() {
yield* classCollect;
}
async function readFile(filename) {
let fullpath = path.resolve(filename);
if(program.verbose) console.log('start processing file:', fullpath);
return await fs.readFile(fullpath, 'utf8').catch(console.log);
}
// 是否对象或数组(简单数组不算)
function isObjectOrArray(keystr, typestr) {
typestr = typestr.toLowerCase().trim();
let isPrimaryType = ['int', 'integer', 'long', 'string', 'bool', 'boolean', 'date', 'float', 'double'].includes(typestr),
isPrimaryList = [intlist, strlist].includes(typestr);
return !isPrimaryType && !isPrimaryList; // 不再考虑用户定义, 发现非简单类型都默认下一行是子表
}
/**
* 根据关键字推断类型
* @prarm str: 关键字
* @param isArray: 是否数组类型
* @param model: 自定义类型
* @return 返回[变量类型, 模型类型]
* 比如 [NSArray<Doctor *> *, Doctor *]
* 一个用于建模, 一个用于写属性
*/
function assumeVarType(str, isArray, model) {
let l_str = str.toLowerCase(),
model_type = str, // 类型
var_type = str; // 字段
if(['string','date'].includes(l_str)) model_type = "NSString *";
else if(['bool', 'boolean'].includes(l_str)) model_type = "BOOL";
else if(['int', 'integer', 'long'].findIndex(v=>(new RegExp(v,'ig')).test(l_str)) >= 0) model_type = "NSInteger";
else if(['float', 'dobule'].findIndex(v=>(new RegExp(v,'ig')).test(l_str)) >= 0) model_type = "CGFloat";
else if(l_str == strlist) model_type = "NSArray<NSString *> *"
else if(l_str == intlist) model_type = "NSArray<NSNumber *> *";
else model_type = model + " *";
var_type = isArray ? "NSArray<"+model_type+"> *" : model_type;;
model_type = model_type.replace(' *','');
return [var_type, model_type];
}
function getPath(...components) {
return path.join(__dirname, ...components);
}
function underscoreToCamel(all, letter, index, text) {
// 第一个参数是当次匹配到的全文
// 第二个参数是当次匹配到第一组, 以此类推
// 只要参数数量足够, 最后两个就是第一个匹配的索引和原始文本
return letter.toUpperCase();
}
async function getFileContent(...components) {
let fullpath = getPath(...components);
return await fs.readFile(fullpath, 'utf8').catch(console.log);
}
async function renderFile(filepath, tpl) {
return await fs.writeFile(filepath, tpl, 'utf8').catch(console.log);
}
// 参数: 模型, 文本, 入参描述
async function parseTemplate() {
if(program.verbose) console.log("开始应用模板");
let copyright = program.copyright,
projectname = program.project,
author = program.author,
modulename = program.module,
httpclient = program.clientbase,
model_path = "templates/model",
task_path = "templates/httpclient",
// 模板内容
h_content = await getFileContent(model_path, 'template.h'),
m_content1 = await getFileContent(model_path, 'template.m'),
m_content2 = await getFileContent(model_path, 'templatebase.m'),
h_task = await getFileContent(task_path, 'task.h'),
m_task = await getFileContent(task_path, 'task.m');
let out_model = "output_model",
out_task = "output_task";
await fs.emptyDir(out_model); // 创建/清空输出文件夹
await fs.emptyDir(out_task); // 创建/清空输出文件夹
if(program.verbose) console.log("开始生成实体类");
// 先去重
let usedModels = [],
datasource = [];
entities.forEach((model, index) => {
if(classCollect.filter(m=>m==model.className).length==1) return datasource.push(model);
if(usedModels.includes(model.className)) return;
usedModels.push(model.className);
datasource.push(model);
});
datasource.forEach(async (model, index) => {
// if(classCollect.filter(m=>m==model.className).length>1) {
// if(exist_file.includes(model)) return;
// exist_file.push(model);
// }
let m_content = model.isRoot ? m_content2 : m_content1;
// 输出路径
let h_file = getPath(out_model, model.className+'.h'),
m_file = getPath(out_model, model.className+'.m');
await renderFile(h_file, eval(h_content)).catch(console.log);
await renderFile(m_file, eval(m_content)).catch(console.log);
});
// ===================
// gen http request (task) file
// ===================
if(program.verbose) console.log("开始生成请求类");
let h_file = getPath(out_task, `${modulename}.h`),
m_file = getPath(out_task, `${modulename}.m`);
if(endpoints.length>methodArgs.length)
return console.log("接口数量与入参数量不一致的", endpoints, methodArgs); // 接口由解析文本来的, 入参描述由解析表格来的, 可能不一致, 但是入参可以多(有的接口不需要post->.json)
if(endpoints.length>methodTitles.length)
return console.log("接口数量与接口标题数量不一致", endpoints, methodTitles); // 分别由解析文本而来, 可能不一致
if(endpoints.length<methodTitles.length)
console.log("警告: 接口数量比接口标题数量少", endpoints, methodTitles); // 这是合理的, 有的接口写在文档上并不需要做请求
endpoints = endpoints.map((e,i)=>{
return {
"httpclient": httpclient,
"path": e,
"method": methods[i],
"model": responseModel[i],
"des": methodTitles[i].replace(/\s/ig, ''),
"args": methodArgs[i]
}
});
await renderFile(h_file, eval(h_task)).catch(console.log);
await renderFile(m_file, eval(m_task)).catch(console.log);
console.log('=====END=====')
}