Skip to content

Commit 4fd2fd5

Browse files
committed
feat: Add new --discriminator-casing argument to specify default casing when no mapping exists
1 parent 5be8d27 commit 4fd2fd5

7 files changed

Lines changed: 232 additions & 27 deletions

File tree

src/main/java/dev/dochia/cli/core/args/ProcessingArguments.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ public class ProcessingArguments {
128128
description = "When set to @|bold true|@, runs a minimal health check using the 'health-check' profile to verify API endpoints are reachable before full testing. Default: @|bold,underline ${DEFAULT-VALUE}|@")
129129
private boolean healthCheck;
130130

131+
@Setter
132+
@CommandLine.Option(names = {"--discriminator-casing"},
133+
description = "The casing convention used for discriminator values when no explicit enum or mapping is defined. Supported values: @|bold PascalCase|@, @|bold camelCase|@, @|bold UPPER_SNAKE_CASE|@, @|bold lower_snake_case|@, @|bold kebab-case|@, @|bold lowercase|@. Default: @|bold,underline ${DEFAULT-VALUE}|@")
134+
private String discriminatorCasing = "UPPER_SNAKE_CASE";
131135

132136

133137
/**

src/main/java/dev/dochia/cli/core/factory/PlaybookDataFactory.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ static boolean hasContent(Operation operation) {
563563

564564
private GenerationResult getRequestPayloadsSamples(MediaType mediaType, String reqSchemaName) {
565565
OpenAPIModelGenerator generator = new OpenAPIModelGenerator(globalContext, validDataFormat, processingArguments.examplesFlags(),
566-
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), REQUEST_ARRAY_SIZE);
566+
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), REQUEST_ARRAY_SIZE, processingArguments.getDiscriminatorCasing());
567567

568568
/* Event though the media type might have an example set, we still generate samples in order to properly map each field with its corresponding data type*/
569569
List<String> result = this.generateSample(reqSchemaName, generator);
@@ -687,7 +687,8 @@ private Map<String, List<String>> getResponseContentTypes(Operation operation) {
687687
private Map<String, List<String>> getResponsePayloads(Operation operation) {
688688
Map<String, List<String>> responses = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
689689
OpenAPIModelGenerator generator = new OpenAPIModelGenerator(globalContext, validDataFormat, processingArguments.examplesFlags(),
690-
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), RESPONSES_ARRAY_SIZE, processingArguments.isResolveXxxOfCombinationForResponses());
690+
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), RESPONSES_ARRAY_SIZE,
691+
processingArguments.isResolveXxxOfCombinationForResponses(), processingArguments.getDiscriminatorCasing());
691692

692693
for (String responseCode : operation.getResponses().keySet()) {
693694
List<String> openapiExamples = this.getExamplesFromApiResponseForResponseCode(operation, responseCode);
@@ -803,7 +804,7 @@ private void inlineSchemaIfNeeded(Parameter parameter) {
803804
parameter.setSchema(schema);
804805

805806
List<String> examples = this.generateSample(schema.get$ref(), new OpenAPIModelGenerator(globalContext, validDataFormat, processingArguments.examplesFlags(),
806-
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), REQUEST_ARRAY_SIZE));
807+
processingArguments.getSelfReferenceDepth(), processingArguments.isUseDefaults(), REQUEST_ARRAY_SIZE, processingArguments.getDiscriminatorCasing()));
807808

808809
schema.setExample(examples.getFirst());
809810
}

src/main/java/dev/dochia/cli/core/openapi/OpenAPIModelGenerator.java

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,21 @@
2525
import java.time.OffsetDateTime;
2626
import java.time.ZoneId;
2727
import java.time.format.DateTimeFormatter;
28-
import java.util.*;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.Base64;
31+
import java.util.Collection;
32+
import java.util.Collections;
33+
import java.util.Date;
34+
import java.util.HashMap;
35+
import java.util.HashSet;
36+
import java.util.List;
37+
import java.util.Locale;
38+
import java.util.Map;
39+
import java.util.Objects;
40+
import java.util.Optional;
41+
import java.util.Random;
42+
import java.util.Set;
2943
import java.util.function.Supplier;
3044
import java.util.stream.Collectors;
3145

