Skip to content

Fix #36 - Introduce FloatOrString to handle numeric values #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -92,7 +91,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand Down Expand Up @@ -173,7 +171,6 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand All @@ -188,7 +185,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Expand Down
5 changes: 3 additions & 2 deletions model/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package model

import (
"github.com/serverlessworkflow/sdk-go/util/floatstr"
"k8s.io/apimachinery/pkg/util/intstr"
)

Expand All @@ -29,9 +30,9 @@ type Retry struct {
// Static value by which the delay increases during each attempt (ISO 8601 time format)
Increment string `json:"increment,omitempty"`
// Numeric value, if specified the delay between retries is multiplied by this value.
Multiplier intstr.IntOrString `json:"multiplier,omitempty" validate:"omitempty,min=0"`
Multiplier floatstr.Float32OrString `json:"multiplier,omitempty" validate:"omitempty,min=0"`
// Maximum number of retry attempts.
MaxAttempts intstr.IntOrString `json:"maxAttempts" validate:"required,min=0"`
// 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)
Jitter intstr.IntOrString `json:"jitter,omitempty" validate:"omitempty,min=0,max=1"`
Jitter floatstr.Float32OrString `json:"jitter,omitempty" validate:"omitempty,min=0,max=1"`
}
10 changes: 10 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ func TestFromFile(t *testing.T) {
assert.NotEmpty(t, dataBaseSwitchState.DataConditions)
assert.Equal(t, "CheckApplication", w.States[0].GetName())
},
// validates: https://github.com/serverlessworkflow/sdk-go/issues/36
"./testdata/patientonboarding.sw.yaml": func(t *testing.T, w *model.Workflow) {
assert.IsType(t, &model.EventState{}, w.States[0])
eventState := w.States[0].(*model.EventState)
assert.NotNil(t, eventState)
assert.NotEmpty(t, w.Retries)
assert.Len(t, w.Retries, 1)
assert.Equal(t, float32(0.0), w.Retries[0].Jitter.FloatVal)
assert.Equal(t, float32(1.1), w.Retries[0].Multiplier.FloatVal)
},
}
for file, f := range files {
workflow, err := FromFile(file)
Expand Down
51 changes: 51 additions & 0 deletions parser/testdata/patientonboarding.sw.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2021 The Serverless Workflow Specification Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

id: patientonboarding
name: Patient Onboarding Workflow
version: '1.0'
start: Onboard
states:
- name: Onboard
type: event
onEvents:
- eventRefs:
- NewPatientEvent
actions:
- functionRef: StorePatient
- functionRef: AssignDoctor
- functionRef: ScheduleAppt
onErrors:
- error: ServiceNotAvailable
code: '503'
retryRef: ServicesNotAvailableRetryStrategy
end: true
end: true
events:
- name: StorePatient
type: new.patients.event
source: newpatient/+
functions:
- name: StoreNewPatientInfo
operation: api/services.json#addPatient
- name: AssignDoctor
operation: api/services.json#assignDoctor
- name: ScheduleAppt
operation: api/services.json#scheduleAppointment
retries:
- name: ServicesNotAvailableRetryStrategy
delay: PT3S
maxAttempts: 10
jitter: 0.0
multiplier: 1.1
105 changes: 105 additions & 0 deletions util/floatstr/floatstr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2021 The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package floatstr

import (
"encoding/json"
"fmt"
"strconv"
)

// Float32OrString is a type that can hold a float32 or a string.
// implementation borrowed from apimachinary intstr package: https://github.com/kubernetes/apimachinery/blob/master/pkg/util/intstr/intstr.go
type Float32OrString struct {
Type Type `json:"type,omitempty"`
FloatVal float32 `json:"floatVal,omitempty"`
StrVal string `json:"strVal,omitempty"`
}

// Type represents the stored type of Float32OrString.
type Type int64

const (
// Float ...
Float Type = iota // The Float32OrString holds a float.
// String ...
String // The Float32OrString holds a string.
)

// FromFloat creates an Float32OrString object with a float32 value. It is
// your responsibility not to call this method with a value greater
// than float32.
func FromFloat(val float32) Float32OrString {
return Float32OrString{Type: Float, FloatVal: val}
}

// FromString creates a Float32OrString object with a string value.
func FromString(val string) Float32OrString {
return Float32OrString{Type: String, StrVal: val}
}

// Parse the given string and try to convert it to a float32 before
// setting it as a string value.
func Parse(val string) Float32OrString {
f, err := strconv.ParseFloat(val, 32)
if err != nil {
return FromString(val)
}
return FromFloat(float32(f))
}

// UnmarshalJSON implements the json.Unmarshaller interface.
func (floatstr *Float32OrString) UnmarshalJSON(value []byte) error {
if value[0] == '"' {
floatstr.Type = String
return json.Unmarshal(value, &floatstr.StrVal)
}
floatstr.Type = Float
return json.Unmarshal(value, &floatstr.FloatVal)
}

// MarshalJSON implements the json.Marshaller interface.
func (floatstr Float32OrString) MarshalJSON() ([]byte, error) {
switch floatstr.Type {
case Float:
return json.Marshal(floatstr.FloatVal)
case String:
return json.Marshal(floatstr.StrVal)
default:
return []byte{}, fmt.Errorf("impossible Float32OrString.Type")
}
}

// String returns the string value, or the float value.
func (floatstr *Float32OrString) String() string {
if floatstr == nil {
return "<nil>"
}
if floatstr.Type == String {
return floatstr.StrVal
}
return strconv.FormatFloat(float64(floatstr.FloatValue()), 'E', -1, 32)
}

// FloatValue returns the FloatVal if type float32, or if
// it is a String, will attempt a conversion to float32,
// returning 0 if a parsing error occurs.
func (floatstr *Float32OrString) FloatValue() float32 {
if floatstr.Type == String {
f, _ := strconv.ParseFloat(floatstr.StrVal, 32)
return float32(f)
}
return floatstr.FloatVal
}
108 changes: 108 additions & 0 deletions util/floatstr/floatstr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2021 The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package floatstr

import (
"encoding/json"
"k8s.io/apimachinery/pkg/util/yaml"
"reflect"
"testing"
)

func TestFromFloat(t *testing.T) {
i := FromFloat(93.93)
if i.Type != Float || i.FloatVal != 93.93 {
t.Errorf("Expected FloatVal=93.93, got %+v", i)
}
}

func TestFromString(t *testing.T) {
i := FromString("76.76")
if i.Type != String || i.StrVal != "76.76" {
t.Errorf("Expected StrVal=\"76.76\", got %+v", i)
}
}

type FloatOrStringHolder struct {
FOrS Float32OrString `json:"val"`
}

func TestIntOrStringUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result Float32OrString
}{
{"{\"val\": 123.123}", FromFloat(123.123)},
{"{\"val\": \"123.123\"}", FromString("123.123")},
}

