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!(
54 "new_project: server error; check fn0-control logs"
55 ));
56 }
57 };
58 *project_id = Some(id.clone());
59 Ok(id)
60}
61
62#[derive(Serialize)]
63struct RenameProjectInput<'a> {
64 project_id: &'a str,
65 new_name: &'a str,
66}
67
68#[derive(Deserialize)]
69#[serde(tag = "t", rename_all_fields = "camelCase")]
70enum RenameProject {
71 Ok,
72 NotLoggedIn,
73 NotFound,
74 InvalidName,
75 InternalError,
76}
77
78#[derive(Serialize)]
79struct DeleteProjectInput<'a> {
80 project_id: &'a str,
81}
82
83#[derive(Deserialize)]
84#[serde(tag = "t", rename_all_fields = "camelCase")]
85enum DeleteProject {
86 Ok,
87 NotLoggedIn,
88 NotFound,
89 InternalError,
90}
91
92pub async fn delete_project(project_id: &str) -> Result<()> {
93 let creds = crate::credentials::require()?;
94 let client = reqwest::Client::new();
95 let url = format!(
96 "{}/__forte_action/delete_project",
97 creds.control_url.trim_end_matches('/')
98 );
99 let resp = client
100 .post(&url)
101 .bearer_auth(&creds.token)
102 .json(&DeleteProjectInput { project_id })
103 .send()
104 .await?
105 .error_for_status()?;
106 let raw: DeleteProject = resp.json().await?;
107 match raw {
108 DeleteProject::Ok => Ok(()),
109 DeleteProject::NotLoggedIn => {
110 Err(anyhow!("control rejected token; run `fn0 login` again."))
111 }
112 DeleteProject::NotFound => Err(anyhow!(
113 "project '{project_id}' not found or not owned by you."
114 )),
115 DeleteProject::InternalError => Err(anyhow!(
116 "delete_project: server error; check fn0-control logs"
117 )),
118 }
119}
120
121pub async fn rename_project(project_id: &str, new_name: &str) -> Result<()> {
122 let creds = crate::credentials::require()?;
123 let client = reqwest::Client::new();
124 let url = format!(
125 "{}/__forte_action/rename_project",
126 creds.control_url.trim_end_matches('/')
127 );
128 let resp = client
129 .post(&url)
130 .bearer_auth(&creds.token)
131 .json(&RenameProjectInput {
132 project_id,
133 new_name,
134 })
135 .send()
136 .await?
137 .error_for_status()?;
138 let raw: RenameProject = resp.json().await?;
139 match raw {
140 RenameProject::Ok => Ok(()),
141 RenameProject::NotLoggedIn => Err(anyhow!("control rejected token; run `fn0 login` again.")),
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}