1use anyhow::{Result, anyhow};
8
9use crate::domain::{
10 CloudflareStatus, DomainStatus, fetch_domain_status, format_cloudflare_status,
11};
12
13pub struct ResolvedAppUrl {
14 pub url: String,
15 pub note: Option<String>,
16}
17
18pub fn default_app_url(control_url: &str, project_id: &str) -> Result<String> {
19 let mut url = reqwest::Url::parse(control_url)
20 .map_err(|e| anyhow!("control URL '{control_url}' is not a valid URL: {e}"))?;
21 let host = url
22 .host_str()
23 .ok_or_else(|| anyhow!("control URL '{control_url}' has no host"))?;
24 let app_host = format!("{project_id}.{host}");
25 url.set_host(Some(&app_host))
26 .map_err(|e| anyhow!("could not build app host '{app_host}': {e}"))?;
27 url.set_path("");
28 Ok(url.to_string().trim_end_matches('/').to_string())
29}
30
31pub async fn resolve_app_url(project_id: &str) -> Result<ResolvedAppUrl> {
32 let creds = crate::credentials::load()?;
33 let control_url = creds
34 .as_ref()
35 .map(|c| c.control_url.clone())
36 .unwrap_or_else(crate::credentials::default_control_url);
37 let default_url = default_app_url(&control_url, project_id)?;
38
39 let Some(creds) = creds else {
40 return Ok(ResolvedAppUrl {
41 url: default_url,
42 note: Some("not signed in, so a custom domain could not be checked".to_string()),
43 });
44 };
45
46 let fallback = |note: String| ResolvedAppUrl {
47 url: default_url.clone(),
48 note: Some(note),
49 };
50
51 Ok(match fetch_domain_status(&creds, project_id).await {
52 Ok(DomainStatus::Configured {
53 domain,
54 cloudflare_status: CloudflareStatus::Active,
55 }) => ResolvedAppUrl {
56 url: format!("https://{domain}"),
57 note: None,
58 },
59 Ok(DomainStatus::Configured {
60 domain,
61 cloudflare_status,
62 }) => fallback(format!(
63 "custom domain '{domain}' is not serving yet ({}), so the default subdomain is used",
64 format_cloudflare_status(&cloudflare_status)
65 )),
66 Ok(DomainStatus::NotConfigured) => ResolvedAppUrl {
67 url: default_url,
68 note: None,
69 },
70 Ok(DomainStatus::NotLoggedIn) => {
71 fallback("control rejected the saved token; run `forte login` again".to_string())
72 }
73 Ok(DomainStatus::NotFound) => fallback(format!(
74 "project '{project_id}' was not found under your account; nothing may be deployed there"
75 )),
76 Ok(DomainStatus::InternalError) => {
77 fallback("control failed to report the custom domain".to_string())
78 }
79 Err(e) => fallback(format!(
80 "could not reach control to check a custom domain: {e}"
81 )),
82 })
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn builds_subdomain_under_the_control_host() {
91 assert_eq!(
92 default_app_url("https://fn0.dev", "abc123").unwrap(),
93 "https://abc123.fn0.dev"
94 );
95 }
96
97 #[test]
98 fn keeps_scheme_and_port_of_a_self_hosted_control() {
99 assert_eq!(
100 default_app_url("http://localhost:3000", "abc123").unwrap(),
101 "http://abc123.localhost:3000"
102 );
103 }
104
105 #[test]
106 fn drops_a_trailing_path_on_the_control_url() {
107 assert_eq!(
108 default_app_url("https://fn0.dev/", "abc123").unwrap(),
109 "https://abc123.fn0.dev"
110 );
111 }
112}