Skip to content

Commit 3f28149

Browse files
Fix #36 - Introduce FloatOrString to handle numeric values (#41)
* Fix #36 - Introduce FloatOrString to handle numeric values Signed-off-by: Ricardo Zanini <[email protected]> * Fix formatting Signed-off-by: Ricardo Zanini <[email protected]>
1 parent 0b69af5 commit 3f28149

File tree

5 files changed

+277
-2
lines changed

5 files changed

+277
-2
lines changed

model/retry.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package model
1616

1717
import (
18+
"github.com/serverlessworkflow/sdk-go/util/floatstr"
1819
"k8s.io/apimachinery/pkg/util/intstr"
1920
)
2021

@@ -29,9 +30,9 @@ type Retry struct {
2930
// Static value by which the delay increases during each attempt (ISO 8601 time format)
3031
Increment string `json:"increment,omitempty"`
3132
// Numeric value, if specified the delay between retries is multiplied by this value.
32-
Multiplier intstr.IntOrString `json:"multiplier,omitempty" validate:"omitempty,min=0"`
33+
Multiplier floatstr.Float32OrString `json:"multiplier,omitempty" validate:"omitempty,min=0"`
3334
// Maximum number of retry attempts.
3435
MaxAttempts intstr.IntOrString `json:"maxAttempts" validate:"required,min=0"`
3536
// If float type, maximum amount of random time added or subtracted from the delay between each retry relative to total delay (between 0 and 1). If string type, absolute maximum amount of random time added or subtracted from the delay between each retry (ISO 8601 duration format)
36-
Jitter intstr.IntOrString `json:"jitter,omitempty" validate:"omitempty,min=0,max=1"`
37+
Jitter floatstr.Float32OrString `json:"jitter,omitempty" validate:"omitempty,min=0,max=1"`
3738
}

parser/parser_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ func TestFromFile(t *testing.T) {
117117
assert.NotEmpty(t, dataBaseSwitchState.DataConditions)
118118
assert.Equal(t, "CheckApplication", w.States[0].GetName())
119119
},
120+
// validates: https://github.com/serverlessworkflow/sdk-go/issues/36
121+
"./testdata/patientonboarding.sw.yaml": func(t *testing.T, w *model.Workflow) {
122+
assert.IsType(t, &model.EventState{}, w.States[0])
123+
eventState := w.States[0].(*model.EventState)
124+
assert.NotNil(t, eventState)
125+
assert.NotEmpty(t, w.Retries)
126+
assert.Len(t, w.Retries, 1)
127+
assert.Equal(t, float32(0.0), w.Retries[0].Jitter.FloatVal)
128+
assert.Equal(t, float32(1.1), w.Retries[0].Multiplier.FloatVal)
129+
},
120130
}
121131
for file, f := range files {
122132
workflow, err := FromFile(file)
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2021 The Serverless Workflow Specification Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
id: patientonboarding
16+
name: Patient Onboarding Workflow
17+
version: '1.0'
18+
start: Onboard
19+
states:
20+
- name: Onboard
21+
type: event
22+
onEvents:
23+
- eventRefs:
24+
- NewPatientEvent
25+
actions:
26+
- functionRef: StorePatient
27+
- functionRef: AssignDoctor
28+
- functionRef: ScheduleAppt
29+
onErrors:
30+
- error: ServiceNotAvailable
31+
code: '503'
32+
retryRef: ServicesNotAvailableRetryStrategy
33+
end: true
34+
end: true
35+
events:
36+
- name: StorePatient
37+
type: new.patients.event
38+
source: newpatient/+
39+
functions:
40+
- name: StoreNewPatientInfo
41+
operation: api/services.json#addPatient
42+
- name: AssignDoctor
43+
operation: api/services.json#assignDoctor
44+
- name: ScheduleAppt
45+
operation: api/services.json#scheduleAppointment
46+
retries:
47+
- name: ServicesNotAvailableRetryStrategy
48+
delay: PT3S
49+
maxAttempts: 10
50+
jitter: 0.0
51+
multiplier: 1.1

util/floatstr/floatstr.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2021 The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package floatstr
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"strconv"
21+
)
22+
23+
// Float32OrString is a type that can hold a float32 or a string.
24+
// implementation borrowed from apimachinary intstr package: https://github.com/kubernetes/apimachinery/blob/master/pkg/util/intstr/intstr.go
25+
type Float32OrString struct {
26+
Type Type `json:"type,omitempty"`
27+
FloatVal float32 `json:"floatVal,omitempty"`
28+
StrVal string `json:"strVal,omitempty"`
29+
}
30+
31+
// Type represents the stored type of Float32OrString.
32+
type Type int64
33+
34+
const (
35+
// Float ...
36+
Float Type = iota // The Float32OrString holds a float.
37+
// String ...
38+
String // The Float32OrString holds a string.
39+
)
40+
41+
// FromFloat creates an Float32OrString object with a float32 value. It is
42+
// your responsibility not to call this method with a value greater
43+
// than float32.
44+
func FromFloat(val float32) Float32OrString {
45+
return Float32OrString{Type: Float, FloatVal: val}
46+
}
47+
48+
// FromString creates a Float32OrString object with a string value.
49+
func FromString(val string) Float32OrString {
50+
return Float32OrString{Type: String, StrVal: val}
51+
}
52+
53+
// Parse the given string and try to convert it to a float32 before
54+
// setting it as a string value.
55+
func Parse(val string) Float32OrString {
56+
f, err := strconv.ParseFloat(val, 32)
57+
if err != nil {
58+
return FromString(val)
59+
}
60+
return FromFloat(float32(f))
61+
}
62+
63+
// UnmarshalJSON implements the json.Unmarshaller interface.
64+
func (floatstr *Float32OrString) UnmarshalJSON(value []byte) error {
65+
if value[0] == '"' {
66+
floatstr.Type = String
67+
return json.Unmarshal(value, &floatstr.StrVal)
68+
}
69+
floatstr.Type = Float
70+
return json.Unmarshal(value, &floatstr.FloatVal)
71+
}
72+
73+
// MarshalJSON implements the json.Marshaller interface.
74+
func (floatstr Float32OrString) MarshalJSON() ([]byte, error) {
75+
switch floatstr.Type {
76+
case Float:
77+
return json.Marshal(floatstr.FloatVal)
78+
case String:
79+
return json.Marshal(floatstr.StrVal)
80+
default:
81+
return []byte{}, fmt.Errorf("impossible Float32OrString.Type")
82+
}
83+
}
84+
85+
// String returns the string value, or the float value.
86+
func (floatstr *Float32OrString) String() string {
87+
if floatstr == nil {
88+
return "<nil>"
89+
}
90+
if floatstr.Type == String {
91+
return floatstr.StrVal
92+
}
93+
return strconv.FormatFloat(float64(floatstr.FloatValue()), 'E', -1, 32)
94+
}
95+
96+
// FloatValue returns the FloatVal if type float32, or if
97+
// it is a String, will attempt a conversion to float32,
98+
// returning 0 if a parsing error occurs.
99+
func (floatstr *Float32OrString) FloatValue() float32 {
100+
if floatstr.Type == String {
101+
f, _ := strconv.ParseFloat(floatstr.StrVal, 32)
102+
return float32(f)
103+
}
104+
return floatstr.FloatVal
105+
}

