diff --git a/src/ServerlessWorkflow.Sdk.Builders/ServerlessWorkflow.Sdk.Builders.csproj b/src/ServerlessWorkflow.Sdk.Builders/ServerlessWorkflow.Sdk.Builders.csproj index fe79026..24ea486 100644 --- a/src/ServerlessWorkflow.Sdk.Builders/ServerlessWorkflow.Sdk.Builders.csproj +++ b/src/ServerlessWorkflow.Sdk.Builders/ServerlessWorkflow.Sdk.Builders.csproj @@ -5,7 +5,7 @@ enable enable 1.0.0 - alpha4.1 + alpha5 $(VersionPrefix) $(VersionPrefix) en diff --git a/src/ServerlessWorkflow.Sdk.IO/ServerlessWorkflow.Sdk.IO.csproj b/src/ServerlessWorkflow.Sdk.IO/ServerlessWorkflow.Sdk.IO.csproj index 201d021..2e9bba6 100644 --- a/src/ServerlessWorkflow.Sdk.IO/ServerlessWorkflow.Sdk.IO.csproj +++ b/src/ServerlessWorkflow.Sdk.IO/ServerlessWorkflow.Sdk.IO.csproj @@ -5,7 +5,7 @@ enable enable 1.0.0 - alpha4.1 + alpha5 $(VersionPrefix) $(VersionPrefix) en diff --git a/src/ServerlessWorkflow.Sdk/Models/CatalogDefinition.cs b/src/ServerlessWorkflow.Sdk/Models/CatalogDefinition.cs new file mode 100644 index 0000000..b9d22b5 --- /dev/null +++ b/src/ServerlessWorkflow.Sdk/Models/CatalogDefinition.cs @@ -0,0 +1,55 @@ +// Copyright © 2024-Present 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. + +namespace ServerlessWorkflow.Sdk.Models; + +/// +/// Represents the definition of a workflow component catalog +/// +[DataContract] +public record CatalogDefinition +{ + + /// + /// Gets the name of the default catalog + /// + public const string DefaultCatalogName = "default"; + + /// + /// Gets/sets the endpoint that defines the root URL at which the catalog is located + /// + [IgnoreDataMember, JsonIgnore, YamlIgnore] + public virtual EndpointDefinition Endpoint + { + get => this.EndpointValue.T1Value ?? new() { Uri = this.EndpointUri }; + set => this.EndpointValue = value; + } + + /// + /// Gets/sets the endpoint that defines the root URL at which the catalog is located + /// + [IgnoreDataMember, JsonIgnore, YamlIgnore] + public virtual Uri EndpointUri + { + get => this.EndpointValue.T1Value?.Uri ?? this.EndpointValue.T2Value!; + set => this.EndpointValue = value; + } + + /// + /// Gets/sets the endpoint that defines the root URL at which the catalog is located + /// + [Required] + [DataMember(Name = "endpoint", Order = 1), JsonInclude, JsonPropertyName("endpoint"), JsonPropertyOrder(1), YamlMember(Alias = "endpoint", Order = 1)] + protected virtual OneOf EndpointValue { get; set; } = null!; + +} diff --git a/src/ServerlessWorkflow.Sdk/Models/ComponentDefinitionCollection.cs b/src/ServerlessWorkflow.Sdk/Models/ComponentDefinitionCollection.cs index e6f7053..a170be0 100644 --- a/src/ServerlessWorkflow.Sdk/Models/ComponentDefinitionCollection.cs +++ b/src/ServerlessWorkflow.Sdk/Models/ComponentDefinitionCollection.cs @@ -26,40 +26,46 @@ public record ComponentDefinitionCollection [DataMember(Name = "authentications", Order = 1), JsonPropertyName("authentications"), JsonPropertyOrder(1), YamlMember(Alias = "authentications", Order = 1)] public virtual EquatableDictionary? Authentications { get; set; } + /// + /// Gets/sets a name/value mapping of the catalogs, if any, from which to import reusable components used within the workflow + /// + [DataMember(Name = "catalogs", Order = 2), JsonPropertyName("catalogs"), JsonPropertyOrder(2), YamlMember(Alias = "catalogs", Order = 2)] + public virtual EquatableDictionary? Catalogs { get; set; } + /// /// Gets/sets a name/value mapping of the workflow's errors, if any /// - [DataMember(Name = "errors", Order = 2), JsonPropertyName("errors"), JsonPropertyOrder(2), YamlMember(Alias = "errors", Order = 2)] + [DataMember(Name = "errors", Order = 3), JsonPropertyName("errors"), JsonPropertyOrder(3), YamlMember(Alias = "errors", Order = 3)] public virtual EquatableDictionary? Errors { get; set; } /// /// Gets/sets a name/value mapping of the workflow's extensions, if any /// - [DataMember(Name = "extensions", Order = 3), JsonPropertyName("extensions"), JsonPropertyOrder(3), YamlMember(Alias = "extensions", Order = 3)] + [DataMember(Name = "extensions", Order = 4), JsonPropertyName("extensions"), JsonPropertyOrder(4), YamlMember(Alias = "extensions", Order = 4)] public virtual EquatableDictionary? Extensions { get; set; } /// /// Gets/sets a name/value mapping of the workflow's reusable functions /// - [DataMember(Name = "functions", Order = 4), JsonPropertyName("functions"), JsonPropertyOrder(4), YamlMember(Alias = "functions", Order = 4)] + [DataMember(Name = "functions", Order = 5), JsonPropertyName("functions"), JsonPropertyOrder(5), YamlMember(Alias = "functions", Order = 5)] public virtual EquatableDictionary? Functions { get; set; } /// /// Gets/sets a name/value mapping of the workflow's reusable retry policies /// - [DataMember(Name = "retries", Order = 5), JsonPropertyName("retries"), JsonPropertyOrder(5), YamlMember(Alias = "retries", Order = 5)] + [DataMember(Name = "retries", Order = 6), JsonPropertyName("retries"), JsonPropertyOrder(6), YamlMember(Alias = "retries", Order = 6)] public virtual EquatableDictionary? Retries { get; set; } /// /// Gets/sets a list containing the workflow's secrets /// - [DataMember(Name = "secrets", Order = 6), JsonPropertyName("secrets"), JsonPropertyOrder(6), YamlMember(Alias = "secrets", Order = 6)] + [DataMember(Name = "secrets", Order = 7), JsonPropertyName("secrets"), JsonPropertyOrder(7), YamlMember(Alias = "secrets", Order = 7)] public virtual EquatableList? Secrets { get; set; } /// /// Gets/sets a name/value mapping of the workflow's reusable timeouts /// - [DataMember(Name = "timeouts", Order = 7), JsonPropertyName("timeouts"), JsonPropertyOrder(7), YamlMember(Alias = "timeouts", Order = 7)] + [DataMember(Name = "timeouts", Order = 7), JsonPropertyName("timeouts"), JsonPropertyOrder(8), YamlMember(Alias = "timeouts", Order = 8)] public virtual EquatableDictionary? Timeouts { get; set; } } diff --git a/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.Designer.cs b/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.Designer.cs index faf0b98..814a7bb 100644 --- a/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.Designer.cs +++ b/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.Designer.cs @@ -60,6 +60,15 @@ internal ValidationErrors() { } } + /// + /// Looks up a localized string similar to Invalid cataloged function call format. Expected format '{functionName}:{functionSemanticVersion}@{catalogName}'. + /// + internal static string InvalidCatalogedFunctionCallFormat { + get { + return ResourceManager.GetString("InvalidCatalogedFunctionCallFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Undefined authentication policy. /// @@ -69,6 +78,15 @@ internal static string UndefinedAuthenticationPolicy { } } + /// + /// Looks up a localized string similar to Undefined catalog. + /// + internal static string UndefinedCatalog { + get { + return ResourceManager.GetString("UndefinedCatalog", resourceCulture); + } + } + /// /// Looks up a localized string similar to Undefined function. /// diff --git a/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.resx b/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.resx index 5946975..d1b7d5a 100644 --- a/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.resx +++ b/src/ServerlessWorkflow.Sdk/Properties/ValidationErrors.resx @@ -132,4 +132,10 @@ Undefined timeout + + Undefined catalog + + + Invalid cataloged function call format. Expected format '{functionName}:{functionSemanticVersion}@{catalogName}' + \ No newline at end of file diff --git a/src/ServerlessWorkflow.Sdk/ServerlessWorkflow.Sdk.csproj b/src/ServerlessWorkflow.Sdk/ServerlessWorkflow.Sdk.csproj index 5d8ecc0..8697ada 100644 --- a/src/ServerlessWorkflow.Sdk/ServerlessWorkflow.Sdk.csproj +++ b/src/ServerlessWorkflow.Sdk/ServerlessWorkflow.Sdk.csproj @@ -5,7 +5,7 @@ enable enable 1.0.0 - alpha4.1 + alpha5 $(VersionPrefix) $(VersionPrefix) en diff --git a/src/ServerlessWorkflow.Sdk/Validation/CallTaskDefinitionValidator.cs b/src/ServerlessWorkflow.Sdk/Validation/CallTaskDefinitionValidator.cs index 2f21bea..3304469 100644 --- a/src/ServerlessWorkflow.Sdk/Validation/CallTaskDefinitionValidator.cs +++ b/src/ServerlessWorkflow.Sdk/Validation/CallTaskDefinitionValidator.cs @@ -14,6 +14,7 @@ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Neuroglia.Serialization; +using Semver; using ServerlessWorkflow.Sdk.Models; using ServerlessWorkflow.Sdk.Models.Calls; using ServerlessWorkflow.Sdk.Models.Tasks; @@ -37,6 +38,14 @@ public CallTaskDefinitionValidator(IServiceProvider serviceProvider, ComponentDe .Must(ReferenceAnExistingFunction) .When(c => !Uri.TryCreate(c.Call, UriKind.Absolute, out _) && !c.Call.Contains('@')) .WithMessage(ValidationErrors.UndefinedFunction); + this.RuleFor(c => c.Call) + .Must(BeWellFormedCatalogedFunctionCall) + .When(c => c.Call.Contains('@') && (!Uri.TryCreate(c.Call, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))) + .WithMessage(ValidationErrors.InvalidCatalogedFunctionCallFormat); + this.RuleFor(c => c.Call) + .Must(ReferenceAnExistingCatalog) + .When(c => c.Call.Contains('@') && (!Uri.TryCreate(c.Call, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))) + .WithMessage(ValidationErrors.UndefinedCatalog); this.When(c => c.Call == Function.AsyncApi, () => { this.RuleFor(c => (AsyncApiCallDefinition)this.JsonSerializer.Convert(c.With, typeof(AsyncApiCallDefinition))!) @@ -81,9 +90,43 @@ public CallTaskDefinitionValidator(IServiceProvider serviceProvider, ComponentDe /// A boolean indicating whether or not the specified function exists protected virtual bool ReferenceAnExistingFunction(string name) { + if (string.IsNullOrWhiteSpace(name)) return false; if (Function.AsEnumerable().Contains(name)) return true; else if (this.Components?.Functions?.ContainsKey(name) == true) return true; else return false; } + /// + /// Determines whether or not the format of the call is a valid cataloged function call + /// + /// The name of the function to check + /// A boolean indicatingwhether or not the format of the call is a valid cataloged function call + protected virtual bool BeWellFormedCatalogedFunctionCall(string name) + { + if (string.IsNullOrWhiteSpace(name)) return false; + var components = name.Split('@', StringSplitOptions.RemoveEmptyEntries); + if (components.Length != 2) return false; + var qualifiedName = components[0]; + components = qualifiedName.Split(':'); + if (components.Length != 2) return false; + var version = components[1]; + if (!SemVersion.TryParse(version, SemVersionStyles.Strict, out var semver)) return false; + return true; + } + + /// + /// Determines whether or not the catalog from which the specified function is imported exists + /// + /// The name of the function to check + /// A boolean indicating whether or not the catalog from which the specified function is imported exists + protected virtual bool ReferenceAnExistingCatalog(string name) + { + if (string.IsNullOrWhiteSpace(name)) return false; + var components = name.Split('@', StringSplitOptions.RemoveEmptyEntries); + var catalogName = components[1]; + if (catalogName == CatalogDefinition.DefaultCatalogName) return true; + else if(this.Components?.Catalogs?.ContainsKey(catalogName) == true) return true; + else return false; + } + } diff --git a/src/ServerlessWorkflow.Sdk/Validation/CatalogDefinitionValidator.cs b/src/ServerlessWorkflow.Sdk/Validation/CatalogDefinitionValidator.cs new file mode 100644 index 0000000..e37eca0 --- /dev/null +++ b/src/ServerlessWorkflow.Sdk/Validation/CatalogDefinitionValidator.cs @@ -0,0 +1,43 @@ +// Copyright © 2024-Present 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. + +using FluentValidation; +using ServerlessWorkflow.Sdk.Models; + +namespace ServerlessWorkflow.Sdk.Validation; + +/// +/// Represents the used to validate s +/// +public class CatalogDefinitionValidator + : AbstractValidator +{ + + /// + public CatalogDefinitionValidator(IServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + this.RuleFor(c => c.Endpoint) + .NotNull() + .When(c => c.EndpointUri == null); + this.RuleFor(c => c.EndpointUri) + .NotNull() + .When(c => c.Endpoint == null); + } + + /// + /// Gets the current + /// + protected IServiceProvider ServiceProvider { get; } + +} \ No newline at end of file diff --git a/src/ServerlessWorkflow.Sdk/Validation/CatalogKeyValuePairValidator.cs b/src/ServerlessWorkflow.Sdk/Validation/CatalogKeyValuePairValidator.cs new file mode 100644 index 0000000..99a726f --- /dev/null +++ b/src/ServerlessWorkflow.Sdk/Validation/CatalogKeyValuePairValidator.cs @@ -0,0 +1,45 @@ +// Copyright © 2024-Present 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. + +using FluentValidation; +using ServerlessWorkflow.Sdk.Models; + +namespace ServerlessWorkflow.Sdk.Validation; + +/// +/// Represents the used to validate key/value pairs +/// +public class CatalogKeyValuePairValidator + : AbstractValidator> +{ + + /// + public CatalogKeyValuePairValidator(IServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + this.RuleFor(t => t.Value) + .Custom((value, context) => + { + var key = context.InstanceToValidate.Key; + var validator = new CatalogDefinitionValidator(serviceProvider); + var validationResult = validator.Validate(value); + foreach (var error in validationResult.Errors) context.AddFailure($"{key}.{error.PropertyName}", error.ErrorMessage); + }); + } + + /// + /// Gets the current + /// + protected IServiceProvider ServiceProvider { get; } + +} diff --git a/src/ServerlessWorkflow.Sdk/Validation/ComponentDefinitionCollectionValidator.cs b/src/ServerlessWorkflow.Sdk/Validation/ComponentDefinitionCollectionValidator.cs index 1e2045d..2ff5d49 100644 --- a/src/ServerlessWorkflow.Sdk/Validation/ComponentDefinitionCollectionValidator.cs +++ b/src/ServerlessWorkflow.Sdk/Validation/ComponentDefinitionCollectionValidator.cs @@ -13,6 +13,7 @@ using FluentValidation; using ServerlessWorkflow.Sdk.Models; +using ServerlessWorkflow.Sdk.Properties; namespace ServerlessWorkflow.Sdk.Validation; @@ -29,6 +30,8 @@ public ComponentDefinitionCollectionValidator(IServiceProvider serviceProvider) this.ServiceProvider = serviceProvider; this.RuleForEach(c => c.Authentications) .SetValidator(c => new AuthenticationPolicyKeyValuePairValidator(this.ServiceProvider, c)); + this.RuleForEach(c => c.Catalogs) + .SetValidator(c => new CatalogKeyValuePairValidator(this.ServiceProvider)); this.RuleForEach(c => c.Functions) .SetValidator(c => new TaskKeyValuePairValidator(this.ServiceProvider, c, c.Functions)); } @@ -38,4 +41,4 @@ public ComponentDefinitionCollectionValidator(IServiceProvider serviceProvider) /// protected IServiceProvider ServiceProvider { get; } -} +} \ No newline at end of file