Skip to content

Commit e2724cb

Browse files
committed
supporting x-protobuf-name vendor extension and simplfy array of map
1 parent c504320 commit e2724cb

4 files changed

Lines changed: 125 additions & 49 deletions

File tree

.github/workflows/convert-proto.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ jobs:
6161
run: npm ci && npm run preprocessing
6262

6363
- name: Clone Protobuf Generator Repository
64-
run: git clone https://github.com/OpenAPITools/openapi-generator cloned-repo
64+
run: |
65+
git clone https://github.com/OpenAPITools/openapi-generator cloned-repo
66+
cd cloned-repo
67+
git checkout 6699ecd9d2f4e0868f23bb36566ea03cd1230e6a
6568
6669
- name: Build Protobuf Generator Tool
6770
run: |

tools/proto-convert/src/SchemaModifier.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class SchemaModifier {
2222
this.handleAdditionalPropertiesUndefined(schema)
2323
this.convertNullTypeToNullValue(schema)
2424
this.collapseOrMergeOneOfArray(schema)
25+
this.removeArrayOfMapWrapper(schema)
2526
},
2627
onSchema: (schema, schemaName) => {
2728
if (!schema || isReferenceObject(schema)) return;
@@ -32,6 +33,7 @@ export class SchemaModifier {
3233
this.handleOneOfConst(schema, schemaName)
3334
this.collapseOrMergeOneOfArray(schema)
3435
this.collapseOneOfObjectPropContainsTitleSchema(schema)
36+
this.removeArrayOfMapWrapper(schema)
3537
},
3638
});
3739
const visit = new Set();
@@ -435,4 +437,42 @@ export class SchemaModifier {
435437

436438
this.logger.info(`Converted additionalProperties to named property '${propertyName}' with type: object`);
437439
}
440+
441+
/**
442+
* Removes the array wrapper if the schema is an array of maps (additionalProperties).
443+
* Converts array of objects with only additionalProperties into just the additionalProperties schema.
444+
*
445+
* Example:
446+
* Input:
447+
* {
448+
* type: "array",
449+
* items: {
450+
* type: "object",
451+
* additionalProperties: {
452+
* $ref: "#/components/schemas/Value"
453+
* }
454+
* }
455+
* }
456+
*
457+
* Output:
458+
* {
459+
* type: "object",
460+
* additionalProperties: {
461+
* $ref: "#/components/schemas/Value"
462+
* }
463+
* }
464+
**/
465+
removeArrayOfMapWrapper(schema: OpenAPIV3.SchemaObject): void {
466+
if (schema.type === 'array' && schema.items && typeof schema.items === 'object' && !('$ref' in schema.items)) {
467+
const items = schema.items as OpenAPIV3.SchemaObject;
468+
469+
if (items.type === 'object' && items.additionalProperties && !items.properties) {
470+
(schema as any).type = 'object';
471+
schema.additionalProperties = items.additionalProperties;
472+
delete (schema as any).items;
473+
474+
this.logger.info(`Removed array wrapper from array of maps schema`);
475+
}
476+
}
477+
}
438478
}

tools/proto-convert/src/VendorExtensionProcessor.ts

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OpenAPIV3 } from 'openapi-types';
22
import { traverse } from './utils/OpenApiTraverser';
3-
import { resolveRef } from './utils/helper';
3+
import { resolveRef, deleteMatchingKeys } from './utils/helper';
44
import Logger from './utils/logger';
55

