1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
use super::client::BuildError;

const DEFAULT_POLLING_BASE_URL: &str = "https://sdk.launchdarkly.com";
const DEFAULT_STREAM_BASE_URL: &str = "https://stream.launchdarkly.com";
const DEFAULT_EVENTS_BASE_URL: &str = "https://events.launchdarkly.com";

/// Specifies the base service URLs used by SDK components.
pub struct ServiceEndpoints {
    polling_base_url: String,
    streaming_base_url: String,
    events_base_url: String,
}

impl ServiceEndpoints {
    pub fn polling_base_url(&self) -> &str {
        self.polling_base_url.as_ref()
    }

    pub fn streaming_base_url(&self) -> &str {
        self.streaming_base_url.as_ref()
    }

    pub fn events_base_url(&self) -> &str {
        self.events_base_url.as_ref()
    }
}

/// Used for configuring the SDKs service URLs.
///
/// The default behavior, if you do not change any of these properties, is that the SDK will connect
/// to the standard endpoints in the LaunchDarkly production service. There are several use cases for
/// changing these properties:
///
/// - You are using the <a href="https://docs.launchdarkly.com/home/advanced/relay-proxy">LaunchDarkly
/// Relay Proxy</a>. In this case, use [ServiceEndpointsBuilder::relay_proxy] with the URL of the relay proxy instance.
///
/// - You are connecting to a private instance of LaunchDarkly, rather than the standard production
/// services. In this case, there will be custom base URIs for each service. You need to configure
/// each endpoint with [ServiceEndpointsBuilder::polling_base_url],
/// [ServiceEndpointsBuilder::streaming_base_url], and [ServiceEndpointsBuilder::events_base_url].
///
/// - You are connecting to a test fixture that simulates the service endpoints. In this case, you
/// may set the base URLs to whatever you want, although the SDK will still set the URL paths to
/// the expected paths for LaunchDarkly services.
///
/// # Examples
///
/// Configure for a Relay Proxy instance.
/// ```
/// # use launchdarkly_server_sdk::{ServiceEndpointsBuilder, ConfigBuilder};
/// # fn main() {
///     ConfigBuilder::new("sdk-key").service_endpoints(ServiceEndpointsBuilder::new()
///         .relay_proxy("http://my-relay-hostname:8080"));
/// # }
/// ```
///
/// Configure for a private LaunchDarkly instance.
/// ```
/// # use launchdarkly_server_sdk::{ServiceEndpointsBuilder, ConfigBuilder};
/// # fn main() {
///    ConfigBuilder::new("sdk-key").service_endpoints(ServiceEndpointsBuilder::new()
///         .polling_base_url("https://sdk.my-private-instance.com")
///         .streaming_base_url("https://stream.my-private-instance.com")
///         .events_base_url("https://events.my-private-instance.com"));
/// # }
/// ```
#[derive(Clone)]
pub struct ServiceEndpointsBuilder {
    polling_base_url: Option<String>,
    streaming_base_url: Option<String>,
    events_base_url: Option<String>,
}

impl ServiceEndpointsBuilder {
    /// Create a new instance of [ServiceEndpointsBuilder] with no URLs specified.
    pub fn new() -> ServiceEndpointsBuilder {
        ServiceEndpointsBuilder {
            polling_base_url: None,
            streaming_base_url: None,
            events_base_url: None,
        }
    }

    /// Sets a custom base URL for the polling service.
    pub fn polling_base_url(&mut self, url: &str) -> &mut Self {
        self.polling_base_url = Some(String::from(url));
        self
    }

    /// Sets a custom base URL for the streaming service.
    pub fn streaming_base_url(&mut self, url: &str) -> &mut Self {
        self.streaming_base_url = Some(String::from(url));
        self
    }

    /// Sets a custom base URI for the events service.
    pub fn events_base_url(&mut self, url: &str) -> &mut Self {
        self.events_base_url = Some(String::from(url));
        self
    }

    /// Specifies a single base URL for a Relay Proxy instance.
    pub fn relay_proxy(&mut self, url: &str) -> &mut Self {
        self.polling_base_url = Some(String::from(url));
        self.streaming_base_url = Some(String::from(url));
        self.events_base_url = Some(String::from(url));
        self
    }

