mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
509 lines
13 KiB
Plaintext
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.
|