diff --git a/.github/workflows/types-codegen-pr.yaml b/.github/workflows/types-codegen-pr.yaml new file mode 100644 index 00000000..ec4cdf36 --- /dev/null +++ b/.github/workflows/types-codegen-pr.yaml @@ -0,0 +1,67 @@ +name: Chat Types PR Validation + +on: + pull_request: + branches: + - main + paths: + - 'types/codegen/schema/**' + +jobs: + validate-pr: + name: Validate PR with chatTypes.json changes + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install root dependencies + working-directory: . + run: npm install + + - name: Install codegen dependencies + working-directory: types/codegen + run: npm install + + - name: Coalesce schemas + working-directory: types/codegen + run: npm run generate-full-schema + + - name: Generate types + working-directory: types/codegen + run: npm run generate:only + + - name: Post Process + working-directory: types/codegen + run: npm run post-process + + - name: Completeness Test + working-directory: types/codegen + run: npm run test + + - name: Unit tests + working-directory: types/codegen + run: npm run test:unit + + - name: Build TypeScript + working-directory: types/codegen/generated/typescript + run: | + npm i + npm run build + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + cache: 'maven' + + - name: Build Java with Maven + working-directory: types/codegen/generated/java + run: | + mvn clean compile -Djava.version=21 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 78e2d7d2..f2dedb28 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { "chat-client-ui-types": "0.1.62", - "runtimes": "0.2.125", + "runtimes": "0.2.126", "types": "0.1.56" } diff --git a/package-lock.json b/package-lock.json index 3d6e7f65..fb9f2ef2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4684,7 +4684,7 @@ }, "runtimes": { "name": "@aws/language-server-runtimes", - "version": "0.2.125", + "version": "0.2.126", "license": "Apache-2.0", "dependencies": { "@aws/language-server-runtimes-types": "^0.1.56", diff --git a/package.json b/package.json index fd98c3fb..7de37c0b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ ], "workspaces": [ "types", + "types/codegen/generated/typescript", "runtimes", "chat-client-ui-types" ], @@ -36,7 +37,8 @@ "test": "npm run test --workspaces --if-present", "preversion": "npm run test", "version": "npm run compile && git add -A .", - "watch": "tsc --build --watch" + "watch": "tsc --build --watch", + "gen-comp": "cd ./types/codegen/ && npm run generate && cd ./generated/typescript && npm run build && cd ../../../ && npm run compile" }, "devDependencies": { "@commitlint/cli": "^19.8.1", diff --git a/runtimes/CHANGELOG.md b/runtimes/CHANGELOG.md index 1b8d07f9..c1758243 100644 --- a/runtimes/CHANGELOG.md +++ b/runtimes/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.126](https://github.com/aws/language-server-runtimes/compare/language-server-runtimes/v0.2.125...language-server-runtimes/v0.2.126) (2025-08-18) + + +### Bug Fixes + +* skip PAC URLs in macOS proxy detection ([#664](https://github.com/aws/language-server-runtimes/issues/664)) ([92fb02d](https://github.com/aws/language-server-runtimes/commit/92fb02d1d4c6aedb7b59b042b8d10e54bb750d92)) + ## [0.2.125](https://github.com/aws/language-server-runtimes/compare/language-server-runtimes/v0.2.124...language-server-runtimes/v0.2.125) (2025-08-12) diff --git a/runtimes/package.json b/runtimes/package.json index 9858afe5..9297202c 100644 --- a/runtimes/package.json +++ b/runtimes/package.json @@ -1,6 +1,6 @@ { "name": "@aws/language-server-runtimes", - "version": "0.2.125", + "version": "0.2.126", "description": "Runtimes to host Language Servers for AWS", "repository": { "type": "git", diff --git a/runtimes/runtimes/standalone.ts b/runtimes/runtimes/standalone.ts index 0c6c8c6a..54e8bb9f 100644 --- a/runtimes/runtimes/standalone.ts +++ b/runtimes/runtimes/standalone.ts @@ -200,7 +200,11 @@ export const standalone = (props: RuntimeProps) => { }, error => { console.error(error) - process.exit(10) + // arbitrary 5 second timeout to ensure console.error flushes before process exit + // note: webpacked version may output exclusively to stdout, not stderr. + setTimeout(() => { + process.exit(10) + }, 5000) } ) .catch((error: Error) => { diff --git a/runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.ts b/runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.ts index cc8c90c3..27462ae6 100644 --- a/runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.ts +++ b/runtimes/runtimes/util/standalone/getProxySettings/getMacProxySettings.ts @@ -35,8 +35,10 @@ export function getMacSystemProxy(): ProxyConfig | undefined { // Honor PAC URL first if configured if (settings.ProxyAutoConfigEnable === '1' && settings.ProxyAutoConfigURLString) { - console.debug(`Using PAC URL: ${settings.ProxyAutoConfigURLString}`) - return { proxyUrl: settings.ProxyAutoConfigURLString, noProxy } + console.debug(`PAC URL detected: ${settings.ProxyAutoConfigURLString}`) + // TODO: Parse PAC file to get actual proxy + // For now, skip PAC and fall through to manual proxy settings + console.warn('PAC file support not yet implemented, falling back to manual proxy settings') } // Otherwise pick the first enabled protocol diff --git a/runtimes/runtimes/util/telemetryLspServer.ts b/runtimes/runtimes/util/telemetryLspServer.ts index a741d5b8..2b4f4c17 100644 --- a/runtimes/runtimes/util/telemetryLspServer.ts +++ b/runtimes/runtimes/util/telemetryLspServer.ts @@ -47,23 +47,30 @@ export function getTelemetryLspServer( const optOut = params.initializationOptions?.telemetryOptOut ?? true // telemetry disabled if option not provided const endpoint = runtime.getConfiguration('TELEMETRY_GATEWAY_ENDPOINT') ?? DEFAULT_TELEMETRY_ENDPOINT - logging.debug(`Configuring Runtimes OperationalTelemetry with endpoint: ${endpoint}`) + // Initialize telemetry asynchronously without blocking + setImmediate(() => { + try { + logging.debug(`Configuring Runtimes OperationalTelemetry with endpoint: ${endpoint}`) - const optel = OperationalTelemetryService.getInstance({ - serviceName: props.name, - serviceVersion: props.version, - extendedClientInfo: params.initializationOptions?.aws?.clientInfo, - logging: logging, - endpoint: endpoint, - telemetryOptOut: optOut, - }) + const optel = OperationalTelemetryService.getInstance({ + serviceName: props.name, + serviceVersion: props.version, + extendedClientInfo: params.initializationOptions?.aws?.clientInfo, + logging: logging, + endpoint: endpoint, + telemetryOptOut: optOut, + }) - OperationalTelemetryProvider.setTelemetryInstance(optel) + OperationalTelemetryProvider.setTelemetryInstance(optel) - logging.info(`Initialized Runtimes OperationalTelemetry with optOut=${optOut}`) + logging.info(`Initialized Runtimes OperationalTelemetry with optOut=${optOut}`) - setServerCrashTelemetryListeners() - setMemoryUsageTelemetry() + setServerCrashTelemetryListeners() + setMemoryUsageTelemetry() + } catch (error) { + logging.warn(`Failed to initialize telemetry: ${error}`) + } + }) return { capabilities: {}, diff --git a/types/.gitignore b/types/.gitignore index 53216207..df9e410c 100644 --- a/types/.gitignore +++ b/types/.gitignore @@ -1,3 +1,6 @@ LICENSE NOTICE -SECURITY.md \ No newline at end of file +SECURITY.md +codegen/scripts/**/* +codegen/generated/**/* +codegen/tests/fixtures/**/* \ No newline at end of file diff --git a/types/codegen/.gitignore b/types/codegen/.gitignore new file mode 100644 index 00000000..2abef54d --- /dev/null +++ b/types/codegen/.gitignore @@ -0,0 +1,16 @@ +# Generated code +generated/ + +# Template bank +typescript-template-bank/ + +# OpenAPI generator metadata +.openapi-generator/ + +# Node.js dependencies +node_modules/ + +# Build outputs +dist/ + +schema/complete-schema.json \ No newline at end of file diff --git a/types/codegen/README.md b/types/codegen/README.md new file mode 100644 index 00000000..4e4af8a9 --- /dev/null +++ b/types/codegen/README.md @@ -0,0 +1,238 @@ +# OpenAPI Code Generation + +This directory contains scripts and configuration for generating TypeScript and Java types from OpenAPI schema definitions using OpenAPI Generator CLI. + +## Overview + +The code generation process combines multiple schema files into a complete OpenAPI specification and generates both TypeScript and Java types with custom templates, import mappings, and validation. + +**Running `npm run generate` will automatically generate:** +- **TypeScript types** in `generated/typescript/` - Complete type definitions +- **Java model classes** in `generated/java/` - Java model classes with custom templates + +## Directory Structure + +``` +types/codegen/ +├── schema/ # Schema definition files +│ ├── chatTypes.json # Main schema definitions +│ └── complete-schema.json # Generated complete OpenAPI spec +├── scripts/ # Generation and validation scripts +│ ├── generate-complete-schema.js # Combines all schema files +│ ├── post-typescript.js # TypeScript post-processing +│ ├── post-test.js # Model validation script +│ ├── clean.js # Cleanup utilities +│ └── constants.ts # Constants to inject +├── custom-templates/ # Custom Mustache templates +│ ├── typescript/ # TypeScript-specific templates +│ └── java/ # Java-specific templates +├── generated/ # Generated output files +│ ├── typescript/ # TypeScript types +│ └── java/ # Java model classes +├── tests/ # Test schemas and validation +└── openapitools.json # Generator configuration +``` + +## Process Flow + +### 1. Schema Generation (`generate-complete-schema.js`) +- Scans all JSON files in the `schema/` directory +- Combines schemas from multiple files into a single collection +- Creates `complete-schema.json` with complete OpenAPI 3.0.0 structure: + - Dynamic version from `openapitools.json` TypeScript generator config + - Combined schemas from all source files + - Proper OpenAPI structure and formatting + - Conflict detection and warnings for duplicate schema names + +### 2. Code Generation (OpenAPI Generator CLI) +- **TypeScript Generator**: Generates complete TypeScript types + - Uses `typescript-fetch` generator with custom templates + - Generates interfaces, types, and enum unions +- **Java Generator**: Generates Java model classes + - Uses custom templates for modern Java features + - Targets Java 21 with modern features + - Supports model filtering via `global-property.models` when needed + +### 3. Post-Processing (`post-typescript.js`) +- Processes import mappings from `openapitools.json` +- Adds external library imports (e.g., `vscode-languageserver-types`) +- Injects constants from `constants.ts` +- Modifies interface visibility (e.g., makes `PartialResultParams` internal) + +### 4. Validation (`post-test.js`) +- Validates generated models against schema definitions +- Checks for missing models (should be generated but aren't) +- Reports extra/intermediate models (generated but not in schema) +- Supports verbose mode for detailed analysis +- Respects `global-property.models` filters for accurate validation + +## Available Scripts + +```bash +# Generate complete schema from individual files +npm run generate-schema + +# Full generation pipeline (schema + generation + post-processing + validation) +npm run generate + +# Generation without post-processing +npm run generate:no-post + +# Validate generated models (errors only) +npm run test +npm run validate + +# Validate with detailed output (includes extra/intermediate models) +npm run test:verbose +``` + +## Configuration + +### Generator Configuration (`openapitools.json`) + +The main configuration file contains detailed settings for both TypeScript and Java generators: + +#### TypeScript Generator Settings +- **Generator**: `typescript-fetch` - Generates TypeScript types +- **ES6 Support**: `supportsES6: true` - Modern JavaScript features +- **Property Naming**: `camelCase` for both models and enums +- **String Enums**: `stringEnums: true` - Generates union types instead of numeric enums +- **Runtime Checks**: `withoutRuntimeChecks: true` - Lighter generated code +- **Additional Properties**: `nullSafeAdditionalProps: false` - Flexible object handling +- **OpenAPI Normalizer**: `REF_AS_PARENT_IN_ALLOF: true` - Better inheritance handling + +#### Java Generator Settings +- **Generator**: `java` - Standard Java model classes +- **Java Version**: Source and target compatibility set to Java 21 +- **OneOf Interfaces**: `useOneOfInterfaces: true` - Union type handling using interfaces +- **Date Library**: `java8` - Uses modern Java time APIs +- **Validation**: Bean validation disabled for lighter models +- **Legacy Behavior**: `legacyDiscriminatorBehavior: false` - Modern discriminator handling + +### Model Filtering (Optional) + +Java generator supports selective model generation via `global-property.models` when needed: +```json +"global-property": { + "models": "IconType:ContextCommandGroup:QuickActionCommand:ContextCommand" +} +``` + +When specified, this generates only the listed models. If omitted, all models are generated. + +### Import Mappings + +Both generators support external type imports, but handle them differently: + +#### Java Import Mappings +```json +"importMappings": { + "Position": "org.eclipse.lsp4j.Position", + "Range": "org.eclipse.lsp4j.Range", + "TextDocumentIdentifier": "org.eclipse.lsp4j.TextDocumentIdentifier" +} +``` + +#### TypeScript Import Mappings +Handled by `post-typescript.js` for more flexible processing: +```json +"importMappings": { + "Position": "vscode-languageserver-types", + "Range": "vscode-languageserver-types", + "TextDocumentIdentifier": "vscode-languageserver-types" +} +``` + +### Type Mappings + +Custom type mappings for language-specific types: + +#### Java Type Mappings +```json +"typeMappings": { + "IconType": "String", + "Uint8Array": "byte[]" +} +``` + +#### TypeScript Type Mappings +```json +"typeMappings": { + "Date": "Date" +} +``` + +### Reserved Words + +Handle language-specific reserved words: +```json +"reservedWordsMappings": { + "export": "export" +} +``` + +## Output + +### TypeScript Output (`generated/typescript/`) +- Complete type definitions for all schema models +- Proper import statements for external dependencies +- Injected constants and utilities +- NPM package ready for distribution + +### Java Output (`generated/java/`) +- Java model classes with custom templates +- Java 21 compatible code +- LSP4J integration for language server types +- Maven/Gradle compatible structure + +## Validation Results + +The validation script provides detailed reporting: +- **Schema models**: Total models defined in schema files +- **TypeScript models**: Generated models (includes intermediate models) +- **Java models**: Generated models (filtered subset based on configuration) +- **Missing models**: Models that should be generated but aren't +- **Extra models**: Intermediate models created by OpenAPI generator + +## Testing + +The `tests/` directory contains comprehensive tests for the OpenAPI code generation pipeline. These tests validate that your `openapitools.json` configuration and custom templates work correctly. + +### Running Tests + +```bash +# Run all tests +npm run test:unit + +# Inspect generated files for debugging +KEEP_TEST_OUTPUT=1 npm run test:unit + +# Run specific test suites +npm run test:unit -- --testPathPattern="field-addition" +npm run test:unit -- --testPathPattern="openapi-normalizer" +``` + +### Test Coverage + +- **Field Addition Tests (8 tests)**: Validates TypeScript interfaces and Java records for optional/required fields +- **Configuration Tests**: Ensures camelCase naming, ES6 exports, Java 21 compatibility, and custom templates +- **OpenAPI Normalizer Tests (2 tests)**: Tests inheritance vs property flattening based on configuration + +The tests use your actual `openapitools.json` configuration (not mocked) to ensure production validation and regression protection. + +**Key Features:** +- Real configuration testing using your actual `openapitools.json` +- Custom template validation (Java records vs classes) +- Language-specific behavior testing (TypeScript inheritance, Java flattening) +- Configuration-aware tests that adapt to your settings + +See [tests/README.md](tests/README.md) for detailed test documentation. + +## Development Workflow + +1. **Add/modify schemas** in `schema/chatTypes.json` +2. **Run generation**: `npm run generate` +3. **Review validation**: Check for missing or extra models +4. **Test integration**: Use generated types in your application +5. **Run tests**: `npm run test:unit` to validate configuration and templates +6. **Commit changes**: Only commit schema files, not generated files \ No newline at end of file diff --git a/types/codegen/custom-templates/java/model.mustache b/types/codegen/custom-templates/java/model.mustache new file mode 100644 index 00000000..a9eaedaa --- /dev/null +++ b/types/codegen/custom-templates/java/model.mustache @@ -0,0 +1,17 @@ +{{>licenseInfo}} + +package {{package}}; + +// Selective imports for records +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import javax.annotation.Nonnull; +{{#imports}} +import {{import}}; +{{/imports}} + +{{#models}} +{{#model}} +{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>record}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/types/codegen/custom-templates/java/pom.mustache b/types/codegen/custom-templates/java/pom.mustache new file mode 100644 index 00000000..895674a0 --- /dev/null +++ b/types/codegen/custom-templates/java/pom.mustache @@ -0,0 +1,299 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{artifactUrl}} + {{artifactDescription}} + + {{scmConnection}} + {{scmDeveloperConnection}} + {{scmUrl}} + +{{#parentOverridden}} + + {{{parentGroupId}}} + {{{parentArtifactId}}} + {{{parentVersion}}} + +{{/parentOverridden}} + + + + {{licenseName}} + {{licenseUrl}} + repo + + + + + + {{developerName}} + {{developerEmail}} + {{developerOrganization}} + {{developerOrganizationUrl}} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + {{#sourceCompatibility}}{{sourceCompatibility}}{{/sourceCompatibility}}{{^sourceCompatibility}}21{{/sourceCompatibility}} + {{#targetCompatibility}}{{targetCompatibility}}{{/targetCompatibility}}{{^targetCompatibility}}21{{/targetCompatibility}} + true + 128m + 512m + + -Xlint:all + -J-Xss4m + + + + + default-testCompile + test-compile + + testCompile + + + true + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + none + {{#sourceCompatibility}}{{sourceCompatibility}}{{/sourceCompatibility}}{{^sourceCompatibility}}21{{/sourceCompatibility}} + + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + + + + + + + + sign-artifacts + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + + + + {{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations-version} + + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations-version} + + {{/swagger2AnnotationLibrary}} + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + + org.eclipse.lsp4j + org.eclipse.lsp4j + 0.22.0 + + + + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson-version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind-version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-version} + + + + + com.google.code.gson + gson + ${gson-version} + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + ${beanvalidation-version} + provided + + {{/useBeanValidation}} + {{#performBeanValidation}} + + + org.hibernate + hibernate-validator + 5.4.1.Final + + {{/performBeanValidation}} + {{#parcelableModel}} + + + com.google.android + android + 4.1.1.4 + provided + + {{/parcelableModel}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation-version} + provided + + {{#useReflectionEqualsHashCode}} + + + org.apache.commons + commons-lang3 + ${commons-lang3-version} + + {{/useReflectionEqualsHashCode}} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-version} + test + + + org.junit.platform + junit-platform-runner + ${junit-platform-runner.version} + test + + + + UTF-8 + {{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + 2.2.15 + {{/swagger2AnnotationLibrary}} + 1.19.4 + 2.17.1 + 2.17.1 + {{#useJakartaEe}} + 2.1.1 + 3.0.2 + {{/useJakartaEe}} + {{^useJakartaEe}} + 1.3.5 + 2.0.2 + {{/useJakartaEe}} + {{#useReflectionEqualsHashCode}} + 3.17.0 + {{/useReflectionEqualsHashCode}} + 2.10.1 + 1.0.0 + 5.10.2 + 1.10.0 + + diff --git a/types/codegen/custom-templates/java/record.mustache b/types/codegen/custom-templates/java/record.mustache new file mode 100644 index 00000000..c63206cc --- /dev/null +++ b/types/codegen/custom-templates/java/record.mustache @@ -0,0 +1,16 @@ +/** + * {{description}}{{^description}}{{classname}}{{/description}} + */ +{{>generatedAnnotation}} +public record {{classname}}( +{{#vars}} +{{#description}} // {{description}} +{{/description}} @JsonProperty("{{baseName}}") + @SerializedName("{{baseName}}") + {{#required}}@Nonnull{{/required}}{{^required}}@Nullable{{/required}} {{{datatypeWithEnum}}} {{name}}{{^-last}},{{/-last}}{{^-last}} + +{{/-last}} +{{/vars}} +) {{#parent}}implements {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ + +} \ No newline at end of file diff --git a/types/codegen/custom-templates/typescript/index.mustache b/types/codegen/custom-templates/typescript/index.mustache new file mode 100644 index 00000000..f7688c7d --- /dev/null +++ b/types/codegen/custom-templates/typescript/index.mustache @@ -0,0 +1,6 @@ +{{!This is a comment. Removed the export of runtimes because we do not need +those functions. Was causing error because exported a def called ResponseError +which is also used by vscode LSP}} +{{#models.0}} +export * from './models/index{{importFileExtension}}'; +{{/models.0}} diff --git a/types/codegen/custom-templates/typescript/modelEnumInterfaces.mustache b/types/codegen/custom-templates/typescript/modelEnumInterfaces.mustache new file mode 100644 index 00000000..5c508270 --- /dev/null +++ b/types/codegen/custom-templates/typescript/modelEnumInterfaces.mustache @@ -0,0 +1,26 @@ +{{#stringEnums}} +/** + * {{#lambda.indented_star_1}}{{{unescapedDescription}}}{{/lambda.indented_star_1}} + * @export + * @type {string} + */ +export type {{classname}} = {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}}; +{{/stringEnums}}{{^stringEnums}} +/** + * {{#lambda.indented_star_1}}{{{unescapedDescription}}}{{/lambda.indented_star_1}} + * @export + */ +export const {{classname}} = { +{{#allowableValues}} +{{#enumVars}} + {{#enumDescription}} + /** + * {{enumDescription}} + */ + {{/enumDescription}} + {{{name}}}: {{{value}}}{{^-last}},{{/-last}} +{{/enumVars}} +{{/allowableValues}} +} as const; +export type {{classname}} = typeof {{classname}}[keyof typeof {{classname}}]; +{{/stringEnums}} \ No newline at end of file diff --git a/types/codegen/custom-templates/typescript/modelGenericInterfaces.mustache b/types/codegen/custom-templates/typescript/modelGenericInterfaces.mustache new file mode 100644 index 00000000..e96ea996 --- /dev/null +++ b/types/codegen/custom-templates/typescript/modelGenericInterfaces.mustache @@ -0,0 +1,46 @@ +/** + * {{#lambda.indented_star_1}}{{{unescapedDescription}}}{{/lambda.indented_star_1}} + * @export + * @interface {{classname}} + */ +export interface {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ +{{#additionalPropertiesType}} + [key: string]: {{{additionalPropertiesType}}}{{#hasVars}} | any{{/hasVars}}; +{{/additionalPropertiesType}} +{{#vars}} + /** + * {{#lambda.indented_star_4}}{{{unescapedDescription}}}{{/lambda.indented_star_4}} + * @type {{=<% %>=}}{<%&datatype%>}<%={{ }}=%> + * @memberof {{classname}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ + {{#isReadOnly}}readonly {{/isReadOnly}}{{name}}{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{#isNullable}} | null{{/isNullable}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{#isNullable}} | null{{/isNullable}}{{/isEnum}}; +{{/vars}} +}{{#hasEnums}} + +{{#vars}} +{{!Changed to make actual string enum}} +{{#isEnum}} +{{#stringEnums}} +/** +* @export +* @type {string} +*/ +export type {{classname}}{{enumName}} = {{#allowableValues}} {{#enumVars}}{{{value}}}{{^-last}} | {{/-last}}{{/enumVars}}{{/allowableValues}}; + +{{/stringEnums}}{{^stringEnums}} +/** + * @export + */ +export const {{classname}}{{enumName}} = { +{{#allowableValues}} + {{#enumVars}} + {{{name}}}: {{{value}}}{{^-last}},{{/-last}} + {{/enumVars}} +{{/allowableValues}} +} as const; +export type {{classname}}{{enumName}} = typeof {{classname}}{{enumName}}[keyof typeof {{classname}}{{enumName}}]; +{{/stringEnums}} +{{/isEnum}}{{/vars}}{{/hasEnums}} \ No newline at end of file diff --git a/types/codegen/generated/java/.openapi-generator-ignore b/types/codegen/generated/java/.openapi-generator-ignore new file mode 100644 index 00000000..d42bd7ed --- /dev/null +++ b/types/codegen/generated/java/.openapi-generator-ignore @@ -0,0 +1,11 @@ +src/test/** +docs/** +api/** +README.md +.travis.yml +.github/** +gradle/** +gradlew* +build.gradle +git_push.sh +src/main/java/org/openapitools/client/auth/** \ No newline at end of file diff --git a/types/codegen/generated/typescript/.openapi-generator-ignore b/types/codegen/generated/typescript/.openapi-generator-ignore new file mode 100644 index 00000000..6f3d39e4 --- /dev/null +++ b/types/codegen/generated/typescript/.openapi-generator-ignore @@ -0,0 +1,2 @@ +# Ignore runtimes.ts file +src/runtime.ts diff --git a/types/codegen/openapitools.json b/types/codegen/openapitools.json new file mode 100644 index 00000000..61cacb3d --- /dev/null +++ b/types/codegen/openapitools.json @@ -0,0 +1,73 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.14.0", + "generators": { + "typescript": { + "generatorName": "typescript-fetch", + "disabled": false, + "output": "#{cwd}/generated/typescript", + "inputSpec": "#{cwd}/schema/complete-schema.json", + "templateDir": "#{cwd}/custom-templates/typescript", + "additionalProperties": { + "supportsES6": true, + "modelPropertyNaming": "camelCase", + "enumPropertyNaming": "camelCase", + "npmName": "@local/language-server-runtimes-generated-types", + "npmVersion": "0.0.1", + "nullSafeAdditionalProps": false, + "withoutRuntimeChecks": true, + "stringEnums": true, + "disallowAdditionalPropertiesIfNotPresent": false + }, + "global-property": {}, + "openapi-normalizer": { + "REF_AS_PARENT_IN_ALLOF": true + }, + "reservedWordsMappings": { + "export": "export" + }, + "typeMappings": { + "Date": "Date" + }, + "importMappings": { + "Position": "vscode-languageserver-types", + "Range": "vscode-languageserver-types", + "TextDocumentIdentifier": "vscode-languageserver-types" + } + }, + "java": { + "generatorName": "java", + "disabled": false, + "output": "#{cwd}/generated/java", + "inputSpec": "#{cwd}/schema/complete-schema.json", + "templateDir": "#{cwd}/custom-templates/java", + "additionalProperties": { + "useOneOfInterfaces": true, + "legacyDiscriminatorBehavior": false, + "java8": true, + "dateLibrary": "java8", + "serializableModel": false, + "useBeanValidation": false, + "performBeanValidation": false, + "sourceCompatibility": "21", + "targetCompatibility": "21" + }, + "global-property": { + "models": "ContextCommandParams:ContextCommandIconType:IconType:ContextCommandGroup:QuickActionCommand:ContextCommand:CursorPosition:FileParams:CopyFileParams:OpenFileDiffParams:ShowOpenDialogParams:ShowSaveFileDialogParams:ShowSaveFileDialogResult", + "supportingFiles": "pom.xml" + }, + "importMappings": { + "Position": "org.eclipse.lsp4j.Position", + "Range": "org.eclipse.lsp4j.Range", + "TextDocumentIdentifier": "org.eclipse.lsp4j.TextDocumentIdentifier" + }, + "typeMappings": { + "IconType": "String", + "Uint8Array": "byte[]" + } + } + } + } +} diff --git a/types/codegen/package.json b/types/codegen/package.json new file mode 100644 index 00000000..702f14f6 --- /dev/null +++ b/types/codegen/package.json @@ -0,0 +1,26 @@ +{ + "name": "type-generator", + "version": "0.0.1", + "description": "Generate TypeScript defintiinons from JSON Schema using OpenAPI Generator", + "main": "generated/typescript/src/index.ts", + "scripts": { + "clean": "node scripts/clean.js", + "process-schemas": "node scripts/generate-complete-schema.js", + "generate": "npm run process-schemas && openapi-generator-cli generate && npm run post-process && npm run test", + "generate:only": "openapi-generator-cli generate", + "generate:no-post": "npm run process-schemas && openapi-generator-cli generate && npm run post-process", + "post-process": "node scripts/post-typescript.js", + "test": "node scripts/post-test.js", + "test:verbose": "node scripts/post-test.js --verbose", + "test:unit": "ts-mocha -b 'tests/unit/**/*.test.js' --bail false", + "test:all": "npm run test && npm run test:unit" + }, + "devDependencies": { + "@openapitools/openapi-generator-cli": "^2.21.4", + "@types/mocha": "^10.0.9", + "@types/node": "^24.0.4", + "ts-mocha": "^11.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/types/codegen/schema/chatTypes.json b/types/codegen/schema/chatTypes.json new file mode 100644 index 00000000..c83cb743 --- /dev/null +++ b/types/codegen/schema/chatTypes.json @@ -0,0 +1,2036 @@ +{ + "ChatItemAction": { + "type": "object", + "required": ["pillText"], + "properties": { + "pillText": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "SourceLink": { + "type": "object", + "required": ["title", "url"], + "properties": { + "title": { + "type": "string", + "description": "The title of the source link" + }, + "url": { + "type": "string", + "description": "The URL of the source link" + }, + "body": { + "type": "string", + "description": "Optional body text for the source link" + } + } + }, + "RecommendationContentSpan": { + "type": "object", + "required": ["start", "end"], + "properties": { + "start": { + "type": "number", + "description": "Start position of the content span" + }, + "end": { + "type": "number", + "description": "End position of the content span" + } + }, + "description": "Content span information" + }, + "ReferenceTrackerInformation": { + "type": "object", + "required": ["information"], + "properties": { + "licenseName": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "url": { + "type": "string" + }, + "recommendationContentSpan": { + "$ref": "#/components/schemas/RecommendationContentSpan" + }, + "information": { + "type": "string" + } + } + }, + "ChatPrompt": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Optional prompt text" + }, + "escapedPrompt": { + "type": "string", + "description": "Optional escaped prompt text" + }, + "command": { + "type": "string", + "description": "Optional command" + } + } + }, + "FeedbackPayload": { + "type": "object", + "required": ["messageId", "tabId", "selectedOption"], + "properties": { + "messageId": { + "type": "string", + "description": "ID of the message" + }, + "tabId": { + "type": "string", + "description": "ID of the tab" + }, + "selectedOption": { + "type": "string", + "description": "Selected feedback option" + }, + "comment": { + "type": "string", + "description": "Optional comment" + } + } + }, + "CodeSelectionType": { + "type": "string", + "enum": ["selection", "block"], + "description": "Type of code selection" + }, + "CursorPosition": { + "type": "object", + "required": ["position"], + "properties": { + "position": { + "type": "Position" + } + } + }, + "CursorRange": { + "type": "object", + "required": ["range"], + "properties": { + "range": { + "type": "Range" + } + } + }, + "CursorState": { + "oneOf": [ + { + "$ref": "#/components/schemas/CursorPosition" + }, + { + "$ref": "#/components/schemas/CursorRange" + } + ] + }, + "PartialResultToken": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "PartialResultParams": { + "type": "object", + "properties": { + "partialResultToken": { + "$ref": "#/components/schemas/PartialResultToken" + } + } + }, + "ChatParams": { + "allOf": [ + { + "$ref": "#/components/schemas/PartialResultParams" + }, + { + "type": "object", + "required": ["tabId", "prompt"], + "properties": { + "tabId": { + "type": "string" + }, + "prompt": { + "$ref": "#/components/schemas/ChatPrompt" + }, + "cursorState": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CursorState" + } + }, + "textDocument": { + "type": "TextDocumentIdentifier" + }, + "context": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuickActionCommand" + }, + "description": "Context of the current chat message to be handled by the servers. Context can be added through QuickActionCommand triggered by `@`." + } + } + } + ] + }, + "InlineChatParams": { + "allOf": [ + { + "$ref": "#/components/schemas/PartialResultParams" + }, + { + "type": "object", + "required": ["prompt"], + "properties": { + "prompt": { + "$ref": "#/components/schemas/ChatPrompt" + }, + "cursorState": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CursorState" + } + }, + "textDocument": { + "type": "TextDocumentIdentifier" + } + } + } + ] + }, + "EncryptedChatParams": { + "allOf": [ + { + "$ref": "#/components/schemas/PartialResultParams" + }, + { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } + } + ] + }, + "FileDetails": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "fullPath": { + "type": "string" + }, + "lineRanges": { + "type": "array", + "items": { + "type": "object", + "required": ["first", "second"], + "properties": { + "first": { + "type": "number" + }, + "second": { + "type": "number" + } + } + } + }, + "changes": { + "type": "object", + "properties": { + "added": { + "type": "number" + }, + "deleted": { + "type": "number" + }, + "total": { + "type": "number" + } + } + }, + "visibleName": { + "type": "string" + }, + "clickable": { + "type": "boolean" + } + } + }, + "FileList": { + "type": "object", + "properties": { + "rootFolderTitle": { + "type": "string" + }, + "filePaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "deletedFiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "details": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FileDetails" + } + } + } + }, + "Status": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "ContextCommandIconType": { + "type": "string", + "enum": ["file", "folder", "code-block", "list-add", "magic"] + }, + "IconType": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContextCommandIconType" + }, + { + "type": "string" + } + ], + "description": "Can be ContextCommandIconType or other values like 'help', 'trash', 'search', 'calendar', or any string" + }, + "ButtonPartialStatus": { + "type": "string", + "enum": ["main", "primary", "clear"] + }, + "ButtonStatus": { + "oneOf": [ + { + "$ref": "#/components/schemas/ButtonPartialStatus" + }, + { + "$ref": "#/components/schemas/Status" + } + ] + }, + "Button": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "disabled": { + "type": "boolean" + }, + "keepCardAfterClick": { + "type": "boolean" + }, + "status": { + "$ref": "#/components/schemas/ButtonStatus" + } + } + }, + "ChatMessage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["answer", "prompt", "system-prompt", "directive", "tool"], + "description": "will default to 'answer'" + }, + "header": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["answer", "prompt", "system-prompt", "directive", "tool"] + }, + "buttons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Button" + } + }, + "body": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "canBeVoted": { + "type": "boolean" + }, + "relatedContent": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SourceLink" + } + } + }, + "required": ["content"] + }, + "summary": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/ChatMessage" + }, + "collapsedContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + } + } + } + }, + "followUp": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatItemAction" + } + } + } + }, + "codeReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReferenceTrackerInformation" + } + }, + "fileList": { + "$ref": "#/components/schemas/FileList" + }, + "contextList": { + "$ref": "#/components/schemas/FileList" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "status": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/Status" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "text": { + "type": "string" + } + } + } + } + }, + "buttons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Button" + } + }, + "body": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "canBeVoted": { + "type": "boolean" + }, + "relatedContent": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SourceLink" + } + } + }, + "required": ["content"] + }, + "summary": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/ChatMessage" + }, + "collapsedContent": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + } + } + } + }, + "followUp": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatItemAction" + } + } + } + }, + "codeReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReferenceTrackerInformation" + } + }, + "fileList": { + "$ref": "#/components/schemas/FileList" + }, + "contextList": { + "$ref": "#/components/schemas/FileList" + }, + "editable": { + "type": "boolean" + }, + "quickSettings": { + "type": "object", + "required": ["type", "messageId", "tabId", "options"], + "properties": { + "type": { + "type": "string", + "enum": ["select", "checkbox", "radio"] + }, + "description": { + "type": "string" + }, + "descriptionLink": { + "type": "object", + "required": ["id", "text", "destination"], + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "destination": { + "type": "string" + } + } + }, + "messageId": { + "type": "string" + }, + "tabId": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "label", "value"], + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "selected": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "ChatResult": { + "description": "Represents the result of a chat interaction. A ChatResult extends ChatMessage and can optionally include additional messages that provide context, reasoning, or intermediate steps that led to the final response. Response for chat prompt request can be empty, if server chooses to handle the request and push updates asynchronously.", + "allOf": [ + { + "$ref": "#/components/schemas/ChatMessage" + }, + { + "type": "object", + "properties": { + "additionalMessages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + }, + "description": "Optional array of supporting messages that provide additional context for the primary message. These can include: - Reasoning steps that led to the final answer - Tool usage and outputs during processing - Intermediate calculations or decision points - Status updates about the processing - Human interactions that influenced the response. The primary message (this ChatResult itself) should contain the final, complete response, while additionalMessages provides transparency into how that response was generated. UI implementations should typically display the primary message prominently, with additionalMessages shown as supporting information when relevant." + } + } + } + ] + }, + "InlineChatResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ChatMessage" + }, + { + "type": "object", + "properties": { + "requestId": { + "type": "string" + } + } + } + ] + }, + "QuickActionCommand": { + "type": "object", + "description": "Configuration object for chat quick action.", + "required": ["command"], + "properties": { + "command": { + "type": "string" + }, + "description": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + } + } + }, + "QuickActionCommandGroup": { + "type": "object", + "description": "Configuration object for registering chat quick actions groups.", + "required": ["commands"], + "properties": { + "groupName": { + "type": "string" + }, + "commands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuickActionCommand" + } + } + } + }, + "QuickActions": { + "type": "object", + "description": "Registration options for a Chat QuickActionRequest.", + "required": ["quickActionsCommandGroups"], + "properties": { + "quickActionsCommandGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuickActionCommandGroup" + }, + "description": "The chat quick actions groups and commands to be executed on server." + } + } + }, + "TabData": { + "type": "object", + "required": ["messages"], + "properties": { + "placeholderText": { + "type": "string" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + } + } + } + }, + "ChatOptions": { + "type": "object", + "description": "Registration options regarding chat data. Currently contains the available quick actions provided by a server and the default tab data to be shown to the user in the chat UI.", + "properties": { + "quickActions": { + "$ref": "#/components/schemas/QuickActions", + "description": "Chat QuickActions, supported by Server. Chat Client renders and sets up actions handler for registered QuickAction in UI." + }, + "mcpServers": { + "type": "boolean" + }, + "modelSelection": { + "type": "boolean", + "description": "Server signals to Chat Client support of model selection." + }, + "history": { + "type": "boolean", + "description": "Server signals to Chat Client support of conversation history." + }, + "export": { + "type": "boolean", + "description": "Server signals to Chat Client support of Chat export feature." + }, + "showLogs": { + "type": "boolean", + "description": "Server signals to Chat Client support of show logs feature." + }, + "subscriptionDetails": { + "type": "boolean", + "description": "Server signals to Client and Chat Client that it supports subscription tier operations" + }, + "chatNotifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + }, + "description": "Server signals to Chat Client support of Chat notifications. Currently used for sending chat notifications for developer profile updates. Can be extended to support other types of notifications." + } + } + }, + "QuickActionParams": { + "allOf": [ + { + "$ref": "#/components/schemas/PartialResultParams" + }, + { + "type": "object", + "required": ["tabId", "quickAction"], + "properties": { + "tabId": { + "type": "string" + }, + "quickAction": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "cursorState": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CursorState" + } + }, + "textDocument": { + "type": "TextDocumentIdentifier" + } + } + } + ] + }, + "EncryptedQuickActionParams": { + "allOf": [ + { + "$ref": "#/components/schemas/PartialResultParams" + }, + { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } + } + ] + }, + "QuickActionResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ChatMessage" + } + ], + "description": "Currently the QuickActionResult and ChatResult share the same shape. Response for quick actions request can be empty, if server chooses to handle the request and push updates asynchronously." + }, + "FeedbackParams": { + "type": "object", + "required": ["tabId", "feedbackPayload"], + "properties": { + "tabId": { + "type": "string" + }, + "feedbackPayload": { + "$ref": "#/components/schemas/FeedbackPayload" + }, + "eventId": { + "type": "string" + } + } + }, + "TabEventParams": { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + } + } + }, + "TabAddParams": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/TabEventParams" + } + ], + "properties": { + "restoredTab": { + "type": "boolean" + } + } + }, + "TabChangeParams": { + "allOf": [ + { + "$ref": "#/components/schemas/TabEventParams" + } + ] + }, + "TabRemoveParams": { + "allOf": [ + { + "$ref": "#/components/schemas/TabEventParams" + } + ] + }, + "InsertToCursorPositionParams": { + "type": "object", + "required": ["tabId", "messageId"], + "properties": { + "tabId": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "cursorPosition": { + "type": "Position" + }, + "textDocument": { + "type": "TextDocumentIdentifier" + }, + "code": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/CodeSelectionType" + }, + "referenceTrackerInformation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReferenceTrackerInformation" + } + }, + "eventId": { + "type": "string" + }, + "codeBlockIndex": { + "type": "number" + }, + "totalCodeBlocks": { + "type": "number" + } + } + }, + "InfoLinkClickParams": { + "type": "object", + "required": ["tabId", "link"], + "properties": { + "tabId": { + "type": "string" + }, + "link": { + "type": "string" + }, + "eventId": { + "type": "string" + } + } + }, + "LinkClickParams": { + "allOf": [ + { + "$ref": "#/components/schemas/InfoLinkClickParams" + }, + { + "type": "object", + "required": ["messageId"], + "properties": { + "messageId": { + "type": "string" + } + } + } + ] + }, + "SourceLinkClickParams": { + "allOf": [ + { + "$ref": "#/components/schemas/InfoLinkClickParams" + }, + { + "type": "object", + "required": ["messageId"], + "properties": { + "messageId": { + "type": "string" + } + } + } + ] + }, + "FollowUpClickParams": { + "type": "object", + "required": ["tabId", "messageId", "followUp"], + "properties": { + "tabId": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "followUp": { + "$ref": "#/components/schemas/ChatItemAction" + } + } + }, + "NewTabOptions": { + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/TabState" + }, + "data": { + "$ref": "#/components/schemas/TabData" + } + } + }, + "OpenTabParams": { + "type": "object", + "description": "Defines parameters for opening a tab. Opens existing tab if `tabId` is provided, otherwise creates a new tab with options provided in `options` parameter and opens it.", + "properties": { + "tabId": { + "type": "string" + }, + "newTabOptions": { + "$ref": "#/components/schemas/NewTabOptions" + } + } + }, + "OpenTabResult": { + "allOf": [ + { + "$ref": "#/components/schemas/TabEventParams" + } + ] + }, + "ButtonClickParams": { + "type": "object", + "required": ["tabId", "messageId", "buttonId"], + "properties": { + "tabId": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "buttonId": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "ButtonClickResult": { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + }, + "failureReason": { + "type": "string" + } + } + }, + "TabState": { + "type": "object", + "properties": { + "inProgress": { + "type": "boolean" + }, + "cancellable": { + "type": "boolean" + } + } + }, + "ChatUpdateParams": { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/TabState" + }, + "data": { + "$ref": "#/components/schemas/TabData" + } + } + }, + "ChatOptionsUpdateParams": { + "type": "object", + "required": ["tabId"], + "description": "Server-initiated chat metadata updates.", + "properties": { + "tabId": { + "type": "string" + }, + "chatNotifications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatMessage" + }, + "description": "Processes changes of developer profiles." + }, + "modelId": { + "type": "string", + "description": "The last selected modelId for the conversation. This is used to allow the server to programmatically update the selected model for persistance across sessions." + } + } + }, + "FileAction": { + "type": "string", + "enum": ["accept-change", "reject-change"] + }, + "FileClickParams": { + "type": "object", + "required": ["tabId", "filePath"], + "properties": { + "tabId": { + "type": "string" + }, + "filePath": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/FileAction" + }, + "messageId": { + "type": "string" + }, + "fullPath": { + "type": "string" + } + } + }, + "ContextCommandGroup": { + "type": "object", + "required": ["commands"], + "properties": { + "groupName": { + "type": "string" + }, + "commands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextCommand" + } + } + } + }, + "ContextCommand": { + "allOf": [ + { + "$ref": "#/components/schemas/QuickActionCommand" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "route": { + "type": "array", + "items": { + "type": "string" + } + }, + "label": { + "type": "string", + "description": "file, folder, code, image" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextCommandGroup" + } + }, + "content": { + "type": "Uint8Array" + } + } + } + ], + "required": [] + }, + "ContextCommandParams": { + "type": "object", + "required": ["contextCommandGroups"], + "properties": { + "contextCommandGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContextCommandGroup" + } + } + } + }, + "PinnedContextParams": { + "allOf": [ + { + "$ref": "#/components/schemas/ContextCommandParams" + }, + { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + }, + "textDocument": { + "type": "TextDocumentIdentifier" + }, + "showRules": { + "type": "boolean" + } + } + } + ] + }, + "CreatePromptParams": { + "type": "object", + "required": ["promptName"], + "properties": { + "promptName": { + "type": "string" + }, + "isRule": { + "type": "boolean" + } + } + }, + "OpenFileDialogParams": { + "type": "object", + "required": ["tabId", "fileType"], + "properties": { + "tabId": { + "type": "string" + }, + "fileType": { + "$ref": "#/components/schemas/FileDialogType" + }, + "insertPosition": { + "type": "number" + } + } + }, + "OpenFileDialogResult": { + "type": "object", + "required": ["tabId", "fileType", "filePaths"], + "properties": { + "tabId": { + "type": "string" + }, + "fileType": { + "$ref": "#/components/schemas/FileDialogType" + }, + "filePaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "errorMessage": { + "type": "string" + }, + "insertPosition": { + "type": "number" + } + } + }, + "DropFilesParams": { + "type": "object", + "required": ["tabId", "files", "insertPosition"], + "properties": { + "tabId": { + "type": "string" + }, + "files": { + "$ref": "#/components/schemas/FileList" + }, + "insertPosition": { + "type": "number" + }, + "errorMessage": { + "type": "string" + } + } + }, + "ProgrammingLanguage": { + "type": "object", + "required": ["languageName"], + "properties": { + "languageName": { + "type": "string" + } + } + }, + "InlineChatUserDecision": { + "type": "string", + "enum": ["ACCEPT", "REJECT", "DISMISS"] + }, + "TextBasedFilterType": { + "type": "string", + "enum": ["textarea", "textinput", "numericinput"] + }, + "OptionBasedFilterType": { + "type": "string", + "enum": ["select", "radiogroup"] + }, + "FileDialogType": { + "type": "string", + "enum": ["image", ""] + }, + "RuleType": { + "type": "string", + "enum": ["folder", "rule"] + }, + "RuleActiveState": { + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/RuleActiveStateString" + } + ] + }, + "RuleActiveStateString": { + "type": "string", + "enum": ["indeterminate"] + }, + "InlineChatResultParams": { + "type": "object", + "required": ["requestId"], + "properties": { + "requestId": { + "type": "string" + }, + "inputLength": { + "type": "number" + }, + "selectedLines": { + "type": "number" + }, + "suggestionAddedChars": { + "type": "number" + }, + "suggestionAddedLines": { + "type": "number" + }, + "suggestionDeletedChars": { + "type": "number" + }, + "suggestionDeletedLines": { + "type": "number" + }, + "codeIntent": { + "type": "boolean" + }, + "userDecision": { + "$ref": "#/components/schemas/InlineChatUserDecision" + }, + "responseStartLatency": { + "type": "number" + }, + "responseEndLatency": { + "type": "number" + }, + "programmingLanguage": { + "$ref": "#/components/schemas/ProgrammingLanguage" + } + } + }, + "FilterValue": { + "type": "string" + }, + "BaseProperties": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + } + }, + "required": ["id"] + }, + "SelectOption": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": ["value", "label"] + }, + "TextBasedFilter": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseProperties" + }, + { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/TextBasedFilterType" + } + }, + "required": ["type"] + } + ] + }, + "OptionBasedFilter": { + "allOf": [ + { + "$ref": "#/components/schemas/BaseProperties" + }, + { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/OptionBasedFilterType" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectOption" + } + } + }, + "required": ["type", "options"] + } + ] + }, + "FilterOption": { + "oneOf": [ + { + "$ref": "#/components/schemas/TextBasedFilter" + }, + { + "$ref": "#/components/schemas/OptionBasedFilter" + } + ] + }, + "Action": { + "type": "object", + "required": ["id", "text"], + "properties": { + "id": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "text": { + "type": "string" + } + } + }, + "ConversationItem": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "description": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Action" + } + } + } + }, + "Model": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "ListAvailableModelsParams": { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + } + } + }, + "ListAvailableModelsResult": { + "type": "object", + "required": ["tabId", "models"], + "properties": { + "tabId": { + "type": "string" + }, + "models": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Model" + } + }, + "selectedModelId": { + "type": "string" + } + } + }, + "SubscriptionDetailsParams": { + "type": "object", + "required": [ + "subscriptionTier", + "subscriptionPeriodReset", + "isOverageEnabled", + "queryUsage", + "queryLimit", + "queryOverage" + ], + "properties": { + "subscriptionTier": { + "type": "string" + }, + "subscriptionPeriodReset": { + "type": "Date" + }, + "isOverageEnabled": { + "type": "boolean" + }, + "queryUsage": { + "type": "number" + }, + "queryLimit": { + "type": "number" + }, + "queryOverage": { + "type": "number" + } + } + }, + "ExecuteShellCommandParams": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + } + } + }, + "ConversationItemGroup": { + "type": "object", + "properties": { + "groupName": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationItem" + } + } + } + }, + "ListConversationsParams": { + "type": "object", + "properties": { + "filter": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FilterValue" + } + } + } + }, + "ListMcpServersParams": { + "type": "object", + "properties": { + "filter": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/FilterValue" + } + } + } + }, + "ListRulesParams": { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + } + } + }, + "ListRulesResult": { + "type": "object", + "required": ["tabId", "rules"], + "properties": { + "tabId": { + "type": "string" + }, + "header": { + "type": "object", + "required": ["title"], + "properties": { + "title": { + "type": "string" + } + } + }, + "filterOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterOption" + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RulesFolder" + } + } + } + }, + "RulesFolder": { + "type": "object", + "required": ["active", "rules"], + "properties": { + "folderName": { + "type": "string" + }, + "active": { + "$ref": "#/components/schemas/RuleActiveState", + "description": "Represents the active state of the folder: - true: all rules in the folder are active - false: all rules in the folder are inactive - 'indeterminate': rules inside the folder have mixed active states (some active, some inactive), similar to a parent checkbox in a nested checkbox list having an indeterminate state" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Rule" + } + } + } + }, + "Rule": { + "type": "object", + "required": ["active", "name", "id"], + "properties": { + "active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "RuleClickParams": { + "type": "object", + "required": ["tabId", "type", "id"], + "properties": { + "tabId": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/RuleType" + }, + "id": { + "type": "string" + } + } + }, + "RuleClickResult": { + "allOf": [ + { + "$ref": "#/components/schemas/RuleClickParams" + }, + { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + ] + }, + "ActiveEditorChangedParams": { + "type": "object", + "properties": { + "cursorState": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CursorState" + } + }, + "textDocument": { + "type": "TextDocumentIdentifier" + } + } + }, + "ConversationsList": { + "type": "object", + "required": ["list"], + "properties": { + "header": { + "type": "object", + "required": ["title"], + "properties": { + "title": { + "type": "string" + } + } + }, + "filterOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterOption" + } + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConversationItemGroup" + } + } + } + }, + "ListConversationsResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ConversationsList" + }, + { + "type": "object", + "properties": {} + } + ] + }, + "McpServerStatus": { + "type": "string", + "enum": ["INITIALIZING", "ENABLED", "FAILED", "DISABLED"] + }, + "DetailedListItem": { + "type": "object", + "required": ["title"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupActions": { + "type": "boolean" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailedListGroup" + } + } + } + }, + "DetailedListGroup": { + "type": "object", + "properties": { + "groupName": { + "type": "string" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailedListItem" + } + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Action" + } + }, + "icon": { + "$ref": "#/components/schemas/IconType" + } + } + }, + "ListMcpServersResultHeaderStatus": { + "type": "object", + "properties": { + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "title": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/Status" + } + } + }, + "ListMcpServersResultHeader": { + "type": "object", + "required": ["title"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ListMcpServersResultHeaderStatus" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Action" + } + } + } + }, + "ListMcpServersResult": { + "type": "object", + "required": ["list"], + "properties": { + "header": { + "$ref": "#/components/schemas/ListMcpServersResultHeader" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailedListGroup" + } + }, + "filterOptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterOption" + } + } + } + }, + "ConversationAction": { + "type": "string", + "enum": ["delete", "export"] + }, + "ConversationClickParams": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/ConversationAction" + } + } + }, + "ConversationClickResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ConversationClickParams" + }, + { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + ] + }, + "McpServerClickParams": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "optionsValues": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "McpServerClickResult": { + "allOf": [ + { + "$ref": "#/components/schemas/McpServerClickParams" + }, + { + "type": "object", + "properties": { + "filterOptions": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterOption" + } + }, + { + "type": "null" + } + ] + }, + "filterActions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Button" + } + }, + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailedListGroup" + } + }, + "header": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "status": { + "type": "object", + "properties": { + "icon": { + "$ref": "#/components/schemas/IconType" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/Status" + } + } + }, + "description": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Action" + } + } + } + } + } + } + ] + }, + "TabBarAction": { + "type": "string", + "enum": ["export", "show_logs"] + }, + "TabBarActionParams": { + "type": "object", + "required": ["action"], + "properties": { + "tabId": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/TabBarAction" + } + } + }, + "TabBarActionResult": { + "allOf": [ + { + "$ref": "#/components/schemas/TabBarActionParams" + }, + { + "type": "object", + "required": ["success"], + "properties": { + "success": { + "type": "boolean" + } + } + } + ] + }, + "GetSerializedChatParams": { + "allOf": [ + { + "$ref": "#/components/schemas/TabEventParams" + }, + { + "type": "object", + "required": ["format"], + "properties": { + "format": { + "type": "string", + "enum": ["html", "markdown"] + } + } + } + ] + }, + "GetSerializedChatResult": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "string" + } + } + }, + "PromptInputOptionChangeParams": { + "type": "object", + "required": ["tabId", "optionsValues"], + "properties": { + "tabId": { + "type": "string" + }, + "optionsValues": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "eventId": { + "type": "string" + } + } + }, + + "EndChatParams": { + "type": "object", + "required": ["tabId"], + "properties": { + "tabId": { + "type": "string" + } + } + } +} diff --git a/types/codegen/schema/windowTypes.json b/types/codegen/schema/windowTypes.json new file mode 100644 index 00000000..b80f4fae --- /dev/null +++ b/types/codegen/schema/windowTypes.json @@ -0,0 +1,54 @@ +{ + "ShowOpenDialogParams": { + "type": "object", + "properties": { + "canSelectFiles": { + "type": "boolean" + }, + "canSelectFolders": { + "type": "boolean" + }, + "canSelectMany": { + "type": "boolean" + }, + "filters": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "defaultUri": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "ShowSaveFileDialogParams": { + "type": "object", + "properties": { + "supportedFormats": { + "type": "array", + "items": { + "type": "string" + } + }, + "defaultUri": { + "type": "string" + } + } + }, + "ShowSaveFileDialogResult": { + "type": "object", + "required": ["targetUri"], + "properties": { + "targetUri": { + "type": "string" + } + } + } +} diff --git a/types/codegen/schema/workspaceTypes.json b/types/codegen/schema/workspaceTypes.json new file mode 100644 index 00000000..28bd413d --- /dev/null +++ b/types/codegen/schema/workspaceTypes.json @@ -0,0 +1,41 @@ +{ + "FileParams": { + "type": "object", + "required": ["path"], + "properties": { + "path": { + "type": "string" + } + } + }, + "CopyFileParams": { + "type": "object", + "required": ["oldPath", "newPath"], + "properties": { + "oldPath": { + "type": "string" + }, + "newPath": { + "type": "string" + } + } + }, + "OpenFileDiffParams": { + "type": "object", + "required": ["originalFileUri", "isDeleted"], + "properties": { + "originalFileUri": { + "type": "string" + }, + "originalFileContent": { + "type": "string" + }, + "isDeleted": { + "type": "boolean" + }, + "fileContent": { + "type": "string" + } + } + } +} diff --git a/types/codegen/scripts/constants.ts b/types/codegen/scripts/constants.ts new file mode 100644 index 00000000..370945b7 --- /dev/null +++ b/types/codegen/scripts/constants.ts @@ -0,0 +1,2 @@ +export type EndChatResult = boolean +export type FilterValue = string diff --git a/types/codegen/scripts/generate-complete-schema.js b/types/codegen/scripts/generate-complete-schema.js new file mode 100644 index 00000000..0f91cd80 --- /dev/null +++ b/types/codegen/scripts/generate-complete-schema.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +// Pre-generation script for all generators +// Combines all schema files in the schema directory and creates complete-schema.json with OpenAPI wrapper + +const schemaDir = path.join(__dirname, '../schema') +const completeSchemaPath = path.join(__dirname, '../schema/complete-schema.json') +const openapiConfigPath = path.join(__dirname, '../openapitools.json') + +// Validate required directories and files exist +if (!fs.existsSync(schemaDir)) { + console.error('Error: schema directory not found') + process.exit(1) +} + +if (!fs.existsSync(openapiConfigPath)) { + console.error('Error: openapitools.json file not found') + process.exit(1) +} + +// get version from openapitools.json +function getVersionFromConfig() { + try { + const configContent = fs.readFileSync(openapiConfigPath, 'utf8') + const config = JSON.parse(configContent) + + const typescriptGenerator = config['generator-cli']?.generators?.typescript + const npmVersion = typescriptGenerator?.additionalProperties?.npmVersion + + if (!npmVersion) { + console.warn('Warning: npmVersion not found in typescript generator config, using default "0.0.1"') + return '0.0.1' + } + + return npmVersion + } catch (error) { + console.error('Error reading openapitools.json:', error.message) + console.warn('Using default version "0.0.1"') + return '0.0.1' + } +} + +// get the version dynamically +const version = getVersionFromConfig() + +// prepend new file with OpenAPI header structure +const openApiHeader = { + openapi: '3.0.0', + info: { + title: 'Chat Types', + version: version, + description: 'Chat Types Definitions for FLARE', + }, + paths: {}, + components: { + schemas: { + // this will be populated with the actual schemas + }, + }, +} + +// Process all JSON files in the schema directory +function processSchemaFiles() { + const allSchemas = {} + const processedFiles = [] + const skippedFiles = [] + const errors = [] + + try { + const files = fs.readdirSync(schemaDir) + + // Filter for JSON files, excluding complete-schema.json + const jsonFiles = files.filter(file => file.endsWith('.json') && file !== 'complete-schema.json') + + if (jsonFiles.length === 0) { + console.error('Error: No JSON schema files found in schema directory') + process.exit(1) + } + + // Process each JSON file + for (const file of jsonFiles) { + const filePath = path.join(schemaDir, file) + + try { + const fileContent = fs.readFileSync(filePath, 'utf8') + + // Validate file is not empty + if (!fileContent.trim()) { + console.warn(`Warning: Skipping empty file ${file}`) + skippedFiles.push({ file, reason: 'empty file' }) + continue + } + + const parsedContent = JSON.parse(fileContent) + + // Extract schemas from the file + let fileSchemas + if (parsedContent.components?.schemas) { + // If it has OpenAPI wrapper, extract the schemas + fileSchemas = parsedContent.components.schemas + } else if (parsedContent && typeof parsedContent === 'object') { + // If it's raw schemas, use content directly + fileSchemas = parsedContent + } else { + console.warn(`Warning: Skipping ${file} - no valid schema structure found`) + skippedFiles.push({ file, reason: 'no valid schema structure' }) + continue + } + + // Validate that we have actual schemas + if (!fileSchemas || typeof fileSchemas !== 'object' || Object.keys(fileSchemas).length === 0) { + console.warn(`Warning: Skipping ${file} - no schemas found`) + skippedFiles.push({ file, reason: 'no schemas found' }) + continue + } + + // Merge schemas into the combined object with conflict detection + const fileSchemaKeys = Object.keys(fileSchemas) + const conflicts = [] + + // Check for naming conflicts + for (const schemaName of fileSchemaKeys) { + if (allSchemas[schemaName]) { + conflicts.push(schemaName) + } + } + + if (conflicts.length > 0) { + console.warn( + `Warning: Schema name conflicts in ${file}: ${conflicts.join(', ')} (will overwrite previous definitions)` + ) + } + + // Merge the schemas + Object.assign(allSchemas, fileSchemas) + processedFiles.push(file) + console.log(`Processed ${fileSchemaKeys.length} schemas from ${file}`) + } catch (error) { + // Handle different types of errors more gracefully + if (error instanceof SyntaxError) { + errors.push({ file, error: 'Invalid JSON', details: error.message }) + } else if (error.code === 'ENOENT') { + errors.push({ file, error: 'File not found', details: error.message }) + } else if (error.code === 'EACCES') { + errors.push({ file, error: 'Permission denied', details: error.message }) + } else { + errors.push({ file, error: 'Processing error', details: error.message }) + } + + // Continue processing other files instead of exiting + skippedFiles.push({ file, reason: `error: ${error.message}` }) + } + } + + // Summary of processing results + if (skippedFiles.length > 0) { + console.warn(`\nSkipped ${skippedFiles.length} files:`) + skippedFiles.forEach(({ file, reason }) => { + console.warn(` - ${file}: ${reason}`) + }) + } + + if (errors.length > 0) { + console.error(`\nEncountered ${errors.length} errors during processing:`) + errors.forEach(({ file, error, details }) => { + console.error(` - ${file}: ${error} (${details})`) + }) + } + + return { schemas: allSchemas, files: processedFiles, skipped: skippedFiles.length, errors: errors.length } + } catch (error) { + console.error('Error reading schema directory:', error.message) + process.exit(1) + } +} + +// Process all schema files +const { schemas, files, skipped, errors } = processSchemaFiles() + +if (!schemas || Object.keys(schemas).length === 0) { + console.error('Error: No schemas found in any files') + process.exit(1) +} + +// create the complete schema with header + schemas + footer +const completeSchema = { + ...openApiHeader, + components: { + schemas: schemas, + }, +} + +// write full OpenAPI spec to complete-schema.json +try { + fs.writeFileSync(completeSchemaPath, JSON.stringify(completeSchema, null, 4)) + + console.log(`\nComplete OpenAPI structure created in complete-schema.json (version: ${version})`) + console.log(`Combined ${Object.keys(schemas).length} total schemas from ${files.length} files: ${files.join(', ')}`) + + if (skipped > 0 || errors > 0) { + console.log(`Processing summary: ${files.length} successful, ${skipped} skipped, ${errors} errors`) + } +} catch (error) { + console.error('Error writing complete-schema.json:', error.message) + process.exit(1) +} diff --git a/types/codegen/scripts/post-test.js b/types/codegen/scripts/post-test.js new file mode 100755 index 00000000..f0e8d7ad --- /dev/null +++ b/types/codegen/scripts/post-test.js @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +// Post-test script to validate generated models against schema definitions +// Compares models in complete-schema.json with generated TypeScript and Java models +// Usage: node post-test.js [--verbose] + +// Parse command line arguments +const args = process.argv.slice(2) +const verbose = args.includes('--verbose') || args.includes('-v') + +// File paths +const completeSchemaPath = path.join(__dirname, '../schema/complete-schema.json') +const typescriptModelsPath = path.join(__dirname, '../generated/typescript/src/models/index.ts') +const javaModelsDir = path.join(__dirname, '../generated/java/src/main/java/org/openapitools/client/model') +const openapiConfigPath = path.join(__dirname, '../openapitools.json') + +console.log('Starting model validation...\n') + +// Check if complete-schema.json exists +if (!fs.existsSync(completeSchemaPath)) { + console.error('Error: complete-schema.json not found in schema directory') + console.error(' Run "npm run generate-schema" first to create the schema file') + process.exit(1) +} + +// Load and parse OpenAPI configuration +let openapiConfig = null +let typescriptFilteredModels = null +let javaFilteredModels = null + +try { + openapiConfig = JSON.parse(fs.readFileSync(openapiConfigPath, 'utf8')) + + // Check for TypeScript global-property models filter + const tsGenerator = openapiConfig['generator-cli']?.generators?.typescript + if (tsGenerator?.['global-property']?.models) { + typescriptFilteredModels = new Set(tsGenerator['global-property'].models.split(':')) + } + + // Check for Java global-property models filter + const javaGenerator = openapiConfig['generator-cli']?.generators?.java + if (javaGenerator?.['global-property']?.models) { + javaFilteredModels = new Set(javaGenerator['global-property'].models.split(':')) + } +} catch (error) { + console.warn('Warning: Could not read OpenAPI configuration:', error.message) +} + +// Load and parse complete-schema.json +let schemaModels = new Set() +try { + const completeSchema = JSON.parse(fs.readFileSync(completeSchemaPath, 'utf8')) + + if (!completeSchema.components || !completeSchema.components.schemas) { + console.error('Error: Invalid OpenAPI structure in complete-schema.json') + console.error(' Missing components.schemas section') + process.exit(1) + } + + schemaModels = new Set(Object.keys(completeSchema.components.schemas)) + console.log(`Found ${schemaModels.size} models in schema\n`) +} catch (error) { + console.error('Error parsing complete-schema.json:', error.message) + process.exit(1) +} + +// Extract TypeScript models +let typescriptModels = new Set() +if (fs.existsSync(typescriptModelsPath)) { + try { + const typescriptContent = fs.readFileSync(typescriptModelsPath, 'utf8') + + // Match interface and type declarations + const interfaceMatches = typescriptContent.match(/(?:export\s+)?(?:interface|type)\s+(\w+)/g) || [] + const enumMatches = typescriptContent.match(/export\s+(?:const\s+)?(\w+)\s*=\s*{[^}]*}\s*as\s+const/g) || [] + + interfaceMatches.forEach(match => { + const modelName = match.replace(/(?:export\s+)?(?:interface|type)\s+/, '') + typescriptModels.add(modelName) + }) + + enumMatches.forEach(match => { + const modelName = match.match(/export\s+(?:const\s+)?(\w+)/)[1] + typescriptModels.add(modelName) + }) + + if (typescriptFilteredModels) { + console.log(`TypeScript generator has model filter: ${typescriptFilteredModels.size} models`) + } + console.log(`Found ${typescriptModels.size} TypeScript models\n`) + } catch (error) { + console.error('Warning: Error reading TypeScript models:', error.message) + } +} else { + console.warn('Warning: TypeScript models file not found at:', typescriptModelsPath) +} + +// Extract Java models +let javaModels = new Set() +if (fs.existsSync(javaModelsDir)) { + try { + const javaFiles = fs.readdirSync(javaModelsDir).filter(file => file.endsWith('.java')) + + javaFiles.forEach(file => { + const modelName = file.replace('.java', '') + javaModels.add(modelName) + }) + + if (javaFilteredModels) { + console.log(`Java generator has model filter: ${javaFilteredModels.size} models`) + } + console.log(`Found ${javaModels.size} Java models\n`) + } catch (error) { + console.error('Warning: Error reading Java models:', error.message) + } +} else { + console.warn('Warning: Java models directory not found at:', javaModelsDir) +} + +// Validation results +let hasErrors = false +const results = { + typescript: { + missing: [], + extra: [], + }, + java: { + missing: [], + extra: [], + }, +} + +// Check TypeScript models +if (typescriptModels.size > 0) { + // Determine expected models based on configuration + const expectedTypescriptModels = typescriptFilteredModels || schemaModels + + // Find missing models (expected but not in TypeScript) + expectedTypescriptModels.forEach(model => { + if (!typescriptModels.has(model)) { + results.typescript.missing.push(model) + } + }) + + // Find extra models (in TypeScript but not expected) + typescriptModels.forEach(model => { + if (!expectedTypescriptModels.has(model)) { + results.typescript.extra.push(model) + } + }) +} + +// Check Java models +if (javaModels.size > 0) { + // Determine expected models based on configuration + const expectedJavaModels = javaFilteredModels || schemaModels + + // Find missing models (expected but not in Java) + expectedJavaModels.forEach(model => { + if (!javaModels.has(model)) { + results.java.missing.push(model) + } + }) + + // Find extra models (in Java but not expected) + javaModels.forEach(model => { + if (!expectedJavaModels.has(model)) { + results.java.extra.push(model) + } + }) +} + +// Report results +console.log('VALIDATION RESULTS') +console.log('='.repeat(50)) + +// TypeScript results +if (typescriptModels.size > 0) { + console.log('\nTypeScript Models:') + if (typescriptFilteredModels) { + console.log(` Note: Only ${typescriptFilteredModels.size} models expected (global-property filter active)`) + } + + if (results.typescript.missing.length === 0 && results.typescript.extra.length === 0) { + console.log(' All models match perfectly!') + } else { + if (results.typescript.missing.length > 0) { + console.log(` Missing models (${results.typescript.missing.length}):`) + results.typescript.missing.sort().forEach(model => { + console.log(` - ${model}`) + }) + hasErrors = true + } + + if (verbose && results.typescript.extra.length > 0) { + console.log(` Extra/Intermediate models (${results.typescript.extra.length}):`) + results.typescript.extra.sort().forEach(model => { + console.log(` + ${model}`) + }) + } + } +} else { + console.log('\nTypeScript Models: Not found or not generated') +} + +// Java results +if (javaModels.size > 0) { + console.log('\nJava Models:') + if (javaFilteredModels) { + console.log(` Note: Only ${javaFilteredModels.size} models expected (global-property filter active)`) + } + + if (results.java.missing.length === 0 && results.java.extra.length === 0) { + console.log(' All models match perfectly!') + } else { + if (results.java.missing.length > 0) { + console.log(` Missing models (${results.java.missing.length}):`) + results.java.missing.sort().forEach(model => { + console.log(` - ${model}`) + }) + hasErrors = true + } + + if (verbose && results.java.extra.length > 0) { + console.log(` Extra/Intermediate models (${results.java.extra.length}):`) + results.java.extra.sort().forEach(model => { + console.log(` + ${model}`) + }) + } + } +} else { + console.log('\nJava Models: Not found or not generated') +} + +// Summary +console.log('\nSUMMARY') +console.log('='.repeat(50)) +console.log(`Schema models: ${schemaModels.size}`) +console.log(`TypeScript models: ${typescriptModels.size}`) +console.log(`Java models: ${javaModels.size}`) + +const totalMissing = results.typescript.missing.length + results.java.missing.length +const totalExtra = results.typescript.extra.length + results.java.extra.length + +if (totalMissing === 0 && totalExtra === 0 && typescriptModels.size > 0 && javaModels.size > 0) { + console.log('\nSUCCESS: All models are properly generated!') +} else { + if (totalMissing > 0) { + console.log(`\n${totalMissing} missing model(s) found`) + } + if (totalExtra > 0) { + console.log(`\n${totalExtra} extra/intermediate model(s) found`) + } + if (typescriptModels.size === 0 || javaModels.size === 0) { + console.log('\nSome generators may not have run successfully') + } +} + +if (totalExtra > 0 && !verbose) { + console.log('\nTIP: Use --verbose flag to see extra/intermediate models') +} + +// Exit with error code if there are missing models +if (hasErrors) { + console.log('\nTIP: Add model as a constant to the post processing script if type alias or an empty model') + process.exit(1) +} + +process.exit(0) diff --git a/types/codegen/scripts/post-typescript.js b/types/codegen/scripts/post-typescript.js new file mode 100644 index 00000000..2d210a9d --- /dev/null +++ b/types/codegen/scripts/post-typescript.js @@ -0,0 +1,130 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +// Post-generation script for TypeScript OpenAPI code generation +// Adds constants, processes import mappings, and modifies generated interfaces + +// File paths +const indexPath = path.join(__dirname, '../generated/typescript/src/models/index.ts') +const constantsPath = path.join(__dirname, 'constants.ts') +const openapiConfigPath = path.join(__dirname, '../openapitools.json') + +// Validate required files exist +if (!fs.existsSync(constantsPath)) { + console.error('Error: constants.ts file not found') + process.exit(1) +} + +if (!fs.existsSync(indexPath)) { + console.error('Error: Generated index.ts file not found') + process.exit(1) +} + +if (!fs.existsSync(openapiConfigPath)) { + console.error('Error: openapitools.json file not found') + process.exit(1) +} + +// Process import mappings from OpenAPI configuration +function processImportMappings() { + let config + try { + const configContent = fs.readFileSync(openapiConfigPath, 'utf8') + config = JSON.parse(configContent) + } catch (error) { + console.error('Error parsing openapitools.json:', error.message) + return { statements: '', totalMappings: 0, processedMappings: 0, importStatements: 0 } + } + + const generators = config['generator-cli']?.generators + const typescriptGenerator = generators?.typescript + const importMappings = typescriptGenerator?.importMappings + + const totalMappings = importMappings ? Object.keys(importMappings).length : 0 + + if (!importMappings || totalMappings === 0) { + return { statements: '', totalMappings, processedMappings: 0, importStatements: 0 } + } + + // Group imports by their source file to avoid duplicate import statements + const fileGroups = {} + Object.entries(importMappings).forEach(([importName, sourcePath]) => { + if (!fileGroups[sourcePath]) { + fileGroups[sourcePath] = [] + } + fileGroups[sourcePath].push(importName) + }) + + // Create properly formatted ES6 import statements + const importStatements = Object.entries(fileGroups).map(([filePath, imports]) => { + const sortedImports = imports.sort() + return `import { ${sortedImports.join(', ')} } from '${filePath}'` + }) + + return { + statements: importStatements.join('\n') + '\n', + totalMappings, + processedMappings: totalMappings, + importStatements: importStatements.length, + } +} + +// Read the constants file and process import mappings +const constants = fs.readFileSync(constantsPath, 'utf8') +const importResult = processImportMappings() + +// Read the generated index.ts file +let indexContent = fs.readFileSync(indexPath, 'utf8') + +// Find the insertion position - either at top of file or after lint disable comments +let insertPos = 0 + +// Check if file starts with lint disable comments +const lintDisablePattern = /^(\s*\/\*\s*tslint:disable\s*\*\/\s*\/\*\s*eslint-disable\s*\*\/\s*)/ +const lintDisableMatch = indexContent.match(lintDisablePattern) + +if (lintDisableMatch) { + // Insert after the lint disable comments + insertPos = lintDisableMatch[0].length +} else { + // Insert at the very top of the file + insertPos = 0 +} + +// Insert import statements and constants after existing imports +const contentToInsert = importResult.statements + (importResult.statements ? '\n' : '') + constants + '\n' +const newContent = indexContent.substring(0, insertPos) + '\n' + contentToInsert + indexContent.substring(insertPos) + +// Modify PartialResultParams interface visibility +const originalContent = newContent +let modifiedContent = newContent.replace( + /export\s+interface\s+PartialResultParams\s*\{/g, + 'interface PartialResultParams {' +) + +// Write the updated content back to the file +fs.writeFileSync(indexPath, modifiedContent) + +// Log import mapping results +if (importResult.totalMappings === 0) { + console.log('Import mappings: 0 mappings found in openapitools.json') +} else { + console.log( + `Import mappings: processed ${importResult.processedMappings}/${importResult.totalMappings} mappings into ${importResult.importStatements} import statements` + ) + + // Warning if numbers don't match + if (importResult.processedMappings !== importResult.totalMappings) { + console.warn( + `Warning: Only ${importResult.processedMappings} out of ${importResult.totalMappings} import mappings were processed` + ) + } +} + +console.log('Constants added to generated index.ts file') + +if (originalContent !== modifiedContent) { + console.log('PartialResultParams interface modified') +} diff --git a/types/codegen/tests/README.md b/types/codegen/tests/README.md new file mode 100644 index 00000000..3641f105 --- /dev/null +++ b/types/codegen/tests/README.md @@ -0,0 +1,104 @@ +# OpenAPI Code Generation Tests + +Comprehensive test suite validating OpenAPI code generation pipeline using your actual `openapitools.json` configuration and custom templates. + +## Test Structure + +``` +tests/ +├── utils/test-helpers.js # Shared utilities +└── unit/ + ├── field-addition.test.js # Field scenarios & configuration validation + └── openapi-normalizer.test.js # OpenAPI normalizer features +``` + +## Running Tests + +```bash +# Normal test run +npm run test:unit + +# Inspect generated files +KEEP_TEST_OUTPUT=1 npm run test:unit + +# Run specific tests +npm run test:unit -- --testPathPattern="field-addition" +``` + +## Test Coverage + +### Field Addition Tests (8 tests) +Validates field addition scenarios and configuration compliance: + +- **Field Testing**: Base schema, optional fields (`field?: string`), required fields (`field: string`) +- **Configuration Validation**: + - TypeScript camelCase naming (`displayName` vs `display_name`) + - ES6 exports (`export interface` vs `module.exports`) + - Java 21 compatibility (no deprecated features) + - Model filtering (`global-property models=TestModel`) + - Custom templates (Java records vs classes) + +### OpenAPI Normalizer Tests (2 tests) +Validates OpenAPI normalizer features with language-specific behavior: + +- **REF_AS_PARENT_IN_ALLOF**: Configuration-aware testing + - TypeScript: Tests inheritance (`extends BaseModel`) vs flattening + - Java: Always flattens properties (records don't support inheritance) +- **Custom Template Compatibility**: Ensures normalizer works with custom templates + +## Key Features + +### Real Configuration Testing +- Uses your actual `openapitools.json` (not mocked) +- Tests adapt behavior based on your configuration +- Validates custom templates are actually used + +### Language-Specific Validation +- TypeScript: Interfaces, optional fields, ES6 exports, inheritance +- Java: Records, annotations (`@Nonnull`, `@Nullable`), property flattening + +### Robust Design +- Shared utilities prevent code duplication +- Configuration-aware tests adapt to changes +- Clean test isolation with conditional cleanup +- Clear failure points with specific test names + +## Generated Validation Examples + +**TypeScript**: +```typescript +export interface TestModel { + id: string; + displayName: string; // camelCase naming + optionalField?: string; // Optional field syntax +} +``` + +**Java**: +```java +public record TestModel( // Custom template (record vs class) + @JsonProperty("id") @Nonnull String id, + @JsonProperty("displayName") @Nonnull String displayName +) {} +``` + +## Adding New Tests + +**Extend existing files** for simple checks fitting current themes. + +**Create new files** for complex features deserving dedicated test suites: + +```javascript +const { generateCodeWithConfig, createTestDirectory } = require('../utils/test-helpers') + +describe('New Feature Tests', () => { + // Use shared utilities for consistency +}) +``` + +## Benefits + +- **Production validation**: Tests your actual build pipeline +- **Regression protection**: Catches configuration and template breaks +- **Maintainable**: Shared utilities, configuration-aware testing +- **Developer-friendly**: Clear test names, easy debugging with `KEEP_TEST_OUTPUT` \ No newline at end of file diff --git a/types/codegen/tests/unit/field-addition.test.js b/types/codegen/tests/unit/field-addition.test.js new file mode 100644 index 00000000..7b40e2c6 --- /dev/null +++ b/types/codegen/tests/unit/field-addition.test.js @@ -0,0 +1,313 @@ +#!/usr/bin/env node + +// Field Addition Tests for OpenAPI Code Generation +// +// To inspect generated output files, run with: +// KEEP_TEST_OUTPUT=1 npm run test:unit +// +// Files will be preserved in: tests/fixtures/field-addition/temp-generated/ + +const fs = require('fs') +const path = require('path') +const assert = require('assert') +const { + generateCodeWithConfig, + createTestDirectory, + cleanupTestFiles, + cleanupTempOutput, + createBaseSchema, +} = require('../utils/test-helpers') + +describe('Field Addition Tests', function () { + this.timeout(30000) // Increase timeout to 30 seconds for code generation + const testDir = path.join(__dirname, '../fixtures/field-addition') + const baseSchemaPath = path.join(testDir, 'base-schema.json') + const optionalFieldSchemaPath = path.join(testDir, 'optional-field-schema.json') + const requiredFieldSchemaPath = path.join(testDir, 'required-field-schema.json') + const tempOutputDir = path.join(testDir, 'temp-generated') + + // Helper function to generate code for a specific schema using shared utilities + const generateCode = schemaPath => { + generateCodeWithConfig(schemaPath, testDir, tempOutputDir) + } + + before(() => { + // Create test directory structure using shared utility + createTestDirectory(testDir) + + // Create base test schema using shared utility + const baseSchema = createBaseSchema({ + TestModel: { + type: 'object', + required: ['id', 'displayName'], + properties: { + id: { + type: 'string', + description: 'Unique identifier', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + }, + }, + }) + + // Create schema with optional field added + const optionalFieldSchema = createBaseSchema({ + TestModel: { + type: 'object', + required: ['id', 'displayName'], + properties: { + id: { + type: 'string', + description: 'Unique identifier', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + optionalField: { + type: 'string', + description: 'Optional field for testing', + }, + }, + }, + }) + + // Create schema with required field added + const requiredFieldSchema = createBaseSchema({ + TestModel: { + type: 'object', + required: ['id', 'displayName', 'requiredField'], + properties: { + id: { + type: 'string', + description: 'Unique identifier', + }, + displayName: { + type: 'string', + description: 'Display name', + }, + requiredField: { + type: 'string', + description: 'Required field for testing', + }, + }, + }, + }) + + // Write test schemas + fs.writeFileSync(baseSchemaPath, JSON.stringify(baseSchema, null, 2)) + fs.writeFileSync(optionalFieldSchemaPath, JSON.stringify(optionalFieldSchema, null, 2)) + fs.writeFileSync(requiredFieldSchemaPath, JSON.stringify(requiredFieldSchema, null, 2)) + }) + + after(() => { + // Clean up test files using shared utility + cleanupTestFiles(testDir) + }) + + // Note: We don't clean up between tests since each test generates its own code + + it('base schema generates correctly', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for base schema test:', error.message) + throw error + } + + // Verify TypeScript generation - check the actual TestModel file + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + assert.strictEqual(fs.existsSync(tsModelPath), true) + + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + assert(tsContent.includes('interface TestModel')) + assert(tsContent.includes('id: string')) + assert(tsContent.includes('displayName: string')) + assert(!tsContent.includes('optionalField')) + assert(!tsContent.includes('requiredField')) + + // Verify Java generation + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + assert.strictEqual(fs.existsSync(javaModelPath), true) + + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + assert(javaContent.includes('record TestModel')) + assert(javaContent.includes('String id')) + assert(javaContent.includes('String displayName')) + // Check that optional and required test fields are not present in base schema + assert(!javaContent.includes('SERIALIZED_NAME_OPTIONAL_FIELD')) + assert(!javaContent.includes('SERIALIZED_NAME_REQUIRED_FIELD')) + assert(!javaContent.includes('private String optionalField')) + assert(!javaContent.includes('private String requiredField')) + }) + + it('optional field is generated correctly', () => { + // Generate code using helper function + try { + generateCode(optionalFieldSchemaPath) + } catch (error) { + console.error('Generation failed for optional field test:', error.message) + throw error + } + + // Verify TypeScript generation + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + + assert(tsContent.includes('interface TestModel')) + assert(tsContent.includes('id: string')) + assert(tsContent.includes('displayName: string')) + assert(tsContent.includes('optionalField?: string')) // Optional field should have ? + + // Verify Java generation + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + + assert(javaContent.includes('record TestModel')) + assert(javaContent.includes('String id')) + assert(javaContent.includes('String displayName')) + assert(javaContent.includes('String optionalField')) + + // Check that optional field doesn't have @NotNull annotation (if using validation) + const optionalFieldLines = javaContent + .split('\n') + .filter(line => line.includes('optionalField') && !line.includes('//')) + assert(optionalFieldLines.length > 0) + }) + + it('required field is generated correctly', () => { + // Generate code using helper function + try { + generateCode(requiredFieldSchemaPath) + } catch (error) { + console.error('Generation failed for required field test:', error.message) + throw error + } + + // Verify TypeScript generation + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + + assert(tsContent.includes('interface TestModel')) + assert(tsContent.includes('id: string')) + assert(tsContent.includes('displayName: string')) + assert(tsContent.includes('requiredField: string')) // Required field should NOT have ? + + // Verify Java generation + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + + assert(javaContent.includes('record TestModel')) + assert(javaContent.includes('String id')) + assert(javaContent.includes('String displayName')) + assert(javaContent.includes('String requiredField')) + }) + + it('TypeScript uses camelCase property naming', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for camelCase test:', error.message) + throw error + } + + // Verify TypeScript configuration settings + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + + // Check camelCase property naming (from modelPropertyNaming: "camelCase") + assert(tsContent.includes('id: string')) // Should be camelCase, not snake_case + assert(tsContent.includes('displayName: string')) // Should be camelCase, not display_name + }) + + it('TypeScript uses ES6 exports (supportsES6: true)', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for ES6 exports test:', error.message) + throw error + } + + // Verify TypeScript uses ES6 module syntax + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + + // Check ES6 support (export statements instead of CommonJS) + assert(tsContent.includes('export interface')) + assert(!tsContent.includes('module.exports')) + }) + + it('Java uses Java 21 compatibility (no deprecated features)', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for Java 21 compatibility test:', error.message) + throw error + } + + // Verify Java configuration settings + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + + // Check Java 21 compatibility (should not have deprecated features) + assert(!javaContent.includes('@SuppressWarnings("deprecation")')) + + // Should use modern annotations + assert(javaContent.includes('@javax.annotation.Generated')) + }) + + it('Java model filtering works (global-property models=TestModel)', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for Java model filtering test:', error.message) + throw error + } + + // Check that only TestModel was generated (due to global-property filter) + const javaModelDir = path.join(tempOutputDir, 'java/src/main/java/org/openapitools/client/model') + const javaFiles = fs.readdirSync(javaModelDir).filter(f => f.endsWith('.java')) + assert.deepStrictEqual(javaFiles, ['TestModel.java']) + }) + + it('custom templates generate Java records instead of classes', () => { + // Generate code using helper function + try { + generateCode(baseSchemaPath) + } catch (error) { + console.error('Generation failed for Java records test:', error.message) + throw error + } + + // Verify Java uses records from custom templates + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + + // Check that custom templates generate records instead of classes + assert(javaContent.includes('public record TestModel')) + assert(!javaContent.includes('public class TestModel')) + }) +}) diff --git a/types/codegen/tests/unit/openapi-normalizer.test.js b/types/codegen/tests/unit/openapi-normalizer.test.js new file mode 100644 index 00000000..f546e1c4 --- /dev/null +++ b/types/codegen/tests/unit/openapi-normalizer.test.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +// OpenAPI Normalizer Tests for OpenAPI Code Generation +// +// To inspect generated output files, run with: +// KEEP_TEST_OUTPUT=1 npm run test:unit +// +// Files will be preserved in: tests/fixtures/openapi-normalizer/temp-generated/ + +const fs = require('fs') +const path = require('path') +const assert = require('assert') +const { + generateCodeWithConfig, + createTestDirectory, + cleanupTestFiles, + cleanupTempOutput, + createBaseSchema, +} = require('../utils/test-helpers') + +describe('OpenAPI Normalizer Tests', function () { + this.timeout(30000) // Increase timeout to 30 seconds for code generation + const testDir = path.join(__dirname, '../fixtures/openapi-normalizer') + const tempOutputDir = path.join(testDir, 'temp-generated') + const allOfSchemaPath = path.join(testDir, 'allof-schema.json') + + before(() => { + createTestDirectory(testDir) + + // Create a schema that uses allOf to test REF_AS_PARENT_IN_ALLOF + const allOfSchema = createBaseSchema({ + BaseModel: { + type: 'object', + required: ['id'], + properties: { + id: { + type: 'string', + description: 'Base identifier', + }, + }, + }, + TestModel: { + allOf: [ + { $ref: '#/components/schemas/BaseModel' }, + { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + description: 'Display name', + }, + }, + }, + ], + }, + }) + + // Write test schema + fs.writeFileSync(allOfSchemaPath, JSON.stringify(allOfSchema, null, 2)) + }) + + after(() => { + cleanupTestFiles(testDir) + }) + + // Note: We don't clean up between tests since each test generates its own code + + it('REF_AS_PARENT_IN_ALLOF normalizer works correctly', () => { + // Generate code using helper function + try { + generateCodeWithConfig(allOfSchemaPath, testDir, tempOutputDir) + } catch (error) { + console.error('Generation failed for REF_AS_PARENT_IN_ALLOF test:', error.message) + throw error + } + + // Read the configuration to check if REF_AS_PARENT_IN_ALLOF is enabled + const originalConfigPath = path.join(__dirname, '../../openapitools.json') + const originalConfig = JSON.parse(fs.readFileSync(originalConfigPath, 'utf8')) + + const tsNormalizer = originalConfig['generator-cli']?.generators?.typescript?.['openapi-normalizer'] + const hasRefAsParent = tsNormalizer?.REF_AS_PARENT_IN_ALLOF === true + + // Verify TypeScript generation handles allOf correctly + const tsModelPath = path.join(tempOutputDir, 'typescript/src/models/index.ts') + assert.strictEqual(fs.existsSync(tsModelPath), true) + + const tsContent = fs.readFileSync(tsModelPath, 'utf8') + + if (hasRefAsParent) { + // With REF_AS_PARENT_IN_ALLOF: true, TestModel should extend BaseModel + assert(tsContent.includes('interface BaseModel')) + assert(tsContent.includes('interface TestModel extends BaseModel')) + + // BaseModel should have its own properties + assert(tsContent.includes('id: string')) + + // TestModel should only have its additional properties (not duplicated base properties) + assert(tsContent.includes('name: string')) + + // TestModel should NOT duplicate the id property from BaseModel + const testModelSection = tsContent.split('interface TestModel extends BaseModel')[1] + if (testModelSection) { + assert(!testModelSection.split('interface')[0].includes('id: string')) + } + } else { + // Without REF_AS_PARENT_IN_ALLOF, properties should be flattened + assert(tsContent.includes('interface TestModel')) + assert(tsContent.includes('id: string')) + assert(tsContent.includes('name: string')) + // Should not have inheritance + assert(!tsContent.includes('extends BaseModel')) + } + }) + + it('Java flattens allOf schemas into single record (no inheritance)', () => { + // Generate code using helper function + try { + generateCodeWithConfig(allOfSchemaPath, testDir, tempOutputDir) + } catch (error) { + console.error('Generation failed for Java allOf flattening test:', error.message) + throw error + } + + // Verify Java generation handles allOf correctly + const javaModelPath = path.join( + tempOutputDir, + 'java/src/main/java/org/openapitools/client/model/TestModel.java' + ) + assert.strictEqual(fs.existsSync(javaModelPath), true) + + const javaContent = fs.readFileSync(javaModelPath, 'utf8') + + // Verify it's still using records from custom templates + assert(javaContent.includes('public record TestModel')) + assert(!javaContent.includes('public class TestModel')) + + // Java records don't support inheritance, so allOf should always flatten all properties + // regardless of REF_AS_PARENT_IN_ALLOF setting + assert(javaContent.includes('String id')) // From BaseModel + assert(javaContent.includes('String name')) // From TestModel + + // Should not have separate BaseModel record (Java flattens allOf) + const javaModelDir = path.join(tempOutputDir, 'java/src/main/java/org/openapitools/client/model') + const javaFiles = fs.readdirSync(javaModelDir).filter(f => f.endsWith('.java')) + assert.deepStrictEqual(javaFiles, ['TestModel.java']) // Only TestModel, no BaseModel + + // Verify both properties are in the same record with proper annotations + assert(javaContent.includes('@JsonProperty("id")')) + assert(javaContent.includes('@JsonProperty("name")')) + assert(javaContent.includes('@Nonnull String id')) + assert(javaContent.includes('@Nonnull String name')) + }) +}) diff --git a/types/codegen/tests/utils/test-helpers.js b/types/codegen/tests/utils/test-helpers.js new file mode 100644 index 00000000..059c089d --- /dev/null +++ b/types/codegen/tests/utils/test-helpers.js @@ -0,0 +1,121 @@ +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') + +/** + * Shared test utilities for OpenAPI code generation tests + */ + +/** + * Generate code using openapitools.json config with test-specific paths + * @param {string} schemaPath - Path to the OpenAPI schema file + * @param {string} testDir - Test directory for output + * @param {string} tempOutputDir - Temporary output directory + * @param {Object} options - Optional configuration overrides + * @param {string} options.javaModels - Models to generate for Java (default: 'TestModel') + */ +function generateCodeWithConfig(schemaPath, testDir, tempOutputDir, options = {}) { + const originalConfigPath = path.join(__dirname, '../../openapitools.json') + const testConfigPath = path.join(testDir, 'openapitools.json') + + try { + // Read the original openapitools.json + const originalConfig = JSON.parse(fs.readFileSync(originalConfigPath, 'utf8')) + + // Modify the config to use our test schema and output directories + const codegenDir = path.join(__dirname, '../..') + const testConfig = { + ...originalConfig, + 'generator-cli': { + ...originalConfig['generator-cli'], + generators: { + typescript: { + ...originalConfig['generator-cli'].generators.typescript, + inputSpec: schemaPath, + output: path.join(tempOutputDir, 'typescript'), + templateDir: path.join(codegenDir, 'custom-templates/typescript'), + }, + java: { + ...originalConfig['generator-cli'].generators.java, + inputSpec: schemaPath, + output: path.join(tempOutputDir, 'java'), + templateDir: path.join(codegenDir, 'custom-templates/java'), + 'global-property': { + ...originalConfig['generator-cli'].generators.java['global-property'], + models: options.javaModels || 'TestModel', + }, + }, + }, + }, + } + + // Write the test config + fs.writeFileSync(testConfigPath, JSON.stringify(testConfig, null, 2)) + + // Run the same command as your package.json: "openapi-generator-cli generate" + execSync('npx openapi-generator-cli generate', { + stdio: 'pipe', // Suppress output for cleaner test runs + cwd: testDir, + }) + } catch (error) { + console.error('Generation failed:', error.message) + throw error + } +} + +/** + * Create a test directory structure + * @param {string} testDir - Base test directory + */ +function createTestDirectory(testDir) { + if (!fs.existsSync(testDir)) { + fs.mkdirSync(testDir, { recursive: true }) + } +} + +/** + * Clean up test files (respects KEEP_TEST_OUTPUT env var) + * @param {string} testDir - Directory to clean up + */ +function cleanupTestFiles(testDir) { + if (!process.env.KEEP_TEST_OUTPUT && fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true, force: true }) + } +} + +/** + * Clean up temp output directory (respects KEEP_TEST_OUTPUT env var) + * @param {string} tempOutputDir - Directory to clean up + */ +function cleanupTempOutput(tempOutputDir) { + if (!process.env.KEEP_TEST_OUTPUT && fs.existsSync(tempOutputDir)) { + fs.rmSync(tempOutputDir, { recursive: true, force: true }) + } +} + +/** + * Create a basic OpenAPI schema with the given models + * @param {Object} models - Schema models to include + * @returns {Object} Complete OpenAPI schema + */ +function createBaseSchema(models) { + return { + openapi: '3.0.0', + info: { + title: 'Test Schema', + version: '1.0.0', + }, + paths: {}, + components: { + schemas: models, + }, + } +} + +module.exports = { + generateCodeWithConfig, + createTestDirectory, + cleanupTestFiles, + cleanupTempOutput, + createBaseSchema, +} diff --git a/types/codegen/tsconfig.json b/types/codegen/tsconfig.json new file mode 100644 index 00000000..b0bb1ec3 --- /dev/null +++ b/types/codegen/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "typeRoots": ["node_modules/@types"] + }, + "include": ["**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/types/index.ts b/types/index.ts index 47a239fe..394ac47c 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,7 +1,71 @@ export * from './auth' -export * from './chat' +export { + SubscriptionUpgradeParams, + CHAT_REQUEST_METHOD, + END_CHAT_REQUEST_METHOD, + QUICK_ACTION_REQUEST_METHOD, + READY_NOTIFICATION_METHOD, + FEEDBACK_NOTIFICATION_METHOD, + TAB_ADD_NOTIFICATION_METHOD, + TAB_CHANGE_NOTIFICATION_METHOD, + TAB_REMOVE_NOTIFICATION_METHOD, + INSERT_TO_CURSOR_POSITION_NOTIFICATION_METHOD, + LINK_CLICK_NOTIFICATION_METHOD, + INFO_LINK_CLICK_NOTIFICATION_METHOD, + SOURCE_LINK_CLICK_NOTIFICATION_METHOD, + FOLLOW_UP_CLICK_NOTIFICATION_METHOD, + OPEN_TAB_REQUEST_METHOD, + BUTTON_CLICK_REQUEST_METHOD, + CHAT_UPDATE_NOTIFICATION_METHOD, + FILE_CLICK_NOTIFICATION_METHOD, + INLINE_CHAT_REQUEST_METHOD, + TAB_BAR_ACTION_REQUEST_METHOD, + CHAT_OPTIONS_UPDATE_NOTIFICATION_METHOD, + PROMPT_INPUT_OPTION_CHANGE_METHOD, + OPEN_FILE_DIALOG_METHOD, + CONTEXT_COMMAND_NOTIFICATION_METHOD, + CREATE_PROMPT_NOTIFICATION_METHOD, + INLINE_CHAT_RESULT_NOTIFICATION_METHOD, + PINNED_CONTEXT_ADD_NOTIFICATION_METHOD, + PINNED_CONTEXT_REMOVE_NOTIFICATION_METHOD, + RULE_CLICK_REQUEST_METHOD, + PINNED_CONTEXT_NOTIFICATION_METHOD, + LIST_RULES_REQUEST_METHOD, + ACTIVE_EDITOR_CHANGED_NOTIFICATION_METHOD, + LIST_CONVERSATIONS_REQUEST_METHOD, + CONVERSATION_CLICK_REQUEST_METHOD, + LIST_MCP_SERVERS_REQUEST_METHOD, + MCP_SERVER_CLICK_REQUEST_METHOD, + GET_SERIALIZED_CHAT_REQUEST_METHOD, + OPEN_WORKSPACE_INDEX_SETTINGS_BUTTON_ID, + LIST_AVAILABLE_MODELS_REQUEST_METHOD, + SUBSCRIPTION_DETAILS_NOTIFICATION_METHOD, + SUBSCRIPTION_UPGRADE_NOTIFICATION_METHOD, +} from './chat' export * from './didChangeDependencyPaths' export * from './inlineCompletionWithReferences' export * from './lsp' -export * from './window' -export * from './workspace' +export { + ShowOpenDialogResult, + SHOW_SAVE_FILE_DIALOG_REQUEST_METHOD, + SHOW_OPEN_FILE_DIALOG_REQUEST_METHOD, + CHECK_DIAGNOSTICS_REQUEST_METHOD, + CheckDiagnosticsParams, + CheckDiagnosticsResult, +} from './window' +export { + SelectWorkspaceItemParams, + WorkspaceItem, + SelectWorkspaceItemResult, + DID_CREATE_DIRECTORY_NOTIFICATION_METHOD, + DID_REMOVE_FILE_OR_DIRECTORY_NOTIFICATION_METHOD, + SELECT_WORKSPACE_ITEM_REQUEST_METHOD, + OPEN_FILE_DIFF_NOTIFICATION_METHOD, + DID_COPY_FILE_NOTIFICATION_METHOD, + DID_APPEND_FILE_NOTIFICATION_METHOD, + DID_WRITE_FILE_NOTIFICATION_METHOD, + OPEN_WORKSPACE_FILE_REQUEST_METHOD, + OpenWorkspaceFileParams, + OpenWorkspaceFileResult, +} from './workspace' +export * from './codegen/generated/typescript/src'