Skip to content

Commit

Permalink
Add crd-generator and java-generator support for Format annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
matteriben committed Apr 17, 2024
1 parent a741e4b commit 4fefc0b
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 52 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

#### Improvements
* Fix #5843: Support existingJavaTypes extension in java-generator
* 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public abstract class AbstractJsonSchema<T, B> {
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";
Expand Down Expand Up @@ -154,6 +155,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;
Expand All @@ -164,19 +166,21 @@ protected static class SchemaPropsOptions {
defaultValue = null;
min = null;
max = null;
format = null;
pattern = null;
nullable = false;
required = false;
preserveUnknownFields = false;
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<KubernetesValidationRule> 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;
Expand All @@ -196,6 +200,10 @@ public Optional<Double> getMax() {
return Optional.ofNullable(max);
}

public Optional<String> getFormat() {
return Optional.ofNullable(format);
}

public Optional<String> getPattern() {
return Optional.ofNullable(pattern);
}
Expand Down Expand Up @@ -358,6 +366,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSche
facade.defaultValue,
facade.min,
facade.max,
facade.format,
facade.pattern,
facade.validationRules,
facade.nullable,
Expand Down Expand Up @@ -396,6 +405,7 @@ private static class PropertyOrAccessor {
private String defaultValue;
private Double min;
private Double max;
private String format;
private String pattern;
private List<KubernetesValidationRule> validationRules;
private boolean nullable;
Expand Down Expand Up @@ -435,6 +445,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;
Expand Down Expand Up @@ -492,6 +505,10 @@ public Optional<Double> getMin() {
return Optional.ofNullable(min);
}

public Optional<String> getFormat() {
return Optional.ofNullable(format);
}

public Optional<String> getPattern() {
return Optional.ofNullable(pattern);
}
Expand Down Expand Up @@ -545,6 +562,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;
Expand Down Expand Up @@ -577,6 +595,7 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
defaultValue = null;
min = null;
max = null;
format = null;
pattern = null;
validationRules = new LinkedList<>();
}
Expand Down Expand Up @@ -607,6 +626,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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationRule> validationRulesFromProperty = options.getValidationRules().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationRule> validationRulesFromProperty = options.getValidationRules().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.crd.generator.annotation.SchemaFrom;
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;
Expand All @@ -27,6 +29,8 @@
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Data;

import java.time.ZonedDateTime;

@Data
public class AnnotatedSpec {
@JsonProperty("from-field")
Expand All @@ -49,6 +53,7 @@ public class AnnotatedSpec {
private AnnotatedEnum anEnum;
@javax.validation.constraints.Min(0) // a non-string value attribute
private int sizedField;
private String password;

@JsonIgnore
private int ignoredFoo;
Expand Down Expand Up @@ -89,6 +94,11 @@ public int getMin() {
return 1;
}

@Format("password")
public String getPassword() {
return password;
}

@Pattern("\\b[1-9]\\b")
public String getSingleDigit() {
return "1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,16 +33,17 @@
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;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
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;
Expand Down Expand Up @@ -103,7 +105,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
assertNotNull(schema);
Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
final JSONSchemaProps specSchema = properties.get("spec");
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 15);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 16);

// check descriptions are present
assertTrue(spec.containsKey("from-field"));
Expand All @@ -120,47 +122,16 @@ 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());
Supplier<JSONSchemaPropsBuilder> typeInteger = () -> new JSONSchemaPropsBuilder().withType("integer");
Supplier<JSONSchemaPropsBuilder> typeString = () -> new JSONSchemaPropsBuilder().withType("string");
assertEquals(typeInteger.get().withMinimum(-5.0).build(), spec.get("min"));
assertEquals(typeInteger.get().withMaximum(5.0).build(), spec.get("max"));
assertEquals(typeString.get().withFormat("password").build(), spec.get("password"));
assertEquals(typeString.get().withPattern("\\b[1-9]\\b").build(), spec.get("singleDigit"));
assertEquals(typeString.get().withNullable(true).build(), spec.get("nullable"));
assertEquals(typeString.get().withDefault(TextNode.valueOf("my-value")).build(), spec.get("defaultValue"));
assertEquals(typeString.get().withDefault(TextNode.valueOf("my-value2")).build(), spec.get("defaultValue2"));
assertEquals(typeString.get().withEnum(TextNode.valueOf("non"), TextNode.valueOf("oui")).build(), spec.get("anEnum"));

// check required list, should register properties with their modified name if needed
final List<String> required = specSchema.getRequired();
Expand All @@ -169,12 +140,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<JsonNode> 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"));
Expand Down
24 changes: 24 additions & 0 deletions doc/CRD-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "date-time"
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`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.
*
* <p>The following formats are validated by Kubernetes:</p>
* <ul>
* <li>{@code bsonobjectid}: a bson object ID, i.e. a 24 characters hex string</li>
* <li>{@code uri}: an URI as parsed by Golang net/url.ParseRequestURI</li>
* <li>{@code email}: an email address as parsed by Golang net/mail.ParseAddress</li>
* <li>{@code hostname}: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].</li>
* <li>{@code ipv4}: an IPv4 IP as parsed by Golang net.ParseIP</li>
* <li>{@code ipv6}: an IPv6 IP as parsed by Golang net.ParseIP</li>
* <li>{@code cidr}: a CIDR as parsed by Golang net.ParseCIDR</li>
* <li>{@code mac}: a MAC address as parsed by Golang net.ParseMAC</li>
* <li>{@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}$}</li>
* <li>{@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}$}</li>
* <li>{@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}$}</li>
* <li>{@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}$}</li>
* <li>{@code isbn}: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041"</li>
* <li>{@code isbn10}: an ISBN10 number string like "0321751043"</li>
* <li>{@code isbn13}: an ISBN13 number string like "978-0321751041"</li>
* <li>{@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</li>
* <li>{@code ssn}: a U.S. social security number following the regex {@code ^\d{3}[- ]?\d{2}[- ]?\d{4}$}</li>
* <li>{@code hexcolor}: an hexadecimal color code like "#FFFFFF: following the regex {@code ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$}</li>
* <li>{@code rgbcolor}: an RGB color code like rgb like "rgb(255,255,2559"</li>
* <li>{@code byte}: base64 encoded binary data</li>
* <li>{@code password}: any kind of string</li>
* <li>{@code date}: a date string like "2006-01-02" as defined by full-date in RFC3339</li>
* <li>{@code duration}: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration
* format</li>
* <li>{@code date-time}: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339.</li>
* </ul>
* <p>
* 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.
* </p>
*
* @see <a href=
* "https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#JSONSchemaProps">
* Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps
* </a>
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Format {
String value();
}
Loading

0 comments on commit 4fefc0b

Please sign in to comment.