thirdshift 0.22.0

Turn a GitHub issue into a ready-for-review pull request, unattended
//! OpenAPI 3.1 from the same entries that register the Console's API routes.
//! Only static metadata is read: descriptions never inspect local credentials.
use serde_json::{Map, Value, json};

use super::{PAGES, PageRoute, View};

pub fn print() -> anyhow::Result<String> {
    Ok(serde_json::to_string_pretty(&document())?)
}

pub(super) async fn serve() -> axum::Json<Value> {
    axum::Json(document())
}

pub(super) fn document() -> Value {
    let mut paths = Map::<String, Value>::new();
    for route in PAGES.iter().filter(|route| route.serves_api()) {
        let (parameters, schema) = operation(route);
        paths.entry(route.api).or_insert_with(|| json!({}))
            [route.method.as_str().to_ascii_lowercase()] = json!({
            "description": format!("CLI twins: {}. The required scope is declared in x-scope.", route.cli.join("; ")),
            "x-scope": route.scope,
            "x-cli-twins": route.cli,
            "parameters": parameters,
            "responses": {
                "200": {
                    "description": "The CLI twin's JSON response, filtered by the caller's scopes.",
                    "content": {"application/json": {"schema": schema}}
                },
                "400": refusal("Invalid path argument."),
                "401": refusal("A valid Console sign-in or bearer token is required."),
                "403": refusal("The caller lacks the required scope."),
                "422": refusal("The CLI twin refused the arguments or selected record."),
                "429": {
                    "description": "Too many failed authentications from this client address.",
                    "headers": {"Retry-After": {
                        "description": "Seconds until another authentication may be attempted.",
                        "schema": {"type": "integer", "minimum": 1}
                    }},
                    "content": {"application/json": {"schema": object(json!({
                        "error": {"type": "string"},
                        "retry_after": {"type": "integer", "minimum": 1}
                    }))}}
                },
                "500": refusal("The Console could not read this view.")
            }
        });
        if route.method == axum::http::Method::POST {
            paths.get_mut(route.api).unwrap()["post"]["requestBody"] = action_body(route.view);
        }
    }
    json!({
        "openapi": "3.1.1",
        "info": {
            "title": "thirdshift Console API",
            "version": env!("CARGO_PKG_VERSION"),
            "description": "Currently served /api/v1 operations. Each operation enforces its x-scope. Bearer tokens require HTTPS or a direct connection to 127.0.0.1 without proxy headers. Local --no-sign-in needs no session."
        },
        "paths": paths,
        "security": [{"ConsoleSession": []}, {"ConsoleBearer": []}],
        "components": {"securitySchemes": {
            "ConsoleSession": {
                "type": "apiKey", "in": "cookie", "name": "__Host-thirdshift-session"
            },
            "ConsoleBearer": {"type": "http", "scheme": "bearer"}
        }}
    })
}

fn refusal(description: &str) -> Value {
    json!({"description": description, "content": {"application/json": {
        "schema": object(json!({"error": {"type": "string"}}))
    }}})
}

fn parameter(name: &str, location: &str, required: bool, schema: Value) -> Value {
    json!({"name": name, "in": location, "required": required, "schema": schema})
}

fn object(properties: Value) -> Value {
    let required = properties
        .as_object()
        .expect("schema properties")
        .keys()
        .collect::<Vec<_>>();
    json!({"type": "object", "properties": properties, "required": required})
}

fn array(items: Value) -> Value {
    json!({"type": "array", "items": items})
}

