From 0e0b7e868eb5fff7f85a7078fecc5c3c964cc6a3 Mon Sep 17 00:00:00 2001 From: Matt Riben Date: Thu, 11 Apr 2024 23:57:51 -0500 Subject: [PATCH] Add crd-generator and java-generator support for Format annotation --- CHANGELOG.md | 2 + .../crd/generator/AbstractJsonSchema.java | 22 +++++- .../fabric8/crd/generator/v1/JsonSchema.java | 1 + .../crd/generator/v1beta1/JsonSchema.java | 1 + .../crd/example/annotated/AnnotatedSpec.java | 7 ++ .../crd/generator/v1/JsonSchemaTest.java | 3 +- doc/CRD-generator.md | 24 ++++++ .../fabric8/generator/annotation/Format.java | 77 +++++++++++++++++++ .../nodes/AbstractJSONSchema2Pojo.java | 7 ++ .../fabric8/java/generator/nodes/JObject.java | 6 ++ .../generator/nodes/ValidationProperties.java | 16 +++- ...ojos.testAkkaMicroservicesCrd.approved.txt | 1 + ...dGeneratePojos.testCrontabCrd.approved.txt | 1 + ...estCrontabExtraAnnotationsCrd.approved.txt | 1 + 14 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f12171c29..319c6c10912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * 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 +* Fix #5867: (crd-generator) Add support to define `format` from `@Format` annotation +* Fix #5867: (java-generator) Add support to define `@Format` annotation from `format` #### 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 878d016b0b2..e9704defec2 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 @@ -119,6 +119,7 @@ public abstract class AbstractJsonSchema { public static final String ANNOTATION_DEFAULT = "io.fabric8.generator.annotation.Default"; public static final String ANNOTATION_MIN = "io.fabric8.generator.annotation.Min"; public static final String ANNOTATION_MAX = "io.fabric8.generator.annotation.Max"; + public static final String ANNOTATION_FORMAT = "io.fabric8.generator.annotation.Format"; public static final String ANNOTATION_PATTERN = "io.fabric8.generator.annotation.Pattern"; public static final String ANNOTATION_NULLABLE = "io.fabric8.generator.annotation.Nullable"; public static final String ANNOTATION_REQUIRED = "io.fabric8.generator.annotation.Required"; @@ -177,6 +178,7 @@ protected static class SchemaPropsOptions { final String defaultValue; final Double min; final Double max; + final String format; final String pattern; final boolean nullable; final boolean required; @@ -187,6 +189,7 @@ protected static class SchemaPropsOptions { defaultValue = null; min = null; max = null; + format = null; pattern = null; nullable = false; required = false; @@ -194,12 +197,13 @@ protected static class SchemaPropsOptions { validationRules = null; } - public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern, + public SchemaPropsOptions(String defaultValue, Double min, Double max, String format, String pattern, List validationRules, boolean nullable, boolean required, boolean preserveUnknownFields) { this.defaultValue = defaultValue; this.min = min; this.max = max; + this.format = format; this.pattern = pattern; this.nullable = nullable; this.required = required; @@ -219,6 +223,10 @@ public Optional getMax() { return Optional.ofNullable(max); } + public Optional getFormat() { + return Optional.ofNullable(format); + } + public Optional getPattern() { return Optional.ofNullable(pattern); } @@ -383,6 +391,7 @@ private T internalFromImpl(TypeDef definition, LinkedHashMap vis facade.defaultValue, facade.min, facade.max, + facade.format, facade.pattern, facade.validationRules, facade.nullable, @@ -421,6 +430,7 @@ private static class PropertyOrAccessor { private String defaultValue; private Double min; private Double max; + private String format; private String pattern; private List validationRules; private boolean nullable; @@ -460,6 +470,9 @@ public void process() { case ANNOTATION_MIN: min = (Double) a.getParameters().get(VALUE); break; + case ANNOTATION_FORMAT: + format = (String) a.getParameters().get(VALUE); + break; case ANNOTATION_PATTERN: pattern = (String) a.getParameters().get(VALUE); break; @@ -522,6 +535,10 @@ public Optional getMin() { return Optional.ofNullable(min); } + public Optional getFormat() { + return Optional.ofNullable(format); + } + public Optional getPattern() { return Optional.ofNullable(pattern); } @@ -575,6 +592,7 @@ private static class PropertyFacade { private String defaultValue; private Double min; private Double max; + private String format; private String pattern; private boolean nullable; private boolean required; @@ -607,6 +625,7 @@ public PropertyFacade(Property property, Map potentialAccessors, defaultValue = null; min = null; max = null; + format = null; pattern = null; validationRules = new LinkedList<>(); } @@ -637,6 +656,7 @@ public Property process() { defaultValue = p.getDefault().orElse(defaultValue); min = p.getMin().orElse(min); max = p.getMax().orElse(max); + format = p.getFormat().orElse(format); pattern = p.getPattern().orElse(pattern); p.getValidationRules().ifPresent(rules -> validationRules.addAll(rules)); diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java index 03f509df071..c9997f78622 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java @@ -79,6 +79,7 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder, }); options.getMin().ifPresent(schema::setMinimum); options.getMax().ifPresent(schema::setMaximum); + options.getFormat().ifPresent(schema::setFormat); options.getPattern().ifPresent(schema::setPattern); List validationRulesFromProperty = options.getValidationRules().stream() diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java index 2cf5e6c048e..aca8917cdb3 100644 --- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java +++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java @@ -80,6 +80,7 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder, }); options.getMin().ifPresent(schema::setMinimum); options.getMax().ifPresent(schema::setMaximum); + options.getFormat().ifPresent(schema::setFormat); options.getPattern().ifPresent(schema::setPattern); List validationRulesFromProperty = options.getValidationRules().stream() 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 3d99583090b..bd8d9776e26 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Format; import io.fabric8.generator.annotation.Max; import io.fabric8.generator.annotation.Min; import io.fabric8.generator.annotation.Nullable; @@ -57,6 +58,7 @@ public class AnnotatedSpec { private String numInt; private String numFloat; private ZonedDateTime issuedAt; + private String password; @JsonIgnore private int ignoredFoo; @@ -97,6 +99,11 @@ public int getMin() { return 1; } + @Format("password") + public String getPassword() { + return password; + } + @Pattern("\\b[1-9]\\b") public String getSingleDigit() { return "1"; 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 4c824116441..cb7fad26e6c 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 @@ -105,7 +105,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti assertNotNull(schema); Map properties = assertSchemaHasNumberOfProperties(schema, 2); final JSONSchemaProps specSchema = properties.get("spec"); - Map spec = assertSchemaHasNumberOfProperties(specSchema, 20); + Map spec = assertSchemaHasNumberOfProperties(specSchema, 21); // check descriptions are present assertTrue(spec.containsKey("from-field")); @@ -125,6 +125,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti 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").withFormat("password").build(), spec.get("password")); 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")); diff --git a/doc/CRD-generator.md b/doc/CRD-generator.md index 036a5466c9c..7865d80fb08 100644 --- a/doc/CRD-generator.md +++ b/doc/CRD-generator.md @@ -231,6 +231,30 @@ The field will have the `maximum` property in the generated CRD, such as: type: object ``` +### io.fabric8.generator.annotation.Format + +If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Format` + +```java +public class ExampleSpec { + @Format("password") + String someValue; +} +``` + +The field will have the `format` property in the generated CRD, such as: + +```yaml + spec: + properties: + someValue: + format: password + type: string + required: + - someValue + type: object +``` + ### io.fabric8.generator.annotation.Pattern If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Pattern` diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java new file mode 100644 index 00000000000..ba228e1408d --- /dev/null +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.generator.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Java representation of the {@code format} field of JSONSchemaProps. + * + *

