This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-xrpc / src / query.rs
7.9 kB 273 lines
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}