kynos_openapi/model/paths/method.rs
1//! The HTTP methods a Path Item has a dedicated field for.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// An HTTP method that has a dedicated Path Item field.
8/// `#[non_exhaustive]` because OpenAPI 3.2 adds to this and the addition is
9/// `#[cfg]`-gated. Cargo unifies features across a dependency graph, so any
10/// crate enabling `openapi32` enables it for every crate in the build -- and
11/// without this attribute that would turn a downstream exhaustive `match` into
12/// a compile error, which is not what "purely additive" is supposed to mean.
13#[non_exhaustive]
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Method {
17 /// `GET`.
18 Get,
19 /// `PUT`.
20 Put,
21 /// `POST`.
22 Post,
23 /// `DELETE`.
24 Delete,
25 /// `OPTIONS`.
26 Options,
27 /// `HEAD`.
28 Head,
29 /// `PATCH`.
30 Patch,
31 /// `TRACE`.
32 Trace,
33 /// `QUERY`, as defined by the HTTP QUERY method draft.
34 ///
35 /// Introduced in OpenAPI 3.2.
36 #[cfg(feature = "openapi32")]
37 Query,
38}
39
40impl Method {
41 /// Every method with a dedicated Path Item field.
42 #[must_use]
43 pub fn all() -> &'static [Self] {
44 &[
45 Self::Get,
46 Self::Put,
47 Self::Post,
48 Self::Delete,
49 Self::Options,
50 Self::Head,
51 Self::Patch,
52 Self::Trace,
53 #[cfg(feature = "openapi32")]
54 Self::Query,
55 ]
56 }
57
58 /// The method name as it appears on the wire.
59 #[must_use]
60 pub fn as_wire_str(self) -> &'static str {
61 match self {
62 Self::Get => "GET",
63 Self::Put => "PUT",
64 Self::Post => "POST",
65 Self::Delete => "DELETE",
66 Self::Options => "OPTIONS",
67 Self::Head => "HEAD",
68 Self::Patch => "PATCH",
69 Self::Trace => "TRACE",
70 #[cfg(feature = "openapi32")]
71 Self::Query => "QUERY",
72 }
73 }
74
75 /// The method with this wire spelling, if it has a Path Item field.
76 ///
77 /// Case-sensitive: HTTP method tokens are, and a description that spelled
78 /// one differently would not be describing the same request. Returns
79 /// `None` for a method OpenAPI has no field for, which is a different
80 /// answer from "not a method" — under `openapi32` those reach a Path Item
81 /// through `additionalOperations` instead.
82 #[must_use]
83 pub fn from_wire_str(name: &str) -> Option<Self> {
84 Some(match name {
85 "GET" => Self::Get,
86 "PUT" => Self::Put,
87 "POST" => Self::Post,
88 "DELETE" => Self::Delete,
89 "OPTIONS" => Self::Options,
90 "HEAD" => Self::Head,
91 "PATCH" => Self::Patch,
92 "TRACE" => Self::Trace,
93 #[cfg(feature = "openapi32")]
94 "QUERY" => Self::Query,
95 _ => return None,
96 })
97 }
98}
99
100impl fmt::Display for Method {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.as_wire_str())
103 }
104}
105
106/// A described method has a wire spelling, so this direction never fails.
107///
108/// The pair exists because `kynos` exposes both this type and `http::Method`
109/// in adjacent APIs — an `Observer` receives a request's method and a route's
110/// — and could not write the conversion itself: both types are foreign to it,
111/// so the orphan rule puts these here.
112#[cfg(feature = "http")]
113impl From<Method> for http::Method {
114 fn from(method: Method) -> Self {
115 // `as_wire_str` returns a token from a closed set, all of which are
116 // valid method tokens, so this cannot fail.
117 Self::from_bytes(method.as_wire_str().as_bytes())
118 .expect("every described method is a valid HTTP method token")
119 }
120}
121
122/// Not every HTTP method is one a Path Item has a field for.
123///
124/// Fallible on purpose, and in two ways worth telling apart in a message
125/// rather than in the type: an extension method has no variant at all, and
126/// `QUERY` has one only under `openapi32`. Both reach a Path Item through
127/// `additionalOperations`, which is 3.2's answer and not a conversion.
128#[cfg(feature = "http")]
129impl TryFrom<&http::Method> for Method {
130 type Error = UnnamedMethod;
131
132 fn try_from(method: &http::Method) -> Result<Self, Self::Error> {
133 Method::from_wire_str(method.as_str()).ok_or_else(|| UnnamedMethod {
134 method: method.as_str().to_owned(),
135 })
136 }
137}
138
139#[cfg(feature = "http")]
140impl TryFrom<http::Method> for Method {
141 type Error = UnnamedMethod;
142
143 fn try_from(method: http::Method) -> Result<Self, Self::Error> {
144 Self::try_from(&method)
145 }
146}
147
148/// An HTTP method no Path Item field names.
149#[cfg(feature = "http")]
150#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
151#[error(
152 "`{method}` is not a method a Path Item has a field for; 3.2 describes one through \
153 `additionalOperations`"
154)]
155pub struct UnnamedMethod {
156 /// The method's wire spelling.
157 pub method: String,
158}