This repository has been archived by the owner on Jun 2, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.ts
271 lines (263 loc) · 10.8 KB
/
utils.ts
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
import * as yaml from 'yamljs'
import * as ts from 'typescript'
import { URL } from 'url'
import * as request from 'request'
import { Export, AnyType } from './constants'
import { readFileSync } from 'fs'
import { join, normalize } from 'path'
import { DefaultFileSystemHost } from 'ts-simple-ast/dist/fileSystem'
import { Schema } from 'swagger-schema-official'
import * as Types from './code/types'
import * as $RefParser from 'json-schema-ref-parser'
import { JSDoc } from './code/server'
// tslint:disable-next-line:quotemark (conflict with prettier)
const ValidCharsInURLSpecButNOTInVarName = "-._~:/?#[]@!&'()*+,;= ".split('')
/**
* A FileSystemHost provided to typescript compiler
*/
export class ReadonlyFileSystemHost extends DefaultFileSystemHost {
public changes = new Map<string, string>()
getPath(filePath: string) {
return normalize(join(this.getCurrentDirectory(), filePath))
}
async writeFile(filePath: string, fileText: string) {
this.changes.set(this.getPath(filePath), fileText)
}
writeFileSync(filePath: string, fileText: string) {
this.changes.set(this.getPath(filePath), fileText)
}
readFileSync(filePath: string, encoding?: string) {
return this.changes.get(this.getPath(filePath)) || super.readFileSync(filePath, encoding)
}
async readFile(filePath: string, encoding?: string) {
const f = this.changes.get(this.getPath(filePath))
return f ? Promise.resolve(f) : super.readFile(filePath, encoding)
}
}
/** Get a valid variable name from url string */
export function getValidVarName(name: string) {
return name
.split('')
.map(char => (ValidCharsInURLSpecButNOTInVarName.indexOf(char) !== -1 ? '_' : char))
.join('')
.replace('{', '$')
.replace('}', '')
}
/** Parse JSON or Yaml */
export function parseJSONorYAML(text: string): any {
try {
return JSON.parse(text)
} catch (jsonerr) {
try {
return yaml.parse(text)
} catch (yamlerr) {
const jsonmsg = jsonerr.message
const yamlmsg = yamlerr.message
throw new SyntaxError(`JSON and Yaml parser failed to parse the input file
JSON: ${jsonmsg}
Yaml: ${yamlmsg} at line ${yamlerr.parsedLine || 'unknown'}`)
}
}
}
/** Request a file */
export function requestFile(url: string): Promise<string> {
try {
const u = new URL(url)
// This also includes https
if (!u.protocol.includes('http')) {
throw new Error('This is a file')
}
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
if (error) return reject(error)
try {
return resolve(body)
} catch (e) {
return reject(e)
}
})
})
} catch {
return Promise.resolve(readFileSync(url, 'utf-8'))
}
}
/** Create a Typescript Async function Declaration */
export function GenerateAsyncFunction(
name: string | ts.Identifier,
body: ts.FunctionBody,
parameters: ts.ParameterDeclaration[] = [],
returnType: ts.TypeNode = AnyType,
JSDocCommet?: string,
modifiers: ts.Modifier[] = [],
) {
const returnTypePromise = ts.createTypeReferenceNode('Promise', [returnType])
const node = ts.createFunctionDeclaration(
undefined,
[Export, ...modifiers],
undefined,
name,
undefined,
parameters,
returnTypePromise,
body,
)
JSDocCommet && ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `* ${JSDocCommet} `, true)
return node
}
export type JSONSchemaPrimitive = 'array' | 'boolean' | 'integer' | 'float' | 'number' | 'null' | 'object' | 'string'
export type OpenAPI2AdditionalPrimitive = 'long' | 'double'
/** JSON Schema -> Schema2ts inner express */
export async function createJSONSchemaToTypes(document: any, nameOptions?: any) {
const doc: $RefParser.$Refs = await ($RefParser as any).resolve(document)
return function JSONSchemaToTypes(from: Schema): Types.Type {
//#region Speical properties
if (from.$ref) {
const result = doc.get(from.$ref)
const name = getJSONRefName(from, nameOptions)
try {
return new Types.TypeReferenceType(name!, JSONSchemaToTypes(result as any))
} catch {
return new Types.TypeReferenceType(name!, Types.shape(result))
}
} else if (from.allOf) {
/**
* allOf, anyOf, oneOf, See this
* https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.3 */
return new Types.And(from.allOf.map(x => JSONSchemaToTypes(x)))
} else if ((from as any).anyOf) {
console.warn(`Typescript can not express JSON 'anyOf' easily, treat as 'oneOf'`)
const newer = { ...from, oneOf: (from as any).anyOf, anyOf: undefined }
return JSONSchemaToTypes(newer)
} else if ((from as any).oneOf) {
return new Types.Or(((from as any).oneOf as Schema[]).map(x => JSONSchemaToTypes(x)))
} else if (from.additionalProperties) {
const newer = { ...from, additionalProperties: undefined }
return new Types.And([JSONSchemaToTypes(newer), JSONSchemaToTypes(from.additionalProperties)])
} else if (from.enum) {
const newer = { ...from, enum: undefined }
if (from.type === 'string' || from.type === 'number') {
const enums: string[] = from.enum.filter(x => typeof x === 'string') as any
let EnumName: string | undefined = undefined
// Speical case for OpenAPI 2.0
if (document.definitions) {
for (const key in document.definitions) {
const _def = document.definitions[key].enum
// Check if enum in the definitions is the same as current enum
// Cause OpenAPI 2.0 enum use yaml reference, so the name is lost in the convertion
// We have to use this way to compare
if (
_def &&
document.definitions[key].type === from.type && // string enum is not equal to number enum
Array.isArray(_def) &&
_def.every((v, i) => v === from.enum![i]) // Since the enum ref is using yaml ref, it must keep the same order
) {
EnumName = key
}
}
}
if (EnumName) {
// We get the name of enum
return new Types.EnumOf(
EnumName,
enums.map((x, index) => ({
name: x as string,
value: from.type === 'string' ? (x as string) : index,
// If this is a string enum, then every enum value is the name of itself
})),
)
} else {
// We can't figure out the name of enum, so we have to use a | b | c type
const or = new Types.Or(
enums.map((member, index) => new Types.Literal(from.type === 'string' ? member : index, true)),
)
from.type === 'number' && or.addJSDoc(['enum:', ...enums.map((x, index) => ` ${x}: ${index}`)])
return or
}
} else {
console.warn(`Schema2ts can not create **enum** of type **${from.type}**, use it original type instead`)
return JSONSchemaToTypes(newer)
}
}
//#endregion
const type: JSONSchemaPrimitive | OpenAPI2AdditionalPrimitive = from && (from.type as any)
if (isNumber(type)) return new Types.Literal(0, false)
switch (type) {
case 'array':
if (Array.isArray(from.items)) {
return new Types.ArrayOf(JSONSchemaToTypes(from.items[0]))
} else if (!from.items) {
return new Types.Any()
} else {
return new Types.ArrayOf(JSONSchemaToTypes(from.items))
}
case 'object':
const props = from.properties!
const ofWhat: Types.ObjectOfWhat[] = []
for (const key in props) {
const schema = props[key]
ofWhat.push({
key: key,
value: JSONSchemaToTypes(schema),
optional: (from.required || []).every(x => x !== key),
jsdoc: from.description,
readonly: from.readOnly,
defaultValue: from.default,
})
}
return new Types.ObjectOf(ofWhat)
case 'string':
return new Types.Literal('', false)
case 'boolean':
return new Types.Literal(true, false)
case 'null':
return new Types.Any()
default:
try {
const newer = { ...from, type: 'object' }
return JSONSchemaToTypes(newer)
} catch {
return JSONSchemaToTypes({ type: 'null' })
}
}
}
}
function isNumber(n?: string) {
switch (n) {
case 'integer':
case 'long':
case 'float':
case 'double':
case 'number':
return true
default:
return false
}
}
export const getJSONRefName = (x: Schema, openapi?: any) => {
// For valid name, see https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-04#section-3
let name = x.$ref
if (typeof name !== 'string') return undefined
if (name === '#') {
return 'Document'
}
if (name.startsWith('#')) name = name.replace(/^#/, '')
if (name === '/') return 'Document'
if (name.startsWith('/')) name = name.replace(/^\//, '')
if (openapi) {
if (name.startsWith('definitions')) return getValidVarName(name).replace(/^definitions_/, '')
}
return getValidVarName(name)
}
export function createJSDoc(jsdoc?: JSDoc) {
if (!jsdoc || Object.keys(jsdoc).filter(x => (jsdoc as any)[x] !== undefined).length === 0) {
return
}
let comment = ''
if (jsdoc.depercated) comment += '\n@depercated'
if (jsdoc.summary) comment += '\n@summary ' + jsdoc.summary
if (jsdoc.description) comment += '\n@description ' + jsdoc.description
if (jsdoc.see) comment += '\n@see ' + jsdoc.see
comment = comment.replace(/^\n/, '')
if (comment.match(/\n/)) comment += '\n'
return comment
}