Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions specs/port/trios-railway/crates/trios-railway-core/src/transport.t27
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// RW-02: the minimal Railway GraphQL client.
// Port of gHashTag/trios-railway:crates/trios-railway-core/src/transport.rs
// (at 8b229e9489ee, 210+ lines) — Roadmap Stage 6, goal #4548, target t27c
// gen-rust.
//
// The original talks to the Railway GraphQL endpoint with either of two
// authorization styles: `team` (the default) sends `Authorization:
// Bearer <token>` for account tokens; `project` sends
// `Project-Access-Token: <token>` for project-scoped tokens. The env
// read allows operators to force the mode (team | bearer | personal →
// team, project → project), and otherwise guesses from the token's
// shape: a canonical 8-4-4-4-12 hex UUID means a project token. The
// token fingerprint is suitable for audit lines and never exposes the
// token. The raw query posts the request, joins any GraphQL errors with
// "; ", and treats absent data as an error of its own.
//
// The HTTP round-trip and the hash are plumbing; the decisions — the
// UUID shape check (36 bytes, dashes at 8, 13, 18, 23, hex elsewhere),
// the env-override and heuristic auth-mode chain, the team default of
// with_token, the endpoint override, the two header names, and the
// query verdicts (errors → graphql, empty data → empty) — are ported
// here as pure functions and tested. The token itself is never stored
// in the port: presence flags carry its decisions (issue #9795, "Port
// the decision, not the plumbing").

module port::trios_railway_core_transport;

// Original line 13: the endpoint constant.
pub fn graphql_endpoint() -> str {
return "https://backboard.railway.com/graphql/v2";
}

// ---------------------------------------------------------------------------
// Original lines 16-33: `fn is_uuid_like(s) -> bool` — the canonical
// 8-4-4-4-12 hex UUID shape, byte by byte.
// ---------------------------------------------------------------------------

pub fn uuid_hex_digit(b: u8) -> bool {
if (b >= '0' and b <= '9') {
return true;
}
if (b >= 'a' and b <= 'f') {
return true;
}
if (b >= 'A' and b <= 'F') {
return true;
}
return false;
}

pub fn is_dash_position(i: usize) -> bool {
if (i == 8) { return true; }
if (i == 13) { return true; }
if (i == 18) { return true; }
if (i == 23) { return true; }
return false;
}

pub fn is_uuid_like(s: []const u8) -> bool {
if (s.len != 36) {
return false;
}
var i: usize = 0;
while (i < 36) {
if (is_dash_position(i)) {
if (s[i] != '-') {
return false;
}
} else {
if (!uuid_hex_digit(s[i])) {
return false;
}
}
i += 1;
}
return true;
}

// ---------------------------------------------------------------------------
// Original lines 88-100: `pub fn from_env()`. The token read fails as
// MissingToken; the RAILWAY_TOKEN_AUTH override forces the mode
// (team | bearer | personal → team, project → project), and without an
// override the UUID heuristic guesses: a UUID-shaped token is a project
// token, anything else a team token. The token itself is the caller's
// plumbing - the port carries its presence and its shape answer.
// ---------------------------------------------------------------------------

pub struct FromEnvOutcome {
built: bool,
auth_mode: str,
}

pub fn Client_from_env(token_set: bool, token_is_uuid_shaped: bool, auth_override: str) -> FromEnvOutcome {
if (!token_set) {
return FromEnvOutcome { built: false, auth_mode: "" };
}
if (auth_override == "team") {
return FromEnvOutcome { built: true, auth_mode: "team" };
}
if (auth_override == "bearer") {
return FromEnvOutcome { built: true, auth_mode: "team" };
}
if (auth_override == "personal") {
return FromEnvOutcome { built: true, auth_mode: "team" };
}
if (auth_override == "project") {
return FromEnvOutcome { built: true, auth_mode: "project" };
}
if (token_is_uuid_shaped) {
return FromEnvOutcome { built: true, auth_mode: "project" };
}
return FromEnvOutcome { built: true, auth_mode: "team" };
}

// Original lines 102-104: `pub fn with_token(token)` — the team default.
pub fn Client_with_token(token_set: bool) -> FromEnvOutcome {
if (!token_set) {
return FromEnvOutcome { built: false, auth_mode: "" };
}
return FromEnvOutcome { built: true, auth_mode: "team" };
}

// Original lines 106-119: `pub fn with_token_and_mode(token, auth)` —
// the explicit mode, endpoint fixed to the Railway GraphQL URL.
pub fn Client_with_token_and_mode(token_set: bool, auth_mode: str) -> FromEnvOutcome {
if (!token_set) {
return FromEnvOutcome { built: false, auth_mode: "" };
}
return FromEnvOutcome { built: true, auth_mode: auth_mode };
}

