Skip to content

Commit 6b22a3e

Browse files
committed
no-issue: Fix eagerly set inputBuilder from/schema to avoid validation errors in runtime
Signed-off-by: Ricardo Zanini <ricardozanini@gmail.com>
1 parent a323be7 commit 6b22a3e

6 files changed

Lines changed: 267 additions & 2 deletions

File tree

fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/InputBuilder.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,32 @@ public class InputBuilder {
2929

3030
InputBuilder() {
3131
this.input = new Input();
32-
this.input.setFrom(new InputFrom());
33-
this.input.setSchema(new SchemaUnion());
3432
}
3533

3634
public InputBuilder from(String expr) {
35+
if (this.input.getFrom() == null) this.input.setFrom(new InputFrom());
36+
else this.input.getFrom().setObject(null);
37+
3738
this.input.getFrom().setString(expr);
3839
return this;
3940
}
4041

4142
public InputBuilder from(Object object) {
43+
if (this.input.getFrom() == null) this.input.setFrom(new InputFrom());
44+
else this.input.getFrom().setString(null);
45+
4246
this.input.getFrom().setObject(object);
4347
return this;
4448
}
4549

4650
public InputBuilder schema(Object schema) {
51+
if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion());
4752
this.input.getSchema().setSchemaInline(new SchemaInline(schema));
4853
return this;
4954
}
5055

