samling 0.13.1

App for managing apparel collections
Documentation
use chrono::{DateTime, FixedOffset};
use samling_clorinde::client::{GenericClient, Params};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::{
    entity_ref::{Id, Ref, RefTarget},
    helpers::slugify,
    organizations::Organization,
    Collection, CollectionSummary, EntityRefPathParam, Environment, Error, ExternalId,
    ExternalIdEntity, OrganizationSummary, PriceList, PriceListSummary, Slug, SlugEntity,
};
use samling_clorinde::queries::{
    group::get_group_id,
    user::{get_org_user_id, GetOrgUserIdParams},
};

use super::rbac::Role;

#[derive(Debug, Clone, Deserialize, Serialize, derive_more::Display, JsonSchema)]
#[display("{id}/{name}")]
pub struct User {
    pub id: Id<User>,
    pub name: String,
    pub email: String,
    pub last_sign_in: Option<DateTime<FixedOffset>>,
    pub created_at: DateTime<FixedOffset>,
    pub updated_at: DateTime<FixedOffset>,
    pub profile_image: Option<Url>,
    pub organizations: Vec<UserOrganization>,
    pub groups: Vec<GroupSummary>,
}

#[derive(Debug, Clone, Deserialize, Serialize, derive_more::Display, JsonSchema)]
#[display("{id}/{name}")]
pub struct UserSummary {
    pub id: Id<User>,
    pub name: String,
    pub email: String,
    pub last_sign_in: Option<DateTime<FixedOffset>>,
    pub profile_image: Option<Url>,
}

impl RefTarget for User {
    async fn lookup_id(
        client: &impl GenericClient,
        organization_id: Id<Organization>,
        entity_ref: &crate::entity_ref::Ref<Self>,
    ) -> crate::Result<Option<Id<Self>>> {
        match entity_ref {
            Ref::Id(user_id) => Ok(get_org_user_id()
                .params(
                    client,
                    &GetOrgUserIdParams {
                        user_id: user_id.into(),
                        organization_id: organization_id.into(),
                    },
                )
                .opt()
                .await?
                .map(Id::new)),
            Ref::ExternalId(_) => Err(Error::ExternalIdReferenceUnsupported(
                Self::short_type_name(),
            )),
            Ref::Slug(_) => Err(Error::SlugReferenceUnsupported(Self::short_type_name())),
        }
    }
}

