use anyhow::bail;
use colored::*;
use is_terminal::IsTerminal;
use std::fmt::Display;
use crate::{
errors::RailwayError,
util::prompt::{fake_select, prompt_options, prompt_options_skippable},
workspace::{Project, Workspace, workspaces},
};
use super::*;
#[derive(Parser)]
pub struct Args {
#[clap(long, short)]
environment: Option<String>,
#[clap(long, short, alias = "project_id")]
project: Option<String>,
#[clap(long, short)]
service: Option<String>,
#[clap(long, short)]
team: Option<String>,
#[clap(long, short)]
workspace: Option<String>,
}
pub async fn command(args: Args) -> Result<()> {
let mut configs = Configs::new()?;
let workspace_arg = match (args.team.as_ref(), args.workspace.as_ref()) {
(Some(_), None) => {
eprintln!(
"{}",
"Warning: The --team flag is deprecated. Please use --workspace instead.".yellow()
);
args.team
}
(None, workspace) => workspace.cloned(),
(Some(_), Some(_)) => {
eprintln!("{}", "Warning: Both --team and --workspace provided. Using --workspace. The --team flag is deprecated.".yellow());
args.workspace
}
};
let workspaces = workspaces().await?;
let workspace = select_workspace(args.project.clone(), workspace_arg, workspaces)?;
let project = select_project(workspace, args.project.clone())?;
let environment = select_environment(args.environment, &project)?;
let service = select_service(&project, &environment, args.service)?;
configs.link_project(
project.id,
Some(project.name.clone()),
environment.id,
Some(environment.name),
)?;
if let Some(service) = service {
configs.link_service(service.id)?;
}
println!(
"\n{} {} {}",
"Project".green(),
project.name.magenta().bold(),
"linked successfully! 🎉".green()
);
configs.write()?;
Ok(())
}
fn select_service(
project: &NormalisedProject,
environment: &NormalisedEnvironment,
service: Option<String>,
) -> Result<Option<NormalisedService>, anyhow::Error> {
let useful_services = project
.services
.iter()
.filter(|&a| {
a.service_instances
.iter()
.any(|instance| instance == &environment.id)
})
.cloned()
.collect::<Vec<NormalisedService>>();
let service = if !useful_services.is_empty() {
if let Some(service) = service {
let service_norm = useful_services.iter().find(|s| {
(s.name.to_lowercase() == service.to_lowercase())
|| (s.id.to_lowercase() == service.to_lowercase())
});
if let Some(service) = service_norm {
fake_select("Select a service", &service.name);
Some(service.clone())
} else {
return Err(RailwayError::ServiceNotFound(service).into());
}
} else if std::io::stdout().is_terminal() {
prompt_options_skippable("Select a service <esc to skip>", useful_services)?
} else {
None
}
} else {
None
};
Ok(service)
}
fn select_environment(
environment: Option<String>,
project: &NormalisedProject,
) -> Result<NormalisedEnvironment, anyhow::Error> {
if project.environments.is_empty() {
if project.has_restricted_environments {
bail!("All environments in this project are restricted");
} else {
bail!("Project has no environments");
}
}
let environment = if let Some(environment) = environment {
let env = project.environments.iter().find(|e| {
(e.name.to_lowercase() == environment.to_lowercase())
|| (e.id.to_lowercase() == environment.to_lowercase())
});
if let Some(env) = env {
fake_select("Select an environment", &env.name);
env.clone()
} else {
return Err(RailwayError::EnvironmentNotFound(environment).into());
}
} else if project.environments.len() == 1 {
let env = project.environments[0].clone();
fake_select("Select an environment", &env.name);
env
} else {
prompt_options("Select an environment", project.environments.clone())?
};
Ok(environment)
}
fn select_project(
workspace: Workspace,
project: Option<String>,
) -> Result<NormalisedProject, anyhow::Error> {
let projects = workspace
.projects()
.into_iter()
.filter(|p| p.deleted_at().is_none())
.collect::<Vec<_>>();
let project = NormalisedProject::from({
if let Some(project) = project {
let proj = projects.into_iter().find(|pro| {
(pro.id().to_lowercase() == project.to_lowercase())
|| (pro.name().to_lowercase() == project.to_lowercase())
});
if let Some(project) = proj {
fake_select("Select a project", &project.to_string());
project
} else {
return Err(RailwayError::ProjectNotFoundInWorkspace(
project,
workspace.name().to_owned(),
)
.into());
}
} else {
prompt_workspace_projects(projects)?
}
});
Ok(project)
}
fn select_workspace(
project: Option<String>,
workspace_name: Option<String>,
workspaces: Vec<Workspace>,
) -> Result<Workspace, anyhow::Error> {
let workspace = match (project, workspace_name) {
(Some(project), None) => {
if let Some(workspace) = workspaces.iter().find(|w| {
w.projects().iter().any(|pro| {
pro.id().to_lowercase() == project.to_lowercase()
|| pro.name().to_lowercase() == project.to_lowercase()
})
}) {
fake_select("Select a workspace", workspace.name());
workspace.clone()
} else {
prompt_workspaces(workspaces)?
}
}
(None, Some(workspace_arg)) | (Some(_), Some(workspace_arg)) => {
if let Some(workspace) = workspaces.iter().find(|w| {
w.id().to_lowercase() == workspace_arg.to_lowercase()
|| w.team_id().map(str::to_lowercase) == Some(workspace_arg.to_lowercase())
|| w.name().to_lowercase() == workspace_arg.to_lowercase()
}) {
fake_select("Select a workspace", workspace.name());
workspace.clone()
} else if workspace_arg.to_lowercase() == "personal" {
bail!(RailwayError::NoPersonalWorkspace);
} else {
return Err(RailwayError::WorkspaceNotFound(workspace_arg.clone()).into());
}
}
(None, None) => prompt_workspaces(workspaces)?,
};
Ok(workspace)
}
fn prompt_workspaces(workspaces: Vec<Workspace>) -> Result<Workspace> {
if workspaces.is_empty() {
return Err(RailwayError::NoProjects.into());
}
if workspaces.len() == 1 {
fake_select("Select a workspace", workspaces[0].name());
return Ok(workspaces[0].clone());
}
prompt_options("Select a workspace", workspaces)
}
fn prompt_workspace_projects(projects: Vec<Project>) -> Result<Project, anyhow::Error> {
prompt_options("Select a project", projects)
}
structstruck::strike! {
#[strikethrough[derive(Debug, Clone, derive_new::new)]]
struct NormalisedProject {
id: String,
name: String,
environments: Vec<struct NormalisedEnvironment {
id: String,
name: String
}>,
services: Vec<struct NormalisedService {
id: String,
name: String,
service_instances: Vec<String>,
}>,
has_restricted_environments: bool,
}
}
impl From<Project> for NormalisedProject {
fn from(value: Project) -> Self {
match value {
Project::External(project) => {
let total_envs = project.environments.edges.len();
let accessible_envs: Vec<_> = project
.environments
.edges
.into_iter()
.filter(|env| env.node.can_access)
.map(|env| NormalisedEnvironment::new(env.node.id, env.node.name))
.collect();
let has_restricted = total_envs > accessible_envs.len();
NormalisedProject::new(
project.id,
project.name,
accessible_envs,
project
.services
.edges
.into_iter()
.map(|service| {
NormalisedService::new(
service.node.id,
service.node.name,
service
.node
.service_instances
.edges
.into_iter()
.map(|instance| instance.node.environment_id)
.collect(),
)
})
.collect(),
has_restricted,
)
}
Project::Workspace(project) => {
let total_envs = project.environments.edges.len();
let accessible_envs: Vec<_> = project
.environments
.edges
.into_iter()
.filter(|env| env.node.can_access)
.map(|env| NormalisedEnvironment::new(env.node.id, env.node.name))
.collect();
let has_restricted = total_envs > accessible_envs.len();
NormalisedProject::new(
project.id,
project.name,
accessible_envs,
project
.services
.edges
.into_iter()
.map(|service| {
NormalisedService::new(
service.node.id,
service.node.name,
service
.node
.service_instances
.edges
.into_iter()
.map(|instance| instance.node.environment_id)
.collect(),
)
})
.collect(),
has_restricted,
)
}
}
}
}
impl Display for NormalisedEnvironment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl Display for NormalisedService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}