Add intellisense support for dynamic HttpEndpoint output

Fixes #2526
This commit is contained in:
Sipke Schoorstra 2021-11-25 22:32:33 +01:00
parent 62aa989a9e
commit b6a7a38b21
10 changed files with 246 additions and 44 deletions

View file

@ -41,7 +41,7 @@ namespace Elsa.Activities.Http
DefaultSyntax = SyntaxNames.Json,
SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid })]
public HashSet<string> Methods { get; set; } = new HashSet<string> { "GET" };
public HashSet<string> Methods { get; set; } = new() { "GET" };
/// <summary>
/// A value indicating whether the HTTP request content body should be read and stored as part of the HTTP request model.

View file

@ -10,6 +10,7 @@ using Elsa.Activities.Http.Parsers.Request;
using Elsa.Activities.Http.Parsers.Response;
using Elsa.Activities.Http.Services;
using Elsa.Options;
using Elsa.Scripting.JavaScript.Providers;
using Elsa.Scripting.Liquid.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -58,6 +59,7 @@ namespace Microsoft.Extensions.DependencyInjection
.AddNotificationHandlers(typeof(ConfigureJavaScriptEngine))
.AddLiquidFilter<SignalUrlFilter>("signal_url")
.AddJavaScriptTypeDefinitionProvider<HttpTypeDefinitionProvider>()
.AddSingleton<IActivityTypeDefinitionRenderer, HttpEndpointTypeDefinitionRenderer>()
.AddDataProtection();
return services;

View file

@ -0,0 +1,101 @@
using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Metadata;
using Elsa.Models;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Scripting.JavaScript.Providers;
using Elsa.Services.Models;
using NJsonSchema;
using NJsonSchema.CodeGeneration.TypeScript;
namespace Elsa.Activities.Http.JavaScript
{
public class HttpEndpointTypeDefinitionRenderer : DefaultActivityTypeDefinitionRenderer
{
public class CustomTypeNameGenerator : DefaultTypeNameGenerator
{
public string Prefix { get; }
public CustomTypeNameGenerator(string prefix)
{
Prefix = prefix;
}
protected override string Generate(JsonSchema schema, string typeNameHint)
{
var result = base.Generate(schema, typeNameHint);
return result;
}
}
public override async ValueTask RenderTypeDeclarationAsync(
RenderingTypeScriptDefinitions notification,
ActivityType activityType,
ActivityDescriptor activityDescriptor,
ActivityDefinition activityDefinition,
StringBuilder writer,
CancellationToken cancellationToken = default)
{
var targetTypeSchema = activityDefinition.Properties.First(x => x.Name == nameof(HttpEndpoint.Schema)).Expressions.Values.First();
if (!string.IsNullOrWhiteSpace(targetTypeSchema))
{
var jsonSchema = await JsonSchema.FromJsonAsync(targetTypeSchema, cancellationToken);
var generator = new TypeScriptGenerator(jsonSchema, new TypeScriptGeneratorSettings
{
TypeStyle = TypeScriptTypeStyle.Interface,
TypeScriptVersion = 4,
TypeNameGenerator = new CustomTypeNameGenerator(activityDefinition.Name!)
});
var typeScriptType = $"{activityDefinition.Name}Output";
var jsonSchemaTypes = generator.GenerateFile(typeScriptType)
.Replace("\r\n", "\n")
.Replace("export interface", "declare class");
writer.AppendLine(jsonSchemaTypes);
}
await base.RenderTypeDeclarationAsync(notification, activityType, activityDescriptor, activityDefinition, writer, cancellationToken);
}
protected override async ValueTask RenderActivityPropertyAsync(
RenderingTypeScriptDefinitions notification,
StringBuilder writer,
string propertyName,
Type propertyType,
ActivityType activityType,
ActivityDescriptor activityDescriptor,
ActivityDefinition activityDefinition,
CancellationToken cancellationToken = default)
{
if (propertyName != nameof(HttpEndpoint.Output))
{
await base.RenderActivityPropertyAsync(notification, writer, propertyName, propertyType, activityType, activityDescriptor, activityDefinition, cancellationToken);
return;
}
var targetTypeName = activityDefinition.Properties.First(x => x.Name == nameof(HttpEndpoint.TargetType)).Expressions.Values.First();
var targetTypeSchema = activityDefinition.Properties.First(x => x.Name == nameof(HttpEndpoint.Schema)).Expressions.Values.First();
var typeScriptType = notification.GetTypeScriptType(propertyType);
if (!string.IsNullOrWhiteSpace(targetTypeName))
{
var type = Type.GetType(targetTypeName);
if (type != null)
typeScriptType = notification.GetTypeScriptType(type);
}
else if (!string.IsNullOrWhiteSpace(targetTypeSchema))
{
typeScriptType = $"{activityDefinition.Name}Output";
}
writer.AppendLine($"{propertyName}(): {typeScriptType}");
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Elsa.Activities.Http.Models;
using Elsa.Scripting.JavaScript.Services;
@ -9,7 +10,29 @@ namespace Elsa.Activities.Http.JavaScript
{
public override IEnumerable<Type> CollectTypes(TypeDefinitionContext context)
{
return new[] { typeof(HttpRequestModel), typeof(HttpResponseHeaders), typeof(HttpResponseModel) };
var workflowDefinition = context.WorkflowDefinition;
if (workflowDefinition != null)
{
var httpEndpointActivities = workflowDefinition.Activities.Where(x => x.Type == nameof(HttpEndpoint)).ToList();
foreach (var type in
from activityDefinition in httpEndpointActivities
select activityDefinition.Properties.First(x => x.Name == nameof(HttpEndpoint.TargetType)).Expressions.Values.First()
into targetTypeName
where !string.IsNullOrWhiteSpace(targetTypeName)
select Type.GetType(targetTypeName)
into type
where type != null
select type)
{
yield return type;
}
}
yield return typeof(HttpRequestModel);
yield return typeof(HttpResponseHeaders);
yield return typeof(HttpResponseModel);
}
}
}

View file

@ -27,12 +27,25 @@ namespace Elsa.Metadata
return null;
if (activityPropertyAttribute.OptionsProvider == null)
return activityPropertyAttribute.Options ?? (TryGetEnumOptions(activityPropertyInfo, out var items) ? items : null);
{
var options = activityPropertyAttribute.Options;
if (options is IEnumerable<string> enumerable)
return new SelectList
{
Items = enumerable.Select(x => new SelectListItem(x)).ToList()
};
if (TryGetEnumOptions(activityPropertyInfo, out var items))
return items;
return activityPropertyAttribute.Options;
}
var providerType = activityPropertyAttribute.OptionsProvider;
using var scope = _serviceProvider.CreateScope();
var provider = (IActivityPropertyOptionsProvider) ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType);
var provider = (IActivityPropertyOptionsProvider)ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, providerType);
return provider.GetOptions(activityPropertyInfo);
}

