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}