kellnr_settings/
origin.rs1use provcfg::{ClapArgs, Configurable};
2use serde::{Deserialize, Serialize};
3
4use crate::protocol::Protocol;
5
6fn default_hostname() -> String {
7 "127.0.0.1".to_string()
8}
9
10fn default_origin_port() -> u16 {
11 std::env::var("KELLNR_ORIGIN__PORT")
12 .ok()
13 .and_then(|v| v.parse::<u16>().ok())
14 .unwrap_or(8000)
15}
16
17#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
18#[serde(default)]
19#[configurable(clap_prefix = "origin")]
20pub struct Origin {
21 pub hostname: String,
23
24 pub port: u16,
26
27 #[arg(skip)]
29 pub protocol: Protocol,
30
31 pub path: String,
33}
34
35impl Default for Origin {
36 fn default() -> Self {
37 Self {
38 hostname: default_hostname(),
39 port: default_origin_port(),
40 protocol: Protocol::Http,
41 path: String::new(),
42 }
43 }
44}
45
46impl Origin {
47 pub fn base_url(&self) -> String {
48 let path_prefix = self.path.trim().trim_end_matches('/');
49 if self.port == 443 || self.port == 80 {
50 format!("{}://{}{}", self.protocol, self.hostname, path_prefix)
51 } else {
52 format!(
53 "{}://{}:{}{}",
54 self.protocol, self.hostname, self.port, path_prefix
55 )
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 fn origin(port: u16, path: &str) -> Origin {
65 Origin {
66 hostname: "kellnr.example.com".to_string(),
67 port,
68 protocol: Protocol::Https,
69 path: path.to_string(),
70 }
71 }
72
73 #[test]
74 fn base_url_omits_default_ports() {
75 assert_eq!(
76 origin(443, "").base_url(),
77 "https://kellnr.example.com",
78 "443 is implied by https and must not be spelled out"
79 );
80
81 let mut http = origin(80, "");
82 http.protocol = Protocol::Http;
83 assert_eq!(http.base_url(), "http://kellnr.example.com");
84 }
85
86 #[test]
87 fn base_url_includes_non_default_port() {
88 assert_eq!(
89 origin(8000, "").base_url(),
90 "https://kellnr.example.com:8000"
91 );
92 }
93
94 #[test]
95 fn base_url_appends_path_prefix() {
96 assert_eq!(
97 origin(8000, "/registry").base_url(),
98 "https://kellnr.example.com:8000/registry"
99 );
100 assert_eq!(
101 origin(443, "/registry").base_url(),
102 "https://kellnr.example.com/registry"
103 );
104 }
105
106 #[test]
109 fn base_url_normalizes_slashes_in_path_prefix() {
110 for path in ["", "/", " / ", "/registry/", "/registry//"] {
111 let url = origin(8000, path).base_url();
112 assert!(
113 !url.ends_with('/'),
114 "base_url for path {path:?} must not end in a slash, got {url}"
115 );
116 }
117
118 assert_eq!(
119 origin(8000, "/registry/").base_url(),
120 "https://kellnr.example.com:8000/registry"
121 );
122 }
123
124 #[test]
127 fn base_url_composes_into_callback_url() {
128 for path in ["", "/", "/registry", "/registry/"] {
129 let url = format!("{}/api/v1/oauth2/callback", origin(443, path).base_url());
130 assert!(
131 !url.contains("com//") && !url.contains("registry//"),
132 "callback URL for path {path:?} has a double slash: {url}"
133 );
134 }
135 }
136}