Skip to content

Commit 842bdc2

Browse files
committed
Add new property to explicitly include propertyNames and required properties in generated map.
1 parent 4330b2f commit 842bdc2

9 files changed

Lines changed: 96 additions & 14 deletions

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
@@ -41,11 +41,13 @@
4141
import java.text.SimpleDateFormat;
4242
import java.util.*;
4343
import java.util.function.BiPredicate;
44+
import java.util.function.Consumer;
4445
import java.util.function.Function;
4546
import java.util.regex.Pattern;
4647
import java.util.stream.Collectors;
4748
import java.util.stream.Stream;
4849

50+
import static java.util.function.Predicate.not;
4951
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.form;
5052
import static org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen.ParameterExpander.ParamStyle.simple;
5153
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
@@ -236,6 +238,9 @@ protected String nullableQuotedJSString(@Nullable String string) {
236238
public static final String NULL_SAFE_ADDITIONAL_PROPS = "nullSafeAdditionalProps";
237239
public static final String NULL_SAFE_ADDITIONAL_PROPS_DESC = "Set to make additional properties types declare that their indexer may return undefined";
238240

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

259264
protected String classEnumSeparator = ".";
265+
@Getter @Setter
266+
protected Boolean enrichedMaps = false;
260267

261268
public AbstractTypeScriptClientCodegen() {
262269
super();
@@ -368,6 +375,7 @@ public AbstractTypeScriptClientCodegen() {
368375
this.cliOptions.add(CliOption.newBoolean(SNAPSHOT,
369376
"When setting this property to true, the version will be suffixed with -SNAPSHOT." + SNAPSHOT_SUFFIX_FORMAT.get().toPattern(),
370377
false));
378+
this.cliOptions.add(CliOption.newBoolean(ENRICHED_MAPS, ENRICHED_MAPS_DESC, enrichedMaps));
371379
this.cliOptions.add(new CliOption(NULL_SAFE_ADDITIONAL_PROPS, NULL_SAFE_ADDITIONAL_PROPS_DESC).defaultValue(String.valueOf(this.getNullSafeAdditionalProps())));
372380
this.cliOptions.add(CliOption.newBoolean(ENUM_PROPERTY_NAMING_REPLACE_SPECIAL_CHAR, ENUM_PROPERTY_NAMING_REPLACE_SPECIAL_CHAR_DESC, false));
373381
}
@@ -416,6 +424,10 @@ public void processOpts() {
416424
if (additionalProperties.containsKey(NPM_NAME)) {
417425
this.setNpmName(additionalProperties.get(NPM_NAME).toString());
418426
}
427+
428+
if (additionalProperties.containsKey(ENRICHED_MAPS)) {
429+
enrichedMaps = Boolean.valueOf(additionalProperties.get(ENRICHED_MAPS).toString());
430+
}
419431
}
420432

421433
@Override
@@ -640,7 +652,7 @@ public String getTypeDeclaration(Schema p) {
640652
if (Boolean.TRUE.equals(inner.getNullable())) {
641653
nullSafeSuffix += " | null";
642654
}
643-
return "{ [key: string]: " + getTypeDeclaration(unaliasSchema(inner)) + nullSafeSuffix + "; }";
655+
return getMapTypeWithExplicitlyDefinedProperties(p, getTypeDeclaration(unaliasSchema(inner)), nullSafeSuffix);
644656
} else if (ModelUtils.isFileSchema(p)) {
645657
return "File";
646658
} else if (ModelUtils.isBinarySchema(p)) {
@@ -659,7 +671,7 @@ protected String getParameterDataType(Parameter parameter, Schema p) {
659671
return this.getSchemaType(p) + "<" + this.getParameterDataType(parameter, inner) + ">";
660672
} else if (ModelUtils.isMapSchema(p)) {
661673
inner = ModelUtils.getAdditionalProperties(p);
662-
return "{ [key: string]: " + this.getParameterDataType(parameter, inner) + "; }";
674+
return getMapTypeWithExplicitlyDefinedProperties(p, this.getParameterDataType(parameter, inner), "");
663675
} else if (ModelUtils.isStringSchema(p)) {
664676
// Handle string enums
665677
if (p.getEnum() != null) {
@@ -693,6 +705,56 @@ else if (ModelUtils.isDateSchema(p)) {
693705
return this.getTypeDeclaration(p);
694706
}
695707

708+
/**
709+
* Converts a map schema to an object respecting propertyNames and required properties if defined.
710+
*
711+
* @param outer map schema
712+
* @param innerDataType type of the values of the map
713+
* @param suffix e.g. " | undefined" or " | null"
714+
* @return an object containing all explicitly defined properties as well as the index signature for string
715+
*/
716+
private String getMapTypeWithExplicitlyDefinedProperties(Schema<?> outer, String innerDataType, String suffix) {
717+
String suffixOrEmpty = Optional.ofNullable(suffix)
718+
.filter(not(String::isBlank))
719+
.orElse("");
720+
721+
if (!enrichedMaps) {
722+
return "{ [key: string]: " + innerDataType + suffixOrEmpty + "; }";
723+
}
724+
725+
List<String> requiredProperties = Optional.ofNullable(outer.getRequired()).orElse(List.of());
726+
List<String> propertyNames = Optional.ofNullable(ModelUtils.getPropertyNames(outer))
727+
.map(Schema::getEnum)
728+
.stream()
729+
.flatMap(Collection::stream)
730+
.filter(not(requiredProperties::contains))
731+
.collect(Collectors.toList());
732+
733+
// Property names are assumed to be optional, so if we have any we need to make sure that the type of the index
734+
// signature includes undefined
735+
String undefinedSuffix = !propertyNames.isEmpty() && !suffixOrEmpty.contains("undefined") ? " | undefined" : "";
736+
StringBuilder sb = new StringBuilder()
737+
.append("{ [key: string]: ")
738+
.append(innerDataType)
739+
.append(suffixOrEmpty)
740+
.append(undefinedSuffix)
741+
.append(";");
742+
743+
requiredProperties.forEach(appendProperty(sb, innerDataType + suffixOrEmpty, true));
744+
propertyNames.forEach(appendProperty(sb, innerDataType + suffixOrEmpty, false));
745+
746+
return sb.append(" }").toString();
747+
}
748+
749+
private Consumer<String> appendProperty(StringBuilder sb, String type, boolean required) {
750+
return (String propertyName) -> sb.append(" ")
751+
.append(propertyName)
752+
.append(required ? "" : "?")
753+
.append(": ")
754+
.append(type)
755+
.append(";");
756+
}
757+
696758
/**
697759
* Converts a list of strings to a literal union for representing enum values as a type.
698760
* 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
@@ -458,14 +458,7 @@ private String getHttpLibForFramework(String object) {
458458

459459
@Override
460460
public String getTypeDeclaration(Schema p) {
461-
if (ModelUtils.isMapSchema(p)) {
462-
Schema<?> inner = getSchemaAdditionalProperties(p);
463-
String postfix = "";
464-
if (Boolean.TRUE.equals(inner.getNullable())) {
465-
postfix = " | null";
466-
}
467-
return "{ [key: string]: " + this.getTypeDeclaration(unaliasSchema(inner)) + postfix + "; }";
468-
} else if (ModelUtils.isFileSchema(p)) {
461+
if (ModelUtils.isFileSchema(p)) {
469462
return "HttpFile";
470463
} else if (ModelUtils.isBinarySchema(p)) {
471464
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
@@ -1465,6 +1465,12 @@ public static Schema getAdditionalProperties(Schema schema) {
14651465
return null;
14661466
}
14671467

1468+
public static Schema<String> getPropertyNames(Schema schema) {
1469+
// This is a raw type but according to the OpenAPI spec it's assumed it's always at least string. See
1470+
// https://json-schema.org/understanding-json-schema/reference/object#propertyNames
1471+
return (Schema<String>) schema.getPropertyNames();
1472+
}
1473+
14681474
public static Header getReferencedHeader(OpenAPI openAPI, Header header) {
14691475
if (header != null && StringUtils.isNotEmpty(header.get$ref())) {
14701476
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
@@ -87,6 +87,7 @@ public Map<String, String> createOptions() {
8787
.put(TypeScriptAngularClientCodegen.SERVICE_FILE_SUFFIX, SERVICE_FILE_SUFFIX)
8888
.put(TypeScriptAngularClientCodegen.MODEL_SUFFIX, MODEL_SUFFIX)
8989
.put(TypeScriptAngularClientCodegen.MODEL_FILE_SUFFIX, MODEL_FILE_SUFFIX)
90+
.put(TypeScriptAngularClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
9091
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
9192
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
9293
.put(TypeScriptAngularClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public Map<String, String> createOptions() {
6262
.put(TypeScriptAureliaClientCodegen.NPM_NAME, NPM_NAME)
6363
.put(TypeScriptAureliaClientCodegen.NPM_VERSION, NPM_VERSION)
6464
.put(TypeScriptAureliaClientCodegen.SNAPSHOT, Boolean.FALSE.toString())
65+
.put(TypeScriptAureliaClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
6566
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
6667
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
6768
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")

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
@@ -79,6 +79,7 @@ public Map<String, String> createOptions() {
7979
.put(TypeScriptFetchClientCodegen.SAGAS_AND_RECORDS, SAGAS_AND_RECORDS)
8080
.put(TypeScriptFetchClientCodegen.IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_VALUE)
8181
.put(TypeScriptFetchClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
82+
.put(TypeScriptFetchClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
8283
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
8384
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
8485
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")

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
@@ -83,6 +83,7 @@ public Map<String, String> createOptions() {
8383
.put(TypeScriptNestjsClientCodegen.SERVICE_FILE_SUFFIX, SERVICE_FILE_SUFFIX)
8484
.put(TypeScriptNestjsClientCodegen.MODEL_SUFFIX, MODEL_SUFFIX)
8585
.put(TypeScriptNestjsClientCodegen.MODEL_FILE_SUFFIX, MODEL_FILE_SUFFIX)
86+
.put(TypeScriptNestjsClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
8687
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
8788
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
8889
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.openapitools.codegen.CodegenConstants;
2222
import org.openapitools.codegen.languages.AbstractTypeScriptClientCodegen;
2323
import org.openapitools.codegen.languages.TypeScriptAngularClientCodegen;
24+
import org.openapitools.codegen.languages.TypeScriptNodeClientCodegen;
2425

2526
import java.util.Map;
2627

@@ -62,10 +63,11 @@ public Map<String, String> createOptions() {
6263
.put(CodegenConstants.ENUM_PROPERTY_NAMING, ENUM_PROPERTY_NAMING_VALUE)
6364
.put(CodegenConstants.MODEL_PROPERTY_NAMING, MODEL_PROPERTY_NAMING_VALUE)
6465
.put(CodegenConstants.PARAM_NAMING, PARAM_NAMING_VALUE)
65-
.put(TypeScriptAngularClientCodegen.NPM_NAME, NMP_NAME)
66-
.put(TypeScriptAngularClientCodegen.NPM_VERSION, NMP_VERSION)
67-
.put(TypeScriptAngularClientCodegen.SNAPSHOT, Boolean.FALSE.toString())
68-
.put(TypeScriptAngularClientCodegen.NPM_REPOSITORY, NPM_REPOSITORY)
66+
.put(TypeScriptNodeClientCodegen.NPM_NAME, NMP_NAME)
67+
.put(TypeScriptNodeClientCodegen.NPM_VERSION, NMP_VERSION)
68+
.put(TypeScriptNodeClientCodegen.SNAPSHOT, Boolean.FALSE.toString())
69+
.put(TypeScriptNodeClientCodegen.NPM_REPOSITORY, NPM_REPOSITORY)
70+
.put(TypeScriptNodeClientCodegen.ENRICHED_MAPS, Boolean.FALSE.toString())
6971
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
7072
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
7173
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")

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
@@ -50,6 +50,21 @@ public void getTypeDeclarationTest() {
5050

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

5570
@Test

0 commit comments

Comments
 (0)