diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd513eb618..c4c96211c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ * Fix #6880: LogWatch interface provides listeners on close stream event * Fix #6971: Exposed Istio v1 models in Istio Client DSL * Fix #6998: Removed unneeded dependency on javax.annotation:javax.annotation-api +* Fix #6999: (crd-generator) introduce JSONSchema annotation for increased control of schema output #### Dependency Upgrade * Fix #6829: Sundrio was upgraded to 0.200.3. In some rare circumstances nested method names will need to be changed. @@ -54,7 +55,7 @@ #### New Features * Fix #5993: Support for Kubernetes v1.31 (elli) -* Fix #6767: Support for Kubernetes v1.32 (penelope) +* Fix #6767: Support for Kubernetes v1.32 (penelope) * Fix #6777: Added Javadoc comments to all generated models * Fix #6802: (java-generator) Added support for required spec and status @@ -225,7 +226,7 @@ * Fix #5357: adding additional Quantity methods * Fix #5635: refined LeaderElector lifecycle and logging * Fix #5787: (crd-generator) add support for deprecated versions for generated CRDs -* Fix #5788: (crd-generator) add support for Kubernetes validation rules +* Fix #5788: (crd-generator) add support for Kubernetes validation rules * Fix #5735: Replace usages of `UUID.randomUUID()` with UUID created via AtomicLong #### New Features @@ -309,7 +310,7 @@ * Fix #5220: refinements and clarifications to the validation of names #### Dependency Upgrade -* Fix #5286: Update Fabric8 OpenShift Model as per OpenShift 4.13.12 +* Fix #5286: Update Fabric8 OpenShift Model as per OpenShift 4.13.12 * Fix #5373: Gradle base API based on v8.2.1 * Fix #5401: Upgrade Fabric8 Kubernetes Model to Kubernetes v1.28.2 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 b99e5b85052..54a3f95af94 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 @@ -41,6 +41,7 @@ import io.fabric8.crdv2.generator.InternalSchemaSwaps.SwapResult; import io.fabric8.crdv2.generator.ResolvingContext.GeneratorObjectSchema; import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.JSONSchema; import io.fabric8.generator.annotation.Max; import io.fabric8.generator.annotation.Min; import io.fabric8.generator.annotation.Nullable; @@ -73,7 +74,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -82,6 +82,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -97,7 +99,7 @@ public abstract class AbstractJsonSchema dependentClasses = new HashSet<>(); private final Set additionalPrinterColumns = new HashSet<>(); @@ -409,10 +411,8 @@ private Optional findMaxInSizeAnnotation(BeanProperty beanProperty) { private T resolveObject(LinkedHashMap visited, InternalSchemaSwaps schemaSwaps, JsonSchema jacksonSchema, String... ignore) { - Set ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) : Collections.emptySet(); - - final T objectSchema = singleProperty("object"); + Set ignores = Set.of(ignore); schemaSwaps = schemaSwaps.branchAnnotations(); final InternalSchemaSwaps swaps = schemaSwaps; @@ -426,6 +426,16 @@ private T resolveObject(LinkedHashMap visited, InternalSchemaSwa Class rawClass = gos.javaType.getRawClass(); collectDependentClasses(rawClass); + T classSchema = resolveSchemaAnnotation( + rawClass.getDeclaredAnnotation(JSONSchema.class), + rawClass, + true, + resolvingContext.ignoreJSONSchemaAnnotation); + + if (classSchema != null) { + return classSchema; + } + consumeRepeatingAnnotation(rawClass, SchemaSwap.class, ss -> { swaps.registerSwap(rawClass, ss.originalType(), @@ -434,12 +444,24 @@ private T resolveObject(LinkedHashMap visited, InternalSchemaSwa }); List required = new ArrayList<>(); + final T objectSchema = singleProperty("object"); - for (Map.Entry property : new TreeMap<>(gos.getProperties()).entrySet()) { + for (Map.Entry property : visibleProperties(gos.getProperties(), ignores).entrySet()) { String name = property.getKey(); - if (ignores.contains(name)) { + BeanProperty beanProperty = gos.beanProperties.get(property.getKey()); + Utils.checkNotNull(beanProperty, "CRD generation works only with bean properties"); + + T propSchema = resolveSchemaAnnotation( + beanProperty.getAnnotation(JSONSchema.class), + beanProperty.getType().getRawClass(), + false, + false); + + if (propSchema != null) { + addProperty(name, objectSchema, propSchema); continue; } + schemaSwaps = schemaSwaps.branchDepths(); SwapResult swapResult = schemaSwaps.lookupAndMark(rawClass, name); LinkedHashMap savedVisited = visited; @@ -447,9 +469,6 @@ private T resolveObject(LinkedHashMap visited, InternalSchemaSwa visited = new LinkedHashMap<>(); } - BeanProperty beanProperty = gos.beanProperties.get(property.getKey()); - Utils.checkNotNull(beanProperty, "CRD generation works only with bean properties"); - JsonSchema propertySchema = property.getValue(); PropertyMetadata propertyMetadata = new PropertyMetadata(propertySchema, beanProperty); @@ -500,6 +519,20 @@ private T resolveObject(LinkedHashMap visited, InternalSchemaSwa return objectSchema; } + private T resolveSchemaAnnotation(JSONSchema annotation, Class rawClass, boolean isTargetType, boolean ignoreAnnotation) { + if (annotation != null && !ignoreAnnotation) { + return fromAnnotation(rawClass, isTargetType, annotation); + } + return null; + } + + private static Map visibleProperties(Map properties, Set ignores) { + return new TreeMap<>( + properties.entrySet().stream() + .filter(e -> !ignores.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + } + private void collectDependentClasses(Class rawClass) { if (rawClass != null && !rawClass.getName().startsWith("java.") && dependentClasses.add(rawClass.getName())) { Stream.of(rawClass.getInterfaces()).forEach(this::collectDependentClasses); @@ -680,7 +713,73 @@ private Set findIgnoredEnumConstants(JavaType type) { return toIgnore; } - V from(ValidationRule validationRule) { + protected T fromAnnotation(Class rawClass, boolean isTargetType, JSONSchema schema) { + T result = mapImplementation(schema.implementation(), isTargetType); + + if (result == null) { + result = singleProperty(null); + } + + setIfDefined(schema.defaultValue(), v -> parseJson(v, rawClass), result::setDefault); + setIfDefined(schema.description(), result::setDescription); + setIfDefined(schema.exclusiveMaximum(), this::mapBoolean, result::setExclusiveMaximum); + setIfDefined(schema.exclusiveMinimum(), this::mapBoolean, result::setExclusiveMinimum); + setIfDefined(schema.format(), result::setFormat); + setIfDefined(schema.maximum(), result::setMaximum); + setIfDefined(schema.maxItems(), result::setMaxItems); + setIfDefined(schema.maxLength(), result::setMaxLength); + setIfDefined(schema.maxProperties(), result::setMaxProperties); + setIfDefined(schema.minimum(), result::setMinimum); + setIfDefined(schema.minItems(), result::setMinItems); + setIfDefined(schema.minLength(), result::setMinLength); + setIfDefined(schema.minProperties(), result::setMinProperties); + setIfDefined(schema.nullable(), this::mapBoolean, result::setNullable); + setIfDefined(schema.pattern(), result::setPattern); + setIfDefined(schema.required(), ArrayList::new, Arrays::asList, result::setRequired); + setIfDefined(schema.xKubernetesPreserveUnknownFields(), this::mapBoolean, result::setXKubernetesPreserveUnknownFields); + return result; + } + + protected static void setIfDefined(A value, Function transformer, Consumer mutator) { + setIfDefined(value, () -> null, transformer, mutator); + } + + protected static void setIfDefined(A value, Consumer mutator) { + setIfDefined(value, () -> null, Function.identity(), mutator); + } + + protected static void setIfDefined(A value, Supplier defaultValue, Function transformer, + Consumer mutator) { + if (JSONSchema.Undefined.isUndefined(value)) { + // Not defined in the annotation (the default), don't touch the model. + } else if (JSONSchema.Suppressed.isSuppressed(value)) { + // Suppressed in the annotation, return the model back to the default value. + mutator.accept(defaultValue.get()); + } else { + mutator.accept(transformer.apply(value)); + } + } + + protected JsonNode parseJson(String value, Class targetType) { + Optional> rawType = Optional.ofNullable(targetType); + + try { + Object typedValue = resolvingContext.kubernetesSerialization.unmarshal(value, rawType.orElse(Object.class)); + return resolvingContext.kubernetesSerialization.convertValue(typedValue, JsonNode.class); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot parse value '" + value + "' as valid YAML or JSON.", e); + } + } + + protected List parseJson(String[] values, Class targetType) { + return Arrays.stream(values).map(value -> parseJson(value, targetType)).collect(Collectors.toList()); + } + + protected Boolean mapBoolean(Class value) { + return value == JSONSchema.True.class ? Boolean.TRUE : Boolean.FALSE; + } + + protected V from(ValidationRule validationRule) { V result = newKubernetesValidationRule(); result.setRule(validationRule.value()); result.setReason(mapNotEmpty(validationRule.reason())); @@ -695,6 +794,8 @@ private static String mapNotEmpty(String s) { return Utils.isNullOrEmpty(s) ? null : s; } + protected abstract T mapImplementation(Class value, boolean isTargetType); + protected abstract V newKubernetesValidationRule(); /** diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/ResolvingContext.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/ResolvingContext.java index 785480ec384..15fea9bcae2 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/ResolvingContext.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/ResolvingContext.java @@ -93,6 +93,7 @@ public JsonObjectFormatVisitor expectObjectFormat(JavaType convertedType) { final KubernetesSerialization kubernetesSerialization; final Map uriToJacksonSchema; final boolean implicitPreserveUnknownFields; + final boolean ignoreJSONSchemaAnnotation; private static ObjectMapper OBJECT_MAPPER; @@ -112,21 +113,29 @@ public static ResolvingContext defaultResolvingContext(boolean implicitPreserveU } public ResolvingContext forkContext() { - return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields); + return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields, + ignoreJSONSchemaAnnotation); + } + + public ResolvingContext forkContext(boolean ignoreJSONSchemaAnnotation) { + return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields, + ignoreJSONSchemaAnnotation); } public ResolvingContext(ObjectMapper mapper, KubernetesSerialization kubernetesSerialization, boolean implicitPreserveUnknownFields) { - this(mapper, kubernetesSerialization, new ConcurrentHashMap<>(), implicitPreserveUnknownFields); + this(mapper, kubernetesSerialization, new ConcurrentHashMap<>(), implicitPreserveUnknownFields, false); } private ResolvingContext(ObjectMapper mapper, KubernetesSerialization kubernetesSerialization, Map uriToJacksonSchema, - boolean implicitPreserveUnknownFields) { + boolean implicitPreserveUnknownFields, + boolean ignoreJSONSchemaAnnotation) { this.uriToJacksonSchema = uriToJacksonSchema; this.objectMapper = mapper; this.kubernetesSerialization = kubernetesSerialization; this.implicitPreserveUnknownFields = implicitPreserveUnknownFields; + this.ignoreJSONSchemaAnnotation = ignoreJSONSchemaAnnotation; generator = new JsonSchemaGenerator(mapper, new WrapperFactory() { @Override diff --git a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/v1/JsonSchema.java b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/v1/JsonSchema.java index 18fff6c2f8a..43ccebddc22 100644 --- a/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/v1/JsonSchema.java +++ b/crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/v1/JsonSchema.java @@ -22,13 +22,26 @@ import io.fabric8.crdv2.generator.ResolvingContext; import io.fabric8.crdv2.generator.v1.JsonSchema.V1JSONSchemaProps; import io.fabric8.crdv2.generator.v1.JsonSchema.V1ValidationRule; +import io.fabric8.generator.annotation.JSONSchema; +import io.fabric8.kubernetes.api.model.apiextensions.v1.ExternalDocumentation; 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.JSONSchemaPropsOrArray; import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsOrBool; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsOrStringArray; import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule; +import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class JsonSchema extends AbstractJsonSchema { @@ -107,4 +120,200 @@ protected V1JSONSchemaProps raw() { return result; } + @Override + protected V1JSONSchemaProps fromAnnotation(Class rawClass, boolean isTargetType, JSONSchema schema) { + V1JSONSchemaProps result = super.fromAnnotation(rawClass, isTargetType, schema); + // maybe override the type if it was determined by reading the optional `implementation` + setIfDefined(schema.type(), result::setType); + setIfDefined(schema.$ref(), result::set$ref); + setIfDefined(schema.$schema(), result::set$schema); + setIfDefined(schema.additionalItems(), this::mapSchemaOrBool, result::setAdditionalItems); + setIfDefined(schema.additionalProperties(), this::mapSchemaOrBool, result::setAdditionalProperties); + setIfDefined(schema.definitions(), LinkedHashMap::new, this::mapSchemaMap, result::setDefinitions); + setIfDefined(schema.dependencies(), LinkedHashMap::new, this::mapDependencies, result::setDependencies); + setIfDefined(schema.enumeration(), ArrayList::new, v -> parseJson(v, rawClass), result::setEnum); + setIfDefined(schema.example(), v -> parseJson(v, rawClass), result::setExample); + setIfDefined(schema.externalDocs(), this::mapExternalDocs, result::setExternalDocs); + setIfDefined(schema.id(), result::setId); + setIfDefined(schema.multipleOf(), result::setMultipleOf); + setIfDefined(schema.patternProperties(), LinkedHashMap::new, this::mapSchemaMap, result::setPatternProperties); + setIfDefined(schema.title(), result::setTitle); + setIfDefined(schema.uniqueItems(), this::mapBoolean, result::setUniqueItems); + setIfDefined(schema.xKubernetesEmbeddedResource(), this::mapBoolean, result::setXKubernetesEmbeddedResource); + setIfDefined(schema.xKubernetesIntOrString(), this::mapBoolean, result::setXKubernetesIntOrString); + setIfDefined(schema.xKubernetesListMapKeys(), ArrayList::new, Arrays::asList, result::setXKubernetesListMapKeys); + setIfDefined(schema.xKubernetesListType(), result::setXKubernetesListType); + setIfDefined(schema.xKubernetesMapType(), result::setXKubernetesMapType); + setIfDefined(schema.xKubernetesValidations(), ArrayList::new, this::mapValidationRules, result::setXKubernetesValidations); + + if (schema.structural()) { + List allOf = new ArrayList<>(schema.allOf().length); + List anyOf = new ArrayList<>(schema.anyOf().length); + AtomicReference items = new AtomicReference<>(); + AtomicReference not = new AtomicReference<>(); + List oneOf = new ArrayList<>(schema.oneOf().length); + Map properties = new LinkedHashMap<>(); + + Consumer addProperties = s -> s.getProperties().forEach(properties::putIfAbsent); + /** + * Only sets the items if they have not yet been set. + */ + Consumer maybeSetItems = s -> items.compareAndSet(null, s.getItems()); + + setIfDefined(schema.items(), this::mapSchemaOrArray, items::set); + + setIfDefined(schema.allOf(), ArrayList::new, this::mapSchemaList, (List schemas) -> { + schemas.stream().map(this::mapJunctorSchema).forEach(allOf::add); + schemas.forEach(addProperties); + schemas.forEach(maybeSetItems); + }); + + setIfDefined(schema.anyOf(), ArrayList::new, this::mapSchemaList, (List schemas) -> { + schemas.stream().map(this::mapJunctorSchema).forEach(anyOf::add); + schemas.forEach(addProperties); + schemas.forEach(maybeSetItems); + }); + + setIfDefined(schema.not(), this::mapSchema, notSchema -> { + not.set(mapJunctorSchema(notSchema)); + addProperties.accept(notSchema); + maybeSetItems.accept(notSchema); + }); + + setIfDefined(schema.oneOf(), ArrayList::new, this::mapSchemaList, (List schemas) -> { + schemas.stream().map(this::mapJunctorSchema).forEach(oneOf::add); + schemas.forEach(addProperties); + schemas.forEach(maybeSetItems); + }); + + setIfDefined(schema.properties(), LinkedHashMap::new, this::mapSchemaMap, properties::putAll); + + result.setAllOf(allOf); + result.setAnyOf(anyOf); + result.setItems(items.get()); + result.setNot(not.get()); + result.setOneOf(oneOf); + result.setProperties(properties); + } else { + setIfDefined(schema.allOf(), ArrayList::new, this::mapSchemaList, result::setAllOf); + setIfDefined(schema.anyOf(), ArrayList::new, this::mapSchemaList, result::setAnyOf); + setIfDefined(schema.items(), this::mapSchemaOrArray, result::setItems); + setIfDefined(schema.not(), this::mapSchema, result::setNot); + setIfDefined(schema.oneOf(), ArrayList::new, this::mapSchemaList, result::setOneOf); + setIfDefined(schema.properties(), LinkedHashMap::new, this::mapSchemaMap, result::setProperties); + } + + return result; + } + + @Override + protected V1JSONSchemaProps mapImplementation(Class value, boolean isTargetType) { + if (JSONSchema.Undefined.isUndefined(value)) { + return null; // NOSONAR + } + return new JsonSchema(resolvingContext.forkContext(isTargetType), value).getSchema(); + } + + private JSONSchemaPropsOrBool mapSchemaOrBool(Class value) { + JSONSchemaPropsOrBool result = new JSONSchemaPropsOrBool(); + + if (JSONSchema.Boolean.class.isAssignableFrom(value)) { + @SuppressWarnings("unchecked") + Class booleanType = (Class) value; + result.setAllows(mapBoolean(booleanType)); + } else { + result.setSchema(new JsonSchema(resolvingContext.forkContext(false), value).getSchema()); + } + + return result; + } + + private JSONSchemaPropsOrArray mapSchemaOrArray(Class[] values) { + return mapSchemaOrArray(mapSchemaList(values)); + } + + private JSONSchemaPropsOrArray mapSchemaOrArray(List schemas) { + JSONSchemaPropsOrArray result = new JSONSchemaPropsOrArray(); + + if (schemas.size() == 1) { + result.setSchema(schemas.get(0)); + } else { + result.setJSONSchemas(schemas); + } + + return result; + } + + private JSONSchemaProps mapSchema(Class value) { + return new JsonSchema(resolvingContext.forkContext(false), value).getSchema(); + } + + private List mapSchemaList(Class[] values) { + return Arrays.stream(values).map(this::mapSchema).collect(Collectors.toList()); + } + + /** + * Suppress attributes disallowed within a logical junctor (allOf, anyOf, oneOf, not). This functionality + * is in support of rule #3 from "Specifying a structural schema". + * + * @see https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema + */ + private JSONSchemaProps mapJunctorSchema(JSONSchemaProps schema) { + JSONSchemaPropsBuilder builder = schema.edit() + .withDescription(null) + .withDefault(null) + .withAdditionalProperties(null) + .withNullable(null) + .withItems(Optional.ofNullable(schema.getItems()) + .map(items -> Stream + .concat( + Optional.ofNullable(items.getJSONSchemas()) + .map(List::stream) + .orElseGet(Stream::empty), + Stream.of(items.getSchema())) + .filter(Objects::nonNull) + .map(this::mapJunctorSchema) + .collect(Collectors.toList())) + .map(this::mapSchemaOrArray) + .orElse(null)) + .withProperties(schema.getProperties().entrySet() + .stream() + // Recursively remove property attributes + .map(e -> Map.entry(e.getKey(), mapJunctorSchema(e.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + + if (!Boolean.TRUE.equals(schema.getXKubernetesIntOrString())) { + builder.withType(null); + } + + return builder.build(); + } + + private Map mapSchemaMap(JSONSchema.Map[] entries) { + return Arrays.stream(entries) + .map(e -> Map.entry(e.name(), mapSchema(e.value()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Map mapDependencies(JSONSchema.DependencyMap[] entries) { + return Arrays.stream(entries) + .map(e -> { + JSONSchemaPropsOrStringArray result = new JSONSchemaPropsOrStringArray(); + setIfDefined(e.value().properties(), ArrayList::new, Arrays::asList, result::setProperty); + setIfDefined(e.value().schema(), this::mapSchema, result::setSchema); + return Map.entry(e.name(), result); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private ExternalDocumentation mapExternalDocs(JSONSchema.ExternalDocumentation externalDocs) { + ExternalDocumentation result = new ExternalDocumentation(); + setIfDefined(externalDocs.description(), result::setDescription); + setIfDefined(externalDocs.url(), result::setUrl); + return result; + } + + private List mapValidationRules(io.fabric8.generator.annotation.ValidationRule[] values) { + return Arrays.stream(values).map(super::from).collect(Collectors.toList()); + } } diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnno.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnno.java new file mode 100644 index 00000000000..8024403f18e --- /dev/null +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnno.java @@ -0,0 +1,22 @@ +/* + * 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.crdv2.example.jsonschema; + +import io.fabric8.kubernetes.client.CustomResource; + +public class JsonSchemaAnno extends CustomResource { + private static final long serialVersionUID = 1L; +} diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoSpec.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoSpec.java new file mode 100644 index 00000000000..f73dabb16bc --- /dev/null +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoSpec.java @@ -0,0 +1,189 @@ +/* + * 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.crdv2.example.jsonschema; + +import io.fabric8.generator.annotation.JSONSchema; +import io.fabric8.generator.annotation.JSONSchema.ExternalDocumentation; +import io.fabric8.generator.annotation.ValidationRule; +import lombok.Data; + +import java.util.List; + +@Data +public class JsonSchemaAnnoSpec { + + @JSONSchema(type = "string") + private Object customizedType; + + /* *********************************************************************** */ + + @JSONSchema(externalDocs = @ExternalDocumentation(url = "https://example.com/docs.txt")) + private String documentedExternally; + + /* *********************************************************************** */ + + @JSONSchema(implementation = StrictItemsSchema.class, additionalProperties = JSONSchema.False.class) + @Data + static class StrictItemsSchema { + String field1; + int field2; + } + + @JSONSchema(type = "array", additionalItems = JSONSchema.False.class, items = StrictItemsSchema.class) + private List strictItems; + + /* *********************************************************************** */ + + @JSONSchema(implementation = LaxItemsSchema1.class, additionalProperties = JSONSchema.True.class) + @Data + static class LaxItemsSchema1 { + String field1; + double field2; + } + + // besides field1 and field2, allows additional properties if they are objects of type LaxItemsSchema1 + @JSONSchema(implementation = LaxItemsSchema2.class, additionalProperties = LaxItemsSchema1.class) + @Data + static class LaxItemsSchema2 { + String field1; + int field2; + } + + @JSONSchema(type = "array", additionalItems = JSONSchema.True.class, items = { LaxItemsSchema1.class, + LaxItemsSchema2.class }) + private List laxItems; + + /* *********************************************************************** */ + + @JSONSchema(implementation = OverriddenPropertiesSchema.class, description = "Has properties that are replaced by referencing type") + @Data + static class OverriddenPropertiesSchema { + String field1; + int field2; + } + + @JSONSchema(type = "object", properties = { + @JSONSchema.Map(name = "field3", value = Long.class), + @JSONSchema.Map(name = "field4", value = Double.class), + }, implementation = OverriddenPropertiesSchema.class) + private OverriddenPropertiesSchema overriddenProperties; + + /* *********************************************************************** */ + + @Data + static class ObjectEnumerationSchema { + String field1; + int field2; + } + + @JSONSchema(type = "object", implementation = ObjectEnumerationSchema.class, defaultValue = "{ \"field1\": \"allowedValue1\", \"field2\": 1 }", enumeration = { + "{ \"field1\": \"allowedValue1\", \"field2\": 1 }", + "{ \"field1\": \"allowedValue2\", \"field2\": 2 }", + }, example = "{ \"field1\": \"allowedValue2\", \"field2\": 2 }") + private OverriddenPropertiesSchema objectEnumeration; + + /* *********************************************************************** */ + + @JSONSchema(type = "integer", nullable = JSONSchema.True.class, minimum = 0d, maximum = 100d, exclusiveMaximum = JSONSchema.True.class) + static class NullableIntegerWithRange { + } + + @Data + @JSONSchema(type = "object", implementation = ObjectEnumerationSchema.class, dependencies = { + // field1 requires the presence of field2 + @JSONSchema.DependencyMap(name = "field1", value = @JSONSchema.Dependency(properties = "field2")), + // field2 requires that field1 is null or an integer between 0 and 100 (exclusive) + @JSONSchema.DependencyMap(name = "field2", value = @JSONSchema.Dependency(schema = NullableIntegerWithRange.class)) + }) + static class DependentPropertiesSchema { + String field1; + Integer field2; + } + + private DependentPropertiesSchema dependentProperties; + + /* *********************************************************************** */ + + @Data + @JSONSchema(implementation = SuppressionSchema.class, additionalProperties = String.class, minProperties = 1, maxProperties = 10, example = "{ \"field2\": 42 }", xKubernetesValidations = @ValidationRule("some rule")) + static class SuppressionSchema { + String field1; + Integer field2; + @JSONSchema(minItems = 3) + List field3; + } + + @JSONSchema(implementation = SuppressionSchema.class, additionalProperties = JSONSchema.Suppressed.class, minProperties = JSONSchema.Suppressed.LONG, example = JSONSchema.Suppressed.STRING, xKubernetesValidations = {}) + private SuppressionSchema suppression; + + /* *********************************************************************** */ + + @Data + static class StructuralSchema1 { + String string1; + } + + @Data + static class StructuralSchema2 { + String string2; + } + + @Data + static class StructuralSchema3 { + String string3; + StructuralSchema1 structural3; + } + + @Data + static class StructuralSchema4 { + List string4; + } + + @Data + static class StructuralSchema5 { + String string5; + } + + @Data + static class StructuralSchema6 { + String string6; + } + + @Data + static class StructuralSchema7 { + String string7; + StructuralSchema8 intOrString7; + } + + @Data + @JSONSchema(xKubernetesIntOrString = JSONSchema.True.class, anyOf = { Integer.class, String.class }) + static class StructuralSchema8 { + String string8; + Integer integer8; + } + + @Data + @JSONSchema(structural = true, allOf = { StructuralSchema4.class, StructuralSchema5.class }, anyOf = { + StructuralSchema1.class, + StructuralSchema3.class }, not = StructuralSchema2.class, oneOf = { StructuralSchema6.class, StructuralSchema7.class }) + static class StructuralSchema { + String string; + Integer integer; + } + + private StructuralSchema structural; + +} diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoStatus.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoStatus.java new file mode 100644 index 00000000000..f827016b09f --- /dev/null +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/example/jsonschema/JsonSchemaAnnoStatus.java @@ -0,0 +1,23 @@ +/* + * 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.crdv2.example.jsonschema; + +import lombok.Data; + +@Data +public class JsonSchemaAnnoStatus { + +} diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaAnnotationTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaAnnotationTest.java new file mode 100644 index 00000000000..48c66e5f418 --- /dev/null +++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/v1/JsonSchemaAnnotationTest.java @@ -0,0 +1,209 @@ +/* + * 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.crdv2.generator.v1; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.fabric8.crdv2.example.jsonschema.JsonSchemaAnno; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps; +import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsOrStringArray; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JsonSchemaAnnotationTest { + + JSONSchemaProps schema; + + @BeforeEach + void setup() { + schema = JsonSchema.from(JsonSchemaAnno.class); + assertNotNull(schema); + } + + @Test + void testCustomizedType() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("customizedType"); + assertEquals("string", target.getType()); + } + + @Test + void testExternalDocumentation() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("documentedExternally"); + assertNull(target.getType()); // type not defined in @JSONSchema + assertNotNull(target.getExternalDocs()); + assertEquals("https://example.com/docs.txt", target.getExternalDocs().getUrl()); + } + + @Test + void testStrictArrayItemSchema() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("strictItems"); + assertEquals("array", target.getType()); + assertEquals(Boolean.FALSE, target.getAdditionalItems().getAllows()); + assertNotNull(target.getItems()); + JSONSchemaProps items = target.getItems().getSchema(); + assertEquals("object", items.getType()); + assertEquals(Boolean.FALSE, items.getAdditionalProperties().getAllows()); + assertTrue(items.getProperties().keySet().containsAll(List.of("field1", "field2"))); + } + + @Test + void testLaxArrayItemSchema() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("laxItems"); + assertEquals("array", target.getType()); + assertEquals(Boolean.TRUE, target.getAdditionalItems().getAllows()); + assertNotNull(target.getItems()); + List itemsSchemas = target.getItems().getJSONSchemas(); + assertEquals(2, itemsSchemas.size()); + assertEquals(Set.of("field1", "field2"), itemsSchemas.get(0).getProperties().keySet()); + assertEquals(Set.of("field1", "field2"), itemsSchemas.get(1).getProperties().keySet()); + + assertEquals(Boolean.TRUE, itemsSchemas.get(0).getAdditionalProperties().getAllows()); + assertNull(itemsSchemas.get(0).getAdditionalProperties().getSchema()); + assertEquals(itemsSchemas.get(0), itemsSchemas.get(1).getAdditionalProperties().getSchema()); + assertNull(itemsSchemas.get(1).getAdditionalProperties().getAllows()); + } + + @Test + void testOverriddenProperties() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("overriddenProperties"); + assertEquals("object", target.getType()); + assertEquals(Set.of("field3", "field4"), target.getProperties().keySet()); + assertEquals("Has properties that are replaced by referencing type", target.getDescription()); + } + + @Test + void testObjectEnumerationWithDefaultAndExample() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("objectEnumeration"); + ObjectNode expected1 = JsonNodeFactory.instance.objectNode().put("field1", "allowedValue1").put("field2", 1); + ObjectNode expected2 = JsonNodeFactory.instance.objectNode().put("field1", "allowedValue2").put("field2", 2); + assertEquals(expected1, target.getDefault()); + assertEquals(expected2, target.getExample()); + assertEquals(List.of(expected1, expected2), target.getEnum()); + } + + @Test + void testDependencies() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("dependentProperties"); + JSONSchemaPropsOrStringArray field1Deps = target.getDependencies().get("field1"); + assertNull(field1Deps.getSchema()); + assertEquals(List.of("field2"), field1Deps.getProperty()); + JSONSchemaPropsOrStringArray field2Deps = target.getDependencies().get("field2"); + assertEquals(Collections.emptyList(), field2Deps.getProperty()); + assertEquals("integer", field2Deps.getSchema().getType()); + assertEquals(Boolean.TRUE, field2Deps.getSchema().getNullable()); + assertEquals(Double.valueOf(0), field2Deps.getSchema().getMinimum()); + assertNull(field2Deps.getSchema().getExclusiveMinimum()); + assertEquals(Double.valueOf(100), field2Deps.getSchema().getMaximum()); + assertEquals(Boolean.TRUE, field2Deps.getSchema().getExclusiveMaximum()); + } + + static class RecursiveTypeChecker { + final AtomicInteger count = new AtomicInteger(0); + final BiConsumer assertion; + + RecursiveTypeChecker(BiConsumer assertion) { + this.assertion = assertion; + } + + Stream itemsToSchemas(JSONSchemaProps schema) { + return Optional.ofNullable(schema.getItems()) + .map(items -> Stream + .concat( + Optional.ofNullable(items.getJSONSchemas()) + .map(List::stream) + .orElseGet(Stream::empty), + Stream.of(items.getSchema())) + .filter(Objects::nonNull)) + .orElseGet(Stream::empty); + } + + void accept(JSONSchemaProps schema, boolean checkProperties) { + List directSchemas = Stream.of( + schema.getAllOf().stream(), + schema.getAnyOf().stream(), + Stream.of(schema.getNot()), + schema.getOneOf().stream(), + checkProperties ? itemsToSchemas(schema) : Stream. empty(), + checkProperties ? schema.getProperties().values().stream() : Stream. empty()) + .flatMap(Function.identity()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + for (JSONSchemaProps s : directSchemas) { + assertion.accept(s, schema); + accept(s, true); + count.incrementAndGet(); + } + } + } + + @Test + void testStructuralSchemaJunctorRemovedTypes() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("structural"); + RecursiveTypeChecker checker = new RecursiveTypeChecker((childSchema, parentSchema) -> { + // Type is allowed with a schema having `x-kubernetes-int-or-string: true` + if (!Boolean.TRUE.equals(parentSchema.getXKubernetesIntOrString())) { + assertNull(childSchema.getType()); + } + }); + checker.accept(target, false); + assertEquals(20, checker.count.get()); + } + + @Test + void testStructuralSchemaJunctorPropertiesCopied() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("structural"); + assertEquals(9, target.getProperties().size()); + assertEquals("string", target.getProperties().get("string1").getType()); + assertEquals("string", target.getProperties().get("string2").getType()); + assertEquals("string", target.getProperties().get("string3").getType()); + assertEquals("object", target.getProperties().get("structural3").getType()); + assertEquals("array", target.getProperties().get("string4").getType()); + assertEquals("string", target.getProperties().get("string4").getItems().getSchema().getType()); + assertEquals("string", target.getProperties().get("string5").getType()); + assertEquals("string", target.getProperties().get("string6").getType()); + assertEquals("string", target.getProperties().get("string7").getType()); + } + + @Test + void testStructuralSchemaJunctorIntOrString() { + JSONSchemaProps target = schema.getProperties().get("spec").getProperties().get("structural"); + assertEquals(2, target.getOneOf().size()); + JSONSchemaProps intOrStringSchema = target.getOneOf().get(1).getProperties().get("intOrString7"); + assertNull(intOrStringSchema.getType()); + assertEquals(Boolean.TRUE, intOrStringSchema.getXKubernetesIntOrString()); + assertEquals(2, intOrStringSchema.getAnyOf().size()); + assertEquals("integer", intOrStringSchema.getAnyOf().get(0).getType()); + assertEquals("string", intOrStringSchema.getAnyOf().get(1).getType()); + } + +} diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/JSONSchema.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/JSONSchema.java new file mode 100644 index 00000000000..47dcaa7cd23 --- /dev/null +++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/JSONSchema.java @@ -0,0 +1,349 @@ +/* + * 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; +import java.lang.reflect.Array; + +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface JSONSchema { + + /** + * Marker interface used to restrict the class types that may be used for + * properties of type boolean. This enables readers of the annotation to + * distinguish between true, false, and undefined values. + */ + interface Boolean { + } + + /** + * Marker class used as a default for annotation properties of type + * {@code Class}. + */ + public static final class Undefined implements Boolean { + /** + * Marker value used as a default for annotation properties of type + * {@code String}. + */ + public static final String STRING = "io.fabric8.generator.annotation.JSONSchema.Undefined"; + + /** + * Marker value used as a default for annotation properties of type + * {@code double}. + */ + public static final double DOUBLE = Double.POSITIVE_INFINITY; + + /** + * Marker value used as a default for annotation properties of type + * {@code long}. Same binary value used for Double.POSITIVE_INFINITY. + */ + public static final long LONG = 0x7ff0000000000000L; + + private Undefined() { + } + + public static boolean isUndefined(Object value) { + if (value == Undefined.class) { + return true; + } + if (STRING.equals(value)) { + return true; + } + if (value instanceof Double && (Double) value == DOUBLE) { + return true; + } + if (value instanceof Long && (Long) value == LONG) { + return true; + } + if (value instanceof Map) { + return isUndefined(((Map) value).value()); + } + if (value instanceof DependencyMap) { + return isUndefined(((DependencyMap) value).value()); + } + if (value instanceof Dependency) { + return isUndefined(((Dependency) value).schema()); + } + if (value instanceof ExternalDocumentation) { + return isUndefined(((ExternalDocumentation) value).url()); + } + if (value instanceof ValidationRule) { + return isUndefined(((ValidationRule) value).value()); + } + return isUndefinedArray(value); + } + + private static boolean isUndefinedArray(Object value) { + if (value.getClass().isArray() && Array.getLength(value) == 1) { + return isUndefined(Array.get(value, 0)); + } + return false; + } + } + + /** + * Marker class used to suppress annotation properties of type {@code Class} that may have been + * set by the use of an `implementation` class. + */ + public static final class Suppressed implements Boolean { + /** + * Marker value used to suppress annotation properties of type + * {@code String} that may have been set by an {@code implementation} class. + */ + public static final String STRING = "io.fabric8.generator.annotation.JSONSchema.Suppressed"; + + /** + * Marker value used to suppress annotation properties of type + * {@code double} that may have been set by an {@code implementation} class. + */ + public static final double DOUBLE = Double.NEGATIVE_INFINITY; + + /** + * Marker value used to suppress annotation properties of type + * {@code long} that may have been set by an {@code implementation} class. + */ + public static final long LONG = 0xfff0000000000000L; + + private Suppressed() { + } + + public static boolean isSuppressed(Object value) { + if (value == Suppressed.class) { + return true; + } + if (STRING.equals(value)) { + return true; + } + if (value instanceof Double && (Double) value == DOUBLE) { + return true; + } + if (value instanceof Long && (Long) value == LONG) { + return true; + } + if (value instanceof ExternalDocumentation) { + return isSuppressed(((ExternalDocumentation) value).url()); + } + return isSuppressedArray(value); + } + + private static boolean isSuppressedArray(Object value) { + if (value.getClass().isArray()) { + switch (Array.getLength(value)) { + case 0: + // Any of the annotation arrays can be set to length zero to indicate suppression. + return true; + case 1: + return isSuppressed(Array.get(value, 0)); + default: + break; + } + } + return false; + } + } + + /** + * Marker class to indicate that a boolean {@code true} schema should be used. + * Additionally, this class is used to set a value of {@code true} for + * properties that allow true, false, or undefined boolean values. + */ + public static final class True implements Boolean { + private True() { + } + } + + /** + * Marker class to indicate that a boolean {@code false} schema should be used. + * Additionally, this class is used to set a value of {@code false} for + * properties that allow true, false, or undefined boolean values. + */ + public static final class False implements Boolean { + private False() { + } + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface Map { + String name(); + + Class value(); + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface Dependency { + String[] properties() default {}; + + Class schema() default Undefined.class; + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface DependencyMap { + String name(); + + Dependency value(); + } + + @Target({}) + @Retention(RetentionPolicy.RUNTIME) + public @interface ExternalDocumentation { + String description() default Undefined.STRING; + + String url(); + } + + /** + * The implementation class allows for an additional type to be scanned as the + * basis for this schema. After scanning the implementation (if specified), the + * remaining properties will be set from this annotation, possibly overriding + * those determine by scanning the implementation class. + */ + Class implementation() default Undefined.class; + + /** + * When true, this property indicates that the schema for this element is structural. The CRD + * generator will enhance the schema to conform to rules 2 and 3 of a Kubernetes structural schema. + *

