This repository has no description
1use axum::extract::{FromRequestParts, Query};
2use http::request::Parts;
3use knot_types::{OwnerDid, RepoDid, RepoPath, RepoRkey};
4use serde::de::{self, Deserialize, DeserializeOwned, Deserializer};
5
6use crate::error::XrpcError;
7
8pub(crate) struct ValidatedQuery<T>(pub(crate) T);
9
10impl<T, S> FromRequestParts<S> for ValidatedQuery<T>
11where
12 T: DeserializeOwned,
13 S: Send + Sync,
14{
15 type Rejection = XrpcError;
16
17 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
18 Query::<T>::try_from_uri(&parts.uri)
19 .map(|query| ValidatedQuery(query.0))
20 .map_err(|rejection| XrpcError::invalid_request(rejection.body_text()))
21 }
22}
23
24// Each route's default and its limit are included in the type,
25// such that a limit that is ok on one endpoint
26// can't be spent on another one with a lower roof.
27#[derive(Clone, Copy)]
28pub(crate) struct Limit<const DEFAULT: usize, const MAX: usize>(usize);
29
30impl<const DEFAULT: usize, const MAX: usize> Limit<DEFAULT, MAX> {
31 pub(crate) fn get(self) -> usize {
32 self.0
33 }
34}
35
36impl<const DEFAULT: usize, const MAX: usize> Default for Limit<DEFAULT, MAX> {
37 fn default() -> Self {
38 Limit(DEFAULT)
39 }
40}
41
42impl<'de, const DEFAULT: usize, const MAX: usize> Deserialize<'de> for Limit<DEFAULT, MAX> {
43 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
44 let raw = String::deserialize(deserializer)?;
45 if raw.is_empty() {
46 return Ok(Limit(DEFAULT));
47 }
48 let value = raw
49 .parse::<i64>()
50 .map_err(|_| de::Error::custom("limit must be an integer"))?;
51 Ok(Limit(usize::try_from(value).unwrap_or(0).min(MAX).max(1)))
52 }
53}
54
55knot_types::scalar_newtype! {
56 #[derive(Default)]
57 pub(crate) struct Offset(usize);
58}
59
60impl<'de> Deserialize<'de> for Offset {
61 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
62 let raw = String::deserialize(deserializer)?;
63 if raw.is_empty() {
64 return Ok(Offset::new(0));
65 }
66 raw.parse::<usize>()
67 .map(Offset::new)
68 .map_err(|_| de::Error::custom("cursor must be an integer"))
69 }
70}
71
72knot_types::scalar_newtype! {
73 pub(crate) struct Total(usize);
74}
75
76pub(crate) fn next_cursor<const DEFAULT: usize, const MAX: usize>(
77 offset: Offset,
78 limit: Limit<DEFAULT, MAX>,
79 total: Total,
80) -> Option<String> {
81 offset
82 .get()
83 .checked_add(limit.get())
84 .filter(|&end| end < total.get())
85 .map(|end| end.to_string())
86}
87
88#[derive(Clone, Copy, Default, PartialEq, Eq)]
89pub(crate) enum Order {
90 #[default]
91 Desc,
92 Asc,
93}
94
95impl Order {
96 pub(crate) fn descending(self) -> bool {
97 matches!(self, Order::Desc)
98 }
99}
100
101impl<'de> Deserialize<'de> for Order {
102 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103 match String::deserialize(deserializer)?.as_str() {
104 "" | "desc" => Ok(Order::Desc),
105 "asc" => Ok(Order::Asc),
106 _ => Err(de::Error::custom("order must be 'asc' or 'desc'")),
107 }
108 }
109}
110
111pub(crate) enum RepoArg {
112 Did(RepoDid),
113 OwnerRkey { owner: OwnerDid, rkey: RepoRkey },
114}
115
116impl RepoArg {
117 pub(crate) fn basename(&self) -> &str {
118 match self {
119 RepoArg::Did(did) => did.as_str(),
120 RepoArg::OwnerRkey { rkey, .. } => rkey.as_str(),
121 }
122 }
123
124 pub(crate) fn to_param(&self) -> String {
125 match self {
126 RepoArg::Did(did) => did.as_str().to_string(),
127 RepoArg::OwnerRkey { owner, rkey } => format!("{}/{}", owner.as_str(), rkey.as_str()),
128 }
129 }
130}
131
132impl<'de> Deserialize<'de> for RepoArg {
133 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
134 let raw = String::deserialize(deserializer)?;
135 if !raw.starts_with("did:") {
136 return Err(de::Error::custom(
137 "missing or invalid repo parameter, expected repo DID",
138 ));
139 }
140 Ok(match raw.split_once('/') {
141 None => RepoArg::Did(RepoDid::new(raw).map_err(de::Error::custom)?),
142 Some((owner, rkey)) => RepoArg::OwnerRkey {
143 owner: OwnerDid::new(owner).map_err(de::Error::custom)?,
144 rkey: RepoRkey::new(rkey).map_err(de::Error::custom)?,
145 },
146 })
147 }
148}
149
150const MAX_REVSPEC_BYTES: usize = 4096;
151
152#[derive(Clone, Default)]
153pub(crate) struct Revspec(String);
154
155impl Revspec {
156 pub(crate) fn as_str(&self) -> &str {
157 &self.0
158 }
159}
160
161impl<'de> Deserialize<'de> for Revspec {
162 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163 let raw = String::deserialize(deserializer)?;
164 match raw.len() <= MAX_REVSPEC_BYTES && !raw.chars().any(char::is_control) {
165 true => Ok(Self(raw)),
166 false => Err(de::Error::custom("invalid revision")),
167 }
168 }
169}
170
171#[derive(Default)]
172pub(crate) struct BranchArg(Option<knot_types::BranchName>);
173
174impl BranchArg {
175 pub(crate) fn get(&self) -> Option<&knot_types::BranchName> {
176 self.0.as_ref()
177 }
178}
179
180impl<'de> serde::Deserialize<'de> for BranchArg {
181 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
182 match String::deserialize(deserializer)? {
183 raw if raw.is_empty() => Ok(Self(None)),
184 raw => knot_types::BranchName::new(raw)
185 .map(|name| Self(Some(name)))
186 .map_err(de::Error::custom),
187 }
188 }
189}
190
191#[derive(Default)]
192pub(crate) struct TagArg(Option<knot_types::TagName>);
193
194impl TagArg {
195 pub(crate) fn get(&self) -> Option<&knot_types::TagName> {
196 self.0.as_ref()
197 }
198}
199
200impl<'de> serde::Deserialize<'de> for TagArg {
201 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
202 match String::deserialize(deserializer)? {
203 raw if raw.is_empty() => Ok(Self(None)),
204 raw => {
205 let short = raw.strip_prefix("refs/tags/").unwrap_or(&raw);
206 knot_types::TagName::new(short)
207 .map(|name| Self(Some(name)))
208 .map_err(de::Error::custom)
209 }
210 }
211 }
212}
213
214#[derive(Default)]
215pub(crate) enum TreePath {
216 #[default]
217 Root,
218 At(RepoPath),
219 Outside(String),
220}
221
222impl TreePath {
223 pub(crate) fn as_str(&self) -> &str {
224 match self {
225 TreePath::Root => "",
226 TreePath::At(path) => path.as_str(),
227 TreePath::Outside(raw) => raw,
228 }
229 }
230
231 pub(crate) fn dir(&self) -> Option<Option<&RepoPath>> {
232 match self {
233 TreePath::Root => Some(None),
234 TreePath::At(path) => Some(Some(path)),
235 TreePath::Outside(_) => None,
236 }
237 }
238
239 pub(crate) fn file(&self) -> Option<&RepoPath> {
240 match self {
241 TreePath::At(path) => Some(path),
242 TreePath::Root | TreePath::Outside(_) => None,
243 }
244 }
245}
246
247impl<'de> Deserialize<'de> for TreePath {
248 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
249 let raw = String::deserialize(deserializer)?;
250 Ok(match raw.is_empty() {
251 true => TreePath::Root,
252 false => match RepoPath::new(&raw) {
253 Ok(path) => TreePath::At(path),
254 Err(_) => TreePath::Outside(raw),
255 },
256 })
257 }
258}
259
260#[derive(Default)]
261pub(crate) struct RawFlag(bool);
262
263impl RawFlag {
264 pub(crate) fn requested(&self) -> bool {
265 self.0
266 }
267}
268
269impl<'de> Deserialize<'de> for RawFlag {
270 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
271 Ok(RawFlag(String::deserialize(deserializer)? == "true"))
272 }
273}