Skip to content

Commit

Permalink
Merge pull request #467 from serverlessworkflow/feat-form-based-input
Browse files Browse the repository at this point in the history
Updates the modal to create a new workflow instance by adding a form-based editor for the instance's input
  • Loading branch information
cdavernas authored Dec 11, 2024
2 parents 896b8d9 + 9f9c8d4 commit ef95e92
Show file tree
Hide file tree
Showing 71 changed files with 1,444 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down Expand Up @@ -43,7 +43,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha5.1" />
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha5.2" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
</ItemGroup>

Expand Down
6 changes: 3 additions & 3 deletions src/api/Synapse.Api.Http/Synapse.Api.Http.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<OutputType>Library</OutputType>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down Expand Up @@ -43,8 +43,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.16.2" />
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.16.2" />
<PackageReference Include="Neuroglia.Mediation.AspNetCore" Version="4.18.0" />
<PackageReference Include="Neuroglia.Security.AspNetCore" Version="4.18.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/api/Synapse.Api.Server/Synapse.Api.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down
4 changes: 2 additions & 2 deletions src/cli/Synapse.Cli/Synapse.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand All @@ -33,7 +33,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="moment.net" Version="1.3.4" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha5.1" />
<PackageReference Include="ServerlessWorkflow.Sdk.IO" Version="1.0.0-alpha5.2" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta4.22272.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<NeutralLanguage>en</NeutralLanguage>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha5.3</VersionSuffix>
<VersionSuffix>alpha5.7</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<Authors>The Synapse Authors</Authors>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Neuroglia.Data.Infrastructure.ResourceOriented;
using ServerlessWorkflow.Sdk;
using ServerlessWorkflow.Sdk.Models.Authentication;
using Synapse.Core.Infrastructure.Services;
using System.Text;

