Skip to content

Commit afebc19

Browse files
committed
Fix simplifySingleMapSchema to generate named wrapper schemas
Signed-off-by: xil <fridalu66@gmail.com>
1 parent 203886f commit afebc19

File tree

6 files changed

+209
-134
lines changed

6 files changed

+209
-134
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
### Added
77

88
### Changed
9-
9+
- Fix simplifySingleMapSchema to generate named wrapper schemas. ([#406](https://github.com/opensearch-project/opensearch-protobufs/pull/406))
1010
### Removed
1111

1212
### Fixed

tools/proto-convert/src/SchemaModifier.ts

Lines changed: 45 additions & 65 deletions
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, is_simple_ref} from './utils/helper';
4+
import {compressMultipleUnderscores, isPrimitiveType, resolveObj, isReferenceObject, isEmptyObjectSchema, is_simple_ref, toSnakeCase} from './utils/helper';
55
import logger from "./utils/logger";
66

77

@@ -21,7 +21,6 @@ export class SchemaModifier {
2121
this.convertNullTypeToNullValue(schema)
2222
this.deduplicateOneOfWithArrayType(schema)
2323
this.collapseSingleItemComposite(schema);
24-
this.removeArrayOfMapWrapper(schema)
2524
},
2625
onSchema: (schema, schemaName) => {
2726
if (!schema || isReferenceObject(schema)) return;
@@ -33,7 +32,6 @@ export class SchemaModifier {
3332
this.deduplicateOneOfWithArrayType(schema)
3433
this.collapseSingleItemComposite(schema);
3534
this.collapseOneOfObjectPropContainsTitleSchema(schema)
36-
this.removeArrayOfMapWrapper(schema)
3735
this.convertOneOfToMinMaxProperties(schema)
3836
},
3937
});
@@ -280,50 +278,70 @@ export class SchemaModifier {
280278
}
281279

