shared/domain/image.rs
1//! Types for images.
2
3pub mod recent;
4pub mod tag;
5pub mod user;
6
7use crate::api::endpoints::PathPart;
8
9use super::{
10 category::CategoryId,
11 meta::{AffiliationId, AgeRangeId, ImageStyleId, ImageTagIndex},
12 Publish,
13};
14use chrono::{DateTime, Utc};
15use macros::make_path_parts;
16use serde::{Deserialize, Serialize};
17#[cfg(feature = "backend")]
18use sqlx::{postgres::PgRow, types::Json};
19use std::collections::HashMap;
20
21make_path_parts!(ImageGetPath => "/v1/image/{}" => ImageId);
22
23/// Represents different sizes of images
24#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
25#[cfg_attr(feature = "backend", derive(sqlx::Type))]
26#[repr(i16)]
27pub enum ImageSize {
28 /// The image is a canvas (background) image
29 Canvas = 0,
30 /// The image is a sticker.
31 Sticker = 1,
32 /// The image is a user profile picture
33 // TODO: Rename since it's also used for circles and schools
34 UserProfile = 2,
35}
36
37impl ImageSize {
38 /// The size of a thumbnail (Width x Height pixels).
39 pub const THUMBNAIL_SIZE: (u32, u32) = (256, 144);
40
41 /// Gets the proper size of the image once resized.
42 #[must_use]
43 pub const fn size(self) -> (u32, u32) {
44 match self {
45 Self::Canvas => (1920, 1080),
46 Self::Sticker => (1440, 810),
47 Self::UserProfile => (256, 256),
48 }
49 }
50
51 /// Returns self represented by a string
52 #[must_use]
53 pub const fn to_str(self) -> &'static str {
54 match self {
55 Self::Canvas => "Canvas",
56 Self::Sticker => "Sticker",
57 Self::UserProfile => "UserProfile",
58 }
59 }
60}
61
62wrap_uuid! {
63 /// Wrapper type around [`Uuid`], represents the ID of a image.
64 ///
65 /// [`Uuid`]: ../../uuid/struct.Uuid.html
66 pub struct ImageId
67}
68
69make_path_parts!(ImageCreatePath => "/v1/image");
70
71// todo: # errors doc section
72/// Request to create a new image.
73#[derive(Serialize, Deserialize, Debug)]
74pub struct ImageCreateRequest {
75 /// The name of the image.
76 pub name: String,
77
78 /// The description of the image.
79 pub description: String,
80
81 /// Is the image premium?
82 pub is_premium: bool,
83
84 /// When to publish the image.
85 ///
86 /// If [`Some`] publish the image according to the `Publish`. Otherwise, don't publish it.
87 pub publish_at: Option<Publish>,
88
89 /// The image's styles.
90 pub styles: Vec<ImageStyleId>,
91
92 /// The image's age ranges.
93 pub age_ranges: Vec<AgeRangeId>,
94
95 /// The image's affiliations.
96 pub affiliations: Vec<AffiliationId>,
97
98 /// The image's tags.
99 pub tags: Vec<ImageTagIndex>,
100
101 /// The image's categories.
102 pub categories: Vec<CategoryId>,
103
104 /// What kind of image this is.
105 pub size: ImageSize,
106}
107
108make_path_parts!(ImageUpdatePath => "/v1/image/{}" => ImageId);
109
110// todo: # errors doc section.
111#[derive(Serialize, Deserialize, Debug, Default)]
112/// Request to update an image.
113///
114/// All fields are optional, any field that is [`None`] will not be updated.
115pub struct ImageUpdateRequest {
116 /// If `Some` change the image's name to this name.
117 #[serde(default)]
118 pub name: Option<String>,
119
120 /// If `Some` change the image's description to this description.
121 #[serde(default)]
122 pub description: Option<String>,
123
124 /// If `Some` mark the image as premium or not.
125 #[serde(default)]
126 pub is_premium: Option<bool>,
127
128 /// If `Some`, change the `publish_at` to the given `Option<Publish>`.
129 ///
130 /// Specifically, if `None`, don't update.
131 /// If `Some(None)`, set the `publish_at` to `None`, unpublishing it if previously published.
132 /// Otherwise set it to the given [`Publish`].
133 ///
134 /// [`Publish`]: struct.Publish.html
135 #[serde(deserialize_with = "super::deserialize_optional_field")]
136 #[serde(skip_serializing_if = "Option::is_none")]
137 #[serde(default)]
138 pub publish_at: Option<Option<Publish>>,
139
140 /// If `Some` replace the image's styles with these.
141 #[serde(default)]
142 pub styles: Option<Vec<ImageStyleId>>,
143
144 /// If `Some` replace the image's age ranges with these.
145 #[serde(default)]
146 pub age_ranges: Option<Vec<AgeRangeId>>,
147
148 /// If `Some` replace the image's affiliations with these.
149 #[serde(default)]
150 pub affiliations: Option<Vec<AffiliationId>>,
151
152 /// If `Some` replace the image's categories with these.
153 #[serde(default)]
154 pub categories: Option<Vec<CategoryId>>,
155
156 /// If `Some` replace the image's tags with these.
157 #[serde(default)]
158 pub tags: Option<Vec<ImageTagIndex>>,
159}
160
161make_path_parts!(ImageSearchPath => "/v1/image");
162
163/// Search for images via the given query string.
164///
165/// * `kind` field must match the case as represented in the returned json body (`PascalCase`?).
166/// * Vector fields, such as `age_ranges` should be given as a comma separated vector (CSV).
167#[derive(Serialize, Deserialize, Clone, Debug, Default)]
168#[serde(rename_all = "camelCase")]
169pub struct ImageSearchQuery {
170 /// The query string.
171 #[serde(default)]
172 pub q: String,
173
174 /// Optionally filter by `kind`
175 #[serde(default)]
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub size: Option<ImageSize>,
178
179 /// The page number of the images to get.
180 #[serde(default)]
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub page: Option<u32>,
183
184 /// Optionally filter by `image_styles`
185 #[serde(default)]
186 #[serde(serialize_with = "super::csv_encode_uuids")]
187 #[serde(deserialize_with = "super::from_csv")]
188 #[serde(skip_serializing_if = "Vec::is_empty")]
189 pub styles: Vec<ImageStyleId>,
190
191 /// Optionally filter by `age_ranges`
192 #[serde(default)]
193 #[serde(serialize_with = "super::csv_encode_uuids")]
194 #[serde(deserialize_with = "super::from_csv")]
195 #[serde(skip_serializing_if = "Vec::is_empty")]
196 pub age_ranges: Vec<AgeRangeId>,
197
198 /// Optionally filter by `affiliations`
199 #[serde(default)]
200 #[serde(serialize_with = "super::csv_encode_uuids")]
201 #[serde(deserialize_with = "super::from_csv")]
202 #[serde(skip_serializing_if = "Vec::is_empty")]
203 pub affiliations: Vec<AffiliationId>,
204
205 /// Optionally filter by `categories`
206 #[serde(default)]
207 #[serde(serialize_with = "super::csv_encode_uuids")]
208 #[serde(deserialize_with = "super::from_csv")]
209 #[serde(skip_serializing_if = "Vec::is_empty")]
210 pub categories: Vec<CategoryId>,
211
212 /// Optionally filter by `tags`
213 #[serde(default)]
214 #[serde(serialize_with = "super::csv_encode_i16_indices")]
215 #[serde(deserialize_with = "super::from_csv")]
216 #[serde(skip_serializing_if = "Vec::is_empty")]
217 pub tags: Vec<ImageTagIndex>,
218
219 /// Optionally order by `tags`, given in decreasing priority.
220 ///
221 /// # Notes on priority
222 /// Consider a request with 4 tags, `[clothing, food, red, sports]`.
223 ///
224 /// "Priority ordering" means that all items tagged as `clothing` will appear before those
225 /// without it, and that `[clothing, food]` will appear before `[clothing]` or `[clothing, red]`.
226 ///
227 /// ## Assigning scores
228 /// The priority is achieved by using Algolia's [filter scoring](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring/) feature with `"sumOrFiltersScore": true`.
229 ///
230 /// Scores are weighted exponentially by a factor of 2. The lowest priority tag is given a score of 1,
231 /// and the `i`th highest priority tag is given a score of `2.pow(i)`. This assignment is *provably*
232 /// correct that we get the desired ranking. This can also be interpreted as bit vector with comparison.
233 ///
234 /// *NOTE*: this means that with `i64` range supported by Algolia, we can only assign priority for
235 /// the first 62 tags. The remaining are all given a score of 1.
236 ///
237 /// ## Example
238 /// For an example request `[clothing, food, red, sports]`, we assign the scores:
239 ///
240 /// | tag name | score | (truncated) bit vector score |
241 /// |-----------|-------|-------------------------------|
242 /// | clothing | 8 | `0b_1000` |
243 /// | food | 4 | `0b_0100` |
244 /// | red | 2 | `0b_0010` |
245 /// | sports | 1 | `0b_0001` |
246 ///
247 /// This means that the entries will be returned in the following order, based on their tags:
248 ///
249 /// | position | entry name | tag names | score | (truncated) bit vector score |
250 /// |-----------|------------|--------------|-------|-------------------------------|
251 /// | 0 | hat | clothing | 8 | `0b_1000` |
252 /// | 1 | cherry | red, food | 6 | `0b_0110` |
253 /// | 2 | cucumber | green, food | 4 | `0b_0100` |
254 /// | 3 | stop sign | red | 2 | `0b_0010` |
255 /// | 4 | basketball | sports | 1 | `0b_0001` |
256 /// | 5 | wallet | [no tags] | 0 | `0b_0000` |
257 #[serde(default)]
258 #[serde(serialize_with = "super::csv_encode_i16_indices")]
259 #[serde(deserialize_with = "super::from_csv")]
260 #[serde(skip_serializing_if = "Vec::is_empty")]
261 pub tags_priority: Vec<ImageTagIndex>,
262
263 /// Optionally filter by `is_premium`
264 #[serde(default)]
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub is_premium: Option<bool>,
267
268 /// Optionally filter by `is_published`
269 #[serde(default)]
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub is_published: Option<bool>,
272
273 /// The limit of results per page.
274 #[serde(default)]
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub page_limit: Option<u32>,
277}
278
279/// Response for successful search.
280#[derive(Serialize, Deserialize, Clone, Debug)]
281pub struct ImageSearchResponse {
282 /// the images returned.
283 pub images: Vec<ImageResponse>,
284
285 /// The number of pages found.
286 pub pages: u32,
287
288 /// The total number of images found
289 pub total_image_count: u64,
290}
291
292make_path_parts!(ImageBrowsePath => "/v1/image/browse");
293
294/// Query for [`Browse`](crate::api::endpoints::image::Browse).
295#[derive(Serialize, Deserialize, Clone, Debug, Default)]
296#[serde(rename_all = "camelCase")]
297pub struct ImageBrowseQuery {
298 /// Optionally filter by `is_published`
299 #[serde(default)]
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub is_published: Option<bool>,
302
303 /// Optionally filter by `size`
304 #[serde(default)]
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub size: Option<ImageSize>,
307
308 /// The page number of the images to get.
309 #[serde(default)]
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub page: Option<u32>,
312
313 /// The limit of results per page.
314 #[serde(default)]
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub page_limit: Option<u32>,
317}
318
319/// Response for [`Browse`](crate::api::endpoints::image::Browse).
320#[derive(Serialize, Deserialize, Clone, Debug)]
321#[serde(rename_all = "camelCase")]
322pub struct ImageBrowseResponse {
323 /// the images returned.
324 pub images: Vec<ImageResponse>,
325
326 /// The number of pages found.
327 pub pages: u32,
328
329 /// The total number of images found
330 pub total_image_count: u64,
331}
332
333/// Response for getting a single image.
334#[derive(Serialize, Deserialize, Clone, Debug)]
335pub struct ImageResponse {
336 /// The image metadata.
337 pub metadata: ImageMetadata,
338}
339
340make_path_parts!(ImageUploadPath => "/v1/image/{}/raw" => ImageId);
341// #[allow(missing_docs)]
342// #[derive(Clone, Debug)]
343// pub struct ImageUploadPath<'a>(&'a ImageId);
344
345// impl crate::api::endpoints::PathParts for ImageUploadPath<'_> {
346// const PATH: &'static str = "/v1/image/{ImageId}/raw";
347// fn get_filled(&self) -> String {
348// let mut src = String::from(Self::PATH);
349// src = src.replace(<ImageId>::PLACEHOLDER, &self.0.get_path_string());
350// src
351// }
352// }
353
354/// Request to indicate the size of an image for upload.
355#[derive(Serialize, Deserialize, Debug)]
356pub struct ImageUploadRequest {
357 /// The size of the image to be uploaded in bytes. Allows the API server to check that the file size is
358 /// within limits and as a verification at GCS that the entire file was uploaded
359 pub file_size: usize,
360}
361
362/// URL to upload an image. Supports resumable uploading.
363#[derive(Serialize, Deserialize, Debug)]
364pub struct ImageUploadResponse {
365 /// The session URI used for uploading, including the query for uploader ID
366 pub session_uri: String,
367}
368
369/// Over the wire representation of an image's metadata.
370#[derive(Serialize, Deserialize, Clone, Debug)]
371pub struct ImageMetadata {
372 /// The image's ID.
373 pub id: ImageId,
374
375 /// The name of the image.
376 pub name: String,
377
378 /// A string describing the image.
379 pub description: String,
380
381 /// A translated descriptions of the image.
382 pub translated_description: HashMap<String, String>,
383
384 /// Whether or not the image is premium.
385 pub is_premium: bool,
386
387 /// What size of image this is.
388 pub size: ImageSize,
389
390 /// When the image should be considered published (if at all).
391 pub publish_at: Option<DateTime<Utc>>,
392
393 /// The styles associated with the image.
394 pub styles: Vec<ImageStyleId>,
395
396 /// The tags associated with the image.
397 pub tags: Vec<ImageTagIndex>,
398
399 /// The age ranges associated with the image.
400 pub age_ranges: Vec<AgeRangeId>,
401
402 /// The affiliations associated with the image.
403 pub affiliations: Vec<AffiliationId>,
404
405 /// The categories associated with the image.
406 pub categories: Vec<CategoryId>,
407
408 /// When the image was originally created.
409 pub created_at: DateTime<Utc>,
410
411 /// When the image was last updated.
412 pub updated_at: Option<DateTime<Utc>>,
413}
414
415/// Response for successfully creating a Image.
416pub type CreateResponse = super::CreateResponse<ImageId>;
417
418// HACK: we can't get `Vec<_>` directly from the DB, so we have to work around it for now.
419// see: https://github.com/launch/sqlx/issues/298
420#[cfg(feature = "backend")]
421impl<'r> sqlx::FromRow<'r, PgRow> for ImageMetadata {
422 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
423 let DbImage {
424 id,
425 size,
426 name,
427 description,
428 translated_description,
429 is_premium,
430 publish_at,
431 styles,
432 age_ranges,
433 affiliations,
434 categories,
435 tags,
436 created_at,
437 updated_at,
438 } = DbImage::from_row(row)?;
439
440 Ok(Self {
441 id,
442 size,
443 name,
444 description,
445 translated_description: translated_description.0,
446 is_premium,
447 publish_at,
448 styles: styles.into_iter().map(|(it,)| it).collect(),
449 age_ranges: age_ranges.into_iter().map(|(it,)| it).collect(),
450 affiliations: affiliations.into_iter().map(|(it,)| it).collect(),
451 categories: categories.into_iter().map(|(it,)| it).collect(),
452 tags: tags.into_iter().map(|(it,)| it).collect(),
453 created_at,
454 updated_at,
455 })
456 }
457}
458
459#[cfg_attr(feature = "backend", derive(sqlx::FromRow))]
460#[cfg(feature = "backend")]
461struct DbImage {
462 pub id: ImageId,
463 pub size: ImageSize,
464 pub name: String,
465 pub description: String,
466 pub translated_description: Json<HashMap<String, String>>,
467 pub is_premium: bool,
468 pub publish_at: Option<DateTime<Utc>>,
469 pub styles: Vec<(ImageStyleId,)>,
470 pub age_ranges: Vec<(AgeRangeId,)>,
471 pub affiliations: Vec<(AffiliationId,)>,
472 pub categories: Vec<(CategoryId,)>,
473 pub tags: Vec<(ImageTagIndex,)>,
474 pub created_at: DateTime<Utc>,
475 pub updated_at: Option<DateTime<Utc>>,
476}
477
478make_path_parts!(ImageDeletePath => "/v1/image/{}" => ImageId);
479
480make_path_parts!(ImagePutPath => "/v1/image/{}/use" => ImageId);