movie detail page + importer architecture fix

This commit is contained in:
2026-05-10 23:59:26 +02:00
parent f2f1317660
commit b2a2aa4262
49 changed files with 1670 additions and 264 deletions

View File

@@ -1,13 +0,0 @@
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("CSV parse error: {0}")]
Csv(String),
#[error("JSON parse error: {0}")]
Json(String),
#[error("XLSX parse error: {0}")]
Xlsx(String),
#[error("Empty file")]
Empty,
#[error("Missing header row")]
NoHeader,
}

View File

@@ -1,12 +1,28 @@
pub mod error;
pub mod mapper;
pub mod parsers;
pub mod types;
mod mapper;
mod parsers;
pub use error::ImportError;
pub use mapper::apply_mapping;
pub use parsers::{parse_csv, parse_json};
pub use types::{AnnotatedRow, DomainField, FieldMapping, ImportRow, ParsedFile, RowResult, Transform};
use domain::{
models::{AnnotatedRow, FieldMapping, FileFormat, ImportError, ParsedFile},
ports::DocumentParser,
};
#[cfg(feature = "xlsx")]
pub use parsers::parse_xlsx;
pub struct ImporterDocumentParser;
impl DocumentParser for ImporterDocumentParser {
fn parse(&self, bytes: &[u8], format: FileFormat) -> Result<ParsedFile, ImportError> {
match format {
FileFormat::Csv => parsers::parse_csv(bytes),
FileFormat::Json => parsers::parse_json(bytes),
FileFormat::Xlsx => {
#[cfg(feature = "xlsx")]
{ parsers::parse_xlsx(bytes) }
#[cfg(not(feature = "xlsx"))]
{ Err(ImportError::Xlsx("XLSX support not compiled in".into())) }
}
}
}
fn apply_mapping(&self, file: &ParsedFile, mappings: &[FieldMapping]) -> Vec<AnnotatedRow> {
mapper::apply_mapping(file, mappings)
}
}

View File

@@ -1,4 +1,6 @@
use crate::types::{AnnotatedRow, DomainField, FieldMapping, ImportRow, ParsedFile, RowResult, Transform};
use domain::models::{
AnnotatedRow, DomainField, FieldMapping, ImportRow, ParsedFile, RowResult, Transform,
};
pub fn apply_mapping(file: &ParsedFile, mappings: &[FieldMapping]) -> Vec<AnnotatedRow> {
file.rows.iter().map(|row| {
@@ -76,7 +78,7 @@ fn set_field(row: &mut ImportRow, field: &DomainField, value: String) {
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{DomainField, FieldMapping, ParsedFile, RowResult, Transform};
use domain::models::{DomainField, FieldMapping, ParsedFile, RowResult, Transform};
fn sample_file() -> ParsedFile {
ParsedFile {

View File

@@ -1,4 +1,4 @@
use crate::{ImportError, types::ParsedFile};
use domain::models::{ImportError, ParsedFile};
pub fn parse_csv(bytes: &[u8]) -> Result<ParsedFile, ImportError> {
if bytes.is_empty() {

View File

@@ -1,5 +1,5 @@
use domain::models::{ImportError, ParsedFile};
use serde_json::Value;
use crate::{ImportError, types::ParsedFile};
pub fn parse_json(bytes: &[u8]) -> Result<ParsedFile, ImportError> {
let value: Value = serde_json::from_slice(bytes)

View File

@@ -1,6 +1,6 @@
use calamine::{Reader, open_workbook_from_rs, Xlsx, Data};
use std::io::Cursor;
use crate::{ImportError, types::ParsedFile};
use domain::models::{ImportError, ParsedFile};
pub fn parse_xlsx(bytes: &[u8]) -> Result<ParsedFile, ImportError> {
let cursor = Cursor::new(bytes);

View File

@@ -1,57 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedFile {
pub columns: Vec<String>,
pub rows: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DomainField {
Title,
ReleaseYear,
Director,
Rating,
WatchedAt,
Comment,
ExternalMetadataId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Transform {
RatingScale(f64),
DateFormat(String),
Identity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldMapping {
pub source_column: String,
pub domain_field: DomainField,
pub transform: Transform,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ImportRow {
pub title: Option<String>,
pub release_year: Option<String>,
pub director: Option<String>,
pub rating: Option<String>,
pub watched_at: Option<String>,
pub comment: Option<String>,
pub external_metadata_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RowResult {
Valid(ImportRow),
Invalid { errors: Vec<String>, raw: Vec<(String, String)> },
}
/// Wraps a RowResult with a duplicate flag so this information persists when
/// serialised as JSON into the import_sessions.row_results DB column.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnnotatedRow {
pub result: RowResult,
pub is_duplicate: bool,
}