use colored::*;
use std::fmt::Display;
use crate::{
controllers::project::get_project,
errors::RailwayError,
queries::project::ProjectProject,
util::prompt::{fake_select, prompt_options},
};
use super::{
queries::{
projects::ProjectsProjectsEdgesNode,
user_projects::{UserProjectsMeProjectsEdgesNode, UserProjectsMeTeamsEdgesNode},
},
*,
};
#[derive(Parser)]
pub struct Args {
#[clap(long, short)]
environment: Option<String>,
#[clap(long, short)]
project_id: Option<String>,
#[clap(long, short)]
service: Option<String>,
}
pub async fn command(args: Args, _json: bool) -> Result<()> {
let mut configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let project = NormalisedProject::from(if let Some(project_id) = args.project_id {
let fetched_project = get_project(&client, &configs, project_id).await?;
if let Some(team) = fetched_project.clone().team {
fake_select("Select a team", &team.name);
}
fake_select("Select a project", fetched_project.name.as_str());
Project(ProjectType::Fetched(fetched_project))
} else {
let me = post_graphql::<queries::UserProjects, _>(
&client,
configs.get_backboard(),
queries::user_projects::Variables {},
)
.await?
.me;
let teams: Vec<_> = me.teams.edges.iter().map(|team| &team.node).collect();
if teams.is_empty() {
prompt_personal_projects(me)?
} else {
let mut team_names = vec![Team::Personal];
team_names.extend(teams.into_iter().map(Team::Team));
match prompt_options("Select a team", team_names)? {
Team::Personal => prompt_personal_projects(me)?,
Team::Team(team) => {
let vars = queries::projects::Variables {
team_id: Some(team.id.clone()),
};
let projects = post_graphql::<queries::Projects, _>(
&client,
configs.get_backboard(),
vars,
)
.await?
.projects;
prompt_team_projects(projects)?
}
}
}
});
let environment = if let Some(environment) = args.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.as_str());
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.as_str());
env
} else {
prompt_options("Select an environment", project.environments)?
};
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() {
Some(if let Some(service) = args.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);
service.clone()
} else {
return Err(RailwayError::ServiceNotFound(service).into());
}
} else {
prompt_options("Select a service", useful_services)?
})
} else {
None
};
configs.link_project(
project.id,
Some(project.name),
environment.id,
Some(environment.name),
)?;
if let Some(service) = service {
configs.link_service(service.id)?;
}
configs.write()?;
Ok(())
}
fn prompt_team_projects(
projects: queries::projects::ProjectsProjects,
) -> Result<Project, anyhow::Error> {
let mut team_projects: Vec<ProjectsProjectsEdgesNode> = projects
.edges
.iter()
.cloned()
.map(|edge| edge.node)
.collect();
team_projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
let prompt_projects = team_projects
.iter()
.cloned()
.map(|project| Project(ProjectType::Team(project)))
.collect::<Vec<Project>>();
prompt_options("Select a project", prompt_projects)
}
fn prompt_personal_projects(
me: queries::user_projects::UserProjectsMe,
) -> Result<Project, anyhow::Error> {
let mut personal_projects = me
.projects
.edges
.iter()
.map(|project| &project.node)
.collect::<Vec<&UserProjectsMeProjectsEdgesNode>>();
if personal_projects.is_empty() {
return Err(RailwayError::NoProjects.into());
}
personal_projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
let prompt_projects = personal_projects
.iter()
.cloned()
.map(|project| Project(ProjectType::Personal(project.clone())))
.collect::<Vec<Project>>();
prompt_options("Select a project", prompt_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>,
}>
}
}
impl From<Project> for NormalisedProject {
fn from(value: Project) -> Self {
match value.0 {
ProjectType::Fetched(fetched) => NormalisedProject::new(
fetched.id,
fetched.name,
fetched
.environments
.edges
.into_iter()
.map(|env| NormalisedEnvironment::new(env.node.id, env.node.name))
.collect(),
fetched
.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(),
),
ProjectType::Personal(personal) => NormalisedProject::new(
personal.id,
personal.name,
personal
.environments
.edges
.into_iter()
.map(|env| NormalisedEnvironment::new(env.node.id, env.node.name))
.collect(),
personal
.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(),
),
ProjectType::Team(team) => NormalisedProject::new(
team.id,
team.name,
team.environments
.edges
.into_iter()
.map(|env| NormalisedEnvironment::new(env.node.id, env.node.name))
.collect(),
team.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(),
),
}
}
}
#[derive(Debug, Clone)]
enum Team<'a> {
Team(&'a UserProjectsMeTeamsEdgesNode),
Personal,
}
impl<'a> Display for Team<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Team::Team(team) => write!(f, "{}", team.name),
Team::Personal => write!(f, "{}", "Personal".bold()),
}
}
}
#[derive(Debug, Clone)]
enum ProjectType {
Personal(UserProjectsMeProjectsEdgesNode),
Team(ProjectsProjectsEdgesNode),
Fetched(ProjectProject),
}
#[derive(Debug, Clone)]
struct Project(ProjectType);
impl Display for Project {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
ProjectType::Personal(personal) => write!(f, "{}", personal.name),
ProjectType::Team(team_project) => write!(f, "{}", team_project.name),
ProjectType::Fetched(fetched) => write!(f, "{}", fetched.name),
}
}
}
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)
}
}