Skip to content

Commit ede2e3a

Browse files
committed
Add new property to explicitly include propertyNames and required properties in generated map.
1 parent 23aa2e2 commit ede2e3a

7 files changed

Lines changed: 89 additions & 10 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@
4040
import java.text.SimpleDateFormat;
4141
import java.util.*;
4242
import java.util.function.BiPredicate;
43+
import java.util.function.Consumer;
4344
import java.util.function.Function;
4445
import java.util.regex.Pattern;
4546
import java.util.stream.Collectors;
4647
import java.util.stream.Stream;
4748

49+
import static java.util.function.Predicate.not;
4850
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.form;
4951
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.simple;
5052
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
@@ -237,6 +239,9 @@ protected String nullableQuotedJSString(@Nullable String string) {
237239

238240
public static final String LICENSE_NAME_DEFAULT_VALUE = "Unlicense";
239241

242+
public static final String ENRICHED_MAPS = "enrichedMaps";
243+
public static final String ENRICHED_MAPS_DESC = "Set to ensure that generated maps explicitly include properties defined via the 'propertyNames' and 'required' properties.";
244+
240245
// NOTE: SimpleDateFormat is not thread-safe and may not be static unless it is thread-local
241246
@SuppressWarnings("squid:S5164")
242247
protected static final ThreadLocal<SimpleDateFormat> SNAPSHOT_SUFFIX_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMddHHmm", Locale.ROOT));
@@ -258,6 +263,8 @@ protected String nullableQuotedJSString(@Nullable String string) {
258263
protected String enumSuffix = "Enum";
259264

260265
protected String classEnumSeparator = ".";
266+
@Getter @Setter
267+
protected Boolean enrichedMaps = false;
261268

262269
@Getter() @Setter
263270
protected String licenseName = getLicenseNameDefaultValue();
@@ -373,6 +380,7 @@ public AbstractTypeScriptClientCodegen() {
373380
this.cliOptions.add(CliOption.newBoolean(SNAPSHOT,
374381
"When setting this property to true, the version will be suffixed with -SNAPSHOT." + SNAPSHOT_SUFFIX_FORMAT.get().toPattern(),
375382
false));
383+
this.cliOptions.add(CliOption.newBoolean(ENRICHED_MAPS, ENRICHED_MAPS_DESC, enrichedMaps));
376384
this.cliOptions.add(new CliOption(NULL_SAFE_ADDITIONAL_PROPS, NULL_SAFE_ADDITIONAL_PROPS_DESC).defaultValue(String.valueOf(this.getNullSafeAdditionalProps())));
377385
this.cliOptions.add(CliOption.newBoolean(ENUM_PROPERTY_NAMING_REPLACE_SPECIAL_CHAR, ENUM_PROPERTY_NAMING_REPLACE_SPECIAL_CHAR_DESC, false));
378386
cliOptions.add(new CliOption(CodegenConstants.LICENSE_NAME, CodegenConstants.LICENSE_NAME_DESC).defaultValue(this.licenseName));
@@ -426,6 +434,10 @@ public void processOpts() {
426434
if (additionalProperties.containsKey(CodegenConstants.LICENSE_NAME)) {
427435
this.setLicenseName(additionalProperties.get(CodegenConstants.LICENSE_NAME).toString());
428436
}
437+
438+
if (additionalProperties.containsKey(ENRICHED_MAPS)) {
439+
enrichedMaps = Boolean.valueOf(additionalProperties.get(ENRICHED_MAPS).toString());
440+
}
429441
}
430442

431443
@Override
@@ -651,7 +663,7 @@ public String getTypeDeclaration(Schema p) {
651663
if (Boolean.TRUE.equals(inner.getNullable())) {
652664
nullSafeSuffix += " | null";
653665
}
654-
return "{ [key: string]: " + getTypeDeclaration(unaliasSchema(inner)) + nullSafeSuffix + "; }";
666+
return getMapTypeWithExplicitlyDefinedProperties(p, getTypeDeclaration(unaliasSchema(inner)), nullSafeSuffix);
655667
} else if (ModelUtils.isFileSchema(p)) {
656668
return "File";
657669
} else if (ModelUtils.isBinarySchema(p)) {
@@ -670,7 +682,7 @@ protected String getParameterDataType(Parameter parameter, Schema p) {
670682
return this.getSchemaType(p) + "<" + this.getParameterDataType(parameter, inner) + ">";
671683
} else if (ModelUtils.isMapSchema(p)) {
672684
inner = ModelUtils.getAdditionalProperties(p);
673-
return "{ [key: string]: " + this.getParameterDataType(parameter, inner) + "; }";
685+
return getMapTypeWithExplicitlyDefinedProperties(p, this.getParameterDataType(parameter, inner), "");
674686
} else if (ModelUtils.isStringSchema(p)) {
675687
// Handle string enums
676688
if (p.getEnum() != null) {
@@ -704,6 +716,56 @@ else if (ModelUtils.isDateSchema(p)) {
704716
return this.getTypeDeclaration(p);
705717
}
706718

719+
/**
720+
* Converts a map schema to an object respecting propertyNames and required properties if defined.
721+
*
722+
* @param outer map schema
723+
* @param innerDataType type of the values of the map
724+
* @param suffix e.g. " | undefined" or " | null"
725+
* @return an object containing all explicitly defined properties as well as the index signature for string
726+
*/
727+
private String getMapTypeWithExplicitlyDefinedProperties(Schema<?> outer, String innerDataType, String suffix) {
728+
String suffixOrEmpty = Optional.ofNullable(suffix)
729+
.filter(not(String::isBlank))
730+
.orElse("");
731+
732+
if (!enrichedMaps) {
733+
return "{ [key: string]: " + innerDataType + suffixOrEmpty + "; }";
734+
}
735+
736+
List<String> requiredProperties = Optional.ofNullable(outer.getRequired()).orElse(List.of());
737+
List<String> propertyNames = Optional.ofNullable(ModelUtils.getPropertyNames(outer))
738+
.map(Schema::getEnum)
739+
.stream()
740+
.flatMap(Collection::stream)
741+
.filter(not(requiredProperties::contains))
742+
.collect(Collectors.toList());
743+
744+
// Property names are assumed to be optional, so if we have any we need to make sure that the type of the index
745+
// signature includes undefined
746+
String undefinedSuffix = !propertyNames.isEmpty() && !suffixOrEmpty.contains("undefined") ? " | undefined" : "";
747+
StringBuilder sb = new StringBuilder()
748+
.append("{ [key: string]: ")
749+
.append(innerDataType)
750+
.append(suffixOrEmpty)
751+
.append(undefinedSuffix)
752+
.append(";");
753+
754+
requiredProperties.forEach(appendProperty(sb, innerDataType + suffixOrEmpty, true));
755+
propertyNames.forEach(appendProperty(sb, innerDataType + suffixOrEmpty, false));
756+
757+
return sb.append(" }").toString();
758+
}
759+
760+
private Consumer<String> appendProperty(StringBuilder sb, String type, boolean required) {
761+
return (String propertyName) -> sb.append(" ")
762+
.append(propertyName)
763+
.append(required ? "" : "?")
764+
.append(": ")
765+
.append(type)
766+
.append(";");
767+
}
768+
707769
/**
708770
* Converts a list of strings to a literal union for representing enum values as a type.
709771
* Example output: 'available' | 'pending' | 'sold'

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptClientCodegen.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -468,14 +468,7 @@ private String getHttpLibForFramework(String object) {
468468

469469
@Override
470470
public String getTypeDeclaration(Schema p) {
471-
if (ModelUtils.isMapSchema(p)) {
472-
Schema<?> inner = getSchemaAdditionalProperties(p);
473-
String postfix = "";
474-
if (Boolean.TRUE.equals(inner.getNullable())) {
475-
postfix = " | null";
476-
}
477-
return "{ [key: string]: " + this.getTypeDeclaration(unaliasSchema(inner)) + postfix + "; }";
478-
} else if (ModelUtils.isFileSchema(p)) {
471+
if (ModelUtils.isFileSchema(p)) {
479472
return "HttpFile";
480473
} else if (ModelUtils.isBinarySchema(p)) {
481474
return "any";

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,12 @@ public static Schema getAdditionalProperties(Schema schema) {
14811481
return null;
14821482
}
14831483

1484+
public static Schema<String> getPropertyNames(Schema schema) {
1485+
// This is a raw type but according to the OpenAPI spec it's assumed it's always at least string. See
1486+
// https://json-schema.org/understanding-json-schema/reference/object#propertyNames
1487+
return (Schema<String>) schema.getPropertyNames();
1488+
}
1489+
14841490
public static Header getReferencedHeader(OpenAPI openAPI, Header header) {
14851491
if (header != null && StringUtils.isNotEmpty(header.get$ref())) {
14861492
String name = getSimpleRef(header.get$ref());

modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptAngularClientOptionsProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public Map<String, String> createOptions() {
5858
.put(TypeScriptAngularClientCodegen.SERVICE_FILE_SUFFIX, SERVICE_FILE_SUFFIX)
5959
.put(TypeScriptAngularClientCodegen.MODEL_SUFFIX, MODEL_SUFFIX)
6060
.put(TypeScriptAngularClientCodegen.MODEL_FILE_SUFFIX, MODEL_FILE_SUFFIX)
61+
.put(TypeScriptAngularClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
6162
.put(TypeScriptAngularClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
6263
.put(TypeScriptAngularClientCodegen.QUERY_PARAM_OBJECT_FORMAT, QUERY_PARAM_OBJECT_FORMAT_VALUE)
6364
.put(TypeScriptAngularClientCodegen.USE_SQUARE_BRACKETS_IN_ARRAY_NAMES, Boolean.FALSE.toString())

modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptFetchClientOptionsProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public Map<String, String> createOptions() {
5050
.put(TypeScriptFetchClientCodegen.SAGAS_AND_RECORDS, SAGAS_AND_RECORDS)
5151
.put(TypeScriptFetchClientCodegen.IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_VALUE)
5252
.put(TypeScriptFetchClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
53+
.put(TypeScriptFetchClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
5354
.put(TypeScriptFetchClientCodegen.STRING_ENUMS, STRING_ENUMS)
5455
.put(TypeScriptFetchClientCodegen.USE_SQUARE_BRACKETS_IN_ARRAY_NAMES, Boolean.FALSE.toString())
5556
.put(TypeScriptFetchClientCodegen.VALIDATION_ATTRIBUTES, Boolean.FALSE.toString())

modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptNestjsClientOptionsProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public Map<String, String> createOptions() {
5151
.put(TypeScriptNestjsClientCodegen.SERVICE_FILE_SUFFIX, SERVICE_FILE_SUFFIX)
5252
.put(TypeScriptNestjsClientCodegen.MODEL_SUFFIX, MODEL_SUFFIX)
5353
.put(TypeScriptNestjsClientCodegen.MODEL_FILE_SUFFIX, MODEL_FILE_SUFFIX)
54+
.put(TypeScriptNestjsClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
5455
.put(TypeScriptNestjsClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
5556
.put(CodegenConstants.USE_SINGLE_REQUEST_PARAMETER, USE_SINGLE_REQUEST_PARAMETER)
5657
.build();

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/TypeScriptClientCodegenTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ public void getTypeDeclarationTest() {
4747

4848
ModelUtils.setGenerateAliasAsModel(true);
4949
Assert.assertEquals(codegen.getTypeDeclaration(parentSchema), "{ [key: string]: Child; }");
50+
51+
parentSchema = new MapSchema().additionalProperties(new StringSchema());
52+
parentSchema.required(List.of("x", "y"));
53+
Assert.assertEquals(codegen.getTypeDeclaration(parentSchema), "{ [key: string]: string; }");
54+
55+
parentSchema.propertyNames(new StringSchema()._enum(List.of("y", "z")));
56+
Assert.assertEquals(codegen.getTypeDeclaration(parentSchema), "{ [key: string]: string; }");
57+
58+
codegen.additionalProperties().put("enrichedMaps", Boolean.TRUE);
59+
codegen.processOpts();
60+
parentSchema.propertyNames(new StringSchema());
61+
Assert.assertEquals(codegen.getTypeDeclaration(parentSchema), "{ [key: string]: string; x: string; y: string; }");
62+
63+
parentSchema.propertyNames(new StringSchema()._enum(List.of("y", "z")));
64+
Assert.assertEquals(codegen.getTypeDeclaration(parentSchema), "{ [key: string]: string | undefined; x: string; y: string; z?: string; }");
5065
}
5166

5267
@Test

0 commit comments

Comments
 (0)