Skip to content

Commit 8ff07a2

Browse files
committed
Fix simplifySingleMapSchema to generate named wrapper schemas
1 parent 203886f commit 8ff07a2

6 files changed

Lines changed: 329 additions & 165 deletions

File tree

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
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
Lines changed: 120 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,129 @@
1-
# Target operation groups to include in proto generation
2-
x-operation-groups:
3-
- bulk
4-
- search
5-
61
# Schemas to exclude from proto generation
72
# These schemas and their nested dependencies will not be included
83
excluded_schemas:
9-
- AggregationContainer
10-
- Aggregate
114
- Suggester
125
- Suggest
136
- DisMaxQuery
14-
- QueryStringQuery
15-
- IntervalsQuery
16-
- ScriptScoreQuery
17-
- CommonTermsQuery
18-
- CombinedFieldsQuery
19-
- DistanceFeatureQuery
20-
- GeoPolygonQuery
21-
- GeoShapeQuery
22-
- HasChildQuery
23-
- HasParentQuery
24-
- MoreLikeThisQuery
25-
- NeuralQuery
26-
- ParentIdQuery
27-
- PercolateQuery
28-
- RankFeatureQuery
29-
- SpanContainingQuery
30-
- SpanFieldMaskingQuery
31-
- SpanFirstQuery
32-
- SpanMultiTermQuery
33-
- SpanNearQuery
34-
- SpanNotQuery
35-
- SpanOrQuery
36-
- SpanTermQuery
37-
- SpanWithinQuery
38-
- ObjectMap
397
- TypeQuery
408
- WrapperQuery
419
- XyShapeQuery
10+
11+
# AggregationContainer - exclude all except TermsAggregation
12+
- AdjacencyMatrixAggregation
13+
- AutoDateHistogramAggregation
14+
- AverageAggregation
15+
- AverageBucketAggregation
16+
- BoxplotAggregation
17+
- BucketScriptAggregation
18+
- BucketSelectorAggregation
19+
- BucketSortAggregation
20+
- CardinalityAggregation
21+
- ChildrenAggregation
22+
- CompositeAggregation
23+
- CumulativeCardinalityAggregation
24+
- CumulativeSumAggregation
25+
- DateHistogramAggregation
26+
- DateRangeAggregation
27+
- DerivativeAggregation
28+
- DiversifiedSamplerAggregation
29+
- ExtendedStatsAggregation
30+
- ExtendedStatsBucketAggregation
31+
- FiltersAggregation
32+
- GeoBoundsAggregation
33+
- GeoCentroidAggregation
34+
- GeoDistanceAggregation
35+
- GeoHashGridAggregation
36+
- GeoTileGridAggregation
37+
- GlobalAggregation
38+
- HistogramAggregation
39+
- IpRangeAggregation
40+
- MatrixStatsAggregation
41+
- MaxBucketAggregation
42+
- MedianAbsoluteDeviationAggregation
43+
- MinBucketAggregation
44+
- MissingAggregation
45+
- MovingAverageAggregation
46+
- MovingFunctionAggregation
47+
- MovingPercentilesAggregation
48+
- MultiTermsAggregation
49+
- NestedAggregation
50+
- NormalizeAggregation
51+
- ParentAggregation
52+
- PercentileRanksAggregation
53+
- PercentilesAggregation
54+
- PercentilesBucketAggregation
55+
- RangeAggregation
56+
- RareTermsAggregation
57+
- RateAggregation
58+
- ReverseNestedAggregation
59+
- SamplerAggregation
60+
- ScriptedMetricAggregation
61+
- SerialDifferencingAggregation
62+
- SignificantTermsAggregation
63+
- SignificantTextAggregation
64+
- StatsAggregation
65+
- StatsBucketAggregation
66+
- SumAggregation
67+
- SumBucketAggregation
68+
- TopHitsAggregation
69+
- TTestAggregation
70+
- ValueCountAggregation
71+
- VariableWidthHistogramAggregation
72+
- WeightedAverageAggregation
73+
74+
# Aggregate - exclude all except TermsAggregate variants (DoubleTerms, LongTerms, StringTerms, UnmappedTerms)
75+
- AdjacencyMatrixAggregate
76+
- AutoDateHistogramAggregate
77+
- AvgAggregate
78+
- BoxPlotAggregate
79+
- BucketMetricValueAggregate
80+
- CardinalityAggregate
81+
- ChildrenAggregate
82+
- CompositeAggregate
83+
- CumulativeCardinalityAggregate
84+
- DateHistogramAggregate
85+
- DateRangeAggregate
86+
- DerivativeAggregate
87+
- ExtendedStatsAggregate
88+
- ExtendedStatsBucketAggregate
89+
- FilterAggregate
90+
- FiltersAggregate
91+
- GeoBoundsAggregate
92+
- GeoCentroidAggregate
93+
- GeoDistanceAggregate
94+
- GeoHashGridAggregate
95+
- GeoTileGridAggregate
96+
- GlobalAggregate
97+
- HdrPercentileRanksAggregate
98+
- HdrPercentilesAggregate
99+
- HistogramAggregate
100+
- IpRangeAggregate
101+
- LongRareTermsAggregate
102+
- MatrixStatsAggregate
103+
- MedianAbsoluteDeviationAggregate
104+
- MissingAggregate
105+
- MultiTermsAggregate
106+
- NestedAggregate
107+
- ParentAggregate
108+
- PercentilesBucketAggregate
109+
- RangeAggregate
110+
- RateAggregate
111+
- ReverseNestedAggregate
112+
- SamplerAggregate
113+
- ScriptedMetricAggregate
114+
- SignificantLongTermsAggregate
115+
- SignificantStringTermsAggregate
116+
- SimpleValueAggregate
117+
- StatsAggregate
118+
- StatsBucketAggregate
119+
- StringRareTermsAggregate
120+
- SumAggregate
121+
- TDigestPercentileRanksAggregate
122+
- TDigestPercentilesAggregate
123+
- TopHitsAggregate
124+
- TTestAggregate
125+
- UnmappedRareTermsAggregate
126+
- UnmappedSignificantTermsAggregate
127+
- ValueCountAggregate
128+
- VariableWidthHistogramAggregate
129+
- WeightedAvgAggregate

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)