+ * The following formats are validated by Kubernetes: + *

+ *
    + *
  • {@code bsonobjectid}: a bson object ID, i.e. a 24 characters hex string
  • + *
  • {@code uri}: an URI as parsed by Golang net/url.ParseRequestURI
  • + *
  • {@code email}: an email address as parsed by Golang net/mail.ParseAddress
  • + *
  • {@code hostname}: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].
  • + *
  • {@code ipv4}: an IPv4 IP as parsed by Golang net.ParseIP
  • + *
  • {@code ipv6}: an IPv6 IP as parsed by Golang net.ParseIP
  • + *
  • {@code cidr}: a CIDR as parsed by Golang net.ParseCIDR
  • + *
  • {@code mac}: a MAC address as parsed by Golang net.ParseMAC
  • + *
  • {@code uuid}: an UUID that allows uppercase defined by the regex + * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$}
  • + *
  • {@code uuid3}: an UUID3 that allows uppercase defined by the regex + * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$}
  • + *
  • {@code uuid4}: an UUID4 that allows uppercase defined by the regex + * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$}
  • + *
  • {@code uuid5}: an UUID5 that allows uppercase defined by the regex + * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$}
  • + *
  • {@code isbn}: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041"
  • + *
  • {@code isbn10}: an ISBN10 number string like "0321751043"
  • + *
  • {@code isbn13}: an ISBN13 number string like "978-0321751041"
  • + *
  • {@code creditcard}: a credit card number defined by the regex + * {@code ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$} + * with any non digit characters mixed in
  • + *
  • {@code ssn}: a U.S. social security number following the regex {@code ^\d{3}[- ]?\d{2}[- ]?\d{4}$}
  • + *
  • {@code hexcolor}: an hexadecimal color code like "#FFFFFF: following the regex + * {@code ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$}
  • + *
  • {@code rgbcolor}: an RGB color code like rgb like "rgb(255,255,2559"
  • + *
  • {@code byte}: base64 encoded binary data
  • + *
  • {@code password}: any kind of string
  • + *
  • {@code date}: a date string like "2006-01-02" as defined by full-date in RFC3339
  • + *
  • {@code duration}: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration + * format
  • + *
  • {@code date-time}: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339.
  • + *