/// Describe the CLI envelopes. Nested Command and Experiment records are open
/// objects so their existing versioned JSON fields remain extensible.
fn operation(route: &PageRoute) -> (Vec<Value>, Value) {
    let mut parameters: Vec<_> = route
        .api
        .split('/')
        .filter_map(|segment| {
            let name = segment.strip_prefix('{')?.strip_suffix('}')?;
            Some(parameter(name, "path", true, json!({"type": "string"})))
        })
        .collect();
    parameters.push(json!({
        "name": "X-Thirdshift-Person", "in": "header", "required": false,
        "description": "Bearer calls only: an address on the owning machine's sign-in list. Effective scopes intersect the token's and this address's scopes.",
        "schema": {"type": "string", "format": "email"}
    }));
    let mut query = |name, required, schema| {
        parameters.push(parameter(name, "query", required, schema));
    };
    let text = json!({"type": "string"});
    let integer = json!({"type": "integer"});
    let record = json!({"type": "object"});
    let version = json!({"type": "integer", "const": 1});
    let schema = match route.view {
        View::Whoami => object(json!({
            "version": version, "hostname": text, "thirdshift_version": text,
            "api_version": {"type": "string", "const": "v1"},
            "identity": {"oneOf": [
                {"type": "object", "required": ["kind", "holder"], "properties": {
                    "kind": {"const": "token"}, "holder": text, "person": text
                }},
                object(json!({"kind": {"const": "session"}, "address": text})),
                object(json!({"kind": {"const": "local"}, "uid": integer}))
            ]},
            "scopes": array(json!({"type": "string", "enum": super::Scope::ALL}))
        })),
        View::OpenApi => object(json!({
            "openapi": {"type": "string", "const": "3.1.1"},
            "info": record, "paths": record, "components": record, "security": array(record.clone())
        })),
        View::Inbox if route.path == "/waterfall" => {
            query("command", false, text);
            tree()
        }
        View::Inbox => {
            if route.path == "/" {
                query("all", false, json!({"type": "boolean", "default": false}));
            }
            object(json!({
                "version": version, "hostname": text, "active_runs": integer,
                "commands": array(record.clone()), "idle_passes": array(record.clone()),
                "pass_starts": array(record), "github_answer_age_seconds": {"type": ["integer", "null"]},
                "github_last_answered_at": {"type": ["string", "null"]},
                "github_failure": {"type": ["string", "null"]}
            }))
        }
        View::Inspect => {
            query("command", true, text.clone());
            object(json!({"version": version, "hostname": text, "command": record}))
        }
        View::Spans => {
            query("command", false, text.clone());
            query("span", false, text);
            json!({"oneOf": [tree(), span()]})
        }
        View::Log => {
            query("command", true, text.clone());
            query("session", false, text.clone());
            object(json!({"version": version, "contents": text}))
        }
        View::Reader => object(json!({"version": version, "contents": text})),
        View::Logs | View::CommandLogs => object(json!({
            "version": version, "logs": array(object(json!({
                "id": text, "name": text, "repository": text, "kind": text,
                "live": {"type": "boolean"}, "command": text, "owners": array(text.clone())
            })))
        })),
        View::Metrics => {
            for name in ["command", "from", "through"] {
                query(name, false, text.clone());
            }
            query("repo", false, array(text.clone()));
            json!({"oneOf": [
                object(json!({"version": version, "hostname": text, "command": record})),
                object(json!({"version": version, "hostname": text, "weeks": array(record)}))
            ]})
        }
        View::BugOrigins => array(object(json!({
            "issue": text, "closed_at": text, "state": {"enum": ["proposed", "confirmed", "rejected"]},
            "commit": {"type": ["string", "null"]}, "reason": text, "pull_requests": array(record)
        }))),
        View::Scores => {
            query("pull_request", false, text.clone());
            array(object(json!({
                "week": text, "window_start": text, "window_end": text,
                "seed": integer, "sample": array(record), "initial_calibration": {"type": "boolean"}
            })))
        }
        View::Experiments => array(experiment()),
        View::Experiment => experiment(),
        View::ExperimentSession => object(json!({
            "id": text, "trace": {"type": "boolean"}, "contents": text
        })),
        View::CronJobs => object(json!({
            "jobs": array(object(json!({"schedule": text, "command": text}))), "pass_starts": array(record)
        })),
        View::Resources | View::ResourceStatus => object(json!({
            "host": object(json!({"total_memory_mb": integer, "available_memory_mb": integer,
                "logical_cpus": integer, "load_one": {"type": "number"}})),
            "limits": object(json!({"min_available_memory_mb": integer, "max_load_per_cpu_percent": integer})),
            "load_per_cpu_percent": {"type": "number"}, "ready": {"type": "boolean"},
            "reasons": array(text)
        })),
        View::ConsoleLog => array(object(
            json!({"time": integer, "email": text, "event": text}),
        )),
        View::ThemeImport | View::ThemeExport => object(json!({
            "name": text, "base": text, "tokens": {"type": "object", "additionalProperties": text}
        })),
        View::Themes => array(text),
        View::Stop => object(json!({"version": version, "name": text, "outcome": text,
            "issue_branches": array(text.clone()), "pull_requests": array(text)})),
        View::Dismiss | View::UndoDismissal => record,
        View::HumanScores => array(record),
        View::SettleBugOrigin => record,
        View::ApproveEvalTask => {
            object(json!({"suite": text, "task": text, "state": {"const": "approved"}}))
        }
        View::Look | View::Theme | View::Sections => record,
        View::Changes
        | View::LogStream
        | View::Account
        | View::SignOut
        | View::SignInPage
        | View::SignIn
        | View::StopPage
        | View::SetPasswordPage
        | View::SendCode
        | View::SetPassword => unreachable!("page-only operation"),
    };
    (parameters, schema)
}