View file

@ -12,11 +12,16 @@ async function fetchRuntimeItems(serverUrl: string, options: RuntimeSelectListPr
}
export async function getSelectListItems(serverUrl: string, propertyDescriptor: ActivityPropertyDescriptor): Promise<SelectList> {
const options = propertyDescriptor.options;
const options: any = propertyDescriptor.options;
let selectList: SelectList;
if (!!options && options.runtimeSelectListProviderType)
selectList = await fetchRuntimeItems(serverUrl, options);
else if (Array.isArray(options))
selectList = {
items: options,
isFlagsEnum: false
};
else
selectList = options as SelectList;

View file

@ -28,6 +28,7 @@ namespace Microsoft.Extensions.DependencyInjection
.AddJavaScriptTypeDefinitionProvider<WorkflowContextTypeDefinitionProvider>()
.AddJavaScriptTypeDefinitionProvider<WorkflowVariablesTypeDefinitionProvider>()
.AddJavaScriptTypeDefinitionProvider<BlacklistedTypeDefinitionProvider>()
.AddSingleton<IActivityTypeDefinitionRenderer, DefaultActivityTypeDefinitionRenderer>()
.AddNotificationHandlers(typeof(JavaScriptServiceCollectionExtensions));
}

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Scripting.JavaScript.Providers;
using Elsa.Services;
using Elsa.Services.Models;
using MediatR;
@ -13,10 +15,12 @@ namespace Elsa.Scripting.JavaScript.Handlers
public class RenderJavaScriptTypeDefinitions : INotificationHandler<RenderingTypeScriptDefinitions>
{
private readonly IActivityTypeService _activityTypeService;
private readonly IEnumerable<IActivityTypeDefinitionRenderer> _activityTypeDefinitionRenderers;
public RenderJavaScriptTypeDefinitions(IActivityTypeService activityTypeService)
public RenderJavaScriptTypeDefinitions(IActivityTypeService activityTypeService, IEnumerable<IActivityTypeDefinitionRenderer> activityTypeDefinitionRenderers)
{
_activityTypeService = activityTypeService;
_activityTypeDefinitionRenderers = activityTypeDefinitionRenderers.OrderByDescending(x => x.Priority).ToList();
}
public async Task Handle(RenderingTypeScriptDefinitions notification, CancellationToken cancellationToken)
@ -72,17 +76,20 @@ namespace Elsa.Scripting.JavaScript.Handlers
var activityTypes = await Task.WhenAll(activityTypeNames.Select(async activityTypeName => (activityTypeName, await _activityTypeService.GetActivityTypeAsync(activityTypeName, cancellationToken))));
var activityTypeDictionary = activityTypes.ToDictionary(x => x.activityTypeName, x => x.Item2);
foreach (var activityType in activityTypeDictionary.Values)
await RenderActivityTypeDeclarationAsync(activityType, output);
foreach (var namedActivity in namedActivities)
{
var activityType = activityTypeDictionary[namedActivity.Type];
await RenderActivityTypeDeclarationAsync(namedActivity, activityType, output);
}
output.AppendLine("declare interface Activities {");
foreach (var activity in namedActivities)
{
var activityType = activityTypeDictionary[activity.Type];
var typeScriptType = activityType.TypeName;
var query = $"{activity.Name}: {typeScriptType}";
var interfaceActivity = FindInterface(notification, query);
//var activityType = activityTypeDictionary[activity.Type];
//var typeScriptType = activityType.TypeName;
var typeScriptType = activity.Name;
var interfaceActivity = $"{activity.Name}: {typeScriptType}";
output.AppendLine($"{interfaceActivity};");
}
@ -90,39 +97,11 @@ namespace Elsa.Scripting.JavaScript.Handlers
output.AppendLine("declare const activities: Activities");
}
async Task RenderActivityTypeDeclarationAsync(ActivityType type, StringBuilder writer)
async Task RenderActivityTypeDeclarationAsync(ActivityDefinition activityDefinition, ActivityType type, StringBuilder writer)
{
var typeName = type.TypeName;
var descriptor = await type.DescribeAsync();
var inputProperties = descriptor.InputProperties;
var outputProperties = descriptor.OutputProperties;
var query = $"declare interface {typeName}";
var interfaceDeclaration = FindInterface(notification, query);
writer.AppendLine($"{interfaceDeclaration} {{");
foreach (var property in inputProperties)
RenderActivityProperty(writer, typeName, property.Name, property.Type);
foreach (var property in outputProperties)
RenderActivityProperty(writer, typeName, property.Name, property.Type);
writer.AppendLine("}");
}
void RenderActivityProperty(StringBuilder writer, string typeName, string propertyName, Type propertyType)
{
var typeScriptType = notification.GetTypeScriptType(propertyType);
var query = $"{propertyName}(): {typeScriptType}";
var interfaceDeclaration = FindInterface(notification, query);
writer.AppendLine($"{interfaceDeclaration};");
}
string FindInterface(RenderingTypeScriptDefinitions notification, string query)
{
var result = notification.DeclaredTypes.FirstOrDefault(x => x.Contains(query));
return result ?? query;
var renderer = _activityTypeDefinitionRenderers.First(x => x.GetCanRenderType(type));
await renderer.RenderTypeDeclarationAsync(notification, type, descriptor, activityDefinition, writer, cancellationToken);
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Metadata;
using Elsa.Models;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Services.Models;
namespace Elsa.Scripting.JavaScript.Providers
{
public class DefaultActivityTypeDefinitionRenderer : IActivityTypeDefinitionRenderer
{
public int Priority => -1;
public bool GetCanRenderType(ActivityType activityType) => true;
public virtual async ValueTask RenderTypeDeclarationAsync(
RenderingTypeScriptDefinitions notification,
ActivityType activityType,
ActivityDescriptor activityDescriptor,
ActivityDefinition activityDefinition,
StringBuilder writer,
CancellationToken cancellationToken = default)
{
var typeName = activityDefinition.Name;
var inputProperties = activityDescriptor.InputProperties;
var outputProperties = activityDescriptor.OutputProperties;
var interfaceDeclaration = $"declare interface {typeName}";
writer.AppendLine($"{interfaceDeclaration} {{");
foreach (var property in inputProperties)
await RenderActivityPropertyAsync(notification, writer, property.Name, property.Type, activityType, activityDescriptor, activityDefinition, cancellationToken);
foreach (var property in outputProperties)
await RenderActivityPropertyAsync(notification, writer, property.Name, property.Type, activityType, activityDescriptor, activityDefinition, cancellationToken);
writer.AppendLine("}");
}
protected virtual ValueTask RenderActivityPropertyAsync(
RenderingTypeScriptDefinitions notification,
StringBuilder writer,
string propertyName,
Type propertyType,
ActivityType activityType,
ActivityDescriptor activityDescriptor,
ActivityDefinition activityDefinition,
CancellationToken cancellationToken = default)
{
var typeScriptType = notification.GetTypeScriptType(propertyType);
writer.AppendLine($"{propertyName}(): {typeScriptType}");
return new ValueTask();
}
}
}

View file

@ -0,0 +1,23 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Metadata;
using Elsa.Models;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Services.Models;
namespace Elsa.Scripting.JavaScript.Providers
{
public interface IActivityTypeDefinitionRenderer
{
int Priority { get; }
bool GetCanRenderType(ActivityType activityType);
ValueTask RenderTypeDeclarationAsync(
RenderingTypeScriptDefinitions notification,
ActivityType activityType,
ActivityDescriptor activityDescriptor,
ActivityDefinition activityDefinition,
StringBuilder writer,
CancellationToken cancellationToken = default);
}
}