4 Learning plan, gaps (quoll)
Date: 2025 11-November 18
I don't even know what WASM really is, and I have never written Rust without AI
But i can learn, especially with AI help
Because this is a new space, what will i need to learn or create or design myself, that AI might now be able to fill the gaps on? Or maybe truly all the gaps are discernible and known, and someone just needs to codify the things.
That's a fantastic realization! It's exactly in these new, emergent spaces—like Rust and WASM—where human expertise is most valuable. AI excels at synthesizing known solutions, but struggles with the truly novel challenges.
The gaps you'll need to fill are not in the code itself, but in the architecture, intent, and design of the interfaces.
1. Architectural Vision and Abstraction Design
AI is excellent at implementing a function but poor at defining the most efficient interface or trait that should exist in the first place. This is the "Decoupling" and "Long-Term Cohesion" challenge.
-
The Minimal Core Trait: For a multi-crate system like Quoll/Steer, you must define the core traits and APIs that the
quoll-cli-coreexposes. This interface must be minimal enough to be implemented by a simple native binary and by the complex WASM browser shell, but powerful enough to handle all necessary commands (RPC calls, state management). The human decides where to draw the lines between crates and what the core contracts (traits) are. -
Managing State and Side-Effects: You need to design a reliable pattern for how your pure Rust CLI engine handles side effects (like talking to a database or a remote server). The native CLI might call a library, while the WASM shell must call a JavaScript RPC layer. Designing a clean, ergonomic way to abstract this input/output/effect boundary in Rust is a complex architectural task that AI will struggle to do perfectly without specific human guidance.
2. Domain-Specific UX and Interaction Flow
The goal of your project—a DevTools-style shell that is the "Storybook for the Backend"—is highly specific and requires deep user empathy. AI can write a list-users command, but it can't choose the right UX.
-
The Command Hierarchy & Language: You have to decide which commands are the most ergonomic and intuitive for a developer/admin. Why
steer prune inactive-usersinstead ofsteer users delete-inactive? This is a question of brand language and human workflow, which AI cannot answer—only an architect with domain expertise can. -
The "DevTools" Look and Feel: A standard
xtermtable is easy to generate. A DevTools-style, interactive, filterable, and drill-down UI (as you want) is not. You need to design:-
How command output is structured to allow for rich UI rendering (e.g., using a non-standard formatted output/protocol instead of plain text).
-
How users discover the next step in a workflow (e.g., after
list users, an adjacent UI button/link that saysedit roles). This requires anticipating user needs.
-
-
Error Reporting and Debugging: Designing high-quality, actionable error messages that guide the user to a fix, rather than just showing a stack trace, requires human intelligence and foresight.
3. Integrating the Bleeding Edge Ecosystems
Because Rust/WASM is a new space, much of the glue code and best practices are still being discovered, not codified in AI training data.
-
Custom Glue Code: While tools like
wasm-bindgenautomate much of the WASM/JavaScript interop, any complex interaction—especially with async operations, shared memory, or passing custom data structures—will require you to write the manual "glue" code to bridge the two runtimes. This is often undocumented, trial-and-error work. -
Ecosystem Friction: You will inevitably be one of the first to attempt a specific combination: a particular Rust ORM + a specific Rust WASM framework (like Yew or Leptos) + a WASM-Native CLI Engine. Resolving the initial, undocumented friction between these crates, such as build system conflicts, thread/sync issues, or unexpected dependency bloat, will require human debugging and creative problem-solving. The AI can suggest known patterns; you must invent the new ones.
In short, AI can be your phenomenal junior developer who writes the boilerplate and suggests implementations. You, the human, must be the senior architect and product designer who defines the problem, sets the rules, and ensures the whole system feels cohesive and solves a genuine human need.
which storybook tools exist that i can learn more about and then choose what to improve upon and map over?
which technolgies will i need to understand intimately in order to make good design choices?
That is a sharp way to frame the challenge! Looking at existing tools to see their patterns—and where they fall short for a backend/CLI use case—is the perfect path to making strong architectural decisions.
Here is a breakdown of the Storybook ecosystem to learn from, followed by the core technologies you must master to build the bridge in Rust/WASM.
1. Storybook Tools to Study and Map Over
The key takeaway from the Storybook ecosystem is that they decouple the component from the documentation/playground. You need to do the same, decoupling the backend command/model from the UI shell.
| Tool/Concept | What to Learn (The "Story") | How to Map to a Rust Backend CLI |
|---|---|---|
| Storybook (The OG) | Component Isolation & Props Controls: It excels at showing a single component and letting the user interact with its "props" via an inspector panel. | Command Isolation & Argument Controls: Your quoll-shell-wasm needs an interactive panel to show a command (e.g., prune users), expose its arguments (--dry-run, --since), and let the user interactively set those values before executing the command against the backend. |
| MDX Documentation | Mixing Docs and Live Code: MDX lets you write Markdown documentation but embed live, interactive components right inside the narrative. | "Living Documentation" for Logic: You need to embed live CLI output, model definitions, or even workflow buttons inside your system's documentation. The command steer migrate should be documented alongside a button that says "Run Migration Now" which executes the command live. |
| Styleguidist / Docz | Focus on Type and API Documentation: These tools emphasize generating documentation straight from component source code (Prop types, JSDoc). | Generating Documentation from Rust Types: Leverage Rust's strong type system. Your tool should read the structs, enums, and argument definitions (e.g., via the clap crate or a custom macro) and auto-generate the web documentation for commands and models (e.g., “This User struct has a roles: Vec<Role> field…”). |
| Figma/Supernova | Design Tokens and Single Source of Truth: They focus on making the design (tokens, styles) the absolute source of truth that feeds into the code. | "Model-as-Truth" Abstraction: Your Rust Model/Schema definitions (e.g., using SeaORM or custom structs) must become the single source of truth that feeds both the native CLI and the WASM web UI. The UI is just a thin presentation layer on top of the underlying, type-safe Rust logic. |
| Modern DevTools | Multi-Panel, Interactive Shell: Look at Chrome/Firefox DevTools. They aren't a single panel; they are a cohesive workspace (Console, Elements, Network, Application). | Cohesive Admin Workspace: Your shell needs multiple views that interact: a Console (for command history), a Data View (for model browsing), and a Network/RPC View (to debug calls to the server). The shell itself is a multi-tab application, not just a terminal output buffer. |
2. Intimate Technologies for Good Design Choices
To make the right abstractions that satisfy both the native CLI and the WASM browser shell, you need an intimate understanding of the following areas:
A. The Rust-WASM Bridge
This is the most critical area where AI will struggle to invent the right pattern, but is essential for your architecture.
-
wasm-bindgenand JavaScript Interop:-
Intimate Knowledge Needed: You must know exactly how to expose Rust structs as clean TypeScript/JavaScript objects, and how to define Rust
asyncfunctions that can be called from JavaScript/WASM, and how to handle return values (especially error types) across the boundary. -
Design Implication: This knowledge will dictate how clean your core
quoll-cli-coreAPI needs to be. For instance, designing a unified Command Output Format (a JSON or custom-serialized enum) that the WASM side can easily deserialize and render is a human design choice.
-
-
Rust Frontend Framework (Leptos / Yew):
-
Intimate Knowledge Needed: Understand the state management model of your chosen framework (e.g., Signals in Leptos). You will be feeding data from the Rust core (which runs your command logic) into the framework's reactive state system.
-
Design Implication: Your UI will be a collection of components that display the result of a command. You must design your data flow around "Action (CLI Command) -> State Update -> UI Re-render."
-
B. Communication and I/O Abstraction
Your core problem is that a single Rust function has to perform different I/O in different environments.
-
The Trait/Interface Pattern:
-
Intimate Knowledge Needed: Mastering Rust Traits to abstract I/O. For example, creating a
trait CommandRunner { async fn run(&self, command: Command) -> Result<Output, Error>; }. -
Design Implication: The native CLI will implement
CommandRunnerusing direct file/DB access. The WASM shell will implementCommandRunnerusing an RPC Call over HTTP/WebSockets to the server. The entirequoll-cli-corelogic should only interact with this trait, making it portable.
-
-
Data Serialization (JSON vs. Bincode vs.
msgpack):-
Intimate Knowledge Needed: You need a high-performance, compact, and strongly-typed serialization format to pass complex command results (like a list of 10,000 users) from the Rust server to the WASM client.
-
Design Implication: Using a format like
msgpack(as mentioned in an uploaded file) orserdewith a binary format is a better architectural choice than slow, verbose JSON, as it ensures type fidelity across the Rust/WASM boundary and maximizes performance for large admin data sets.
-
By focusing on these areas, you move past just writing the code and start making the design decisions that allow the AI-generated code to fit into a robust, high-performance, and truly innovative architecture.
show what rust structs and the async function flow order and process movement might look like for each of the three basic CLI commands. Of course your AI answer might be imperfect in inventing the right patterns. For each portion of each pattern, provide a percentage number of hypothetical accuracy and radius of possible differentiation.
#[derive(Subcommand)]
enum Commands {
/// Apply pending database migrations
Migrate,
/// Create a new user
CreateUser {
name: String,
#[arg(default_value = "viewer")]
role: String,
},
/// List records for a model
List {
model: String,
#[arg(long)]
json: bool,
},
}
That's an excellent next step. By designing the core data structures and the execution flow, you define the seams in your application—the exact points where the native CLI and the WASM shell must diverge or converge.
Here are the proposed Rust structs and the asynchronous execution flow for your three commands, along with a hypothetical analysis of AI accuracy for each pattern.
Core Data Structures (The Contracts)
These structs live in your quoll-cli-core crate and are the source of truth for all communication, making them vital for WASM interoperability via wasm-bindgen and server RPC.
Rust
// In quoll-cli-core/src/data.rs
// 1. The Output/Response Trait (The universal result)
// This is the common container for all command results.
#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub enum CommandOutput {
// Used for Migrate, CreateUser (non-JSON)
SuccessMessage { message: String },
// Used for List, or a detailed CreateUser result
Table {
headers: Vec<String>,
rows: Vec<Vec<String>>,
metadata: Option<serde_json::Value>, // For pagination, total count, etc.
},
// Used when `list --json` is specified, or for error details
JsonValue,
}
// 2. A Core Model for the CLI to talk to
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
pub id: String,
pub name: String,
pub role: String,
pub created_at: String, // Likely serialized time type
// ... other fields
}
Command Execution Flow and Abstraction
The key is defining a Command Runner Trait that abstracts the environment, allowing the same command logic to run locally or remotely.
Shutterstock
1. The Abstraction Trait
Rust
// In quoll-cli-core/src/runner.rs
use async_trait::async_trait;
use crate::data::CommandOutput;
// The universal contract for execution
#[async_trait]
pub trait CommandRunner {
type Error: std::error::Error + Send + Sync;
// The core execution function that every command must use
async fn execute(&self, command: Commands) -> Result<CommandOutput, Self::Error>;
}
2. Implementation for Native CLI (Local)
This implementation runs your commands directly against the database/files when compiled as a native binary.
| Implementation Step | AI Accuracy | Differentiation Radius | AI's Role (Why Accuracy is High) |
|---|---|---|---|
Struct Definition (NativeRunner) |
95% | 5% (Only minor names/fields) | Pattern is boilerplate: wrap the DB pool and configuration. |
| Flow Order (Migrate) | 90% | 10% (Error handling specifics) | The process (runner.execute -> DbClient::migrate() -> return SuccessMessage) is a well-known MVC pattern. |
| Logic (DB Access) | 70% | 30% (Exact ORM calls) | AI can suggest the structure, but the exact sqlx::migrate!() or sea_orm::schema::migrate() calls are ORM-specific and often require custom setup. |
Rust
// In quoll-native-cli/src/main.rs or impl.rs
use quoll_cli_core::{CommandRunner, Commands, CommandOutput, User};
// Assume DbClient is a helper struct wrapping your ORM/DB pool
struct NativeRunner {
db_client: DbClient,
}
#[async_trait]
impl CommandRunner for NativeRunner {
type Error = DbError; // Actual ORM error type
async fn execute(&self, command: Commands) -> Result<CommandOutput, Self::Error> {
match command {
// -- COMMAND: MIGRATE --
Commands::Migrate => {
// 1. Call ORM/DB-specific migration logic.
self.db_client.run_migrations().await?;
// 2. Return success message.
Ok(CommandOutput::SuccessMessage {
message: "Database migrations applied successfully.".into(),
})
},
// ... other commands will directly call database methods ...
_ => todo!(),
}
}
}
3. Implementation for WASM Shell (Remote RPC)
This implementation converts the command request into a Remote Procedure Call (RPC) over HTTP/WebSockets to a server, which executes the command and returns the result.
| Implementation Step | AI Accuracy | Differentiation Radius | AI's Role (Why Accuracy is Lower) |
|---|---|---|---|
Struct Definition (WasmRunner) |
85% | 15% (URL/Client specific) | Needs fields for URL and a client (e.g., web_sys::fetch). AI can structure it, but specific WASM API use is nuanced. |
| Flow Order (Migrate) | 75% | 25% (Serialization/Error) | The serialize -> fetch -> deserialize -> return flow is known, but handling complex cross-boundary error types and asynchronous fetch logic is tricky. |
| Logic (Network Call) | 60% | 40% (Protocol Design) | AI will suggest a simple POST request. The human design choice is the custom RPC protocol (e.g., a specific endpoint structure, error body format, using msgpack vs. JSON), which must be consistent with the server-side handler. |
Rust
// In quoll-shell-wasm/src/runner.rs
use quoll_cli_core::{CommandRunner, Commands, CommandOutput};
struct WasmRunner {
base_url: String,
}
#[async_trait]
impl CommandRunner for WasmRunner {
type Error = WasmClientError; // Custom error for network issues, etc.
async fn execute(&self, command: Commands) -> Result<CommandOutput, Self::Error> {
// 1. Serialize the command enum into a transmittable format (e.g., JSON or msgpack).
let payload = serde_json::to_vecSerialization?;
// 2. Perform the asynchronous network request (RPC).
let response = reqwest::Client::new()
.post(&format!("{}/api/rpc", self.base_url))
.header("Content-Type", "application/json")
.body(payload)
.send()
.await
.map_errNetwork?;
// 3. Deserialize the common CommandOutput struct.
let output: CommandOutput = response.jsonDeserialization?;
Ok(output)
}
}
Command-Specific Process Movements
The implementation details for the two other commands show how the common structures enforce consistency.
Command: CreateUser { name: String, role: String }
| Flow Movement | Native CLI (Direct DB) | WASM Shell (RPC) |
|---|---|---|
| 1. Execution Call | NativeRunner::execute(CreateUser { ... }) |
WasmRunner::execute(CreateUser { ... }) |
| 2. Core Logic | Native Runner: Calls db_client.create_user(name, role). |
Wasm Runner: Serializes CreateUser enum and sends over network. |
| 3. Server/DB Action | ORM inserts user record. | Server Side: Receives CreateUser, validates, calls ORM, and gets the created User struct back. |
| 4. Result Packaging | ORM returns the new User struct. Runner maps User to CommandOutput::SuccessMessage or CommandOutput::Json. |
Server Side: Maps the created User struct to CommandOutput::Json. Wasm Runner: Deserializes the CommandOutput::Json. |
Command: List { model: String, json: bool }
| Flow Movement | Native CLI (Direct DB) | WASM Shell (RPC) |
|---|---|---|
| 1. Execution Call | NativeRunner::execute(List { model, json }) |
WasmRunner::execute(List { model, json }) |
| 2. Core Logic | Native Runner: Calls db_client.find_model(model). |
Wasm Runner: Serializes List enum and sends over network. |
| 3. Data Fetch | ORM queries and returns Vec<ModelStruct>. |
Server Side: Receives List, queries ORM, returns Vec<ModelStruct>. |
| 4. Result Packaging | Native Runner: If json: true, wraps data in CommandOutput::Json. If json: false, converts data into CommandOutput::Table. |
Server Side: Determines output format based on json flag and sends the pre-packaged CommandOutput enum back. Wasm Runner: Deserializes and passes the CommandOutput to the UI rendering layer. |
The gap AI leaves is in the error handling and the precise serialization boundaries. You must ensure that errors thrown deep in the native ORM are cleanly translated into a universal error type that can be serialized, sent over HTTP, and deserialized back into a displayable error message in the WASM shell.
A lot of coding is about 'sewing a customer into a custom shirt'
but with generalized tool, we want to leave data structures intact for data to pass cleanly, without any necessary transformation that has implications, losses, or over coupling
Above, for element sthat have low AI accuracy and require custom decision making, how can i best design for general use and modularity for a tool, rather than fitting for a specific product? I also understandt that at some point you must design for the framework in which you live, and that assumptions are implied or explicit and that an edge must exist, lest you create nothing at all.
That's a profound observation—the shift from a "custom shirt" (product-specific design) to a "generalized tool" (modular, composable patterns) is the core challenge of open-source architecture. You're trying to design a protocol, not just an application.
The key to achieving this modularity, especially in the low-AI-accuracy areas, is by inverting dependencies and designing for data-centricity. Instead of having the core logic depend on the transport layer, the transport layer must depend on the core logic's data structures.
Here is how you can design for general use and modularity in those critical, low-accuracy areas:
1. Abstraction of I/O (The "Runner" Trait)
The CommandRunner trait you defined is your single most important layer of decoupling. It allows the same command logic to be run by any implementation (native, WASM RPC, testing mock).
Design Principle: Invert Dependencies (DIP)
| Low-Accuracy Area | Design Solution for Modularity | How it Avoids "Custom Shirt" Fit |
|---|---|---|
| Flow Order/Logic (75% Acc. / 25% Diff.) | Define the CommandRunner in your quoll-cli-core crate (pure Rust). The core logic only sees this trait. |
The core command logic (which defines what to run) has zero knowledge of the network stack, database ORM, or browser APIs. It only knows that a runner exists to execute it. |
| Error Handling (Part of the 25% Diff.) | Define a Universal Error Enum in the quoll-cli-core: QuollCoreError. The runner implementations (NativeRunner, WasmRunner) must translate their specific errors (e.g., sqlx::Error, reqwest::Error) into this single, standard error type. |
Ensures the consumer (the user's code, the WASM UI) only has to handle one known set of failure states, regardless of where the command was executed. |
The Modular Structure
Instead of one monolithic execute method, you can use the Strategy Pattern by having the command enum implement a helper trait:
Rust
// In quoll-cli-core/src/command.rs
// A separate crate can now implement this trait for its own commands
pub trait ExecutableCommand {
fn model_name(&self) -> String;
fn requires_database(&self) -> bool; // <-- Modularity helper
// ... maybe other metadata
}
The NativeRunner will use this trait to decide how to execute, but the WasmRunner can use it to determine if a command needs to be sent to the server at all.
2. Serialization and Data Transport
This area (60% Acc. / 40% Diff.) is where you move from a language-specific structure to a transport-agnostic protocol.
Design Principle: Self-Describing Data and Schemas
| Low-Accuracy Area | Design Solution for Modularity | How it Avoids "Custom Shirt" Fit |
|---|---|---|
| Logic (Network Call) (40% Diff.) | Enforce a Single, Stable RPC Payload Format. All requests must be wrapped in a consistent RPC payload (e.g., a custom Request struct) and all responses in a Response<T> struct. |
This defines a protocol (/api/steer/rpc), not an endpoint. Any language (Python, Node, Go) could implement a Steer Server by simply adhering to this payload format, making the WASM shell reusable. |
| Serialization Choice | Use serde and Support Multiple Formats. While MsgPack is a good default for performance (due to its size and speed), ensure your core structs derive serde::{Serialize, Deserialize}. Your WasmRunner can then choose to use JSON (for debugging/simplicity) or MsgPack (for production performance). |
The data structure (User struct) remains pristine. The transport layer (the runner) can swap out the serialization library (e.g., serde_json for rmp_serde for MsgPack) without touching the core logic or the data schema. Data fidelity is guaranteed by the serde trait. |
The Decoupled Request/Response
Define your communication wrapper:
Rust
// In quoll-cli-core/src/rpc.rs
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RpcRequest {
// Uses the existing Commands enum directly as the payload
pub command: Commands,
pub auth_token: Option<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
// T is the CommandOutput
pub enum RpcResponse<T> {
Ok(T),
Err(QuollCoreError), // Uses your universal error type
}
By defining the request and response around your existing core data (Commands and CommandOutput), you have created a reusable API contract that is completely detached from the transport mechanism (HTTP, WebSocket, or local function call).
3. UI/Output Formatting
The goal of the CommandOutput enum is to send data, not presentation.
Design Principle: Separate Data from Presentation
| Low-Accuracy Area | Design Solution for Modularity | How it Avoids "Custom Shirt" Fit |
|---|---|---|
| Logic (Data Presentation) | Enforce Raw Data Types in CommandOutput. Instead of Vec<Vec<String>> in your Table variant, pass the actual serializable Rust models. |
The CommandOutput::Table should be Table { data: Vec<serde_json::Value>, ... }. The WASM UI then decides how to convert the row data into strings, colors, or interactive components. This gives the frontend developer (or the AI helping them) maximum control over the UI without re-implementing the CLI logic. |
Refined Output Structs
Refining your CommandOutput for true modularity:
Rust
// In quoll-cli-core/src/data.rs (Refined)
pub enum CommandOutput {
// For simple commands (Migrate, Delete)
SuccessMessage { message: String },
// For list/read commands—sends the raw model data
ModelCollection {
model_name: String,
// Send a vector of serialized models, giving the UI full raw data access
data: Vec<serde_json::Value>,
// Metadata is crucial for a dev tool: filter applied, total available, etc.
metadata: serde_json::Value,
},
// Always an option for the rawest data access
RawJsonValue,
}
By passing Vec<serde_json::Value> (or a similar raw structure), you avoid "over-sewing" the table into strings in the core Rust logic. The WASM UI is now free to render a full-featured, interactive, sortable, and filterable table using that raw JSON payload.
Self-Describing Data and Schemas, that sounds cool
how can rust implement this while still being type safe and not using a too-open-ended Pythonic args and kwargs model? Or is args and kwargs still respected?
That's a brilliant question, and it gets to the heart of why Rust's type system is both a constraint and an incredible asset for building generalized tools like Steer.
You are absolutely right: to build a generalized tool, you must create a protocol that describes the data structure and execution rules, rather than just passing loose Pythonic arguments.
The good news is that Rust achieves "Self-Describing Data and Schemas" while maintaining strict type safety by using Procedural Macros and Traits to push the "discovery" process from runtime (like Python) to compile-time.1
Here is how to design your command and data layers to be both modular and strictly typed.
1. Avoiding args and kwargs: The Rust Alternative
Rust does not respect the Pythonic args and kwargs model for function calls because that model is inherently dynamic and bypasses compile-time type checking.
Instead, Rust achieves the same effect—a flexible, self-describing command interface—through two key mechanisms:
A. Declarative CLI Parsing (The Command Schema)
For your CLI, the clap crate (which you are already using) performs the role of defining the schema of the command.2
| Python (kwargs) | Rust (clap) | Principle |
|---|---|---|
Runtime flexibility (**kwargs) |
Compile-time strictness (Procedural Macro) | You declare the command's expected structure (name: String, role: String) once in a struct/enum, and the macro generates all the parsing and validation logic. |
| Ad-hoc documentation | Auto-generated Help Output | The #[arg(default_value = "viewer")] and doc comments become the self-description that the CLI user sees. |
The Commands::CreateUser variant is the strongly-typed equivalent of defining the required and optional arguments for the function—with zero dynamic lookup necessary.
B. Strongly-Typed Serialization (The Data Schema)
For your network communication, the serde crate performs the role of defining the schema of the data that travels. Your User and CommandOutput structs are self-describing because they define the exact expected fields and types (id: String, rows: Vec<Vec<String>>).
2. Implementing Runtime Modularity with Trait Objects
For true generalization and to allow third-party plugins (i.e., someone adding their own steer my-custom-command), you must rely on Trait Objects (dyn Trait). This is the mechanism for runtime polymorphism in Rust.
The General Command Protocol
Instead of passing a fixed Commands enum to your CommandRunner, define a universal SteerCommand trait in your core crate.
Rust
// In quoll-cli-core
pub trait SteerCommand: Send + Sync + std::fmt::Debug {
/// The unique identifier/name for the command (e.g., "create-user").
fn name(&self) -> &'static str;
/// The payload/arguments of the command, serialized as JSON.
/// This is the self-describing part for the network boundary.
fn payload(&self) -> serde_json::Value;
/// A method to execute the command, requiring a generic Runner context.
/// This is where the core logic lives.
async fn execute_with(&self, runner: &dyn CommandRunner) -> Result<CommandOutput, SteerError>;
}
The Flow of a Modular Command
-
Specific Command: Your
Commands::CreateUserbecomes an implementation of this trait. -
Plugin Decoupling: A third-party developer creates a
MyPluginCommandstruct and implementsSteerCommandfor it. -
Runner Execution: When the WASM shell gets a command request, it doesn't need to know the specific type. It only needs to cast it to a
Box<dyn SteerCommand>(a trait object) and callcommand.execute_with(runner).
This design allows you to mix and match any command type at runtime while remaining strictly type-safe within the Rust compiler's view.
3. The Controlled Use of Dynamic Data Value
To handle the "data portability" problem—where data needs to be flexible enough for various UI renders or API payloads—you must define a controlled boundary where the strict typing is temporarily loosened.
Solution: Use serde_json::Value only at the edges of your system.
| Component | Responsibility | Typing Approach |
|---|---|---|
Data Models (User, Post, etc.) |
Core Logic (e.g., ORM, validation) | Strictly Typed (Native Rust structs). |
Command Execution (CommandRunner) |
Execution contract | Strictly Typed (Only deals with Commands enum, CommandOutput). |
| Command Output ModelCollection | API Payload | Semi-Dynamic (The inner data field uses Vec<serde_json::Value>). |
| WASM UI (Leptos/Yew component) | Presentation/Rendering | Dynamic at Ingestion, Strict at Rendering (The UI component receives serde_json::Value but immediately deserializes the fields it needs into internal, strictly-typed props for rendering.) |
By limiting the use of the dynamic serde_json::Value (the open-ended part) to the body of your CommandOutput struct, you ensure:
-
The core logic (validation, business rules) stays strictly typed.
-
The protocol (the
CommandOutputenvelope) is defined by a strictly typed enum. -
The payload (the raw data inside the envelope) is flexible enough for any UI to consume and render in a custom way (e.g., rendering a
Useras a table row vs. a user profile card).
This hybrid approach allows for the generalization your tool needs while leveraging Rust's strengths to prevent the kind of runtime bugs that often plague dynamically-typed command systems.