pub struct Workspace { /* private fields */ }Expand description
Editable view of an apimock workspace.
§Internal layout
The Workspace holds the loaded TOML model (as a Config) plus
two index maps:
id_to_address: NodeId → where the node lives inconfig.address_to_id: reverse — used when rebuilding snapshots.
On every apply() that could move nodes around (Add / Remove /
Move), these tables are partially rebuilt. Reloading the config
discards them and re-seeds with fresh IDs.
Implementations§
Source§impl Workspace
impl Workspace
Sourcepub fn rule_set_id_at(&self, rule_set: usize) -> Option<NodeId>
pub fn rule_set_id_at(&self, rule_set: usize) -> Option<NodeId>
The NodeId of the rule set at this index, if one exists.
Sourcepub fn rule_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId>
pub fn rule_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId>
The NodeId of the rule at (rule_set, rule), if one exists.
Both indices are 0-based, matching get’s JSON matched block
(RFC 057’s handoff § 1.3 — set takes the same base as the
machine-readable contract, not the 1-based text display).
Sourcepub fn respond_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId>
pub fn respond_id_at(&self, rule_set: usize, rule: usize) -> Option<NodeId>
The NodeId of the respond block at (rule_set, rule), if one
exists. Distinct from rule_id_at’s NodeId — respond is its
own addressable node (EditCommand::UpdateRespond targets it,
not the rule).
Sourcepub fn describe(&self, id: NodeId) -> Option<String>
pub fn describe(&self, id: NodeId) -> Option<String>
Render a NodeId back to a human-readable natural-key
description — never the UUID itself. Used to label --dry-run
previews and diff-summary rows without the single highest-risk
mistake this RFC’s handoff calls out: serialising DiffItem
(whose target: NodeId is #[serde(transparent)] over a
Uuid) directly into set’s JSON output.
A plain String rather than a typed enum deliberately: RFC 057
§ 2 Unresolved 3 asks for a renderer, not a second address type
to keep stable — a string has no shape to freeze. Returns None
only for a NodeId this workspace’s index has never seen (a
stale ID from a different load()), which set’s one-load,
one-invocation lifecycle should never actually produce.
Sourcepub fn preview_changes(&self) -> Vec<DiffItem>
pub fn preview_changes(&self) -> Vec<DiffItem>
What a save() right now would write, without writing it.
Thin public wrapper over the existing pub(super)
compute_diff_summary — RFC 057’s --dry-run needs exactly
this and nothing save()’s other side effects (the atomic
writes, the baseline refresh).
Source§impl Workspace
impl Workspace
Sourcepub fn apply(&mut self, cmd: EditCommand) -> Result<ApplyResult, ApplyError>
pub fn apply(&mut self, cmd: EditCommand) -> Result<ApplyResult, ApplyError>
Apply one edit command, mutating the in-memory workspace.
§Shape of the implementation
Each EditCommand variant maps to a small helper method. The
helpers return Result<Vec<NodeId>, ApplyError>; apply wraps
the ok-path in an ApplyResult with the right requires_reload
flag and reruns validation so the result carries up-to-date
diagnostics.
§ID stability on structural changes
Commands that change positional layout (Remove / Delete / Move
/ Add) touch self.ids carefully so NodeIds that refer to the
same logical node survive the operation. For example, after
RemoveRuleSet { id } at index i, rule sets at positions
i+1.. shift down by one: the code below explicitly migrates
their IDs so a GUI that selected rule-set #3 before the edit
still has the same ID pointing at what is now rule-set #2.
Source§impl Workspace
impl Workspace
Sourcepub fn save(&mut self) -> Result<SaveResult, SaveError>
pub fn save(&mut self) -> Result<SaveResult, SaveError>
Save the workspace back to disk.
§Algorithm
- Render each editable file (root + each rule set) to a
canonical TOML string and an editable-subset
Table. - Compare the canonical string against
baseline_files. Files whose canonical output is byte-identical to the baseline are skipped entirely — nothing about them changed. - For files that do differ: first confirm none of them changed on disk since we last saw them (RFC 056 §2 Q3) — checked for every file before any write, so a conflict on one file can’t leave another half-written.
- Mutate each file’s own previous text in place
(
toml_writer::apply_in_place) rather than rebuilding it, so comments, blank lines and key order survive; only the values that actually changed do. Write atomically viatempfile::NamedTempFile::persist(same-directory rename(2) on POSIX,MoveFileExWon Windows). On any single-file write failure, the partial state is whatever rename(2)s have already succeeded — see the type-level docstring onSaveErrorfor the rationale. - After all writes succeed, refresh
baseline_files(to the canonical string) andoriginal_text(to the just-written text) so a subsequent save() and conflict check both compare against what’s now actually on disk. - Compute
DiffItems by node, comparing the in-memory state to the load-time baseline (parsed; not text-diff). - Compute
requires_reload/requires_restartfrom the set of changed files: changes to[listener]need a restart, everything else just a reload.
Sourcepub fn has_unsaved_changes(&self) -> bool
pub fn has_unsaved_changes(&self) -> bool
True when at least one editable file’s rendered output differs from its load-time baseline.
§Use case
A GUI’s “unsaved changes” indicator polls this. Cheap relative to a full save (no file I/O, just renders + string compares).
Source§impl Workspace
impl Workspace
Sourcepub fn snapshot(&self) -> WorkspaceSnapshot
pub fn snapshot(&self) -> WorkspaceSnapshot
Build a snapshot for GUI rendering.
§Allocation cost
A snapshot fully owns its data (no borrows into the workspace) so the GUI can serialise / send / store it without lifetime gymnastics. This is O(total editable nodes) allocation per call; the GUI should call it once per edit, not once per render frame.
Source§impl Workspace
impl Workspace
Sourcepub fn validate(&self) -> ValidationReport
pub fn validate(&self) -> ValidationReport
Validate the workspace and return a GUI-ready report.
Uses the same per-node checks snapshot() does so the numbers
line up: a node rendered with a red underline in the snapshot
will appear in report.diagnostics with the same message.
Source§impl Workspace
impl Workspace
Sourcepub fn load(root: PathBuf) -> Result<Self, WorkspaceError>
pub fn load(root: PathBuf) -> Result<Self, WorkspaceError>
Load a workspace rooted at the given apimock.toml-like path.
Accepts either a direct path to the config file or the
directory containing one; a missing file-path is searched for
as apimock.toml inside root. Mirrors the CLI’s existing
resolution rules.
Sourcepub fn has_external_changes(&self) -> bool
pub fn has_external_changes(&self) -> bool
Returns true if any tracked config file has been modified on disk
since the last load() or save().
Polls file metadata (mtime + size). Returns false on stat errors
to avoid spurious “changed” signals from transient temp-file churn.
§Usage
Call periodically from the GUI and re-render when true:
if ws.has_external_changes() {
ws.sync_from_disk().unwrap();
}Sourcepub fn sync_from_disk(&mut self) -> Result<(), WorkspaceError>
pub fn sync_from_disk(&mut self) -> Result<(), WorkspaceError>
Reload all config files from disk, replacing the in-memory model.
§Every NodeId is reassigned (RFC 042)
A sync is a fresh load: the old IdIndex is
discarded and a new one is seeded from scratch, so every
NodeId changes, whether or not the address it names actually
did. This previously stated the opposite — that IDs for
unchanged addresses survived — which was never true; *self = fresh here has always replaced the whole workspace, ID index
included. RFC 042 corrects the claim rather than building the
preservation it described, because NodeAddress is positional
(Rule { rule_set: usize, rule: usize }) and an external edit —
the one case this method exists for — is exactly the case where
positions shift, making “preserve by address” reassign identity
onto the wrong rule as often as it would help.
After a sync, re-read the tree — do not reuse a NodeId held
from before the call. A GUI calling this is already
re-rendering (it just observed has_external_changes
return true), so re-querying every ID from the new
snapshot costs nothing extra.
On parse error, the workspace is left unchanged and the error is returned. The GUI can surface the error and retry.
After a successful sync, has_external_changes() returns false
until the next external modification.
Sourcepub fn config(&self) -> &Config
pub fn config(&self) -> &Config
Access the underlying Config. Intended for embedders that
need to build a running Server from the same workspace. Edit
via apply() instead of touching Config directly — changes
made through this reference are invisible to the ID index.
Sourcepub fn list_directory(&self, path: &Path) -> Vec<FileNodeView>
pub fn list_directory(&self, path: &Path) -> Vec<FileNodeView>
Expand a directory in the file tree on demand.
§When the GUI calls this
Workspace::snapshot() returns a FileTreeView populated with
just the top-level entries of the fallback respond dir. Each
directory entry carries children: Some(Vec::new()) to flag it
as expandable. When a user clicks to expand one of those nodes,
the GUI calls list_directory(&entry.path) and gets back the
next depth’s entries (still not recursed past that depth — the
same lazy contract holds).
§Why path-based and not NodeId-based
File-tree entries don’t carry NodeIds (see FileNodeView). The
reason is lifecycle: the editable node space (rules, rule sets,
respond blocks) is small, stable, and survives apply() calls
— perfect for UUID-keyed state. The file tree is large,
transient, and reflects the filesystem rather than the model;
keying it by path keeps the API simple and avoids mixing two
kinds of identity.