diff --git a/build.gradle b/build.gradle index 743ce03..9c8aa05 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { subprojects { subproject -> - version = "3.0.23" + version = "3.0.24" apply plugin: 'com.github.ben-manes.versions' diff --git a/gradle-plugin/src/main/groovy/ch/silviowangler/gradle/restapi/RestApiPlugin.groovy b/gradle-plugin/src/main/groovy/ch/silviowangler/gradle/restapi/RestApiPlugin.groovy index 3508e47..109da31 100644 --- a/gradle-plugin/src/main/groovy/ch/silviowangler/gradle/restapi/RestApiPlugin.groovy +++ b/gradle-plugin/src/main/groovy/ch/silviowangler/gradle/restapi/RestApiPlugin.groovy @@ -105,7 +105,7 @@ class RestApiPlugin implements Plugin { project.configurations.maybeCreate(CONFIGURATION_REST_API) - final String pluginVersion = "3.0.23" + final String pluginVersion = "3.0.24" final String libPhoneNumberVersion = "8.11.5" final List deps = [ diff --git a/gradle/micronaut.gradle b/gradle/micronaut.gradle index e1f60b6..0170ff5 100644 --- a/gradle/micronaut.gradle +++ b/gradle/micronaut.gradle @@ -12,6 +12,7 @@ micronaut { dependencies { annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + testAnnotationProcessor("io.micronaut.serde:micronaut-serde-processor") implementation("io.micronaut.serde:micronaut-serde-jackson") runtimeOnly("ch.qos.logback:logback-classic") testImplementation("io.micronaut:micronaut-http-client") diff --git a/rest-model/src/main/java/ch/silviowangler/rest/model/Expand.java b/rest-model/src/main/java/ch/silviowangler/rest/model/Expand.java index e3341c7..27196c4 100644 --- a/rest-model/src/main/java/ch/silviowangler/rest/model/Expand.java +++ b/rest-model/src/main/java/ch/silviowangler/rest/model/Expand.java @@ -24,7 +24,6 @@ package ch.silviowangler.rest.model; import io.micronaut.serde.annotation.Serdeable; -import java.util.Collection; import java.util.List; /** @@ -37,7 +36,7 @@ public class Expand { private String name; - private List data; + private List data; public Expand() {} @@ -45,7 +44,7 @@ public Expand(String name) { this.name = name; } - public Expand(String name, List data) { + public Expand(String name, List data) { this.name = name; this.data = data; } @@ -58,11 +57,11 @@ public void setName(String name) { this.name = name; } - public Collection getData() { + public List getData() { return data; } - public void setData(List data) { + public void setData(List data) { this.data = data; } } diff --git a/rest-model/src/main/java/ch/silviowangler/rest/model/ResourceModel.java b/rest-model/src/main/java/ch/silviowangler/rest/model/ResourceModel.java index a75a8a0..685fc46 100644 --- a/rest-model/src/main/java/ch/silviowangler/rest/model/ResourceModel.java +++ b/rest-model/src/main/java/ch/silviowangler/rest/model/ResourceModel.java @@ -23,6 +23,8 @@ */ package ch.silviowangler.rest.model; +import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.CLASS; + import com.fasterxml.jackson.annotation.JsonTypeInfo; /** @@ -30,5 +32,5 @@ * * @author Silvio Wangler */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonTypeInfo(use = CLASS) public interface ResourceModel {} diff --git a/rest-model/src/main/java/ch/silviowangler/rest/serdes/ResourceModelDeserializer.java b/rest-model/src/main/java/ch/silviowangler/rest/serdes/ResourceModelDeserializer.java new file mode 100644 index 0000000..efda546 --- /dev/null +++ b/rest-model/src/main/java/ch/silviowangler/rest/serdes/ResourceModelDeserializer.java @@ -0,0 +1,77 @@ +/* + * MIT License + *

+ * Copyright (c) 2016 - 2023 Silvio Wangler (silvio.wangler@gmail.com) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package ch.silviowangler.rest.serdes; + +import ch.silviowangler.rest.model.ResourceModel; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.micronaut.context.annotation.Secondary; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.type.Argument; +import io.micronaut.json.tree.JsonNode; +import io.micronaut.serde.Decoder; +import io.micronaut.serde.Deserializer; +import jakarta.inject.Singleton; +import java.io.IOException; + +@Singleton +@Secondary +public class ResourceModelDeserializer implements Deserializer { + @Override + public @Nullable ResourceModel deserialize( + @NonNull Decoder decoder, + @NonNull DecoderContext context, + @NonNull Argument type) + throws IOException { + + JsonNode jsonNode = decoder.decodeNode(); + JsonNode classValue = jsonNode.get(JsonTypeInfo.Id.CLASS.getDefaultPropertyName()); + + try { + Class clazz = + (Class) Class.forName(classValue.getStringValue()); + BeanIntrospection introspection = BeanIntrospection.getIntrospection(clazz); + + ResourceModel resourceModel = introspection.instantiate(); + introspection + .getBeanProperties() + .forEach( + beanProperty -> + beanProperty.set( + resourceModel, + context + .getConversionService() + .convert( + jsonNode.get(beanProperty.getName()).getValue(), + beanProperty.getType()) + .orElseThrow())); + + return resourceModel; + + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rest-model/src/test/groovy/ch/silviowangler/rest/validation/ExpandSerdeSpec.groovy b/rest-model/src/test/groovy/ch/silviowangler/rest/validation/ExpandSerdeSpec.groovy new file mode 100644 index 0000000..aa90d6d --- /dev/null +++ b/rest-model/src/test/groovy/ch/silviowangler/rest/validation/ExpandSerdeSpec.groovy @@ -0,0 +1,50 @@ +package ch.silviowangler.rest.validation + +import ch.silviowangler.rest.model.Expand +import io.micronaut.serde.ObjectMapper +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Shared +import spock.lang.Specification + +import java.time.LocalDate + +@MicronautTest +class ExpandSerdeSpec extends Specification { + + @Inject + ObjectMapper objectMapper + + @Shared + final Expand expand = new Expand("aaa", [new ActivitiesGetResourceModel(id: 'hh', key: 'hhh', number: 12, dob: LocalDate.of(1978, 11, 1))]) + + void "Serialize Expand"() { + + expect: + objectMapper.writeValueAsString(expand) == '{"name":"aaa","data":[{"@class":"ch.silviowangler.rest.validation.ActivitiesGetResourceModel","key":"hhh","id":"hh","number":12,"dob":"1978-11-01"}]}' + } + + + void "Deserialize Expand"() { + + given: + final String json = objectMapper.writeValueAsString(expand) + + when: + Expand result = objectMapper.readValue(json, Expand) + + then: + result.name == expand.name + + and: + result.data.size() == 1 + + and: + with(result.data.first()) { ActivitiesGetResourceModel res -> + res.id == 'hh' + res.key == 'hhh' + res.number == 12 + res.dob == LocalDate.of(1978, 11, 1) + } + } +} diff --git a/rest-model/src/test/java/ch/silviowangler/rest/validation/ActivitiesGetResourceModel.java b/rest-model/src/test/java/ch/silviowangler/rest/validation/ActivitiesGetResourceModel.java new file mode 100644 index 0000000..051e0ee --- /dev/null +++ b/rest-model/src/test/java/ch/silviowangler/rest/validation/ActivitiesGetResourceModel.java @@ -0,0 +1,68 @@ +/* + * MIT License + *

+ * Copyright (c) 2016 - 2023 Silvio Wangler (silvio.wangler@gmail.com) + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package ch.silviowangler.rest.validation; + +import ch.silviowangler.rest.model.ResourceModel; +import io.micronaut.serde.annotation.Serdeable; +import java.time.LocalDate; + +@Serdeable +public class ActivitiesGetResourceModel implements ResourceModel { + private String key; + private String id; + private Integer number; + private LocalDate dob; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public LocalDate getDob() { + return dob; + } + + public void setDob(LocalDate dob) { + this.dob = dob; + } +}