Skip to content

Commit

Permalink
Merge pull request #77 from axonivy/conditional
Browse files Browse the repository at this point in the history
allow @conditional on property, that asserts a sibling value
  • Loading branch information
ivy-rew authored Jun 7, 2024
2 parents aba4975 + f16a2ce commit 2993017
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.axonivy.json.schema.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Documents fields which only will be present, if a sibling has a certain value.
*
* @see "https://json-schema.org/understanding-json-schema/reference/conditionals.html"
* @see Condition
*
*/
@Repeatable(Conditional.List.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Conditional {

/**
* @return the 'name' of a sibling property to asserted
*/
public String ifProperty();

/**
* @return the 'value' to assert on the field
*/
public String[] hasConst();

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@interface List {
Conditional[] value();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.github.victools.jsonschema.generator.SchemaVersion;

import io.github.axonivy.json.schema.annotations.Condition;
import io.github.axonivy.json.schema.annotations.Conditional;

public class ConditionalFieldProvider implements CustomDefinitionProviderV2 {

Expand All @@ -44,26 +45,39 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
}

private void annotate(ConditionBuilder builder, ResolvedField field) {
Consumer<Condition> add = condition -> builder.addCondition(condition, field.getName());
Annotations annotations = field.getAnnotations();
Condition single = field.get(Condition.class);
if (single != null) {
add.accept(single);

Consumer<Condition> add = condition -> builder.addCondition(condition, field.getName());
Condition condition = field.get(Condition.class);
if (condition != null) {
add.accept(condition);
}
Condition.List conditions = annotations.get(Condition.List.class);
if (conditions != null) {
Arrays.stream(conditions.value()).forEachOrdered(add);
}

Consumer<Conditional> addConditional = c -> builder.addConditional(c, field);
Conditional conditional = field.get(Conditional.class);
if (conditional != null) {
addConditional.accept(conditional);
}
Conditional.List conditionals = annotations.get(Conditional.List.class);
if (conditionals != null) {
Arrays.stream(conditionals.value()).forEachOrdered(addConditional);
}
}

public static class ConditionBuilder {

private final SchemaVersion version;
private final List<ObjectNode> conditions = new ArrayList<>();
private DynamicRefs refs = new DynamicRefs();
private SchemaGenerationContext context;

public ConditionBuilder(SchemaGenerationContext context) {
this.version = context.getGeneratorConfig().getSchemaVersion();
this.context = context;
}

public ConditionBuilder refs(DynamicRefs resolver) {
Expand All @@ -78,6 +92,11 @@ public ObjectNode addCondition(Condition condition, String field) {
return addCondition(field, condition.ifConst(), condition.thenProperty(), ref);
}

public ObjectNode addConditional(Conditional condition, ResolvedField field) {
var resolved = context.createStandardDefinitionReference(field.getType(), null);
return addCondition(condition.ifProperty(), condition.hasConst(), field.getName(), resolved);
}

public ObjectNode addCondition(String field, String expect, String thenProp, JsonNode thenRef) {
return addCondition(field, new String[] {expect}, thenProp, thenRef);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.github.axonivy.json.schema.annotations.Condition;
import io.github.axonivy.json.schema.annotations.Conditional;

class TestFieldCondition {

Expand Down Expand Up @@ -61,7 +62,6 @@ static class MyMultiConditionalField {
@Test
void multiple_constsToVerfiy() {
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyAnyOfConditionalField.class);
System.out.println(schema.toPrettyString());

JsonNode provider = schema.get("if").get("properties").get("provider");
var anyOf = provider.get("anyOf");
Expand Down Expand Up @@ -89,4 +89,76 @@ public static class ComplexType {
}
}

@Test
void conditionalOtherProp() {
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyConditionalFieldSibling.class);

JsonNode ifProvider = schema.get("if").get("properties").get("provider");
assertThat(ifProvider.get("const").asText())
.isEqualTo("azure");

JsonNode thenProperty = schema.get("then").get("properties").get("always");
assertThat(thenProperty.get("$ref").asText())
.isEqualTo("#/$defs/ComplexType");
}

static class MyConditionalFieldSibling {
public String $schema; // self-ref

public String provider;

@Conditional(ifProperty = "provider", hasConst = { "azure" })
public ComplexType always;

public static class ComplexType {
public String name;
}
}

@Test
void conditionAndConditionalOtherProp() {
ObjectNode schema = new ExpressiveSchemaGenerator().generateSchema(MyMixedCondition.class);

var allOf = (ArrayNode)schema.get("allOf");
assertThat(allOf)
.as("multiple kinds of conditionals as one 'allOf' condition")
.isInstanceOf(ArrayNode.class);

JsonNode first = allOf.get(0);
JsonNode ifProvider = first.get("if").get("properties").get("provider");
assertThat(ifProvider.get("const").asText())
.isEqualTo("ms-ad");
JsonNode thenProperty = first.get("then").get("properties").get("ifAd");
assertThat(thenProperty.get("$ref").asText())
.isEqualTo("#/$defs/ComplexType");

JsonNode second = allOf.get(1);
JsonNode ifOther = second.get("if").get("properties").get("provider");
assertThat(ifOther.get("const").asText())
.isEqualTo("azure");
JsonNode thenThis = second.get("then").get("properties").get("ifAzure");
assertThat(thenThis.get("$ref").asText())
.isEqualTo("#/$defs/AzureType");
}

static class MyMixedCondition {
public String $schema; // self-ref

@Condition(ifConst = { "ms-ad" }, thenProperty = "ifAd", thenRef = "#/$defs/ComplexType")
public String provider;

@Conditional(ifProperty = "provider", hasConst = { "azure" })
public AzureType ifAzure;

public static class AzureType {
public String id;
}

public ComplexType always;

public static class ComplexType {
public String name;
}
}

}

0 comments on commit 2993017

Please sign in to comment.