using System.Text.Json;
using System.Text.Json.Serialization;
using w4c_workflows.Models;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace w4c_workflows.Services;
/// A successfully compiled workflow + its task tree (EF entities, detached).
public sealed record CompiledWorkflow(Workflow Workflow, IReadOnlyList Tasks);
public sealed class CompileResult
{
public List Errors { get; } = new();
public bool Success => Errors.Count == 0;
public CompiledWorkflow? Workflow { get; set; }
}
///
/// 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.
///
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, string repoName = "workflows")
{
var result = new CompileResult();
WorkflowDefinition def;
try
{
def = _yaml.Deserialize(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, repoName);
return result;
}
private CompiledWorkflow Build(WorkflowDefinition def, string path, string tenantId, string repoName)
{
var graph = WorkflowGraph.Compute(def);
var now = DateTime.UtcNow;
var workflowId = DeterministicGuid.For($"wf::{tenantId}::{repoName}::{path}");
var workflow = new Workflow
{
Id = workflowId,
TenantId = tenantId,
Name = def.Name!,
Path = path,
Repo = repoName,
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();
// Root task = the workflow entry (language/mode/entry/env from the header).
var rootId = DeterministicGuid.For($"task::{tenantId}::{repoName}::{path}::root");
var headId = graph.HeadId == null
? (Guid?)null
: DeterministicGuid.For($"task::{tenantId}::{repoName}::{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}::{repoName}::{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}::{repoName}::{path}::{task.Parent}"),
NextId = string.IsNullOrEmpty(task.Next)
? null
: DeterministicGuid.For($"task::{tenantId}::{repoName}::{path}::{task.Next}"),
OnErrorId = string.IsNullOrEmpty(task.OnError)
? null
: DeterministicGuid.For($"task::{tenantId}::{repoName}::{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? env)
=> env == null || env.Count == 0 ? null : JsonSerializer.Serialize(env, Json);
}