railwayapp 5.44.2

Interact with Railway via CLI
use serde::{Deserialize, Serialize};
use serde_json::Value;

use super::json::field_str;

pub const RAILWAY_GRAPH_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RailwayGraph {
    pub version: u32,
    pub project: ProjectNode,
    #[serde(default)]
    pub environments: Vec<EnvironmentNode>,
    #[serde(default)]
    pub resources: Vec<Value>,
    #[serde(default)]
    pub edges: Vec<Edge>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ProjectNode {
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct EnvironmentNode {
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Edge {
    pub from: String,
    pub to: String,
    #[serde(rename = "type")]
    pub kind: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
}

pub fn resource_address(resource_type: &str, name: &str) -> String {
    format!("{resource_type}.{name}")
}

pub fn resource_type(resource: &Value) -> &str {
    field_str(resource, "type").unwrap_or("")
}

pub fn resource_name(resource: &Value) -> &str {
    field_str(resource, "name").unwrap_or("")
}

pub fn resource_addr(resource: &Value) -> String {
    field_str(resource, "address")
        .map(str::to_string)
        .unwrap_or_else(|| resource_address(resource_type(resource), resource_name(resource)))
}

pub fn validate_graph(graph: &RailwayGraph) -> Vec<String> {
    let mut errors = Vec::new();
    if graph.version != RAILWAY_GRAPH_VERSION {
        errors.push(format!("Unsupported graph version: {}", graph.version));
    }
    let mut addresses = std::collections::HashSet::new();
    for resource in &graph.resources {
        let address = resource_addr(resource);
        if !addresses.insert(address.clone()) {
            errors.push(format!("Duplicate resource address: {address}"));
        }
    }
    for edge in &graph.edges {
        if !addresses.contains(&edge.from) {
            errors.push(format!("Edge references missing source: {}", edge.from));
        }
        if !addresses.contains(&edge.to) {
            errors.push(format!("Edge references missing target: {}", edge.to));
        }
    }
    errors
}