Skip to main content

fren_date/
lib.rs

1//! `fren` - file renamer that understands dates.
2//!
3//! Library crate. The CLI binary lives in `fren-cli` and is a thin consumer
4//! of this library.
5//!
6//! ## Quick example
7//!
8//! ```
9//! use fren_date::{slugify_camel_iso_with_year, SlugOpts};
10//! use slug_preserve::CaseMode;
11//!
12//! let legacy_python = SlugOpts {
13//!     separator: '_',
14//!     case: CaseMode::Capitalize,
15//!     split_camel: true,
16//! };
17//! let out = slugify_camel_iso_with_year("Hello World 2024-01-15.txt", &legacy_python, 2024);
18//! assert!(out.starts_with("Hello"));
19//! ```
20//!
21//! ## Library discipline
22//!
23//! This crate must obey:
24//! - No `panic!` on user input - return `Err(FrenError::*)`
25//! - No `println!`/`eprintln!` - output flows through trait objects
26//! - No global state - all configuration via opts structs
27
28#![deny(missing_docs)]
29#![deny(rustdoc::broken_intra_doc_links)]
30
31pub use slug_preserve::{CaseMode, SlugOpts};
32
33mod error;
34pub use error::FrenError;
35
36mod opts;
37pub use opts::{ConflictPolicy, PlanOpts, RenameOpts};
38
39mod plan_types;
40pub use plan_types::{DateKind, DetectedDate, ItemKind, RenamePlan};
41
42mod report;
43pub use report::ExecutionReport;
44
45mod date;
46mod execute;
47mod log;
48mod merge;
49mod plan;
50mod slugify;
51
52pub use execute::{
53    execute, execute_with_log, execute_with_progress, NullProgressSink, ProgressSink,
54};
55pub use log::{JsonlLogSink, LogRecord, LogSink, NullLogSink};
56pub use merge::{merge_directories, unique_file_name, MergeMove, MergeReport};
57pub use plan::{plan, plan_with_year, sort_bottom_up};
58pub use slugify::{slugify_camel_iso, slugify_camel_iso_with_year};
59
60use std::path::Path;
61
62/// High-level entry point: plan + (optionally) execute a rename batch.
63///
64/// If `opts.apply` is `false`, returns the plan vector wrapped in an
65/// `ExecutionReport` with `applied = 0`; the caller is responsible for
66/// displaying the dry-run preview.
67///
68/// If `opts.apply` is `true`, executes the plan and returns a real report.
69pub fn rename(
70    roots: &[&Path],
71    opts: &RenameOpts,
72) -> Result<(Vec<RenamePlan>, ExecutionReport), FrenError> {
73    let plans = plan(roots, &opts.slugify, &opts.plan)?;
74    let report = if opts.apply {
75        execute(&plans)?
76    } else {
77        ExecutionReport {
78            applied: 0,
79            skipped: 0,
80            errors: Vec::new(),
81            batch_id: plans
82                .first()
83                .map(|p| p.batch_id)
84                .unwrap_or_else(uuid::Uuid::nil),
85            log_path: None,
86        }
87    };
88    Ok((plans, report))
89}
90
91/// Slugify a single string with the given options.
92///
93/// Wrapper around [`slug_preserve::slugify`]. See that crate's docs for
94/// details. This is re-exported so library users don't need to depend on
95/// `slug-preserve` directly.
96#[must_use]
97pub fn slugify_str(input: &str, opts: &SlugOpts) -> String {
98    slug_preserve::slugify(input, opts)
99}