Skip to content

Commit 7860628

Browse files
committed
Add structural support, differentiate between undefined/suppressed
Signed-off-by: Michael Edgar <medgar@redhat.com>
1 parent 8069911 commit 7860628

File tree

7 files changed

+538
-166
lines changed

7 files changed

+538
-166
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
* Fix #6880: LogWatch interface provides listeners on close stream event
3535
* Fix #6971: Exposed Istio v1 models in Istio Client DSL
3636
* Fix #6998: Removed unneeded dependency on javax.annotation:javax.annotation-api
37+
* Fix #6999: (crd-generator) introduce JSONSchema annotation for increased control of schema output
3738

3839
#### Dependency Upgrade
3940
* 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 @@
5455

5556
#### New Features
5657
* Fix #5993: Support for Kubernetes v1.31 (elli)
57-
* Fix #6767: Support for Kubernetes v1.32 (penelope)
58+
* Fix #6767: Support for Kubernetes v1.32 (penelope)
5859
* Fix #6777: Added Javadoc comments to all generated models
5960
* Fix #6802: (java-generator) Added support for required spec and status
6061

@@ -225,7 +226,7 @@
225226
* Fix #5357: adding additional Quantity methods
226227
* Fix #5635: refined LeaderElector lifecycle and logging
227228
* Fix #5787: (crd-generator) add support for deprecated versions for generated CRDs
228-
* Fix #5788: (crd-generator) add support for Kubernetes validation rules
229+
* Fix #5788: (crd-generator) add support for Kubernetes validation rules
229230
* Fix #5735: Replace usages of `UUID.randomUUID()` with UUID created via AtomicLong
230231

231232
#### New Features
@@ -309,7 +310,7 @@
309310
* Fix #5220: refinements and clarifications to the validation of names
310311

311312
#### Dependency Upgrade
312-
* Fix #5286: Update Fabric8 OpenShift Model as per OpenShift 4.13.12
313+
* Fix #5286: Update Fabric8 OpenShift Model as per OpenShift 4.13.12
313314
* Fix #5373: Gradle base API based on v8.2.1
314315
* Fix #5401: Upgrade Fabric8 Kubernetes Model to Kubernetes v1.28.2
315316

crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/AbstractJsonSchema.java

Lines changed: 70 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
import java.util.HashMap;
7575
import java.util.HashSet;
7676
import java.util.LinkedHashMap;
77-
import java.util.LinkedHashSet;
7877
import java.util.List;
7978
import java.util.Map;
8079
import java.util.Map.Entry;
@@ -84,6 +83,7 @@
8483
import java.util.TreeMap;
8584
import java.util.function.Consumer;
8685
import java.util.function.Function;
86+
import java.util.function.Supplier;
8787
import java.util.stream.Collectors;
8888
import java.util.stream.Stream;
8989