+ *

+ * Unknown formats are ignored by Kubernetes and if another consumer is unaware of the meaning of the format, + * they shall fall back to using the basic type without format. + *

+ * + * @see + * Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps + * + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Format { + String value(); +} diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java index 3cb55d2be5d..7258908e898 100644 --- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java +++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java @@ -56,6 +56,7 @@ public static final AnnotationExpr newGeneratedAnnotation() { protected Double maximum; protected Double minimum; + protected String format; protected String pattern; public Double getMaximum() { @@ -66,6 +67,10 @@ public Double getMinimum() { return minimum; } + public String getFormat() { + return format; + } + public String getPattern() { return pattern; } @@ -109,6 +114,7 @@ protected AbstractJSONSchema2Pojo(Config config, String description, final boole if (validationProperties != null) { this.maximum = validationProperties.getMaximum(); this.minimum = validationProperties.getMinimum(); + this.format = validationProperties.getFormat(); this.pattern = validationProperties.getPattern(); } } @@ -268,6 +274,7 @@ private static AbstractJSONSchema2Pojo fromJsonSchema( ValidationProperties.Builder.getInstance() .withMaximum(prop.getMaximum()) .withMinimum(prop.getMinimum()) + .withFormat(prop.getFormat()) .withPattern(prop.getPattern()) .build()); case ARRAY: 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 51c60e66845..2bd1222e9e6 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 @@ -249,6 +249,12 @@ public GeneratorResult generateJava() { new Name("io.fabric8.generator.annotation.Min"), new DoubleLiteralExpr(prop.getMinimum()))); } + if (prop.getFormat() != null) { + objField.addAnnotation( + new SingleMemberAnnotationExpr( + new Name("io.fabric8.generator.annotation.Format"), + new StringLiteralExpr(StringEscapeUtils.escapeJava(prop.getFormat())))); + } if (prop.getPattern() != null) { objField.addAnnotation( new SingleMemberAnnotationExpr( diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java index 88d8b5b748b..6befa7d42df 100644 --- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java +++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java @@ -22,11 +22,13 @@ public class ValidationProperties { private final Double maximum; private final Double minimum; + private final String format; private final String pattern; - private ValidationProperties(final Double maximum, final Double minimum, final String pattern) { + private ValidationProperties(final Double maximum, final Double minimum, final String format, final String pattern) { this.maximum = maximum; this.minimum = minimum; + this.format = format; this.pattern = pattern; } @@ -38,6 +40,10 @@ public Double getMinimum() { return minimum; } + public String getFormat() { + return format; + } + public String getPattern() { return pattern; } @@ -45,6 +51,7 @@ public String getPattern() { public static final class Builder { private Double maximum; private Double minimum; + private String format; private String pattern; private Builder() { @@ -64,13 +71,18 @@ public Builder withMinimum(final Double minimum) { return this; } + public Builder withFormat(final String format) { + this.format = format; + return this; + } + public Builder withPattern(final String pattern) { this.pattern = pattern; return this; } public ValidationProperties build() { - return new ValidationProperties(maximum, minimum, pattern); + return new ValidationProperties(maximum, minimum, format, pattern); } } } diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt index 4b26bcf4f94..7dd38cfbeeb 100644 --- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt +++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt @@ -549,6 +549,7 @@ public class AkkaMicroserviceStatus implements io.fabric8.kubernetes.api.model.K * Total number of available pods targeted by this AkkaMicroservice. */ @com.fasterxml.jackson.annotation.JsonProperty("availableReplicas") + @io.fabric8.generator.annotation.Format("int32") @com.fasterxml.jackson.annotation.JsonPropertyDescription("Total number of available pods targeted by this AkkaMicroservice.") @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP) private Integer availableReplicas; 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 5992be30192..8fcfdb42d50 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 @@ -41,6 +41,7 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.model.KubernetesRe } @com.fasterxml.jackson.annotation.JsonProperty("issuedAt") + @io.fabric8.generator.annotation.Format("date-time") @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP) private java.time.ZonedDateTime 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 57f66fc597c..78af1b8ecc5 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 @@ -75,6 +75,7 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.builder.Editable