haveapi-rust-client 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ require 'json'
2
+ require 'digest'
3
+ require 'uri'
4
+
5
+ module HaveAPI::RustClient
6
+ module Snapshot
7
+ KEYS = %w[schema_format api_version protocol_version source_url profile mount_path description].freeze
8
+ PROSE = %w[description label examples message comment title].freeze
9
+
10
+ def self.canonical(value)
11
+ normalized = case value
12
+ when Hash
13
+ value.keys.map(&:to_s).sort.to_h do |key|
14
+ [key, canonical_value(value.fetch(key))]
15
+ end
16
+ else
17
+ canonical_value(value)
18
+ end
19
+ "#{JSON.pretty_generate(normalized)}\n"
20
+ end
21
+
22
+ def self.canonical_value(value)
23
+ case value
24
+ when Hash
25
+ value.keys.sort.to_h { |key| [key, canonical_value(value.fetch(key))] }
26
+ when Array
27
+ value.map { |item| canonical_value(item) }
28
+ else
29
+ value
30
+ end
31
+ end
32
+
33
+ def self.sha256(snapshot)
34
+ Digest::SHA256.hexdigest(canonical(snapshot))
35
+ end
36
+
37
+ def self.load(path)
38
+ validate(JSON.parse(File.read(path)))
39
+ rescue JSON::ParserError
40
+ raise Error, 'snapshot is not valid JSON'
41
+ end
42
+
43
+ def self.fetch(url:, api_version:, profile:, diagnostics: [])
44
+ require 'haveapi/client'
45
+ version!(api_version)
46
+ endpoint = endpoint!(url)
47
+ suffix = "/v#{api_version}/"
48
+ raise Error, 'fetch URL must select the exact requested version' unless endpoint.path.end_with?(suffix)
49
+
50
+ mount = endpoint.path.delete_suffix("v#{api_version}/")
51
+ # The shared metadata client owns parsing/protocol compatibility. Keep the
52
+ # explicit version request at the supplied mount and disable redirects.
53
+ root = endpoint.dup
54
+ root.path = mount
55
+ communicator = HaveAPI::Client::Communicator.new(root.to_s)
56
+ communicator.instance_variable_set(
57
+ :@rest,
58
+ RestClient::Resource.new(root.to_s, max_redirects: 0, open_timeout: 10, read_timeout: 30)
59
+ )
60
+ description = communicator.describe_api(api_version)
61
+ from_description(
62
+ description: JSON.parse(JSON.generate(description)),
63
+ api_version:, protocol_version: communicator.instance_variable_get(:@proto_version),
64
+ source_url: url, profile:, mount_path: mount, diagnostics:
65
+ )
66
+ rescue HaveAPI::Client::ProtocolError
67
+ raise Error, 'incompatible HaveAPI protocol version'
68
+ rescue RestClient::Exception, URI::InvalidURIError
69
+ raise Error, 'metadata acquisition failed (HTTP, redirect or URL error)'
70
+ end
71
+
72
+ def self.from_envelope(envelope, api_version:, source_url:, profile:, mount_path: '/', diagnostics: [])
73
+ raise Error, 'metadata envelope is not successful' unless envelope['status'] == true
74
+
75
+ from_description(
76
+ description: envelope.fetch('response'), protocol_version: envelope.fetch('version'),
77
+ api_version:, source_url:, profile:, mount_path:, diagnostics:
78
+ )
79
+ rescue KeyError
80
+ raise Error, 'metadata envelope is incomplete'
81
+ end
82
+
83
+ def self.from_description(description:, api_version:, protocol_version:, source_url:, profile:, mount_path: '/', diagnostics: [])
84
+ snapshot = {
85
+ 'schema_format' => 1, 'api_version' => api_version, 'protocol_version' => protocol_version,
86
+ 'source_url' => source_url, 'profile' => profile, 'mount_path' => mount_path,
87
+ 'description' => sanitize(description, diagnostics:)
88
+ }
89
+ validate(snapshot)
90
+ end
91
+
92
+ # Prose is removed only from structural description objects. Parameter names,
93
+ # validator choice keys and Custom/default JSON are data, never filter keys.
94
+ def self.sanitize(value, context = :description, diagnostics: [], path: [])
95
+ return value.map { |item| sanitize(item, context, diagnostics:, path:) } if value.is_a?(Array)
96
+ return value unless value.is_a?(Hash)
97
+
98
+ value.each_with_object({}) do |(key, item), result|
99
+ next if PROSE.include?(key) && !%i[map data choices].include?(context)
100
+
101
+ if context == :description && key == 'default' && value['protected'] == true && !item.nil?
102
+ diagnostics << "removed protected default: #{path.join('/')}"
103
+ next
104
+ end
105
+
106
+ validate_machine_endpoint(key, item) if context == :description
107
+ child_context = sanitization_context(context, key, item)
108
+ result[key] = if context == :validators && key == 'custom'
109
+ true
110
+ elsif context == :choices
111
+ nil
112
+ else
113
+ sanitize(item, child_context, diagnostics:, path: path + [key])
114
+ end
115
+ end
116
+ end
117
+
118
+ def self.validate_machine_endpoint(key, value)
119
+ return if value.nil?
120
+ return endpoint!(value) if key.end_with?('_url')
121
+ return unless %w[path help].include?(key) || key.end_with?('_path')
122
+
123
+ path, query = value.is_a?(String) ? value.split('?', 2) : [nil, nil]
124
+ unless path && path.start_with?('/') && !path.include?('#') && safe_segments?(path) &&
125
+ (query.nil? || (key == 'help' && query.match?(/\Amethod=(?:GET|POST|PUT|PATCH|DELETE|OPTIONS)\z/)))
126
+ raise Error, 'unsafe machine endpoint in API description'
127
+ end
128
+ end
129
+
130
+ def self.sanitization_context(context, key, item)
131
+ return :data if %i[data choices].include?(context)
132
+ return :description if context == :map
133
+ return :map if %w[resources actions parameters authentication].include?(key)
134
+ return :data if %w[default format].include?(key) && context != :validator
135
+ return item.is_a?(Hash) ? :choices : :data if key == 'values'
136
+ return :validators if key == 'validators'
137
+ return :validator if context == :validators
138
+
139
+ :description
140
+ end
141
+
142
+ def self.validate(snapshot)
143
+ raise Error, 'snapshot must be an object' unless snapshot.is_a?(Hash)
144
+ raise Error, 'unsupported snapshot envelope keys' unless snapshot.keys.sort == KEYS.sort
145
+ raise Error, 'unsupported snapshot format' unless snapshot['schema_format'] == 1
146
+
147
+ version!(snapshot['api_version'])
148
+ protocol = snapshot['protocol_version']
149
+ raise Error, 'unsupported HaveAPI protocol major' unless protocol.is_a?(String) && protocol.match?(/\A2\.\d+\z/)
150
+ raise Error, 'profile must be a nonempty label' unless snapshot['profile'].is_a?(String) && !snapshot['profile'].empty?
151
+
152
+ endpoint = endpoint!(snapshot['source_url'])
153
+ mount = snapshot['mount_path']
154
+ unless mount.is_a?(String) && mount.start_with?('/') && mount.end_with?('/') && safe_segments?(mount)
155
+ raise Error, 'invalid capture mount path'
156
+ end
157
+
158
+ expected = "#{mount}v#{snapshot['api_version']}/"
159
+ raise Error, 'snapshot source URL/version mismatch' unless endpoint.path == expected
160
+
161
+ description = snapshot['description']
162
+ unless description.is_a?(Hash) && description['resources'].is_a?(Hash) &&
163
+ description['authentication'].is_a?(Hash) && description['meta'].is_a?(Hash)
164
+ raise Error, 'invalid API description structure'
165
+ end
166
+ raise Error, 'snapshot description/version mismatch' unless description['help'] == expected
167
+
168
+ description.fetch('authentication').each do |provider, config|
169
+ raise Error, "invalid authentication description for #{provider}" unless config.is_a?(Hash)
170
+
171
+ config.each do |key, value|
172
+ endpoint!(value) if key.end_with?('_url') && value
173
+ next unless key.end_with?('_path') && value
174
+ next if value.is_a?(String) && value.start_with?('/') && !value.match?(/[?#]/) && safe_segments?(value)
175
+
176
+ raise Error, "unsafe authentication path for #{provider}"
177
+ end
178
+ end
179
+ snapshot
180
+ end
181
+
182
+ def self.version!(version)
183
+ return if version.is_a?(String) && version.match?(/\A\d+(?:\.\d+)*\z/)
184
+
185
+ raise Error, 'an explicit numeric API version is required'
186
+ end
187
+
188
+ def self.endpoint!(url)
189
+ endpoint = URI.parse(url)
190
+ unless %w[http https].include?(endpoint.scheme) && endpoint.host &&
191
+ endpoint.userinfo.nil? && endpoint.query.nil? && endpoint.fragment.nil? && safe_segments?(endpoint.path)
192
+ raise Error, 'endpoint must be an HTTP URL without credentials, query or fragment'
193
+ end
194
+
195
+ endpoint
196
+ rescue URI::InvalidURIError, TypeError
197
+ raise Error, 'invalid endpoint URL'
198
+ end
199
+
200
+ def self.safe_segments?(path)
201
+ !path.include?('\\') && !path.start_with?('//') && path.split('/').none? do |segment|
202
+ decoded = URI::DEFAULT_PARSER.unescape(segment)
203
+ %w[. ..].include?(decoded) || decoded.match?(%r{[/\\\x00-\x1f\x7f]})
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,5 @@
1
+ module HaveAPI
2
+ module RustClient
3
+ VERSION = '0.30.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'rust_client/version'
2
+
3
+ module HaveAPI
4
+ module RustClient
5
+ class Error < StandardError; end
6
+ end
7
+ end
8
+
9
+ require_relative 'rust_client/snapshot'
10
+ require_relative 'rust_client/naming'
11
+ require_relative 'rust_client/ir'
12
+ require_relative 'rust_client/generator'
@@ -0,0 +1,97 @@
1
+ use std::fmt;
2
+
3
+ use crate::runtime::Error;
4
+
5
+ /// A credential requiring explicit access. Cloning does not zeroize its storage.
6
+ #[derive(Clone, PartialEq, Eq)]
7
+ pub struct SecretString(String);
8
+
9
+ impl SecretString {
10
+ pub fn expose_secret(&self) -> &str {
11
+ &self.0
12
+ }
13
+ }
14
+
15
+ impl From<String> for SecretString {
16
+ fn from(value: String) -> Self {
17
+ Self(value)
18
+ }
19
+ }
20
+
21
+ impl From<&str> for SecretString {
22
+ fn from(value: &str) -> Self {
23
+ Self(value.to_owned())
24
+ }
25
+ }
26
+
27
+ impl fmt::Debug for SecretString {
28
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29
+ f.write_str("[REDACTED]")
30
+ }
31
+ }
32
+
33
+ impl fmt::Display for SecretString {
34
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35
+ f.write_str("[REDACTED]")
36
+ }
37
+ }
38
+
39
+ #[non_exhaustive]
40
+ pub enum TokenStep {
41
+ Complete {
42
+ token: SecretString,
43
+ },
44
+ Challenge {
45
+ token: SecretString,
46
+ next_action: String,
47
+ },
48
+ }
49
+
50
+ impl TokenStep {
51
+ pub fn from_parts(
52
+ complete: bool,
53
+ token: Option<String>,
54
+ next_action: Option<String>,
55
+ known_steps: &[&str],
56
+ ) -> Result<Self, Error> {
57
+ let token = token
58
+ .filter(|token| !token.trim().is_empty())
59
+ .ok_or_else(|| Error::protocol(None, "authentication response is missing its token"))?;
60
+ if complete {
61
+ if next_action
62
+ .as_ref()
63
+ .is_some_and(|action| !action.is_empty())
64
+ {
65
+ return Err(Error::protocol(
66
+ None,
67
+ "completed authentication contains a continuation",
68
+ ));
69
+ }
70
+ Ok(Self::Complete {
71
+ token: token.into(),
72
+ })
73
+ } else {
74
+ let next_action = next_action
75
+ .filter(|action| !action.is_empty())
76
+ .ok_or_else(|| {
77
+ Error::protocol(None, "authentication response is missing its continuation")
78
+ })?;
79
+ if !known_steps.contains(&next_action.as_str()) {
80
+ return Err(Error::UnsupportedAuthStep { next_action });
81
+ }
82
+ Ok(Self::Challenge {
83
+ token: token.into(),
84
+ next_action,
85
+ })
86
+ }
87
+ }
88
+ }
89
+
90
+ impl fmt::Debug for TokenStep {
91
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92
+ f.write_str(match self {
93
+ Self::Complete { .. } => "TokenStep::Complete { [REDACTED] }",
94
+ Self::Challenge { .. } => "TokenStep::Challenge { [REDACTED] }",
95
+ })
96
+ }
97
+ }
@@ -0,0 +1,380 @@
1
+ use std::{collections::BTreeMap, fmt, marker::PhantomData, sync::Arc, time::Duration};
2
+
3
+ use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderName, HeaderValue, USER_AGENT};
4
+ use serde::{Serialize, de::DeserializeOwned};
5
+
6
+ #[cfg(feature = "blocking")]
7
+ use crate::runtime::BlockingTransport;
8
+ use crate::runtime::{
9
+ ActionSpec, ApiSpec, AsyncTransport, Error, JsonMap, Mode, PreparedRequest, RequestAuth,
10
+ RequestParts, Response, Validate, decode, prepare,
11
+ };
12
+
13
+ pub struct ClientConfig {
14
+ pub(crate) base: url::Url,
15
+ pub(crate) headers: HeaderMap,
16
+ pub(crate) auth: Option<(HeaderName, HeaderValue)>,
17
+ pub(crate) timeout: Duration,
18
+ pub(crate) connect_timeout: Duration,
19
+ pub(crate) max_response_bytes: usize,
20
+ }
21
+
22
+ #[derive(Clone)]
23
+ pub struct Client<C: Mode> {
24
+ config: Arc<ClientConfig>,
25
+ transport: C,
26
+ }
27
+
28
+ impl<C: Mode> Client<C> {
29
+ pub fn config(&self) -> &ClientConfig {
30
+ &self.config
31
+ }
32
+ }
33
+
34
+ enum Credentials {
35
+ Basic(String, String),
36
+ Token(String),
37
+ OAuth2(String),
38
+ }
39
+
40
+ pub struct ClientBuilder<C: Mode> {
41
+ base: String,
42
+ api: &'static ApiSpec,
43
+ credentials: Option<Credentials>,
44
+ auth_selections: usize,
45
+ timeout: Duration,
46
+ connect_timeout: Duration,
47
+ max_response_bytes: usize,
48
+ user_agent: String,
49
+ mode: PhantomData<C>,
50
+ }
51
+
52
+ impl<C: Mode> ClientBuilder<C> {
53
+ pub fn new(base: impl AsRef<str>, api: &'static ApiSpec) -> Self {
54
+ Self {
55
+ base: base.as_ref().to_owned(),
56
+ api,
57
+ credentials: None,
58
+ auth_selections: 0,
59
+ timeout: Duration::from_secs(30),
60
+ connect_timeout: Duration::from_secs(10),
61
+ max_response_bytes: 16 * 1024 * 1024,
62
+ user_agent: "haveapi-rust-client".into(),
63
+ mode: PhantomData,
64
+ }
65
+ }
66
+
67
+ pub fn basic_auth(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
68
+ self.credentials = Some(Credentials::Basic(username.into(), password.into()));
69
+ self.auth_selections += 1;
70
+ self
71
+ }
72
+
73
+ pub fn token(mut self, token: impl Into<String>) -> Self {
74
+ self.credentials = Some(Credentials::Token(token.into()));
75
+ self.auth_selections += 1;
76
+ self
77
+ }
78
+
79
+ pub fn oauth2_token(mut self, token: impl Into<String>) -> Self {
80
+ self.credentials = Some(Credentials::OAuth2(token.into()));
81
+ self.auth_selections += 1;
82
+ self
83
+ }
84
+
85
+ pub fn timeout(mut self, timeout: Duration) -> Self {
86
+ self.timeout = timeout;
87
+ self
88
+ }
89
+ pub fn connect_timeout(mut self, timeout: Duration) -> Self {
90
+ self.connect_timeout = timeout;
91
+ self
92
+ }
93
+ pub fn max_response_bytes(mut self, bytes: usize) -> Self {
94
+ self.max_response_bytes = bytes;
95
+ self
96
+ }
97
+ pub fn user_agent(mut self, value: impl Into<String>) -> Self {
98
+ self.user_agent = value.into();
99
+ self
100
+ }
101
+
102
+ fn finish(self) -> Result<ClientConfig, Error> {
103
+ if self.api.protocol_major != 2 {
104
+ return Err(Error::configuration("unsupported HaveAPI protocol major"));
105
+ }
106
+ if self.api.api_version.is_empty() {
107
+ return Err(Error::configuration("API version is required"));
108
+ }
109
+ if self.auth_selections > 1 {
110
+ return Err(Error::configuration(
111
+ "select only one authentication method",
112
+ ));
113
+ }
114
+ if self.timeout.is_zero() || self.connect_timeout.is_zero() {
115
+ return Err(Error::configuration("HTTP timeouts must be positive"));
116
+ }
117
+ if self.max_response_bytes == 0 {
118
+ return Err(Error::configuration("response size limit must be positive"));
119
+ }
120
+ let base = crate::runtime::protocol::parse_base(&self.base)?;
121
+ let mut headers = HeaderMap::new();
122
+ headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
123
+ headers.insert(
124
+ USER_AGENT,
125
+ HeaderValue::from_str(&self.user_agent)
126
+ .map_err(|_| Error::configuration("invalid user agent"))?,
127
+ );
128
+ let auth = match self.credentials {
129
+ None => None,
130
+ Some(Credentials::Basic(username, password)) => {
131
+ if !self.api.basic_auth {
132
+ return Err(Error::configuration(
133
+ "Basic authentication is not described",
134
+ ));
135
+ }
136
+ if username.contains(':') {
137
+ return Err(Error::configuration(
138
+ "Basic username cannot contain a colon",
139
+ ));
140
+ }
141
+ let encoded = base64(format!("{username}:{password}").as_bytes());
142
+ let mut value = HeaderValue::from_str(&format!("Basic {encoded}"))
143
+ .map_err(|_| Error::configuration("invalid Basic authentication"))?;
144
+ value.set_sensitive(true);
145
+ Some((AUTHORIZATION, value))
146
+ }
147
+ Some(Credentials::Token(token)) => Some(token_header(self.api.token_header, token)?),
148
+ Some(Credentials::OAuth2(token)) => Some(token_header(self.api.oauth2_header, token)?),
149
+ };
150
+ Ok(ClientConfig {
151
+ base,
152
+ headers,
153
+ auth,
154
+ timeout: self.timeout,
155
+ connect_timeout: self.connect_timeout,
156
+ max_response_bytes: self.max_response_bytes,
157
+ })
158
+ }
159
+ }
160
+
161
+ impl ClientBuilder<AsyncTransport> {
162
+ pub fn build(self) -> Result<Client<AsyncTransport>, Error> {
163
+ let config = self.finish()?;
164
+ let transport = AsyncTransport::new(&config)?;
165
+ Ok(Client {
166
+ config: Arc::new(config),
167
+ transport,
168
+ })
169
+ }
170
+ }
171
+
172
+ #[cfg(feature = "blocking")]
173
+ impl ClientBuilder<BlockingTransport> {
174
+ /// Build, use, and drop this client outside async runtime threads.
175
+ pub fn build(self) -> Result<Client<BlockingTransport>, Error> {
176
+ let config = self.finish()?;
177
+ let transport = BlockingTransport::new(&config)?;
178
+ Ok(Client {
179
+ config: Arc::new(config),
180
+ transport,
181
+ })
182
+ }
183
+ }
184
+
185
+ pub struct Request<'a, C: Mode, I, O, M> {
186
+ client: &'a Client<C>,
187
+ spec: &'static ActionSpec,
188
+ path_params: BTreeMap<String, String>,
189
+ input: I,
190
+ global_meta: Option<JsonMap>,
191
+ object_meta: Option<JsonMap>,
192
+ output: PhantomData<(O, M)>,
193
+ }
194
+
195
+ impl<'a, C: Mode, I, O, M> Request<'a, C, I, O, M> {
196
+ pub fn new(
197
+ client: &'a Client<C>,
198
+ spec: &'static ActionSpec,
199
+ path_params: BTreeMap<String, String>,
200
+ input: I,
201
+ ) -> Self {
202
+ Self {
203
+ client,
204
+ spec,
205
+ path_params,
206
+ input,
207
+ global_meta: None,
208
+ object_meta: None,
209
+ output: PhantomData,
210
+ }
211
+ }
212
+ pub fn input(mut self, input: I) -> Self {
213
+ *self.input_mut() = input;
214
+ self
215
+ }
216
+ pub fn global_meta(mut self, meta: JsonMap) -> Self {
217
+ self.global_meta = Some(meta);
218
+ self
219
+ }
220
+ pub fn object_meta(mut self, meta: JsonMap) -> Self {
221
+ self.object_meta = Some(meta);
222
+ self
223
+ }
224
+ pub(crate) fn input_mut(&mut self) -> &mut I {
225
+ &mut self.input
226
+ }
227
+ }
228
+
229
+ impl<C: Mode, I: Serialize + Validate, O, M> Request<'_, C, I, O, M> {
230
+ fn prepared(self) -> Result<PreparedRequest, Error> {
231
+ self.input.validate()?;
232
+ let input = if self.spec.input.is_some() {
233
+ Some(serde_json::to_value(self.input).map_err(|_| Error::Encode)?)
234
+ } else {
235
+ None
236
+ };
237
+ prepare(
238
+ &self.client.config,
239
+ self.spec,
240
+ RequestParts {
241
+ path_params: self.path_params,
242
+ input,
243
+ global_meta: self.global_meta,
244
+ object_meta: self.object_meta,
245
+ },
246
+ )
247
+ }
248
+ }
249
+
250
+ impl<I: Serialize + Validate, O: DeserializeOwned, M: DeserializeOwned>
251
+ Request<'_, AsyncTransport, I, O, M>
252
+ {
253
+ pub async fn send(self) -> Result<Response<O, M>, Error> {
254
+ let client = self.client;
255
+ let spec = self.spec;
256
+ let request = self.prepared()?;
257
+ decode(spec, client.transport.execute(request).await?)
258
+ }
259
+ }
260
+
261
+ #[cfg(feature = "blocking")]
262
+ impl<I: Serialize + Validate, O: DeserializeOwned, M: DeserializeOwned>
263
+ Request<'_, BlockingTransport, I, O, M>
264
+ {
265
+ pub fn send(self) -> Result<Response<O, M>, Error> {
266
+ let client = self.client;
267
+ let spec = self.spec;
268
+ let request = self.prepared()?;
269
+ decode(spec, client.transport.execute(request)?)
270
+ }
271
+ }
272
+
273
+ fn token_header(header: Option<&str>, token: String) -> Result<(HeaderName, HeaderValue), Error> {
274
+ let name = HeaderName::from_bytes(
275
+ header
276
+ .ok_or_else(|| Error::configuration("requested token provider is not described"))?
277
+ .as_bytes(),
278
+ )
279
+ .map_err(|_| Error::configuration("invalid authentication header name"))?;
280
+ if matches!(
281
+ name.as_str(),
282
+ "host"
283
+ | "content-length"
284
+ | "content-type"
285
+ | "accept"
286
+ | "user-agent"
287
+ | "connection"
288
+ | "transfer-encoding"
289
+ | "proxy-authorization"
290
+ | "proxy-authenticate"
291
+ | "te"
292
+ | "trailer"
293
+ | "upgrade"
294
+ | "cookie"
295
+ | "referer"
296
+ | "location"
297
+ ) {
298
+ return Err(Error::configuration("reserved authentication header name"));
299
+ }
300
+ if token.trim().is_empty() {
301
+ return Err(Error::configuration("token cannot be empty"));
302
+ }
303
+ let mut value = HeaderValue::from_str(&token)
304
+ .map_err(|_| Error::configuration("invalid token header value"))?;
305
+ value.set_sensitive(true);
306
+ Ok((name, value))
307
+ }
308
+
309
+ fn base64(bytes: &[u8]) -> String {
310
+ const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
311
+ let mut result = String::with_capacity(bytes.len().div_ceil(3) * 4);
312
+ for chunk in bytes.chunks(3) {
313
+ let first = chunk[0];
314
+ let second = chunk.get(1).copied().unwrap_or(0);
315
+ let third = chunk.get(2).copied().unwrap_or(0);
316
+ result.push(TABLE[usize::from(first >> 2)] as char);
317
+ result.push(TABLE[usize::from((first & 3) << 4 | second >> 4)] as char);
318
+ result.push(if chunk.len() > 1 {
319
+ TABLE[usize::from((second & 15) << 2 | third >> 6)] as char
320
+ } else {
321
+ '='
322
+ });
323
+ result.push(if chunk.len() > 2 {
324
+ TABLE[usize::from(third & 63)] as char
325
+ } else {
326
+ '='
327
+ });
328
+ }
329
+ result
330
+ }
331
+
332
+ impl fmt::Debug for ClientConfig {
333
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334
+ f.write_str("ClientConfig { [REDACTED] }")
335
+ }
336
+ }
337
+ impl<C: Mode> fmt::Debug for Client<C> {
338
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339
+ f.write_str("Client { [REDACTED] }")
340
+ }
341
+ }
342
+ impl<C: Mode> fmt::Debug for ClientBuilder<C> {
343
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344
+ f.write_str("ClientBuilder { [REDACTED] }")
345
+ }
346
+ }
347
+ impl<C: Mode, I, O, M> fmt::Debug for Request<'_, C, I, O, M> {
348
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349
+ f.write_str("Request { [REDACTED] }")
350
+ }
351
+ }
352
+
353
+ impl ClientConfig {
354
+ pub(crate) fn request_headers(&self, auth: RequestAuth) -> HeaderMap {
355
+ let mut headers = self.headers.clone();
356
+ if auth == RequestAuth::Configured
357
+ && let Some((name, value)) = &self.auth
358
+ {
359
+ headers.insert(name.clone(), value.clone());
360
+ }
361
+ headers
362
+ }
363
+ }
364
+
365
+ #[cfg(test)]
366
+ mod tests {
367
+ use super::*;
368
+
369
+ #[test]
370
+ fn basic_encoding_matches_rfc7617() {
371
+ assert_eq!(
372
+ base64(b"Aladdin:open sesame"),
373
+ "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
374
+ );
375
+ assert_eq!(base64(b""), "");
376
+ assert_eq!(base64(b"a"), "YQ==");
377
+ assert_eq!(base64(b"ab"), "YWI=");
378
+ assert_eq!(base64(b"abc"), "YWJj");
379
+ }
380
+ }