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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
// OPCUA for Rust
// SPDX-License-Identifier: MPL-2.0
// Copyright (C) 2017-2022 Adam Lock

use std::path::PathBuf;

use opcua_core::config::Config;

use crate::{client::*, config::*};

/// The `ClientBuilder` is a builder for producing a [`Client`]. It is an alternative to constructing
/// a [`ClientConfig`] from file or from scratch.
///
/// # Example
///
/// ```no_run
/// use opcua_client::prelude::*;
///
/// fn main() {
///     let builder = ClientBuilder::new()
///         .application_name("OPC UA Sample Client")
///         .application_uri("urn:SampleClient")
///         .pki_dir("./pki")
///         .endpoints(vec![
///             ("sample_endpoint", ClientEndpoint {
///                 url: String::from("opc.tcp://127.0.0.1:4855/"),
///                 security_policy: String::from(SecurityPolicy::None.to_str()),
///                 security_mode: String::from(MessageSecurityMode::None),
///                 user_token_id: ANONYMOUS_USER_TOKEN_ID.to_string(),
///             }),
///         ])
///         .default_endpoint("sample_endpoint")
///         .create_sample_keypair(true)
///         .trust_server_certs(true)
///         .user_token("sample_user", ClientUserToken::user_pass("sample1", "sample1pwd"));
///     let client = builder.client().unwrap();
/// }
/// ```
///
/// [`Client`]: ../client/struct.Client.html
/// [`ClientConfig`]: ../config/struct.ClientConfig.html
///
pub struct ClientBuilder {
    config: ClientConfig,
}

impl Default for ClientBuilder {
    fn default() -> Self {
        ClientBuilder {
            config: ClientConfig::default(),
        }
    }
}

impl ClientBuilder {
    /// Creates a `ClientBuilder`
    pub fn new() -> ClientBuilder {
        ClientBuilder::default()
    }

    /// Creates a `ClientBuilder` using a configuration file as the initial state.
    pub fn from_config<T>(path: T) -> Result<ClientBuilder, ()>
    where
        T: Into<PathBuf>,
    {
        Ok(ClientBuilder {
            config: ClientConfig::load(&path.into())?,
        })
    }

    /// Yields a [`Client`] from the values set by the builder. If the builder is not in a valid state
    /// it will return `None`.
    ///
    /// [`Client`]: ../client/struct.Client.html
    pub fn client(self) -> Option<Client> {
        if self.is_valid() {
            Some(Client::new(self.config))
        } else {
            None
        }
    }

    /// Yields a [`ClientConfig`] from the values set by the builder.
    ///
    /// [`ClientConfig`]: ../config/struct.ClientConfig.html
    pub fn config(self) -> ClientConfig {
        self.config
    }

    /// Tests if the builder is in a valid state to be able to yield a `Client`.
    pub fn is_valid(&self) -> bool {
        self.config.is_valid()
    }

    /// Sets the application name.
    pub fn application_name<T>(mut self, application_name: T) -> Self
    where
        T: Into<String>,
    {
        self.config.application_name = application_name.into();
        self
    }

    /// Sets the application uri
    pub fn application_uri<T>(mut self, application_uri: T) -> Self
    where
        T: Into<String>,
    {
        self.config.application_uri = application_uri.into();
        self
    }

    /// Sets the product uri.
    pub fn product_uri<T>(mut self, product_uri: T) -> Self
    where
        T: Into<String>,
    {
        self.config.product_uri = product_uri.into();
        self
    }

    /// Sets whether the client should generate its own key pair if there is none found in the pki
    /// directory.
    pub fn create_sample_keypair(mut self, create_sample_keypair: bool) -> Self {
        self.config.create_sample_keypair = create_sample_keypair;
        self
    }

    /// Sets a custom client certificate path. The path is required to be provided as a partial
    /// path relative to the PKI directory. If set, this path will be used to read the client
    /// certificate from disk. The certificate can be in either the .der or .pem format.
    pub fn certificate_path<T>(mut self, certificate_path: T) -> Self
    where
        T: Into<PathBuf>,
    {
        self.config.certificate_path = Some(certificate_path.into());
        self
    }

    /// Sets a custom private key path. The path is required to be provided as a partial path
    /// relative to the PKI directory. If set, this path will be used to read the private key
    /// from disk.
    pub fn private_key_path<T>(mut self, private_key_path: T) -> Self
    where
        T: Into<PathBuf>,
    {
        self.config.private_key_path = Some(private_key_path.into());
        self
    }

    /// Sets whether the client should automatically trust servers. If this is not set then
    /// the client will reject the server upon first connect and the server's certificate
    /// must be manually moved from pki's `/rejected` folder to the `/trusted` folder. If it is
    /// set, then the server cert will automatically be stored in the `/trusted` folder.
    pub fn trust_server_certs(mut self, trust_server_certs: bool) -> Self {
        self.config.trust_server_certs = trust_server_certs;
        self
    }

    /// Sets whether the client should verify server certificates. Regardless of this setting,
    /// server certificates are always checked to see if they are trusted and have a valid key
    /// length. In addition (if `verify_server_certs` is unset or is set to `true`) it will
    /// verify the hostname, application uri and the not before / after values to ensure validity.
    pub fn verify_server_certs(mut self, verify_server_certs: bool) -> Self {
        self.config.verify_server_certs = verify_server_certs;
        self
    }

