1use crate::name::MAX_PROJECT_NAME_LEN;
2use anyhow::{Result, anyhow};
3use serde::{Deserialize, Serialize};
4
5#[derive(Serialize)]
6struct NewProjectInput<'a> {
7 name: &'a str,
8}
9
10#[derive(Deserialize)]
11#[serde(tag = "t", rename_all_fields = "camelCase")]
12enum NewProject {
13 Ok { project_id: String },
14 NotLoggedIn,
15 InvalidName,
16 InternalError,
17}
18
19pub async fn ensure_project_id(
20 client: &reqwest::Client,
21 control_url: &str,
22 token: &str,
23 project_name: &str,
24 project_id: &mut Option<String>,
25) -> Result<String> {
26 if let Some(id) = project_id.as_ref() {
27 return Ok(id.clone());
28 }
29 let url = format!(
30 "{}/__forte_action/new_project",
31 control_url.trim_end_matches('/')
32 );
33 let resp = client
34 .post(&url)
35 .bearer_auth(token)
36 .json(&NewProjectInput { name: project_name })
37 .send()
38 .await?
39 .error_for_status()
40 .map_err(|e| anyhow!("new_project failed: {e}"))?;
41 let raw: NewProject = resp.json().await?;
42 let id = match raw {
43 NewProject::Ok { project_id } => project_id,
44 NewProject::NotLoggedIn => {
45 return Err(anyhow!("control rejected token; run `fn0 login` again."));
46 }
47 NewProject::InvalidName => {
48 return Err(anyhow!(
49 "control rejected project name '{project_name}': must be 1-{MAX_PROJECT_NAME_LEN} chars of letters, digits, '.', '_', '-'"
50 ));
51 }
52 NewProject::InternalError => {
53 return Err(anyhow!("new_project: server error; check fn0-control logs"));
54 }
55 };
56 *project_id = Some(id.clone());
57 Ok(id)
58}
59
60#[derive(Serialize)]
61struct RenameProjectInput<'a> {
62 project_id: &'a str,
63 new_name: &'a str,
64}
65
66#[derive(Deserialize)]
67#[serde(tag = "t", rename_all_fields = "camelCase")]
68enum RenameProject {
69 Ok,
70 NotLoggedIn,
71 NotFound,
72 InvalidName,
73 InternalError,
74}
75
76#[derive(Serialize)]
77struct DeleteProjectInput<'a> {
78 project_id: &'a str,
79}
80
81#[derive(Deserialize)]
82#[serde(tag = "t", rename_all_fields = "camelCase")]
83enum DeleteProject {
84 Ok,
85 NotLoggedIn,
86 NotFound,
87 InternalError,
88}
89
90pub async fn delete_project(project_id: &str) -> Result<()> {
91 let creds = crate::credentials::require()?;
92 let client = reqwest::Client::new();
93 let url = format!(
94 "{}/__forte_action/delete_project",
95 creds.control_url.trim_end_matches('/')
96 );
97 let resp = client
98 .post(&url)
99 .bearer_auth(&creds.token)
100 .json(&DeleteProjectInput { project_id })
101 .send()
102 .await?
103 .error_for_status()?;
104 let raw: DeleteProject = resp.json().await?;
105 match raw {
106 DeleteProject::Ok => Ok(()),
107 DeleteProject::NotLoggedIn => {
108 Err(anyhow!("control rejected token; run `fn0 login` again."))
109 }
110 DeleteProject::NotFound => Err(anyhow!(
111 "project '{project_id}' not found or not owned by you."
112 )),
113 DeleteProject::InternalError => Err(anyhow!(
114 "delete_project: server error; check fn0-control logs"
115 )),
116 }
117}
118
119pub async fn rename_project(project_id: &str, new_name: &str) -> Result<()> {
120 let creds = crate::credentials::require()?;
121 let client = reqwest::Client::new();
122 let url = format!(
123 "{}/__forte_action/rename_project",
124 creds.control_url.trim_end_matches('/')
125 );
126 let resp = client
127 .post(&url)
128 .bearer_auth(&creds.token)
129 .json(&RenameProjectInput {
130 project_id,
131 new_name,
132 })
133 .send()
134 .await?
135 .error_for_status()?;
136 let raw: RenameProject = resp.json().await?;
137 match raw {
138 RenameProject::Ok => Ok(()),
139 RenameProject::NotLoggedIn => {
140 Err(anyhow!("control rejected token; run `fn0 login` again."))
141 }
142 RenameProject::NotFound => Err(anyhow!(
143 "project '{project_id}' not found or not owned by you."
144 )),
145 RenameProject::InvalidName => Err(anyhow!(
146 "control rejected name '{new_name}': must be 1-{MAX_PROJECT_NAME_LEN} chars of letters, digits, '.', '_', '-'"
147 )),
148 RenameProject::InternalError => Err(anyhow!(
149 "rename_project: server error; check fn0-control logs"
150 )),
151 }
152}