Skip to content

feat(api): Implement MagicDatabase builder pattern for fluent database construction #45

@unclesp1d3r

Description

@unclesp1d3r

Summary

Add a fluent builder API to MagicDatabase so callers can compose configuration and rule loading in a readable, chainable style — complementing the existing convenience constructors without breaking any current call sites.

Background & Motivation

The current MagicDatabase API provides four static constructors:

Constructor Behaviour
with_builtin_rules() Built-in compiled rules, default config
with_builtin_rules_and_config(config) Built-in rules, custom config
load_from_file(path) External magic file, default config
load_from_file_with_config(path, config) External magic file, custom config

These work well for simple cases. However, as configuration options grow (timeouts, MIME-type toggling, depth limits, etc.) the call-site becomes unwieldy when callers need to compose multiple options before deciding on a rule source. A builder provides:

  • A natural left-to-right reading order: create → configure → load
  • Early Result propagation per step (invalid configs fail fast)
  • Extensibility — new builder methods can be added without new top-level constructors
  • Alignment with idiomatic Rust patterns used across the ecosystem (e.g. reqwest::ClientBuilder, tokio::runtime::Builder)

This issue tracks Core Flow 5 (Library API — Advanced Usage) from the Phase 1 spec and is part of Epic #24.

Proposed API

// Minimal usage — load from path, default config
let db = MagicDatabase::new()
    .load("/usr/share/file/magic/Magdir/")?;

// With explicit configuration
let db = MagicDatabase::new()
    .with_config(EvaluationConfig::performance())?
    .load("/usr/share/file/magic/Magdir/")?;

// With built-in rules
let db = MagicDatabase::new()
    .with_config(config)?
    .with_builtin_rules()?;

All existing constructors (load_from_file, load_from_file_with_config, with_builtin_rules, with_builtin_rules_and_config) are unchanged — they remain as convenience methods.

Implementation Plan

1. Introduce MagicDatabaseBuilder in src/lib.rs

/// Builder for `MagicDatabase` — use `MagicDatabase::new()` to obtain one.
#[derive(Debug, Default)]
pub struct MagicDatabaseBuilder {
    config: EvaluationConfig,
}

impl MagicDatabaseBuilder {
    /// Apply and validate an `EvaluationConfig`.
    ///
    /// Returns `Err` immediately if the config fails validation
    /// (e.g. `timeout_ms` is `Some(0)`).
    pub fn with_config(mut self, config: EvaluationConfig) -> Result<Self> {
        config.validate()?;
        self.config = config;
        Ok(self)
    }

    /// Load magic rules from a file or directory and build the database.
    ///
    /// Delegates to `MagicDatabase::load_from_file_with_config`.
    pub fn load<P: AsRef<Path>>(self, path: P) -> Result<MagicDatabase> {
        MagicDatabase::load_from_file_with_config(path, self.config)
    }

    /// Build the database using built-in compiled rules.
    ///
    /// Delegates to `MagicDatabase::with_builtin_rules_and_config`.
    pub fn with_builtin_rules(self) -> Result<MagicDatabase> {
        MagicDatabase::with_builtin_rules_and_config(self.config)
    }
}

2. Add MagicDatabase::new()

impl MagicDatabase {
    /// Create a new builder for `MagicDatabase`.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use libmagic_rs::{MagicDatabase, EvaluationConfig};
    ///
    /// let db = MagicDatabase::new()
    ///     .with_config(EvaluationConfig::performance())?
    ///     .load("/usr/share/file/magic/Magdir/")?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    #[must_use]
    pub fn new() -> MagicDatabaseBuilder {
        MagicDatabaseBuilder::default()
    }
    // … existing methods unchanged …
}

3. Unit tests (in src/tests.rs)

  • test_builder_default_config_loadMagicDatabase::new().load(path) succeeds
  • test_builder_with_configwith_config(EvaluationConfig::performance()) applies config correctly
  • test_builder_invalid_config_fails_fast — zero-ms timeout returns Err from with_config
  • test_builder_with_builtin_rulesMagicDatabase::new().with_builtin_rules() matches with_builtin_rules()
  • test_builder_chaining_is_equivalent_to_convenience_constructor — builder result equals static constructor result

Acceptance Criteria

  • MagicDatabase::new() returns a MagicDatabaseBuilder
  • MagicDatabaseBuilder::with_config(config) validates config and returns Result<Self>
  • MagicDatabaseBuilder::load(path) loads rules and returns Result<MagicDatabase>
  • MagicDatabaseBuilder::with_builtin_rules() returns Result<MagicDatabase>
  • Existing convenience constructors are not modified
  • All public builder methods have rustdoc examples
  • Unit tests listed above are implemented and passing
  • #[deny(missing_docs)] remains satisfied (doc all new public items)

Files to Modify

  • src/lib.rs — add MagicDatabaseBuilder struct, MagicDatabase::new(), and builder methods
  • src/tests.rs — add builder unit tests

References

  • Core Flows spec: Flow 5 (Library API — Advanced Usage) — docs/plan/rust_libmagic_spec.md
  • Epic: Epic: libmagic-rs Phase 1 MVP #24
  • EvaluationConfig::validate()src/config.rs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for Feature.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions