- Added the `op!` macro to streamline the registration of library actions, core actions, and queries, reducing boilerplate and promoting consistency across the codebase. - Implemented helper macros for generating method strings for actions and queries, enhancing clarity in operation declarations. - Updated existing file operations (copy, delete, duplicate detection) to utilize the new macro, simplifying their input and action structures. - Introduced comprehensive documentation for the operations initialization API, outlining usage patterns and conversion mechanisms. These changes significantly enhance the modularity and maintainability of the operation registration process while ensuring a consistent API surface.
6.9 KiB
Operations Initialization API (op!)
This document defines a simple, uniform API for declaring and registering all operations (library actions, core actions, and queries) in Core. The goals are:
- Make operations easy to add, understand, and maintain
- Keep inputs pure (no session, no library_id)
- Keep actions free of transport concerns (no library_id in action structs)
- Provide consistent method naming and automatic inventory registration
- Reduce boilerplate and promote repeatable patterns
Core Concepts
- Input: External API payload (CLI/GraphQL). Contains only operation-specific fields.
- Action: Executable business logic. Receives
Arc<Library>(for library actions) or onlyCoreContext(for core actions) during execution. - Query: Read-only operation returning data.
- Method: Opaque string used for routing (e.g.,
action:files.copy.input.v1,query:core.status.v1). - Registry: Inventory-backed lookup that dispatches by method.
Inputs are decoded, converted to actions (for actions), and executed. For library actions, the library_id is resolved by the dispatcher and never appears in inputs or actions.
The op! Macro (Proposed)
One macro, three variants. Uniform API for every operation type.
- Library actions:
op!(library_action InputType => ActionType, "files.copy", via = BuilderType);
// or, if no builder is needed:
op!(library_action InputType => ActionType, "files.validation");
- Core actions:
op!(core_action InputType => ActionType, "libraries.create", via = BuilderType);
// or
op!(core_action InputType => ActionType, "libraries.delete");
- Queries:
op!(query QueryType, "core.status");
What op! does
- Generates the method string automatically:
- Library/Core actions →
action:{domain}.{op}.input.v1 - Queries →
query:{domain}.{op}.v1
- Library/Core actions →
- Implements
Wirefor the type with that method - Implements the appropriate build/dispatch glue and registers with
inventory - For actions, wires Input → Action using one of:
TryFrom<Input> for Action(preferred), orvia = BuilderType(callsBuilderType::from_input(input).build()) if provided
This keeps every operation declaration small and consistent.
Conversion: Input → Action
Use TryFrom<Input> for Action as the single source of truth for how inputs become actions. For complex operations that already have builders, the TryFrom impl can delegate to the builder.
Example (File Copy):
impl TryFrom<FileCopyInput> for FileCopyAction {
type Error = String;
fn try_from(input: FileCopyInput) -> Result<Self, Self::Error> {
use crate::infra::action::builder::ActionBuilder;
FileCopyActionBuilder::from_input(input).build().map_err(|e| e.to_string())
}
}
op!(library_action FileCopyInput => FileCopyAction, "files.copy");
Example (Validation – direct construction):
impl TryFrom<FileValidationInput> for ValidationAction {
type Error = String;
fn try_from(input: FileValidationInput) -> Result<Self, Self::Error> {
Ok(ValidationAction::new(input.paths, input.verify_checksums, input.deep_scan))
}
}
op!(library_action FileValidationInput => ValidationAction, "files.validation");
Method Naming & Versioning
- Actions:
action:{domain}.{operation}.input.v{version} - Queries:
query:{domain}.{operation}.v{version} - Default version:
v1. Bump when the wire contract changes. - Keep
{domain}.{operation}short, stable, and human-readable.
Optional helpers:
action_method!("files.copy")→"action:files.copy.input.v1"query_method!("core.status")→"query:core.status.v1"
op! can call these internally so call sites only specify "files.copy" or "core.status".
Dispatch Flow (Library Action)
- Client sends
Inputwithmethod - Core registry decodes and builds
Action(pure conversion) - Registry resolves
library_idfrom session ActionManager::dispatch_library(library_id, action)- Manager fetches
Arc<Library>, validates, creates audit entry, then callsaction.execute(library, context)
Core actions are the same minus step 3.
Implementation Checklist
- Traits/Manager (done):
LibraryActionhas nolibrary_id()requirementActionManager::dispatch_library(library_id, action)resolves library and logs
- Registry (done):
BuildLibraryActionInput::build(self) -> Action(pure)- Handler resolves
library_idfrom session once
- Inputs/Actions:
- Inputs are pure (no session/library_id)
- Actions do not store
library_id - Add
TryFrom<Input> for Action(delegate to builder when needed)
- Macro:
- Provide
op!(three variants) and method helpers (optional)
- Provide
Migration Plan
- Introduce
op!and helpers inops::registry - Convert existing operations:
- Files: copy, delete, validation, duplicate_detection (copy: via builder; others: direct TryFrom)
- Indexing: implement TryFrom or use builder; remove library_id from action if present
- Libraries/Core ops: create/rename/delete via
op!(core_action …) - Queries: swap to
op!(query …)where appropriate
- Delete old per-op registration boilerplate
- Run registry tests to verify:
- All required methods registered
- Naming convention checks pass
- No duplicates across queries/actions
Examples (End-to-End)
- File Copy:
impl TryFrom<FileCopyInput> for FileCopyAction { /* delegate to builder */ }
op!(library_action FileCopyInput => FileCopyAction, "files.copy");
- Validation:
impl TryFrom<FileValidationInput> for ValidationAction { /* construct directly */ }
op!(library_action FileValidationInput => ValidationAction, "files.validation");
- Library Create (Core Action):
impl TryFrom<LibraryCreateInput> for LibraryCreateAction { /* construct directly */ }
op!(core_action LibraryCreateInput => LibraryCreateAction, "libraries.create");
- Core Status (Query):
op!(query CoreStatusQuery, "core.status");
Testing & Tooling
-
Use existing registry tests:
test_method_naming_conventiontest_has_registered_operationstest_no_duplicate_methodslist_registered_operations()to debug
-
Add unit tests for
TryFrom<Input> for Actionwhere logic is non-trivial (builder path, validation errors).
Do & Don’t
- Do keep inputs pure and actions context-free
- Do resolve
library_idonce at dispatch time - Do prefer
TryFrom<Input> for Actionand reuse builders when present - Don’t put
library_idinto inputs or actions - Don’t pass
SessionStateintobuild() - Don’t hardcode method strings inconsistently—use
op!
Future Extensions
- Derive
#[derive(LibraryOpInput("files.copy"))]to generateTryFrom,Wire, and registration automatically for simple ops - Add lint to enforce method naming and versioning conventions
- Method helpers
action_method!/query_method!to centralize formatting