Skip to main content

platform_provider/
config.rs

1#[derive(Clone, PartialEq, Eq)]
2pub struct ProviderConfig {
3    pub name: String,
4    pub export_key: String,
5    pub base_url: String,
6    pub transport: ProviderTransport,
7    pub(crate) auth_token: Option<String>,
8    pub timeout_ms: u64,
9    pub(crate) service_release_digest: Option<String>,
10    pub(crate) module_release_digest: Option<String>,
11    pub(crate) manifest_digest: Option<String>,
12    pub(crate) contract_digests: Vec<String>,
13    pub(crate) allowed_host_function_names: Vec<String>,
14}
15
16impl std::fmt::Debug for ProviderConfig {
17    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        formatter
19            .debug_struct("ProviderConfig")
20            .field("name", &self.name)
21            .field("export_key", &self.export_key)
22            .field("base_url", &self.base_url)
23            .field("transport", &self.transport)
24            .field("auth_configured", &self.auth_token.is_some())
25            .field("timeout_ms", &self.timeout_ms)
26            .field("locked", &self.service_release_digest.is_some())
27            .finish()
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ProviderTransport {
33    HttpJson,
34    Grpc,
35}
36
37impl ProviderConfig {
38    #[must_use]
39    pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
40        let (transport, base_url) = normalize_base_url(base_url.into());
41        Self {
42            name: name.into(),
43            export_key: String::new(),
44            base_url,
45            transport,
46            auth_token: None,
47            timeout_ms: 5_000,
48            service_release_digest: None,
49            module_release_digest: None,
50            manifest_digest: None,
51            contract_digests: Vec::new(),
52            allowed_host_function_names: Vec::new(),
53        }
54    }
55
56    #[must_use]
57    pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
58        self.auth_token = Some(token.into());
59        self
60    }
61
62    #[must_use]
63    pub fn auth_configured(&self) -> bool {
64        self.auth_token.is_some()
65    }
66
67    #[must_use]
68    pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
69        self.timeout_ms = timeout_ms;
70        self
71    }
72
73    #[must_use]
74    pub(crate) fn with_transport(
75        mut self,
76        transport: ProviderTransport,
77        base_url: impl Into<String>,
78    ) -> Self {
79        self.transport = transport;
80        self.base_url = base_url.into().trim_end_matches('/').to_owned();
81        self
82    }
83
84    #[must_use]
85    pub(crate) fn with_export_key(mut self, export_key: impl Into<String>) -> Self {
86        self.export_key = export_key.into();
87        self
88    }
89
90    #[must_use]
91    pub(crate) fn with_locked_contract(
92        mut self,
93        service_release_digest: impl Into<String>,
94        module_release_digest: impl Into<String>,
95        manifest_digest: impl Into<String>,
96        contract_digests: Vec<String>,
97    ) -> Self {
98        self.service_release_digest = Some(service_release_digest.into());
99        self.module_release_digest = Some(module_release_digest.into());
100        self.manifest_digest = Some(manifest_digest.into());
101        self.contract_digests = contract_digests;
102        self
103    }
104
105    #[must_use]
106    pub(crate) fn with_allowed_host_functions(
107        mut self,
108        function_names: impl IntoIterator<Item = String>,
109    ) -> Self {
110        self.allowed_host_function_names = function_names.into_iter().collect();
111        self.allowed_host_function_names.sort();
112        self.allowed_host_function_names.dedup();
113        self
114    }
115
116    /// Matches the canonical manifest identity to this legacy runtime source.
117    ///
118    /// Standalone provider sources predate fully-qualified Module IDs, so their
119    /// host-local source key remains a path-safe slug. Service-provided modules
120    /// use their full Module ID as the source key and therefore match exactly.
121    #[must_use]
122    pub fn matches_module_id(&self, module_id: &str) -> bool {
123        self.name == module_id
124            || module_id
125                .rsplit_once('/')
126                .is_some_and(|(_, slug)| slug == self.name)
127    }
128}
129
130fn normalize_base_url(base_url: String) -> (ProviderTransport, String) {
131    let trimmed = base_url.trim().trim_end_matches('/');
132    match trimmed.strip_prefix("grpc://") {
133        Some(rest) => (
134            ProviderTransport::Grpc,
135            format!("http://{}", rest.trim_end_matches('/')),
136        ),
137        None => match trimmed.strip_prefix("grpcs://") {
138            Some(rest) => (
139                ProviderTransport::Grpc,
140                format!("https://{}", rest.trim_end_matches('/')),
141            ),
142            None => (ProviderTransport::HttpJson, trimmed.to_owned()),
143        },
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn defaults_to_http_json_transport() {
153        let config =
154            ProviderConfig::new("provider-crm", "http://127.0.0.1:4100/lenso/provider/v1/");
155
156        assert_eq!(config.transport, ProviderTransport::HttpJson);
157        assert_eq!(config.base_url, "http://127.0.0.1:4100/lenso/provider/v1");
158    }
159
160    #[test]
161    fn grpc_scheme_selects_grpc_transport() {
162        let config = ProviderConfig::new("provider-crm", "grpc://127.0.0.1:50051/");
163
164        assert_eq!(config.transport, ProviderTransport::Grpc);
165        assert_eq!(config.base_url, "http://127.0.0.1:50051");
166    }
167
168    #[test]
169    fn grpcs_scheme_selects_grpc_transport_with_tls_endpoint() {
170        let config = ProviderConfig::new("provider-crm", "grpcs://provider.example.test:50051/");
171
172        assert_eq!(config.transport, ProviderTransport::Grpc);
173        assert_eq!(config.base_url, "https://provider.example.test:50051");
174    }
175}