Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,17 @@
# Linker Configuration
# =============================================================================
# Using faster linkers significantly reduces link time during development.
# - macOS: lld (install with: brew install lld)
# - Linux: mold (install with: apt install mold)
#
# If the linker is not installed, Cargo will fall back to the default linker.
#
# macOS uses the system linker. lld is not an option here: against the macOS 27
# SDK it fails to resolve libSystem primitives (bzero, memset, lchown, fchown),
# so every binary and test link dies with hundreds of undefined symbols. This is
# not a version lag that an upgrade clears, and Apple's ld is fast enough that
# the lost link time does not justify a broken build.
# =============================================================================

# macOS (Apple Silicon)
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

# macOS (Intel)
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

# Linux (x86_64) - used in CI
[target.x86_64-unknown-linux-gnu]
linker = "clang"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fn default_true() -> bool {

/// Query parameters for `GET /api/v1/reading-progress/export`.
#[derive(Debug, Clone, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportReadingProgressQuery {
/// Sessions are opt-out: they are the only source of every reading
/// statistic, so leaving them out is easy to do by accident and hard to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use codex_services::reading_transfer::import::{ImportError, ImportOptions};
get,
path = "/api/v1/reading-progress/export",
params(
("include_sessions" = Option<bool>, Query, description = "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.")
("includeSessions" = Option<bool>, Query, description = "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.")
),
responses(
(status = 200, description = "The export document", body = codex_services::reading_transfer::model::ReadingProgressExportDocument),
Expand Down Expand Up @@ -89,8 +89,8 @@ pub async fn export_reading_progress(
/// ever written against it.
///
/// Matching never guesses: any step (external id, path, file name, or, under
/// `hash_mode = "match"`, hash) that finds more than one candidate reports
/// `ambiguous` and writes nothing for that series or book. `dry_run: true`
/// `hashMode = "match"`, hash) that finds more than one candidate reports
/// `ambiguous` and writes nothing for that series or book. `dryRun: true`
/// returns the identical response shape without writing anything, which is
/// what makes it safe to preview before committing.
///
Expand Down
38 changes: 26 additions & 12 deletions crates/codex-services/src/reading_transfer/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
//! API layer only serializes what the service already built. `codex-api`
//! re-exports this module under its `dto` namespace for OpenAPI registration.
//!
//! The document itself is a portable file meant to survive a library
//! reorganisation or a move to a different Codex instance, so its field names
//! are deliberately **not** camelCased like the rest of the API's DTOs: it is
//! a versioned interchange format rather than a shape a frontend deserializes,
//! and plain `snake_case` matches what anyone writing a compatible tool would
//! expect from the documented shape.
//! The document is a portable file meant to survive a library reorganisation
//! or a move to a different Codex instance, but it is an ordinary API payload
//! too: the export returns it as a response body and the import nests it in
//! its request. So the structs here are `camelCase` on the wire like every
//! other DTO, and a single import body does not carry two naming conventions
//! with an invisible boundary between them. The enums stay `snake_case`,
//! matching how enum variants are spelled everywhere else in the API.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
Expand All @@ -29,6 +30,7 @@ pub const READING_PROGRESS_VERSION: i32 = 1;
/// One external identifier attached to a series (a plugin match, a ComicInfo
/// value, or a manual entry).
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportExternalIdDto {
/// `plugin:<name>`, `comicinfo`, `epub`, or `manual`.
#[schema(example = "plugin:mangabaka")]
Expand All @@ -40,6 +42,7 @@ pub struct ExportExternalIdDto {
/// The live resume position for one book. Retains `r2_progression`: it is the
/// only place the EPUB locator survives, since sessions strip it.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportProgressDto {
pub current_page: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
Expand All @@ -56,6 +59,7 @@ pub struct ExportProgressDto {
/// One finished read-through. Keeps its original id so re-importing the same
/// file is a no-op rather than a duplicate.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportCompletionDto {
pub id: Uuid,
pub started_at: DateTime<Utc>,
Expand All @@ -66,6 +70,7 @@ pub struct ExportCompletionDto {
/// absent: nothing reads a session's historical locator, and it is the only
/// non-scalar column on the row.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportSessionDto {
pub id: Uuid,
pub device_id: String,
Expand Down Expand Up @@ -95,6 +100,7 @@ pub struct ExportSessionDto {
/// rather than by id: the whole point of the file is that ids on the far side
/// are expected to be different.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportBookDto {
/// Relative to the series folder, so a series move does not invalidate it.
#[schema(example = "Vol 01/v01.cbz")]
Expand All @@ -112,13 +118,14 @@ pub struct ExportBookDto {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub completions: Vec<ExportCompletionDto>,
/// Omitted entirely (not an empty array) when the export was taken with
/// `include_sessions=false`.
/// `includeSessions=false`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sessions: Option<Vec<ExportSessionDto>>,
}

/// One series and everything the exporting user recorded against its books.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportSeriesDto {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub external_ids: Vec<ExportExternalIdDto>,
Expand All @@ -143,6 +150,7 @@ pub struct ExportSeriesDto {
/// The whole export: one user's reading state, self-describing enough to be
/// matched back against a differently organised library.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ReadingProgressExportDocument {
#[schema(example = "codex-reading-progress")]
pub format: String,
Expand Down Expand Up @@ -190,6 +198,7 @@ fn default_reattach_sessions() -> bool {

/// `POST /api/v1/reading-progress/import` request body.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportReadingProgressRequest {
/// Compute and report the outcome without writing anything.
#[serde(default)]
Expand Down Expand Up @@ -236,12 +245,12 @@ pub enum SeriesDisposition {
pub enum BookDisposition {
Matched,
/// Matched only by filename stem (e.g. a `.cbr` repacked to `.cbz`).
/// Applied only when `accept_stem_matches` is set.
/// Applied only when `acceptStemMatches` is set.
StemMatch,
Ambiguous,
Unmatched,
/// A single candidate was found by path or name, but its `file_hash`
/// disagreed with the export under `hash_mode = "verify"` or `"match"`.
/// disagreed with the export under `hashMode = "verify"` or `"match"`.
HashMismatch,
}

Expand All @@ -259,6 +268,7 @@ pub enum FieldOutcome {
/// Insert/reattach/skip counts for an append-only table (completions or
/// sessions) within one book.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct WriteCounts {
pub inserted: u32,
/// Adopted an orphaned row (`book_id IS NULL`) rather than inserting a
Expand All @@ -270,6 +280,7 @@ pub struct WriteCounts {

/// The outcome for one book in the import file.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportBookReport {
pub path: String,
pub file_name: String,
Expand All @@ -278,7 +289,7 @@ pub struct ImportBookReport {
pub matched_book_id: Option<Uuid>,
/// Whether any writes were attempted for this book. False for every
/// disposition except `matched`, and except `stem_match` when
/// `accept_stem_matches` is off.
/// `acceptStemMatches` is off.
pub applied: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress: Option<FieldOutcome>,
Expand All @@ -288,6 +299,7 @@ pub struct ImportBookReport {

/// The outcome for one series in the import file.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportSeriesReport {
pub library_relative_path: String,
pub name: String,
Expand All @@ -309,6 +321,7 @@ pub struct ImportSeriesReport {

/// Totals across every series in the file, for a one-line summary.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportSummary {
pub series_total: u32,
pub series_matched: u32,
Expand All @@ -331,12 +344,13 @@ pub struct ImportSummary {

/// The response for both a real import and a dry run: the shape is identical
/// either way, so a client cannot tell from the response alone whether
/// anything was written. Only `dry_run` (and the DB) says that.
/// anything was written. Only `dryRun` (and the DB) says that.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ImportReadingProgressResponse {
pub dry_run: bool,
pub sessions_in_file: bool,
/// Informational notes about the request, e.g. `reattach_sessions` having
/// Informational notes about the request, e.g. `reattachSessions` having
/// no effect because the file carries no sessions.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notices: Vec<String>,
Expand Down
Loading
Loading