Created
May 18, 2019 19:57
-
-
Save kpp/a147cb55125ff480111ab23df8f1aad2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Message = | |
{ kind: "request", request: string, fields: [string, string][] } | | |
{ kind: "response", response: string, fields: [string, string][] } | | |
{ kind: "event", event: string, fields: [string, string][] } | |
type Enum = { kind: "enum", enum: string, items: string[] } | |
type SpecItem = Message | Enum | |
function messageName(msg: Message): string { | |
switch(msg.kind) { | |
case "request": | |
return msg.request | |
case "response": | |
return msg.response | |
case "event": | |
return msg.event | |
} | |
} | |
class SpecParser { | |
state: SpecItem | null | |
result: SpecItem[] | |
constructor() { | |
this.state = null | |
this.result = [] | |
} | |
parse(input: string): SpecItem[] { | |
const lines = input.split("\n") | |
.filter(l => l != "") | |
lines.forEach(line => { | |
this.parseLine(line) | |
}); | |
this.completeItem() | |
return this.result | |
} | |
parseLine(line: string) { | |
const words = line.split(" ") | |
.filter(w => w != "") | |
if(line.charAt(0) != ' ') { | |
this.completeItem() | |
switch(words[0]) { | |
case "request": { | |
this.state = { kind: "request", request: words[1], fields: [] } | |
break | |
} | |
case "response": { | |
this.state = { kind: "response", response: words[1], fields: [] } | |
break | |
} | |
case "event": { | |
this.state = { kind: "event", event: words[1], fields: [] } | |
break | |
} | |
case "enum": { | |
this.state = { kind: "enum", enum: words[1], items: [] } | |
break | |
} | |
default: | |
throw new Error("Invalid spec item") | |
} | |
} | |
else if(notBlank(line)) { | |
if(this.state.kind != "enum") { | |
(this.state as Message).fields.push([words[0], words[1]]) | |
} | |
else { | |
(this.state as Enum).items.push(words[0]) | |
} | |
} | |
} | |
completeItem() { | |
if (this.state != null) { | |
this.result.push(this.state) | |
} | |
} | |
} | |
function notBlank(line: string): boolean { | |
return line.match(/\w+/) != null | |
} | |
function generateRustEnum(spec: SpecItem[]): string { | |
const messages = spec.filter(i => i.kind != "enum") as Message[] | |
let enums = [] | |
messages.forEach(m => { | |
const name = messageName(m) | |
const fields = m.fields | |
.map(f => `${f[0]}: ${f[1]}`) | |
.join(', ') | |
enums.push(`${name} { ${fields} }`) | |
}) | |
return enums.join(",\n") | |
} | |
function tsType(ty: string): string { | |
switch(ty) { | |
case "String": | |
return "string" | |
case "u32": | |
return "number" | |
case "u64": | |
return "number" | |
case "usize": | |
return "number" | |
case "bool": | |
return "boolean" | |
case "Vec<u32>": | |
return "number[]" | |
case "Vec<u8>": | |
return "string" | |
default: | |
return ty | |
} | |
} | |
function capitalKind(kind: string): string { | |
switch(kind) { | |
case "request": { | |
return "Request" | |
} | |
case "response": { | |
return "Response" | |
} | |
case "event": { | |
return "Event" | |
} | |
} | |
} | |
function generateTsInterfaces(spec: SpecItem[]): string { | |
const messages = spec.filter(i => i.kind != "enum") as Message[] | |
let imports = [] | |
let interfaces = [] | |
messages.forEach(m => { | |
const name = messageName(m) | |
const fields = m.fields | |
.map(f => ` "${f[0]}": ${tsType(f[1])}`) | |
.join("\n") | |
let base = capitalKind(m.kind) | |
imports.push(`${base}s.${name}`) | |
interfaces.push(`export interface ${name} extends ${base} {\ | |
\n "${m.kind}": "${name}"\ | |
\n${fields}\ | |
\n}`) | |
}) | |
return `${imports.join(" |\n")}\n\n${interfaces.join("\n\n")}` | |
} | |
function generateSchemas(spec: SpecItem[]) { | |
const enums = spec.filter(i => i.kind == "enum") as Enum[] | |
const messages = spec.filter(i => i.kind != "enum" && i.kind != "request") as Message[] | |
const refs = [] | |
const schemas = [] | |
messages.forEach(m => { | |
const name = messageName(m) | |
const header = | |
`"${name}": {\ | |
\n "type": "object",\ | |
\n "properties": {\ | |
\n "${m.kind}": { "enum": ["${name}"] },` | |
let fieldNames = m.fields.map(f => `"${f[0]}"`) | |
let schemaFields = generateSchemaFields(enums, m.fields) | |
const footer = | |
` },\ | |
\n "required": [ "${m.kind}", ${fieldNames.join(", ")} ]\ | |
\n}` | |
refs.push(`{ "$ref": "#/definitions/${capitalKind(m.kind)}/definitions/${name}" }`) | |
schemas.push(`${header}\n${schemaFields}\n${footer}`) | |
}) | |
return `${refs.join(",\n")}\n\n${schemas.join(",\n")}` | |
} | |
function generateSchemaFields(enums: Enum[], fields: [string, string][]): string { | |
const schemaFields = [] | |
fields.forEach(f => { | |
const enu = enums.filter(e => e.enum == f[1])[0] | |
if(enu != undefined) { | |
const items = enu.items | |
.map(i => `"${i}"`) | |
.join(", ") | |
schemaFields.push(` "${f[0]}": { "enum": [ ${items} ] }`) | |
} | |
else { | |
const ty = tsType(f[1]) | |
schemaFields.push(` "${f[0]}": { "type": "${ty}" }`) | |
} | |
}); | |
return schemaFields.join(",\n") | |
} | |
function generateAll(source: string) { | |
const parser = new SpecParser() | |
const spec = parser.parse(source) | |
const rustEnum = generateRustEnum(spec) | |
const tsInterfaces = generateTsInterfaces(spec) | |
const schemas = generateSchemas(spec) | |
console.log(rustEnum, "\n") | |
console.log(tsInterfaces, "\n") | |
console.log(schemas) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment