Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Migrating from Swift to Rust

The Rust crate preserves the observable Specification Pattern behavior, not the Swift source API. The Swift package is a behavioral reference; ownership, trait bounds, and concurrency guarantees follow Rust conventions.

Core mapping

Swift SpecificationCoreRust specification-corePractical difference
SpecificationSpecification<T>Swift receives T; Rust borrows &T
PredicateSpec / spec {}Fn(&T) -> boolClosures implement the trait directly
AndSpecification / OrSpecification / NotSpecificationAnd / Or / NotBoth short-circuit; Rust uses named methods
AnySpecification<T>BoxedSpecification<T>Explicit owned trait-object boundary
—SharedSpecification<T>Explicit Arc + Send + Sync sharing boundary
DecisionSpecDecisionSpecification<T>Rust returns Option<&Decision>
FirstMatchSpecFirstMatch<T, Decision>Same ordered first-match behavior
EvaluationContextEvaluationContext<UserData>Generic typed data instead of [String: Any]
DefaultContextProvider.sharedapplication-owned snapshotNo global mutable context provider
Date in contextinjected Clock + DurationDeterministic, application-defined epoch
async throwsFuture<Output = Result<bool, Error>>Typed errors and Send futures
@Satisfies, @Maybe, @Decidesexplicit method callsNo hidden context acquisition in property access
@specs, @AutoContext#[specification(Name)]Rust macro is intentionally narrower

Composition

Swift:

let adult = PredicateSpec<User> { $0.age >= 18 }
let verified = PredicateSpec<User> { $0.verified }
let eligible = adult.and(verified)

let result = eligible.isSatisfiedBy(user)

Rust:

use specification_core::Specification;

let adult = |user: &User| user.age >= 18;
let verified = |user: &User| user.verified;
let eligible = adult.and(verified);

let result = eligible.is_satisfied_by(&user);

Swift’s protocol method is by-value at the API boundary. Rust’s borrowed candidate makes ownership explicit and lets a rule inspect a large value without taking it from the caller.

Type erasure and concurrency

AnySpecification is the usual Swift answer for heterogeneous collections. In Rust choose deliberately:

use specification_core::{BoxedSpecification, SharedSpecification};

let local = BoxedSpecification::new(|user: &User| user.age >= 18);
let shared = SharedSpecification::new(|user: &User| user.age >= 18);

BoxedSpecification owns a local Box<dyn Specification<T>>. The shared form owns Arc<dyn Specification<T> + Send + Sync> and rejects non-thread-safe captures at compile time. A regular specification does not need to be Send or Sync, which keeps single-threaded rules flexible.

Context providers and property wrappers

Swift commonly obtains a fresh snapshot through DefaultContextProvider.shared and evaluates it through a property wrapper. Rust keeps both operations visible:

let context = EvaluationContext::new(user)
    .with_flag("enabled", true)
    .with_counter("attempts", 1);

let enabled = Flag::new("enabled")
    .is_satisfied_by(&context);

Mutation and refresh of counters remain application responsibilities. This avoids hidden global state and makes a test’s inputs explicit.

Async and errors

Swift’s throws can carry any error conforming to Error. Rust associates one concrete Error type with each async specification; composing rules therefore requires a shared application error enum. Rust also requires the future to be Send and the candidate to be Sync, making executor/thread assumptions visible in the API.

The Rust async layer does not select Tokio or another runtime. The caller awaits the future using the runtime already chosen by the application.

What is intentionally not identical

Rust 0.1.0 does not port Swift property wrappers, Combine observation, global providers, segments, or the full date-rule convenience surface. Those features are platform or application concerns. Cross-language conformance fixtures cover observable synchronous behavior—composition, first-match, context defaults, and cooldown boundaries—rather than requiring identical types or serialization.

For the historical source inventory and the full adopt/adapt/defer/exclude matrix, see the maintainer-only DocC Migration Map and Swift Reference Audit.