diff --git a/CHANGELOG.md b/CHANGELOG.md index e82ddb08f59..d7f12171c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ #### Improvements * Fix #5878: (java-generator) Add implements Editable for extraAnnotations * Fix #5878: (java-generator) Update documentation to include dependencies +* Fix #5867: (crd-generator) Imply schemaFrom via JsonFormat shape (SchemaFrom takes precedence) +* Fix #5867: (java-generator) Add JsonFormat shape to date-time #### Dependency Upgrade diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java index c4feaf5b0a1..878d016b0b2 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java @@ -15,6 +15,7 @@ */ package io.fabric8.crd.generator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -91,6 +92,8 @@ public abstract class AbstractJsonSchema { protected static final TypeDef DATE = TypeDef.forName(Date.class.getName()); protected static final TypeRef DATE_REF = DATE.toReference(); + private static final String JSON_FORMAT_SHAPE = "shape"; + private static final Map JSON_FORMAT_SHAPE_MAPPING = new HashMap<>(); private static final String VALUE = "value"; private static final String INT_OR_STRING_MARKER = "int_or_string"; @@ -107,6 +110,7 @@ public abstract class AbstractJsonSchema { .build(); private static final Map COMMON_MAPPINGS = new HashMap<>(); + public static final String ANNOTATION_JSON_FORMAT = "com.fasterxml.jackson.annotation.JsonFormat"; public static final String ANNOTATION_JSON_PROPERTY = "com.fasterxml.jackson.annotation.JsonProperty"; public static final String ANNOTATION_JSON_PROPERTY_DESCRIPTION = "com.fasterxml.jackson.annotation.JsonPropertyDescription"; public static final String ANNOTATION_JSON_IGNORE = "com.fasterxml.jackson.annotation.JsonIgnore"; @@ -151,6 +155,12 @@ public abstract class AbstractJsonSchema { // initialize with client defaults new KubernetesSerialization(mapper, false); GENERATOR = new JsonSchemaGenerator(mapper); + + JSON_FORMAT_SHAPE_MAPPING.put(JsonFormat.Shape.BOOLEAN, Types.typeDefFrom(Boolean.class).toReference()); + JSON_FORMAT_SHAPE_MAPPING.put(JsonFormat.Shape.NUMBER, Types.typeDefFrom(Double.class).toReference()); + JSON_FORMAT_SHAPE_MAPPING.put(JsonFormat.Shape.NUMBER_FLOAT, Types.typeDefFrom(Double.class).toReference()); + JSON_FORMAT_SHAPE_MAPPING.put(JsonFormat.Shape.NUMBER_INT, Types.typeDefFrom(Long.class).toReference()); + JSON_FORMAT_SHAPE_MAPPING.put(JsonFormat.Shape.STRING, Types.typeDefFrom(String.class).toReference()); } public static String getSchemaTypeFor(TypeRef typeRef) { @@ -456,6 +466,11 @@ public void process() { case ANNOTATION_REQUIRED: required = true; break; + case ANNOTATION_JSON_FORMAT: + if (schemaFrom == null) { + schemaFrom = JSON_FORMAT_SHAPE_MAPPING.get((JsonFormat.Shape) a.getParameters().get(JSON_FORMAT_SHAPE)); + } + break; case ANNOTATION_JSON_PROPERTY: final String nameFromAnnotation = (String) a.getParameters().get(VALUE); if (!Strings.isNullOrEmpty(nameFromAnnotation) && !propertyName.equals(nameFromAnnotation)) { diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java index 40d1b6e6e8f..3d99583090b 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java @@ -15,6 +15,7 @@ */ package io.fabric8.crd.example.annotated; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; @@ -27,6 +28,8 @@ import io.fabric8.generator.annotation.ValidationRule; import lombok.Data; +import java.time.ZonedDateTime; + @Data public class AnnotatedSpec { @JsonProperty("from-field") @@ -49,6 +52,11 @@ public class AnnotatedSpec { private AnnotatedEnum anEnum; @javax.validation.constraints.Min(0) // a non-string value attribute private int sizedField; + private String bool; + private String num; + private String numInt; + private String numFloat; + private ZonedDateTime issuedAt; @JsonIgnore private int ignoredFoo; @@ -114,6 +122,31 @@ public void setEmptySetter2(boolean emptySetter2) { this.emptySetter2 = emptySetter2; } + @JsonFormat(shape = JsonFormat.Shape.BOOLEAN) + public String getBool() { + return bool; + } + + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + public String getNum() { + return num; + } + + @JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT) + public String getNumFloat() { + return numFloat; + } + + @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) + public String getNumInt() { + return numInt; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssVV") + public java.time.ZonedDateTime getIssuedAt() { + return issuedAt; + } + public enum AnnotatedEnum { non("N"), @JsonProperty("oui") diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java index a26a1427e7c..4c824116441 100644 --- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java +++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; import io.fabric8.crd.example.annotated.Annotated; import io.fabric8.crd.example.basic.Basic; import io.fabric8.crd.example.extraction.CollectionCyclicSchemaSwap; @@ -32,6 +33,7 @@ import io.fabric8.crd.generator.utils.Types; import io.fabric8.kubernetes.api.model.AnyType; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsBuilder; import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule; import io.sundr.model.TypeDef; import org.junit.jupiter.api.Test; @@ -39,9 +41,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; -import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -103,7 +105,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); - Map spec = assertSchemaHasNumberOfProperties(specSchema, 15); + Map spec = assertSchemaHasNumberOfProperties(specSchema, 20); // check descriptions are present assertTrue(spec.containsKey("from-field")); @@ -120,47 +122,19 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertNull(spec.get("emptySetter").getDescription()); assertTrue(spec.containsKey("anEnum")); - final JSONSchemaProps min = spec.get("min"); - assertNull(min.getDefault()); - assertEquals(-5.0, min.getMinimum()); - assertNull(min.getMaximum()); - assertNull(min.getPattern()); - assertNull(min.getNullable()); - - final JSONSchemaProps max = spec.get("max"); - assertNull(max.getDefault()); - assertEquals(5.0, max.getMaximum()); - assertNull(max.getMinimum()); - assertNull(max.getPattern()); - assertNull(max.getNullable()); - - final JSONSchemaProps pattern = spec.get("singleDigit"); - assertNull(pattern.getDefault()); - assertEquals("\\b[1-9]\\b", pattern.getPattern()); - assertNull(pattern.getMinimum()); - assertNull(pattern.getMaximum()); - assertNull(pattern.getNullable()); - - final JSONSchemaProps nullable = spec.get("nullable"); - assertNull(nullable.getDefault()); - assertTrue(nullable.getNullable()); - assertNull(nullable.getMinimum()); - assertNull(nullable.getMaximum()); - assertNull(nullable.getPattern()); - - final JSONSchemaProps defaultValue = spec.get("defaultValue"); - assertEquals("my-value", YAML_MAPPER.writeValueAsString(defaultValue.getDefault()).trim()); - assertNull(defaultValue.getNullable()); - assertNull(defaultValue.getMinimum()); - assertNull(defaultValue.getMaximum()); - assertNull(defaultValue.getPattern()); - - final JSONSchemaProps defaultValue2 = spec.get("defaultValue2"); - assertEquals("my-value2", YAML_MAPPER.writeValueAsString(defaultValue2.getDefault()).trim()); - assertNull(defaultValue2.getNullable()); - assertNull(defaultValue2.getMinimum()); - assertNull(defaultValue2.getMaximum()); - assertNull(defaultValue2.getPattern()); + Function type = t -> new JSONSchemaPropsBuilder().withType(t); + assertEquals(type.apply("integer").withMinimum(-5.0).build(), spec.get("min")); + assertEquals(type.apply("integer").withMaximum(5.0).build(), spec.get("max")); + assertEquals(type.apply("string").withPattern("\\b[1-9]\\b").build(), spec.get("singleDigit")); + assertEquals(type.apply("string").withNullable(true).build(), spec.get("nullable")); + assertEquals(type.apply("string").withDefault(TextNode.valueOf("my-value")).build(), spec.get("defaultValue")); + assertEquals(type.apply("string").withDefault(TextNode.valueOf("my-value2")).build(), spec.get("defaultValue2")); + assertEquals(type.apply("string").withEnum(TextNode.valueOf("non"), TextNode.valueOf("oui")).build(), spec.get("anEnum")); + assertEquals(type.apply("boolean").build(), spec.get("bool")); + assertEquals(type.apply("number").build(), spec.get("num")); + assertEquals(type.apply("number").build(), spec.get("numFloat")); + assertEquals(type.apply("integer").build(), spec.get("numInt")); + assertEquals(type.apply("string").build(), spec.get("issuedAt")); // check required list, should register properties with their modified name if needed final List required = specSchema.getRequired(); @@ -169,12 +143,6 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertTrue(required.contains("emptySetter2")); assertTrue(required.contains("from-getter")); - // check the enum values - final JSONSchemaProps anEnum = spec.get("anEnum"); - final List enumValues = anEnum.getEnum(); - assertEquals(2, enumValues.size()); - enumValues.stream().map(JsonNode::textValue).forEach(s -> assertTrue("oui".equals(s) || "non".equals(s))); - // check ignored fields assertFalse(spec.containsKey("ignoredFoo")); assertFalse(spec.containsKey("ignoredBar")); diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java index bc0f034a9c9..51c60e66845 100644 --- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java +++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java @@ -225,12 +225,13 @@ public GeneratorResult generateJava() { MethodDeclaration fieldSetter = objField.createSetter(); if (prop.getClassType().equals(DATETIME_NAME)) { + final String jsonFormat = "com.fasterxml.jackson.annotation.JsonFormat"; fieldGetter.addAnnotation(new SingleMemberAnnotationExpr( - new Name("com.fasterxml.jackson.annotation.JsonFormat"), - new NameExpr("pattern = \"" + config.getSerDatetimeFormat() + "\""))); + new Name(jsonFormat), + new NameExpr("shape = " + jsonFormat + ".Shape.STRING, pattern = \"" + config.getSerDatetimeFormat() + "\""))); fieldSetter.addAnnotation(new SingleMemberAnnotationExpr( - new Name("com.fasterxml.jackson.annotation.JsonFormat"), - new NameExpr("pattern = \"" + config.getDeserDatetimeFormat() + "\""))); + new Name(jsonFormat), + new NameExpr("shape = " + jsonFormat + ".Shape.STRING, pattern = \"" + config.getDeserDatetimeFormat() + "\""))); } if (isRequired) { diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt index 620fd94a290..5992be30192 100644 --- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt +++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt @@ -44,12 +44,12 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.model.KubernetesRe @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP) private java.time.ZonedDateTime issuedAt; - @com.fasterxml.jackson.annotation.JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssVV") + @com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssVV") public java.time.ZonedDateTime getIssuedAt() { return issuedAt; } - @com.fasterxml.jackson.annotation.JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[XXX][VV]") + @com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss[XXX][VV]") public void setIssuedAt(java.time.ZonedDateTime issuedAt) { this.issuedAt = issuedAt; } diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt index e1b46e6d5ea..57f66fc597c 100644 --- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt +++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt @@ -78,12 +78,12 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.builder.Editable