    /// Called internally by the SDK to create a configuration instance. Applications do not need
    /// to call this method.
    ///
    /// # Errors
    ///
    /// When using custom endpoints it is important that all of the URLs are set.
    /// If some URLs are set, but others are not, then this will return an error.
    /// If no URLs are set, then the default values will be used.
    /// This prevents a combination of custom and default values from being used.
    pub fn build(&self) -> Result<ServiceEndpoints, BuildError> {
        match (
            &self.polling_base_url,
            &self.streaming_base_url,
            &self.events_base_url,
        ) {
            (Some(polling_base_url), Some(streaming_base_url), Some(events_base_url)) => {
                Ok(ServiceEndpoints {
                    polling_base_url: polling_base_url.trim_end_matches('/').to_string(),
                    streaming_base_url: streaming_base_url.trim_end_matches('/').to_string(),
                    events_base_url: events_base_url.trim_end_matches('/').to_string(),
                })
            }
            (None, None, None) => Ok(ServiceEndpoints {
                polling_base_url: String::from(DEFAULT_POLLING_BASE_URL),
                streaming_base_url: String::from(DEFAULT_STREAM_BASE_URL),
                events_base_url: String::from(DEFAULT_EVENTS_BASE_URL),
            }),
            _ => Err(BuildError::InvalidConfig(
                "If you specify any endpoints,\
             then you must specify all endpoints."
                    .into(),
            )),
        }
    }
}

impl Default for ServiceEndpointsBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_case::test_case;

    #[test_case("localhost", "localhost"; "requires no trimming")]
    #[test_case("http://localhost", "http://localhost"; "requires no trimming with scheme")]
    #[test_case("localhost/", "localhost"; "trims trailing slash")]
    #[test_case("http://localhost/", "http://localhost"; "trims trailing slash with scheme")]
    #[test_case("localhost////////", "localhost"; "trims multiple trailing slashes")]
    fn service_endpoints_trims_base_urls(url: &str, expected: &str) {
        let endpoints = ServiceEndpointsBuilder::new()
            .polling_base_url(url)
            .streaming_base_url(url)
            .events_base_url(url)
            .build()
            .expect("Provided URLs should parse successfully");

        assert_eq!(expected, endpoints.events_base_url());
        assert_eq!(expected, endpoints.streaming_base_url());
        assert_eq!(expected, endpoints.polling_base_url());
    }

    #[test]
    fn default_configuration() -> Result<(), BuildError> {
        let endpoints = ServiceEndpointsBuilder::new().build()?;
        assert_eq!(
            "https://events.launchdarkly.com",
            endpoints.events_base_url()
        );
        assert_eq!(
            "https://stream.launchdarkly.com",
            endpoints.streaming_base_url()
        );
        assert_eq!("https://sdk.launchdarkly.com", endpoints.polling_base_url());
        Ok(())
    }

    #[test]
    fn full_custom_configuration() -> Result<(), BuildError> {
        let endpoints = ServiceEndpointsBuilder::new()
            .polling_base_url("https://sdk.my-private-instance.com")
            .streaming_base_url("https://stream.my-private-instance.com")
            .events_base_url("https://events.my-private-instance.com")
            .build()?;

        assert_eq!(
            "https://events.my-private-instance.com",
            endpoints.events_base_url()
        );
        assert_eq!(
            "https://stream.my-private-instance.com",
            endpoints.streaming_base_url()
        );
        assert_eq!(
            "https://sdk.my-private-instance.com",
            endpoints.polling_base_url()
        );
        Ok(())
    }

    #[test]
    fn partial_definition() {
        assert!(ServiceEndpointsBuilder::new()
            .polling_base_url("https://sdk.my-private-instance.com")
            .build()
            .is_err());
    }

    #[test]
    fn configure_relay_proxy() -> Result<(), BuildError> {
        let endpoints = ServiceEndpointsBuilder::new()
            .relay_proxy("http://my-relay-hostname:8080")
            .build()?;
        assert_eq!("http://my-relay-hostname:8080", endpoints.events_base_url());
        assert_eq!(
            "http://my-relay-hostname:8080",
            endpoints.streaming_base_url()
        );
        assert_eq!(
            "http://my-relay-hostname:8080",
            endpoints.polling_base_url()
        );
        Ok(())
    }
}