main
rs 136 lines 4.22 KB
Raw
1 //! Which sigit.si deployment a client talks to, and where its local state lives.
2
3 use std::{fmt, str::FromStr};
4
5 /// The deployment a [`crate::Client`] points at.
6 ///
7 /// `SIGIT_HOST` overrides the host for either variant, so a client can be aimed
8 /// at a staging box or a tunnel without a code change:
9 ///
10 /// ```text
11 /// SIGIT_HOST=sigit.example.com si repo list
12 /// SIGIT_HOST=http://localhost:4000 si repo list
13 /// ```
14 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
15 pub enum Environment {
16 /// A local Rails server (`bin/rails s`), http on port 3000.
17 Dev,
18 /// https://sigit.si
19 #[default]
20 Production,
21 }
22
23 impl Environment {
24 /// Scheme + host, no trailing slash — e.g. `https://sigit.si`.
25 ///
26 /// Both the JSON API and the web UI live on this origin, so it doubles as
27 /// the base for browser URLs and `.git` clone URLs.
28 pub fn base_url(&self) -> String {
29 match std::env::var("SIGIT_HOST") {
30 Ok(host) => normalize_host(host.trim(), self.default_scheme()),
31 Err(_) => match self {
32 Environment::Dev => "http://localhost:3000".to_string(),
33 Environment::Production => "https://sigit.si".to_string(),
34 },
35 }
36 }
37
38 /// Base for JSON API calls — e.g. `https://sigit.si/api/v1`.
39 pub fn api_url(&self) -> String {
40 format!("{}/api/v1", self.base_url())
41 }
42
43 /// Directory under `$HOME` holding the token and config for this
44 /// environment. Dev gets its own so a local login can't clobber the real
45 /// one.
46 pub fn config_dir_name(&self) -> &'static str {
47 match self {
48 Environment::Dev => ".sigit-dev",
49 Environment::Production => ".sigit",
50 }
51 }
52
53 fn default_scheme(&self) -> &'static str {
54 match self {
55 Environment::Dev => "http",
56 Environment::Production => "https",
57 }
58 }
59 }
60
61 /// Accept `SIGIT_HOST` with or without a scheme, and strip any trailing slash.
62 fn normalize_host(host: &str, default_scheme: &str) -> String {
63 let with_scheme = if host.contains("://") {
64 host.to_string()
65 } else {
66 format!("{default_scheme}://{host}")
67 };
68 with_scheme.trim_end_matches('/').to_string()
69 }
70
71 impl fmt::Display for Environment {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 let name = match self {
74 Environment::Dev => "dev",
75 Environment::Production => "production",
76 };
77 f.write_str(name)
78 }
79 }
80
81 impl FromStr for Environment {
82 type Err = String;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 match s.to_ascii_lowercase().as_str() {
86 "dev" | "development" | "local" => Ok(Environment::Dev),
87 "production" | "prod" => Ok(Environment::Production),
88 other => Err(format!(
89 "Unknown environment {other:?}. Valid values are: dev, production."
90 )),
91 }
92 }
93 }
94
95 #[cfg(test)]
96 mod tests {
97 use super::*;
98
99 #[test]
100 fn api_url_is_the_base_plus_the_version_prefix() {
101 // SIGIT_HOST is process-global; this asserts the un-overridden default,
102 // which is what every test process sees unless one sets the var.
103 if std::env::var("SIGIT_HOST").is_ok() {
104 return;
105 }
106 assert_eq!(Environment::Production.api_url(), "https://sigit.si/api/v1");
107 assert_eq!(Environment::Dev.api_url(), "http://localhost:3000/api/v1");
108 }
109
110 #[test]
111 fn bare_hosts_get_the_environment_default_scheme() {
112 assert_eq!(
113 normalize_host("sigit.example.com", "https"),
114 "https://sigit.example.com"
115 );
116 assert_eq!(
117 normalize_host("localhost:4000", "http"),
118 "http://localhost:4000"
119 );
120 }
121
122 #[test]
123 fn explicit_schemes_and_trailing_slashes_survive_normalization() {
124 assert_eq!(
125 normalize_host("http://localhost:4000/", "https"),
126 "http://localhost:4000"
127 );
128 }
129
130 #[test]
131 fn environments_round_trip_through_strings() {
132 assert_eq!("dev".parse(), Ok(Environment::Dev));
133 assert_eq!("PROD".parse(), Ok(Environment::Production));
134 assert!("staging".parse::<Environment>().is_err());
135 }
136 }