spacedrive/docs/core/ops.mdx
Jamie Pine b4024c860e Mobile app in React Native
+ validation support for Actions
2025-12-05 15:16:41 -08:00

509 lines
13 KiB
Plaintext

---
title: Operations
sidebarTitle: Operations
---
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.
## How It Works
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.
### Define Operations
Operations are either Actions (write) or Queries (read):
```rust
#[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 and Generate
Register the operation with a single macro:
```rust
register_core_action!(CreateLibraryAction, "libraries.create");
```
The build process automatically:
1. Extracts type information using Specta
2. Generates Swift and TypeScript type definitions
3. Creates native API methods for each client
### Use Generated Clients
Swift:
```swift
let library = try await spacedrive.libraries.create(
CreateLibraryInput(name: "My Library", description: nil)
)
```
TypeScript:
```typescript
const library = await spacedrive.libraries.create({
name: "My Library",
});
```
## Operation Types
### Actions
Actions modify state and typically return job receipts or updated entities:
```rust
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;
}
```
### Validation and Confirmation
Actions support a validation phase that can request user confirmation before execution. This enables safe, interactive operations with clear user feedback.
#### ValidationResult
The `validate()` method returns one of two results:
```rust
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>,
}
```
#### Example: File Copy with Conflict Resolution
```rust
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"
}
}
```
#### CLI Integration
The CLI handles confirmations interactively:
```rust
// 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?;
}
}
```
<Note>
The `validate()` method takes `&self` (a reference), while `execute()` takes `self` (consumes the action). This ensures validation doesn't modify state, while execution can take ownership to transform the action into its result.
</Note>
### Queries
Queries retrieve data without side effects:
```rust
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>>;
}
```
## Type System
All standard Rust types are supported through Specta:
```rust
#[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 },
}
```
<Note>
The `Type` derive is required for all types used in operations. This enables
Specta to extract type information for client generation.
</Note>
## Wire Protocol
Operations use a consistent wire protocol:
- Actions: `action:{category}.{operation}.input.v{version}`
- Queries: `query:{scope}.{operation}.v{version}`
Examples:
- `action:files.copy.input`
- `query:library.stats`
## Adding Operations
### 1. Create Input/Output Types
```rust
#[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,
}
```
### 2. Implement the Operation
For a query:
```rust
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
}
}
```
### 3. Register It
```rust
register_library_query!(SearchQuery, "search");
```
### 4. Build and Use
After building, the operation is available in all clients automatically.
## iOS Integration
The iOS app embeds the Rust core and communicates through FFI:
```rust
#[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.
## Code Generation Details
### Build Process
The build script runs during `cargo build`:
```rust
// build.rs
fn main() {
generate_swift_api_code().expect("Failed to generate Swift code");
}
```
### Type Extraction
A binary extracts all registered operations:
```rust
// 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)?;
}
```
### Registration Internals
The registration macros use inventory for compile-time collection:
```rust
inventory::submit! {
TypeExtractorEntry {
extractor: SearchQuery::extract_types,
identifier: "search",
}
}
```
## Best Practices
### Operation Design
Keep operations focused with clear inputs and outputs. Use appropriate scopes (Library vs Core) based on whether the operation needs library context.
### When to Use Validation Confirmations
Use the confirmation pattern for operations that:
1. **Have Destructive Side Effects**: Deleting files, overwriting data, or making irreversible changes
2. **Encounter Conflicts**: File name collisions, duplicate entries, or conflicting states
3. **Need User Decisions**: Multiple valid approaches where user preference matters
4. **Risk Data Loss**: Operations that could result in unexpected data loss
Examples of good confirmation use cases:
- File copy/move when destination exists
- Deleting non-empty directories
- Overwriting modified files
- Removing locations with indexed content
- Irreversible format conversions
Keep confirmations minimal - only ask when truly necessary. Don't confirm routine operations or when the intent is already clear from the input.
### Type Design
Flatten structures when possible and use Rust enums for variants. Document fields as comments flow through to generated code.
### Error Handling
Define specific error types for each operation:
```rust
#[derive(Debug, Serialize, Deserialize, Type)]
pub enum SearchError {
InvalidQuery(String),
IndexNotReady,
TooManyResults { max: u32, requested: u32 },
}
```
### Performance
For large result sets, consider pagination or streaming:
```rust
#[derive(Type)]
pub struct PaginatedSearch {
pub query: String,
pub cursor: Option<String>,
pub limit: u32,
}
```
## Advanced Features
### Batch Operations
```rust
#[derive(Type)]
pub struct BatchDeleteInput {
pub items: Vec<ItemIdentifier>,
pub skip_trash: bool,
}
```
### Operation Metadata
Actions can define metadata for UI presentation:
```rust
impl ActionMetadata for DeleteAction {
fn display_name() -> &'static str {
"Delete Items"
}
fn description() -> &'static str {
"Permanently delete selected items"
}
fn is_dangerous() -> bool {
true
}
}
```
<Note>
Confirmation is handled dynamically through the `validate()` method, not as static metadata. This allows context-aware confirmations based on the actual operation state.
</Note>
<Tip>
Run `cargo run --bin generate_swift_types` to debug type extraction issues.
Check the generated files in
`packages/swift/Sources/SpacedriveClient/Generated/`.
</Tip>
The operations system eliminates manual API maintenance while providing type-safe, performant clients across all platforms.