Skip to content

Commit 2c766a5

Browse files
feat(webserver): move oneOf to anyOf in JsonSchemaGenerator to have better autocompletion
1 parent 959737f commit 2c766a5

File tree

8 files changed

+64
-63
lines changed

8 files changed

+64
-63
lines changed

core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java

+30-28
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,21 @@ public <T> Map<String, Object> schemas(Class<? extends T> cls) {
6464
return this.schemas(cls, false);
6565
}
6666

67+
private void replaceOneOfWithAnyOf(ObjectNode objectNode) {
68+
objectNode.findParents("oneOf").forEach(jsonNode -> {
69+
if (jsonNode instanceof ObjectNode oNode) {
70+
oNode.set("anyOf", oNode.remove("oneOf"));
71+
}
72+
});
73+
}
74+
6775
public <T> Map<String, Object> schemas(Class<? extends T> cls, boolean arrayOf) {
6876
SchemaGeneratorConfigBuilder builder = new SchemaGeneratorConfigBuilder(
6977
SchemaVersion.DRAFT_7,
7078
OptionPreset.PLAIN_JSON
7179
);
7280

73-
this.build(builder,true);
81+
this.build(builder, true);
7482

7583
SchemaGeneratorConfig schemaGeneratorConfig = builder.build();
7684

@@ -80,8 +88,8 @@ public <T> Map<String, Object> schemas(Class<? extends T> cls, boolean arrayOf)
8088
if (arrayOf) {
8189
objectNode.put("type", "array");
8290
}
83-
replaceAnyOfWithOneOf(objectNode);
84-
pullDocumentationAndDefaultFromOneOf(objectNode);
91+
replaceOneOfWithAnyOf(objectNode);
92+
pullDocumentationAndDefaultFromAnyOf(objectNode);
8593
removeRequiredOnPropsWithDefaults(objectNode);
8694

8795
return JacksonMapper.toMap(objectNode);
@@ -111,23 +119,15 @@ private void removeRequiredOnPropsWithDefaults(ObjectNode objectNode) {
111119
});
112120
}
113121

