Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/convert-proto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ jobs:
- name: Download Release Assets
uses: robinraju/release-downloader@v1
with:
repository: 'opensearch-project/opensearch-api-specification'
latest: true
fileName: 'opensearch-openapi.yaml'
tag: 'main-latest'
preRelease: true
repository: 'opensearch-project/opensearch-api-specification'
latest: true
fileName: 'opensearch-openapi.yaml'
tag: 'main-latest'
preRelease: true

- name: Get Latest Commit ID
id: get_commit
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:

- name: Reformat proto files
run: |
buf format -w protos/generated
buf format -w protos/generated

- name: Configure Git User
run: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
### Added
- Preprocessing - Support x-protobuf-name overrides existing property and parameter name ([#306](https://github.com/opensearch-project/opensearch-protobufs/pull/306))
- Preprocessing - Handling spec added/deprecated versioning.([#309](https://github.com/opensearch-project/opensearch-protobufs/pull/309))
- preprocessing - Support maxProperties=1 constraints by marking them as `oneof` for protobuf generation ([#317](https://github.com/opensearch-project/opensearch-protobufs/pull/317))

### Changed

Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/lodash.isequal": "^4.5.8",
"@types/semver": "^7.7.1",
"jest": "^29.7.0",
"ts-jest": "^29.3.0",
"typescript": "^5.8.2"
Expand Down
83 changes: 82 additions & 1 deletion tools/proto-convert/src/SchemaModifier.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {OpenAPIV3} from "openapi-types";
import {traverse} from './utils/OpenApiTraverser';
import isEqual from 'lodash.isequal';
import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema} from './utils/helper';
import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema, is_simple_ref} from './utils/helper';
import Logger from "./utils/logger";


Expand Down Expand Up @@ -47,6 +47,7 @@ export class SchemaModifier {
if (!schema || isReferenceObject(schema)) return;
this.simplifySingleMapSchema(schema, visit)
this.handleAdditionalPropertiesUndefined(schema)
this.markOneOfExtensions(schema);
},
});
return this.root
Expand Down Expand Up @@ -475,4 +476,84 @@ export class SchemaModifier {
}
}
}

/**
* Marks schemas and properties with oneOf extensions.
* Adds x-oneof-property to properties when schema has maxProperties=1.
* Adds x-oneof-schema to schemas that have the max pattern AND to parent schemas
**/
markOneOfExtensions(schema: OpenAPIV3.SchemaObject): void {
const hasDirectPattern = schema.maxProperties === 1;
const hasNestedPattern = this.hasNestedOneOfPattern(schema);

if (!hasDirectPattern && !hasNestedPattern) {
return;
}

if (hasDirectPattern) {
if (schema.properties) {
for (const propName in schema.properties) {
const prop = schema.properties[propName] as any;
if (prop && typeof prop === 'object') {
prop['x-oneof-property'] = true;

if ('$ref' in prop) {
this.markReferencedSchemaAsOneof(prop.$ref);
}
}
}
}
(schema as any)['x-oneof-schema'] = true;
this.logger.info(`Added x-oneof-property to properties and marked schema with x-oneof-schema`);
} else if (hasNestedPattern) {
(schema as any)['x-oneof-schema'] = true;
this.logger.info(`Marked parent schema with x-oneof-schema (contains nested oneOf pattern)`);
}
}

/**
* Recursively marks $ref schemas that are part of a oneOf, but stops when reaching a schema with actual content.
**/
private markReferencedSchemaAsOneof(ref: string, visited: Set<string> = new Set()): void {
if (visited.has(ref)) {
return;
}
visited.add(ref);

const schemaName = ref.split('/').pop();
if (!schemaName || !this.root.components?.schemas) {
return;
}

const schema = this.root.components.schemas[schemaName];
if (!schema) {
return;
}

if (is_simple_ref(schema)) {
(schema as any)['x-oneof-property'] = true;
this.markReferencedSchemaAsOneof((schema as any).$ref, visited);
}
}

/**
* Checks if schema has nested items (in allOf/anyOf/oneOf) with oneOf pattern.
**/
private hasNestedOneOfPattern(schema: OpenAPIV3.SchemaObject): boolean {
const composedKeys = ['allOf', 'anyOf', 'oneOf'] as const;
for (const key of composedKeys) {
const items = schema[key];
if (Array.isArray(items)) {
for (const item of items) {
if (item && typeof item === 'object' && !('$ref' in item)) {
const itemSchema = item as any;
if (itemSchema.maxProperties === 1) {
return true;
}
}
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import "{{{import}}}.proto";
{{/oneOf}}
{{^oneOf}}
{{#vars}}
{{^vendorExtensions.x-oneof-property}}
{{#description}}
// {{{.}}}
{{/description}}
Expand All @@ -48,7 +49,20 @@ import "{{{import}}}.proto";
{{enumName}} {{name}} = {{vendorExtensions.x-protobuf-index}};
{{/isEnum}}

{{/vendorExtensions.x-oneof-property}}
{{/vars}}
{{#vendorExtensions.x-oneof-schema}}
oneof {{classVarName}} {
{{#vars}}
{{#vendorExtensions.x-oneof-property}}
{{#description}}
// {{{.}}}
{{/description}}
{{#vendorExtensions.x-protobuf-type}}{{{.}}} {{/vendorExtensions.x-protobuf-type}}{{{vendorExtensions.x-protobuf-data-type}}} {{{name}}} = {{vendorExtensions.x-protobuf-index}}{{#vendorExtensions.x-protobuf-packed}} [packed=true]{{/vendorExtensions.x-protobuf-packed}}{{#vendorExtensions.x-protobuf-json-name}} [json_name="{{vendorExtensions.x-protobuf-json-name}}"]{{/vendorExtensions.x-protobuf-json-name}};
{{/vendorExtensions.x-oneof-property}}
{{/vars}}
}
{{/vendorExtensions.x-oneof-schema}}
{{/oneOf}}
}
{{/isEnum}}
Expand Down
11 changes: 11 additions & 0 deletions tools/proto-convert/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,14 @@ export function remove_unused(spec: OpenAPIV3.Document): void {
obj.$ref !== undefined && !_.includes(remaining, obj.$ref)
);
}

/**
* Checks if a schema object is a simple $ref with no other properties.
*/
export function is_simple_ref(schema: any): boolean {
if (!schema || typeof schema !== 'object') {
return false;
}
const keys = Object.keys(schema);
return keys.length === 1 && '$ref' in schema;
}