Skip to content

Commit

Permalink
feat(crd-generator): Add support for validation rules
Browse files Browse the repository at this point in the history
  • Loading branch information
baloo42 committed Mar 12, 2024
1 parent 464e32e commit 42e34c6
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.fabric8.crd.generator.InternalSchemaSwaps.SwapResult;
import io.fabric8.crd.generator.annotation.SchemaSwap;
import io.fabric8.crd.generator.utils.Types;
import io.fabric8.generator.annotation.ValidationRule;
import io.fabric8.kubernetes.api.model.Duration;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Quantity;
Expand Down Expand Up @@ -48,6 +49,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static io.sundr.model.utils.Types.BOOLEAN_REF;
import static io.sundr.model.utils.Types.DOUBLE_REF;
Expand Down Expand Up @@ -111,6 +113,8 @@ public abstract class AbstractJsonSchema<T, B> {
public static final String ANNOTATION_PERSERVE_UNKNOWN_FIELDS = "io.fabric8.crd.generator.annotation.PreserveUnknownFields";
public static final String ANNOTATION_SCHEMA_SWAP = "io.fabric8.crd.generator.annotation.SchemaSwap";
public static final String ANNOTATION_SCHEMA_SWAPS = "io.fabric8.crd.generator.annotation.SchemaSwaps";
public static final String ANNOTATION_VALIDATION_RULE = "io.fabric8.generator.annotation.ValidationRule";
public static final String ANNOTATION_VALIDATION_RULES = "io.fabric8.generator.annotation.ValidationRules";

public static final String JSON_NODE_TYPE = "com.fasterxml.jackson.databind.JsonNode";
public static final String ANY_TYPE = "io.fabric8.kubernetes.api.model.AnyType";
Expand Down Expand Up @@ -150,8 +154,8 @@ protected static class SchemaPropsOptions {
final String pattern;
final boolean nullable;
final boolean required;

final boolean preserveUnknownFields;
final List<KubernetesValidationRule> validationRules;

SchemaPropsOptions() {
defaultValue = null;
Expand All @@ -161,9 +165,11 @@ protected static class SchemaPropsOptions {
nullable = false;
required = false;
preserveUnknownFields = false;
validationRules = null;
}

public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern,
List<KubernetesValidationRule> validationRules,
boolean nullable, boolean required, boolean preserveUnknownFields) {
this.defaultValue = defaultValue;
this.min = min;
Expand All @@ -172,6 +178,7 @@ public SchemaPropsOptions(String defaultValue, Double min, Double max, String pa
this.nullable = nullable;
this.required = required;
this.preserveUnknownFields = preserveUnknownFields;
this.validationRules = validationRules;
}

public Optional<String> getDefault() {
Expand Down Expand Up @@ -201,6 +208,11 @@ public boolean getRequired() {
public boolean isPreserveUnknownFields() {
return preserveUnknownFields;
}

public Optional<List<KubernetesValidationRule>> getValidationRules() {
return Optional.ofNullable(validationRules)
.flatMap(rules -> rules.isEmpty() ? Optional.empty() : Optional.of(rules));
}
}

/**
Expand Down Expand Up @@ -332,6 +344,7 @@ private T internalFromImpl(TypeDef definition, Set<String> visited, InternalSche
facade.min,
facade.max,
facade.pattern,
facade.validationRules,
facade.nullable,
facade.required,
facade.preserveUnknownFields);
Expand Down Expand Up @@ -362,6 +375,7 @@ private static class PropertyOrAccessor {
private Double min;
private Double max;
private String pattern;
private List<KubernetesValidationRule> validationRules;
private boolean nullable;
private boolean required;
private boolean ignored;
Expand Down Expand Up @@ -428,6 +442,14 @@ public void process() {
case ANNOTATION_SCHEMA_FROM:
schemaFrom = extractClassRef(a.getParameters().get("type"));
break;
case ANNOTATION_VALIDATION_RULE:
validationRules = Collections.singletonList(KubernetesValidationRule.from(a));
break;
case ANNOTATION_VALIDATION_RULES:
validationRules = Arrays.stream(((ValidationRule[]) a.getParameters().get(VALUE)))
.map(KubernetesValidationRule::from)
.collect(Collectors.toList());
break;
}
});
}
Expand Down Expand Up @@ -456,6 +478,10 @@ public Optional<String> getPattern() {
return Optional.ofNullable(pattern);
}

public Optional<List<KubernetesValidationRule>> getValidationRules() {
return Optional.ofNullable(validationRules);
}

public boolean isRequired() {
return required;
}
Expand Down Expand Up @@ -510,6 +536,7 @@ private static class PropertyFacade {
private String nameContributedBy;
private String descriptionContributedBy;
private TypeRef schemaFrom;
private List<KubernetesValidationRule> validationRules;

public PropertyFacade(Property property, Map<String, Method> potentialAccessors, ClassRef schemaSwap) {
original = property;
Expand All @@ -533,6 +560,7 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
min = null;
max = null;
pattern = null;
validationRules = null;
}

public Property process() {
Expand Down Expand Up @@ -562,6 +590,7 @@ public Property process() {
min = p.getMin().orElse(min);
max = p.getMax().orElse(max);
pattern = p.getPattern().orElse(pattern);
validationRules = p.getValidationRules().orElse(validationRules);

if (p.isNullable()) {
nullable = true;
Expand All @@ -588,6 +617,69 @@ public Property process() {
}
}

protected static class KubernetesValidationRule {
protected String fieldPath;
protected String message;
protected String messageExpression;
protected Boolean optionalOldSelf;
protected String reason;
protected String rule;

public String getFieldPath() {
return fieldPath;
}

public String getMessage() {
return message;
}

public String getMessageExpression() {
return messageExpression;
}

public Boolean getOptionalOldSelf() {
return optionalOldSelf;
}

public String getReason() {
return reason;
}

public String getRule() {
return rule;
}

static KubernetesValidationRule from(AnnotationRef annotationRef) {
KubernetesValidationRule result = new KubernetesValidationRule();
result.rule = (String) annotationRef.getParameters().get(VALUE);
result.reason = mapNotEmpty((String) annotationRef.getParameters().get("reason"));
result.message = mapNotEmpty((String) annotationRef.getParameters().get("message"));
result.messageExpression = mapNotEmpty((String) annotationRef.getParameters().get("messageExpression"));
result.fieldPath = mapNotEmpty((String) annotationRef.getParameters().get("fieldPath"));
result.optionalOldSelf = ((Boolean) annotationRef.getParameters().get("optionalOldSelf")) ? true : null;
return result;
}

static KubernetesValidationRule from(ValidationRule validationRule) {
KubernetesValidationRule result = new KubernetesValidationRule();
result.rule = validationRule.value();
result.reason = mapNotEmpty(validationRule.reason());
result.message = mapNotEmpty(validationRule.message());
result.messageExpression = mapNotEmpty(validationRule.messageExpression());
result.fieldPath = mapNotEmpty(validationRule.fieldPath());
result.optionalOldSelf = validationRule.optionalOldSelf() ? true : null;
return result;
}

private static String mapNotEmpty(String s) {
if (s == null)
return null;
if (s.isEmpty())
return null;
return s;
}
}

private boolean isPotentialAccessor(Method method) {
final String name = method.getName();
return name.startsWith("is") || name.startsWith("get") || name.startsWith("set");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import io.fabric8.crd.generator.AbstractJsonSchema;
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.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRuleBuilder;
import io.sundr.model.Property;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;

import java.util.List;
import java.util.stream.Collectors;

import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER;

Expand Down Expand Up @@ -77,6 +80,10 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
options.getMax().ifPresent(schema::setMaximum);
options.getPattern().ifPresent(schema::setPattern);

options.getValidationRules()
.map(this::mapValidationRules)
.ifPresent(schema::setXKubernetesValidations);

if (options.isNullable()) {
schema.setNullable(true);
}
Expand Down Expand Up @@ -139,4 +146,21 @@ protected JSONSchemaProps addDescription(JSONSchemaProps schema, String descript
.withDescription(description)
.build();
}

private List<ValidationRule> mapValidationRules(List<KubernetesValidationRule> validationRules) {
return validationRules.stream()
.map(this::mapValidationRule)
.collect(Collectors.toList());
}

private ValidationRule mapValidationRule(KubernetesValidationRule validationRule) {
return new ValidationRuleBuilder()
.withRule(validationRule.getRule())
.withMessage(validationRule.getMessage())
.withMessageExpression(validationRule.getMessageExpression())
.withReason(validationRule.getReason())
.withFieldPath(validationRule.getFieldPath())
.withOptionalOldSelf(validationRule.getOptionalOldSelf())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
import io.fabric8.crd.generator.AbstractJsonSchema;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaProps;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.JSONSchemaPropsBuilder;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRule;
import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.ValidationRuleBuilder;
import io.sundr.model.Property;
import io.sundr.model.TypeDef;
import io.sundr.model.TypeRef;

import java.util.List;
import java.util.stream.Collectors;

import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER;

Expand Down Expand Up @@ -78,6 +81,10 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
options.getMax().ifPresent(schema::setMaximum);
options.getPattern().ifPresent(schema::setPattern);

options.getValidationRules()
.map(this::mapValidationRules)
.ifPresent(schema::setXKubernetesValidations);

if (options.isNullable()) {
schema.setNullable(true);
}
Expand Down Expand Up @@ -142,4 +149,21 @@ protected JSONSchemaProps addDescription(JSONSchemaProps schema, String descript
.withDescription(description)
.build();
}

private List<ValidationRule> mapValidationRules(List<KubernetesValidationRule> validationRules) {
return validationRules.stream()
.map(this::mapValidationRule)
.collect(Collectors.toList());
}

private ValidationRule mapValidationRule(KubernetesValidationRule validationRule) {
return new ValidationRuleBuilder()
.withRule(validationRule.getRule())
.withMessage(validationRule.getMessage())
.withMessageExpression(validationRule.getMessageExpression())
.withReason(validationRule.getReason())
.withFieldPath(validationRule.getFieldPath())
.withOptionalOldSelf(validationRule.getOptionalOldSelf())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.fabric8.generator.annotation.Nullable;
import io.fabric8.generator.annotation.Pattern;
import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Data;

@Data
Expand Down Expand Up @@ -54,6 +55,14 @@ public class AnnotatedSpec {

private boolean ignoredBar;

@ValidationRule("a.rule")
private String kubernetesValidation;

@ValidationRule("a.rule")
@ValidationRule("a.second.rule")
@ValidationRule("a.third.rule")
private String kubernetesValidations;

@JsonProperty("from-getter")
@JsonPropertyDescription("from-getter-description")
@Required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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.ValidationRule;
import io.sundr.model.TypeDef;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -102,7 +103,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, 13);
Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 15);

// check descriptions are present
assertTrue(spec.containsKey("from-field"));
Expand Down Expand Up @@ -177,6 +178,16 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
// check ignored fields
assertFalse(spec.containsKey("ignoredFoo"));
assertFalse(spec.containsKey("ignoredBar"));

final JSONSchemaProps kubernetesValidation = spec.get("kubernetesValidation");
final List<ValidationRule> kubernetesValidationRules = kubernetesValidation.getXKubernetesValidations();
assertNotNull(kubernetesValidationRules);
assertEquals(1, kubernetesValidationRules.size());

final JSONSchemaProps kubernetesValidationsRepeated = spec.get("kubernetesValidations");
final List<ValidationRule> kubernetesValidationsRepeatedRules = kubernetesValidationsRepeated.getXKubernetesValidations();
assertNotNull(kubernetesValidationsRepeatedRules);
assertEquals(3, kubernetesValidationsRepeatedRules.size());
}

@Test
Expand Down
Loading

0 comments on commit 42e34c6

Please sign in to comment.