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);