// Original lines 121-124: `pub fn auth_mode(&self)` — the readback.
pub fn Client_auth_mode(client: FromEnvOutcome) -> str {
return client.auth_mode;
}

// Original lines 126-131: `pub fn with_endpoint(self, endpoint)` —
// the endpoint override for integration tests.
pub fn Client_with_endpoint(client: FromEnvOutcome, endpoint: str) -> str {
return endpoint;
}

// Original lines 133-137: `pub fn token_fingerprint(&self)` — delegates
// to the hash module's fingerprint; audit lines see it, never the
// token. Delegation is plumbing.
pub fn Client_token_fingerprint(token: str) -> ?str {
return undefined; // plumbing: delegates to hash::token_fingerprint (audit-safe, never the token)
}

// ---------------------------------------------------------------------------
// Original lines 139-170: `pub async fn query(query, variables)`. The
// raw GraphQL POST: the team mode sends the Bearer header, the project
// mode the Project-Access-Token header; GraphQL errors are joined with
// "; " and answer an error of kind graphql; absent data answers an
// error of its own. The HTTP round-trip is plumbing; the header choice
// and the verdicts are the decisions.
// ---------------------------------------------------------------------------

pub fn team_auth_header() -> str {
return "Authorization: Bearer";
}

pub fn project_auth_header() -> str {
return "Project-Access-Token";
}

pub struct QueryOutcome {
ok: bool,
error_kind: str,
}

pub fn Client_query(auth_mode: str, errors_present: bool, data_present: bool) -> QueryOutcome {
if (errors_present) {
return QueryOutcome { ok: false, error_kind: "graphql" };
}
if (!data_present) {
return QueryOutcome { ok: false, error_kind: "empty data" };
}
return QueryOutcome { ok: true, error_kind: "" };
}

pub fn query_header_for_mode(auth_mode: str) -> str {
if (auth_mode == "project") {
return project_auth_header();
}
return team_auth_header();
}

// ---------------------------------------------------------------------------
// Tests: every decision above asserted against values read from the
// original. No test calls the fingerprint transport.
// ---------------------------------------------------------------------------

test uuid_like_accepts_canonical_shape {
assert(is_uuid_like("e4fe33bb-3b09-4842-9782-7d2dea1abc9b"));
}

test uuid_like_rejects_wrong_shapes {
assert(!is_uuid_like("e4fe33bb-3b09-4842-9782-7d2dea1abc9"));
assert(!is_uuid_like("e4fe33bbx3b09-4842-9782-7d2dea1abc9b"));
assert(!is_uuid_like("e4fe33bb-3b09-4842-9782-7d2dea1abc9g"));
assert(uuid_hex_digit('0'));
assert(uuid_hex_digit('f'));
assert(uuid_hex_digit('A'));
assert(!uuid_hex_digit('g'));
assert(!uuid_hex_digit('-'));
}

test from_env_missing_token_fails {
const out = Client_from_env(false, false, "");
assert(!out.built);
}

test from_env_overrides_force_the_mode {
assert(Client_from_env(true, false, "team").auth_mode == "team");
assert(Client_from_env(true, false, "bearer").auth_mode == "team");
assert(Client_from_env(true, false, "personal").auth_mode == "team");
assert(Client_from_env(true, true, "project").auth_mode == "project");
}

test from_env_heuristic_guesses_from_the_shape {
assert(Client_from_env(true, true, "").auth_mode == "project");
assert(Client_from_env(true, false, "").auth_mode == "team");
}

test with_token_defaults_to_team_and_readback {
const out = Client_with_token(true);
assert(out.built);
assert(Client_auth_mode(out) == "team");
const explicit = Client_with_token_and_mode(true, "project");
assert(Client_auth_mode(explicit) == "project");
}

test endpoint_override {
const out = Client_with_token(true);
assert(Client_with_endpoint(out, "http://localhost:9333/graphql") == "http://localhost:9333/graphql");
}

test endpoint_constant_dash_positions_and_headers {
assert(graphql_endpoint() == "https://backboard.railway.com/graphql/v2");
assert(is_dash_position(8));
assert(is_dash_position(13));
assert(is_dash_position(18));
assert(is_dash_position(23));
assert(!is_dash_position(0));
assert(!is_dash_position(12));
assert(team_auth_header() == "Authorization: Bearer");
assert(project_auth_header() == "Project-Access-Token");
}

test query_headers_and_verdicts {
assert(query_header_for_mode("team") == "Authorization: Bearer");
assert(query_header_for_mode("project") == "Project-Access-Token");
const ok = Client_query("team", false, true);
assert(ok.ok);
const errs = Client_query("team", true, true);
assert(!errs.ok);
assert(errs.error_kind == "graphql");
const empty = Client_query("team", false, false);
assert(!empty.ok);
assert(empty.error_kind == "empty data");
}
Loading