282280
/**
283-
* Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into standard schema by reconstructing
284-
* the additional property definitions.
281+
* Extracts type name from $ref or title
282+
* Note: Schema names have already been sanitized by Sanitizer, so '___' prefixes are already removed
283+
**/
284+
private getTypeName(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): string | null {
285+
if ('$ref' in schema) {
286+
const parts = schema.$ref.split('/');
287+
return parts[parts.length - 1];
288+
}
289+
if ('title' in schema && schema.title) {
290+
return schema.title;
291+
}
292+
return null;
293+
}
294+
295+
/**
296+
* Transforms SchemaObject that single-key maps (`minProperties = 1` and `maxProperties = 1`) into a new wrapper schema.
297+
*
298+
*
285299
* Example:
286300
* Input:
287301
* {
288302
* type: "object",
289303
* additionalProperties: {
290-
* - ref: "#/components/schemas/Model"
304+
* $ref: "#/components/schemas/SortOrder"
291305
* },
292306
* minProperties: 1,
293307
* maxProperties: 1,
294-
* };
295-
* Model:
296-
* properties: {
297-
* properties1: string
298-
* properties2: string
299-
* }
300-
*
308+
* }
301309
*
302-
*Output:
303-
* {
304-
* ref: "#/components/schemas/Example
305-
* }
306-
*
307-
* Model:
308-
* properties: {
309-
* field: string
310-
* properties1: string
311-
* properties2: string
312-
* }
310+
* Output:
311+
* {
312+
* type: "object",
313+
* title: "SortOrderMap",
314+
* properties: {
315+
* field: { type: "string" },
316+
* sort_order: { $ref: "#/components/schemas/SortOrder" }
317+
* },
318+
* required: ["field", "sort_order"]
319+
* }
313320
*
314321
**/
315322
simplifySingleMapSchema(schema: OpenAPIV3.SchemaObject, visit: Set<any>): void {
316323
if (schema.type === 'object' && typeof schema.additionalProperties === 'object' &&
317324
!Array.isArray(schema.additionalProperties) && schema.minProperties === 1 && schema.maxProperties === 1){
318325

319-
const reconstructAdditionalPropertySchema = this.reconstructAdditionalPropertySchema(schema.additionalProperties, visit);
326+
const valueSchema = schema.additionalProperties;
327+
const typeName = this.getTypeName(valueSchema) || 'Value'; // Use 'Value' as fallback
320328

321-
Object.assign(schema, reconstructAdditionalPropertySchema)
329+
// Create new wrapper schema
330+
// Avoid collision with the reserved 'field' key property name
331+
const rawPropertyName = toSnakeCase(typeName);
332+
const valuePropertyName = rawPropertyName === 'field' ? `${rawPropertyName}_value` : rawPropertyName;
333+
const wrapperTitle = schema.title || `${typeName}Map`;
334+
335+
schema.title = wrapperTitle;
336+
schema.properties = {
337+
field: { type: 'string' as const },
338+
[valuePropertyName]: valueSchema
339+
};
340+
schema.required = ['field', valuePropertyName];
322341

323342
delete schema.additionalProperties;
324343
delete schema.minProperties;
325344
delete schema.maxProperties;
326-
delete schema.type
327345
if ('propertyNames' in schema) {
328346
delete schema.propertyNames;
329347
}
@@ -449,44 +467,6 @@ export class SchemaModifier {
449467
logger.info(`Converted additionalProperties to named property '${propertyName}' with type: object`);
450468
}
451469

452-
/**
453-
* Removes the array wrapper if the schema is an array of maps (additionalProperties).
454-
* Converts array of objects with only additionalProperties into just the additionalProperties schema.
455-
*
456-
* Example:
457-
* Input:
458-
* {
459-
* type: "array",
460-
* items: {
461-
* type: "object",
462-
* additionalProperties: {
463-
* $ref: "#/components/schemas/Value"
464-
* }
465-
* }
466-
* }
467-
*
468-
* Output:
469-
* {
470-
* type: "object",
471-
* additionalProperties: {
472-
* $ref: "#/components/schemas/Value"
473-
* }
474-
* }
475-
**/
476-
removeArrayOfMapWrapper(schema: OpenAPIV3.SchemaObject): void {
477-
if (schema.type === 'array' && schema.items && typeof schema.items === 'object' && !('$ref' in schema.items)) {
478-
const items = schema.items as OpenAPIV3.SchemaObject;
479-
480-
if (items.type === 'object' && items.additionalProperties && !items.properties) {
481-
(schema as any).type = 'object';
482-
schema.additionalProperties = items.additionalProperties;
483-
delete (schema as any).items;
484-
485-
logger.info(`Removed array wrapper from array of maps schema`);
486-
}
487-
}
488-
}
489-
490470
/**
491471
* Converts oneOf pattern with single-property objects into minProperties/maxProperties pattern.
492472
* For AggregationContainer

tools/proto-convert/src/config/spec-filter.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ excluded_schemas:
1818
- CombinedFieldsQuery
1919
- DistanceFeatureQuery
2020
- GeoPolygonQuery
21-
- GeoShapeQuery
2221
- HasChildQuery
2322
- HasParentQuery
2423
- MoreLikeThisQuery

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ export function compressMultipleUnderscores(str: string): string {
5555
return str.replace(/_+/g, '_');
5656
}
5757

58+
/**
59+
* Converts PascalCase or camelCase string to snake_case
60+
* Consecutive capitals are treated as a single word.
61+
* Example: "SortOrder" -> "sort_order", "APIResponse" -> "api_response", "HTTPSConnection" -> "https_connection"
62+
*/
63+
export function toSnakeCase(str: string): string {
64+
return str
65+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // e.g. "HTTPSCo" -> "HTTPS_Co"
66+
.replace(/([a-z\d])([A-Z])/g, '$1_$2') // e.g. "sortOrder" -> "sort_Order"
67+
.toLowerCase()
68+
.replace(/^_/, '');
69+
}
70+
5871
export function resolveObj(
5972
obj: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
6073
root: OpenAPIV3.Document

0 commit comments

Comments
 (0)