diff --git a/README.md b/README.md index 075e6f2..786333e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The current status of features implemented in the SDK is listed below: | [v1.0.0](https://github.com/serverlessworkflow/sdk-go/releases/tag/v1.0.0) | [v0.5](https://github.com/serverlessworkflow/specification/tree/0.5.x) | | [v2.0.1](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.0.1) | [v0.6](https://github.com/serverlessworkflow/specification/tree/0.6.x) | | [v2.1.2](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.1.2) | [v0.7](https://github.com/serverlessworkflow/specification/tree/0.7.x) | -| [v2.4.1](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.4.1) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) | +| [v2.4.3](https://github.com/serverlessworkflow/sdk-go/releases/tag/v2.4.1) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) | | [v3.0.0](https://github.com/serverlessworkflow/sdk-go/releases/tag/v3.0.0) | [v1.0.0](https://github.com/serverlessworkflow/specification/releases/tag/v1.0.0-alpha5) | --- @@ -67,7 +67,30 @@ import "github.com/serverlessworkflow/sdk-go/v3/model" You can now use the SDK types and functions, for example: ```go -myHttpTask := model.CallHTTP{} +package main + +import ( + "github.com/serverlessworkflow/sdk-go/v3/builder" + "github.com/serverlessworkflow/sdk-go/v3/model" +) + +func main() { + workflowBuilder := New(). + SetDocument("1.0.0", "examples", "example-workflow", "1.0.0"). + AddTask("task1", &model.CallHTTP{ + TaskBase: model.TaskBase{ + If: &model.RuntimeExpression{Value: "${condition}"}, + }, + Call: "http", + With: model.HTTPArguments{ + Method: "GET", + Endpoint: model.NewEndpoint("http://example.com"), + }, + }) + workflow, _ := builder.Object(workflowBuilder) + // use your models +} + ``` ### Parsing Workflow Files diff --git a/model/validator.go b/model/validator.go index b093803..91c34b9 100644 --- a/model/validator.go +++ b/model/validator.go @@ -17,15 +17,15 @@ package model import ( "errors" "fmt" - "regexp" - "github.com/go-playground/validator/v10" + "regexp" + "strings" ) var ( - iso8601DurationPattern = regexp.MustCompile(`^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$`) + iso8601DurationPattern = regexp.MustCompile(`^P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$`) semanticVersionPattern = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) - hostnameRFC1123Pattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) + hostnameRFC1123Pattern = regexp.MustCompile(`^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z]{2,63}|[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)$`) ) var validate *validator.Validate @@ -47,11 +47,8 @@ func init() { registerValidator("client_auth_type", validateOptionalOAuthClientAuthentication) registerValidator("encoding_type", validateOptionalOAuth2TokenRequestEncoding) - registerValidator("semver_pattern", func(fl validator.FieldLevel) bool { - return semanticVersionPattern.MatchString(fl.Field().String()) - }) registerValidator("hostname_rfc1123", func(fl validator.FieldLevel) bool { - return hostnameRFC1123Pattern.MatchString(fl.Field().String()) + return isHostnameValid(fl.Field().String()) }) registerValidator("uri_pattern", func(fl validator.FieldLevel) bool { value, ok := fl.Field().Interface().(string) @@ -67,6 +64,7 @@ func init() { } return LiteralUriTemplatePattern.MatchString(value) }) + registerValidator("semver_pattern", validateSemanticVersion) registerValidator("iso8601_duration", validateISO8601Duration) registerValidator("object_or_string", validateObjectOrString) @@ -349,9 +347,43 @@ func validateJsonPointerOrRuntimeExpr(fl validator.FieldLevel) bool { } func validateISO8601Duration(fl validator.FieldLevel) bool { - value, ok := fl.Field().Interface().(string) + input, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return isISO8601DurationValid(input) +} + +func validateSemanticVersion(fl validator.FieldLevel) bool { + input, ok := fl.Field().Interface().(string) if !ok { return false } - return iso8601DurationPattern.MatchString(value) + + return isSemanticVersionValid(input) +} + +// isISO8601DurationValid validates if a string is a valid ISO 8601 duration. +func isISO8601DurationValid(input string) bool { + if !iso8601DurationPattern.MatchString(input) { + return false + } + + trimmed := strings.TrimPrefix(input, "P") + if trimmed == "" || trimmed == "T" { + return false + } + + return true +} + +// isSemanticVersionValid validates if a string is a valid semantic version. +func isSemanticVersionValid(input string) bool { + return semanticVersionPattern.MatchString(input) +} + +// isHostnameValid validates if a string is a valid RFC 1123 hostname. +func isHostnameValid(input string) bool { + return hostnameRFC1123Pattern.MatchString(input) } diff --git a/model/validator_test.go b/model/validator_test.go new file mode 100644 index 0000000..bbe3009 --- /dev/null +++ b/model/validator_test.go @@ -0,0 +1,54 @@ +package model + +import ( + "testing" +) + +func TestRegexValidators(t *testing.T) { + testCases := []struct { + name string + validate func(string) bool + input string + expected bool + }{ + // ISO 8601 Duration Tests + {"ISO 8601 Duration Valid 1", isISO8601DurationValid, "P2Y", true}, + {"ISO 8601 Duration Valid 2", isISO8601DurationValid, "P1DT12H30M", true}, + {"ISO 8601 Duration Valid 3", isISO8601DurationValid, "P1Y2M3D", true}, + {"ISO 8601 Duration Valid 4", isISO8601DurationValid, "P1Y2M3D4H", false}, + {"ISO 8601 Duration Valid 5", isISO8601DurationValid, "P1Y", true}, + {"ISO 8601 Duration Valid 6", isISO8601DurationValid, "PT1H", true}, + {"ISO 8601 Duration Valid 7", isISO8601DurationValid, "P1Y2M3D4H5M6S", false}, + {"ISO 8601 Duration Invalid 1", isISO8601DurationValid, "P", false}, + {"ISO 8601 Duration Invalid 2", isISO8601DurationValid, "P1Y2M3D4H5M6S7", false}, + {"ISO 8601 Duration Invalid 3", isISO8601DurationValid, "1Y", false}, + + // Semantic Versioning Tests + {"Semantic Version Valid 1", isSemanticVersionValid, "1.0.0", true}, + {"Semantic Version Valid 2", isSemanticVersionValid, "1.2.3", true}, + {"Semantic Version Valid 3", isSemanticVersionValid, "1.2.3-beta", true}, + {"Semantic Version Valid 4", isSemanticVersionValid, "1.2.3-beta.1", true}, + {"Semantic Version Valid 5", isSemanticVersionValid, "1.2.3-beta.1+build.123", true}, + {"Semantic Version Invalid 1", isSemanticVersionValid, "v1.2.3", false}, + {"Semantic Version Invalid 2", isSemanticVersionValid, "1.2", false}, + {"Semantic Version Invalid 3", isSemanticVersionValid, "1.2.3-beta.x", true}, + + // RFC 1123 Hostname Tests + {"RFC 1123 Hostname Valid 1", isHostnameValid, "example.com", true}, + {"RFC 1123 Hostname Valid 2", isHostnameValid, "my-hostname", true}, + {"RFC 1123 Hostname Valid 3", isHostnameValid, "subdomain.example.com", true}, + {"RFC 1123 Hostname Invalid 1", isHostnameValid, "127.0.0.1", false}, + {"RFC 1123 Hostname Invalid 2", isHostnameValid, "example.com.", false}, + {"RFC 1123 Hostname Invalid 3", isHostnameValid, "example..com", false}, + {"RFC 1123 Hostname Invalid 4", isHostnameValid, "example.com-", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.validate(tc.input) + if result != tc.expected { + t.Errorf("Validation failed for '%s': input='%s', expected=%v, got=%v", tc.name, tc.input, tc.expected, result) + } + }) + } +}