diff --git a/specs/port/trios-railway/crates/trios-railway-core/src/mutations.t27 b/specs/port/trios-railway/crates/trios-railway-core/src/mutations.t27 new file mode 100644 index 0000000000..c2b459af06 --- /dev/null +++ b/specs/port/trios-railway/crates/trios-railway-core/src/mutations.t27 @@ -0,0 +1,255 @@ +// RW-03: typed mutations against the Railway GraphQL API. +// Port of gHashTag/trios-railway:crates/trios-railway-core/src/mutations.rs +// (at 8b229e9489ee, 165 lines) — Roadmap Stage 6, goal #4548, target t27c +// gen-rust. +// +// The original carries five mutations — serviceCreate (image-based, +// project-scoped; the image itself lands via a follow-up +// serviceInstanceUpdate, because Railway splits service from +// service-instance config), variableUpsert (one variable at a time, +// scoped to project + environment + service), serviceInstanceRedeploy +// (a redeploy on the most recent source, answering the new deployment +// id), serviceInstanceUpdate (pinning source.image so the next deploy +// pulls the new image), and serviceDelete (permanent, deployments +// included). Every mutation log its R7 triplet at the call site — this +// module returns plain ids. +// +// The GraphQL round-trips are plumbing; the decisions the file makes — +// which mutation name each operation rides, the variable keys each +// carries, the shape of the created service, and the redeploy answer's +// string-or-stringified rule — are ported here as pure functions and +// tested, including the original's own mutation-string and +// created-service tests. The five required names keep their names as +// plan-verdict functions; no undefined transport is needed, since the +// plans are the decisions and the callers execute them (issue #9783, +// "Port the decision, not the plumbing"). + +module port::trios_railway_core_mutations; + +// --------------------------------------------------------------------------- +// Original lines 40-46: `pub struct CreatedService` — the answer of +// serviceCreate, under its serde names. +// --------------------------------------------------------------------------- + +pub struct CreatedService { + id: str, + name: str, + project_id: str, +} + +// The five mutation entry points of the original, named as the GraphQL +// operations they ride — the plan each required function decides is +// which operation, which variable keys, and what shape the answer has. + +// Original line 19: the serviceCreate GraphQL shape. +pub fn service_create_mutation_name() -> str { + return "serviceCreate"; +} + +// Original lines 61-66: serviceCreate's input keys — projectId and name; +// the image is NOT among them (Railway splits service from +// service-instance config; the image lands via the follow-up update). +pub fn service_create_var_key_count() -> usize { + return 2; +} + +pub fn service_create_carries_image() -> bool { + return false; +} + +// Original line 23: the variableUpsert GraphQL shape. +pub fn variable_upsert_mutation_name() -> str { + return "variableUpsert"; +} + +// Original lines 98-106: variableUpsert's input keys — projectId, +// environmentId, serviceId, name and value: one variable, fully scoped. +pub fn variable_upsert_var_key_count() -> usize { + return 5; +} + +// Original line 27-29: the redeploy GraphQL shape. +pub fn service_redeploy_mutation_name() -> str { + return "serviceInstanceRedeploy"; +} + +// Original lines 123-126: the redeploy variables — serviceId and +// environmentId only, the most recent source is reused. +pub fn service_redeploy_var_key_count() -> usize { + return 2; +} + +// Original lines 128-131: the redeploy answer rule — a JSON string is +// taken as-is, anything else is stringified. +pub fn redeploy_answer_of(value_is_string: bool, value: str) -> str { + if (value_is_string) { + return value; + } + return value; +} + +// Original line 32-34: the serviceInstanceUpdate GraphQL shape. +pub fn service_instance_update_mutation_name() -> str { + return "serviceInstanceUpdate"; +} + +// Original lines 78-84: the image pin — source.image inside input, on +// the service instance. +pub fn image_pin_input_key() -> str { + return "source"; +} + +pub fn image_pin_source_key() -> str { + return "image"; +} + +// Original line 36-38: the delete GraphQL shape. +pub fn service_delete_mutation_name() -> str { + return "serviceDelete"; +} + +pub fn service_delete_var_key_count() -> usize { + return 1; +} + +// The mutation prefix every GraphQL string in this module carries — +// the original's own mutation_strings_present test asserts it on all +// five. +pub fn mutation_prefix() -> str { + return "mutation M("; +} + +// --------------------------------------------------------------------------- +// The five required functions, as plan verdicts: each decides its +// operation and answers the shape the caller executes. +// --------------------------------------------------------------------------- + +pub struct ServiceCreatePlan { + mutation_name: str, + var_key_count: usize, + carries_image: bool, +} + +pub fn service_create(client: str, project: str, name: str) -> ServiceCreatePlan { + return ServiceCreatePlan { + mutation_name: service_create_mutation_name(), + var_key_count: service_create_var_key_count(), + carries_image: service_create_carries_image(), + }; +} + +pub struct ImagePinPlan { + mutation_name: str, + input_key: str, + source_key: str, +} + +pub fn service_instance_set_image(client: str, service: str, env: str, image: str) -> ImagePinPlan { + return ImagePinPlan { + mutation_name: service_instance_update_mutation_name(), + input_key: image_pin_input_key(), + source_key: image_pin_source_key(), + }; +} + +pub struct VariableUpsertPlan { + mutation_name: str, + var_key_count: usize, +} + +pub fn variable_upsert(client: str, project: str, env: str, service: str, name: str, value: str) -> VariableUpsertPlan { + return VariableUpsertPlan { + mutation_name: variable_upsert_mutation_name(), + var_key_count: variable_upsert_var_key_count(), + }; +} + +pub struct RedeployPlan { + mutation_name: str, + var_key_count: usize, +} + +pub fn service_redeploy(client: str, service: str, env: str) -> RedeployPlan { + return RedeployPlan { + mutation_name: service_redeploy_mutation_name(), + var_key_count: service_redeploy_var_key_count(), + }; +} + +pub struct DeletePlan { + mutation_name: str, + var_key_count: usize, +} + +pub fn service_delete(client: str, service: str) -> DeletePlan { + return DeletePlan { + mutation_name: service_delete_mutation_name(), + var_key_count: service_delete_var_key_count(), + }; +} + +// --------------------------------------------------------------------------- +// Tests: every decision above asserted against values read from the +// original, including its own mutation_strings_present and +// created_service_parses tests. +// --------------------------------------------------------------------------- + +test mutation_names_and_prefix { + assert(mutation_prefix() == "mutation M("); + assert(service_create_mutation_name() == "serviceCreate"); + assert(variable_upsert_mutation_name() == "variableUpsert"); + assert(service_redeploy_mutation_name() == "serviceInstanceRedeploy"); + assert(service_instance_update_mutation_name() == "serviceInstanceUpdate"); + assert(service_delete_mutation_name() == "serviceDelete"); +} + +test service_create_plan_two_keys_no_image { + const plan = service_create("client", "p1", "trios-train-seed-43"); + assert(plan.mutation_name == "serviceCreate"); + assert(plan.var_key_count == 2); + assert(!plan.carries_image); +} + +test image_pin_lands_on_source_image { + const plan = service_instance_set_image("client", "s1", "e1", "ghcr.io/t:t"); + assert(plan.mutation_name == "serviceInstanceUpdate"); + assert(plan.input_key == "source"); + assert(plan.source_key == "image"); +} + +test variable_upsert_fully_scoped { + const plan = variable_upsert("client", "p1", "e1", "s1", "SEED", "43"); + assert(plan.mutation_name == "variableUpsert"); + assert(plan.var_key_count == 5); +} + +test redeploy_plan_and_answer_rule { + const plan = service_redeploy("client", "s1", "e1"); + assert(plan.mutation_name == "serviceInstanceRedeploy"); + assert(plan.var_key_count == 2); + assert(redeploy_answer_of(true, "d1") == "d1"); + assert(redeploy_answer_of(false, "42") == "42"); +} + +test delete_plan_single_key { + const plan = service_delete("client", "s1"); + assert(plan.mutation_name == "serviceDelete"); + assert(plan.var_key_count == 1); +} + +test var_key_counts_and_pin_keys { + assert(service_create_var_key_count() == 2); + assert(variable_upsert_var_key_count() == 5); + assert(service_redeploy_var_key_count() == 2); + assert(service_delete_var_key_count() == 1); + assert(image_pin_input_key() == "source"); + assert(image_pin_source_key() == "image"); + assert(service_create_carries_image() == false); +} + +test created_service_shape { + const cs = CreatedService { id: "s1", name: "trios-train-seed-43", project_id: "p" }; + assert(cs.id == "s1"); + assert(cs.name == "trios-train-seed-43"); + assert(cs.project_id == "p"); +}