693 lines
23 KiB
Rust
693 lines
23 KiB
Rust
use axum::{
|
|
Router,
|
|
extract::{Path, Query, State},
|
|
response::{Html, IntoResponse, Json},
|
|
routing::{get, post},
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use shared::content::{ContentQuery, ContentState, ContentType, PageContent};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use super::{ContentRenderer, ContentService, TocEntry};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ApiResponse<T> {
|
|
pub success: bool,
|
|
pub data: Option<T>,
|
|
pub message: Option<String>,
|
|
pub errors: Option<Vec<String>>,
|
|
}
|
|
|
|
impl<T> ApiResponse<T> {
|
|
pub fn success(data: T) -> Self {
|
|
Self {
|
|
success: true,
|
|
data: Some(data),
|
|
message: None,
|
|
errors: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ApiResponse<()> {
|
|
pub fn error<U>(message: String) -> ApiResponse<U> {
|
|
ApiResponse {
|
|
success: false,
|
|
data: None,
|
|
message: Some(message),
|
|
errors: None,
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn validation_error<U>(errors: Vec<String>) -> ApiResponse<U> {
|
|
ApiResponse {
|
|
success: false,
|
|
data: None,
|
|
message: Some("Validation failed".to_string()),
|
|
errors: Some(errors),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ContentQueryParams {
|
|
pub content_type: Option<String>,
|
|
pub state: Option<String>,
|
|
pub author_id: Option<String>,
|
|
pub category: Option<String>,
|
|
pub tags: Option<String>,
|
|
pub require_login: Option<bool>,
|
|
pub search: Option<String>,
|
|
pub limit: Option<i64>,
|
|
pub offset: Option<i64>,
|
|
pub sort_by: Option<String>,
|
|
pub sort_order: Option<String>,
|
|
}
|
|
|
|
impl From<ContentQueryParams> for ContentQuery {
|
|
fn from(params: ContentQueryParams) -> Self {
|
|
let mut query = ContentQuery::new();
|
|
|
|
if let Some(content_type) = params.content_type {
|
|
query.content_type = Some(ContentType::from(content_type));
|
|
}
|
|
|
|
if let Some(state) = params.state {
|
|
query.state = Some(ContentState::from(state));
|
|
}
|
|
|
|
if let Some(author_id) = params.author_id {
|
|
if let Ok(uuid) = Uuid::parse_str(&author_id) {
|
|
query.author_id = Some(uuid);
|
|
}
|
|
}
|
|
|
|
if let Some(category) = params.category {
|
|
query.category = Some(category);
|
|
}
|
|
|
|
if let Some(tags) = params.tags {
|
|
let tag_list: Vec<String> = tags.split(',').map(|s| s.trim().to_string()).collect();
|
|
query.tags = Some(tag_list);
|
|
}
|
|
|
|
query.require_login = params.require_login;
|
|
query.search = params.search;
|
|
query.limit = params.limit;
|
|
query.offset = params.offset;
|
|
query.sort_by = params.sort_by;
|
|
query.sort_order = params.sort_order;
|
|
|
|
query
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ContentResponse {
|
|
pub content: PageContent,
|
|
pub rendered_html: String,
|
|
pub table_of_contents: Vec<TocEntry>,
|
|
pub excerpt: String,
|
|
pub reading_time: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ContentListResponse {
|
|
pub contents: Vec<PageContent>,
|
|
pub total_count: i64,
|
|
pub has_more: bool,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateContentRequest {
|
|
pub slug: String,
|
|
pub title: String,
|
|
pub name: String,
|
|
pub author: Option<String>,
|
|
pub author_id: Option<Uuid>,
|
|
pub content_type: String,
|
|
pub content_format: Option<String>,
|
|
pub content: String,
|
|
pub container: String,
|
|
pub state: Option<String>,
|
|
pub require_login: Option<bool>,
|
|
pub tags: Option<Vec<String>>,
|
|
pub category: Option<String>,
|
|
pub featured_image: Option<String>,
|
|
pub excerpt: Option<String>,
|
|
pub seo_title: Option<String>,
|
|
pub seo_description: Option<String>,
|
|
pub allow_comments: Option<bool>,
|
|
pub sort_order: Option<i32>,
|
|
pub metadata: Option<HashMap<String, String>>,
|
|
}
|
|
|
|
pub fn create_content_routes() -> Router<Arc<ContentService>> {
|
|
Router::new()
|
|
.route("/contents", get(list_contents).post(create_content))
|
|
.route("/contents/:id", get(get_content_by_id))
|
|
.route("/contents/slug/:slug", get(get_content_by_slug))
|
|
.route("/contents/slug/:slug/render", get(render_content_by_slug))
|
|
.route("/contents/search", get(search_contents))
|
|
.route("/contents/published", get(get_published_contents))
|
|
.route("/contents/stats", get(get_content_stats))
|
|
.route("/contents/tags", get(get_all_tags))
|
|
.route("/contents/categories", get(get_all_categories))
|
|
.route("/contents/type/:content_type", get(get_contents_by_type))
|
|
.route(
|
|
"/contents/category/:category",
|
|
get(get_contents_by_category),
|
|
)
|
|
.route("/contents/author/:author_id", get(get_contents_by_author))
|
|
.route("/contents/recent", get(get_recent_contents))
|
|
.route("/contents/popular", get(get_popular_contents))
|
|
.route("/contents/:id/increment-view", post(increment_view_count))
|
|
.route("/contents/:id/render", get(render_content_by_id))
|
|
.route("/contents/:id/toc", get(get_table_of_contents))
|
|
.route("/contents/reload", post(reload_content))
|
|
.route(
|
|
"/contents/publish-scheduled",
|
|
post(publish_scheduled_content),
|
|
)
|
|
}
|
|
|
|
pub async fn list_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Query(params): Query<ContentQueryParams>,
|
|
) -> impl IntoResponse {
|
|
let query = ContentQuery::from(params);
|
|
|
|
match service.query_contents(&query).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
let has_more = query.limit.map_or(false, |limit| total_count >= limit);
|
|
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to list contents: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve contents".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_content_by_id(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
match service.get_content_by_id(id).await {
|
|
Ok(Some(content)) => Json(ApiResponse::success(content)),
|
|
Ok(None) => Json(ApiResponse::error("Content not found".to_string())),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content by ID {}: {}", id, e);
|
|
Json(ApiResponse::error("Failed to retrieve content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_content_by_slug(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(slug): Path<String>,
|
|
) -> impl IntoResponse {
|
|
match service.get_content_by_slug(&slug).await {
|
|
Ok(Some(content)) => Json(ApiResponse::success(content)),
|
|
Ok(None) => Json(ApiResponse::error("Content not found".to_string())),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content by slug {}: {}", slug, e);
|
|
Json(ApiResponse::error("Failed to retrieve content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn render_content_by_slug(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(slug): Path<String>,
|
|
) -> impl IntoResponse {
|
|
match service.get_content_by_slug(&slug).await {
|
|
Ok(Some(content)) => {
|
|
let renderer = ContentRenderer::new();
|
|
|
|
match renderer.render_content(&content) {
|
|
Ok(rendered_html) => {
|
|
let table_of_contents = renderer
|
|
.generate_table_of_contents(&content)
|
|
.unwrap_or_default();
|
|
let excerpt = renderer.extract_excerpt(&content, 200);
|
|
|
|
// Calculate reading time (rough estimate: 200 words per minute)
|
|
let word_count = content.content.split_whitespace().count();
|
|
let reading_time = Some(((word_count as f32 / 200.0).ceil() as i32).max(1));
|
|
|
|
Json(ApiResponse::success(ContentResponse {
|
|
content,
|
|
rendered_html,
|
|
table_of_contents,
|
|
excerpt,
|
|
reading_time,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to render content: {}", e);
|
|
Json(ApiResponse::error("Failed to render content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
Ok(None) => Json(ApiResponse::error("Content not found".to_string())),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content by slug {}: {}", slug, e);
|
|
Json(ApiResponse::error("Failed to retrieve content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn render_content_by_id(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
match service.get_content_by_id(id).await {
|
|
Ok(Some(content)) => {
|
|
let renderer = ContentRenderer::new();
|
|
|
|
match renderer.render_content(&content) {
|
|
Ok(rendered_html) => Html(rendered_html),
|
|
Err(e) => {
|
|
tracing::error!("Failed to render content: {}", e);
|
|
Html("<p>Error rendering content</p>".to_string())
|
|
}
|
|
}
|
|
}
|
|
Ok(None) => Html("<p>Content not found</p>".to_string()),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content by ID {}: {}", id, e);
|
|
Html("<p>Error retrieving content</p>".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn create_content(
|
|
State(service): State<Arc<ContentService>>,
|
|
Json(request): Json<CreateContentRequest>,
|
|
) -> impl IntoResponse {
|
|
let content_type = ContentType::from(request.content_type);
|
|
let content_format = request
|
|
.content_format
|
|
.map(shared::content::ContentFormat::from)
|
|
.unwrap_or(shared::content::ContentFormat::Markdown);
|
|
let state = request
|
|
.state
|
|
.map(ContentState::from)
|
|
.unwrap_or(ContentState::Draft);
|
|
|
|
let mut content = PageContent::new(
|
|
request.slug,
|
|
request.title,
|
|
request.name,
|
|
content_type,
|
|
request.content,
|
|
request.container,
|
|
request.author_id,
|
|
);
|
|
|
|
content.author = request.author;
|
|
content.content_format = content_format;
|
|
content.state = state;
|
|
content.require_login = request.require_login.unwrap_or(false);
|
|
content.tags = request.tags.unwrap_or_default();
|
|
content.category = request.category;
|
|
content.featured_image = request.featured_image;
|
|
content.excerpt = request.excerpt;
|
|
content.seo_title = request.seo_title;
|
|
content.seo_description = request.seo_description;
|
|
content.allow_comments = request.allow_comments.unwrap_or(true);
|
|
content.sort_order = request.sort_order.unwrap_or(0);
|
|
content.metadata = request.metadata.unwrap_or_default();
|
|
|
|
match service.create_content(&content).await {
|
|
Ok(()) => Json(ApiResponse::success(content)),
|
|
Err(e) => {
|
|
tracing::error!("Failed to create content: {}", e);
|
|
Json(ApiResponse::error("Failed to create content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: Update and delete endpoints removed for now to fix compilation
|
|
// They can be re-added later with proper implementation
|
|
|
|
pub async fn search_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
let search_term = params.get("q").cloned().unwrap_or_default();
|
|
let limit = params
|
|
.get("limit")
|
|
.and_then(|l| l.parse::<i64>().ok())
|
|
.unwrap_or(20);
|
|
|
|
if search_term.is_empty() {
|
|
return Json(ApiResponse::error("Search term is required".to_string()));
|
|
}
|
|
|
|
match service.search_contents(&search_term, Some(limit)).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: total_count >= limit,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to search contents: {}", e);
|
|
Json(ApiResponse::error("Failed to search contents".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_published_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
let limit = params
|
|
.get("limit")
|
|
.and_then(|l| l.parse::<i64>().ok())
|
|
.unwrap_or(50);
|
|
|
|
match service.get_published_contents(Some(limit)).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: total_count >= limit,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get published contents: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve published contents".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_content_stats(State(service): State<Arc<ContentService>>) -> impl IntoResponse {
|
|
match service.get_content_stats().await {
|
|
Ok(stats) => Json(ApiResponse::success(stats)),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content stats: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve content statistics".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_all_tags(State(service): State<Arc<ContentService>>) -> impl IntoResponse {
|
|
match service.get_all_tags().await {
|
|
Ok(tags) => Json(ApiResponse::success(tags)),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get tags: {}", e);
|
|
Json(ApiResponse::error("Failed to retrieve tags".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_all_categories(State(service): State<Arc<ContentService>>) -> impl IntoResponse {
|
|
match service.get_all_categories().await {
|
|
Ok(categories) => Json(ApiResponse::success(categories)),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get categories: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve categories".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_contents_by_type(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(content_type): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let content_type = ContentType::from(content_type);
|
|
|
|
match service.get_contents_by_type(content_type).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: false,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get contents by type: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve contents by type".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_contents_by_category(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(category): Path<String>,
|
|
) -> impl IntoResponse {
|
|
match service.get_contents_by_category(&category).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: false,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get contents by category: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve contents by category".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_contents_by_author(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(author_id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
match service.get_contents_by_author(author_id).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: false,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get contents by author: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve contents by author".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_recent_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
let limit = params
|
|
.get("limit")
|
|
.and_then(|l| l.parse::<i64>().ok())
|
|
.unwrap_or(10);
|
|
|
|
match service.get_recent_contents(limit).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: total_count >= limit,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get recent contents: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve recent contents".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_popular_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Query(params): Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
let limit = params
|
|
.get("limit")
|
|
.and_then(|l| l.parse::<i64>().ok())
|
|
.unwrap_or(10);
|
|
|
|
match service.get_popular_contents(limit).await {
|
|
Ok(contents) => {
|
|
let total_count = contents.len() as i64;
|
|
Json(ApiResponse::success(ContentListResponse {
|
|
contents,
|
|
total_count,
|
|
has_more: total_count >= limit,
|
|
}))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get popular contents: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to retrieve popular contents".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn increment_view_count(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
match service.increment_view_count(id).await {
|
|
Ok(()) => Json(ApiResponse::success("View count incremented")),
|
|
Err(e) => {
|
|
tracing::error!("Failed to increment view count: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to increment view count".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_table_of_contents(
|
|
State(service): State<Arc<ContentService>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse {
|
|
match service.get_content_by_id(id).await {
|
|
Ok(Some(content)) => {
|
|
let renderer = ContentRenderer::new();
|
|
match renderer.generate_table_of_contents(&content) {
|
|
Ok(toc) => Json(ApiResponse::success(toc)),
|
|
Err(e) => {
|
|
tracing::error!("Failed to generate table of contents: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to generate table of contents".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
Ok(None) => Json(ApiResponse::error("Content not found".to_string())),
|
|
Err(e) => {
|
|
tracing::error!("Failed to get content for TOC: {}", e);
|
|
Json(ApiResponse::error("Failed to retrieve content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn reload_content(State(service): State<Arc<ContentService>>) -> impl IntoResponse {
|
|
match service.reload_file_content().await {
|
|
Ok(()) => Json(ApiResponse::success("Content reloaded successfully")),
|
|
Err(e) => {
|
|
tracing::error!("Failed to reload content: {}", e);
|
|
Json(ApiResponse::error("Failed to reload content".to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn publish_scheduled_content(
|
|
State(service): State<Arc<ContentService>>,
|
|
) -> impl IntoResponse {
|
|
match service.publish_scheduled_content().await {
|
|
Ok(published_contents) => {
|
|
let count = published_contents.len();
|
|
Json(ApiResponse::success(format!(
|
|
"Published {} scheduled contents",
|
|
count
|
|
)))
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to publish scheduled content: {}", e);
|
|
Json(ApiResponse::error(
|
|
"Failed to publish scheduled content".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_content_query_params_conversion() {
|
|
let params = ContentQueryParams {
|
|
content_type: Some("blog".to_string()),
|
|
state: Some("published".to_string()),
|
|
author_id: None,
|
|
category: Some("tech".to_string()),
|
|
tags: Some("rust,web".to_string()),
|
|
require_login: Some(false),
|
|
search: Some("test".to_string()),
|
|
limit: Some(10),
|
|
offset: Some(0),
|
|
sort_by: Some("created_at".to_string()),
|
|
sort_order: Some("DESC".to_string()),
|
|
};
|
|
|
|
let query = ContentQuery::from(params);
|
|
assert_eq!(query.content_type, Some(ContentType::Blog));
|
|
assert_eq!(query.state, Some(ContentState::Published));
|
|
assert_eq!(query.category, Some("tech".to_string()));
|
|
assert_eq!(
|
|
query.tags,
|
|
Some(vec!["rust".to_string(), "web".to_string()])
|
|
);
|
|
assert_eq!(query.require_login, Some(false));
|
|
assert_eq!(query.search, Some("test".to_string()));
|
|
assert_eq!(query.limit, Some(10));
|
|
assert_eq!(query.offset, Some(0));
|
|
assert_eq!(query.sort_by, Some("created_at".to_string()));
|
|
assert_eq!(query.sort_order, Some("DESC".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_api_response_creation() {
|
|
let success_response = ApiResponse::success("test data");
|
|
assert!(success_response.success);
|
|
assert_eq!(success_response.data, Some("test data"));
|
|
assert!(success_response.message.is_none());
|
|
assert!(success_response.errors.is_none());
|
|
|
|
let error_response: ApiResponse<()> = ApiResponse::error("test error".to_string());
|
|
assert!(!error_response.success);
|
|
assert!(error_response.data.is_none());
|
|
assert_eq!(error_response.message, Some("test error".to_string()));
|
|
assert!(error_response.errors.is_none());
|
|
|
|
let validation_response: ApiResponse<()> =
|
|
ApiResponse::validation_error(vec!["error1".to_string(), "error2".to_string()]);
|
|
assert!(!validation_response.success);
|
|
assert!(validation_response.data.is_none());
|
|
assert_eq!(
|
|
validation_response.message,
|
|
Some("Validation failed".to_string())
|
|
);
|
|
assert_eq!(
|
|
validation_response.errors,
|
|
Some(vec!["error1".to_string(), "error2".to_string()])
|
|
);
|
|
}
|
|
}
|