Skip to content

Commit 5f299eb

Browse files
committed
preprocessing - Support minProperties=1 and maxProperties=1 constraints by marking them as oneof for protobuf generation
Signed-off-by: xil <fridalu66@gmail.com>
1 parent 4660202 commit 5f299eb

7 files changed

Lines changed: 125 additions & 8 deletions

File tree

.github/workflows/convert-proto.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
jobs:
1111
auto-proto-convert:
1212
runs-on: ubuntu-latest
13-
if: github.repository == 'opensearch-project/opensearch-protobufs'
13+
#if: github.repository == 'opensearch-project/opensearch-protobufs'
1414
steps:
1515
- name: Checkout Repository
1616
uses: actions/checkout@v4
@@ -34,11 +34,11 @@ jobs:
3434
- name: Download Release Assets
3535
uses: robinraju/release-downloader@v1
3636
with:
37-
repository: 'opensearch-project/opensearch-api-specification'
38-
latest: true
39-
fileName: 'opensearch-openapi.yaml'
40-
tag: 'main-latest'
41-
preRelease: true
37+
repository: 'opensearch-project/opensearch-api-specification'
38+
latest: true
39+
fileName: 'opensearch-openapi.yaml'
40+
tag: 'main-latest'
41+
preRelease: true
4242

4343
- name: Get Latest Commit ID
4444
id: get_commit
@@ -126,7 +126,7 @@ jobs:
126126

127127
- name: Reformat proto files
128128
run: |
129-
buf format -w protos/generated
129+
buf format -w protos/generated
130130
131131
- name: Configure Git User
132132
run: |

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
### Added
77
- Preprocessing - Support x-protobuf-name overrides existing property and parameter name ([#306](https://github.com/opensearch-project/opensearch-protobufs/pull/306))
88
- Preprocessing - Handling spec added/deprecated versioning.([#309](https://github.com/opensearch-project/opensearch-protobufs/pull/309))
9+
- preprocessing - Support minProperties=1 and maxProperties=1 constraints by marking them as `oneof` for protobuf generation ([#317](https://github.com/opensearch-project/opensearch-protobufs/pull/317))
910

1011
### Changed
1112

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"devDependencies": {
5959
"@types/jest": "^29.5.14",
6060
"@types/lodash.isequal": "^4.5.8",
61+
"@types/semver": "^7.7.1",
6162
"jest": "^29.7.0",
6263
"ts-jest": "^29.3.0",
6364
"typescript": "^5.8.2"

tools/proto-convert/src/SchemaModifier.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type {OpenAPIV3} from "openapi-types";
22
import {traverse} from './utils/OpenApiTraverser';
33
import isEqual from 'lodash.isequal';
4-
import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema} from './utils/helper';
4+
import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema, is_simple_ref} from './utils/helper';
55
import Logger from "./utils/logger";
66

77

@@ -47,6 +47,7 @@ export class SchemaModifier {
4747
if (!schema || isReferenceObject(schema)) return;
4848
this.simplifySingleMapSchema(schema, visit)
4949
this.handleAdditionalPropertiesUndefined(schema)
50+
this.markOneOfExtensions(schema);
5051
},
5152
});
5253
return this.root
@@ -475,4 +476,85 @@ export class SchemaModifier {
475476
}
476477
}
477478
}
479+
480+
/**
481+
* Marks schemas and properties with oneOf extensions.
482+
* Adds x-oneof-property to properties when schema has minProperties=1 and maxProperties=1.
483+
* Adds x-oneof-schema to schemas that have the min/max pattern AND to parent schemas
484+
* that contain nested schemas with the pattern.
485+
**/
486+
markOneOfExtensions(schema: OpenAPIV3.SchemaObject): void {
487+
const hasDirectPattern = schema.minProperties === 1 && schema.maxProperties === 1;
488+
const hasNestedPattern = this.hasNestedOneOfPattern(schema);
489+
490+
if (!hasDirectPattern && !hasNestedPattern) {
491+
return;
492+
}
493+
494+
if (hasDirectPattern) {
495+
if (schema.properties) {
496+
for (const propName in schema.properties) {
497+
const prop = schema.properties[propName] as any;
498+
if (prop && typeof prop === 'object') {
499+
prop['x-oneof-property'] = true;
500+
501+
if ('$ref' in prop) {
502+
this.markReferencedSchemaAsOneof(prop.$ref);
503+
}
504+
}
505+
}
506+
}
507+
(schema as any)['x-oneof-schema'] = true;
508+
this.logger.info(`Added x-oneof-property to properties and marked schema with x-oneof-schema`);
509+
} else if (hasNestedPattern) {
510+
(schema as any)['x-oneof-schema'] = true;
511+
this.logger.info(`Marked parent schema with x-oneof-schema (contains nested oneOf pattern)`);
512+
}
513+
}
514+
515+
/**
516+
* Recursively marks $ref schemas that are part of a oneOf, but stops when reaching a schema with actual content.
517+
**/
518+
private markReferencedSchemaAsOneof(ref: string, visited: Set<string> = new Set()): void {
519+
if (visited.has(ref)) {
520+
return;
521+
}
522+
visited.add(ref);
523+
524+
const schemaName = ref.split('/').pop();
525+
if (!schemaName || !this.root.components?.schemas) {
526+
return;
527+
}
528+
529+
const schema = this.root.components.schemas[schemaName];
530+
if (!schema) {
531+
return;
532+
}
533+
534+
if (is_simple_ref(schema)) {
535+
(schema as any)['x-oneof-property'] = true;
536+
this.markReferencedSchemaAsOneof((schema as any).$ref, visited);
537+
}
538+
}
539+
540+
/**
541+
* Checks if schema has nested items (in allOf/anyOf/oneOf) with oneOf pattern.
542+
**/
543+
private hasNestedOneOfPattern(schema: OpenAPIV3.SchemaObject): boolean {
544+
const composedKeys = ['allOf', 'anyOf', 'oneOf'] as const;
545+
for (const key of composedKeys) {
546+
const items = schema[key];
547+
if (Array.isArray(items)) {
548+
for (const item of items) {
549+
if (item && typeof item === 'object' && !('$ref' in item)) {
550+
const itemSchema = item as any;
551+
if (itemSchema.minProperties === 1 && itemSchema.maxProperties === 1) {
552+
return true;
553+
}
554+
}
555+
}
556+
}
557+
}
558+
return false;
559+
}
478560
}

