osauth/session.rs
1// Copyright 2019-2020 Dmitry Tantsur <dtantsur@protonmail.com>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Session structure definition.
16
17use std::collections::HashMap;
18use std::sync::Arc;
19use std::time::Duration;
20
21#[cfg(feature = "stream")]
22use async_trait::async_trait;
23#[cfg(feature = "stream")]
24use futures::Stream;
25use http::header::{HeaderMap, HeaderName, HeaderValue};
26use http::Error as HttpError;
27use reqwest::{Body, Client, Method, Response, Url};
28use serde::de::DeserializeOwned;
29use serde::Serialize;
30use static_assertions::assert_impl_all;
31
32use super::cache::EndpointCache;
33use super::client::{self, AuthenticatedClient, RequestBuilder, NO_PATH};
34use super::loading::CloudConfig;
35use super::protocol::ServiceInfo;
36use super::services::{ServiceType, VersionedService};
37use super::url as url_utils;
38use super::{Adapter, ApiVersion, AuthType, EndpointFilters, Error, InterfaceType};
39
40#[cfg(feature = "stream")]
41use super::stream::{paginated, FetchNext, PaginatedResource};
42
43/// An OpenStack API session.
44///
45/// The session object serves as a wrapper around an [authentication type](trait.AuthType.html),
46/// providing convenient methods to make HTTP requests and work with microversions.
47///
48/// # Note
49///
50/// All clones of one session share the same authentication and endpoint cache. Use
51/// [with_auth_type](#method.with_auth_type) to detach a session.
52#[derive(Debug, Clone)]
53pub struct Session {
54 client: AuthenticatedClient,
55 endpoint_cache: Arc<EndpointCache>,
56}
57
58assert_impl_all!(Session: Sync, Send);
59
60impl Session {
61 /// Create a new session with a given authentication plugin.
62 ///
63 /// The resulting session will use the default endpoint interface (usually, public).
64 pub async fn new<Auth: AuthType + 'static>(auth_type: Auth) -> Result<Session, Error> {
65 Session::new_with_client(Client::new(), auth_type).await
66 }
67
68 /// Create a new session with a given authenticated client.
69 pub fn new_with_authenticated_client(client: AuthenticatedClient) -> Session {
70 Session {
71 client,
72 endpoint_cache: Arc::new(EndpointCache::new()),
73 }
74 }
75
76 /// Create a new session with a given authentication plugin and an HTTP client.
77 ///
78 /// The resulting session will use the default endpoint interface (usually, public).
79 pub async fn new_with_client<Auth: AuthType + 'static>(
80 client: Client,
81 auth_type: Auth,
82 ) -> Result<Session, Error> {
83 Ok(Session::new_with_authenticated_client(
84 AuthenticatedClient::new(client, auth_type).await?,
85 ))
86 }
87
88 /// Create a `Session` from a `clouds.yaml` configuration file.
89 ///
90 /// See [openstacksdk
91 /// documentation](https://docs.openstack.org/openstacksdk/latest/user/guides/connect_from_config.html)
92 /// for detailed information on the format of the configuration file.
93 ///
94 /// The `cloud_name` argument is a name of the cloud entry to use.
95 ///
96 /// Supported features are:
97 /// 1. Password and HTTP basic authentication, as well as no authentication.
98 /// 2. Users, projects and domains by name.
99 /// 3. Region names (for password authentication).
100 /// 4. Custom TLS CA certificates.
101 /// 5. Profiles from `clouds-public.yaml`.
102 /// 6. Credentials from `secure.yaml`.
103 ///
104 /// A non-exhaustive list of features that are not currently supported:
105 /// 1. Users, projects and domains by ID.
106 /// 2. Adapter options, such as interfaces, default API versions and endpoint overrides.
107 /// 3. Other authentication methods.
108 /// 4. Identity v2.
109 #[inline]
110 pub async fn from_config<S: AsRef<str>>(cloud_name: S) -> Result<Session, Error> {
111 CloudConfig::from_config(cloud_name)?.create_session().await
112 }
113
114 /// Create a `Session` from environment variables.
115 ///
116 /// Supports the following authentication types: `password`, `v3token`, `http_basic` and `noop`.
117 ///
118 /// Understands the following variables:
119 /// * `OS_CLOUD` (equivalent to calling [from_config](#method.from_config) with the given cloud).
120 /// * `OS_AUTH_TYPE` (defaults to `v3token` if `OS_TOKEN` is provided otherwise to `password`).
121 /// * `OS_AUTH_URL` for `password` and `v3token`, `OS_ENDPOINT` for `http_basic` and `noop`.
122 /// * `OS_USERNAME` and `OS_PASSWORD`.
123 /// * `OS_PROJECT_NAME` or `OS_PROJECT_ID`.
124 /// * `OS_USER_DOMAIN_NAME` or `OS_USER_DOMAIN_ID` (defaults to `Default`).
125 /// * `OS_PROJECT_DOMAIN_NAME` or `OS_PROJECT_DOMAIN_ID`.
126 /// * `OS_TOKEN` (for `v3token`).
127 /// * `OS_REGION_NAME` and `OS_INTERFACE`.
128 #[inline]
129 pub async fn from_env() -> Result<Session, Error> {
130 CloudConfig::from_env()?.create_session().await
131 }
132
133 /// Create an adapter for the specific service type.
134 ///
135 /// The new `Adapter` will share the same authentication and will initially use the same
136 /// endpoint interface (although it can be changed later without affecting the `Session`).
137 ///
138 /// If you don't need the `Session` any more, using [into_adapter](#method.into_adapter) is a
139 /// bit more efficient.
140 ///
141 /// ```rust,no_run
142 /// # async fn example() -> Result<(), osauth::Error> {
143 /// let session = osauth::Session::from_env().await?;
144 /// let adapter = session.adapter(osauth::services::COMPUTE);
145 /// # Ok(()) }
146 /// # #[tokio::main]
147 /// # async fn main() { example().await.unwrap(); }
148 /// ```
149 #[inline]
150 pub fn adapter<Srv>(&self, service: Srv) -> Adapter<Srv> {
151 Adapter::from_session(self.clone(), service)
152 }
153
154 /// Create an adapter for the specific service type.
155 ///
156 /// The new `Adapter` will share the same authentication and will initially use the same
157 /// endpoint interface (although it can be changed later without affecting the `Session`).
158 ///
159 /// This method is a bit more efficient than [adapter](#method.adapter) since it does not
160 /// involve cloning internal structures.
161 ///
162 /// ```rust,no_run
163 /// # async fn example() -> Result<(), osauth::Error> {
164 /// let adapter = osauth::Session::from_env()
165 /// .await?
166 /// .into_adapter(osauth::services::COMPUTE);
167 /// # Ok(()) }
168 /// # #[tokio::main]
169 /// # async fn main() { example().await.unwrap(); }
170 /// ```
171 #[inline]
172 pub fn into_adapter<Srv>(self, service: Srv) -> Adapter<Srv> {
173 Adapter::from_session(self, service)
174 }
175
176 /// Get a reference to the authentication type in use.
177 #[inline]
178 pub fn auth_type(&self) -> &dyn AuthType {
179 self.client.auth_type()
180 }
181
182 /// Get a reference to the authenticated client in use.
183 #[inline]
184 pub fn client(&self) -> &AuthenticatedClient {
185 &self.client
186 }
187
188 /// Endpoint filters in use.
189 #[inline]
190 pub fn endpoint_filters(&self) -> &EndpointFilters {
191 &self.endpoint_cache.filters
192 }
193
194 /// Modify endpoint filters.
195 ///
196 /// This call clears the cached service information for this `Session`.
197 /// It does not, however, affect clones of this `Session`.
198 pub fn endpoint_filters_mut(&mut self) -> &mut EndpointFilters {
199 &mut Arc::make_mut(&mut self.endpoint_cache).clear().filters
200 }
201
202 /// Endpoint overrides in use.
203 #[inline]
204 pub fn endpoint_overrides(&self) -> &HashMap<String, Url> {
205 &self.endpoint_cache.overrides
206 }
207
208 /// Modify endpoint overrides.
209 ///
210 /// This call clears the cached service information for this `Session`.
211 /// It does not, however, affect clones of this `Session`.
212 pub fn endpoint_overrides_mut(&mut self) -> &mut HashMap<String, Url> {
213 &mut Arc::make_mut(&mut self.endpoint_cache).clear().overrides
214 }
215
216 /// Update the authentication and purges cached endpoint information.
217 ///
218 /// # Warning
219 ///
220 /// Authentication will also be updated for clones of this `Session`, since they share the same
221 /// authentication object.
222 #[inline]
223 pub async fn refresh(&mut self) -> Result<(), Error> {
224 self.reset_cache();
225 self.client.refresh().await
226 }
227
228 /// Reset the internal cache of this instance.
229 #[inline]
230 fn reset_cache(&mut self) {
231 let _ = Arc::make_mut(&mut self.endpoint_cache).clear();
232 }
233
234 /// Set a new authentication for this `Session`.
235 ///
236 /// This call clears the cached service information for this `Session`.
237 /// It does not, however, affect clones of this `Session`.
238 #[inline]
239 pub fn set_auth_type<Auth: AuthType + 'static>(&mut self, auth_type: Auth) {
240 self.reset_cache();
241 self.client.set_auth_type(auth_type);
242 }
243
244 /// A convenience call to set an endpoint interface.
245 ///
246 /// This call clears the cached service information for this `Session`.
247 /// It does not, however, affect clones of this `Session`.
248 #[inline]
249 pub fn set_endpoint_interface(&mut self, endpoint_interface: InterfaceType) {
250 self.endpoint_filters_mut()
251 .set_interfaces(endpoint_interface);
252 }
253
254 /// A convenience call to set an endpoint override for one service.
255 ///
256 /// This call clears the cached service information for this `Session`.
257 /// It does not, however, affect clones of this `Session`.
258 pub fn set_endpoint_override<Svc: ServiceType>(&mut self, service: Svc, url: Url) {
259 let _ = self
260 .endpoint_overrides_mut()
261 .insert(service.catalog_type().to_string(), url);
262 }
263
264 /// A convenience call to set a region.
265 ///
266 /// This call clears the cached service information for this `Session`.
267 /// It does not, however, affect clones of this `Session`.
268 pub fn set_region<T: Into<String>>(&mut self, region: T) {
269 self.endpoint_filters_mut().region = Some(region.into());
270 }
271
272 /// Convert this session into one using the given authentication.
273 #[inline]
274 pub fn with_auth_type<Auth: AuthType + 'static>(mut self, auth_method: Auth) -> Session {
275 self.set_auth_type(auth_method);
276 self
277 }
278
279 /// Convert this session into one using the given endpoint filters.
280 #[inline]
281 pub fn with_endpoint_filters(mut self, endpoint_filters: EndpointFilters) -> Session {
282 *self.endpoint_filters_mut() = endpoint_filters;
283 self
284 }
285
286 /// Convert this session into one using the given endpoint filters.
287 #[inline]
288 pub fn with_endpoint_interface(mut self, endpoint_interface: InterfaceType) -> Session {
289 self.set_endpoint_interface(endpoint_interface);
290 self
291 }
292
293 /// Convert this session into one using the given endpoint override for the given service.
294 #[inline]
295 pub fn with_endpoint_override<Srv: ServiceType>(mut self, service: Srv, url: Url) -> Session {
296 self.set_endpoint_override(service, url);
297 self
298 }
299
300 /// Convert this session into one using the given endpoint overrides.
301 #[inline]
302 pub fn with_endpoint_overrides(mut self, endpoint_overrides: HashMap<String, Url>) -> Session {
303 *self.endpoint_overrides_mut() = endpoint_overrides;
304 self
305 }
306
307 /// Convert this session into one using the given region.
308 #[inline]
309 pub fn with_region<T: Into<String>>(mut self, region: T) -> Session {
310 self.set_region(region);
311 self
312 }
313
314 /// Get minimum/maximum API (micro)version information.
315 ///
316 /// Returns `None` if the range cannot be determined, which usually means
317 /// that microversioning is not supported.
318 ///
319 /// ```rust,no_run
320 /// # async fn example() -> Result<(), osauth::Error> {
321 /// let session = osauth::Session::from_env()
322 /// .await
323 /// .expect("Failed to create an identity provider from the environment");
324 /// let maybe_versions = session
325 /// .get_api_versions(osauth::services::COMPUTE)
326 /// .await?;
327 /// if let Some((min, max)) = maybe_versions {
328 /// println!("The compute service supports versions {} to {}", min, max);
329 /// } else {
330 /// println!("The compute service does not support microversioning");
331 /// }
332 /// # Ok(()) }
333 /// # #[tokio::main]
334 /// # async fn main() { example().await.unwrap(); }
335 /// ```
336 pub async fn get_api_versions<Srv: ServiceType + Send>(
337 &self,
338 service: Srv,
339 ) -> Result<Option<(ApiVersion, ApiVersion)>, Error> {
340 self.extract_service_info(service, ServiceInfo::get_api_versions)
341 .await
342 }
343
344 /// Construct and endpoint for the given service from the path.
345 ///
346 /// You won't need to use this call most of the time, since all request calls can fetch the
347 /// endpoint automatically.
348 pub async fn get_endpoint<Srv, I>(&self, service: Srv, path: I) -> Result<Url, Error>
349 where
350 Srv: ServiceType + Send,
351 I: IntoIterator + Send,
352 I::Item: AsRef<str>,
353 {
354 self.extract_service_info(service, |info| info.get_endpoint(path))
355 .await
356 }
357
358 /// Get the currently used major version from the given service.
359 ///
360 /// Can return `None` if the service does not support API version discovery at all.
361 pub async fn get_major_version<Srv>(&self, service: Srv) -> Result<Option<ApiVersion>, Error>
362 where
363 Srv: ServiceType + Send,
364 {
365 self.extract_service_info(service, |info| info.major_version)
366 .await
367 }
368
369 /// Pick the highest API version supported by the service.
370 ///
371 /// Returns `None` if none of the requested versions are available.
372 ///
373 /// ```rust,no_run
374 /// # async fn example() -> Result<(), osauth::Error> {
375 /// let session = osauth::Session::from_env()
376 /// .await
377 /// .expect("Failed to create an identity provider from the environment");
378 /// let candidates = [osauth::ApiVersion(1, 2), osauth::ApiVersion(1, 42)];
379 /// let maybe_version = session
380 /// .pick_api_version(osauth::services::COMPUTE, candidates)
381 /// .await?;
382 ///
383 /// let request = session.get(osauth::services::COMPUTE, &["servers"]);
384 /// let response = if let Some(version) = maybe_version {
385 /// println!("Using version {}", version);
386 /// request.api_version(version)
387 /// } else {
388 /// println!("Using the base version");
389 /// request
390 /// }.send().await?;
391 /// # Ok(()) }
392 /// # #[tokio::main]
393 /// # async fn main() { example().await.unwrap(); }
394 /// ```
395 pub async fn pick_api_version<Srv, I>(
396 &self,
397 service: Srv,
398 versions: I,
399 ) -> Result<Option<ApiVersion>, Error>
400 where
401 Srv: ServiceType + Send,
402 I: IntoIterator<Item = ApiVersion> + Send,
403 {
404 self.extract_service_info(service, |info| info.pick_api_version(versions))
405 .await
406 }
407
408 /// Check if the service supports the API version.
409 pub async fn supports_api_version<Srv>(
410 &self,
411 service: Srv,
412 version: ApiVersion,
413 ) -> Result<bool, Error>
414 where
415 Srv: ServiceType + Send,
416 {
417 self.extract_service_info(service, |info| info.supports_api_version(version))
418 .await
419 }
420
421 /// Make an HTTP request to the given service.
422 ///
423 /// The `service` argument is an object implementing the
424 /// [ServiceType](services/trait.ServiceType.html) trait. Some known service types are available
425 /// in the [services](services/index.html) module.
426 ///
427 /// The `path` argument is a URL path without the service endpoint (e.g. `/servers/1234`). For
428 /// an empty path, [NO_PATH](request/constant.NO_PATH.html) can be used.
429 ///
430 /// If `api_version` is set, it is send with the request to enable a higher API version.
431 /// Otherwise the base API version is used. You can use
432 /// [pick_api_version](#method.pick_api_version) to choose an API version to use.
433 ///
434 /// The result is a `ServiceRequestBuilder` that can be customized further. Error checking and response
435 /// parsing can be done using functions from the [request](request/index.html) module.
436 ///
437 /// ```rust,no_run
438 /// # async fn example() -> Result<(), osauth::Error> {
439 /// let session = osauth::Session::from_env()
440 /// .await
441 /// .expect("Failed to create an identity provider from the environment");
442 /// let response = session
443 /// .request(osauth::services::COMPUTE, reqwest::Method::HEAD, &["servers", "1234"])
444 /// .send()
445 /// .await?;
446 /// println!("Response: {:?}", response);
447 /// # Ok(()) }
448 /// # #[tokio::main]
449 /// # async fn main() { example().await.unwrap(); }
450 /// ```
451 ///
452 /// This is the most generic call to make a request. You may prefer to use more specific `get`,
453 /// `post`, `put` or `delete` calls instead.
454 pub fn request<Srv, I>(
455 &self,
456 service: Srv,
457 method: Method,
458 path: I,
459 ) -> ServiceRequestBuilder<Srv>
460 where
461 Srv: ServiceType + Send,
462 I: IntoIterator,
463 I::Item: AsRef<str>,
464 {
465 // NOTE(dtantsur): What is going on here? Since we don't know the URL upfront,
466 // we build a fake URL. The real URL is fetched in ServiceRequestBuilder::send_unchecked,
467 // and the host, port and scheme are replaced. Anyone who invents a better procedure
468 // gets a drink from me at the nearest occasion.
469 let url_with_path = url_utils::extend(FAKE_URL.clone(), path.into_iter());
470
471 ServiceRequestBuilder {
472 inner: self.client.request(method, url_with_path),
473 endpoint_cache: self.endpoint_cache.clone(),
474 service,
475 }
476 }
477
478 /// Start a GET request.
479 ///
480 /// See [request](#method.request) for an explanation of the parameters.
481 #[inline]
482 pub fn get<Srv, I>(&self, service: Srv, path: I) -> ServiceRequestBuilder<Srv>
483 where
484 Srv: ServiceType + Send + Clone,
485 I: IntoIterator,
486 I::Item: AsRef<str>,
487 {
488 self.request(service, Method::GET, path)
489 }
490
491 /// Fetch a JSON using the GET request.
492 ///
493 /// ```rust,no_run
494 /// # async fn example() -> Result<(), osauth::Error> {
495 /// use osauth::common::IdAndName;
496 /// use serde::Deserialize;
497 ///
498 /// #[derive(Debug, Deserialize)]
499 /// pub struct ServersRoot {
500 /// pub servers: Vec<IdAndName>,
501 /// }
502 ///
503 /// let session = osauth::Session::from_env()
504 /// .await
505 /// .expect("Failed to create an identity provider from the environment");
506 ///
507 /// let servers: ServersRoot = session
508 /// .get_json(osauth::services::COMPUTE, &["servers"])
509 /// .await?;
510 /// for srv in servers.servers {
511 /// println!("ID = {}, Name = {}", srv.id, srv.name);
512 /// }
513 /// # Ok(()) }
514 /// # #[tokio::main]
515 /// # async fn main() { example().await.unwrap(); }
516 /// ```
517 ///
518 /// See [`Session::request`] for an explanation of the parameters.
519 ///
520 /// Note that this call does not handle pagination. Use [`Session::get`] in combination
521 /// with [`ServiceRequestBuilder::fetch_paginated`] instead.
522 #[inline]
523 pub async fn get_json<Srv, I, T>(&self, service: Srv, path: I) -> Result<T, Error>
524 where
525 Srv: ServiceType + Send + Clone,
526 I: IntoIterator,
527 I::Item: AsRef<str>,
528 T: DeserializeOwned + Send,
529 {
530 self.request(service, Method::GET, path).fetch().await
531 }
532
533 /// Start a POST request.
534 ///
535 /// See [request](#method.request) for an explanation of the parameters.
536 #[inline]
537 pub fn post<Srv, I>(&self, service: Srv, path: I) -> ServiceRequestBuilder<Srv>
538 where
539 Srv: ServiceType + Send + Clone,
540 I: IntoIterator,
541 I::Item: AsRef<str>,
542 {
543 self.request(service, Method::POST, path)
544 }
545
546 /// Start a PUT request.
547 ///
548 /// See [request](#method.request) for an explanation of the parameters.
549 #[inline]
550 pub fn put<Srv, I>(&self, service: Srv, path: I) -> ServiceRequestBuilder<Srv>
551 where
552 Srv: ServiceType + Send + Clone,
553 I: IntoIterator,
554 I::Item: AsRef<str>,
555 {
556 self.request(service, Method::PUT, path)
557 }
558
559 /// Start a DELETE request.
560 ///
561 /// See [request](#method.request) for an explanation of the parameters.
562 #[inline]
563 pub fn delete<Srv, I>(&self, service: Srv, path: I) -> ServiceRequestBuilder<Srv>
564 where
565 Srv: ServiceType + Send + Clone,
566 I: IntoIterator,
567 I::Item: AsRef<str>,
568 {
569 self.request(service, Method::DELETE, path)
570 }
571
572 /// Ensure service info and return the cache.
573 async fn extract_service_info<Srv, F, T>(&self, service: Srv, filter: F) -> Result<T, Error>
574 where
575 Srv: ServiceType + Send,
576 F: FnOnce(&ServiceInfo) -> T + Send,
577 T: Send,
578 {
579 self.endpoint_cache
580 .extract_service_info(&self.client, service, filter)
581 .await
582 }
583
584 #[cfg(test)]
585 pub(crate) fn cache_fake_service(
586 &mut self,
587 service_type: &'static str,
588 service_info: ServiceInfo,
589 ) {
590 self.endpoint_cache = Arc::new(EndpointCache::new_with(service_type, service_info));
591 }
592}
593
594/// A request builder for a service.
595#[derive(Debug)]
596#[must_use = "preparing a request is not enough to run it"]
597pub struct ServiceRequestBuilder<S: ServiceType> {
598 inner: RequestBuilder,
599 endpoint_cache: Arc<EndpointCache>,
600 service: S,
601}
602
603lazy_static::lazy_static! {
604 static ref FAKE_URL: Url = Url::parse("http://openstack").expect("fake URL must parse");
605}
606
607impl<S> ServiceRequestBuilder<S>
608where
609 S: ServiceType,
610{
611 /// Get a reference to the client.
612 #[inline]
613 pub fn client(&self) -> &AuthenticatedClient {
614 self.inner.client()
615 }
616
617 /// Add a body to the request.
618 pub fn body<T: Into<Body>>(self, body: T) -> ServiceRequestBuilder<S> {
619 ServiceRequestBuilder {
620 inner: self.inner.body(body),
621 ..self
622 }
623 }
624
625 /// Add a header to the request.
626 pub fn header<K, V>(self, key: K, value: V) -> ServiceRequestBuilder<S>
627 where
628 HeaderName: TryFrom<K>,
629 <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
630 HeaderValue: TryFrom<V>,
631 <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
632 {
633 ServiceRequestBuilder {
634 inner: self.inner.header(key, value),
635 ..self
636 }
637 }
638
639 /// Add headers to a request.
640 pub fn headers(self, headers: HeaderMap) -> ServiceRequestBuilder<S> {
641 ServiceRequestBuilder {
642 inner: self.inner.headers(headers),
643 ..self
644 }
645 }
646
647 /// Add a JSON body to the request.
648 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> ServiceRequestBuilder<S> {
649 ServiceRequestBuilder {
650 inner: self.inner.json(json),
651 ..self
652 }
653 }
654
655 /// Send a query with the request.
656 ///
657 /// [Query](struct.Query.html) objects can be used for type-safe querying without creating
658 /// a potentially large structure with all possible filters:
659 ///
660 /// ```rust,no_run
661 /// use std::borrow::Cow;
662 ///
663 /// #[derive(Debug)]
664 /// enum NodeFilter {
665 /// Associated(bool),
666 /// ResourceClass(String),
667 /// Limit(usize),
668 /// // ... a lot of items here ...
669 /// }
670 ///
671 /// impl osauth::QueryItem for NodeFilter {
672 /// fn query_item(&self) -> Result<(&str, Cow<str>), osauth::Error> {
673 /// Ok(match self {
674 /// NodeFilter::Associated(a) => ("associated", a.to_string().into()),
675 /// NodeFilter::ResourceClass(s) => ("resource_class", s.into()),
676 /// NodeFilter::Limit(l) => ("limit", l.to_string().into()),
677 /// })
678 /// }
679 /// }
680 ///
681 /// # async fn example() -> Result<(), osauth::Error> {
682 /// let session = osauth::Session::from_env().await?;
683 /// let query = osauth::Query::default()
684 /// .with(NodeFilter::Associated(true))
685 /// .with(NodeFilter::ResourceClass("x1.large".into()))
686 /// .with(NodeFilter::Limit(100));
687 /// let request = session
688 /// .get(osauth::services::BAREMETAL, &["nodes"])
689 /// .query(&query);
690 /// # Ok(()) }
691 /// # #[tokio::main]
692 /// # async fn main() { example().await.unwrap(); }
693 /// ```
694 pub fn query<T: Serialize + ?Sized>(self, query: &T) -> ServiceRequestBuilder<S> {
695 ServiceRequestBuilder {
696 inner: self.inner.query(query),
697 ..self
698 }
699 }
700
701 /// Override the timeout for the request.
702 pub fn timeout(self, timeout: Duration) -> ServiceRequestBuilder<S> {
703 ServiceRequestBuilder {
704 inner: self.inner.timeout(timeout),
705 ..self
706 }
707 }
708
709 /// Send the request and receive JSON in response.
710 pub async fn fetch<T>(self) -> Result<T, Error>
711 where
712 T: DeserializeOwned + Send,
713 S: Send,
714 {
715 self.send().await?.json::<T>().await.map_err(Error::from)
716 }
717
718 /// Send the request and check for errors.
719 pub async fn send(self) -> Result<Response, Error>
720 where
721 S: Send,
722 {
723 client::check(self.send_unchecked().await?).await
724 }
725
726 /// Send the request without checking for HTTP and OpenStack errors.
727 pub async fn send_unchecked(self) -> Result<Response, Error>
728 where
729 S: Send,
730 {
731 let url = self
732 .endpoint_cache
733 .extract_service_info(self.inner.client(), self.service, |info| {
734 info.get_endpoint(NO_PATH)
735 })
736 .await?;
737 self.inner.send_unchecked_to(&url).await
738 }
739}
740
741impl<S> ServiceRequestBuilder<S>
742where
743 S: VersionedService,
744{
745 /// Add an API version to this request.
746 pub fn api_version<A: Into<ApiVersion>>(self, version: A) -> ServiceRequestBuilder<S> {
747 let (name, value) = self.service.get_version_header(version.into());
748 ServiceRequestBuilder {
749 inner: self.inner.header(name, value),
750 ..self
751 }
752 }
753
754 /// Set the API version on the request.
755 pub fn set_api_version<A: Into<ApiVersion>>(&mut self, version: A) {
756 take_mut::take(self, |rb| rb.api_version(version));
757 }
758}
759
760impl<S> ServiceRequestBuilder<S>
761where
762 S: ServiceType + Clone,
763{
764 /// Send the request and receive JSON in response with pagination.
765 ///
766 /// Note that the actual requests will happen only on iteration over the results.
767 ///
768 /// To use this feature, you need to implement [`PaginatedResource`] for your resource
769 /// class. This can be done with `derive`:
770 ///
771 /// ```rust,no_run
772 /// # async fn example() -> Result<(), osauth::Error> {
773 /// use futures::pin_mut;
774 /// use futures::stream::TryStreamExt;
775 /// use serde::Deserialize;
776 /// use osauth::PaginatedResource;
777 ///
778 /// #[derive(Debug, Deserialize, PaginatedResource)]
779 /// #[collection_name = "servers"]
780 /// pub struct Server {
781 /// #[resource_id]
782 /// pub id: String,
783 /// pub name: String,
784 /// }
785 ///
786 /// let session = osauth::Session::from_env().await?;
787 ///
788 /// let servers = session
789 /// .get(osauth::services::COMPUTE, &["servers"])
790 /// .fetch_paginated::<Server>(None, None)
791 /// .await;
792 ///
793 /// pin_mut!(servers);
794 /// while let Some(srv) = servers.try_next().await? {
795 /// println!("ID = {}, Name = {}", srv.id, srv.name);
796 /// }
797 /// # Ok(()) }
798 /// # #[tokio::main]
799 /// # async fn main() { example().await.unwrap(); }
800 /// ```
801 ///
802 /// See [`PaginatedResource`] for more information on how to implement it.
803 ///
804 /// # Panics
805 ///
806 /// Will panic during iteration if the request builder has a streaming body.
807 #[cfg(feature = "stream")]
808 pub async fn fetch_paginated<T>(
809 self,
810 limit: Option<usize>,
811 starting_with: Option<<T as PaginatedResource>::Id>,
812 ) -> impl Stream<Item = Result<T, Error>>
813 where
814 S: Send + Sync,
815 T: PaginatedResource + Unpin,
816 <T as PaginatedResource>::Root: Into<Vec<T>> + Send,
817 {
818 paginated(self, limit, starting_with)
819 }
820
821 /// Attempt to clone this request builder.
822 pub fn try_clone(&self) -> Option<ServiceRequestBuilder<S>> {
823 self.inner.try_clone().map(|inner| ServiceRequestBuilder {
824 inner,
825 endpoint_cache: self.endpoint_cache.clone(),
826 service: self.service.clone(),
827 })
828 }
829}
830
831#[cfg(feature = "stream")]
832#[async_trait]
833impl<S: ServiceType + Clone + Send + Sync> FetchNext for ServiceRequestBuilder<S> {
834 async fn fetch_next<Q: Serialize + Send, T: DeserializeOwned + Send>(
835 &self,
836 query: Q,
837 ) -> Result<T, Error> {
838 let prepared = self
839 .try_clone()
840 .expect("Builder with a streaming body cannot be used")
841 .query(&query);
842 prepared.fetch().await
843 }
844}
845
846impl<S> From<ServiceRequestBuilder<S>> for RequestBuilder
847where
848 S: ServiceType,
849{
850 fn from(value: ServiceRequestBuilder<S>) -> RequestBuilder {
851 value.inner
852 }
853}
854
855#[cfg(test)]
856pub(crate) mod test_session {
857 use reqwest::Url;
858
859 use super::super::protocol::ServiceInfo;
860 use super::super::services::{GenericService, VersionSelector};
861 use super::super::{ApiVersion, NoAuth};
862 use super::Session;
863
864 pub const URL: &str = "http://127.0.0.1:5000/";
865
866 pub const URL_WITH_SUFFIX: &str = "http://127.0.0.1:5000/v2/servers";
867
868 pub async fn new_simple_session(url: &str) -> Session {
869 let service_info = ServiceInfo {
870 root_url: Url::parse(url).unwrap(),
871 major_version: None,
872 minimum_version: None,
873 current_version: None,
874 };
875 new_session(url, service_info).await
876 }
877
878 pub async fn new_session(url: &str, service_info: ServiceInfo) -> Session {
879 let auth = NoAuth::new(url).unwrap();
880 let mut session = Session::new(auth).await.unwrap();
881 session.cache_fake_service("fake", service_info);
882 session
883 }
884
885 pub const FAKE: GenericService = GenericService::new("fake", VersionSelector::Any);
886
887 #[tokio::test]
888 async fn test_get_endpoint() {
889 let s = new_simple_session(URL).await;
890 let ep = s.get_endpoint(FAKE, &[""]).await.unwrap();
891 assert_eq!(&ep.to_string(), URL);
892 }
893
894 #[tokio::test]
895 async fn test_get_endpoint_slice() {
896 let s = new_simple_session(URL).await;
897 let ep = s.get_endpoint(FAKE, &["v2", "servers"]).await.unwrap();
898 assert_eq!(&ep.to_string(), URL_WITH_SUFFIX);
899 }
900
901 #[tokio::test]
902 async fn test_get_endpoint_vec() {
903 let s = new_simple_session(URL).await;
904 let ep = s
905 .get_endpoint(FAKE, vec!["v2".to_string(), "servers".to_string()])
906 .await
907 .unwrap();
908 assert_eq!(&ep.to_string(), URL_WITH_SUFFIX);
909 }
910
911 #[tokio::test]
912 async fn test_get_major_version_absent() {
913 let s = new_simple_session(URL).await;
914 let res = s.get_major_version(FAKE).await.unwrap();
915 assert!(res.is_none());
916 }
917
918 pub const MAJOR_VERSION: ApiVersion = ApiVersion(2, 0);
919
920 #[tokio::test]
921 async fn test_get_major_version_present() {
922 let service_info = ServiceInfo {
923 root_url: Url::parse(URL).unwrap(),
924 major_version: Some(MAJOR_VERSION),
925 minimum_version: None,
926 current_version: None,
927 };
928 let s = new_session(URL, service_info).await;
929 let res = s.get_major_version(FAKE).await.unwrap();
930 assert_eq!(res, Some(MAJOR_VERSION));
931 }
932
933 pub const MIN_VERSION: ApiVersion = ApiVersion(2, 1);
934 pub const MAX_VERSION: ApiVersion = ApiVersion(2, 42);
935
936 pub fn fake_service_info() -> ServiceInfo {
937 ServiceInfo {
938 root_url: Url::parse(URL).unwrap(),
939 major_version: Some(MAJOR_VERSION),
940 minimum_version: Some(MIN_VERSION),
941 current_version: Some(MAX_VERSION),
942 }
943 }
944
945 #[tokio::test]
946 async fn test_pick_api_version_empty() {
947 let service_info = fake_service_info();
948 let s = new_session(URL, service_info).await;
949 let res = s.pick_api_version(FAKE, None).await.unwrap();
950 assert!(res.is_none());
951 }
952
953 #[tokio::test]
954 async fn test_pick_api_version_empty_vec() {
955 let service_info = fake_service_info();
956 let s = new_session(URL, service_info).await;
957 let res = s.pick_api_version(FAKE, Vec::new()).await.unwrap();
958 assert!(res.is_none());
959 }
960
961 #[tokio::test]
962 async fn test_pick_api_version() {
963 let service_info = fake_service_info();
964 let s = new_session(URL, service_info).await;
965 let choice = vec![
966 ApiVersion(2, 0),
967 ApiVersion(2, 2),
968 ApiVersion(2, 4),
969 ApiVersion(2, 99),
970 ];
971 let res = s.pick_api_version(FAKE, choice).await.unwrap();
972 assert_eq!(res, Some(ApiVersion(2, 4)));
973 }
974
975 #[tokio::test]
976 async fn test_pick_api_version_option() {
977 let service_info = fake_service_info();
978 let s = new_session(URL, service_info).await;
979 let res = s
980 .pick_api_version(FAKE, Some(ApiVersion(2, 4)))
981 .await
982 .unwrap();
983 assert_eq!(res, Some(ApiVersion(2, 4)));
984 }
985
986 #[tokio::test]
987 async fn test_pick_api_version_impossible() {
988 let service_info = fake_service_info();
989 let s = new_session(URL, service_info).await;
990 let choice = vec![ApiVersion(2, 0), ApiVersion(2, 99)];
991 let res = s.pick_api_version(FAKE, choice).await.unwrap();
992 assert!(res.is_none());
993 }
994}
995
996#[cfg(test)]
997mod test_request_builder {
998 use std::sync::Arc;
999
1000 use http::Method;
1001 use reqwest::{Client, Url};
1002
1003 use crate::cache::EndpointCache;
1004 use crate::client::AuthenticatedClient;
1005 use crate::{services, NoAuth};
1006
1007 use super::ServiceRequestBuilder;
1008
1009 #[tokio::test]
1010 async fn test_api_version() {
1011 let cli = AuthenticatedClient::new(Client::new(), NoAuth::new_without_endpoint())
1012 .await
1013 .unwrap();
1014 let rb = ServiceRequestBuilder {
1015 inner: cli.request(Method::GET, Url::parse("http://127.0.0.1").unwrap()),
1016 endpoint_cache: Arc::new(EndpointCache::new()),
1017 service: services::BAREMETAL,
1018 }
1019 .api_version((1, 42));
1020 let req = rb.inner.build().unwrap();
1021 let hdr = req.headers().get("x-openstack-ironic-api-version").unwrap();
1022 assert_eq!(hdr.to_str().unwrap(), "1.42");
1023 }
1024
1025 #[tokio::test]
1026 async fn test_set_api_version() {
1027 let cli = AuthenticatedClient::new(Client::new(), NoAuth::new_without_endpoint())
1028 .await
1029 .unwrap();
1030 let mut rb = ServiceRequestBuilder {
1031 inner: cli.request(Method::GET, Url::parse("http://127.0.0.1").unwrap()),
1032 endpoint_cache: Arc::new(EndpointCache::new()),
1033 service: services::BAREMETAL,
1034 };
1035 rb.set_api_version((1, 42));
1036 let req = rb.inner.build().unwrap();
1037 let hdr = req.headers().get("x-openstack-ironic-api-version").unwrap();
1038 assert_eq!(hdr.to_str().unwrap(), "1.42");
1039 }
1040}