Compare commits

...

3 Commits

Author SHA1 Message Date
mathieuRA
8ed1a7655e handle null opaqueref for relation 2025-03-14 16:06:54 +01:00
mathieuRA
8cac7a7e6d only properties not published or published after dundee can be undefined. Also add deprecated class 2025-03-13 17:16:19 +01:00
Thierry
92f44d1d9d wip(ts):xen-api type generator 2025-02-05 16:19:27 +01:00
18 changed files with 547 additions and 0 deletions

View File

@@ -0,0 +1 @@
dist/

View File

@@ -0,0 +1,10 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}

View File

@@ -0,0 +1,19 @@
JSON file : https://github.com/xapi-project/xen-api/blob/master/doc/data/xenapi.json
```bash
yarn install
yarn build
```
## Command
```bash
node dist/generate.js
```
## Example
```bash
node dist/generate.js all --json /absolute/path/to/xenapi.json > generated.ts
```

View File

@@ -0,0 +1,30 @@
{
"name": "@xen-orchestra/xapi-generator",
"version": "0.1.0",
"description": "Generate typed xen-api lib from JSON file",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"license": "ISC",
"private": true,
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"scripts": {
"dev": "tsc --watch",
"build": "tsc",
"lint": "eslint . --ext .ts"
},
"devDependencies": {
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.28.1",
"typescript": "^5.2.2"
},
"dependencies": {
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"commander": "^11.0.0",
"eslint": "^8.48.0",
"yargs": "^17.7.2"
}
}

View File

@@ -0,0 +1,17 @@
import { Command } from 'commander'
import process from 'process'
import { allCommand } from './generators/all'
import { baseCommand } from './generators/base'
import { enumsCommand } from './generators/enums'
import { recordCommand } from './generators/record'
import { recordsCommand } from './generators/records'
const program = new Command('generate')
program.addCommand(allCommand)
program.addCommand(baseCommand)
program.addCommand(recordCommand)
program.addCommand(recordsCommand)
program.addCommand(enumsCommand)
program.parse(process.argv)

View File

@@ -0,0 +1,18 @@
import { Command } from 'commander'
import { loadJson } from '../utils/load-json'
import { generateBase } from './base'
import { generateEnums } from './enums'
import { generateRecords } from './records'
const allCommand = new Command('all')
.description('Generate all types and enums')
.option('-j, --json <path>', 'Path to the JSON file')
.action(async ({ json }) => {
const items = await loadJson(json)
console.log(generateBase())
console.log(generateEnums(items))
console.log(generateRecords(items))
})
export { allCommand }

View File

@@ -0,0 +1,19 @@
import { Command } from 'commander'
export const generateBase = () => {
const brand = 'declare const __brand: unique symbol'
const branded = 'export type Branded<TBrand extends string, TType = string> = TType & { [__brand]: TBrand }'
const opaqueRefEnum = "export const OPAQUE_REF = { EMPTY: 'OpaqueRef:NULL' } as const"
const opaqueRefType = "export type OPAQUE_REF_NULL = typeof OPAQUE_REF['EMPTY']"
return `${brand}\n\n${branded}\n\n${opaqueRefEnum}\n\n${opaqueRefType}\n`
}
const baseCommand = new Command('base').description('Generate base types').action(async () => {
console.log(generateBase())
})
export { baseCommand }

View File

@@ -0,0 +1,41 @@
import { Command } from 'commander'
import { XapiEnum, XapiItem } from '../types'
import { loadJson } from '../utils/load-json'
import { toEnumProperty } from '../utils/to-enum-property'
export const generateEnum = (e: XapiEnum) => {
const lines = new Set<string>()
e.values.forEach(value => {
lines.add(` ${toEnumProperty(value.name)}: "${value.name}"`)
})
return [
`export const ${toEnumProperty(e.name)} = {\n${Array.from(lines.values()).sort().join(',\n')}\n} as const`,
'',
`export type ${toEnumProperty(e.name)} = typeof ${toEnumProperty(e.name)}[keyof typeof ${toEnumProperty(e.name)}]`,
].join('\n')
}
export const generateEnums = (items: XapiItem[]) => {
const enums = new Set<string>()
items.forEach(item => {
item.enums.forEach(enumItem => {
enums.add(generateEnum(enumItem))
})
})
return Array.from(enums.values()).join('\n\n')
}
const enumsCommand = new Command('enums')
.description('Generate enums')
.option('-j, --json <path>', 'Path to the JSON file')
.action(async ({ json }) => {
const items = await loadJson(json)
console.log(generateEnums(items))
})
export { enumsCommand }

