Skip to content

Commit

Permalink
Merge pull request #293 from muehmar/160-support-custom-types-with-va…
Browse files Browse the repository at this point in the history
…lidation

160 support custom types with validation
  • Loading branch information
muehmar authored Oct 8, 2024
2 parents 80e4553 + da2216a commit baa1eb2
Show file tree
Hide file tree
Showing 333 changed files with 26,678 additions and 5,070 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Add the plugin section in your `build.gradle`:

```
plugins {
id 'com.github.muehmar.openapischema' version '3.1.6'
id 'com.github.muehmar.openapischema' version '3.2.0'
}
```

Expand Down
47 changes: 41 additions & 6 deletions doc/010_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ openApiGenerator {
formatTypeMapping {
formatType = "username"
classType = "com.package.UserName"
conversion.fromCustomType = "getValue"
conversion.toCustomType = "UserName#fromString"
}
// Additional format type mapping
Expand Down Expand Up @@ -176,14 +178,22 @@ stagedBuilder {

### Class Mappings

The plugin allows one to map specific classes to custom types. The following example would use the custom List
implementation `com.package.CustomList` for lists instead of `java.util.List`. The config-property `toClass` should be
the fully qualified classname to properly generate import-statements.
The plugin allows one to map specific standard java classes, used in the DTO to custom types. The mapping is not applied
to generated DTO classes itself, this only includes the java class used for properties in the DTO. The following example
would use the custom List implementation `com.package.CustomList`
for lists instead of
`java.util.List`. The config-property
`toClass` should be the fully qualified classname to properly generate import-statements. The
`conversion.fromCustomType` and
`conversion.toCustomType` are used in the DTO to convert from and to the custom type,
see [Conversion for Mappings](#conversions-for-mappings).

```
classMapping {
fromClass = "List"
toClass = "com.package.CustomList"
conversion.fromCustomType = "asList"
conversion.toCustomType = "CustomList#fromList"
}
```
Expand All @@ -209,14 +219,39 @@ and a formatTypeMapping block in the configuration
formatTypeMapping {
formatType = "username"
classType = "com.package.UserName"
conversion.fromCustomType = "getValue"
conversion.toCustomType = "UserName#fromString"
}
```

will use the class `com.package.UserName` for the property `userName`. The config-property `classType` should be the
fully qualified classname to properly generate import-statements.
fully qualified classname to properly generate import-statements. The `conversion.fromCustomType` and
`conversion.toCustomType` are used in the DTO to convert from and to the custom type,
see [Conversion for Mappings](#conversions-for-mappings).

Repeat this block for each format type mapping.

### Conversions for mappings

The conversion can be defined in one of the following ways:

| Type | Description | Example |
|-----------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| instance method | If a class provides a method to convert to the other type which can be called directly. | `conversion.fromCustomType = "getValue"` |
| static method | The static factory method can either be a static method of the custom type itself or a static method of any other factory class. A fully qualified classname is necessary for arbitrary factory class to generate proper import statements.<br/> The method name an the class is separated by a `#`. | `conversion.toCustomType = "UserName#fromString"`<br/> `conversion.toCustomType = "com.package.UserNameFactory#fromString"` |

Format type mappings and class mappings could also be used without conversion. In this case, the custom types are
directly used in the DTO with the following consequences:

* The custom type is serialized by Jackson, therefore one needs to configure Jackson to properly serialize and
deserialize the custom type.
* Automatic validation will most likely not be possible, as the standard validation frameworks like hibernate can only
validate the standard java types. The plugin does not generate any validation annotations for custom types without
mapping.

The plugin provides warnings for defined mappings without conversion to be able to ensure one does not encounter one of
the mentioned issues with mappings without conversions.

### Schema Name Mappings

The schema name defines the generated classname of the DTO's. Constant mappings can be configured to adjust the
Expand All @@ -236,8 +271,8 @@ Multiple configured constant mappings are applied in the order they are configur

### Enum description extraction

Enables and configures the extraction of a description for enums from the openapi specification.
The `enumDescriptionExtraction` block is optional.
Enables and configures the extraction of a description for enums from the openapi specification. The
`enumDescriptionExtraction` block is optional.

```
enumDescriptionExtraction {
Expand Down
13 changes: 7 additions & 6 deletions doc/030_warnings.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
## Warnings
The plugin emit warnings for certain scenarios. These warnings are printed to the console of
the gradle build. These warnings can also be turned off completely if necessary via configuration of
the plugin.

The plugin emit warnings for certain scenarios. These warnings are printed to the console of the gradle build. These
warnings can also be turned off completely if necessary via configuration of the plugin.

The plugin can also be configured to let the generation fail in case warnings occurred (similar to the -Werror flag for
the Java compiler). This can be done globally for every warning or selective for any warning type, see the
[Configuration](#configuration) section.

The plugin generates the following warnings:

| Type | Description |
|------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| UNSUPPORTED_VALIDATION | Validation of custom types is currently not supported. This means, if a property has some constraints but is mapped to a custom type, no validation will be performed for this property. This may be supported in a future version of the plugin, see issue [#160](https://github.com/muehmar/gradle-openapi-schema/issues/160). |
| Type | Description |
|----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| UNSUPPORTED_VALIDATION | Mappings can be defined without conversions. For custom types without conversion, no validation annotations will be generated which will produce this warning. |
| MISSING_MAPPING_CONVERSION | Mappings without conversion may lead to serialisation or validations issues (see [asd](010_configuration.md#conversions-for-mappings). This warning is generated for each mapping without conversion. |
5 changes: 3 additions & 2 deletions doc/110_limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
* For `allOf`, `anyOf` and `oneOf` compositions, properties with the same name but different types or constraints are
currently not supported. The generator will throw an exception in this
case ([Issue 133](https://github.com/muehmar/gradle-openapi-schema/issues/133)).
* Only object types are supported with
compositions (`allOf`, `anyOf`, `oneOf`) ([Issue 265](https://github.com/muehmar/gradle-openapi-schema/issues/265)).
* Only object types are supported with compositions (`allOf`, `anyOf`,
`oneOf`) ([Issue 265](https://github.com/muehmar/gradle-openapi-schema/issues/265)).
* Conversions for mappings of maps as additional property is currently not supported.
5 changes: 5 additions & 0 deletions doc/130_change_log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Change Log

* 3.2.0
* Support for conversions of type and class mappings (issue `#160`)
* Fix serialisation of multiple nested required and nullable property in anyOf or oneOf compositions (issue `#278`)
* Fix mapping for map keys (issue `#286`)
* 3.1.9 - Fix inlining of container types, e.g. array items and map values (issue `#287`)
* 3.1.6 - Fix inlining of container types, e.g. array items and map values (issue `#287`)
* 3.1.5 - Fix serialisation of required not nullable properties with special naming pattern (issue `#272`)
* 3.1.4 - Fix generation of inline enum definitions for item types of arrays (issue `#280`)
Expand Down
14 changes: 7 additions & 7 deletions example-jakarta-3/build.gradle
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
plugins {
alias(libs.plugins.openApiSchema)
alias(pluginLibs.plugins.openApiSchema)
id "com.diffplug.spotless"
id 'openapischema.java8'
}

dependencies {
implementation libs.bundles.jackson
implementation sampleLibs.bundles.jackson

implementation libs.jakarta3.validation.api
implementation sampleLibs.jakarta3.validation.api

testImplementation libs.junit
testImplementation libs.junit.params
testImplementation testLibs.junit
testImplementation testLibs.junit.params

testImplementation libs.hibernate8.validator
testRuntimeOnly libs.glassfish.jakarta.el4
testImplementation sampleLibs.hibernate8.validator
testRuntimeOnly sampleLibs.glassfish.jakarta.el4
}

openApiGenerator {
Expand Down
22 changes: 11 additions & 11 deletions example/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
alias(libs.plugins.openApiSchema)
alias(pluginLibs.plugins.openApiSchema)
id "com.diffplug.spotless"
id 'openapischema.java8'
id 'java-test-fixtures'
Expand All @@ -26,21 +26,21 @@ sourceSets {
}

dependencies {
implementation libs.bundles.jackson
implementation libs.javax2.validation.api
implementation sampleLibs.bundles.jackson
implementation sampleLibs.javax2.validation.api

noValidationImplementation libs.jackson.databind
noValidationImplementation libs.jackson.annotations
noValidationImplementation sampleLibs.jackson.databind
noValidationImplementation sampleLibs.jackson.annotations

noJsonImplementation libs.javax2.validation.api
noJsonImplementation sampleLibs.javax2.validation.api

testImplementation libs.junit
testImplementation libs.junit.params
testImplementation testLibs.junit
testImplementation testLibs.junit.params

testImplementation libs.hibernate6.validator
testRuntimeOnly libs.glassfish.javax.el3
testImplementation sampleLibs.hibernate6.validator
testRuntimeOnly sampleLibs.glassfish.javax.el3

testImplementation libs.reflections
testImplementation testLibs.reflections
testImplementation testFixtures(project(":java-snapshot"))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,24 @@ void serialize_when_withArrayAsAdditionalProperty_then_correctJson()
final String json = MAPPER.writeValueAsString(dto);
assertEquals("{\"name\":\"name\",\"hello\":[\"world\"]}", json);
}

@Test
void deserialize_when_withArrayAsAdditionalProperty_then_correctDto()
throws JsonProcessingException {
final String json = "{\"name\":\"name\",\"hello\":[\"world\"]}";

final ArrayAdditionalPropertiesDto dto =
MAPPER.readValue(json, ArrayAdditionalPropertiesDto.class);

final ArrayAdditionalPropertiesDto expectedDto =
ArrayAdditionalPropertiesDto.builder()
.setName("name")
.andAllOptionals()
.addAdditionalProperty(
"hello",
new ArrayAdditionalPropertiesPropertyDto(Collections.singletonList("world")))
.build();

assertEquals(expectedDto, dto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,23 @@ void serialize_when_dtoWithAdditionalProperties_then_correctJson()

assertEquals("{\"name\":\"name\",\"HELLO\":\"WORLD\",\"hello\":\"world\"}", json);
}

@Test
void deserialize_when_jsonWithAdditionalProperties_then_correctDto()
throws JsonProcessingException {
final String json = "{\"name\":\"name\",\"HELLO\":\"WORLD\",\"hello\":\"world\"}";

final StringAdditionalPropertiesDto dto =
MAPPER.readValue(json, StringAdditionalPropertiesDto.class);

final StringAdditionalPropertiesDto expectedDto =
StringAdditionalPropertiesDto.builder()
.setName("name")
.andAllOptionals()
.addAdditionalProperty("hello", "world")
.addAdditionalProperty("HELLO", "WORLD")
.build();

assertEquals(expectedDto, dto);
}
}
4 changes: 2 additions & 2 deletions java-snapshot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {

dependencies {
testFixturesImplementation libs.codeGenerator
testFixturesApi libs.javaSnapshot
testFixturesApi testLibs.javaSnapshot

testFixturesApi libs.junit
testFixturesApi testLibs.junit
}
2 changes: 1 addition & 1 deletion plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies {

testImplementation testFixtures(project(":java-snapshot"))

testImplementation libs.bundles.mockito
testImplementation testLibs.bundles.mockito

testRuntimeOnly files(createClasspathManifest)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

import com.github.muehmar.gradle.openapi.generator.settings.ClassTypeMapping;
import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Data;

@EqualsAndHashCode
@ToString
@Data
public class ClassMapping implements Serializable {
private String fromClass;
private String toClass;
private final Conversion conversion;

public void setFromClass(String fromClass) {
this.fromClass = fromClass;
public ClassMapping() {
this.conversion = new Conversion();
}

public void setToClass(String toClass) {
this.toClass = toClass;
public ClassTypeMapping toSettingsClassMapping() {
return new ClassTypeMapping(fromClass, toClass, conversion.toTypeConversion());
}

public ClassTypeMapping toSettingsClassMapping() {
return new ClassTypeMapping(fromClass, toClass);
void assertCompleteTypeConversion() {
conversion.assertValidConfig(
String.format("classTypeMapping with fromClass '%s' and toClass '%s'", fromClass, toClass));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.github.muehmar.gradle.openapi.dsl;

import com.github.muehmar.gradle.openapi.exception.OpenApiGeneratorException;
import com.github.muehmar.gradle.openapi.generator.settings.TypeConversion;
import java.io.Serializable;
import java.util.Optional;
import lombok.Data;

@Data
public class Conversion implements Serializable {
String fromCustomType;
String toCustomType;

public Optional<TypeConversion> toTypeConversion() {
if (fromCustomType == null || toCustomType == null) {
return Optional.empty();
}
return Optional.of(new TypeConversion(fromCustomType, toCustomType));
}

void assertValidConfig(String mappingIdentifier) {
if (fromCustomType == null ^ toCustomType == null) {
final String missingConfig =
fromCustomType == null ? "fromCustomType is missing" : "toCustomType is missing";
throw new OpenApiGeneratorException(
"Invalid configuration for conversion for %s: Both "
+ "fromCustomType and toCustomType must be set but %s.",
mappingIdentifier, missingConfig);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
package com.github.muehmar.gradle.openapi.dsl;

import java.io.Serializable;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Data;

@EqualsAndHashCode
@ToString
@Data
public class FormatTypeMapping implements Serializable {
private String formatType;
private String classType;
private final Conversion conversion;

public void setFormatType(String formatType) {
this.formatType = formatType;
}

public void setClassType(String classType) {
this.classType = classType;
public FormatTypeMapping() {
this.conversion = new Conversion();
}

public com.github.muehmar.gradle.openapi.generator.settings.FormatTypeMapping
toSettingsFormatTypeMapping() {
return new com.github.muehmar.gradle.openapi.generator.settings.FormatTypeMapping(
formatType, classType);
formatType, classType, conversion.toTypeConversion());
}

void assertCompleteTypeConversion() {
conversion.assertValidConfig(
String.format(
"formatTypeMapping with formatType '%s' and classType '%s'", formatType, classType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ public WarningsConfig getWarnings() {
public void classMapping(Action<ClassMapping> action) {
final ClassMapping classMapping = new ClassMapping();
action.execute(classMapping);
classMapping.assertCompleteTypeConversion();
classMappings.add(classMapping);
}

Expand All @@ -260,6 +261,7 @@ SingleSchemaExtension withCommonClassMappings(List<ClassMapping> other) {
public void formatTypeMapping(Action<FormatTypeMapping> action) {
final FormatTypeMapping formatTypeMapping = new FormatTypeMapping();
action.execute(formatTypeMapping);
formatTypeMapping.assertCompleteTypeConversion();
formatTypeMappings.add(formatTypeMapping);
}

Expand Down
Loading

0 comments on commit baa1eb2

Please sign in to comment.