Skip to content

Commit a1e3d37

Browse files
authored
Add number type conversion validation to ensure proper casting between numeric types (#1205)
Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
1 parent 2853033 commit a1e3d37

4 files changed

Lines changed: 270 additions & 1 deletion

File tree

experimental/model/src/main/java/io/serverlessworkflow/impl/model/func/JavaModel.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import io.serverlessworkflow.impl.AbstractWorkflowModel;
1919
import io.serverlessworkflow.impl.WorkflowModel;
20+
import java.math.BigDecimal;
21+
import java.math.BigInteger;
2022
import java.time.OffsetDateTime;
2123
import java.util.Collection;
2224
import java.util.Collections;
@@ -64,6 +66,28 @@ public Optional<Number> asNumber() {
6466
return object instanceof Number value ? Optional.of(value) : Optional.empty();
6567
}
6668

69+
@Override
70+
protected <N extends Number> Optional<N> asNumber(Class<N> targetNumberClass) {
71+
if (!(object instanceof Number num)) {
72+
return Optional.empty();
73+
}
74+
if (targetNumberClass == Integer.class || targetNumberClass == BigInteger.class) {
75+
return Optional.of(targetNumberClass.cast(num.intValue()));
76+
} else if (targetNumberClass == Long.class) {
77+
return Optional.of(targetNumberClass.cast(num.longValue()));
78+
} else if (targetNumberClass == Double.class || targetNumberClass == BigDecimal.class) {
79+
return Optional.of(targetNumberClass.cast(num.doubleValue()));
80+
} else if (targetNumberClass == Float.class) {
81+
return Optional.of(targetNumberClass.cast(num.floatValue()));
82+
} else if (targetNumberClass == Short.class) {
83+
return Optional.of(targetNumberClass.cast(num.shortValue()));
84+
} else if (targetNumberClass == Byte.class) {
85+
return Optional.of(targetNumberClass.cast(num.byteValue()));
86+
} else {
87+
return Optional.of(targetNumberClass.cast(num));
88+
}
89+
}
90+
6791
@Override
6892
public Optional<Map<String, Object>> asMap() {
6993
return object instanceof Map ? Optional.of((Map<String, Object>) object) : Optional.empty();

impl/core/src/main/java/io/serverlessworkflow/impl/AbstractWorkflowModel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public abstract class AbstractWorkflowModel implements WorkflowModel {
2424

2525
protected abstract <T> Optional<T> convert(Class<T> clazz);
2626

27+
protected abstract <N extends Number> Optional<N> asNumber(Class<N> targetNumberClass);
28+
2729
@Override
2830
public <T> Optional<T> as(Class<T> clazz) {
2931
if (WorkflowModel.class.isAssignableFrom(clazz)) {
@@ -35,7 +37,7 @@ public <T> Optional<T> as(Class<T> clazz) {
3537
} else if (OffsetDateTime.class.isAssignableFrom(clazz)) {
3638
return (Optional<T>) asDate();
3739
} else if (Number.class.isAssignableFrom(clazz)) {
38-
return (Optional<T>) asNumber();
40+
return (Optional<T>) asNumber(clazz.asSubclass(Number.class));
3941
} else if (Collection.class.isAssignableFrom(clazz)) {
4042
Collection<?> collection = asCollection();
4143
return collection.isEmpty() ? Optional.empty() : (Optional<T>) Optional.of(collection);

impl/model/src/main/java/io/serverlessworkflow/impl/model/jackson/JacksonModel.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import io.serverlessworkflow.impl.AbstractWorkflowModel;
2525
import io.serverlessworkflow.impl.WorkflowModel;
2626
import io.serverlessworkflow.impl.jackson.JsonUtils;
27+
import java.math.BigDecimal;
28+
import java.math.BigInteger;
2729
import java.time.OffsetDateTime;
2830
import java.util.Collection;
2931
import java.util.Collections;
@@ -69,6 +71,32 @@ public Optional<Number> asNumber() {
6971
return node.isNumber() ? Optional.of(node.asLong()) : Optional.empty();
7072
}
7173

74+
@Override
75+
protected <N extends Number> Optional<N> asNumber(Class<N> targetNumberClass) {
76+
if (!node.isNumber()) {
77+
return Optional.empty();
78+
}
79+
if (targetNumberClass == Integer.class) {
80+
return Optional.of(targetNumberClass.cast(node.asInt()));
81+
} else if (targetNumberClass == Long.class) {
82+
return Optional.of(targetNumberClass.cast(node.asLong()));
83+
} else if (targetNumberClass == Double.class) {
84+
return Optional.of(targetNumberClass.cast(node.asDouble()));
85+
} else if (targetNumberClass == Float.class) {
86+
return Optional.of(targetNumberClass.cast((float) node.asDouble()));
87+
} else if (targetNumberClass == Short.class) {
88+
return Optional.of(targetNumberClass.cast((short) node.asInt()));
89+
} else if (targetNumberClass == Byte.class) {
90+
return Optional.of(targetNumberClass.cast((byte) node.asInt()));
91+
} else if (targetNumberClass == BigDecimal.class) {
92+
return Optional.of(targetNumberClass.cast(node.decimalValue()));
93+
} else if (targetNumberClass == BigInteger.class) {
94+
return Optional.of(targetNumberClass.cast(node.bigIntegerValue()));
95+
} else {
96+
return Optional.of(targetNumberClass.cast(node.numberValue()));
97+
}
98+
}
99+
72100
@Override
73101
public String toString() {
74102
return node.toPrettyString();
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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.impl.test;
17+
18+
import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.function;
19+
20+
import io.serverlessworkflow.api.types.Workflow;
21+
import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder;
22+
import io.serverlessworkflow.impl.WorkflowApplication;
23+
import io.serverlessworkflow.impl.WorkflowModel;
24+
import java.math.BigDecimal;
25+
import java.math.BigInteger;
26+
import java.util.function.Function;
27+
import org.junit.jupiter.api.Assertions;
28+
import org.junit.jupiter.api.Test;
29+
30+
public class WorkflowNumberConversionTest {
31+
32+
@Test
33+
void integer_score_from_task_output_is_compatible_with_outputAs_integer_class() {
34+
Workflow workflow =
35+
FuncWorkflowBuilder.workflow("numbers")
36+
.tasks(
37+
function(
38+
"scoreProposal",
39+
(Proposal input) -> {
40+
Integer score = calculateScore(input.abstractText());
41+
return score;
42+
},
43+
Proposal.class)
44+
.outputAs(
45+
(Integer score) -> new ProposalScore(score, score >= 7), Integer.class))
46+
.build();
47+
48+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
49+
WorkflowModel model =
50+
app.workflowDefinition(workflow)
51+
.instance(new Proposal("Workflow, workflow, workflow..."))
52+
.start()
53+
.join();
54+
Assertions.assertNotNull(model);
55+
ProposalScore result = model.as(ProposalScore.class).orElseThrow();
56+
Assertions.assertEquals(10, result.score());
57+
Assertions.assertTrue(result.accepted());
58+
}
59+
}
60+
61+
@Test
62+
void long_to_integer_conversion() {
63+
Workflow workflow =
64+
FuncWorkflowBuilder.workflow("longToInt")
65+
.tasks(
66+
function("convertLong", Function.identity(), Long.class)
67+
.outputAs((Integer result) -> result * 2, Integer.class))
68+
.build();
69+
70+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
71+
WorkflowModel model = app.workflowDefinition(workflow).instance(100L).start().join();
72+
Integer result = model.as(Integer.class).orElseThrow();
73+
Assertions.assertEquals(200, result);
74+
}
75+
}
76+
77+
@Test
78+
void integer_to_long_conversion() {
79+
Workflow workflow =
80+
FuncWorkflowBuilder.workflow("intToLong")
81+
.tasks(
82+
function("convertInt", Function.identity(), Integer.class)
83+
.outputAs((Long result) -> result * 3L, Long.class))
84+
.build();
85+
86+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
87+
WorkflowModel model = app.workflowDefinition(workflow).instance(50).start().join();
88+
Long result = model.as(Long.class).orElseThrow();
89+
Assertions.assertEquals(150L, result);
90+
}
91+
}
92+
93+
@Test
94+
void integer_to_big_integer_conversion() {
95+
Workflow workflow =
96+
FuncWorkflowBuilder.workflow("integerToBigInteger")
97+
.tasks(
98+
function("convertInt", Function.identity(), Integer.class)
99+
.outputAs(
100+
(BigInteger result) -> result.multiply(BigInteger.valueOf(3)),
101+
BigInteger.class))
102+
.build();
103+
104+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
105+
WorkflowModel model = app.workflowDefinition(workflow).instance(50).start().join();
106+
BigInteger result = model.as(BigInteger.class).orElseThrow();
107+
Assertions.assertEquals(BigInteger.valueOf(150), result);
108+
}
109+
}
110+
111+
@Test
112+
void double_to_integer_conversion() {
113+
Workflow workflow =
114+
FuncWorkflowBuilder.workflow("doubleToInt")
115+
.tasks(
116+
function("convertDouble", Function.identity(), Double.class)
117+
.outputAs((Integer result) -> result + 5, Integer.class))
118+
.build();
119+
120+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
121+
WorkflowModel model = app.workflowDefinition(workflow).instance(42.7).start().join();
122+
Integer result = model.as(Integer.class).orElseThrow();
123+
Assertions.assertEquals(47, result);
124+
}
125+
}
126+
127+
@Test
128+
void double_to_big_decimal_conversion() {
129+
Workflow workflow =
130+
FuncWorkflowBuilder.workflow("doubleToInt")
131+
.tasks(
132+
function("convertDouble", Function.identity(), Double.class)
133+
.outputAs(
134+
(BigDecimal result) -> result.add(BigDecimal.valueOf(5)), BigDecimal.class))
135+
.build();
136+
137+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
138+
WorkflowModel model = app.workflowDefinition(workflow).instance(42.7).start().join();
139+
BigDecimal result = model.as(BigDecimal.class).orElseThrow();
140+
Assertions.assertEquals(BigDecimal.valueOf(47.7), result);
141+
}
142+
}
143+
144+
@Test
145+
void float_to_double_conversion() {
146+
Workflow workflow =
147+
FuncWorkflowBuilder.workflow("floatToDouble")
148+
.tasks(
149+
function("convertFloat", Function.identity(), Float.class)
150+
.outputAs((Double result) -> result * 1.5, Double.class))
151+
.build();
152+
153+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
154+
WorkflowModel model = app.workflowDefinition(workflow).instance(10.0f).start().join();
155+
Double result = model.as(Double.class).orElseThrow();
156+
Assertions.assertEquals(15.0, result, 0.001);
157+
}
158+
}
159+
160+
@Test
161+
void short_to_integer_conversion() {
162+
Workflow workflow =
163+
FuncWorkflowBuilder.workflow("shortToInt")
164+
.tasks(
165+
function("convertShort", (Short input) -> input.intValue(), Short.class)
166+
.outputAs((Integer result) -> result * 10, Integer.class))
167+
.build();
168+
169+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
170+
WorkflowModel model = app.workflowDefinition(workflow).instance((short) 5).start().join();
171+
Integer result = model.as(Integer.class).orElseThrow();
172+
Assertions.assertEquals(50, result);
173+
}
174+
}
175+
176+
@Test
177+
void byte_to_integer_conversion() {
178+
Workflow workflow =
179+
FuncWorkflowBuilder.workflow("byteToInt")
180+
.tasks(
181+
function("convertByte", Function.identity(), Byte.class)
182+
.outputAs((Integer result) -> result + 100, Integer.class))
183+
.build();
184+
185+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
186+
WorkflowModel model = app.workflowDefinition(workflow).instance((byte) 25).start().join();
187+
Integer result = model.as(Integer.class).orElseThrow();
188+
Assertions.assertEquals(125, result);
189+
}
190+
}
191+
192+
@Test
193+
void number_conversion_with_string_output() {
194+
// This verifies that model.as(Integer.class) (via asNumber(Integer.class)) returns
195+
// Optional.empty()
196+
Workflow workflow =
197+
FuncWorkflowBuilder.workflow("stringOutput")
198+
.tasks(function("returnString", (Integer input) -> "result: " + input, Integer.class))
199+
.build();
200+
201+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
202+
WorkflowModel model = app.workflowDefinition(workflow).instance(42).start().join();
203+
Assertions.assertTrue(model.as(Integer.class).isEmpty());
204+
Assertions.assertEquals("result: 42", model.as(String.class).orElseThrow());
205+
}
206+
}
207+
208+
private Integer calculateScore(String abstractText) {
209+
return abstractText.contains("Workflow") ? 10 : 5;
210+
}
211+
212+
public record ProposalScore(Integer score, boolean accepted) {}
213+
214+
public record Proposal(String abstractText) {}
215+
}

0 commit comments

Comments
 (0)