    /// Sets the pki directory where client's own key pair is stored and where `/trusted` and
    /// `/rejected` server certificates are stored.
    pub fn pki_dir<T>(mut self, pki_dir: T) -> Self
    where
        T: Into<PathBuf>,
    {
        self.config.pki_dir = pki_dir.into();
        self
    }

    /// Sets the preferred locales of the client. These are passed to the server during session
    /// creation to ensure localized strings are in the preferred language.
    pub fn preferred_locales(mut self, preferred_locales: Vec<String>) -> Self {
        self.config.preferred_locales = preferred_locales;
        self
    }

    /// Sets the id of the default endpoint to connect to.
    pub fn default_endpoint<T>(mut self, endpoint_id: T) -> Self
    where
        T: Into<String>,
    {
        self.config.default_endpoint = endpoint_id.into();
        self
    }

    /// Adds an endpoint to the list of endpoints the client knows of.
    pub fn endpoint<T>(mut self, endpoint_id: T, endpoint: ClientEndpoint) -> Self
    where
        T: Into<String>,
    {
        self.config.endpoints.insert(endpoint_id.into(), endpoint);
        self
    }

    /// Adds multiple endpoints to the list of endpoints the client knows of.
    pub fn endpoints<T>(mut self, endpoints: Vec<(T, ClientEndpoint)>) -> Self
    where
        T: Into<String>,
    {
        for e in endpoints {
            self.config.endpoints.insert(e.0.into(), e.1);
        }
        self
    }

    /// Adds a user token to the list supported by the client.
    pub fn user_token<T>(mut self, user_token_id: T, user_token: ClientUserToken) -> Self
    where
        T: Into<String>,
    {
        let user_token_id = user_token_id.into();
        if user_token_id == ANONYMOUS_USER_TOKEN_ID {
            panic!("User token id {} is reserved", user_token_id);
        }
        self.config.user_tokens.insert(user_token_id, user_token);
        self
    }

    /// Sets the session retry limit.
    pub fn session_retry_limit(mut self, session_retry_limit: i32) -> Self {
        if session_retry_limit < 0 && session_retry_limit != -1 {
            panic!("Session retry limit must be -1, 0 or a positive number");
        }
        self.config.session_retry_limit = session_retry_limit;
        self
    }

    /// Sets the session retry interval.
    pub fn session_retry_interval(mut self, session_retry_interval: u32) -> Self {
        self.config.session_retry_interval = session_retry_interval;
        self
    }

    /// Sets the session timeout period.
    pub fn session_timeout(mut self, session_timeout: u32) -> Self {
        self.config.session_timeout = session_timeout;
        self
    }

    /// Sets whether the client should ignore clock skew so the client can make a successful
    /// connection to the server, even when the client and server clocks are out of sync.
    pub fn ignore_clock_skew(mut self) -> Self {
        self.config.performance.ignore_clock_skew = true;
        self
    }

    /// Configures the client to use a single-threaded executor. This reduces the number of
    /// threads used by the client.
    pub fn single_threaded_executor(mut self) -> Self {
        self.config.performance.single_threaded_executor = true;
        self
    }

    /// Configures the client to use a multi-threaded executor.
    pub fn multi_threaded_executor(mut self) -> Self {
        self.config.performance.single_threaded_executor = false;
        self
    }

    /// Session name - the default name to use for a new session
    pub fn session_name<T>(mut self, session_name: T) -> Self
    where
        T: Into<String>,
    {
        self.config.session_name = session_name.into();
        self
    }
}

#[test]
fn client_builder() {
    use std::str::FromStr;

    // The builder should produce a config that reflects the values that are explicitly set upon it.
    let b = ClientBuilder::new()
        .application_name("appname")
        .application_uri("http://appname")
        .product_uri("http://product")
        .create_sample_keypair(true)
        .certificate_path("certxyz")
        .private_key_path("keyxyz")
        .trust_server_certs(true)
        .verify_server_certs(false)
        .pki_dir("pkixyz")
        .preferred_locales(vec!["a".to_string(), "b".to_string(), "c".to_string()])
        .default_endpoint("http://default")
        .session_retry_interval(1234)
        .session_retry_limit(999)
        .session_timeout(777)
        .ignore_clock_skew()
        .single_threaded_executor()
        .session_name("SessionName")
        // TODO user tokens, endpoints
        ;

    let c = b.config();

    assert_eq!(c.application_name, "appname");
    assert_eq!(c.application_uri, "http://appname");
    assert_eq!(c.product_uri, "http://product");
    assert_eq!(c.create_sample_keypair, true);
    assert_eq!(c.certificate_path, Some(PathBuf::from("certxyz")));
    assert_eq!(c.private_key_path, Some(PathBuf::from("keyxyz")));
    assert_eq!(c.trust_server_certs, true);
    assert_eq!(c.verify_server_certs, false);
    assert_eq!(c.pki_dir, PathBuf::from_str("pkixyz").unwrap());
    assert_eq!(
        c.preferred_locales,
        vec!["a".to_string(), "b".to_string(), "c".to_string()]
    );
    assert_eq!(c.default_endpoint, "http://default");
    assert_eq!(c.session_retry_interval, 1234);
    assert_eq!(c.session_retry_limit, 999);
    assert_eq!(c.session_timeout, 777);
    assert_eq!(c.performance.ignore_clock_skew, true);
    assert_eq!(c.performance.single_threaded_executor, true);
    assert_eq!(c.session_name, "SessionName");
}