@@ -76,36 +90,54 @@ public class OpenAPIModelGenerator {
7690
private final boolean resolveAnyOfAsMultipleSchema;
7791
private int currentPropertiesDepth;
7892
private final int totalDepth;
93+
private final String discriminatorCasing;
7994

8095
/**
81-
* Constructs an OpenAPIModelGeneratorV2 with the specified configuration.
96+
* Constructs an OpenAPIModelGenerator with the specified configuration.
8297
* The default value for {@code resolveAnyOfAsMultipleSchema=true}. The default value for {@code totalDepth=200}.
8398
*
84-
* @param globalContext The global context.
99+
* @param globalContext The global context for DOCHIA.
85100
* @param validDataFormat The format to use for generating valid data.
86101
* @param useExamplesArgument Flag indicating whether to use examples from the OpenAPI specification.
87102
* @param selfReferenceDepth The maximum depth for generating self-referencing models.
88103
* @param useDefaults Whether to use default values if available
89104
* @param maxArraySize The maximum size for arrays
90105
*/
91106
public OpenAPIModelGenerator(GlobalContext globalContext, ValidDataFormat validDataFormat, ProcessingArguments.ExamplesFlags useExamplesArgument, int selfReferenceDepth, boolean useDefaults, int maxArraySize) {
107+
this(globalContext, validDataFormat, useExamplesArgument, selfReferenceDepth, useDefaults, maxArraySize, "UPPER_SNAKE_CASE");
108+
}
109+
110+
/**
111+
* Constructs an OpenAPIModelGenerator with the specified configuration.
112+
* The default value for {@code resolveAnyOfAsMultipleSchema=true}. The default value for {@code totalDepth=200}.
113+
*
114+
* @param globalContext The global context for dochia.
115+
* @param validDataFormat The format to use for generating valid data.
116+
* @param useExamplesArgument Flag indicating whether to use examples from the OpenAPI specification.
117+
* @param selfReferenceDepth The maximum depth for generating self-referencing models.
118+
* @param useDefaults Whether to use default values if available
119+
* @param maxArraySize The maximum size for arrays
120+
* @param discriminatorCasing The casing convention for discriminator values
121+
*/
122+
public OpenAPIModelGenerator(GlobalContext globalContext, ValidDataFormat validDataFormat, ProcessingArguments.ExamplesFlags useExamplesArgument, int selfReferenceDepth, boolean useDefaults, int maxArraySize, String discriminatorCasing) {
92123
this.globalContext = globalContext;
93-
this.random = CommonUtils.random();
124+
this.random = DochiaRandom.instance();
94125
this.examplesFlags = useExamplesArgument;
95126
this.selfReferenceDepth = selfReferenceDepth;
96127
this.validDataFormat = validDataFormat;
97128
this.callStackCounter = new HashMap<>();
98129
this.useDefaults = useDefaults;
99130
this.maxArraySize = maxArraySize;
131+
this.discriminatorCasing = discriminatorCasing;
100132

101133
this.resolveAnyOfAsMultipleSchema = true;
102134
this.totalDepth = REQUEST_TOTAL_DEPTH;
103135
}
104136

105137
/**
106-
* Constructs an OpenAPIModelGeneratorV2 with the specified configuration. The default value for {@code totalDepth=50}
138+
* Constructs an OpenAPIModelGenerator with the specified configuration. The default value for {@code totalDepth=50}
107139
*
108-
* @param globalContext The global context.
140+
* @param globalContext The global context for DOCHIA.
109141
* @param validDataFormat The format to use for generating valid data.
110142
* @param useExamplesArgument Flag indicating whether to use examples from the OpenAPI specification.
111143
* @param selfReferenceDepth The maximum depth for generating self-referencing models.
@@ -114,15 +146,32 @@ public OpenAPIModelGenerator(GlobalContext globalContext, ValidDataFormat validD
114146
* @param maxArraySize The maximum size for arrays
115147
*/
116148
public OpenAPIModelGenerator(GlobalContext globalContext, ValidDataFormat validDataFormat, ProcessingArguments.ExamplesFlags useExamplesArgument, int selfReferenceDepth, boolean useDefaults, int maxArraySize, boolean resolveAnyOfAsMultipleSchema) {
149+
this(globalContext, validDataFormat, useExamplesArgument, selfReferenceDepth, useDefaults, maxArraySize, resolveAnyOfAsMultipleSchema, "UPPER_SNAKE_CASE");
150+
}
151+
152+
/**
153+
* Constructs an OpenAPIModelGenerator with the specified configuration. The default value for {@code totalDepth=50}
154+
*
155+
* @param globalContext The global context for DOCHIA.
156+
* @param validDataFormat The format to use for generating valid data.
157+
* @param useExamplesArgument Flag indicating whether to use examples from the OpenAPI specification.
158+
* @param selfReferenceDepth The maximum depth for generating self-referencing models.
159+
* @param resolveAnyOfAsMultipleSchema If true it will resolve all combinations of oneOf/anyOf schemas
160+
* @param useDefaults Whether to use default values if available
161+
* @param maxArraySize The maximum size for arrays
162+
* @param discriminatorCasing The casing convention for discriminator values
163+
*/
164+
public OpenAPIModelGenerator(GlobalContext globalContext, ValidDataFormat validDataFormat, ProcessingArguments.ExamplesFlags useExamplesArgument, int selfReferenceDepth, boolean useDefaults, int maxArraySize, boolean resolveAnyOfAsMultipleSchema, String discriminatorCasing) {
117165
this.globalContext = globalContext;
118-
this.random = CommonUtils.random();
166+
this.random = DochiaRandom.instance();
119167
this.examplesFlags = useExamplesArgument;
120168
this.selfReferenceDepth = selfReferenceDepth;
121169
this.validDataFormat = validDataFormat;
122170
this.callStackCounter = new HashMap<>();
123171
this.useDefaults = useDefaults;
124172
this.maxArraySize = maxArraySize;
125173
this.resolveAnyOfAsMultipleSchema = resolveAnyOfAsMultipleSchema;
174+
this.discriminatorCasing = discriminatorCasing;
126175
this.totalDepth = RESPONSE_TOTAL_DEPTH;
127176
}
128177

@@ -489,7 +538,7 @@ private List<Map<String, Object>> resolveAllOfSchemaProperties(Schema schema, St
489538

490539
String detectedCasing = parentWithDiscriminator != null
491540
? detectCasingConvention(parentWithDiscriminator, discriminatorPropertyName)
492-
: "UPPER_SNAKE_CASE";
541+
: this.discriminatorCasing;
493542
String discriminatorValue = WordUtils.convertToDetectedCasing(propertyName, detectedCasing);
494543

495544
for (Map<String, Object> example : examples) {
@@ -767,14 +816,15 @@ private String detectCasingConvention(Schema schema, String propertyName) {
767816
return WordUtils.detectCasingFromString(firstEnum);
768817
}
769818

770-
// Try to detect from discriminator mappings
771-
Optional<String> mappingValue = globalContext.getDiscriminators()
772-
.stream()
773-
.filter(discriminator -> discriminator.getPropertyName().equalsIgnoreCase(propertyName) && discriminator.getMapping() != null)
774-
.findFirst()
775-
.flatMap(discriminator -> discriminator.getMapping().keySet().stream().findFirst());
819+
// Try to detect from the schema's own discriminator mapping only
820+
if (schema.getDiscriminator() != null && schema.getDiscriminator().getMapping() != null) {
821+
Optional<String> mappingKey = schema.getDiscriminator().getMapping().keySet().stream().findFirst();
822+
if (mappingKey.isPresent()) {
823+
return WordUtils.detectCasingFromString(mappingKey.get());
824+
}
825+
}
776826

777-
return mappingValue.map(WordUtils::detectCasingFromString).orElse("UPPER_SNAKE_CASE");
827+
return this.discriminatorCasing;
778828
}
779829

780830

src/main/java/dev/dochia/cli/core/playbook/field/base/BaseFieldsPlaybook.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ private static boolean isValidDate(String value) {
277277
try {
278278
LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
279279
return true;
280-
} catch (DateTimeParseException e) {
280+
} catch (DateTimeParseException _) {
281281
return false;
282282
}
283283
}
@@ -286,7 +286,7 @@ private static boolean isValidDateTime(String value) {
286286
try {
287287
OffsetDateTime.parse(value, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
288288
return true;
289-
} catch (DateTimeParseException e) {
289+
} catch (DateTimeParseException _) {
290290
return false;
291291
}
292292
}
@@ -295,7 +295,7 @@ private static boolean isValidUuid(String value) {
295295
try {
296296
UUID.fromString(value);
297297
return true;
298-
} catch (IllegalArgumentException e) {
298+
} catch (IllegalArgumentException _) {
299299
return false;
300300
}
301301
}

src/main/java/dev/dochia/cli/core/util/WordUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ public static String detectCasingFromString(String sample) {
333333
* @return the converted string
334334
*/
335335
public static String convertToDetectedCasing(String name, String casingConvention) {
336+
if (name == null) {
337+
return null;
338+
}
339+
if (casingConvention == null) {
340+
return name;
341+
}
336342
return switch (casingConvention) {
337343
case "lower_snake_case" -> name.replaceAll("([a-z])([A-Z])", "$1_$2")
338344
.replaceAll("([A-Z])([A-Z][a-z])", "$1_$2")

0 commit comments

Comments
 (0)