114-
private void replaceAnyOfWithOneOf(ObjectNode objectNode) {
115-
objectNode.findParents("anyOf").forEach(jsonNode -> {
116-
if (jsonNode instanceof ObjectNode oNode) {
117-
oNode.set("oneOf", oNode.remove("anyOf"));
118-
}
119-
});
120-
}
121-
122-
// This hack exists because for Property we generate a oneOf for properties that are not strings.
123-
// By default, the 'default' is in each oneOf which Monaco editor didn't take into account.
124-
// So, we pull off the 'default' from any of the oneOf to the parent.
122+
// This hack exists because for Property we generate a anyOf for properties that are not strings.
123+
// By default, the 'default' is in each anyOf which Monaco editor didn't take into account.
124+
// So, we pull off the 'default' from any of the anyOf to the parent.
125125
// same thing for documentation fields: 'title', 'description', '$deprecated'
126-
private void pullDocumentationAndDefaultFromOneOf(ObjectNode objectNode) {
127-
objectNode.findParents("oneOf").forEach(jsonNode -> {
126+
private void pullDocumentationAndDefaultFromAnyOf(ObjectNode objectNode) {
127+
objectNode.findParents("anyOf").forEach(jsonNode -> {
128128
if (jsonNode instanceof ObjectNode oNode) {
129-
JsonNode oneOf = oNode.get("oneOf");
130-
if (oneOf instanceof ArrayNode arrayNode) {
129+
JsonNode anyOf = oNode.get("anyOf");
130+
if (anyOf instanceof ArrayNode arrayNode) {
131131
Iterator<JsonNode> it = arrayNode.elements();
132132
var nodesToPullUp = new HashMap<String, Optional<JsonNode>>(Map.ofEntries(
133133
Map.entry("default", Optional.empty()),
@@ -287,11 +287,11 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
287287
TypeContext context = target.getContext();
288288
Class<?> erasedType = javaType.getTypeParameters().getFirst().getErasedType();
289289

290-
if(String.class.isAssignableFrom(erasedType)) {
290+
if (String.class.isAssignableFrom(erasedType)) {
291291
return List.of(
292292
context.resolve(String.class)
293293
);
294-
} else if(Object.class.equals(erasedType)) {
294+
} else if (Object.class.equals(erasedType)) {
295295
return List.of(
296296
context.resolve(Object.class)
297297
);
@@ -401,7 +401,7 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
401401
// handle deprecated tasks
402402
Schema schema = scope.getType().getErasedType().getAnnotation(Schema.class);
403403
Deprecated deprecated = scope.getType().getErasedType().getAnnotation(Deprecated.class);
404-
if ((schema != null && schema.deprecated()) || deprecated != null ) {
404+
if ((schema != null && schema.deprecated()) || deprecated != null) {
405405
collectedTypeAttributes.put("$deprecated", "true");
406406
}
407407
});
@@ -426,7 +426,7 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
426426
});
427427

428428
// Subtype resolver for all plugins
429-
if(builder.build().getSchemaVersion() != SchemaVersion.DRAFT_2019_09) {
429+
if (builder.build().getSchemaVersion() != SchemaVersion.DRAFT_2019_09) {
430430
builder.forTypesInGeneral()
431431
.withSubtypeResolver((declaredType, context) -> {
432432
TypeContext typeContext = context.getTypeContext();
@@ -615,7 +615,7 @@ private boolean defaultInAllOf(JsonNode property) {
615615
if (property.has("allOf")) {
616616
for (Iterator<JsonNode> it = property.get("allOf").elements(); it.hasNext(); ) {
617617
JsonNode child = it.next();
618-
if(child.has("default")) {
618+
if (child.has("default")) {
619619
return true;
620620
}
621621
}
@@ -629,7 +629,7 @@ protected <T> Map<String, Object> generate(Class<? extends T> cls, @Nullable Cla
629629
OptionPreset.PLAIN_JSON
630630
);
631631

632-
this.build(builder,false);
632+
this.build(builder, false);
633633

634634
// we don't return base properties unless specified with @PluginProperty
635635
builder
@@ -641,8 +641,8 @@ protected <T> Map<String, Object> generate(Class<? extends T> cls, @Nullable Cla
641641
SchemaGenerator generator = new SchemaGenerator(schemaGeneratorConfig);
642642
try {
643643
ObjectNode objectNode = generator.generateSchema(cls);
644-
replaceAnyOfWithOneOf(objectNode);
645-
pullDocumentationAndDefaultFromOneOf(objectNode);
644+
replaceOneOfWithAnyOf(objectNode);
645+
pullDocumentationAndDefaultFromAnyOf(objectNode);
646646
removeRequiredOnPropsWithDefaults(objectNode);
647647

648648
return JacksonMapper.toMap(extractMainRef(objectNode));
@@ -753,7 +753,8 @@ private Object defaultValue(Object instance, Class<?> cls, String fieldName) {
753753

754754
field.setAccessible(true);
755755
return field.invoke(instance);
756-
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IllegalArgumentException ignored) {
756+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
757+
IllegalArgumentException ignored) {
757758

758759
}
759760

@@ -762,7 +763,8 @@ private Object defaultValue(Object instance, Class<?> cls, String fieldName) {
762763

763764
field.setAccessible(true);
764765
return field.invoke(instance);
765-
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IllegalArgumentException ignored) {
766+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
767+
IllegalArgumentException ignored) {
766768

767769
}
768770

core/src/main/resources/docs/macro.peb

+4-4
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ This property is currently in beta. While it is considered safe for use, please
2424
* **Type:** {{ type(data) -}}
2525
{%- elseif data['$ref'] -%}
2626
* **Type:** {{ type(data) }}
27-
{%- elseif data.oneOf != null -%}
27+
{%- elseif data.anyOf != null -%}
2828
* **Type:**
29-
{%- for current in data.oneOf %}
29+
{%- for current in data.anyOf %}
3030
* {{ type(current) -}}
3131
{%- endfor -%}
3232
{%- else -%}
@@ -50,8 +50,8 @@ This property is currently in beta. While it is considered safe for use, please
5050
{%- else -%}
5151
{%- set dynamic = "❌" -%}
5252
{% endif -%}
53-
{%- elseif data.oneOf != null -%}
54-
{%- for current in data.oneOf -%}
53+
{%- elseif data.anyOf != null -%}
54+
{%- for current in data.anyOf -%}
5555
{%- if current['$dynamic'] == true -%}
5656
{%- set dynamic = "✔️" -%}
5757
{%- else -%}

core/src/test/java/io/kestra/core/docs/ClassPluginDocumentationTest.java

+11-12
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
import io.kestra.core.Helpers;
44
import io.kestra.core.models.property.DynamicPropertyExampleTask;
5-
import io.kestra.core.models.tasks.runners.TaskRunner;
6-
import io.kestra.core.models.triggers.TriggerInterface;
7-
import io.kestra.core.plugins.PluginClassAndMetadata;
8-
import io.kestra.plugin.core.runner.Process;
95
import io.kestra.core.models.tasks.Task;
6+
import io.kestra.core.models.tasks.runners.TaskRunner;
107
import io.kestra.core.models.triggers.AbstractTrigger;
11-
import io.kestra.plugin.core.trigger.Schedule;
8+
import io.kestra.core.plugins.PluginClassAndMetadata;
129
import io.kestra.core.plugins.PluginScanner;
1310
import io.kestra.core.plugins.RegisteredPlugin;
11+
import io.kestra.plugin.core.runner.Process;
12+
import io.kestra.plugin.core.trigger.Schedule;
1413
import org.junit.jupiter.api.Test;
1514

1615
import java.net.URISyntaxException;
@@ -149,13 +148,13 @@ void dynamicProperty() throws URISyntaxException {
149148
assertThat(properties, aMapWithSize(21));
150149

151150
Map<String, Object> number = (Map<String, Object>) properties.get("number");
152-
assertThat(number.get("oneOf"), notNullValue());
153-
List<Map<String, Object>> oneOf = (List<Map<String, Object>>) number.get("oneOf");
154-
assertThat(oneOf, hasSize(2));
155-
assertThat(oneOf.getFirst().get("type"), is("integer"));
156-
assertThat(oneOf.getFirst().get("$dynamic"), is(true));
157-
assertThat(oneOf.get(1).get("type"), is("string"));
158-
// assertThat(oneOf.get(1).get("pattern"), is(".*{{.*}}.*"));
151+
assertThat(number.get("anyOf"), notNullValue());
152+
List<Map<String, Object>> anyOf = (List<Map<String, Object>>) number.get("anyOf");
153+
assertThat(anyOf, hasSize(2));
154+
assertThat(anyOf.getFirst().get("type"), is("integer"));
155+
assertThat(anyOf.getFirst().get("$dynamic"), is(true));
156+
assertThat(anyOf.get(1).get("type"), is("string"));
157+
// assertThat(anyOf.get(1).get("pattern"), is(".*{{.*}}.*"));
159158

160159
Map<String, Object> withDefault = (Map<String, Object>) properties.get("withDefault");
161160
assertThat(withDefault.get("type"), is("string"));

core/src/test/java/io/kestra/core/docs/JsonSchemaGeneratorTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ void flow() throws URISyntaxException {
9393
.get("tasks")
9494
.get("items")
9595
);
96-
assertThat(items.containsKey("anyOf"), is(false));
97-
assertThat(items.containsKey("oneOf"), is(true));
96+
assertThat(items.containsKey("anyOf"), is(true));
97+
assertThat(items.containsKey("oneOf"), is(false));
9898

9999
var bash = definitions.get(Log.class.getName());
100100
assertThat((List<String>) bash.get("required"), not(contains("level")));
@@ -123,7 +123,7 @@ void task() throws URISyntaxException {
123123

124124
var definitions = (Map<String, Map<String, Object>>) generate.get("definitions");
125125
var task = definitions.get(Task.class.getName());
126-
Assertions.assertNotNull(task.get("oneOf"));
126+
Assertions.assertNotNull(task.get("anyOf"));
127127
});
128128
}
129129

ui/src/components/flows/tasks/OneOfContent.vue ui/src/components/flows/tasks/AnyOfContent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
};
3636
},
3737
created() {
38-
this.schemas = this.schema?.oneOf ?? [];
38+
this.schemas = this.schema?.anyOf ?? [];
3939
4040
const schema = this.schemaOptions.find((item) =>
4141
typeof this.modelValue === "string"

ui/src/components/flows/tasks/Task.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export default {
5656
return "complex";
5757
}
5858

59-
if (Object.prototype.hasOwnProperty.call(property, "oneOf")) {
60-
return "one-of";
59+
if (Object.prototype.hasOwnProperty.call(property, "anyOf")) {
60+
return "any-of";
6161
}
6262

6363
if (Object.prototype.hasOwnProperty.call(property, "additionalProperties")) {

ui/src/components/flows/tasks/TaskOneOf.vue ui/src/components/flows/tasks/TaskAnyOf.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
breadcrumb: {
1010
label: root,
1111
to: {},
12-
component: h(OneOfContent, {
12+
component: h(AnyOfContent, {
1313
modelValue,
1414
schema,
1515
definitions,
@@ -31,7 +31,7 @@
3131
<script setup>
3232
import {h} from "vue";
3333
import Eye from "vue-material-design-icons/Eye.vue";
34-
import OneOfContent from "./OneOfContent.vue";
34+
import AnyOfContent from "./AnyOfContent.vue";
3535
</script>
3636

3737
<script>

ui/src/utils/init.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {createStore} from "vuex";
22
import {createRouter, createWebHistory} from "vue-router";
33
import VueGtag from "vue-gtag";
4-
import {setI18nLanguage, loadLocaleMessages, setupI18n} from "../translations/i18n";
4+
import {loadLocaleMessages, setI18nLanguage, setupI18n} from "../translations/i18n";
55
import moment from "moment-timezone";
66
import "moment/dist/locale/de"
77
import "moment/dist/locale/es"
@@ -17,19 +17,19 @@ import "moment/dist/locale/zh-cn"
1717
import {extendMoment} from "moment-range";
1818
import VueSidebarMenu from "vue-sidebar-menu";
1919
import {
20-
Chart,
20+
ArcElement,
21+
BarController,
22+
BarElement,
2123
CategoryScale,
24+
Chart,
25+
DoughnutController,
26+
Filler,
27+
Legend,
2228
LinearScale,
23-
BarElement,
24-
BarController,
25-
LineElement,
2629
LineController,
30+
LineElement,
2731
PointElement,
2832
Tooltip,
29-
Filler,
30-
Legend,
31-
ArcElement,
32-
DoughnutController,
3333
} from "chart.js";
3434
import Vue3Tour from "vue3-tour"
3535
import VueVirtualScroller from "vue-virtual-scroller";
@@ -52,7 +52,7 @@ import TaskNumber from "../components/flows/tasks/TaskNumber.vue";
5252
import TaskObject from "../components/flows/tasks/TaskObject.vue";
5353
import TaskString from "../components/flows/tasks/TaskString.vue";
5454
import TaskTask from "../components/flows/tasks/TaskTask.vue";
55-
import TaskOneOf from "../components/flows/tasks/TaskOneOf.vue";
55+
import TaskAnyOf from "../components/flows/tasks/TaskAnyOf.vue";
5656
import TaskSubflowNamespace from "../components/flows/tasks/TaskSubflowNamespace.vue";
5757
import TaskSubflowId from "../components/flows/tasks/TaskSubflowId.vue";
5858
import TaskSubflowInputs from "../components/flows/tasks/TaskSubflowInputs.vue";
@@ -187,7 +187,7 @@ export default async (app, routes, stores, translations, additionalTranslations
187187
app.component("TaskComplex", TaskComplex)
188188
app.component("TaskString", TaskString)
189189
app.component("TaskTask", TaskTask)
190-
app.component("TaskOneOf", TaskOneOf)
190+
app.component("TaskAnyOf", TaskAnyOf)
191191
app.component("TaskSubflowNamespace", TaskSubflowNamespace)
192192
app.component("TaskSubflowId", TaskSubflowId)
193193
app.component("TaskSubflowInputs", TaskSubflowInputs)

0 commit comments

Comments
 (0)