@@ -170,7 +170,7 @@ private T resolveRoot(Class<?> definition) {
170170
private <A extends Annotation> T mapAnnotation(A annotation,
171171
Function<A, T> mapper) {
172172
if (annotation != null) {
173-
return mapper.apply(annotation);
173+
return mapper.apply(annotation);
174174
}
175175
return null;
176176
}
@@ -419,8 +419,8 @@ private Optional<Long> findMaxInSizeAnnotation(BeanProperty beanProperty) {
419419

420420
private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwaps schemaSwaps, JsonSchema jacksonSchema,
421421
String... ignore) {
422-
Set<String> ignores = ignore.length > 0 ? new LinkedHashSet<>(Arrays.asList(ignore)) : Collections.emptySet();
423422

423+
Set<String> ignores = Set.of(ignore);
424424
schemaSwaps = schemaSwaps.branchAnnotations();
425425
final InternalSchemaSwaps swaps = schemaSwaps;
426426

@@ -434,8 +434,11 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
434434
Class<?> rawClass = gos.javaType.getRawClass();
435435
collectDependentClasses(rawClass);
436436

437-
JSONSchema schemaAnnotation = resolvingContext.ignoreJSONSchemaAnnotation ? null : rawClass.getDeclaredAnnotation(JSONSchema.class);
438-
T classSchema = mapAnnotation(schemaAnnotation, schema -> fromAnnotation(rawClass, true, schema));
437+
T classSchema = resolveSchemaAnnotation(
438+
rawClass.getDeclaredAnnotation(JSONSchema.class),
439+
rawClass,
440+
true,
441+
resolvingContext.ignoreJSONSchemaAnnotation);
439442

440443
if (classSchema != null) {
441444
return classSchema;
@@ -451,17 +454,16 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
451454
List<String> required = new ArrayList<>();
452455
final T objectSchema = singleProperty("object");
453456

454-
for (Map.Entry<String, JsonSchema> property : new TreeMap<>(gos.getProperties()).entrySet()) {
457+
for (Map.Entry<String, JsonSchema> property : visibleProperties(gos.getProperties(), ignores).entrySet()) {
455458
String name = property.getKey();
456-
if (ignores.contains(name)) {
457-
continue;
458-
}
459459
BeanProperty beanProperty = gos.beanProperties.get(property.getKey());
460460
Utils.checkNotNull(beanProperty, "CRD generation works only with bean properties");
461461

462-
Class<?> propRawClass = beanProperty.getType().getRawClass();
463-
JSONSchema propSchemaAnnotation = beanProperty.getAnnotation(JSONSchema.class);
464-
T propSchema = mapAnnotation(propSchemaAnnotation, schema -> fromAnnotation(propRawClass, false, schema));
462+
T propSchema = resolveSchemaAnnotation(
463+
beanProperty.getAnnotation(JSONSchema.class),
464+
beanProperty.getType().getRawClass(),
465+
false,
466+
false);
465467

466468
if (propSchema != null) {
467469
addProperty(name, objectSchema, propSchema);
@@ -525,6 +527,20 @@ private T resolveObject(LinkedHashMap<String, String> visited, InternalSchemaSwa
525527
return objectSchema;
526528
}
527529

530+
private T resolveSchemaAnnotation(JSONSchema annotation, Class<?> rawClass, boolean isTargetType, boolean ignoreAnnotation) {
531+
if (annotation != null && !ignoreAnnotation) {
532+
return fromAnnotation(rawClass, isTargetType, annotation);
533+
}
534+
return null;
535+
}
536+
537+
private static Map<String, JsonSchema> visibleProperties(Map<String, JsonSchema> properties, Set<String> ignores) {
538+
return new TreeMap<>(
539+
properties.entrySet().stream()
540+
.filter(e -> !ignores.contains(e.getKey()))
541+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
542+
}
543+
528544
private void collectDependentClasses(Class<?> rawClass) {
529545
if (rawClass != null && !rawClass.getName().startsWith("java.") && dependentClasses.add(rawClass.getName())) {
530546
Stream.of(rawClass.getInterfaces()).forEach(this::collectDependentClasses);
@@ -706,77 +722,68 @@ private Set<String> findIgnoredEnumConstants(JavaType type) {
706722
}
707723

708724
protected T fromAnnotation(Class<?> rawClass, boolean isTargetType, JSONSchema schema) {
709-
T result = mapImplementation(schema.implementation(), isTargetType);
725+
T result = mapImplementation(schema.implementation(), isTargetType);
710726

711-
if (result == null) {
712-
result = singleProperty(mapDefined(schema.type()));
713-
}
727+
if (result == null) {
728+
result = singleProperty(null);
729+
}
714730

715-
setIfDefined(mapDefined(schema.defaultValue(), rawClass), result::setDefault);
716-
setIfDefined(mapDefined(schema.description()), result::setDescription);
717-
setIfDefined(mapBoolean(schema.exclusiveMaximum()), result::setExclusiveMaximum);
718-
setIfDefined(mapBoolean(schema.exclusiveMinimum()), result::setExclusiveMinimum);
719-
setIfDefined(mapDefined(schema.format()), result::setFormat);
720-
setIfDefined(mapDefined(schema.maximum()), result::setMaximum);
721-
setIfDefined(mapDefined(schema.maxItems()), result::setMaxItems);
722-
setIfDefined(mapDefined(schema.maxLength()), result::setMaxLength);
723-
setIfDefined(mapDefined(schema.maxProperties()), result::setMaxProperties);
724-
setIfDefined(mapDefined(schema.minimum()), result::setMinimum);
725-
setIfDefined(mapDefined(schema.minItems()), result::setMinItems);
726-
setIfDefined(mapDefined(schema.minLength()), result::setMinLength);
727-
setIfDefined(mapDefined(schema.minProperties()), result::setMinProperties);
728-
setIfDefined(mapBoolean(schema.nullable()), result::setNullable);
729-
setIfDefined(mapDefined(schema.pattern()), result::setPattern);
730-
setIfDefined(mapDefined(schema.required()), result::setRequired);
731-
setIfDefined(mapBoolean(schema.xKubernetesPreserveUnknownFields()), result::setXKubernetesPreserveUnknownFields);
732-
return result;
731+
setIfDefined(schema.defaultValue(), v -> parseJson(v, rawClass), result::setDefault);
732+
setIfDefined(schema.description(), result::setDescription);
733+
setIfDefined(schema.exclusiveMaximum(), this::mapBoolean, result::setExclusiveMaximum);
734+
setIfDefined(schema.exclusiveMinimum(), this::mapBoolean, result::setExclusiveMinimum);
735+
setIfDefined(schema.format(), result::setFormat);
736+
setIfDefined(schema.maximum(), result::setMaximum);
737+
setIfDefined(schema.maxItems(), result::setMaxItems);
738+
setIfDefined(schema.maxLength(), result::setMaxLength);
739+
setIfDefined(schema.maxProperties(), result::setMaxProperties);
740+
setIfDefined(schema.minimum(), result::setMinimum);
741+
setIfDefined(schema.minItems(), result::setMinItems);
742+
setIfDefined(schema.minLength(), result::setMinLength);
743+
setIfDefined(schema.minProperties(), result::setMinProperties);
744+
setIfDefined(schema.nullable(), this::mapBoolean, result::setNullable);
745+
setIfDefined(schema.pattern(), result::setPattern);
746+
setIfDefined(schema.required(), ArrayList::new, Arrays::asList, result::setRequired);
747+
setIfDefined(schema.xKubernetesPreserveUnknownFields(), this::mapBoolean, result::setXKubernetesPreserveUnknownFields);
748+
return result;
733749
}
734750

735-
protected static <P> void setIfDefined(P value, Consumer<P> mutator) {
736-
if (value != null) {
737-
mutator.accept(value);
738-
}
751+
protected static <A, M> void setIfDefined(A value, Function<A, M> transformer, Consumer<M> mutator) {
752+
setIfDefined(value, () -> null, transformer, mutator);
739753
}
740754

741-
protected JsonNode mapDefined(String value, Class<?> targetType) {
742-
if ((value = mapDefined(value)) == null) {
743-
return null;
755+
protected static <A> void setIfDefined(A value, Consumer<A> mutator) {
756+
setIfDefined(value, () -> null, Function.identity(), mutator);
757+
}
758+
759+
protected static <A, M> void setIfDefined(A value, Supplier<M> defaultValue, Function<A, M> transformer,
760+
Consumer<M> mutator) {
761+
if (JSONSchema.Undefined.isUndefined(value)) {
762+
// Not defined in the annotation (the default), don't touch the model.
763+
} else if (JSONSchema.Suppressed.isSuppressed(value)) {
764+
// Suppressed in the annotation, return the model back to the default value.
765+
mutator.accept(defaultValue.get());
766+
} else {
767+
mutator.accept(transformer.apply(value));
744768
}
769+
}
745770

771+
protected JsonNode parseJson(String value, Class<?> targetType) {
746772
Optional<Class<?>> rawType = Optional.ofNullable(targetType);
747773

748774
try {
749775
Object typedValue = resolvingContext.kubernetesSerialization.unmarshal(value, rawType.orElse(Object.class));
750776
return resolvingContext.kubernetesSerialization.convertValue(typedValue, JsonNode.class);
751777
} catch (Exception e) {
752-
if (value.isEmpty()) {
753-
LOGGER.warn("Cannot parse value '{}' from JSONSchema annotation as valid YAML or JSON, no value will be used.", value);
754-
return null;
755-
}
756778
throw new IllegalArgumentException("Cannot parse value '" + value + "' as valid YAML or JSON.", e);
757779
}
758780
}
759781

760-
protected static String mapDefined(String value) {
761-
return JSONSchema.Undefined.STRING.equals(value) ? null : value;
762-
}
763-
764-
protected static List<String> mapDefined(String[] values) {
765-
return values.length == 0 ? null : List.of(values);
766-
}
767-
768-
protected static Double mapDefined(double value) {
769-
return JSONSchema.Undefined.DOUBLE == value ? null : value;
782+
protected List<JsonNode> parseJson(String[] values, Class<?> targetType) {
783+
return Arrays.stream(values).map(value -> parseJson(value, targetType)).collect(Collectors.toList());
770784
}
771785

772-
protected static Long mapDefined(long value) {
773-
return JSONSchema.Undefined.LONG == value ? null : value;
774-
}
775-
776-
protected static <A extends JSONSchema.Boolean> Boolean mapBoolean(Class<A> value) {
777-
if (value == JSONSchema.Undefined.class) {
778-
return null; // NOSONAR
779-
}
786+
protected <A extends JSONSchema.Boolean> Boolean mapBoolean(Class<A> value) {
780787
return value == JSONSchema.True.class ? Boolean.TRUE : Boolean.FALSE;
781788
}
782789

crd-generator/api-v2/src/main/java/io/fabric8/crdv2/generator/ResolvingContext.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,13 @@ public static ResolvingContext defaultResolvingContext(boolean implicitPreserveU
113113
}
114114

115115
public ResolvingContext forkContext() {
116-
return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields, ignoreJSONSchemaAnnotation);
116+
return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields,
117+
ignoreJSONSchemaAnnotation);
117118
}
118119

119120
public ResolvingContext forkContext(boolean ignoreJSONSchemaAnnotation) {
120-
return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields, ignoreJSONSchemaAnnotation);
121+
return new ResolvingContext(objectMapper, kubernetesSerialization, uriToJacksonSchema, implicitPreserveUnknownFields,
122+
ignoreJSONSchemaAnnotation);
121123
}
122124

123125
public ResolvingContext(ObjectMapper mapper, KubernetesSerialization kubernetesSerialization,

0 commit comments

Comments
 (0)