Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
Expand All @@ -26,7 +27,7 @@
<dependency>
<groupId>it.aboutbits</groupId>
<artifactId>archunit-toolbox</artifactId>
<version>1.0.0-RC1</version>
<version>1.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import org.jspecify.annotations.NullMarked;
import org.springdoc.core.customizers.OpenApiCustomizer;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.util.ArrayList;
import java.util.Map;

@NullMarked
public class NullableCustomizer implements OpenApiCustomizer {
public static final String NULLABLE_MARKER = "NULLABLE";

@Override
@SuppressWarnings("unchecked")
public void customise(OpenAPI openApi) {
Expand All @@ -23,57 +23,130 @@ public void customise(OpenAPI openApi) {
var requiredProperties = new ArrayList<String>();
if (((Schema<?>) schema).getProperties() != null) {
var properties = ((Schema<?>) schema).getProperties();
processProperties(properties, requiredProperties);
processProperties(schema.getName(), properties, requiredProperties);
}
if (schema.getAllOf() != null) {
schema.getAllOf().forEach(allOfSchema -> {
var allOfSchemaTyped = (Schema<?>) allOfSchema;
if (allOfSchemaTyped.getProperties() != null) {
var properties = allOfSchemaTyped.getProperties();
processProperties(properties, requiredProperties);
processProperties(schema.getName(), properties, requiredProperties);
}
});
}
schema.setRequired(requiredProperties);
});
}

private static void processProperties(Map<String, Schema> properties, ArrayList<String> requiredProperties) {
@SuppressWarnings("rawtypes")
private static void processProperties(
String modelFqn,
Map<String, Schema> properties,
ArrayList<String> requiredProperties
) {
var cls = loadClass(modelFqn);
if (cls == null) {
return;
}

properties.forEach((propertyName, property) -> {
var isNullable = isNullable(property);
var isNullable = isNullable(cls, propertyName);

if (!isNullable) {
requiredProperties.add(propertyName);
} else {
requiredProperties.remove(propertyName);
}
if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) {
property.setTitle(null);
}
if (property.get$ref() != null) {
property.set$ref(property.get$ref().replace(NULLABLE_MARKER, ""));
}
if (property.getItems() != null && property.getItems().get$ref() != null) {
property.getItems().set$ref(property.getItems().get$ref().replace(NULLABLE_MARKER, ""));
}
});
}

private static boolean isNullable(Schema<?> property) {
if (property.getTitle() != null && property.getTitle().equals(NULLABLE_MARKER)) {
return true;
@org.jspecify.annotations.Nullable
private static Class<?> loadClass(String fqn) {
try {
return Class.forName(fqn);
} catch (ClassNotFoundException _) {
// if this does not work, we probably have a parameterized type where the fqn is concatenated
}

if (property.get$ref() != null && property.get$ref().endsWith(NULLABLE_MARKER)) {
return true;
var lastDotIndex = -1;
for (var i = 0; i <= fqn.length(); i++) {
if (i == fqn.length() || fqn.charAt(i) == '.') {
var fullPart = fqn.substring(lastDotIndex + 1, i);
if (!fullPart.isEmpty() && Character.isUpperCase(fullPart.charAt(0))) {
// Try the full part first
var baseFqn = fqn.substring(0, i);
try {
return Class.forName(baseFqn);
} catch (ClassNotFoundException _) {
}

// Try stripping capitalized segments from the end of the part
// e.g., LabelAndDescriptionChoiceCom -> try LabelAndDescriptionChoice, then LabelAndDescription, etc.
for (var j = fullPart.length() - 1; j > 0; j--) {
if (Character.isUpperCase(fullPart.charAt(j))) {
var strippedPart = fullPart.substring(0, j);
var candidateFqn = fqn.substring(0, lastDotIndex + 1) + strippedPart;
try {
return Class.forName(candidateFqn);
} catch (ClassNotFoundException _) {
}
}
}
}
lastDotIndex = i;
}
}
return null;
}

private static boolean isNullable(Class<?> cls, String propertyName) {
var currentClass = cls;
while (currentClass != null) {
try {
var field = currentClass.getDeclaredField(propertyName);
if (isNullable(field.getAnnotatedType(), field.getAnnotations())) {
return true;
}
} catch (NoSuchFieldException _) {
}

for (var method : currentClass.getDeclaredMethods()) {
if (method.getName().equals(propertyName)
|| method.getName().equals("get" + capitalize(propertyName))
|| method.getName().equals("is" + capitalize(propertyName))) {
if (isNullable(method.getAnnotatedReturnType(), method.getAnnotations())) {
return true;
}
}
}

if (property.getItems() != null && property.getItems().get$ref() != null && property.getItems()
.get$ref()
.endsWith(NULLABLE_MARKER)) {
return true;
currentClass = currentClass.getSuperclass();
}

return false;
}

private static boolean isNullable(
AnnotatedType annotatedType,
Annotation[] annotations
) {
for (var annotation : annotatedType.getAnnotations()) {
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
return true;
}
}
for (var annotation : annotations) {
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
return true;
}
}
return false;
}

private static String capitalize(String str) {
if (str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
import org.springframework.beans.factory.annotation.Autowired;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

@ApplicationTest
@NullMarked
Expand Down Expand Up @@ -141,12 +142,10 @@ void givenQueryWithMultipleResults_shouldFail() {

var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");

assertThrows(
IllegalStateException.class,
() -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asSingleResult()
assertThatIllegalStateException().isThrownBy(() -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asSingleResult()
);
}
}
Expand All @@ -172,11 +171,10 @@ void givenQueryWithOneResult_shouldPass() {
void givenQueryWithOneResult_shouldFail() {
var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");

assertThrows(
EntityNotFoundException.class, () -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asSingleResultOrFail()
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asSingleResultOrFail()
);
}
}
Expand Down Expand Up @@ -221,12 +219,10 @@ void givenQuery_wrongTargetClass_shouldFail() {

var query = entityManager.createQuery("select q, 'xxx' from QueryTransformerTestModel q");

assertThrows(
TransformerRuntimeException.class,
() -> QueryTransformer
.of(entityManager, WrongContainer.class)
.withQuery(query)
.asList()
assertThatExceptionOfType(TransformerRuntimeException.class).isThrownBy(() -> QueryTransformer
.of(entityManager, WrongContainer.class)
.withQuery(query)
.asList()
);
}
}
Expand Down Expand Up @@ -351,12 +347,10 @@ void givenVariousQueries_shouldPassReturningTheRightTotalCount() {
void givenQueryWithSelectDistinct_shouldFail() {
var query = entityManager.createQuery("select distinct q, 'xxx' from QueryTransformerTestModel q");

assertThrows(
IllegalStateException.class,
() -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asPage(1, 2)
assertThatIllegalStateException().isThrownBy(() -> QueryTransformer
.of(entityManager, TestModelContainer.class)
.withQuery(query)
.asPage(1, 2)
);
}
}
Expand Down
Loading