-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
executable file
·202 lines (164 loc) · 5.03 KB
/
index.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
#!/usr/bin/env node
const fs = require('fs')
const {
parse,
AST_NODE_TYPES,
} = require('@typescript-eslint/typescript-estree')
const AST_NODES_TO_OPEN_RPC_TYPES = require('./ast-nodes-to-ts-open-rpc')
const REF = '$ref'
// Validate command line input
const [, , methodName, tsFilePath] = process.argv
if (!methodName) {
throw new Error('Must specify a method name as first argument.')
}
if (!tsFilePath || !tsFilePath.endsWith('.ts') || !fs.existsSync(tsFilePath)) {
throw new Error('Specified file is not a .ts file or does not exist.')
}
// Read file and pass contents to AST parser, validate AST conents
const tsFileContent = fs.readFileSync(tsFilePath)
const tsParsedContent = parse(tsFileContent)
const { body } = tsParsedContent
if (!body.length) {
throw new Error('TypeScript file is empty.')
}
if (body.length > 1) {
console.warn(
'TypeScript file has multiple top-level nodes. This experimental utility will ignore all but the first one.'
)
}
const [topNode] = body
if (topNode.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
throw new Error(
`Node is of type '${topNode.type}'. Only interfaces are supported.`
)
}
if (topNode.body.type !== AST_NODE_TYPES.TSInterfaceBody) {
throw new Error(
`Unexpected interface declaration body type: ${topNode.body.type}`
)
}
// Start conversion of AST to OpenRPC interface
const { name: interfaceName } = topNode.id
const intermediaryNodes = []
const openRpcComponents = { schemas: {} }
const tsInterface = topNode.body.body
for (const tsNode of tsInterface) {
if (tsNode.type !== AST_NODE_TYPES.TSPropertySignature) {
throw new Error(`Unexpected node type: ${tsNode.type}`)
}
const intermediaryNode = {
name: tsNode.key.name,
required: !tsNode.optional,
}
const tsNodeType = tsNode.typeAnnotation.typeAnnotation.type
if (tsNodeType in AST_NODES_TO_OPEN_RPC_TYPES) {
intermediaryNode.type = AST_NODES_TO_OPEN_RPC_TYPES[tsNodeType]
} else if (tsNodeType === AST_NODE_TYPES.TSTypeLiteral) {
processTypeLiteral()
} else {
throw new Error(`Unsupported node type: ${tsNodeType}`)
}
intermediaryNodes.push(intermediaryNode)
// console.log(JSON.stringify(tsNode, null, 2))
function processTypeLiteral() {
const innerNode = tsNode.typeAnnotation.typeAnnotation
// in this case, we have an inline interface
if (innerNode.members) {
const componentKey =
intermediaryNode.name.charAt(0).toUpperCase() +
intermediaryNode.name.slice(1)
const intermediaryProperties = []
for (const memberNode of innerNode.members) {
const memberName = memberNode.key.name
const memberOpenRpcType =
AST_NODES_TO_OPEN_RPC_TYPES[
memberNode.typeAnnotation.typeAnnotation.type
]
if (!memberOpenRpcType) {
throw new Error(
`Unsupported inner node type: ${memberNode.typeAnnotation.typeAnnotation.type}`
)
}
intermediaryProperties.push({
name: memberName,
required: !memberNode.optional,
type: memberOpenRpcType,
})
}
sortIntermediaryNodeObjects(intermediaryProperties)
openRpcComponents.schemas[componentKey] = getOpenRpcComponentObject(
intermediaryProperties
)
intermediaryNode[REF] = getComponentPathString(componentKey)
} else {
throw new Error('Unsupported literal type.')
}
}
}
sortIntermediaryNodeObjects(intermediaryNodes)
openRpcComponents.schemas[interfaceName] = getOpenRpcComponentObject(
intermediaryNodes
)
// Create final OpenRPC object
const openRpc = {
name: methodName,
params: [
{
name: interfaceName,
description: `The ${interfaceName}`,
schema: { [REF]: getComponentPathString(interfaceName) },
},
],
components: {
// sort the component schemas alphabetically before adding them
schemas: Object.keys(openRpcComponents.schemas)
.sort()
.reduce((newComponents, key) => {
newComponents[key] = openRpcComponents.schemas[key]
return newComponents
}, {}),
},
}
// Output
console.log(JSON.stringify(openRpc, null, 2))
//
// Utils
//
/**
* Gets an OpenRPC component path string.
*/
function getComponentPathString(schemaName) {
return `#/components/schema/${schemaName}`
}
/**
* Sort param object property nodes alphabetically and then by required, in place.
*/
function sortIntermediaryNodeObjects(nodeArray) {
nodeArray.sort((a, b) => a.name.localeCompare(b.name))
nodeArray.sort((a, b) => Number(b.required) - Number(a.required))
}
/**
* Gets an OpenRPC component object from an array of intermediary node objects.
*/
function getOpenRpcComponentObject(nodes) {
const component = {
type: 'object',
required: [],
properties: {},
}
nodes.forEach((node) => {
if (node.required) {
component.required.push(node.name)
}
if (REF in node) {
component.properties[node.name] = {
[REF]: node[REF],
}
} else {
component.properties[node.name] = {
type: node.type,
}
}
})
return component
}