Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use JsonFormat shape in crd-generator and java-generator #5917

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,6 +92,8 @@ public abstract class AbstractJsonSchema<T, B> {
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<JsonFormat.Shape, TypeRef> JSON_FORMAT_SHAPE_MAPPING = new HashMap<>();
private static final String VALUE = "value";

private static final String INT_OR_STRING_MARKER = "int_or_string";
Expand All @@ -107,6 +110,7 @@ public abstract class AbstractJsonSchema<T, B> {
.build();

private static final Map<TypeRef, String> 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";
Expand Down Expand Up @@ -151,6 +155,12 @@ public abstract class AbstractJsonSchema<T, B> {
// 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) {
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,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 +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;
Expand Down Expand Up @@ -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")
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.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;
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, 20);

// check descriptions are present
assertTrue(spec.containsKey("from-field"));
Expand All @@ -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<String, JSONSchemaPropsBuilder> 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<String> required = specSchema.getRequired();
Expand All @@ -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<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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.builder.Editable<C
@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;
}
Expand Down
Loading