#[derive(
    Debug, Clone, Deserialize, Serialize, JsonSchema, derive_more::Display, derive_more::Constructor,
)]
#[display("{organization} {roles:?}")]
pub struct UserOrganization {
    pub organization: OrganizationSummary,
    pub roles: Vec<Role>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserRoles {
    pub user: Id<User>,
    pub roles: Vec<Role>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Group {
    pub id: Id<Self>,
    pub slug: Slug<Self>,
    pub external_id: Option<ExternalId<Self>>,
    pub name: String,
    pub description: String,
    pub created_by: Option<Id<User>>,
    pub created_at: DateTime<FixedOffset>,
    pub updated_at: DateTime<FixedOffset>,
    pub users: Vec<UserSummary>,
    pub collections: Vec<CollectionSummary>,
    pub price_lists: Vec<PriceListSummary>,
}

impl EntityRefPathParam for Group {
    fn parameter_name() -> &'static str {
        "group_ref"
    }
}

impl RefTarget for Group {
    async fn lookup_id(
        client: &impl GenericClient,
        organization_id: Id<Organization>,
        entity_ref: &crate::entity_ref::Ref<Self>,
    ) -> crate::Result<Option<Id<Self>>> {
        let (id, external_id, slug) = entity_ref.to_owned().take_all_inner();
        Ok(get_group_id()
            .bind(
                client,
                &organization_id.into(),
                &id,
                &external_id.as_deref(),
                &slug.as_deref(),
            )
            .opt()
            .await?
            .map(Id::new))
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct GroupSummary {
    pub id: Id<Group>,
    pub slug: Slug<Group>,
    pub external_id: Option<ExternalId<Group>>,
    pub name: String,
    pub description: String,
    pub num_users: u32,
    pub num_collections: u32,
    pub num_price_lists: u32,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CreateGroup {
    pub slug: Option<Slug<Group>>,
    pub external_id: Option<ExternalId<Group>>,
    pub name: String,
    #[serde(default)]
    pub description: String,
    #[serde(default)]
    pub users: Vec<Ref<User>>,
    #[serde(default)]
    pub collections: Vec<Ref<Collection>>,
    #[serde(default)]
    pub price_lists: Vec<Ref<PriceList>>,
}

impl SlugEntity for CreateGroup {
    type RefTarget = Group;
    fn generate_slug(&self, prefix: &str) -> Option<Slug<Self::RefTarget>> {
        Some(Slug::new(slugify(&[prefix, &self.name])))
    }

    fn slug(&self) -> Option<Slug<Self::RefTarget>> {
        self.slug.clone()
    }

    fn set_slug(&mut self, value: Slug<Self::RefTarget>) {
        self.slug = Some(value);
    }
}

impl ExternalIdEntity for CreateGroup {
    type RefTarget = Group;

    fn external_id(&self) -> Option<ExternalId<Self::RefTarget>> {
        self.external_id.clone()
    }

    fn set_external_id(&mut self, value: ExternalId<Self::RefTarget>) {
        self.external_id = Some(value);
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct UpdateGroup {
    pub slug: Option<Slug<Group>>,
    pub external_id: Option<ExternalId<Group>>,
    pub name: Option<String>,
    pub description: Option<String>,
    pub users: Option<Vec<Ref<User>>>,
    pub collections: Option<Vec<Ref<Collection>>>,
    pub price_lists: Option<Vec<Ref<PriceList>>>,
}

impl SlugEntity for UpdateGroup {
    type RefTarget = Group;

    fn slug(&self) -> Option<Slug<Self::RefTarget>> {
        self.slug.clone()
    }

    fn set_slug(&mut self, value: Slug<Self::RefTarget>) {
        self.slug = Some(value);
    }
}

impl ExternalIdEntity for UpdateGroup {
    type RefTarget = Group;

    fn external_id(&self) -> Option<ExternalId<Self::RefTarget>> {
        self.external_id.clone()
    }

    fn set_external_id(&mut self, value: ExternalId<Self::RefTarget>) {
        self.external_id = Some(value);
    }
}

impl From<CreateGroup> for UpdateGroup {
    fn from(data: CreateGroup) -> Self {
        Self {
            name: Some(data.name),
            description: Some(data.description),
            slug: data.slug,
            external_id: data.external_id,
            users: Some(data.users),
            collections: Some(data.collections),
            price_lists: Some(data.price_lists),
        }
    }
}

#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateUser {
    pub email: String,
    pub password: Option<String>,
    pub name: String,
    pub profile_image: Option<Url>,
    pub roles: Option<Vec<Role>>,
    pub groups: Option<Vec<Ref<Group>>>,
}

#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct UpdateUser {
    pub email: Option<String>,
    pub password: Option<String>,
    pub name: Option<String>,
    pub profile_image: Option<Url>,
    pub roles: Option<Vec<Role>>,
    pub groups: Option<Vec<Ref<Group>>>,
}

impl From<CreateUser> for UpdateUser {
    fn from(user: CreateUser) -> Self {
        Self {
            email: Some(user.email),
            password: user.password,
            name: Some(user.name),
            profile_image: user.profile_image,
            roles: user.roles,
            groups: user.groups,
        }
    }
}

#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct UpdateOwnUser {
    pub email: Option<String>,
    pub password: Option<String>,
    pub name: Option<String>,
    pub profile_image: Option<Url>,
}

impl From<UpdateOwnUser> for UpdateUser {
    fn from(user: UpdateOwnUser) -> Self {
        Self {
            email: user.email,
            password: user.password,
            name: user.name,
            profile_image: user.profile_image,
            roles: None,
            groups: None,
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct UserLogin {
    pub email: String,
    pub password: String,
}

impl User {
    pub(crate) fn authenticated(
        self,
        environment: Environment,
        token: String,
    ) -> AuthenticatedUser {
        AuthenticatedUser {
            user: self,
            environment,
            token,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, derive_more::Constructor)]
pub struct AuthenticatedUser {
    pub user: User,
    pub environment: Environment,
    pub token: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn user_id_stringifies_to_just_integer_value() {
        assert_eq!(Id::<User>::new(123).to_string(), "123");
    }
}