util/floatstr/floatstr_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2021 The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package floatstr
16+
17+
import (
18+
"encoding/json"
19+
"k8s.io/apimachinery/pkg/util/yaml"
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestFromFloat(t *testing.T) {
25+
i := FromFloat(93.93)
26+
if i.Type != Float || i.FloatVal != 93.93 {
27+
t.Errorf("Expected FloatVal=93.93, got %+v", i)
28+
}
29+
}
30+
31+
func TestFromString(t *testing.T) {
32+
i := FromString("76.76")
33+
if i.Type != String || i.StrVal != "76.76" {
34+
t.Errorf("Expected StrVal=\"76.76\", got %+v", i)
35+
}
36+
}
37+
38+
type FloatOrStringHolder struct {
39+
FOrS Float32OrString `json:"val"`
40+
}
41+
42+
func TestIntOrStringUnmarshalJSON(t *testing.T) {
43+
cases := []struct {
44+
input string
45+
result Float32OrString
46+
}{
47+
{"{\"val\": 123.123}", FromFloat(123.123)},
48+
{"{\"val\": \"123.123\"}", FromString("123.123")},
49+
}
50+
51+
for _, c := range cases {
52+
var result FloatOrStringHolder
53+
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
54+
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
55+
}
56+
if result.FOrS != c.result {
57+
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
58+
}
59+
}
60+
}
61+
62+
func TestIntOrStringMarshalJSON(t *testing.T) {
63+
cases := []struct {
64+
input Float32OrString
65+
result string
66+
}{
67+
{FromFloat(123.123), "{\"val\":123.123}"},
68+
{FromString("123.123"), "{\"val\":\"123.123\"}"},
69+
}
70+
71+
for _, c := range cases {
72+
input := FloatOrStringHolder{c.input}
73+
result, err := json.Marshal(&input)
74+
if err != nil {
75+
t.Errorf("Failed to marshal input '%v': %v", input, err)
76+
}
77+
if string(result) != c.result {
78+
t.Errorf("Failed to marshal input '%v': expected: %+v, got %q", input, c.result, string(result))
79+
}
80+
}
81+
}
82+
83+
func TestIntOrStringMarshalJSONUnmarshalYAML(t *testing.T) {
84+
cases := []struct {
85+
input Float32OrString
86+
}{
87+
{FromFloat(123.123)},
88+
{FromString("123.123")},
89+
}
90+
91+
for _, c := range cases {
92+
input := FloatOrStringHolder{c.input}
93+
jsonMarshalled, err := json.Marshal(&input)
94+
if err != nil {
95+
t.Errorf("1: Failed to marshal input: '%v': %v", input, err)
96+
}
97+
98+
var result FloatOrStringHolder
99+
err = yaml.Unmarshal(jsonMarshalled, &result)
100+
if err != nil {
101+
t.Errorf("2: Failed to unmarshal '%+v': %v", string(jsonMarshalled), err)
102+
}
103+
104+
if !reflect.DeepEqual(input, result) {
105+
t.Errorf("3: Failed to marshal input '%+v': got %+v", input, result)
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)