diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java index 670e5a7c207..0fde0603d72 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java @@ -220,7 +220,9 @@ class PropertyMetadata { private String description; private String defaultValue; private Double min; + private Boolean exclusiveMinimum; private Double max; + private Boolean exclusiveMaximum; private String pattern; private boolean nullable; private String format; @@ -257,8 +259,19 @@ public PropertyMetadata(JsonSchema value, BeanProperty beanProperty) { // TODO: should probably move to a standard annotations // see ValidationSchemaFactoryWrapper nullable = beanProperty.getAnnotation(Nullable.class) != null; - max = ofNullable(beanProperty.getAnnotation(Max.class)).map(Max::value).orElse(max); - min = ofNullable(beanProperty.getAnnotation(Min.class)).map(Min::value).orElse(min); + + ofNullable(beanProperty.getAnnotation(Max.class)).ifPresent(a -> { + max = a.value(); + if (!a.inclusive()) { + exclusiveMaximum = true; + } + }); + ofNullable(beanProperty.getAnnotation(Min.class)).ifPresent(a -> { + min = a.value(); + if (!a.inclusive()) { + exclusiveMinimum = true; + } + }); // TODO: should the following be deprecated? required = beanProperty.getAnnotation(Required.class) != null; @@ -280,7 +293,9 @@ public void updateSchema(T schema) { schema.setNullable(nullable); } schema.setMaximum(max); + schema.setExclusiveMaximum(exclusiveMaximum); schema.setMinimum(min); + schema.setExclusiveMinimum(exclusiveMinimum); schema.setPattern(pattern); schema.setFormat(format); if (preserveUnknownFields) { @@ -513,8 +528,18 @@ private void handleTypeAnnotations(final T schema, BeanProperty beanProperty, Cl .map(a -> a[typeIndex]) .forEach(at -> { Optional.ofNullable(at.getAnnotation(Pattern.class)).ifPresent(a -> schema.setPattern(a.value())); - Optional.ofNullable(at.getAnnotation(Min.class)).ifPresent(a -> schema.setMinimum(a.value())); - Optional.ofNullable(at.getAnnotation(Max.class)).ifPresent(a -> schema.setMaximum(a.value())); + Optional.ofNullable(at.getAnnotation(Min.class)).ifPresent(a -> { + schema.setMinimum(a.value()); + if (!a.inclusive()) { + schema.setExclusiveMinimum(true); + } + }); + Optional.ofNullable(at.getAnnotation(Max.class)).ifPresent(a -> { + schema.setMaximum(a.value()); + if (!a.inclusive()) { + schema.setExclusiveMaximum(true); + } + }); }); } diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/KubernetesJSONSchemaProps.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/KubernetesJSONSchemaProps.java index e548f08b517..171bd1d823e 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/KubernetesJSONSchemaProps.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/KubernetesJSONSchemaProps.java @@ -32,8 +32,12 @@ public interface KubernetesJSONSchemaProps { void setMaximum(Double max); + void setExclusiveMaximum(Boolean b); + void setMinimum(Double min); + void setExclusiveMinimum(Boolean b); + void setPattern(String pattern); void setFormat(String format); diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.java index 77af9556817..dac0a54b884 100644 --- a/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.java +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.java @@ -28,6 +28,7 @@ import io.fabric8.crd.generator.approvaltests.map.ContainingMaps; import io.fabric8.crd.generator.approvaltests.nocyclic.NoCyclic; import io.fabric8.crd.generator.approvaltests.replica.Replica; +import io.fabric8.crd.generator.approvaltests.validation.Validation; import io.fabric8.kubernetes.client.CustomResource; import io.sundr.utils.Strings; import org.approvaltests.Approvals; @@ -72,7 +73,7 @@ void tearDown() { } @ParameterizedTest(name = "{1}.{2} parallel={3}") - @MethodSource("crdApprovalTests") + @MethodSource("crdApprovalTestsApiV1") @DisplayName("CRD Generator V1 Approval Tests") void apiV1ApprovalTest( Class>[] crClasses, String expectedCrd, String version, boolean parallel) { @@ -97,7 +98,7 @@ void apiV1ApprovalTest( } @ParameterizedTest(name = "{1}.{2} parallel={3}") - @MethodSource("crdV1ApprovalTests") + @MethodSource("crdApprovalTestsApiV2") @DisplayName("CRD Generator V2 Approval Tests") void apiV2ApprovalTest( Class>[] crClasses, String expectedCrd, String version, boolean parallel) { @@ -122,14 +123,16 @@ void apiV2ApprovalTest( new Namer(expectedCrd, version)); } - static Stream crdApprovalTests() { + static Stream crdApprovalTestsApiV1() { return Stream.concat( crdApprovalBaseCases("v1"), crdApprovalBaseCases("v1beta1")).map(tc -> Arguments.of(tc.crClasses, tc.expectedCrd, tc.version, tc.parallel)); } - static Stream crdV1ApprovalTests() { - return crdApprovalBaseCases("v1") + static Stream crdApprovalTestsApiV2() { + return Stream.concat( + crdApprovalBaseCases("v1"), + crdApprovalCasesApiV2("v1")) .map(tc -> Arguments.of(tc.crClasses, tc.expectedCrd, tc.version, tc.parallel)); } @@ -151,6 +154,14 @@ static Stream crdApprovalBaseCases(String crdVersion) { return cases.stream(); } + static Stream crdApprovalCasesApiV2(String crdVersion) { + final List cases = new ArrayList<>(); + for (boolean parallel : new boolean[] { false, true }) { + cases.add(new TestCase("validations.samples.fabric8.io", crdVersion, parallel, Validation.class)); + } + return cases.stream(); + } + private static final class TestCase { private final Class>[] crClasses; private final String expectedCrd; diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/Validation.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/Validation.java new file mode 100644 index 00000000000..bc77956e814 --- /dev/null +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/Validation.java @@ -0,0 +1,25 @@ +/* + * 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.crd.generator.approvaltests.validation; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; + +@Version("v1alpha1") +@Group("samples.fabric8.io") +public class Validation extends CustomResource { +} diff --git a/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/ValidationSpec.java b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/ValidationSpec.java new file mode 100644 index 00000000000..ddeb87bf4b2 --- /dev/null +++ b/crd-generator/test/src/test/java/io/fabric8/crd/generator/approvaltests/validation/ValidationSpec.java @@ -0,0 +1,186 @@ +/* + * 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.crd.generator.approvaltests.validation; + +import io.fabric8.generator.annotation.Max; +import io.fabric8.generator.annotation.Min; +import io.fabric8.generator.annotation.Pattern; +import lombok.Data; + +@Data +public class ValidationSpec { + + private ValidationOnInteger onInteger; + private ValidationOnIntegerPrim onIntegerPrim; + private ValidationOnLong onLong; + private ValidationOnLongPrim onLongPrim; + private ValidationOnFloat onFloat; + private ValidationOnFloatPrim onFloatPrim; + private ValidationOnDouble onDouble; + private ValidationOnDoublePrim onDoublePrim; + private ValidationOnString onString; + + @Data + static class ValidationOnInteger { + @Min(1) + private Integer minimum1; + @Min(value = 1, inclusive = false) + private Integer minimumExclusive1; + @Max(1) + private Integer maximum1; + @Max(value = 1, inclusive = false) + private Integer maximumExclusive1; + @Min(1) + @Max(3) + private Integer minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private Integer minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnIntegerPrim { + @Min(1) + private int minimum1; + @Min(value = 1, inclusive = false) + private int minimumExclusive1; + @Max(1) + private int maximum1; + @Max(value = 1, inclusive = false) + private int maximumExclusive1; + @Min(1) + @Max(3) + private int minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private int minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnLong { + @Min(1) + private Long minimum1; + @Min(value = 1, inclusive = false) + private Long minimumExclusive1; + @Max(1) + private Long maximum1; + @Max(value = 1, inclusive = false) + private Long maximumExclusive1; + @Min(1) + @Max(3) + private Long minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private Long minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnLongPrim { + @Min(1) + private long minimum1; + @Min(value = 1, inclusive = false) + private long minimumExclusive1; + @Max(1) + private long maximum1; + @Max(value = 1, inclusive = false) + private long maximumExclusive1; + @Min(1) + @Max(3) + private long minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private long minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnFloat { + @Min(1) + private Float minimum1; + @Min(value = 1, inclusive = false) + private Float minimumExclusive1; + @Max(1) + private Float maximum1; + @Max(value = 1, inclusive = false) + private Float maximumExclusive1; + @Min(1) + @Max(3) + private Float minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private Float minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnFloatPrim { + @Min(1) + private float minimum1; + @Min(value = 1, inclusive = false) + private float minimumExclusive1; + @Max(1) + private float maximum1; + @Max(value = 1, inclusive = false) + private float maximumExclusive1; + @Min(1) + @Max(3) + private float minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private float minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnDouble { + @Min(1) + private Double minimum1; + @Min(value = 1, inclusive = false) + private Double minimumExclusive1; + @Max(1) + private Double maximum1; + @Max(value = 1, inclusive = false) + private Double maximumExclusive1; + @Min(1) + @Max(3) + private Double minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private Double minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnDoublePrim { + @Min(1) + private double minimum1; + @Min(value = 1, inclusive = false) + private double minimumExclusive1; + @Max(1) + private double maximum1; + @Max(value = 1, inclusive = false) + private double maximumExclusive1; + @Min(1) + @Max(3) + private double minimum1Maximum3; + @Min(value = 1, inclusive = false) + @Max(value = 3, inclusive = false) + private double minimumExclusive1MaximumExclusive3; + } + + @Data + static class ValidationOnString { + @Pattern("(a|b)+") + private String pattern; + } + +} diff --git a/crd-generator/test/src/test/resources/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.approvalTest.validations.samples.fabric8.io.v1.approved.yml b/crd-generator/test/src/test/resources/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.approvalTest.validations.samples.fabric8.io.v1.approved.yml new file mode 100644 index 00000000000..24dbfaf842e --- /dev/null +++ b/crd-generator/test/src/test/resources/io/fabric8/crd/generator/approvaltests/CRDGeneratorApprovalTest.approvalTest.validations.samples.fabric8.io.v1.approved.yml @@ -0,0 +1,247 @@ +# Generated by Fabric8 CRDGenerator, manual edits might get overwritten! +apiVersion: "apiextensions.k8s.io/v1" +kind: "CustomResourceDefinition" +metadata: + name: "validations.samples.fabric8.io" +spec: + group: "samples.fabric8.io" + names: + kind: "Validation" + plural: "validations" + singular: "validation" + scope: "Cluster" + versions: + - name: "v1alpha1" + schema: + openAPIV3Schema: + properties: + spec: + properties: + onDouble: + properties: + maximum1: + maximum: 1.0 + type: "number" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "number" + minimum1: + minimum: 1.0 + type: "number" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "number" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "number" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "number" + type: "object" + onDoublePrim: + properties: + maximum1: + maximum: 1.0 + type: "number" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "number" + minimum1: + minimum: 1.0 + type: "number" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "number" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "number" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "number" + type: "object" + onFloat: + properties: + maximum1: + maximum: 1.0 + type: "number" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "number" + minimum1: + minimum: 1.0 + type: "number" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "number" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "number" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "number" + type: "object" + onFloatPrim: + properties: + maximum1: + maximum: 1.0 + type: "number" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "number" + minimum1: + minimum: 1.0 + type: "number" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "number" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "number" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "number" + type: "object" + onInteger: + properties: + maximum1: + maximum: 1.0 + type: "integer" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "integer" + minimum1: + minimum: 1.0 + type: "integer" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "integer" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "integer" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "integer" + type: "object" + onIntegerPrim: + properties: + maximum1: + maximum: 1.0 + type: "integer" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "integer" + minimum1: + minimum: 1.0 + type: "integer" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "integer" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "integer" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "integer" + type: "object" + onLong: + properties: + maximum1: + maximum: 1.0 + type: "integer" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "integer" + minimum1: + minimum: 1.0 + type: "integer" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "integer" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "integer" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "integer" + type: "object" + onLongPrim: + properties: + maximum1: + maximum: 1.0 + type: "integer" + maximumExclusive1: + exclusiveMaximum: true + maximum: 1.0 + type: "integer" + minimum1: + minimum: 1.0 + type: "integer" + minimum1Maximum3: + maximum: 3.0 + minimum: 1.0 + type: "integer" + minimumExclusive1: + exclusiveMinimum: true + minimum: 1.0 + type: "integer" + minimumExclusive1MaximumExclusive3: + exclusiveMaximum: true + exclusiveMinimum: true + maximum: 3.0 + minimum: 1.0 + type: "integer" + type: "object" + onString: + properties: + pattern: + pattern: "(a|b)+" + type: "string" + type: "object" + type: "object" + status: + type: "object" + type: "object" + served: true + storage: true diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java index 7489307428b..5d1d4e77f86 100644 --- a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Max.java @@ -31,5 +31,17 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface Max { + /** + * @return the value of the minimum constraint + */ double value(); + + /** + * Specifies whether the specified maximum is inclusive or exclusive. + * By default, it is inclusive. + * + * @return {@code true} if the value must be higher or equal to the specified minimum, + * {@code false} if the value must be higher + */ + boolean inclusive() default true; } diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java index 2a472ceedb4..6694d5d615f 100644 --- a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Min.java @@ -31,5 +31,17 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface Min { + /** + * @return the value of the minimum constraint + */ double value(); + + /** + * Specifies whether the specified minimum is inclusive or exclusive. + * By default, it is inclusive. + * + * @return {@code true} if the value must be higher or equal to the specified minimum, + * {@code false} if the value must be higher + */ + boolean inclusive() default true; }