for _, c := range cases {
var result FloatOrStringHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
}
if result.FOrS != c.result {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
}

func TestIntOrStringMarshalJSON(t *testing.T) {
cases := []struct {
input Float32OrString
result string
}{
{FromFloat(123.123), "{\"val\":123.123}"},
{FromString("123.123"), "{\"val\":\"123.123\"}"},
}

for _, c := range cases {
input := FloatOrStringHolder{c.input}
result, err := json.Marshal(&input)
if err != nil {
t.Errorf("Failed to marshal input '%v': %v", input, err)
}
if string(result) != c.result {
t.Errorf("Failed to marshal input '%v': expected: %+v, got %q", input, c.result, string(result))
}
}
}

func TestIntOrStringMarshalJSONUnmarshalYAML(t *testing.T) {
cases := []struct {
input Float32OrString
}{
{FromFloat(123.123)},
{FromString("123.123")},
}

for _, c := range cases {
input := FloatOrStringHolder{c.input}
jsonMarshalled, err := json.Marshal(&input)
if err != nil {
t.Errorf("1: Failed to marshal input: '%v': %v", input, err)
}

var result FloatOrStringHolder
err = yaml.Unmarshal(jsonMarshalled, &result)
if err != nil {
t.Errorf("2: Failed to unmarshal '%+v': %v", string(jsonMarshalled), err)
}

if !reflect.DeepEqual(input, result) {
t.Errorf("3: Failed to marshal input '%+v': got %+v", input, result)
}
}
}