@@ -4,7 +4,7 @@ use crate::{Error, Result};
44use async_stream:: try_stream;
55use futures:: { Stream , StreamExt , pin_mut} ;
66use http:: header:: { HeaderName , USER_AGENT } ;
7- use reqwest:: { ClientBuilder , IntoUrl , Method , StatusCode , header:: HeaderMap } ;
7+ use reqwest:: { ClientBuilder , IntoUrl , Method , StatusCode , header:: HeaderMap , header :: HeaderValue } ;
88use serde:: { Serialize , de:: DeserializeOwned } ;
99use serde_json:: { Map , Value } ;
1010use stac:: api:: { GetItems , Item , ItemCollection , Items , Search , UrlBuilder } ;
@@ -23,8 +23,13 @@ pub async fn search(
2323 href : & str ,
2424 mut search : Search ,
2525 max_items : Option < usize > ,
26+ headers : & [ ( String , String ) ] ,
2627) -> Result < ItemCollection > {
27- let client = Client :: new ( href) ?;
28+ let mut builder = ApiClientBuilder :: new ( href) ?;
29+ if !headers. is_empty ( ) {
30+ builder = builder. with_headers ( headers) ?;
31+ }
32+ let client = builder. build ( ) ?;
2833 if search. limit . is_none ( )
2934 && let Some ( max_items) = max_items
3035 {
@@ -72,24 +77,91 @@ pub struct BlockingIterator {
7277 stream : Pin < Box < dyn Stream < Item = Result < Item > > > > ,
7378}
7479
75- impl Client {
76- /// Creates a new API client.
80+ /// Builder for configuring and constructing a [`Client`] for STAC APIs.
81+ ///
82+ /// This type is used to create a `Client` with a specific base URL and a set
83+ /// of default HTTP headers. A typical usage pattern is:
84+ ///
85+ /// - Call [`ApiClientBuilder::new`] with the API root URL.
86+ /// - Optionally call [`ApiClientBuilder::with_headers`] to add extra headers.
87+ /// - Call [`ApiClientBuilder::build`] to obtain a configured [`Client`].
88+ ///
89+ /// Internally, `ApiClientBuilder` prepares a `reqwest::Client` and then
90+ /// delegates to [`Client::with_client`] to produce the final STAC API client.
91+ pub struct ApiClientBuilder {
92+ url : String ,
93+ headers : HeaderMap ,
94+ }
95+
96+ impl ApiClientBuilder {
97+ /// Create ApiClientBuilder
7798 ///
7899 /// # Examples
79100 ///
80101 /// ```
81- /// # use stac_io::api::Client ;
82- /// let client = Client ::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap();
102+ /// # use stac_io::api::ApiClientBuilder ;
103+ /// let builder = ApiClientBuilder ::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap();
83104 /// ```
84- pub fn new ( url : & str ) -> Result < Client > {
85- // TODO support HATEOS (aka look up the urls from the root catalog)
105+ pub fn new ( url : & str ) -> Result < Self > {
86106 let mut headers = HeaderMap :: new ( ) ;
87107 let _ = headers. insert (
88108 USER_AGENT ,
89109 format ! ( "rustac/{}" , env!( "CARGO_PKG_VERSION" ) ) . parse ( ) ?,
90110 ) ;
91- let client = ClientBuilder :: new ( ) . default_headers ( headers) . build ( ) ?;
92- Client :: with_client ( client, url)
111+ Ok ( Self {
112+ url : url. to_string ( ) ,
113+ headers,
114+ } )
115+ }
116+
117+ /// Additional headers to pass to default_headers
118+ ///
119+ /// # Examples
120+ ///
121+ /// ```
122+ /// # use stac_io::api::ApiClientBuilder;
123+ /// let headers = vec![("x-my-header".to_string(), "value".to_string())];
124+ /// let builder = ApiClientBuilder::new("https://planetarycomputer.microsoft.com/api/stac/v1")
125+ /// .unwrap()
126+ /// .with_headers(&headers)
127+ /// .unwrap();
128+ /// ```
129+ pub fn with_headers ( mut self , headers : & [ ( String , String ) ] ) -> Result < Self > {
130+ for ( key, val) in headers. iter ( ) {
131+ let header_name = key. parse :: < HeaderName > ( ) ?;
132+ let header_value = HeaderValue :: from_str ( val) ?;
133+ self . headers . insert ( header_name, header_value) ;
134+ }
135+ Ok ( self )
136+ }
137+
138+ /// Builds a [`Client`] from this builder.
139+ ///
140+ /// This finalizes the configuration (including any default and custom headers)
141+ /// and constructs the underlying `reqwest::Client`.
142+ ///
143+ /// # Examples
144+ ///
145+ /// ```
146+ /// # use stac_io::api::ApiClientBuilder;
147+ /// let client = ApiClientBuilder::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap().build().unwrap();
148+ pub fn build ( self ) -> Result < Client > {
149+ let client = ClientBuilder :: new ( ) . default_headers ( self . headers ) . build ( ) ?;
150+ Client :: with_client ( client, & self . url )
151+ }
152+ }
153+
154+ impl Client {
155+ /// Creates a new API client.
156+ ///
157+ /// # Examples
158+ ///
159+ /// ```
160+ /// # use stac_io::api::Client;
161+ /// let client = Client::new("https://planetarycomputer.microsoft.com/api/stac/v1").unwrap();
162+ /// ```
163+ pub fn new ( url : & str ) -> Result < Client > {
164+ ApiClientBuilder :: new ( url) ?. build ( )
93165 }
94166
95167 /// Creates a new API client with the given [Client].
@@ -407,6 +479,8 @@ fn not_found_to_none<T>(result: Result<T>) -> Result<Option<T>> {
407479
408480#[ cfg( test) ]
409481mod tests {
482+ use crate :: api:: ApiClientBuilder ;
483+
410484 use super :: Client ;
411485 use futures:: StreamExt ;
412486 use mockito:: { Matcher , Server } ;
@@ -586,4 +660,25 @@ mod tests {
586660 let client = Client :: new ( & server. url ( ) ) . unwrap ( ) ;
587661 let _ = client. search ( Default :: default ( ) ) . await . unwrap ( ) ;
588662 }
663+ #[ tokio:: test]
664+ async fn custom_header ( ) {
665+ let mut server = Server :: new_async ( ) . await ;
666+ let _ = server
667+ . mock ( "POST" , "/search" )
668+ . with_body_from_file ( "mocks/items-page-1.json" )
669+ . match_header ( "x-my-header" , "value" )
670+ . match_header ( "x-my-other-header" , "othervalue" )
671+ . create_async ( )
672+ . await ;
673+ let headers = vec ! [
674+ ( "x-my-header" . to_string( ) , "value" . to_string( ) ) ,
675+ ( "x-my-other-header" . to_string( ) , "othervalue" . to_string( ) ) ,
676+ ] ;
677+ let builder = ApiClientBuilder :: new ( & server. url ( ) )
678+ . unwrap ( )
679+ . with_headers ( & headers)
680+ . unwrap ( ) ;
681+ let client = builder. build ( ) . unwrap ( ) ;
682+ let _ = client. search ( Default :: default ( ) ) . await . unwrap ( ) ;
683+ }
589684}
0 commit comments