5156
public InputBuilder schema(String schema) {
57+
if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion());
5258
this.input
5359
.getSchema()
5460
.setSchemaExternal(
@@ -61,6 +67,12 @@ public InputBuilder schema(String schema) {
6167
return this;
6268
}
6369

70+
public InputBuilder schemaAsJsonString(String schema) {
71+
if (this.input.getSchema() == null) this.input.setSchema(new SchemaUnion());
72+
this.input.getSchema().setSchemaInline(new SchemaInline(schema));
73+
return this;
74+
}
75+
6476
public Input build() {
6577
return this.input;
6678
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.fluent.spec;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
22+
import io.serverlessworkflow.api.types.Input;
23+
import java.util.Map;
24+
import org.junit.jupiter.api.Test;
25+
26+
/** Unit tests for InputBuilder to verify lazy initialization and proper field handling. */
27+
public class InputBuilderTest {
28+
29+
@Test
30+
void testEmptyInputBuilder() {
31+
// When no methods are called, from and schema should be null
32+
Input input = new InputBuilder().build();
33+
34+
assertNotNull(input, "Input should not be null");
35+
assertNull(input.getFrom(), "From should be null when not set");
36+
assertNull(input.getSchema(), "Schema should be null when not set");
37+
}
38+
39+
@Test
40+
void testFromStringOnly() {
41+
// Setting only from(String) should not initialize schema
42+
Input input = new InputBuilder().from("$.data").build();
43+
44+
assertNotNull(input.getFrom(), "From should be set");
45+
assertEquals("$.data", input.getFrom().getString(), "From string should match");
46+
assertNull(input.getFrom().getObject(), "From object should be null");
47+
assertNull(input.getSchema(), "Schema should be null when not set");
48+
}
49+
50+
@Test
51+
void testFromObjectOnly() {
52+
// Setting only from(Object) should not initialize schema
53+
Map<String, Object> data = Map.of("key", "value");
54+
Input input = new InputBuilder().from(data).build();
55+
56+
assertNotNull(input.getFrom(), "From should be set");
57+
assertNotNull(input.getFrom().getObject(), "From object should be set");
58+
assertNull(input.getFrom().getString(), "From string should be null");
59+
assertEquals(data, input.getFrom().getObject(), "From object should match");
60+
assertNull(input.getSchema(), "Schema should be null when not set");
61+
}
62+
63+
@Test
64+
void testSchemaObjectOnly() {
65+
// This is the key test case - setting only schema should not initialize from
66+
Map<String, Object> schema = Map.of("type", "object", "properties", Map.of());
67+
Input input = new InputBuilder().schema(schema).build();
68+
69+
assertNotNull(input.getSchema(), "Schema should be set");
70+
assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set");
71+
assertNull(input.getFrom(), "From should be null when not set");
72+
}
73+
74+
@Test
75+
void testSchemaStringOnly() {
76+
// Setting only schema(String) should not initialize from
77+
String schemaUri = "http://example.com/schema.json";
78+
Input input = new InputBuilder().schema(schemaUri).build();
79+
80+
assertNotNull(input.getSchema(), "Schema should be set");
81+
assertNotNull(input.getSchema().getSchemaExternal(), "Schema external should be set");
82+
assertNull(input.getFrom(), "From should be null when not set");
83+
}
84+
85+
@Test
86+
void testSchemaAsJsonStringOnly() {
87+
// Setting only schemaAsJsonString should not initialize from
88+
String jsonSchema = "{\"type\":\"object\",\"properties\":{}}";
89+
Input input = new InputBuilder().schemaAsJsonString(jsonSchema).build();
90+
91+
assertNotNull(input.getSchema(), "Schema should be set");
92+
assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set");
93+
assertNull(input.getFrom(), "From should be null when not set");
94+
}
95+
96+
@Test
97+
void testFromStringThenObject() {
98+
// Setting from(String) then from(Object) should clear the string
99+
Map<String, Object> data = Map.of("foo", "bar");
100+
Input input = new InputBuilder().from("$.initial").from(data).build();
101+
102+
assertNotNull(input.getFrom(), "From should be set");
103+
assertNotNull(input.getFrom().getObject(), "From object should be set");
104+
assertNull(input.getFrom().getString(), "From string should be cleared");
105+
assertEquals(data, input.getFrom().getObject(), "From object should be the last set value");
106+
}
107+
108+
@Test
109+
void testFromObjectThenString() {
110+
// Setting from(Object) then from(String) should clear the object
111+
Map<String, Object> data = Map.of("foo", "bar");
112+
Input input = new InputBuilder().from(data).from("$.final").build();
113+
114+
assertNotNull(input.getFrom(), "From should be set");
115+
assertEquals(
116+
"$.final", input.getFrom().getString(), "From string should be the last set value");
117+
assertNull(input.getFrom().getObject(), "From object should be cleared");
118+
}
119+
120+
@Test
121+
void testSchemaObjectThenString() {
122+
// Setting schema(Object) then schema(String) sets external schema
123+
// Note: SchemaUnion may keep both inline and external, last one set takes precedence
124+
Map<String, Object> inlineSchema = Map.of("type", "object");
125+
String externalUri = "http://example.com/schema.json";
126+
Input input = new InputBuilder().schema(inlineSchema).schema(externalUri).build();
127+
128+
assertNotNull(input.getSchema(), "Schema should be set");
129+
assertNotNull(input.getSchema().getSchemaExternal(), "Schema external should be set");
130+
}
131+
132+
@Test
133+
void testSchemaStringThenObject() {
134+
// Setting schema(String) then schema(Object) should replace external with inline
135+
String externalUri = "http://example.com/schema.json";
136+
Map<String, Object> inlineSchema = Map.of("type", "object");
137+
Input input = new InputBuilder().schema(externalUri).schema(inlineSchema).build();
138+
139+
assertNotNull(input.getSchema(), "Schema should be set");
140+
assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set");
141+
// Note: SchemaUnion may keep both, but inline takes precedence in serialization
142+
}
143+
144+
@Test
145+
void testBothFromAndSchema() {
146+
// Setting both from and schema should work correctly
147+
String fromExpr = "$.input";
148+
Map<String, Object> schema = Map.of("type", "object");
149+
Input input = new InputBuilder().from(fromExpr).schema(schema).build();
150+
151+
assertNotNull(input.getFrom(), "From should be set");
152+
assertEquals(fromExpr, input.getFrom().getString(), "From string should match");
153+
assertNotNull(input.getSchema(), "Schema should be set");
154+
assertNotNull(input.getSchema().getSchemaInline(), "Schema inline should be set");
155+
}
156+
157+
@Test
158+
void testSchemaOnlyDoesNotCreateFrom() {
159+
// Critical test: verifies the fix for the original issue
160+
// When only schema is set, from should remain null to avoid validation errors
161+
Map<String, Object> schema =
162+
Map.of("type", "object", "properties", Map.of("name", Map.of("type", "string")));
163+
164+
Input input = new InputBuilder().schema(schema).build();
165+
166+
assertNull(
167+
input.getFrom(),
168+
"From must be null when only schema is set (this was the bug being fixed)");
169+
assertNotNull(input.getSchema(), "Schema should be set");
170+
}
171+
172+
@Test
173+
void testMultipleFromCalls() {
174+
// Multiple calls to from() should keep updating correctly
175+
Input input =
176+
new InputBuilder().from("$.first").from(Map.of("second", true)).from("$.third").build();
177+
178+
assertNotNull(input.getFrom(), "From should be set");
179+
assertEquals("$.third", input.getFrom().getString(), "From should be the last value set");
180+
assertNull(input.getFrom().getObject(), "From object should be null");
181+
}
182+
183+
@Test
184+
void testMultipleSchemaCalls() {
185+
// Multiple calls to schema methods should keep updating correctly
186+
Input input =
187+
new InputBuilder()
188+
.schema(Map.of("type", "string"))
189+
.schema("http://example.com/schema")
190+
.schemaAsJsonString("{\"type\":\"number\"}")
191+
.build();
192+
193+
assertNotNull(input.getSchema(), "Schema should be set");
194+
assertNotNull(input.getSchema().getSchemaInline(), "Last schema call was inline");
195+
}
196+
197+
@Test
198+
void testSchemaObjectInitializesLazily() {
199+
// Verify that SchemaUnion is only created when schema() is called
200+
InputBuilder builder = new InputBuilder();
201+
Input input1 = builder.build();
202+
assertNull(input1.getSchema(), "Schema should be null before any schema() call");
203+
204+
InputBuilder builder2 = new InputBuilder();
205+
builder2.schema(Map.of("type", "object"));
206+
Input input2 = builder2.build();
207+
assertNotNull(input2.getSchema(), "Schema should be initialized after schema() call");
208+
}
209+
210+
@Test
211+
void testFromObjectInitializesLazily() {
212+
// Verify that InputFrom is only created when from() is called
213+
InputBuilder builder = new InputBuilder();
214+
Input input1 = builder.build();
215+
assertNull(input1.getFrom(), "From should be null before any from() call");
216+
217+
InputBuilder builder2 = new InputBuilder();
218+
builder2.from("$.test");
219+
Input input2 = builder2.build();
220+
assertNotNull(input2.getFrom(), "From should be initialized after from() call");
221+
}
222+
223+
@Test
224+
void testSchemaOnlyWithExplicitSetFromNull() {
225+
// Simulates the original use case from AgenticFlow where users had to
226+
// call setFrom(null) to avoid validation errors
227+
// This test verifies that calling setFrom(null) after setting schema works
228+
Map<String, Object> schema = Map.of("type", "object");
229+
Input input = new InputBuilder().schema(schema).build();
230+
231+
// Manually setting from to null (as users had to do before the fix)
232+
input.setFrom(null);
233+
234+
assertNull(input.getFrom(), "From should be null");
235+
assertNotNull(input.getSchema(), "Schema should still be set");
236+
}
237+
238+
@Test
239+
void testSchemaOnlyDoesNotRequireSetFromNull() {
240+
// This test verifies the fix - when only schema is set,
241+
// from is already null, so users don't need to call setFrom(null)
242+
Map<String, Object> schema = Map.of("type", "object", "properties", Map.of());
243+
Input input = new InputBuilder().schema(schema).build();
244+
245+
// No need to call input.setFrom(null) anymore!
246+
assertNull(
247+
input.getFrom(), "From should be null without needing to explicitly call setFrom(null)");
248+
assertNotNull(input.getSchema(), "Schema should be set");
249+
250+
// Verify this would not cause "Both object and str are null" validation error
251+
// (in the actual runtime, this input would be validated without errors)
252+
}
253+
}

impl/test/db-samples/running.db

0 Bytes
Binary file not shown.

impl/test/db-samples/running_v1.db

0 Bytes
Binary file not shown.

impl/test/db-samples/suspended.db

0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)