Skip to main content

cloud_sdk/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4#[cfg(feature = "std")]
5extern crate std;
6
7macro_rules! impl_static_error {
8    ($error:ty, $($pattern:pat => $message:literal),+ $(,)?) => {
9        impl core::fmt::Display for $error {
10            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
11                formatter.write_str(match self {
12                    $($pattern => $message,)+
13                })
14            }
15        }
16
17        impl core::error::Error for $error {}
18    };
19}
20
21pub mod action_polling;
22pub mod buffer;
23pub mod operation;
24pub mod pagination;
25pub mod rate_limit;
26pub mod transport;
27
28/// Cloud provider namespace.
29#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub enum Provider {
31    /// Hetzner Cloud and DNS APIs.
32    Hetzner,
33}
34
35/// Provider API family.
36#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub enum ApiFamily {
38    /// Cloud infrastructure API.
39    Cloud,
40    /// DNS API.
41    Dns,
42    /// Security-related API resources.
43    Security,
44    /// Storage-related API resources.
45    Storage,
46    /// Provider-specific post-1.0 API surface.
47    Extended,
48}
49
50/// HTTP method for a provider operation.
51#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub enum Method {
53    /// GET request.
54    Get,
55    /// POST request.
56    Post,
57    /// PUT request.
58    Put,
59    /// DELETE request.
60    Delete,
61}
62
63impl Method {
64    /// Returns the HTTP method token.
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::Get => "GET",
69            Self::Post => "POST",
70            Self::Put => "PUT",
71            Self::Delete => "DELETE",
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::{ApiFamily, Method, Provider};
79    use crate::action_polling::ActionPollError;
80    use crate::operation::{
81        OperationMetadataError, PreparedExecutionError, ResponsePolicyError,
82        ResponsePolicyValidationError,
83    };
84    use crate::pagination::PaginationError;
85    use crate::rate_limit::RateLimitError;
86    use crate::transport::{ContentTypeError, EndpointIdentityError, RequestTargetError};
87    use core::fmt::{self, Write};
88
89    #[test]
90    fn exposes_provider_neutral_domains() {
91        assert_eq!(Provider::Hetzner, Provider::Hetzner);
92        assert_eq!(ApiFamily::Cloud, ApiFamily::Cloud);
93        assert_eq!(Method::Get, Method::Get);
94        assert_eq!(Method::Post.as_str(), "POST");
95    }
96
97    #[test]
98    fn public_errors_implement_payload_free_core_error() {
99        fn assert_error<E: core::error::Error>() {}
100
101        assert_error::<PaginationError>();
102        assert_error::<RateLimitError>();
103        assert_error::<ContentTypeError>();
104        assert_error::<EndpointIdentityError>();
105        assert_error::<RequestTargetError>();
106        assert_error::<ActionPollError<&'static str>>();
107        assert_error::<OperationMetadataError>();
108        assert_error::<ResponsePolicyError>();
109        assert_error::<ResponsePolicyValidationError>();
110        assert_error::<PreparedExecutionError<()>>();
111
112        assert_display(PaginationError::PageZero, "page number must be nonzero");
113        assert_display(RateLimitError::LimitZero, "rate limit must be nonzero");
114        assert_display(ContentTypeError::Empty, "content type is empty");
115        assert_display(
116            EndpointIdentityError::UnboundTransport,
117            "transport endpoint identity is unbound",
118        );
119        assert_display(RequestTargetError::Empty, "request target is empty");
120        assert_display(
121            OperationMetadataError::NonIdempotentRetry,
122            "non-idempotent operation cannot be retry eligible",
123        );
124        assert_display(
125            ResponsePolicyError::UnexpectedContentType,
126            "response content type is not accepted",
127        );
128        assert_display(
129            ResponsePolicyValidationError::MissingSuccessStatus,
130            "response policy has no success status",
131        );
132        assert_display(
133            PreparedExecutionError::<()>::Transport(()),
134            "prepared request transport failed",
135        );
136        assert_display(
137            ActionPollError::Policy("sentinel-secret"),
138            "action poll policy failed",
139        );
140    }
141
142    fn assert_display(error: impl fmt::Display, expected: &str) {
143        let mut output = DisplayBuffer::new();
144        assert!(write!(&mut output, "{error}").is_ok());
145        assert_eq!(output.as_str(), expected);
146    }
147
148    struct DisplayBuffer {
149        bytes: [u8; 128],
150        len: usize,
151    }
152
153    impl DisplayBuffer {
154        const fn new() -> Self {
155            Self {
156                bytes: [0; 128],
157                len: 0,
158            }
159        }
160
161        fn as_str(&self) -> &str {
162            core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
163        }
164    }
165
166    impl Write for DisplayBuffer {
167        fn write_str(&mut self, value: &str) -> fmt::Result {
168            let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
169            let target = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
170            target.copy_from_slice(value.as_bytes());
171            self.len = end;
172            Ok(())
173        }
174    }
175}