namespace Synapse;
Expand Down Expand Up @@ -41,28 +45,28 @@ public class AuthorizationInfo(string scheme, string parameter)
/// <summary>
/// Creates a new <see cref="AuthorizationInfo"/> based on the specified <see cref="AuthenticationPolicyDefinition"/>
/// </summary>
/// <param name="workflow">The <see cref="WorkflowDefinition"/> that defines the <see cref="AuthenticationPolicyDefinition"/> to create a new <see cref="AuthorizationInfo"/> for</param>
/// <param name="authentication">The <see cref="AuthenticationPolicyDefinition"/> to create a new <see cref="AuthorizationInfo"/> for</param>
/// <param name="serviceProvider">The current <see cref="IServiceProvider"/></param>
/// <param name="workflow">The <see cref="WorkflowDefinition"/>, if any, that defines the <see cref="AuthenticationPolicyDefinition"/> to create a new <see cref="AuthorizationInfo"/> for</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
/// <returns>A new <see cref="AuthorizationInfo"/> based on the specified <see cref="AuthenticationPolicyDefinition"/></returns>
public static async Task<AuthorizationInfo> CreateAsync(WorkflowDefinition workflow, AuthenticationPolicyDefinition authentication, IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
public static async Task<AuthorizationInfo> CreateAsync( AuthenticationPolicyDefinition authentication, IServiceProvider serviceProvider, WorkflowDefinition? workflow = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(authentication);
ArgumentNullException.ThrowIfNull(serviceProvider);
string scheme, parameter;
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("AuthenticationPolicyHandler");
if (!string.IsNullOrWhiteSpace(authentication.Use))
{
if (workflow.Use?.Authentications?.TryGetValue(authentication.Use, out AuthenticationPolicyDefinition? referencedAuthentication) != true || referencedAuthentication == null) throw new NullReferenceException($"Failed to find the specified authentication policy '{authentication.Use}'");
if (workflow?.Use?.Authentications?.TryGetValue(authentication.Use, out AuthenticationPolicyDefinition? referencedAuthentication) != true || referencedAuthentication == null) throw new NullReferenceException($"Failed to find the specified authentication policy '{authentication.Use}'");
else authentication = referencedAuthentication;
}
var isSecretBased = authentication.TryGetBaseSecret(out var secretName);
object? authenticationProperties = null;
if (isSecretBased && !string.IsNullOrWhiteSpace(secretName))
{
logger.LogDebug("Authentication is secret based");
var secretsManager = serviceProvider.GetRequiredService<ISecretsManager>();
var secretsManager = serviceProvider.GetService<ISecretsManager>() ?? throw new NotSupportedException("Secret based authentication is not supported in this context");
var secrets = await secretsManager.GetSecretsAsync(cancellationToken).ConfigureAwait(false);
if (!secrets.TryGetValue(secretName, out authenticationProperties) || authenticationProperties == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ public static class HttpClientExtensions
/// Configures the <see cref="HttpClient"/> to use the specified authentication mechanism
/// </summary>
/// <param name="httpClient">The <see cref="HttpClient"/> to configure</param>
/// <param name="workflow">The <see cref="WorkflowDefinition"/> that defines the authentication to configure</param>
/// <param name="authentication">An object that describes the authentication mechanism to use</param>
/// <param name="serviceProvider">The current <see cref="IServiceProvider"/></param>
/// <param name="workflow">The <see cref="WorkflowDefinition"/>, if any, that defines the authentication to configure</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
/// <returns>A new awaitable <see cref="Task"/></returns>
public static async Task ConfigureAuthenticationAsync(this HttpClient httpClient, WorkflowDefinition workflow, AuthenticationPolicyDefinition? authentication, IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
public static async Task ConfigureAuthenticationAsync(this HttpClient httpClient, AuthenticationPolicyDefinition? authentication, IServiceProvider serviceProvider, WorkflowDefinition? workflow = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(workflow);
ArgumentNullException.ThrowIfNull(serviceProvider);
if (authentication == null) return;
var authorization = await AuthorizationInfo.CreateAsync(workflow, authentication, serviceProvider, cancellationToken).ConfigureAwait(false);
var authorization = await AuthorizationInfo.CreateAsync(authentication, serviceProvider, workflow, cancellationToken).ConfigureAwait(false);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(authorization.Scheme, authorization.Parameter);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Neuroglia.Plugins;
using Neuroglia.Security.Services;
using Neuroglia.Serialization;
using Neuroglia.Serialization.Xml;
using ServerlessWorkflow.Sdk.IO;
using Synapse.Core.Infrastructure.Services;
using Synapse.Resources;
Expand All @@ -45,6 +46,7 @@ public static IServiceCollection AddSynapse(this IServiceCollection services, IC
{
services.AddHttpClient();
services.AddSerialization();
services.AddSingleton<IXmlSerializer, XmlSerializer>();
services.AddMediator();
services.AddScoped<IUserInfoProvider, UserInfoProvider>();
services.AddServerlessWorkflowIO();
Expand All @@ -61,8 +63,12 @@ public static IServiceCollection AddSynapse(this IServiceCollection services, IC
services.AddSingleton<ITextDocumentRepository<string>, RedisTextDocumentRepository<string>>();
services.AddSingleton<ITextDocumentRepository>(provider => provider.GetRequiredService<ITextDocumentRepository<string>>());

services.AddSingleton<IExternalResourceProvider, ExternalResourceProvider>();
services.AddSingleton<IOAuth2TokenManager, OAuth2TokenManager>();
services.AddSingleton<ISchemaHandlerProvider, SchemaHandlerProvider>();
services.AddSingleton<ISchemaHandler, AvroSchemaHandler>();
services.AddSingleton<ISchemaHandler, JsonSchemaHandler>();
services.AddSingleton<ISchemaHandler, XmlSchemaHandler>();

services.AddScoped<IResourceRepository, ResourceRepository>();
services.AddScoped<IAdmissionControl, AdmissionControl>();
Expand Down
177 changes: 177 additions & 0 deletions src/core/Synapse.Core.Infrastructure/Services/AvroSchemaHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright © 2024-Present The Synapse 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 Neuroglia;
using Neuroglia.Serialization;
using ServerlessWorkflow.Sdk;
using System.Net;
using Avro.IO;
using Avro;
using Avro.Generic;

namespace Synapse.Core.Infrastructure.Services;

/// <summary>
/// Represents the <see cref="ISchemaHandler"/> implementation used to handle Avro schemas
/// </summary>
/// <param name="externalResourceProvider">The service used to provide external resources</param>
/// <param name="jsonSerializer">The service used to serialize/deserialize data to/from JSON</param>
public class AvroSchemaHandler(IExternalResourceProvider externalResourceProvider, IJsonSerializer jsonSerializer)
: ISchemaHandler
{

/// <summary>
/// Gets the service used to provide external resources
/// </summary>
protected IExternalResourceProvider ExternalResourceProvider { get; } = externalResourceProvider;

/// <summary>
/// Gets the service used to serialize/deserialize data to/from JSON
/// </summary>
protected IJsonSerializer JsonSerializer { get; } = jsonSerializer;

/// <inheritdoc/>
public virtual bool Supports(string format) => format.Equals(SchemaFormat.Avro, StringComparison.OrdinalIgnoreCase);

/// <inheritdoc/>
public virtual async Task<IOperationResult> ValidateAsync(object graph, SchemaDefinition schema, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(schema);
if (!this.Supports(schema.Format)) throw new NotSupportedException($"The specified schema format '{schema.Format}' is not supported in this context");
Schema avroSchema;
try
{
var json = string.Empty;
if (schema.Resource == null)
{
json = this.JsonSerializer.SerializeToText(schema.Document);
}
else
{
using var stream = await this.ExternalResourceProvider.ReadAsync(schema.Resource, cancellationToken: cancellationToken).ConfigureAwait(false);
using var streamReader = new StreamReader(stream);
json = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
}
avroSchema = Schema.Parse(json);
}
catch (Exception ex)
{
return new OperationResult((int)HttpStatusCode.BadRequest, null, new Neuroglia.Error()
{
Type = ErrorType.Validation,
Title = ErrorTitle.Validation,
Status = ErrorStatus.Validation,
Detail = $"An error occurred while parsing the specified schema: {ex}"
});
}
byte[] avroData;
try
{
using var memoryStream = new MemoryStream();
var writer = new BinaryEncoder(memoryStream);
var datumWriter = new GenericDatumWriter<object>(avroSchema);
var avroObject = ConvertToAvroCompatible(graph, avroSchema);
datumWriter.Write(avroObject, writer);
avroData = memoryStream.ToArray();
}
catch (Exception ex)
{
return new OperationResult((int)HttpStatusCode.BadRequest, null, new Neuroglia.Error()
{
Type = ErrorType.Validation,
Title = ErrorTitle.Validation,
Status = ErrorStatus.Validation,
Detail = $"An error occurred while serializing the specified data to Avro: {ex}"
});
}
try
{
using var memoryStream = new MemoryStream(avroData);
var reader = new BinaryDecoder(memoryStream);
var datumReader = new GenericDatumReader<GenericRecord>(avroSchema, avroSchema);
datumReader.Read(null!, reader);
return await Task.FromResult(new OperationResult((int)HttpStatusCode.OK));
}
catch (Exception ex)
{
return new OperationResult((int)HttpStatusCode.BadRequest, null, new Neuroglia.Error()
{
Type = ErrorType.Validation,
Title = ErrorTitle.Validation,
Status = ErrorStatus.Validation,
Detail = ex.Message
});
}
}

object ConvertToAvroCompatible(object graph, Schema schema)
{
return schema switch
{
RecordSchema recordSchema => ConvertToGenericRecord(graph, recordSchema),
ArraySchema arraySchema => ConvertToArray(graph, arraySchema),
MapSchema mapSchema => ConvertToMap(graph, mapSchema),
UnionSchema unionSchema => ConvertToUnion(graph, unionSchema),
FixedSchema fixedSchema => graph,
EnumSchema enumSchema => graph,
PrimitiveSchema primitiveSchema => graph,
_ => throw new NotSupportedException($"Unsupported schema type: {schema.GetType().Name}")
};
}

GenericRecord ConvertToGenericRecord(object graph, RecordSchema recordSchema)
{
var record = new GenericRecord(recordSchema);
var properties = graph.GetType().GetProperties();
foreach (var field in recordSchema.Fields)
{
var property = properties.FirstOrDefault(p => p.Name.Equals(field.Name, StringComparison.OrdinalIgnoreCase));
if (property != null)
{
var value = property.GetValue(graph)!;
record.Add(field.Name, ConvertToAvroCompatible(value, field.Schema));
}
}
return record;
}

object ConvertToArray(object graph, ArraySchema arraySchema)
{
if (graph is not IEnumerable<object> enumerable) throw new InvalidOperationException("Provided object is not an array or enumerable");
return enumerable.Select(item => ConvertToAvroCompatible(item, arraySchema.ItemSchema)).ToList();
}

object ConvertToMap(object graph, MapSchema mapSchema)
{
if (graph is not IDictionary<string, object> dictionary) throw new InvalidOperationException("Provided object is not a map or dictionary");
return dictionary.ToDictionary(kvp => kvp.Key, kvp => ConvertToAvroCompatible(kvp.Value, mapSchema.ValueSchema));
}

object ConvertToUnion(object graph, UnionSchema unionSchema)
{
foreach (var schema in unionSchema.Schemas)
{
try
{
return ConvertToAvroCompatible(graph, schema);
}
catch
{
// Continue to next schema in the union
}
}
throw new InvalidOperationException("Provided object does not match any schema in the union.");
}

}
Loading

0 comments on commit ef95e92

Please sign in to comment.