1use chrono::{DateTime, Utc};
3use macros::make_path_parts;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub mod curation;
8
9pub mod report;
10pub use report::{ReportId, ResourceReport};
11
12use crate::api::endpoints::PathPart;
13use crate::domain::UpdateNonNullable;
14
15use super::{
16 additional_resource::AdditionalResource,
17 asset::{DraftOrLive, OrderBy, PrivacyLevel, UserOrMe},
18 category::CategoryId,
19 meta::{AffiliationId, AgeRangeId, ResourceTypeId},
20 module::LiteModule,
21 user::UserId,
22};
23
24wrap_uuid! {
25 pub struct ResourceId
27}
28
29make_path_parts!(ResourceCreatePath => "/v1/resource");
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
34#[serde(rename_all = "camelCase")]
35pub struct ResourceResponse {
36 pub id: ResourceId,
38
39 pub published_at: Option<DateTime<Utc>>,
41
42 pub creator_id: Option<UserId>,
44
45 pub author_id: Option<UserId>,
47
48 pub author_name: Option<String>,
50
51 pub likes: i64,
53
54 pub views: i64,
56
57 pub live_up_to_date: bool,
59
60 pub is_liked: bool,
62
63 pub resource_data: ResourceData,
65
66 pub admin_data: ResourceAdminData,
68}
69
70#[derive(Serialize, Deserialize, Debug, Clone)]
72#[serde(rename_all = "camelCase")]
73pub struct ResourceData {
74 pub draft_or_live: DraftOrLive,
76
77 pub display_name: String,
79
80 pub cover: Option<LiteModule>,
84
85 pub age_ranges: Vec<AgeRangeId>,
87
88 pub affiliations: Vec<AffiliationId>,
90
91 pub language: String,
95
96 pub categories: Vec<CategoryId>,
98
99 pub description: String,
101
102 pub created_at: DateTime<Utc>,
104
105 pub last_edited: Option<DateTime<Utc>>,
107
108 pub privacy_level: PrivacyLevel,
110
111 pub locked: bool,
113
114 pub other_keywords: String,
116
117 pub translated_keywords: String,
119
120 #[serde(default)]
122 pub translated_description: HashMap<String, String>,
123
124 pub additional_resources: Vec<AdditionalResource>,
126}
127
128#[derive(Serialize, Deserialize, Debug, Default)]
132#[serde(rename_all = "camelCase")]
133pub struct ResourceCreateRequest {
134 #[serde(default)]
136 pub display_name: String,
137
138 #[serde(default)]
140 pub description: String,
141
142 #[serde(default)]
144 #[serde(skip_serializing_if = "Vec::is_empty")]
145 pub age_ranges: Vec<AgeRangeId>,
146
147 #[serde(default)]
149 #[serde(skip_serializing_if = "Vec::is_empty")]
150 pub affiliations: Vec<AffiliationId>,
151
152 #[serde(default)]
158 pub language: String,
159
160 #[serde(default)]
162 #[serde(skip_serializing_if = "Vec::is_empty")]
163 pub categories: Vec<CategoryId>,
164}
165
166make_path_parts!(ResourceGetLivePath => "/v1/resource/{}/live" => ResourceId);
167
168make_path_parts!(ResourceGetDraftPath => "/v1/resource/{}/draft" => ResourceId);
169
170make_path_parts!(ResourceUpdateDraftDataPath => "/v1/resource/{}" => ResourceId);
171
172#[derive(Serialize, Deserialize, Debug, Default)]
174#[serde(rename_all = "camelCase")]
175pub struct ResourceUpdateDraftDataRequest {
176 #[serde(default)]
178 #[serde(skip_serializing_if = "Option::is_none")]
179 pub display_name: Option<String>,
180
181 #[serde(default)]
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub language: Option<String>,
187
188 #[serde(default)]
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub categories: Option<Vec<CategoryId>>,
192
193 #[serde(default)]
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub age_ranges: Option<Vec<AgeRangeId>>,
197
198 #[serde(default)]
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub affiliations: Option<Vec<AffiliationId>>,
202
203 #[serde(default)]
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub author_id: Option<UserId>,
207
208 #[serde(default)]
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub description: Option<String>,
212
213 #[serde(default)]
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub privacy_level: Option<PrivacyLevel>,
217
218 #[serde(default)]
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub other_keywords: Option<String>,
222}
223
224make_path_parts!(ResourcePublishPath => "/v1/resource/{}/draft/publish" => ResourceId);
225
226make_path_parts!(ResourceBrowsePath => "/v1/resource/browse");
227
228#[derive(Serialize, Deserialize, Clone, Debug, Default)]
230#[serde(rename_all = "camelCase")]
231pub struct ResourceBrowseQuery {
232 #[serde(default)]
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub is_published: Option<bool>,
236
237 #[serde(default)]
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub author_id: Option<UserOrMe>,
241
242 #[serde(default)]
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub page: Option<u32>,
246
247 #[serde(default)]
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub draft_or_live: Option<DraftOrLive>,
251
252 #[serde(default)]
254 #[serde(deserialize_with = "super::from_csv")]
255 #[serde(skip_serializing_if = "Vec::is_empty")]
256 pub privacy_level: Vec<PrivacyLevel>,
257
258 #[serde(default)]
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub blocked: Option<bool>,
262
263 #[serde(default)]
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub page_limit: Option<u32>,
267
268 #[serde(default)]
270 #[serde(serialize_with = "super::csv_encode_uuids")]
271 #[serde(deserialize_with = "super::from_csv")]
272 #[serde(skip_serializing_if = "Vec::is_empty")]
273 pub resource_types: Vec<ResourceTypeId>,
274
275 #[serde(default)]
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub order_by: Option<OrderBy>,
279}
280
281#[derive(Serialize, Deserialize, Clone, Debug)]
283#[serde(rename_all = "camelCase")]
284pub struct ResourceBrowseResponse {
285 pub resources: Vec<ResourceResponse>,
287
288 pub pages: u32,
290
291 pub total_resource_count: u64,
293}
294
295make_path_parts!(ResourceSearchPath => "/v1/resource");
296
297pub struct DeleteUserResources {
299 pub resource_id: ResourceId,
301}
302
303#[derive(Serialize, Deserialize, Clone, Debug, Default)]
305#[serde(rename_all = "camelCase")]
306pub struct ResourceSearchQuery {
307 #[serde(default)]
309 #[serde(skip_serializing_if = "String::is_empty")]
310 pub q: String,
311
312 #[serde(default)]
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub page: Option<u32>,
316
317 #[serde(default)]
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub language: Option<String>,
321
322 #[serde(default)]
326 #[serde(serialize_with = "super::csv_encode_uuids")]
327 #[serde(deserialize_with = "super::from_csv")]
328 #[serde(skip_serializing_if = "Vec::is_empty")]
329 pub age_ranges: Vec<AgeRangeId>,
330
331 #[serde(default)]
335 #[serde(serialize_with = "super::csv_encode_uuids")]
336 #[serde(deserialize_with = "super::from_csv")]
337 #[serde(skip_serializing_if = "Vec::is_empty")]
338 pub affiliations: Vec<AffiliationId>,
339
340 #[serde(default)]
342 #[serde(serialize_with = "super::csv_encode_uuids")]
343 #[serde(deserialize_with = "super::from_csv")]
344 #[serde(skip_serializing_if = "Vec::is_empty")]
345 pub resource_types: Vec<ResourceTypeId>,
346
347 #[serde(default)]
349 #[serde(serialize_with = "super::csv_encode_uuids")]
350 #[serde(deserialize_with = "super::from_csv")]
351 #[serde(skip_serializing_if = "Vec::is_empty")]
352 pub categories: Vec<CategoryId>,
353
354 #[serde(default)]
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub is_published: Option<bool>,
358
359 #[serde(default)]
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub author_id: Option<UserOrMe>,
363
364 #[serde(default)]
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub author_name: Option<String>,
368
369 #[serde(default)]
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub other_keywords: Option<String>,
373
374 #[serde(default)]
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub translated_keywords: Option<String>,
378
379 #[serde(default)]
381 #[serde(deserialize_with = "super::from_csv")]
382 #[serde(skip_serializing_if = "Vec::is_empty")]
383 pub privacy_level: Vec<PrivacyLevel>,
384
385 #[serde(default)]
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub blocked: Option<bool>,
389
390 #[serde(default)]
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub page_limit: Option<u32>,
394
395 #[serde(default)]
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub is_rated: Option<bool>,
399}
400
401#[derive(Serialize, Deserialize, Clone, Debug)]
403#[serde(rename_all = "camelCase")]
404pub struct ResourceSearchResponse {
405 pub resources: Vec<ResourceResponse>,
407
408 pub pages: u32,
410
411 pub total_resource_count: u64,
413}
414
415#[derive(Serialize, Deserialize, Clone, Debug)]
417#[serde(rename_all = "camelCase")]
418pub struct ResourceIdResponse {
419 pub id: ResourceId,
421}
422
423make_path_parts!(ResourceClonePath => "/v1/resource/{}/clone" => ResourceId);
424
425make_path_parts!(ResourceDeletePath => "/v1/resource/{}" => ResourceId);
426
427make_path_parts!(ResourceDeleteAllPath => "/v1/resource");
428
429make_path_parts!(ResourceCoverPath => "/v1/resource/{}/cover" => ResourceId);
430
431make_path_parts!(ResourceCountPath => "/v1/resource/count");
432
433#[derive(Serialize, Deserialize, Clone, Debug)]
435#[serde(rename_all = "camelCase")]
436pub struct ResourceCountResponse {
437 pub total_count: u64,
439}
440
441#[derive(Serialize, Deserialize, Clone, Debug)]
443pub struct ResourceLikedResponse {
444 pub is_liked: bool,
446}
447
448#[derive(Serialize, Deserialize, Clone, Debug)]
450#[serde(rename_all = "camelCase")]
451pub struct ResourceAdminData {
452 #[serde(default)]
454 pub rating: Option<ResourceRating>,
455
456 pub blocked: bool,
458
459 pub curated: bool,
461
462 pub premium: bool,
464}
465
466#[derive(Serialize, Deserialize, Clone, Debug, Default)]
468#[serde(rename_all = "camelCase")]
469pub struct ResourceUpdateAdminDataRequest {
470 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
472 pub rating: UpdateNonNullable<ResourceRating>,
473
474 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
476 pub blocked: UpdateNonNullable<bool>,
477
478 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
480 pub curated: UpdateNonNullable<bool>,
481
482 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
484 pub premium: UpdateNonNullable<bool>,
485}
486
487#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
489#[cfg_attr(feature = "backend", derive(sqlx::Type))]
490#[serde(rename_all = "camelCase")]
491#[repr(i16)]
492pub enum ResourceRating {
493 #[allow(missing_docs)]
494 One = 1,
495 #[allow(missing_docs)]
496 Two = 2,
497 #[allow(missing_docs)]
498 Three = 3,
499}
500
501impl TryFrom<u8> for ResourceRating {
502 type Error = ();
503
504 fn try_from(num: u8) -> Result<Self, Self::Error> {
505 match num {
506 1 => Ok(Self::One),
507 2 => Ok(Self::Two),
508 3 => Ok(Self::Three),
509 _ => Err(()),
510 }
511 }
512}
513
514make_path_parts!(ResourceLikePath => "/v1/resource/{}/like" => ResourceId);
515
516make_path_parts!(ResourceUnlikePath => "/v1/resource/{}/unlike" => ResourceId);
517
518make_path_parts!(ResourceLikedPath => "/v1/resource/{}/like" => ResourceId);
519
520make_path_parts!(ListLikedPath => "/v1/resource/likes");
521
522#[derive(Serialize, Deserialize, Clone, Debug, Default)]
524#[serde(rename_all = "camelCase")]
525pub struct ListLikedRequest {
526 #[serde(default)]
528 #[serde(skip_serializing_if = "Option::is_none")]
529 pub page: Option<u32>,
530
531 #[serde(default)]
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub page_limit: Option<u32>,
535}
536#[derive(Serialize, Deserialize, Clone, Debug)]
538#[serde(rename_all = "camelCase")]
539pub struct ListLikedResponse {
540 pub resources: Vec<ResourceResponse>,
542
543 pub total_resource_count: u64,
545}
546
547make_path_parts!(ResourceViewPath => "/v1/resource/{}/view" => ResourceId);
548
549make_path_parts!(ResourceAdminDataUpdatePath => "/v1/resource/{}/admin" => ResourceId);