+ * From the Kubernetes documentation, these rules are as follows (rules 1 and 4 are omitted since they are not relevant to the + * processing enabled by {@linkplain #structural}). + *

    + *
  1. for each field in an object and each item in an array which is specified within any of {@code allOf}, {@code anyOf}, + * {@code oneOf} or {@code not}, the schema also specifies the field/item outside of those logical junctors
  2. + *
  3. does not set {@code description}, {@code type}, {@code default}, {@code additionalProperties}, {@code nullable} within + * an {@code allOf}, {@code anyOf}, {@code oneOf} or {@code not}, with the exception of the two pattern for + * {@code x-kubernetes-int-or-string: true}
  4. + *
+ *

+ * This means that if this schema specifies classes within one or more of the logical junctors ({@code allOf}, {@code anyOf}, + * {@code oneOf} or {@code not}), the CRD generator will + * ensure that + *

    + *
  1. Each property/item of each entry within the logical junctors is also added to the properties of the structural schema. + * If a property name is duplicated, only the first occurrence will be used. If two or more schemas specify array + * {@code items}, only the first {@code items} will be used. Finally, if this schema itself specifies {@code items}, any + * {@code items} of the schemas within the logical junctors will not be used.
  2. + *
  3. The forbidden attributes of schemas within the logical junctors will be removed. These are {@code description}, + * {@code type}, {@code default}, {@code additionalProperties}, and {@code nullable}. Note that these attributes will be + * present in the schemas that are set in the properties in step 1.
  4. + *
+ * + * @see Extend + * the Kubernetes API with CustomResourceDefinitions: Specifying a structural schema + * + */ + boolean structural() default false; + + String $ref() default Undefined.STRING; // NOSONAR + + String $schema() default Undefined.STRING; // NOSONAR + + Class additionalItems() default Undefined.class; + + Class additionalProperties() default Undefined.class; + + Class[] allOf() default Undefined.class; + + Class[] anyOf() default Undefined.class; + + String defaultValue() default Undefined.STRING; + + Map[] definitions() default @Map(name = Undefined.STRING, value = Undefined.class); + + DependencyMap[] dependencies() default @DependencyMap(name = Undefined.STRING, value = @Dependency()); + + String description() default Undefined.STRING; + + /* + * Parsed into com.fasterxml.jackson.databind.JsonNode depending on `type` + */ + String[] enumeration() default Undefined.STRING; + + /* + * Parsed into com.fasterxml.jackson.databind.JsonNode depending on `type` + */ + String example() default Undefined.STRING; + + Class exclusiveMaximum() default Undefined.class; + + Class exclusiveMinimum() default Undefined.class; + + ExternalDocumentation externalDocs() default @ExternalDocumentation(url = Undefined.STRING); + + String format() default Undefined.STRING; + + String id() default Undefined.STRING; + + Class[] items() default Undefined.class; + + long maxItems() default Undefined.LONG; + + long maxLength() default Undefined.LONG; + + long maxProperties() default Undefined.LONG; + + double maximum() default Undefined.DOUBLE; + + long minItems() default Undefined.LONG; + + long minLength() default Undefined.LONG; + + long minProperties() default Undefined.LONG; + + double minimum() default Undefined.DOUBLE; + + double multipleOf() default Undefined.DOUBLE; + + Class not() default Undefined.class; + + Class nullable() default Undefined.class; + + Class[] oneOf() default Undefined.class; + + String pattern() default Undefined.STRING; + + Map[] patternProperties() default @Map(name = Undefined.STRING, value = Undefined.class); + + Map[] properties() default @Map(name = Undefined.STRING, value = Undefined.class); + + String[] required() default Undefined.STRING; + + String title() default Undefined.STRING; + + String type() default Undefined.STRING; + + Class uniqueItems() default Undefined.class; + + Class xKubernetesEmbeddedResource() default Undefined.class; + + Class xKubernetesIntOrString() default Undefined.class; + + String[] xKubernetesListMapKeys() default Undefined.STRING; + + String xKubernetesListType() default Undefined.STRING; + + String xKubernetesMapType() default Undefined.STRING; + + Class xKubernetesPreserveUnknownFields() default Undefined.class; + + ValidationRule[] xKubernetesValidations() default @ValidationRule(value = Undefined.STRING); +}