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); }