openapi cleanup
This commit is contained in:
@@ -13,6 +13,7 @@ use diesel::{pg::PgConnection, prelude::*};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -37,15 +38,16 @@ use webauthn_rs::prelude::RegisterPublicKeyCredential;
|
||||
|
||||
const REFRESH_COOKIE_NAME: &str = "refresh_token";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
pub preferred_tenant_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[derive(Deserialize, Serialize, ToSchema)]
|
||||
pub struct LoginResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
@@ -53,46 +55,55 @@ pub struct LoginResponse {
|
||||
pub tenant: TenantSnippet,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantSnippet {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantSelectionResponse {
|
||||
pub access_token: String,
|
||||
pub tenants: Vec<TenantSnippet>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct TenantListResponse {
|
||||
pub tenants: Vec<TenantSnippet>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct TenantSelectionRequest {
|
||||
pub tenant_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SignupStartRequest {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct SignupStartResponse {
|
||||
pub signup_token: String,
|
||||
pub challenge: RegistrationChallengeResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SignupFinishRequest {
|
||||
pub signup_token: String,
|
||||
#[schema(value_type = Object)]
|
||||
pub credential: RegisterPublicKeyCredential,
|
||||
#[schema(nullable)]
|
||||
pub nickname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum LoginResponseVariants {
|
||||
Token(LoginResponse),
|
||||
Selection(TenantSelectionResponse),
|
||||
}
|
||||
|
||||
pub async fn login(_state: State<AppState>, _payload: Json<LoginRequest>) -> AppResult<Response> {
|
||||
Err(AppError::bad_request(
|
||||
"password authentication is no longer supported",
|
||||
|
||||
@@ -5,6 +5,7 @@ use chrono::Utc;
|
||||
use diesel::{dsl::count_star, prelude::*, result::DatabaseErrorKind, PgConnection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -18,31 +19,35 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CorrespondentUsage {
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CorrespondentSummary {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[schema(value_type = Object)]
|
||||
pub metadata: Value,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub usage: CorrespondentUsage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateCorrespondentRequest {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Object)]
|
||||
pub metadata: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UpdateCorrespondentRequest {
|
||||
#[schema(nullable)]
|
||||
pub name: Option<String>,
|
||||
#[schema(nullable, value_type = Object)]
|
||||
pub metadata: Option<Value>,
|
||||
}
|
||||
|
||||
|
||||
@@ -60,14 +60,17 @@ const PRESIGNED_URL_EXPIRY_SECONDS: u64 = 300;
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct DocumentListQuery {
|
||||
#[schema(nullable)]
|
||||
pub folder_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
pub include_descendants: Option<bool>,
|
||||
pub query: Option<String>,
|
||||
pub tags: Option<String>,
|
||||
pub correspondents: Option<String>,
|
||||
pub document_types: Option<String>,
|
||||
#[serde(default = "default_document_status_filter")]
|
||||
#[schema(default = "active")]
|
||||
pub status: DocumentStatusFilter,
|
||||
}
|
||||
|
||||
@@ -87,6 +90,7 @@ pub enum DocumentStatusFilter {
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct AssetRequestQuery {
|
||||
#[serde(default)]
|
||||
#[schema(default = false)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
@@ -100,16 +104,22 @@ pub struct DocumentCheckQuery {
|
||||
pub struct DocumentCheckResponse {
|
||||
pub exists: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub document_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub filename: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub version_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub version_number: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
@@ -117,6 +127,7 @@ pub struct DocumentCheckResponse {
|
||||
pub struct TagResponse {
|
||||
pub id: Uuid,
|
||||
pub label: String,
|
||||
#[schema(nullable)]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
@@ -151,21 +162,29 @@ pub struct DocumentResponse {
|
||||
pub filename: String,
|
||||
pub title: String,
|
||||
pub original_name: String,
|
||||
#[schema(nullable)]
|
||||
pub content_type: Option<String>,
|
||||
#[schema(nullable)]
|
||||
pub folder_id: Option<Uuid>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[schema(nullable)]
|
||||
pub deleted_at: Option<String>,
|
||||
#[schema(nullable)]
|
||||
pub issued_at: Option<String>,
|
||||
#[schema(value_type = Object)]
|
||||
pub metadata: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub document_type_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub document_type: Option<DocumentTypeResponse>,
|
||||
pub tags: Vec<TagResponse>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub correspondents: Vec<DocumentCorrespondentResponse>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(nullable)]
|
||||
pub current_version: Option<DocumentVersionDetailResponse>,
|
||||
}
|
||||
#[derive(Serialize, ToSchema)]
|
||||
@@ -181,11 +200,13 @@ pub struct BulkReanalyzeResponse {
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkMoveRequest {
|
||||
pub document_ids: Vec<Uuid>,
|
||||
#[schema(nullable)]
|
||||
pub folder_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct DocumentMetadataUpdate {
|
||||
#[schema(value_type = Object)]
|
||||
pub value: Value,
|
||||
#[serde(default)]
|
||||
#[schema(default = false)]
|
||||
@@ -200,7 +221,7 @@ pub struct UpdateDocumentRequest {
|
||||
#[schema(nullable, value_type = Option<String>)]
|
||||
pub issued_at: Option<Value>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
#[schema(nullable, value_type = Object)]
|
||||
pub metadata: Option<DocumentMetadataUpdate>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Option<Uuid>)]
|
||||
@@ -247,6 +268,7 @@ pub struct CorrespondentAssignmentInput {
|
||||
pub struct AssignCorrespondentsRequest {
|
||||
pub assignments: Vec<CorrespondentAssignmentInput>,
|
||||
#[serde(default)]
|
||||
#[schema(default = false)]
|
||||
pub replace: bool,
|
||||
}
|
||||
|
||||
@@ -284,6 +306,7 @@ pub struct BulkCorrespondentsRequest {
|
||||
pub struct BulkReanalyzeSelectionRequest {
|
||||
pub document_ids: Vec<Uuid>,
|
||||
#[serde(default = "default_true")]
|
||||
#[schema(default = true)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
@@ -316,7 +339,7 @@ pub struct UploadDocumentForm {
|
||||
pub file: String,
|
||||
#[schema(nullable)]
|
||||
pub folder_id: Option<Uuid>,
|
||||
#[schema(nullable)]
|
||||
#[schema(nullable, value_type = Object)]
|
||||
pub metadata: Option<Value>,
|
||||
#[schema(nullable)]
|
||||
pub title: Option<String>,
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
use diesel::{dsl::exists, prelude::*, PgConnection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{Document, DocumentType, Folder, NewFolder};
|
||||
@@ -22,38 +22,40 @@ use crate::documents::{
|
||||
asset::load_primary_assets, correspondents::load_correspondents_for_documents,
|
||||
tags::load_tags_for_documents,
|
||||
};
|
||||
use crate::utils::{
|
||||
json::{classify_nullable, NullableValue},
|
||||
time::to_iso,
|
||||
};
|
||||
use crate::utils::time::to_iso;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateFolderRequest {
|
||||
pub name: String,
|
||||
#[schema(nullable)]
|
||||
pub parent_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct EnsureFolderPathRequest {
|
||||
#[schema(nullable)]
|
||||
pub parent_id: Option<Uuid>,
|
||||
pub segments: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FolderResponse {
|
||||
pub folder: FolderInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FolderContentsResponse {
|
||||
#[schema(nullable)]
|
||||
pub folder: Option<FolderInfo>,
|
||||
pub subfolders: Vec<FolderInfo>,
|
||||
pub documents: Vec<DocumentResponse>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, IntoParams, ToSchema)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct FolderContentsQuery {
|
||||
#[serde(default = "default_include_documents")]
|
||||
#[schema(default = true)]
|
||||
pub include_documents: bool,
|
||||
}
|
||||
|
||||
@@ -61,15 +63,27 @@ const fn default_include_documents() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FolderInfo {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[schema(nullable)]
|
||||
pub parent_id: Option<Uuid>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, ToSchema)]
|
||||
#[serde(default)]
|
||||
pub struct UpdateFolderRequest {
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Option<Uuid>)]
|
||||
pub parent_id: Option<Option<Uuid>>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
pub name: Option<Option<String>>,
|
||||
}
|
||||
|
||||
pub async fn get_folder(
|
||||
Path(folder_id): Path<Uuid>,
|
||||
TenantScopedConn {
|
||||
@@ -435,15 +449,8 @@ pub async fn update_folder(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(body): Json<Value>,
|
||||
Json(payload): Json<UpdateFolderRequest>,
|
||||
) -> AppResult<StatusCode> {
|
||||
if !body.is_object() {
|
||||
return Err(AppError::bad_request("request body must be a JSON object"));
|
||||
}
|
||||
|
||||
let parent_class = classify_nullable(body.get("parent_id")).map_err(AppError::bad_request)?;
|
||||
let name_class = classify_nullable(body.get("name")).map_err(AppError::bad_request)?;
|
||||
|
||||
conn.transaction::<(), AppError, _>(|conn| {
|
||||
let folder: Folder = folders::table
|
||||
.find(folder_id)
|
||||
@@ -452,21 +459,15 @@ pub async fn update_folder(
|
||||
|
||||
let mut next_parent = folder.parent_id;
|
||||
let mut parent_changed = false;
|
||||
match parent_class {
|
||||
NullableValue::Omitted => {}
|
||||
NullableValue::Null => {
|
||||
match payload.parent_id {
|
||||
None => {}
|
||||
Some(None) => {
|
||||
if folder.parent_id.is_some() {
|
||||
parent_changed = true;
|
||||
}
|
||||
next_parent = None;
|
||||
}
|
||||
NullableValue::String(value) => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request("parent_id must not be empty"));
|
||||
}
|
||||
let parent_id = Uuid::parse_str(trimmed)
|
||||
.map_err(|_| AppError::bad_request("parent_id must be a valid UUID or null"))?;
|
||||
Some(Some(parent_id)) => {
|
||||
if parent_id == folder_id {
|
||||
return Err(AppError::bad_request("folder cannot be its own parent"));
|
||||
}
|
||||
@@ -492,12 +493,12 @@ pub async fn update_folder(
|
||||
|
||||
let mut new_name = folder.name.clone();
|
||||
let mut name_changed = false;
|
||||
match name_class {
|
||||
NullableValue::Omitted => {}
|
||||
NullableValue::Null => {
|
||||
match payload.name {
|
||||
None => {}
|
||||
Some(None) => {
|
||||
return Err(AppError::bad_request("name cannot be null"));
|
||||
}
|
||||
NullableValue::String(value) => {
|
||||
Some(Some(value)) => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request("name must not be empty"));
|
||||
|
||||
@@ -5,6 +5,7 @@ use axum::{
|
||||
};
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::{
|
||||
@@ -20,32 +21,39 @@ use crate::models::WebdavToken;
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{db::no_content, time::to_iso};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct WebdavTokenResponse {
|
||||
pub id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
#[schema(nullable)]
|
||||
pub label: Option<String>,
|
||||
pub created_at: String,
|
||||
#[schema(nullable)]
|
||||
pub last_used_at: Option<String>,
|
||||
#[schema(nullable)]
|
||||
pub expires_at: Option<String>,
|
||||
#[schema(nullable)]
|
||||
pub revoked_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct WebdavTokenCreatedResponse {
|
||||
pub token: String,
|
||||
pub token_info: WebdavTokenResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateWebdavTokenRequest {
|
||||
#[schema(nullable)]
|
||||
pub label: Option<String>,
|
||||
#[schema(nullable)]
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct RevokePasskeyQuery {
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
+27
-18
@@ -1,9 +1,8 @@
|
||||
use crate::utils::json::{classify_nullable, NullableValue};
|
||||
use axum::{extract::Path, http::StatusCode, Json};
|
||||
use diesel::{dsl::count_star, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::TenantScopedConn;
|
||||
@@ -12,9 +11,10 @@ use crate::models::{NewTag, Tag};
|
||||
use crate::schema::{document_tags, tags};
|
||||
use crate::utils::db::{no_content, EnsureEntity, IntoJsonResponse};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateTagRequest {
|
||||
pub label: String,
|
||||
#[schema(nullable)]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
@@ -25,14 +25,26 @@ struct UpdateTagChangeset<'a> {
|
||||
color: Option<Option<&'a str>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct TagCatalogEntry {
|
||||
pub id: Uuid,
|
||||
pub label: String,
|
||||
#[schema(nullable)]
|
||||
pub color: Option<String>,
|
||||
pub usage_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, ToSchema)]
|
||||
#[serde(default)]
|
||||
pub struct UpdateTagRequest {
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Option<String>)]
|
||||
pub label: Option<Option<String>>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Option<String>)]
|
||||
pub color: Option<Option<String>>,
|
||||
}
|
||||
|
||||
pub async fn list_tags(
|
||||
TenantScopedConn {
|
||||
mut conn,
|
||||
@@ -121,19 +133,16 @@ pub async fn update_tag(
|
||||
tenant_id,
|
||||
..
|
||||
}: TenantScopedConn,
|
||||
Json(body): Json<Value>,
|
||||
Json(payload): Json<UpdateTagRequest>,
|
||||
) -> AppResult<Json<TagCatalogEntry>> {
|
||||
let existing: Tag = tags::table
|
||||
.find(tag_id)
|
||||
.filter(tags::tenant_id.eq(tenant_id))
|
||||
.first(&mut conn)
|
||||
.one()?;
|
||||
let label_class = classify_nullable(body.get("label")).map_err(AppError::bad_request)?;
|
||||
let color_class = classify_nullable(body.get("color")).map_err(AppError::bad_request)?;
|
||||
let UpdateTagRequest { label, color } = payload;
|
||||
|
||||
if matches!(label_class, NullableValue::Omitted)
|
||||
&& matches!(color_class, NullableValue::Omitted)
|
||||
{
|
||||
if label.is_none() && color.is_none() {
|
||||
let usage_count: i64 = document_tags::table
|
||||
.filter(document_tags::tag_id.eq(tag_id))
|
||||
.select(count_star())
|
||||
@@ -149,12 +158,12 @@ pub async fn update_tag(
|
||||
|
||||
let mut new_label: Option<String> = None;
|
||||
let mut label_changed = false;
|
||||
match label_class {
|
||||
NullableValue::Omitted => {}
|
||||
NullableValue::Null => {
|
||||
match label {
|
||||
None => {}
|
||||
Some(None) => {
|
||||
return Err(AppError::bad_request("label cannot be null"));
|
||||
}
|
||||
NullableValue::String(value) => {
|
||||
Some(Some(value)) => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request("label must not be empty"));
|
||||
@@ -177,13 +186,13 @@ pub async fn update_tag(
|
||||
|
||||
let mut color_change: Option<Option<String>> = None;
|
||||
let mut color_changed = false;
|
||||
match color_class {
|
||||
NullableValue::Omitted => {}
|
||||
NullableValue::Null => {
|
||||
match color {
|
||||
None => {}
|
||||
Some(None) => {
|
||||
color_change = Some(None);
|
||||
color_changed = true;
|
||||
}
|
||||
NullableValue::String(value) => {
|
||||
Some(Some(value)) => {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request("color must not be empty"));
|
||||
|
||||
Reference in New Issue
Block a user