docs/core/ops.mdx
The operations system automatically generates type-safe Swift and TypeScript clients from Rust API definitions. Define your API once in Rust and get native clients for iOS, web, and desktop without manual synchronization.
The system uses compile-time type extraction to discover all operations and generate client code during the build process. This eliminates the traditional API boundary.
Operations are either Actions (write) or Queries (read):
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct CreateLibraryInput {
pub name: String,
pub description: Option<String>,
}
pub struct CreateLibraryAction {
input: CreateLibraryInput
// action state can be held here
}
impl CoreAction for CreateLibraryAction {
type Input = CreateLibraryInput;
type Output = Library;
async fn validate(&self, context: Arc<CoreContext>)
-> Result<ValidationResult, ActionError> {
// Check if the library already exists and return validation result
Ok(ValidationResult::Success)
}
async fn execute(self, context: Arc<CoreContext>) -> Result<Self::Output, ActionError> {
// Create library and return it
}
fn action_kind(&self) -> &'static str {
"libraries.create"
}
}
Register the operation with a single macro:
register_core_action!(CreateLibraryAction, "libraries.create");
The build process automatically:
Swift:
let library = try await spacedrive.libraries.create(
CreateLibraryInput(name: "My Library", description: nil)
)
TypeScript:
const library = await spacedrive.libraries.create({
name: "My Library",
});
Actions modify state and typically return job receipts or updated entities:
pub trait LibraryAction {
type Input: Send + Sync + 'static;
type Output: Send + Sync + 'static;
fn from_input(input: Self::Input) -> Result<Self, String>;
async fn validate(&self, library: &Arc<Library>, context: Arc<CoreContext>)
-> Result<ValidationResult, ActionError>;
fn resolve_confirmation(&mut self, choice_index: usize)
-> Result<(), ActionError>;
async fn execute(self, library: Arc<Library>, context: Arc<CoreContext>)
-> impl Future<Output = Result<Self::Output, ActionError>>;
fn action_kind(&self) -> &'static str;
}
Actions support a validation phase that can request user confirmation before execution. This enables safe, interactive operations with clear user feedback.
The validate() method returns one of two results:
pub enum ValidationResult {
/// Action is valid and can proceed
Success,
/// Action requires user confirmation
RequiresConfirmation(ConfirmationRequest),
}
pub struct ConfirmationRequest {
/// Message to display to the user
pub message: String,
/// List of choices for the user
pub choices: Vec<String>,
}
impl LibraryAction for FileCopyAction {
type Input = FileCopyInput;
type Output = JobReceipt;
async fn validate(&self, library: &Arc<Library>, context: Arc<CoreContext>)
-> Result<ValidationResult, ActionError> {
// Check if destination file exists
if !self.options.overwrite && self.destination_exists().await? {
return Ok(ValidationResult::RequiresConfirmation(ConfirmationRequest {
message: format!(
"Destination file already exists: {}",
self.destination.display()
),
choices: vec![
"Overwrite the existing file".to_string(),
"Rename the new file (e.g., file.txt -> file (1).txt)".to_string(),
"Abort this copy operation".to_string(),
],
}));
}
Ok(ValidationResult::Success)
}
fn resolve_confirmation(&mut self, choice_index: usize)
-> Result<(), ActionError> {
match choice_index {
0 => {
self.on_conflict = Some(FileConflictResolution::Overwrite);
Ok(())
}
1 => {
self.on_conflict = Some(FileConflictResolution::AutoModifyName);
Ok(())
}
2 => Err(ActionError::Cancelled),
_ => Err(ActionError::Validation {
field: "choice".to_string(),
message: "Invalid choice selected".to_string(),
})
}
}
async fn execute(mut self, library: Arc<Library>, context: Arc<CoreContext>)
-> Result<Self::Output, ActionError> {
// Apply the conflict resolution strategy if set
if let Some(resolution) = self.on_conflict {
match resolution {
FileConflictResolution::Overwrite => {
self.options.overwrite = true;
}
FileConflictResolution::AutoModifyName => {
self.destination = self.generate_unique_name().await?;
}
_ => {}
}
}
// Execute the copy operation
let job = FileCopyJob::new(self.sources, self.destination)
.with_options(self.options);
let receipt = library.jobs().dispatch(job).await?;
Ok(receipt)
}
fn action_kind(&self) -> &'static str {
"files.copy"
}
}
The CLI handles confirmations interactively:
// In CLI handler
let mut action = FileCopyAction::from_input(input)?;
// Validate the action
let validation_result = action.validate(&library, context).await?;
match validation_result {
ValidationResult::Success => {
// Proceed with execution
let result = action.execute(library, context).await?;
}
ValidationResult::RequiresConfirmation(request) => {
// Prompt user for choice
let choice_index = prompt_for_choice(request)?;
// Resolve the confirmation
action.resolve_confirmation(choice_index)?;
// Now execute with resolved choice
let result = action.execute(library, context).await?;
}
}
Queries retrieve data without side effects:
pub trait LibraryQuery {
type Input: Send + Sync + 'static;
type Output: Send + Sync + 'static;
fn from_input(input: Self::Input) -> QueryResult<Self>;
fn execute(self, context: Arc<CoreContext>, session: SessionContext)
-> impl Future<Output = QueryResult<Self::Output>>;
}
All standard Rust types are supported through Specta:
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct FileOperationResult {
pub succeeded: Vec<PathBuf>,
pub failed: HashMap<PathBuf, String>,
pub stats: OperationStats,
}
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub enum OperationError {
NotFound(String),
PermissionDenied,
DiskFull { required: u64, available: u64 },
}
Operations use a consistent wire protocol:
action:{category}.{operation}.input.v{version}query:{scope}.{operation}.v{version}Examples:
action:files.copy.inputquery:library.stats#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct SearchInput {
pub query: String,
pub filters: SearchFilters,
pub limit: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub struct SearchResult {
pub items: Vec<SearchItem>,
pub total_count: u64,
}
For a query:
pub struct SearchQuery {
query: String,
filters: SearchFilters,
limit: u32,
}
impl LibraryQuery for SearchQuery {
type Input = SearchInput;
type Output = SearchResult;
fn from_input(input: Self::Input) -> QueryResult<Self> {
Ok(Self {
query: input.query,
filters: input.filters,
limit: input.limit,
})
}
async fn execute(self, context: Arc<CoreContext>, session: SessionContext)
-> QueryResult<Self::Output> {
// Perform search and return results
}
}
register_library_query!(SearchQuery, "search");
After building, the operation is available in all clients automatically.
The iOS app embeds the Rust core and communicates through FFI:
#[no_mangle]
pub extern "C" fn handle_core_msg(
query: *const c_char,
callback: extern "C" fn(*mut c_void, *const c_char),
callback_data: *mut c_void,
) {
// Parse JSON-RPC request
// Execute operation using same registry
// Return JSON response
}
Swift calls through the FFI boundary using the generated types.
The build script runs during cargo build:
// build.rs
fn main() {
generate_swift_api_code().expect("Failed to generate Swift code");
}
A binary extracts all registered operations:
// generate_swift_types binary
fn main() {
let (operations, queries, types) = generate_spacedrive_api();
// Generate Swift code
let swift_types = specta_swift::Swift::new().export(&types)?;
let api_methods = generate_api_methods(&operations, &queries);
// Write to Swift package
fs::write("SpacedriveTypes.swift", swift_types)?;
fs::write("SpacedriveAPI.swift", api_methods)?;
}
The registration macros use inventory for compile-time collection:
inventory::submit! {
TypeExtractorEntry {
extractor: SearchQuery::extract_types,
identifier: "search",
}
}
Keep operations focused with clear inputs and outputs. Use appropriate scopes (Library vs Core) based on whether the operation needs library context.
Use the confirmation pattern for operations that:
Examples of good confirmation use cases:
Keep confirmations minimal - only ask when truly necessary. Don't confirm routine operations or when the intent is already clear from the input.
Flatten structures when possible and use Rust enums for variants. Document fields as comments flow through to generated code.
Define specific error types for each operation:
#[derive(Debug, Serialize, Deserialize, Type)]
pub enum SearchError {
InvalidQuery(String),
IndexNotReady,
TooManyResults { max: u32, requested: u32 },
}
For large result sets, consider pagination or streaming:
#[derive(Type)]
pub struct PaginatedSearch {
pub query: String,
pub cursor: Option<String>,
pub limit: u32,
}
#[derive(Type)]
pub struct BatchDeleteInput {
pub items: Vec<ItemIdentifier>,
pub skip_trash: bool,
}
Actions can define metadata for UI presentation:
impl ActionMetadata for DeleteAction {
fn display_name() -> &'static str {
"Delete Items"
}
fn description() -> &'static str {
"Permanently delete selected items"
}
fn is_dangerous() -> bool {
true
}
}
The operations system eliminates manual API maintenance while providing type-safe, performant clients across all platforms.