66
/**
@@ -10,6 +10,7 @@ import Logger from './utils/logger';
1010
export class VendorExtensionProcessor {
1111
private static readonly PROTOBUF_EXCLUDED_EXTENSION = 'x-protobuf-excluded';
1212
private static readonly PROTOBUF_TYPE_EXTENSION = 'x-protobuf-type';
13+
private static readonly PROTOBUF_NAME_EXTENSION = 'x-protobuf-name';
1314

1415
private static readonly PROTOBUF_TYPE_MAPPING: Record<string, { type: string; format?: string }> = {
1516
'int32': { type: 'integer', format: 'int32' },
@@ -30,23 +31,26 @@ export class VendorExtensionProcessor {
3031

3132
/**
3233
* Process the spec by pruning anything marked with x-protobuf-excluded
33-
* Direct path-level handling + traverse for schemas only
34+
* and applying vendor extensions (x-protobuf-name, x-protobuf-type)
3435
*/
3536
public process(): OpenAPIV3.Document {
37+
deleteMatchingKeys(this.root, (item: any) => this.hasProtobufExcluded(item));
3638

37-
this.removeProtobufExcludedFromPaths();
3839
traverse(this.root, {
40+
onParameter: (param: any, name: string) => {
41+
this.applyNameOverrideToParameter(param);
42+
},
3943
onSchema: (schema: any, name: string) => {
40-
this.removeProtobufExcludedProperties(schema);
4144
this.applyTypeOverride(schema);
45+
this.applyNameOverride(schema);
4246
},
4347
onResponseSchema: (schema: any, name: string) => {
44-
this.removeProtobufExcludedProperties(schema);
4548
this.applyTypeOverride(schema);
49+
this.applyNameOverride(schema);
4650
},
4751
onRequestSchema: (schema: any, name: string) => {
48-
this.removeProtobufExcludedProperties(schema);
4952
this.applyTypeOverride(schema);
53+
this.applyNameOverride(schema);
5054
},
5155
onSchemaProperty: (schema: any, name: string) => {
5256
this.applyTypeOverride(schema);
@@ -73,55 +77,61 @@ export class VendorExtensionProcessor {
7377
}
7478

7579
/**
76-
* Remove x-protobuf-excluded items from path-level elements directly
80+
* Apply name override to a parameter if it has x-protobuf-name
7781
*/
78-
private removeProtobufExcludedFromPaths(): void {
79-
if (!this.root.paths) return;
80-
81-
for (const pathKey in this.root.paths) {
82-
const pathItem = this.root.paths[pathKey];
83-
if (!pathItem || typeof pathItem !== 'object' || '$ref' in pathItem) continue;
84-
85-
// Handle operations
86-
for (const method in pathItem) {
87-
if (method === 'parameters' || method === '$ref' || method === 'summary' ||
88-
method === 'description' || method === 'servers') continue;
89-
90-
const operation = (pathItem as any)[method];
91-
if (!operation || typeof operation !== 'object') continue;
92-
93-
// Remove parameters with x-protobuf-excluded
94-
if (Array.isArray(operation.parameters)) {
95-
const originalLength = operation.parameters.length;
96-
operation.parameters = operation.parameters.filter((p: any) => !this.hasProtobufExcluded(p));
97-
const removedCount = originalLength - operation.parameters.length;
98-
if (removedCount > 0) {
99-
this.logger.info(`Removed ${removedCount} parameter(s) from ${method.toUpperCase()} ${pathKey} (${VendorExtensionProcessor.PROTOBUF_EXCLUDED_EXTENSION})`);
100-
}
101-
}
82+
private applyNameOverrideToParameter(param: OpenAPIV3.ParameterObject): void {
83+
if (!param || typeof param !== 'object' || !(VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION in param)) return;
84+
85+
const newName = param[VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
86+
if (typeof newName === 'string' && param.name && newName !== param.name) {
87+
const oldName = param.name;
88+
param.name = newName;
89+
delete param[VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
90+
this.logger.info(`Renamed parameter '${oldName}' -> '${newName}' (${VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION})`);
91+
}
92+
}
93+
10294

103-
// Remove responses with x-protobuf-excluded
104-
if (operation.responses) {
105-
for (const status in operation.responses) {
106-
if (this.hasProtobufExcluded(operation.responses[status])) {
107-
delete operation.responses[status];
108-
this.logger.info(`Removed response ${status} from ${method.toUpperCase()} ${pathKey} (${VendorExtensionProcessor.PROTOBUF_EXCLUDED_EXTENSION})`);
109-
}
95+
/**
96+
* Apply name override to schema properties and composed schemas (oneOf, anyOf, allOf)
97+
* - For properties: renames property keys
98+
* - For composed schemas: sets title field for sub-schemas
99+
*/
100+
private applyNameOverride(schema: any): void {
101+
if (!schema || typeof schema !== 'object') return;
102+
103+
// Rename properties within schema.properties collection
104+
if (schema?.properties) {
105+
for (const prop in schema.properties) {
106+
const propSchema = schema.properties[prop];
107+
if (propSchema && typeof propSchema === 'object' && VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION in propSchema) {
108+
const newName = propSchema[VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
109+
if (typeof newName === 'string' && newName !== prop) {
110+
schema.properties[newName] = schema.properties[prop];
111+
delete schema.properties[prop];
112+
delete schema.properties[newName][VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
113+
114+
this.logger.info(`Renamed property '${prop}' -> '${newName}' (${VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION})`);
110115
}
111116
}
112117
}
113118
}
114-
}
115119

116-
private removeProtobufExcludedProperties(schema: OpenAPIV3.SchemaObject): void {
117-
if (!schema?.properties) return;
118-
119-
for (const prop in schema.properties) {
120-
const propSchema = schema.properties[prop];
121-
if (propSchema && typeof propSchema === 'object') {
122-
if (this.hasProtobufExcluded(propSchema)) {
123-
delete schema.properties[prop];
124-
this.logger.info(`Removed schema property ${prop} (${VendorExtensionProcessor.PROTOBUF_EXCLUDED_EXTENSION})`);
120+
// Set title for composed schemas (oneOf, anyOf, allOf)
121+
const composedKeys = ['allOf', 'anyOf', 'oneOf'] as const;
122+
for (const key of composedKeys) {
123+
const subschemas = schema[key];
124+
if (!Array.isArray(subschemas)) continue;
125+
126+
for (const subschema of subschemas) {
127+
if (subschema && typeof subschema === 'object' && VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION in subschema) {
128+
const titleValue = subschema[VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
129+
if (typeof titleValue === 'string') {
130+
const oldTitle = subschema.title;
131+
subschema.title = titleValue;
132+
delete subschema[VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION];
133+
this.logger.info(`Set title for ${key} sub-schema: '${oldTitle}' -> '${titleValue}' (${VendorExtensionProcessor.PROTOBUF_NAME_EXTENSION})`);
134+
}
125135
}
126136
}
127137
}
@@ -170,4 +180,5 @@ export class VendorExtensionProcessor {
170180
this.logger.info(`Applied ${VendorExtensionProcessor.PROTOBUF_TYPE_EXTENSION}: ${protoType} -> type: ${schema.type}${schema.format ? `, format: ${schema.format}` : ''}`);
171181
}
172182
}
183+
173184
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {mkdirSync, writeFileSync, readFileSync} from 'fs'
22
import {parse, visit, Document} from 'yaml'
33
import {dirname} from "path";
44
import {OpenAPIV3} from "openapi-types";
5+
import _ from 'lodash';
56

67
export function read_yaml<T = Record<string, any>> (file_path: string, exclude_schema: boolean = false): T {
78
const doc = parse(readFileSync(file_path, 'utf8'))
@@ -113,3 +114,24 @@ export function isEmptyObjectSchema(schema: OpenAPIV3.SchemaObject): boolean {
113114
export function isReferenceObject(schema: any): schema is OpenAPIV3.ReferenceObject {
114115
return schema !== null && typeof schema === 'object' && '$ref' in schema;
115116
}
117+
118+
/**
119+
* Recursively delete all items matching the given condition
120+
* This includes removing them from their parent collections and cleaning up empty arrays
121+
*/
122+
export function deleteMatchingKeys(obj: any, condition: (item: any) => boolean): void {
123+
for (const key in obj) {
124+
const item = obj[key];
125+
126+
if (_.isObject(item)) {
127+
if (condition(item)) {
128+
delete obj[key];
129+
} else {
130+
deleteMatchingKeys(item, condition);
131+
if (_.isArray(item)) {
132+
obj[key] = _.compact(item);
133+
}
134+
}
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)