1use chrono::{DateTime, FixedOffset};
2use serde_derive::{Deserialize, Serialize};
3use std::collections::HashMap;
4use url::Url;
5
6use crate::spec::Object;
7
8#[derive(PartialEq, Eq, Debug, Deserialize)]
10pub struct BatchRequest {
11 pub operation: Operation,
12 #[serde(default = "Transfer::default_vec")]
13 pub transfer: Vec<Transfer>,
14 #[serde(rename = "ref")]
15 pub ref_property: Option<Ref>,
16 pub objects: Vec<Object>,
17}
18
19#[derive(PartialEq, Eq, Debug, Serialize)]
21pub struct BatchResponse {
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub transfer: Option<Transfer>,
24 pub objects: Vec<ObjectResponse>,
25}
26
27#[derive(PartialEq, Eq, Debug, Deserialize, Serialize)]
29#[serde(rename_all = "lowercase")]
30pub enum Operation {
31 Download,
32 Upload,
33}
34
35#[derive(PartialEq, Eq, Debug, Deserialize, Serialize)]
37#[serde(rename_all = "lowercase")]
38pub enum Transfer {
39 Basic,
40 Custom,
41}
42
43impl Transfer {
44 fn default_vec() -> Vec<Self> {
45 vec![Transfer::Basic]
46 }
47}
48
49impl Default for Transfer {
50 fn default() -> Self {
51 Transfer::Basic
52 }
53}
54
55#[derive(PartialEq, Eq, Debug, Deserialize)]
57pub struct Ref {
58 pub name: String,
59}
60
61#[derive(PartialEq, Eq, Debug, Serialize)]
63#[serde(untagged)]
64pub enum ObjectResponse {
65 Success {
66 #[serde(flatten)]
67 object: Object,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 authenticated: Option<bool>,
70 actions: Actions,
71 },
72 Error {
73 #[serde(flatten)]
74 object: Object,
75 error: ObjectError,
76 },
77}
78
79impl ObjectResponse {
80 pub fn success(object: Object, actions: Actions) -> Self {
81 ObjectResponse::Success {
82 object,
83 authenticated: None,
84 actions,
85 }
86 }
87
88 pub fn error(object: Object, error: ObjectError) -> Self {
89 ObjectResponse::Error { object, error }
90 }
91}
92
93#[derive(PartialEq, Eq, Debug, Serialize)]
95pub struct ObjectSuccess {
96 #[serde(skip_serializing_if = "Option::is_none")]
97 authenticated: Option<bool>,
98 actions: Actions,
99}
100
101#[derive(PartialEq, Eq, Debug, Serialize)]
103pub struct ObjectError {
104 code: u16,
105 message: &'static str,
106}
107
108impl ObjectError {
109 pub fn DoesNotExist() -> Self {
110 Self {
111 code: 404u16,
112 message: "Object does not exist",
113 }
114 }
115
116 pub fn RemovedByOwner() -> Self {
117 Self {
118 code: 410u16,
119 message: "Object removed by owner",
120 }
121 }
122 pub fn ValidationError() -> Self {
123 Self {
124 code: 422u16,
125 message: "Validation error",
126 }
127 }
128}
129
130#[derive(PartialEq, Eq, Debug, Serialize)]
132#[serde(untagged)]
133pub enum Actions {
134 Download { download: Action },
135 None,
136 Upload { upload: Action },
137 UploadAndVerify { upload: Action, verify: Action },
138}
139
140#[derive(PartialEq, Eq, Debug, Serialize)]
142pub struct Action {
143 #[serde(with = "url_serde")]
144 href: Url,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 header: Option<HashMap<String, String>>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 expires_in: Option<i32>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 expires_at: Option<DateTime<FixedOffset>>,
151}
152
153impl Action {
154 pub fn new(href: Url) -> Self {
155 Self {
156 href,
157 header: None,
158 expires_in: None,
159 expires_at: None,
160 }
161 }
162}
163
164#[derive(PartialEq, Eq, Debug, Serialize)]
166pub struct LfsErrorResponse {
167 message: &'static str,
168 #[serde(with = "url_serde")]
169 documentation_url: Option<Url>,
170 request_id: Option<String>,
171 #[serde(skip)]
172 status: u16,
173}
174
175impl LfsErrorResponse {
176 const ACCEPT_HEADER_INCORRECT: Self = Self {
177 message: "The Accept header needs to be `application/vnd.git-lfs+json`.",
178 documentation_url: None,
179 request_id: None,
180 status: 406u16,
181 };
182 const RATE_LIMIT_HIT: Self = Self {
183 message: "A rate limit has been hit with the server.",
184 documentation_url: None,
185 request_id: None,
186 status: 429u16,
187 };
188 const NOT_IMPLEMENTED: Self = Self {
189 message: "The server has not implemented the current method.",
190 documentation_url: None,
191 request_id: None,
192 status: 501u16,
193 };
194 const INSUFFICIENT_STORAGE: Self = Self {
195 message: "The server has insufficient storage capacity to complete the request.",
196 documentation_url: None,
197 request_id: None,
198 status: 507u16,
199 };
200
201 const BANDWIDTH_LIMIT_EXCEEDED: Self = Self {
202 message: "A bandwidth limit has been exceeded.",
203 documentation_url: None,
204 request_id: None,
205 status: 509u16,
206 };
207}
208
209#[cfg(test)]
210mod test {
211 use super::*;
212 use pretty_assertions::assert_eq;
213
214 #[test]
215 fn batch_response_serializes_correctly() {
216 assert_eq!(
217 include_str!("test/batch_response_success.json"),
218 serde_json::to_string_pretty(&BatchResponse {
219 transfer: Some(Transfer::Basic),
220 objects: vec![ObjectResponse::Success {
221 object: Object {
222 oid: "1111111".to_string(),
223 size: 123,
224 },
225 authenticated: Some(true),
226 actions: Actions::Download {
227 download: Action {
228 href: Url::parse("https://some-download.com").unwrap(),
229 header: Some(
230 [("Key", "value")]
231 .iter()
232 .map(|(k, v)| (k.to_string(), v.to_string()))
233 .collect()
234 ),
235 expires_in: None,
236 expires_at: DateTime::parse_from_rfc3339("2016-11-10T15:29:07Z")
237 .unwrap()
238 .into()
239 }
240 }
241 }],
242 })
243 .unwrap(),
244 );
245
246 assert_eq!(
247 include_str!("test/batch_response_error.json"),
248 serde_json::to_string_pretty(&BatchResponse {
249 transfer: Some(Transfer::Basic),
250 objects: vec![ObjectResponse::Error {
251 error: ObjectError::DoesNotExist(),
252 object: Object {
253 oid: "1111111".to_string(),
254 size: 123,
255 },
256 }],
257 })
258 .unwrap()
259 );
260 }
261
262 #[test]
263 fn lfs_error_serializes_correctly() {
264 assert_eq!(
265 include_str!("test/lfs_error.json"),
266 serde_json::to_string_pretty(&LfsErrorResponse {
267 message: "Not found",
268 documentation_url: Url::parse("https://lfs-server.com/docs/errors")
269 .unwrap()
270 .into(),
271 request_id: Some("123".to_string()),
272 status: 404u16
273 })
274 .unwrap(),
275 );
276 }
277}