tools/proto-convert/src/config/protobuf-schema-template/model.mustache

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import "{{{import}}}.proto";
3030
{{/oneOf}}
3131
{{^oneOf}}
3232
{{#vars}}
33+
{{^vendorExtensions.x-oneof-property}}
3334
{{#description}}
3435
// {{{.}}}
3536
{{/description}}
@@ -48,7 +49,20 @@ import "{{{import}}}.proto";
4849
{{enumName}} {{name}} = {{vendorExtensions.x-protobuf-index}};
4950
{{/isEnum}}
5051

52+
{{/vendorExtensions.x-oneof-property}}
53+
{{/vars}}
54+
{{#vendorExtensions.x-oneof-schema}}
55+
oneof {{classVarName}} {
56+
{{#vars}}
57+
{{#vendorExtensions.x-oneof-property}}
58+
{{#description}}
59+
// {{{.}}}
60+
{{/description}}
61+
{{#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}};
62+
{{/vendorExtensions.x-oneof-property}}
5163
{{/vars}}
64+
}
65+
{{/vendorExtensions.x-oneof-schema}}
5266
{{/oneOf}}
5367
}
5468
{{/isEnum}}

tools/proto-convert/src/utils/helper.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,14 @@ export function remove_unused(spec: OpenAPIV3.Document): void {
200200
obj.$ref !== undefined && !_.includes(remaining, obj.$ref)
201201
);
202202
}
203+
204+
/**
205+
* Checks if a schema object is a simple $ref with no other properties.
206+
*/
207+
export function is_simple_ref(schema: any): boolean {
208+
if (!schema || typeof schema !== 'object') {
209+
return false;
210+
}
211+
const keys = Object.keys(schema);
212+
return keys.length === 1 && '$ref' in schema;
213+
}

0 commit comments

Comments
 (0)