157 lines
5.5 KiB
C#
157 lines
5.5 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using w4c_workflows.Models;
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
namespace w4c_workflows.Services;
|
|
|
|
/// <summary>A successfully compiled workflow + its task tree (EF entities, detached).</summary>
|
|
public sealed record CompiledWorkflow(Workflow Workflow, IReadOnlyList<WorkflowTask> Tasks);
|
|
|
|
public sealed class CompileResult
|
|
{
|
|
public List<string> Errors { get; } = new();
|
|
public bool Success => Errors.Count == 0;
|
|
public CompiledWorkflow? Workflow { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses and compiles a workflow YAML definition into the EF entity model.
|
|
/// Ids are deterministic (tenant + path + task id) so re-syncing the same file
|
|
/// upserts instead of duplicating. The compiler performs no DB writes — the
|
|
/// sync service persists the result.
|
|
/// </summary>
|
|
public class WorkflowCompiler
|
|
{
|
|
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
};
|
|
|
|
private readonly WorkflowValidator _validator;
|
|
private readonly IDeserializer _yaml;
|
|
|
|
public WorkflowCompiler(WorkflowValidator validator)
|
|
{
|
|
_validator = validator;
|
|
_yaml = new DeserializerBuilder()
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.IgnoreUnmatchedProperties()
|
|
.Build();
|
|
}
|
|
|
|
public CompileResult Compile(string yaml, string path, string tenantId)
|
|
{
|
|
var result = new CompileResult();
|
|
|
|
WorkflowDefinition def;
|
|
try
|
|
{
|
|
def = _yaml.Deserialize<WorkflowDefinition>(yaml) ?? new WorkflowDefinition();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Errors.Add($"YAML parse error: {ex.Message}");
|
|
return result;
|
|
}
|
|
|
|
result.Errors.AddRange(_validator.Validate(def));
|
|
if (!result.Success)
|
|
return result;
|
|
|
|
result.Workflow = Build(def, path, tenantId);
|
|
return result;
|
|
}
|
|
|
|
private CompiledWorkflow Build(WorkflowDefinition def, string path, string tenantId)
|
|
{
|
|
var graph = WorkflowGraph.Compute(def);
|
|
var now = DateTime.UtcNow;
|
|
|
|
var workflowId = DeterministicGuid.For($"wf::{tenantId}::{path}");
|
|
var workflow = new Workflow
|
|
{
|
|
Id = workflowId,
|
|
TenantId = tenantId,
|
|
Name = def.Name!,
|
|
Path = path,
|
|
Status = WorkflowStatus.Compiled,
|
|
Mode = def.Mode!,
|
|
Version = string.IsNullOrWhiteSpace(def.Version) ? "1.0.0" : def.Version!,
|
|
Target = string.IsNullOrWhiteSpace(def.Target) ? "default" : def.Target,
|
|
TriggerJson = def.Trigger == null ? null : JsonSerializer.Serialize(def.Trigger, Json),
|
|
CompiledAt = now,
|
|
CreatedAt = now,
|
|
UpdatedAt = now,
|
|
};
|
|
|
|
var tasks = new List<WorkflowTask>();
|
|
|
|
// Root task = the workflow entry (language/mode/entry/env from the header).
|
|
var rootId = DeterministicGuid.For($"task::{tenantId}::{path}::root");
|
|
var headId = graph.HeadId == null
|
|
? (Guid?)null
|
|
: DeterministicGuid.For($"task::{tenantId}::{path}::{graph.HeadId}");
|
|
tasks.Add(new WorkflowTask
|
|
{
|
|
Id = rootId,
|
|
WorkflowId = workflowId,
|
|
Key = "root",
|
|
ParentId = null,
|
|
NextId = headId,
|
|
OnErrorId = null,
|
|
Language = def.Language!,
|
|
Mode = def.Mode!,
|
|
EntryJson = JsonSerializer.Serialize(new EntryDefinition
|
|
{
|
|
File = def.Entry!.File,
|
|
Function = def.Entry.Function,
|
|
}, Json),
|
|
EnvJson = SerializeEnv(def.Env),
|
|
Server = NormalizeServer(def.Server),
|
|
Order = 0,
|
|
});
|
|
|
|
for (var i = 0; i < graph.Tasks.Count; i++)
|
|
{
|
|
var task = graph.Tasks[i];
|
|
var taskId = DeterministicGuid.For($"task::{tenantId}::{path}::{task.Id}");
|
|
|
|
tasks.Add(new WorkflowTask
|
|
{
|
|
Id = taskId,
|
|
WorkflowId = workflowId,
|
|
Key = task.Id!,
|
|
ParentId = string.IsNullOrEmpty(task.Parent) || task.Parent == "root"
|
|
? rootId
|
|
: DeterministicGuid.For($"task::{tenantId}::{path}::{task.Parent}"),
|
|
NextId = string.IsNullOrEmpty(task.Next)
|
|
? null
|
|
: DeterministicGuid.For($"task::{tenantId}::{path}::{task.Next}"),
|
|
OnErrorId = string.IsNullOrEmpty(task.OnError)
|
|
? null
|
|
: DeterministicGuid.For($"task::{tenantId}::{path}::{task.OnError}"),
|
|
Language = task.Language ?? def.Language!,
|
|
Mode = task.Mode ?? def.Mode!,
|
|
EntryJson = JsonSerializer.Serialize(new EntryDefinition
|
|
{
|
|
File = task.Entry!.File,
|
|
Function = task.Entry.Function,
|
|
}, Json),
|
|
EnvJson = SerializeEnv(task.Env),
|
|
Server = NormalizeServer(task.Server ?? def.Server),
|
|
Order = i + 1,
|
|
});
|
|
}
|
|
|
|
return new CompiledWorkflow(workflow, tasks);
|
|
}
|
|
|
|
private static string? NormalizeServer(string? value)
|
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
|
|
private static string? SerializeEnv(Dictionary<string, string>? env)
|
|
=> env == null || env.Count == 0 ? null : JsonSerializer.Serialize(env, Json);
|
|
}
|