View File

@@ -0,0 +1,96 @@
import { Command } from 'commander'
import { XapiItem } from '../types'
import { loadJson } from '../utils/load-json'
import { toClassName } from '../utils/to-class-name'
import { toType } from '../utils/to-type'
const VERSION_BY_CODE_NAME: Record<string, number> = {
rio: 4.0,
miami: 4.1,
symc: 4.1,
orlando: 5.0,
'orlando-update-1': 5.1,
george: 5.5,
'midnight-ride': 5.6,
cowley: 5.6,
boston: 6.0,
tampa: 6.1,
clearwater: 6.2,
'vgpu-tech-preview': 6.2,
'vgpu-productisation': 6.2,
'clearwater-felton': 6.2,
creedence: 6.5,
cream: 6.5,
indigo: 6.5,
dundee: 7.0,
}
function sortProperties(p1: string, p2: string) {
return p1.replace('/** @deprecated */', '').trim().localeCompare(p2.replace('/** @deprecated */', '').trim())
}
export const generateRecord = (item: XapiItem) => {
const properties = item.fields.map(field => {
const isPublished = field.lifecycle.state === 'Published_s'
const isPublishedAfterDundee =
isPublished &&
field.lifecycle.transitions.some(
t => t.transition === 'published' && VERSION_BY_CODE_NAME[t.release] === undefined
)
const isDeprecated = field.lifecycle.transitions.some(t => t.transition === 'deprecated')
const isEmptyOpaqueRef =
field.default === 'OpaqueRef:NULL' || (field.default === 'Null' && field.type.endsWith('ref'))
function isAlwaysExisting() {
if (!isPublished || isPublishedAfterDundee) {
return false
}
return true
}
// const isAlwaysExisting =
// field.name === 'uuid' || field.type.endsWith(' set') || (isPublished && isFromRio && !isDeprecated && !isOptional)
const result = [` ${field.name}${isAlwaysExisting() ? '' : '?'}: ${toType(field.type)}`]
if (isEmptyOpaqueRef) {
result[0] = result[0].concat(' | OPAQUE_REF_NULL')
}
if (isDeprecated) {
result.unshift(' /** @deprecated */')
}
return result.join('\n')
})
properties.unshift(` $ref: Branded<'${item.name}'>`)
const isInterfaceDeprecated = item.lifecycle.transitions.some(t => t.transition === 'deprecated')
const interface_schema = [
`export interface ${toClassName(item.name)} {\n${properties.sort(sortProperties).join('\n')}\n}`,
]
if (isInterfaceDeprecated) {
interface_schema.unshift('/** @deprecated */')
}
return interface_schema.join('\n')
}
const recordCommand = new Command('record')
.argument('<type>', 'Type of the record (vm, host, ...)')
.description('Generate a record type')
.option('-j, --json <path>', 'Path to the JSON file')
.action(async (type, { json }) => {
const items = await loadJson(json)
const item = items.find(item => item.name.toLowerCase() === type.toLowerCase())
if (item === undefined) {
console.error(`Type ${type} not found`)
process.exit(1)
}
console.log(generateRecord(item))
})
export { recordCommand }

View File

@@ -0,0 +1,29 @@
import { Command } from 'commander'
import { toClassName } from '../utils/to-class-name'
import { XapiItem } from '../types'
import { loadJson } from '../utils/load-json'
import { generateRecord } from './record'
export const generateRecords = (items: XapiItem[]) => {
const unionTypes: string[] = []
const types = items
.map(item => {
unionTypes.push(toClassName(item.name))
return generateRecord(item)
})
.join('\n\n')
return `${types}\n\nexport type XenApiRecord = ${unionTypes.join(' | ')}`
}
const recordsCommand = new Command('records')
.description('Generate all records type')
.option('-j, --json <path>', 'Path to the JSON file')
.action(async ({ json }) => {
const items = await loadJson(json)
console.log(generateRecords(items))
})
export { recordsCommand }

