Skip to content

Commit d633aef

Browse files
authored
feat: support properties with oneOf constraint (#203)
Support properties with oneOf constraint
1 parent 3b9e8cc commit d633aef

File tree

7 files changed

+153
-36
lines changed

7 files changed

+153
-36
lines changed

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsomParser.java

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package org.eclipse.dsp.generation.jsom;
1616

1717
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import org.eclipse.dsp.generation.jsom.SchemaProperty.ConstraintType;
1819
import org.jetbrains.annotations.NotNull;
1920
import org.jetbrains.annotations.Nullable;
2021

@@ -174,8 +175,8 @@ private void parseAttributes(Map<String, Object> definition, SchemaType schemaTy
174175
schemaType.properties(schemaProperties);
175176
}
176177

177-
// parse allOf properties
178-
parseAllOf(definition, schemaType);
178+
parseReferences(definition, ConstraintType.ALL_OF, schemaType);
179+
parseReferences(definition, ConstraintType.ONE_OF, schemaType);
179180

180181
// parse contains
181182
var contains = (Map<String, Object>) definition.get(CONTAINS);
@@ -198,24 +199,34 @@ private List<ElementDefinition> parseElementDefinition(Map<String, Object> conta
198199
}
199200

200201
@SuppressWarnings("unchecked")
201-
private void parseAllOf(Map<String, Object> definition, SchemaType schemaType) {
202-
var allOfDefinition = (List<Map<String, Object>>) definition.getOrDefault(ALL_OF, emptyList());
203-
var allOfProperties = allOfDefinition.stream()
202+
private void parseReferences(Map<String, Object> definition, ConstraintType constraintType, SchemaType schemaType) {
203+
var constraintDef = (List<Map<String, Object>>) definition.getOrDefault(constraintType.key(), emptyList());
204+
var properties = constraintDef.stream()
204205
.map(e -> (Map<String, Object>) e.get(PROPERTIES))
205206
.filter(Objects::nonNull)
206207
.flatMap(e -> e.entrySet().stream())
207208
.map(e -> parseProperty(e.getKey(), (Map<String, Object>) e.getValue()))
208209
.toList();
209-
schemaType.properties(allOfProperties);
210+
schemaType.properties(properties);
210211

211-
// parse allOf references
212-
var allOf = allOfDefinition
212+
// parse references
213+
var references = constraintDef
213214
.stream()
214215
.map(e -> e.get(REF))
215216
.filter(Objects::nonNull)
216217
.map(Object::toString)
217218
.toList();
218-
schemaType.allOf(allOf);
219+
switch (constraintType) {
220+
case ONE_OF -> {
221+
schemaType.oneOf(references);
222+
}
223+
case ALL_OF -> {
224+
schemaType.allOf(references);
225+
}
226+
default -> {
227+
throw new UnsupportedOperationException("Unsupported constraint type: " + constraintType);
228+
}
229+
}
219230
}
220231

221232
@SuppressWarnings("unchecked")
@@ -244,7 +255,7 @@ private SchemaProperty parseProperty(String name, Map<String, Object> value) {
244255
var oneOf = value.get(ONE_OF);
245256
//noinspection rawtypes
246257
if (oneOf instanceof List oneOfList) {
247-
return parseListProperty(name, oneOfList, comment);
258+
return parseListProperty(name, oneOfList, comment, ConstraintType.ONE_OF);
248259
}
249260
return SchemaProperty.Builder.newInstance()
250261
.name(name)
@@ -271,7 +282,7 @@ private SchemaProperty parseProperty(String name, Map<String, Object> value) {
271282
}
272283

273284
@SuppressWarnings({ "rawtypes", "unchecked" })
274-
private SchemaProperty parseListProperty(String name, List list, String comment) {
285+
private SchemaProperty parseListProperty(String name, List list, String comment, ConstraintType type) {
275286
var types = new HashSet<String>();
276287
var itemTypes = new HashSet<ElementDefinition>();
277288
for (var entry : list) {
@@ -283,28 +294,32 @@ private SchemaProperty parseListProperty(String name, List list, String comment)
283294
if (items instanceof Map) {
284295
itemTypes.addAll(parseElementDefinition((Map<String, Object>) items));
285296
} else if (items instanceof String itemString) {
286-
itemTypes.add(parseJsonElementDefinition(itemString));
297+
parseArrayOfTypeElementDefinition(itemString, itemTypes);
287298
}
288-
} else if (subtype == null) {
289-
var oneOfRef = oneOfMap.get(REF);
290-
if (oneOfRef instanceof String oneOfRefString) {
291-
itemTypes.add(parseRefElementDefinition(oneOfRefString));
299+
} else if (subtype != null) {
300+
itemTypes.add(parseJsonElementDefinition(subtype.toString()));
301+
} else {
302+
var ref = oneOfMap.get(REF);
303+
if (ref instanceof String refString) {
304+
itemTypes.add(new ElementDefinition(REFERENCE, refString));
292305
}
293306
}
294307
}
295308
}
296309
return SchemaProperty.Builder.newInstance()
297310
.name(name)
311+
.constraintType(type)
298312
.types(types)
299313
.itemTypes(itemTypes)
300314
.description(comment)
301315
.build();
302316
}
303317

304-
private @NotNull ElementDefinition parseRefElementDefinition(String ref) {
305-
var elementDefinition = new ElementDefinition(REFERENCE, ref);
306-
elementDefinition.resolvedType(JsonTypes.STRING);
307-
return elementDefinition;
318+
private void parseArrayOfTypeElementDefinition(String itemString, HashSet<ElementDefinition> itemTypes) {
319+
var elementDefinition = new ElementDefinition(JSON, ARRAY.getBaseType());
320+
var itemType = new SchemaType("array[" + itemString + "]", ARRAY.getBaseType());
321+
elementDefinition.resolvedType(itemType);
322+
itemTypes.add(elementDefinition);
308323
}
309324

310325
private @NotNull ElementDefinition parseJsonElementDefinition(String type) {

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonSchemaKeywords.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public interface JsonSchemaKeywords {
2626
String CONTAINS = "contains";
2727
String DEFINITIONS = "definitions";
2828
String ITEMS = "items";
29+
String NOT = "not";
2930
String ONE_OF = "oneOf";
3031
String PROPERTIES = "properties";
3132
String REQUIRED = "required";

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModelContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ public void resolve() {
103103
.filter(Objects::nonNull)
104104
.forEach(type::resolvedAllOfType));
105105

106+
types.forEach(type -> type.getOneOf().stream()
107+
.map(ref -> resolveType(ref, type.getSchemaUri()))
108+
.filter(Objects::nonNull)
109+
.forEach(type::resolvedOneOfType));
110+
106111
// resolve all property type references and link them
107112
types.forEach(type -> type.getProperties()
108113
.forEach(property -> property.getTypes().stream()
@@ -123,6 +128,13 @@ public void resolve() {
123128
.filter(Objects::nonNull)).findFirst().orElse(null);
124129

125130
ref.resolvedProperty(resolved);
131+
132+
resolved = type.getResolvedOneOf().stream()
133+
.flatMap(t -> t.getProperties().stream()
134+
.map(p -> p.getName().equals(ref.getName()) ? p : null)
135+
.filter(Objects::nonNull)).findFirst().orElse(null);
136+
137+
ref.resolvedProperty(resolved);
126138
}));
127139

128140

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaProperty.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,29 @@
2727
* A property defined in a schema type.
2828
*/
2929
public class SchemaProperty implements Comparable<SchemaProperty> {
30+
public enum ConstraintType {
31+
ANY_OF(JsonSchemaKeywords.ANY_OF), ONE_OF(JsonSchemaKeywords.ONE_OF), ALL_OF(JsonSchemaKeywords.ALL_OF), NOT(JsonSchemaKeywords.NOT), UNDEFINED("undefined");
32+
33+
private final String key;
34+
35+
public String key() {
36+
return key;
37+
}
38+
39+
ConstraintType(String key) {
40+
this.key = key;
41+
}
42+
}
43+
3044
private String name;
3145
private String description = "";
3246
private String constantValue;
3347
private Set<String> types = new TreeSet<>();
3448
private final Set<SchemaType> resolvedTypes = new TreeSet<>();
3549
private final Set<ElementDefinition> itemTypes = new TreeSet<>();
36-
private final Set<Object> enumValues = new TreeSet<>();
50+
private final Set<Object> enumValues = new TreeSet<>();
51+
52+
private ConstraintType constraintType = ConstraintType.UNDEFINED;
3753

3854
public String getName() {
3955
return name;
@@ -63,6 +79,10 @@ public void resolvedType(SchemaType resolvedType) {
6379
this.resolvedTypes.add(resolvedType);
6480
}
6581

82+
public ConstraintType getConstraintType() {
83+
return constraintType;
84+
}
85+
6686
public Set<ElementDefinition> getItemTypes() {
6787
return itemTypes;
6888
}
@@ -108,6 +128,11 @@ public Builder types(Set<String> types) {
108128
return this;
109129
}
110130

131+
public Builder constraintType(ConstraintType type) {
132+
property.constraintType = type;
133+
return this;
134+
}
135+
111136
public Builder itemTypes(Collection<ElementDefinition> elementTypes) {
112137
property.itemTypes.addAll(elementTypes);
113138
return this;

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaType.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ public class SchemaType implements Comparable<SchemaType> {
4040

4141
private final String name;
4242
private final String baseType;
43-
private String itemType;
43+
private final String itemType;
4444
private final boolean rootDefinition;
4545
private final String schemaUri;
4646
private final Set<String> allOf = new HashSet<>();
4747
private final Set<SchemaType> resolvedAllOf = new HashSet<>();
48+
private final Set<String> oneOf = new HashSet<>();
49+
private final Set<SchemaType> resolvedOneOf = new HashSet<>();
4850
private final Set<SchemaProperty> properties = new TreeSet<>();
4951
private final Set<ElementDefinition> contains = new TreeSet<>();
5052
private final Map<String, SchemaProperty> propertiesMap = new HashMap<>();
@@ -119,17 +121,19 @@ public Collection<SchemaPropertyReference> getRequiredProperties() {
119121

120122
@NotNull
121123
public Set<SchemaPropertyReference> getTransitiveRequiredProperties() {
122-
return concat(requiredProperties.values().stream(), resolvedAllOf.stream()
123-
.flatMap(type -> type.getTransitiveRequiredProperties().stream()))
124+
return concat(requiredProperties.values().stream(),
125+
concat(resolvedAllOf.stream().flatMap(type -> type.getTransitiveRequiredProperties().stream()),
126+
resolvedOneOf.stream().flatMap(type -> type.getTransitiveRequiredProperties().stream())))
124127
.collect((toCollection(TreeSet::new)));
125128
}
126129

127130
@NotNull
128131
public Set<SchemaPropertyReference> getTransitiveOptionalProperties() {
129-
// a type by include multple other types (allOf) where a property is optional in one but mandatory in another - filter it
132+
// a type may include multiple other types (allOf) where a property is optional in one but mandatory in another - filter it
130133
var required = getRequiredProperties().stream().map(SchemaPropertyReference::getName).collect(Collectors.toSet());
131-
return concat(optionalProperties.values().stream(), resolvedAllOf.stream()
132-
.flatMap(type -> type.getTransitiveOptionalProperties().stream()))
134+
return concat(optionalProperties.values().stream(),
135+
concat(resolvedAllOf.stream().flatMap(type -> type.getTransitiveOptionalProperties().stream()),
136+
resolvedOneOf.stream().flatMap(type -> type.getTransitiveOptionalProperties().stream())))
133137
.filter(prop -> !required.contains(prop.getName())) // filter required
134138
.collect((toCollection(TreeSet::new)));
135139
}
@@ -164,6 +168,23 @@ public void resolvedAllOfType(SchemaType type) {
164168
this.resolvedAllOf.add(type);
165169
}
166170

171+
@NotNull
172+
public Set<String> getOneOf() {
173+
return oneOf;
174+
}
175+
176+
public Set<SchemaType> getResolvedOneOf() {
177+
return resolvedOneOf;
178+
}
179+
180+
public void oneOf(Collection<String> oneOf) {
181+
this.oneOf.addAll(oneOf);
182+
}
183+
184+
public void resolvedOneOfType(SchemaType type) {
185+
this.resolvedOneOf.add(type);
186+
}
187+
167188
public void required(Collection<SchemaPropertyReference> required) {
168189
required.forEach(ref -> requiredProperties.put(ref.getName(), ref));
169190
}

artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformer.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import org.eclipse.dsp.generation.jsom.SchemaType;
2121
import org.jetbrains.annotations.NotNull;
2222

23-
import java.util.Objects;
2423
import java.util.Set;
2524
import java.util.stream.Collectors;
2625
import java.util.stream.Stream;
@@ -60,21 +59,21 @@ private void transformProperty(SchemaPropertyReference propertyReference, boolea
6059
if (resolvedProperty != null) {
6160
String resolvedTypes = "";
6261
if (!resolvedProperty.getItemTypes().isEmpty()) {
63-
resolvedTypes = getArrayTypeName(resolvedProperty);
62+
resolvedTypes = getConstraintOrArrayTypeName(resolvedProperty);
6463
} else {
6564
resolvedTypes = parseResolvedTypes(resolvedProperty);
6665
}
6766
builder.append(format("<td>%s</td>", resolvedTypes));
6867
if (resolvedProperty.getConstantValue() != null) {
6968
builder.append(format("<td>Value must be <span class=\"code\">%s</span></td>", resolvedProperty.getConstantValue()));
70-
} else if (!resolvedProperty.getEnumValues().isEmpty()){
69+
} else if (!resolvedProperty.getEnumValues().isEmpty()) {
7170
var values = resolvedProperty.getEnumValues().stream()
7271
.map(Object::toString)
7372
.collect(Collectors.joining(","));
74-
builder.append(format("<td>Must be of the following:<br><span class=\"code\">%s</span></td>",values));
73+
builder.append(format("<td>Must be of the following:<br><span class=\"code\">%s</span></td>", values));
7574
} else {
7675
var constants = resolvedProperty.getResolvedTypes().stream()
77-
.flatMap(t -> concat(Stream.of(t), t.getResolvedAllOf().stream())) // search the contains of the current type and any references 'allOf' types
76+
.flatMap(t -> concat(Stream.of(t), t.getResolvedAllOf().stream())) // search the current type and any references 'allOf' types
7877
.flatMap(t -> t.getContains().stream())
7978
.filter(cd -> cd.getType() == CONSTANT)
8079
.map(ElementDefinition::getValue)
@@ -103,7 +102,7 @@ private String parseResolvedTypes(SchemaProperty property) {
103102
return resolvedTypes;
104103
}
105104

106-
private @NotNull String getArrayTypeName(SchemaProperty resolvedProperty) {
105+
private @NotNull String getConstraintOrArrayTypeName(SchemaProperty resolvedProperty) {
107106
var itemTypes = resolvedProperty.getItemTypes().stream()
108107
.flatMap(t -> t.getResolvedTypes().stream())
109108
.map(e -> {
@@ -115,8 +114,8 @@ private String parseResolvedTypes(SchemaProperty property) {
115114
.collect(joining(", "));
116115
if (itemTypes.isEmpty()) {
117116
itemTypes = resolvedProperty.getResolvedTypes().stream()
118-
.filter(e->getTypeName(e)!=null)
119-
.map(e->{
117+
.filter(e -> getTypeName(e) != null)
118+
.map(e -> {
120119
if (e.isJsonBaseType()) {
121120
return String.format("%s", getTypeName(e));
122121
}
@@ -127,17 +126,30 @@ private String parseResolvedTypes(SchemaProperty property) {
127126
return "array";
128127
}
129128
}
129+
if (SchemaProperty.ConstraintType.ONE_OF == resolvedProperty.getConstraintType()) {
130+
return "one of [" + itemTypes + "]";
131+
} else if (SchemaProperty.ConstraintType.ANY_OF == resolvedProperty.getConstraintType()) {
132+
return "any of [" + itemTypes + "]";
133+
} else if (SchemaProperty.ConstraintType.ALL_OF == resolvedProperty.getConstraintType()) {
134+
return "all of [" + itemTypes + "]";
135+
} else if (SchemaProperty.ConstraintType.NOT == resolvedProperty.getConstraintType()) {
136+
return "not [" + itemTypes + "]";
137+
}
130138
return "array[" + itemTypes + "]";
131139
}
132140

133141
private String getTypeName(SchemaType schemaType) {
134142
if (schemaType.isRootDefinition()) {
135143
// root definition, check to see if it has an allOf, and if not, fallback to the Json base type
136144
if (!schemaType.getResolvedAllOf().isEmpty()) {
137-
// ue the allOf types and return the type name if it is a Json base object type; otherwise use the base type name
145+
// use the allOf types and return the type name if it is a Json base object type; otherwise use the base type name
138146
return schemaType.getResolvedAllOf().stream()
139147
.map(t -> OBJECT.getName().equals(t.getBaseType()) ? t.getName() : t.getBaseType())
140148
.collect(joining(", "));
149+
} else if (!schemaType.getResolvedOneOf().isEmpty()) {
150+
return schemaType.getResolvedOneOf().stream()
151+
.map(t -> OBJECT.getName().equals(t.getBaseType()) ? t.getName() : t.getBaseType())
152+
.collect(joining(" or "));
141153
}
142154
return schemaType.getBaseType();
143155
}

artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaModelContextTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,37 @@ void verifyAllOfTypeResolution() {
104104
assertThat(fooSchema.getTransitiveRequiredProperties().iterator().next().getResolvedProperty()).isSameAs(abstractRequiredProperty);
105105
}
106106

107+
@Test
108+
void verifyOneOfTypeResolution() {
109+
var abstractFooSchema = new SchemaType("AbstractFoo", "SchemaFile");
110+
111+
var abstractProperty = SchemaProperty.Builder.newInstance()
112+
.name("abstractProperty")
113+
.types(Set.of("#/definitions/Foo"))
114+
.build();
115+
116+
var abstractRequiredProperty = SchemaProperty.Builder.newInstance()
117+
.name("abstractRequiredProperty")
118+
.types(Set.of("string"))
119+
.build();
120+
121+
abstractFooSchema.properties(List.of(abstractProperty, abstractRequiredProperty));
122+
123+
var fooSchema = new SchemaType("Foo", "SchemaFile");
124+
fooSchema.oneOf(Set.of("#/definitions/AbstractFoo"));
125+
fooSchema.required(Set.of(new SchemaPropertyReference("abstractRequiredProperty")));
126+
127+
modelContext.addType(abstractFooSchema);
128+
modelContext.addType(fooSchema);
129+
modelContext.resolve();
130+
131+
assertThat(fooSchema.getTransitiveOptionalProperties().size()).isEqualTo(1);
132+
assertThat(fooSchema.getTransitiveOptionalProperties().iterator().next().getResolvedProperty()).isSameAs(abstractProperty);
133+
134+
assertThat(fooSchema.getTransitiveRequiredProperties().size()).isEqualTo(1);
135+
assertThat(fooSchema.getTransitiveRequiredProperties().iterator().next().getResolvedProperty()).isSameAs(abstractRequiredProperty);
136+
}
137+
107138
@Test
108139
void verifyContainsTypeResolution() {
109140
var fooSchema = new SchemaType("Foo", "SchemaFile");

0 commit comments

Comments
 (0)