using System.Text.Json;
namespace w4c_workflows.Services.Execution;
///
/// Wire contract for a task.run job pulled off the tenant's jobs stream.
/// The control plane (step 9) enqueues these; the worker reads, executes and
/// acks them. Field names are flat Redis Stream entries so the contract is
/// transport-agnostic (Redis Streams now, RabbitMQ later).
///
public sealed record TaskInvocation(
string Type,
string RunId,
string TaskId,
string TaskKey,
string Language,
string EntryFile,
string? EntryFunction,
IReadOnlyDictionary Env,
string? Input,
string? WorkingDir,
string TenantId,
int Attempt,
string? Server = null)
{
public const string TypeValue = "task.run";
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
/// Parses a stream message into an invocation, throwing if a required field is missing.
public static TaskInvocation FromFields(IReadOnlyDictionary fields)
{
static string Get(IReadOnlyDictionary fields, string key) =>
fields.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value)
? value
: throw new InvalidOperationException($"missing required job field '{key}'");
static int GetInt(IReadOnlyDictionary fields, string key, int fallback) =>
fields.TryGetValue(key, out var raw) && int.TryParse(raw, out var n) ? n : fallback;
var env = fields.TryGetValue("env", out var envJson) && !string.IsNullOrWhiteSpace(envJson)
? JsonSerializer.Deserialize>(envJson, Json) ?? new()
: new Dictionary();
return new TaskInvocation(
Get(fields, "type"),
Get(fields, "run_id"),
Get(fields, "task_id"),
Get(fields, "task_key"),
Get(fields, "language"),
Get(fields, "entry_file"),
fields.GetValueOrDefault("entry_function"),
env,
fields.GetValueOrDefault("input"),
fields.GetValueOrDefault("working_dir"),
Get(fields, "tenant_id"),
GetInt(fields, "attempt", 1),
fields.GetValueOrDefault("server"));
}
}