View File

@@ -0,0 +1,63 @@
export type XapiEnumValue = {
name: string;
doc: string;
}
export type XapiEnum = {
name: string;
values: XapiEnumValue[];
}
export type XapiMessageError = {
name: string;
doc: string;
}
export type XapiMessageParam = {
type: string;
name: string;
doc: string;
}
export type XapiMessage= {
name: string;
description: string;
result: [type: string, description: string];
params: XapiMessageParam[]
errors: XapiMessageError[];
roles: string[];
tag: string;
lifecycle: XapiLifecycle;
implicit: boolean;
}
export type XapiTransition = {
transition: string;
release: string;
description: string;
}
export type XapiLifecycle = {
state: string;
transitions: XapiTransition[];
}
export type XapiField = {
name: string;
description: string;
type: string;
qualifier: string;
tag: string;
lifecycle: XapiLifecycle;
default?: string;
}
export type XapiItem = {
name: string;
description: string;
fields: XapiField[];
messages: XapiMessage[];
enums: XapiEnum[];
lifecycle: XapiLifecycle;
tag: string;
}

View File

@@ -0,0 +1,16 @@
import { existsSync } from 'fs'
import { XapiItem } from '../types'
export const loadJson = async (path: string): Promise<XapiItem[]> => {
if (!path) {
console.error(`JSON path is required`)
process.exit(1)
}
if (!existsSync(path)) {
console.error(`File ${path} does not exist`)
process.exit(1)
}
return (await import(path)).default;
}

View File

@@ -0,0 +1,5 @@
import { toClassName } from './to-class-name'
export function propertyLink(itemName: string, property: string) {
return `${toClassName(itemName)}["${property}"]`
}

View File

@@ -0,0 +1,5 @@
import { toPascalCase } from './to-pascal-case'
export function toClassName(name: string) {
return 'XenApi' + toPascalCase(name)
}

View File

@@ -0,0 +1,6 @@
export function toEnumProperty(name: string) {
return name
.replace('-', '_')
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.toUpperCase()
}

View File

@@ -0,0 +1,6 @@
export function toPascalCase(str: string) {
return str
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.toLowerCase().slice(1))
.join('')
}

View File

@@ -0,0 +1,54 @@
import { propertyLink } from './property-link'
import { toClassName } from './to-class-name'
import { toEnumProperty } from './to-enum-property'
import { XapiItem } from '../types'
export function toType(type: string, propertyName?: string, item?: XapiItem): string {
if (propertyName === 'uuid' && item !== undefined) {
return propertyLink(item.name, 'uuid')
}
switch (type) {
case 'an event batch':
return 'XapiEvent[]'
case 'void':
return 'void'
case 'string':
case 'datetime':
return 'string'
case 'bool':
return 'boolean'
case 'int':
case 'float':
return 'number'
case '<class> record':
return 'object'
}
if (type.endsWith(' option')) {
return 'unknown'
}
if (type.endsWith(' set')) {
return `${toType(type.replace(' set', ''), propertyName, item)}[]`
}
if (type.endsWith(' ref')) {
return propertyLink(type.replace(' ref', ''), '$ref')
}
if (type.startsWith('enum ')) {
return toEnumProperty(type.replace('enum ', ''))
}
if (type.endsWith(' map')) {
const [key, ...value] = type.replace(/ map$/, '').slice(1, -1).split(' -> ')
return `Record<${toType(key, propertyName, item)}, ${toType(value.join(' -> '), propertyName, item)}>`
}
if (type.endsWith(' record')) {
return toClassName(type.replace(' record', ''))
}
return toClassName(type)
}

View File

@@ -0,0 +1,112 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "CommonJS", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/**/*"
]
}