http_endpoint/
endpoint.rs

1// Copyright (C) 2019-2021 Daniel Mueller <deso@posteo.net>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::error::Error;
5
6use http::Error as HttpError;
7use http::HeaderMap;
8use http::Method;
9use http::StatusCode;
10
11use crate::Bytes;
12use crate::Str;
13
14
15/// A trait describing an HTTP endpoint.
16///
17/// An endpoint for our intents and purposes is basically a path and an
18/// HTTP request method (e.g., GET or POST). The path will be combined
19/// with an "authority" (scheme, host, and port) into a full URL. Query
20/// parameters are supported as well.
21/// An endpoint is used by the `Trader` who invokes the various methods.
22pub trait Endpoint {
23  /// The type of data being passed in as part of a request to this
24  /// endpoint.
25  type Input;
26  /// The type of data being returned in the response from this
27  /// endpoint.
28  type Output;
29  /// The type of error this endpoint can report.
30  type Error: Error + From<HttpError> + From<Self::ConversionError> + 'static;
31  /// An error emitted when converting between formats.
32  type ConversionError: Error;
33  /// An error emitted by the API.
34  type ApiError: Error;
35
36  /// Retrieve the base URL to use.
37  ///
38  /// By default no URL is provided for the endpoint, in which case it
39  /// is the client's responsibility to supply one.
40  fn base_url() -> Option<Str> {
41    None
42  }
43
44  /// Retrieve the HTTP method to use.
45  ///
46  /// The default method being used is GET.
47  fn method() -> Method {
48    Method::GET
49  }
50
51  /// Inquire the path the request should go to.
52  fn path(input: &Self::Input) -> Str;
53
54  /// Inquire the query the request should use.
55  ///
56  /// By default no query is emitted.
57  #[allow(unused)]
58  fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
59    Ok(None)
60  }
61
62  /// Gather the request headers to set.
63  #[allow(unused)]
64  fn headers(input: &Self::Input) -> Result<Option<HeaderMap>, Self::ConversionError> {
65    Ok(None)
66  }
67
68  /// Retrieve the request's body.
69  ///
70  /// By default this method creates an empty body.
71  #[allow(unused)]
72  fn body(input: &Self::Input) -> Result<Option<Bytes>, Self::ConversionError> {
73    Ok(None)
74  }
75
76  /// Parse the body into the final result.
77  fn parse(body: &[u8]) -> Result<Self::Output, Self::ConversionError>;
78
79  /// Parse an API error.
80  fn parse_err(body: &[u8]) -> Result<Self::ApiError, Vec<u8>>;
81
82  /// Evaluate an HTTP status and body, converting it into an output or
83  /// error, depending on the status.
84  ///
85  /// This method is not meant to be implemented manually. It will be
86  /// auto-generated.
87  #[doc(hidden)]
88  fn evaluate(status: StatusCode, body: &[u8]) -> Result<Self::Output, Self::Error>;
89}
90
91
92/// A macro used for defining the properties for a request to a
93/// particular HTTP endpoint.
94#[macro_export]
95macro_rules! EndpointDef {
96  ( $(#[$docs:meta])* $pub:vis $name:ident($in:ty),
97    // We just ignore any documentation for success cases: there is
98    // nowhere we can put it.
99    Ok => $out:ty, [$($(#[$ok_docs:meta])* $ok_status:ident,)*],
100    Err => $err:ident, [$($(#[$err_docs:meta])* $err_status:ident => $variant:ident,)*],
101    ConversionErr => $conv_err:ty,
102    ApiErr => $api_err:ty,
103    $($defs:tt)* ) => {
104
105    $(#[$docs])*
106    #[derive(Clone, Copy, Debug)]
107    $pub struct $name;
108
109    /// An enum representing the various errors this endpoint may
110    /// encounter.
111    #[allow(unused_qualifications)]
112    #[derive(Debug)]
113    $pub enum $err {
114      $(
115        $(#[$err_docs])*
116        $variant(Result<$api_err, Vec<u8>>),
117      )*
118      /// An HTTP status not present in the endpoint's definition was
119      /// encountered.
120      UnexpectedStatus(::http::StatusCode, Result<$api_err, Vec<u8>>),
121      /// An HTTP related error.
122      Http(::http::Error),
123      /// Some kind of conversion error was encountered.
124      Conversion($conv_err),
125    }
126
127    #[allow(unused_qualifications)]
128    impl ::std::fmt::Display for $err {
129      fn fmt(&self, fmt: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
130        fn format_message(message: &Result<$api_err, Vec<u8>>) -> String {
131          match message {
132            Ok(err) => err.to_string(),
133            Err(body) => {
134              match ::std::str::from_utf8(&body) {
135                Ok(body) => format!("{}", body),
136                Err(err) => format!("{:?}", body),
137              }
138            },
139          }
140        }
141
142        match self {
143          $(
144            $err::$variant(message) => {
145              let status = ::http::StatusCode::$err_status;
146              let message = format_message(message);
147              write!(fmt, "HTTP status {}: {}", status, message)
148            },
149          )*
150          $err::UnexpectedStatus(status, message) => {
151            let message = format_message(message);
152            write!(fmt, "Unexpected HTTP status {}: {}", status, message)
153          },
154          $err::Http(err) => write!(fmt, "{}", err),
155          $err::Conversion(err) => write!(fmt, "{}", err),
156        }
157      }
158    }
159
160    #[allow(unused_qualifications)]
161    impl ::std::error::Error for $err {
162      fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
163        match self {
164          $(
165            $err::$variant(..) => None,
166          )*
167          $err::UnexpectedStatus(..) => None,
168          $err::Http(err) => err.source(),
169          $err::Conversion(err) => err.source(),
170        }
171      }
172    }
173
174    #[allow(unused_qualifications)]
175    impl ::std::convert::From<::http::Error> for $err {
176      fn from(src: ::http::Error) -> Self {
177        $err::Http(src)
178      }
179    }
180
181    #[allow(unused_qualifications)]
182    impl ::std::convert::From<$conv_err> for $err {
183      fn from(src: $conv_err) -> Self {
184        $err::Conversion(src)
185      }
186    }
187
188    #[allow(unused_qualifications)]
189    impl ::std::convert::From<$err> for ::http_endpoint::Error<$conv_err> {
190      fn from(src: $err) -> Self {
191        match src {
192          $(
193            $err::$variant(result) => {
194              let status = ::http::StatusCode::$err_status;
195              match result {
196                Ok(err) => {
197                  ::http_endpoint::Error::HttpStatus(status, err.to_string().into_bytes())
198                },
199                Err(data) => ::http_endpoint::Error::HttpStatus(status, data),
200              }
201            },
202          )*
203          $err::UnexpectedStatus(status, result) => {
204            match result {
205              Ok(err) => {
206                ::http_endpoint::Error::HttpStatus(status, err.to_string().into_bytes())
207              },
208              Err(data) => ::http_endpoint::Error::HttpStatus(status, data),
209            }
210          },
211          $err::Http(err) => ::http_endpoint::Error::Http(err),
212          $err::Conversion(err) => ::http_endpoint::Error::Conversion(err),
213        }
214      }
215    }
216
217    #[allow(unused_qualifications)]
218    impl ::http_endpoint::Endpoint for $name {
219      type Input = $in;
220      type Output = $out;
221      type Error = $err;
222      type ConversionError = $conv_err;
223      type ApiError = $api_err;
224
225      $($defs)*
226
227      #[allow(unused_qualifications)]
228      fn evaluate(
229        status: ::http::StatusCode,
230        body: &[u8],
231      ) -> Result<$out, $err> {
232        match status {
233          $(
234            ::http::StatusCode::$ok_status => {
235              <$name as ::http_endpoint::Endpoint>::parse(&body).map_err($err::from)
236            },
237          )*
238          status => {
239            let res = <$name as ::http_endpoint::Endpoint>::parse_err(&body);
240            match status {
241              $(
242                ::http::StatusCode::$err_status => {
243                  Err($err::$variant(res))
244                },
245              )*
246              _ => Err($err::UnexpectedStatus(status, res)),
247            }
248          },
249        }
250      }
251    }
252  };
253}