fn action_body(view: View) -> Value {
    let text = json!({"type": "string"});
    let (properties, required) = match view {
        View::ThemeImport => {
            return json!({"required": true, "content": {"application/json": {"schema": {
                "type": "object", "required": ["name"], "properties": {
                    "name": text, "base": text,
                    "tokens": {"type": "object", "additionalProperties": text}
                }
            }}}});
        }
        View::Stop => (
            json!({"command": text, "requeue": {"type":"boolean", "default":false}}),
            vec!["command"],
        ),
        View::Dismiss | View::UndoDismissal => (json!({"command": text}), vec!["command"]),
        View::Theme => (json!({"theme": text}), vec!["theme"]),
        View::Sections => (json!({"hide": text, "show": text}), vec![]),
        View::SettleBugOrigin => (
            json!({"issue": text, "action": {"enum":["confirm","correct","reject"]}, "pull_request": text}),
            vec!["issue", "action"],
        ),
        View::HumanScores => {
            let mut properties = json!({"pull_request": text});
            let mut required = vec!["pull_request".to_owned()];
            for criterion in crate::scoring::Criterion::ALL {
                let name = criterion.name();
                properties[name] = json!({"enum":["pass","fail"]});
                properties[format!("{name}_reason")] = text.clone();
                required.push(name.to_owned());
                required.push(format!("{name}_reason"));
            }
            return json!({"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{
                "type":"object", "properties":properties,"required":required
            }}}});
        }
        View::ApproveEvalTask => (json!({}), vec![]),
        View::Whoami
        | View::OpenApi
        | View::BugOrigins
        | View::Scores
        | View::Experiments
        | View::Experiment
        | View::ExperimentSession
        | View::CronJobs
        | View::Resources
        | View::ResourceStatus
        | View::Metrics
        | View::Inbox
        | View::Inspect
        | View::Spans
        | View::Log
        | View::Changes
        | View::Logs
        | View::CommandLogs
        | View::Reader
        | View::LogStream
        | View::ConsoleLog
        | View::Account
        | View::SignOut
        | View::SignInPage
        | View::SignIn
        | View::Look
        | View::Themes
        | View::ThemeExport
        | View::StopPage
        | View::SetPasswordPage
        | View::SendCode
        | View::SetPassword => unreachable!("not a served action"),
    };
    json!({"required":!required.is_empty(),"content":{"application/x-www-form-urlencoded":{"schema":{
        "type":"object", "properties":properties,"required":required
    }}}})
}

fn tree() -> Value {
    object(
        json!({"command": {"type": "string"}, "now": {"type": "number"}, "spans": array(span())}),
    )
}

fn span() -> Value {
    object(json!({
        "id": {"type": "string"}, "parent_id": {"type": ["string", "null"]},
        "kind": {"type": "string"}, "name": {"type": "string"},
        "start": {"type": ["number", "null"]}, "end": {"type": ["number", "null"]},
        "duration_ms": {"type": ["number", "null"]}, "status": {"type": "string"},
        "error": {"type": ["string", "null"]}, "estimated": {"type": "boolean"},
        "attributes": {"type": "object"}, "input": {"type": ["string", "null"]},
        "output": {"type": ["string", "null"]}, "command": {"type": "string"},
        "command_url": {"type": "string"}, "conversation_url": {"type": ["string", "null"]},
        "run": {"type": ["integer", "null"]}, "session_kind": {"type": ["string", "null"]}
    }))
}

fn experiment() -> Value {
    object(json!({
        "format": {"type": "integer", "const": 4}, "id": {"type": "string"},
        "name": {"type": "string"}, "state": {"type": "string"}, "created_at": {"type": "string"},
        "tasks": array(json!({"type": "object"})), "results": array(json!({"type": "object"})),
        "sessions": array(json!({"type": "object"})), "running_trials": array(json!({"type": "object"}))
    }))
}