1use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct Link {
49 pub href: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub templated: Option<bool>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub title: Option<String>,
59
60 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
62 pub media_type: Option<String>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub deprecation: Option<String>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub name: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub profile: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub hreflang: Option<String>,
79}
80
81impl Link {
82 pub fn new(href: impl Into<String>) -> Self {
84 Self {
85 href: href.into(),
86 templated: None,
87 title: None,
88 media_type: None,
89 deprecation: None,
90 name: None,
91 profile: None,
92 hreflang: None,
93 }
94 }
95
96 pub fn templated(href: impl Into<String>) -> Self {
105 Self {
106 href: href.into(),
107 templated: Some(true),
108 ..Self::new("")
109 }
110 }
111
112 pub fn set_templated(mut self, templated: bool) -> Self {
114 self.templated = Some(templated);
115 self
116 }
117
118 pub fn title(mut self, title: impl Into<String>) -> Self {
120 self.title = Some(title.into());
121 self
122 }
123
124 pub fn media_type(mut self, media_type: impl Into<String>) -> Self {
126 self.media_type = Some(media_type.into());
127 self
128 }
129
130 pub fn deprecation(mut self, deprecation_url: impl Into<String>) -> Self {
132 self.deprecation = Some(deprecation_url.into());
133 self
134 }
135
136 pub fn name(mut self, name: impl Into<String>) -> Self {
138 self.name = Some(name.into());
139 self
140 }
141
142 pub fn profile(mut self, profile: impl Into<String>) -> Self {
144 self.profile = Some(profile.into());
145 self
146 }
147
148 pub fn hreflang(mut self, hreflang: impl Into<String>) -> Self {
150 self.hreflang = Some(hreflang.into());
151 self
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct Resource<T> {
177 #[serde(flatten)]
179 pub data: T,
180
181 #[serde(rename = "_links")]
183 pub links: HashMap<String, LinkOrArray>,
184
185 #[serde(rename = "_embedded", skip_serializing_if = "Option::is_none")]
187 pub embedded: Option<HashMap<String, serde_json::Value>>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192#[serde(untagged)]
193pub enum LinkOrArray {
194 Single(Link),
196 Array(Vec<Link>),
198}
199
200impl From<Link> for LinkOrArray {
201 fn from(link: Link) -> Self {
202 LinkOrArray::Single(link)
203 }
204}
205
206impl From<Vec<Link>> for LinkOrArray {
207 fn from(links: Vec<Link>) -> Self {
208 LinkOrArray::Array(links)
209 }
210}
211
212impl<T> Resource<T> {
213 pub fn new(data: T) -> Self {
215 Self {
216 data,
217 links: HashMap::new(),
218 embedded: None,
219 }
220 }
221
222 pub fn link(mut self, rel: impl Into<String>, href: impl Into<String>) -> Self {
224 self.links
225 .insert(rel.into(), LinkOrArray::Single(Link::new(href)));
226 self
227 }
228
229 pub fn link_object(mut self, rel: impl Into<String>, link: Link) -> Self {
231 self.links.insert(rel.into(), LinkOrArray::Single(link));
232 self
233 }
234
235 pub fn links(mut self, rel: impl Into<String>, links: Vec<Link>) -> Self {
237 self.links.insert(rel.into(), LinkOrArray::Array(links));
238 self
239 }
240
241 pub fn self_link(self, href: impl Into<String>) -> Self {
243 self.link("self", href)
244 }
245
246 pub fn embed<E: Serialize>(
248 mut self,
249 rel: impl Into<String>,
250 resources: E,
251 ) -> Result<Self, serde_json::Error> {
252 let embedded = self.embedded.get_or_insert_with(HashMap::new);
253 embedded.insert(rel.into(), serde_json::to_value(resources)?);
254 Ok(self)
255 }
256
257 pub fn embed_raw(mut self, rel: impl Into<String>, value: serde_json::Value) -> Self {
259 let embedded = self.embedded.get_or_insert_with(HashMap::new);
260 embedded.insert(rel.into(), value);
261 self
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ResourceCollection<T> {
293 #[serde(rename = "_embedded")]
295 pub embedded: HashMap<String, Vec<T>>,
296
297 #[serde(rename = "_links")]
299 pub links: HashMap<String, LinkOrArray>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub page: Option<PageInfo>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct PageInfo {
309 pub size: usize,
311 #[serde(rename = "totalElements")]
313 pub total_elements: usize,
314 #[serde(rename = "totalPages")]
316 pub total_pages: usize,
317 pub number: usize,
319}
320
321impl PageInfo {
322 pub fn new(size: usize, total_elements: usize, total_pages: usize, number: usize) -> Self {
324 Self {
325 size,
326 total_elements,
327 total_pages,
328 number,
329 }
330 }
331
332 pub fn calculate(total_elements: usize, page_size: usize, current_page: usize) -> Self {
334 let total_pages = total_elements.div_ceil(page_size);
335 Self {
336 size: page_size,
337 total_elements,
338 total_pages,
339 number: current_page,
340 }
341 }
342}
343
344impl<T> ResourceCollection<T> {
345 pub fn new(rel: impl Into<String>, items: Vec<T>) -> Self {
347 let mut embedded = HashMap::new();
348 embedded.insert(rel.into(), items);
349
350 Self {
351 embedded,
352 links: HashMap::new(),
353 page: None,
354 }
355 }
356
357 pub fn link(mut self, rel: impl Into<String>, href: impl Into<String>) -> Self {
359 self.links
360 .insert(rel.into(), LinkOrArray::Single(Link::new(href)));
361 self
362 }
363
364 pub fn self_link(self, href: impl Into<String>) -> Self {
366 self.link("self", href)
367 }
368
369 pub fn first_link(self, href: impl Into<String>) -> Self {
371 self.link("first", href)
372 }
373
374 pub fn last_link(self, href: impl Into<String>) -> Self {
376 self.link("last", href)
377 }
378
379 pub fn next_link(self, href: impl Into<String>) -> Self {
381 self.link("next", href)
382 }
383
384 pub fn prev_link(self, href: impl Into<String>) -> Self {
386 self.link("prev", href)
387 }
388
389 pub fn page_info(mut self, page: PageInfo) -> Self {
391 self.page = Some(page);
392 self
393 }
394
395 pub fn with_pagination(mut self, base_url: &str) -> Self {
397 let page_info = self.page.clone();
399
400 if let Some(page) = page_info {
401 self = self.self_link(format!(
402 "{}?page={}&size={}",
403 base_url, page.number, page.size
404 ));
405 self = self.first_link(format!("{}?page=0&size={}", base_url, page.size));
406
407 if page.total_pages > 0 {
408 self = self.last_link(format!(
409 "{}?page={}&size={}",
410 base_url,
411 page.total_pages - 1,
412 page.size
413 ));
414 }
415
416 if page.number > 0 {
417 self = self.prev_link(format!(
418 "{}?page={}&size={}",
419 base_url,
420 page.number - 1,
421 page.size
422 ));
423 }
424
425 if page.number < page.total_pages.saturating_sub(1) {
426 self = self.next_link(format!(
427 "{}?page={}&size={}",
428 base_url,
429 page.number + 1,
430 page.size
431 ));
432 }
433 }
434 self
435 }
436}
437
438pub trait Linkable: Sized + Serialize {
440 fn with_links(self) -> Resource<Self> {
442 Resource::new(self)
443 }
444}
445
446impl<T: Serialize> Linkable for T {}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use serde::Serialize;
453
454 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
455 struct User {
456 id: i64,
457 name: String,
458 }
459
460 #[test]
461 fn test_link_creation() {
462 let link = Link::new("/users/1")
463 .title("Get user")
464 .media_type("application/json");
465
466 assert_eq!(link.href, "/users/1");
467 assert_eq!(link.title, Some("Get user".to_string()));
468 assert_eq!(link.media_type, Some("application/json".to_string()));
469 }
470
471 #[test]
472 fn test_templated_link() {
473 let link = Link::templated("/users/{id}");
474 assert!(link.templated.unwrap());
475 }
476
477 #[test]
478 fn test_resource_with_links() {
479 let user = User {
480 id: 1,
481 name: "John".to_string(),
482 };
483 let resource = Resource::new(user)
484 .self_link("/users/1")
485 .link("orders", "/users/1/orders");
486
487 assert!(resource.links.contains_key("self"));
488 assert!(resource.links.contains_key("orders"));
489
490 let json = serde_json::to_string_pretty(&resource).unwrap();
491 assert!(json.contains("_links"));
492 assert!(json.contains("/users/1"));
493 }
494
495 #[test]
496 fn test_resource_collection() {
497 let users = vec![
498 User {
499 id: 1,
500 name: "John".to_string(),
501 },
502 User {
503 id: 2,
504 name: "Jane".to_string(),
505 },
506 ];
507
508 let page = PageInfo::calculate(100, 20, 2);
509 let collection = ResourceCollection::new("users", users)
510 .page_info(page)
511 .with_pagination("/api/users");
512
513 assert!(collection.links.contains_key("self"));
514 assert!(collection.links.contains_key("first"));
515 assert!(collection.links.contains_key("prev"));
516 assert!(collection.links.contains_key("next"));
517 }
518
519 #[test]
520 fn test_page_info_calculation() {
521 let page = PageInfo::calculate(95, 20, 0);
522 assert_eq!(page.total_pages, 5);
523 assert_eq!(page.size, 20);
524 }
525
526 #[test]
527 fn test_linkable_trait() {
528 let user = User {
529 id: 1,
530 name: "Test".to_string(),
531 };
532 let resource = user.with_links().self_link("/users/1");
533 assert!(resource.links.contains_key("self"));
534 }
535}