Skip to content

Commit 5337ae1

Browse files
committed
fix: Fix for #168 - When uniqueItems is true, generate unique array elements
1 parent 79bc24a commit 5337ae1

File tree

3 files changed

+67
-21
lines changed

3 files changed

+67
-21
lines changed

src/main/java/com/endava/cats/openapi/OpenAPIModelGeneratorV2.java

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,7 @@
2424
import java.time.OffsetDateTime;
2525
import java.time.ZoneId;
2626
import java.time.format.DateTimeFormatter;
27-
import java.util.ArrayList;
28-
import java.util.Arrays;
29-
import java.util.Base64;
30-
import java.util.Collection;
31-
import java.util.Collections;
32-
import java.util.Date;
33-
import java.util.HashMap;
34-
import java.util.List;
35-
import java.util.Locale;
36-
import java.util.Map;
37-
import java.util.Objects;
38-
import java.util.Optional;
39-
import java.util.Random;
40-
import java.util.Set;
27+
import java.util.*;
4128
import java.util.function.Supplier;
4229
import java.util.stream.Collectors;
4330

@@ -545,10 +532,7 @@ private List<Object> resolvePropertyToExamples(String propertyName, Schema prope
545532
recordRequestSchema(currentProperty, property);
546533

547534
if (CatsModelUtils.isArraySchema(property)) {
548-
Schema itemSchema = CatsModelUtils.getSchemaItems(property);
549-
List<Object> itemExamples = resolvePropertyToExamples(propertyName + ".items", itemSchema);
550-
int arraySize = getArrayLength(property);
551-
examples.addAll(itemExamples.stream().map(innerExample -> Collections.nCopies(arraySize, innerExample)).toList());
535+
createExamplesArray(propertyName, property, examples);
552536
} else if (CatsModelUtils.isMapSchema(property)) {
553537
Map<String, Object> mapExample = new HashMap<>();
554538
Schema additionalProperties = CatsModelUtils.getAdditionalProperties(property);
@@ -585,6 +569,25 @@ private List<Object> resolvePropertyToExamples(String propertyName, Schema prope
585569
return examples;
586570
}
587571

572+
private void createExamplesArray(String propertyName, Schema property, List<Object> examples) {
573+
if (Boolean.TRUE.equals(property.getUniqueItems())) {
574+
logger.trace("Creating unique items array for property {}", propertyName);
575+
int arraySize = getArrayLength(property);
576+
Schema itemSchema = CatsModelUtils.getSchemaItems(property);
577+
Set<Object> itemExamples = new HashSet<>();
578+
while (itemExamples.size() < arraySize) {
579+
Object itemExample = resolvePropertyToExample(propertyName + ".items", itemSchema, false);
580+
itemExamples.add(itemExample);
581+
}
582+
examples.add(itemExamples);
583+
} else {
584+
Schema itemSchema = CatsModelUtils.getSchemaItems(property);
585+
List<Object> itemExamples = resolvePropertyToExamples(propertyName + ".items", itemSchema);
586+
int arraySize = getArrayLength(property);
587+
examples.addAll(itemExamples.stream().map(innerExample -> Collections.nCopies(arraySize, innerExample)).toList());
588+
}
589+
}
590+
588591
private void mapDiscriminator(Schema<?> composedSchema, List<Schema> anyOf) {
589592
if (composedSchema.getDiscriminator() != null) {
590593
logger.trace("Mapping discriminator for schema {}", composedSchema.getName());
@@ -731,8 +734,17 @@ private <T> Object generateStringValue(String propertyName, Schema<T> propertySc
731734

732735
private <T> Object getEnumOrDefault(Schema<T> propertySchema) {
733736
List<T> enumValues = propertySchema.getEnum();
737+
734738
if (!CollectionUtils.isEmpty(enumValues)) {
735-
return enumValues.stream().filter(Objects::nonNull).findAny().orElse(enumValues.getFirst());
739+
List<T> nonNullEnumValues = enumValues.stream()
740+
.filter(Objects::nonNull)
741+
.toList();
742+
743+
if (!nonNullEnumValues.isEmpty()) {
744+
return nonNullEnumValues.get(CatsUtil.random().nextInt(nonNullEnumValues.size()));
745+
}
746+
747+
return enumValues.getFirst(); // fallback if all were null
736748
}
737749

738750
if (propertySchema.getDefault() != null && useDefaults) {
@@ -791,10 +803,10 @@ private List<Map<String, Object>> combineExampleLists(List<Map<String, Object>>
791803
return combined;
792804
}
793805

794-
private Object resolvePropertyToExample(String propertyName, Schema propertySchema) {
806+
private Object resolvePropertyToExample(String propertyName, Schema propertySchema, boolean useExamples) {
795807
logger.trace("resolvePropertyToExample for property {}", propertyName);
796808
//examples will take first priority
797-
Object example = this.extractExampleFromSchema(propertySchema, examplesFlags.usePropertyExamples());
809+
Object example = this.extractExampleFromSchema(propertySchema, useExamples);
798810
if (example != null) {
799811
logger.trace("Example set in swagger spec, returning example: '{}'", example);
800812
return example;
@@ -813,6 +825,10 @@ private Object resolvePropertyToExample(String propertyName, Schema propertySche
813825
return generateExampleBySchemaType(propertyName, propertySchema);
814826
}
815827

828+
private Object resolvePropertyToExample(String propertyName, Schema propertySchema) {
829+
return resolvePropertyToExample(propertyName, propertySchema, examplesFlags.usePropertyExamples());
830+
}
831+
816832
private <T> Object generateExampleBySchemaType(String propertyName, Schema<T> propertySchema) {
817833
if (CatsModelUtils.isStringSchema(propertySchema)) {
818834
return this.getExampleFromStringSchema(propertyName, propertySchema);

src/test/java/com/endava/cats/factory/FuzzingDataFactoryTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.endava.cats.openapi.OpenAPIModelGeneratorV2;
1414
import com.endava.cats.util.JsonUtils;
1515
import com.endava.cats.util.OpenApiUtils;
16+
import com.fasterxml.jackson.databind.JsonNode;
1617
import com.fasterxml.jackson.databind.ObjectMapper;
1718
import com.fasterxml.jackson.databind.node.ObjectNode;
1819
import com.google.gson.JsonParser;
@@ -39,6 +40,7 @@
3940
import java.math.BigDecimal;
4041
import java.nio.file.Files;
4142
import java.nio.file.Paths;
43+
import java.util.HashSet;
4244
import java.util.List;
4345
import java.util.Map;
4446
import java.util.Objects;
@@ -1039,6 +1041,33 @@ void shouldGenerateOpenApi31Specs() throws Exception {
10391041
"residency");
10401042
}
10411043

1044+
@Test
1045+
void shouldGenerateUniqueValuesWhenUniqueItemsTrue() throws Exception {
1046+
System.getProperties().setProperty("bind-type", "true");
1047+
List<FuzzingData> dataList = setupFuzzingData("/pet/findByStatus", "src/test/resources/petstore31.yaml");
1048+
Assertions.assertThat(dataList).hasSize(1);
1049+
FuzzingData data = dataList.getFirst();
1050+
String payload = data.getPayload();
1051+
ObjectMapper mapper = new ObjectMapper();
1052+
JsonNode root = mapper.readTree(payload);
1053+
1054+
JsonNode statusNode = root.get("status");
1055+
Assertions.assertThat(statusNode).isNotNull();
1056+
Assertions.assertThat(statusNode.isArray()).isTrue();
1057+
1058+
Set<String> uniqueValues = new HashSet<>();
1059+
for (JsonNode element : statusNode) {
1060+
boolean added = uniqueValues.add(element.asText());
1061+
Assertions.assertThat(added)
1062+
.as("Duplicate found: " + element.asText())
1063+
.isTrue();
1064+
}
1065+
1066+
Assertions.assertThat(uniqueValues.size())
1067+
.as("Status array contains duplicate elements.")
1068+
.isEqualTo(statusNode.size());
1069+
}
1070+
10421071
@Test
10431072
void shouldRecordErrorWhenParamRefDoesNotExist() throws Exception {
10441073
List<FuzzingData> dataList = setupFuzzingData("/pet-types", "src/test/resources/petstore.yml");

src/test/resources/petstore31.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ paths:
9191
deprecated: true
9292
schema:
9393
type: array
94+
uniqueItems: true
9495
items:
9596
type: string
9697
enum:

0 commit comments

Comments
 (0)