Skip to main content

apimock_config/
view.rs

1//! Read-only views on workspace state, and the command + result types
2//! the editing API uses.
3//!
4//! # 5.1.0 — spec alignment
5//!
6//! In 5.0.0 this module carried a placeholder shape defined only by
7//! rustdoc; 5.1.0 re-aligns it with the 5.1 spec:
8//!
9//! - `WorkspaceSnapshot { files, routes, diagnostics }` (spec §4.2)
10//! - each node carries `id: NodeId` + `source_file` + `toml_path` +
11//!   `display_name` + `kind` + `validation`
12//! - `EditCommand` is eight variants covering every editable action
13//!   (spec §4.3)
14//! - `ApplyResult { changed_nodes, diagnostics, requires_reload }`
15//!   (spec §4.4)
16//! - `SaveResult { changed_files, diff_summary, requires_reload }`
17//!   (spec §4.5) — populated in Step 4
18//! - `ValidationReport { diagnostics, is_valid }` (spec §4.6)
19//! - `Diagnostic { node_id, file, severity, message }` (spec §4.7)
20//!
21//! # Why UUIDs and not positional IDs
22//!
23//! The spec's §4.3 says "すべて NodeId で対象を指定". Positional IDs
24//! (`rule_sets[0].rules[3]`) would shift on every insert / delete /
25//! move, forcing the GUI to re-index its selection set after every
26//! edit. UUIDs are stable within a `Workspace` instance regardless of
27//! reordering.
28
29use serde::{Deserialize, Serialize};
30use serde_json::Value as JsonValue;
31use uuid::Uuid;
32
33use std::path::PathBuf;
34
35use apimock_routing::view::RouteCatalogSnapshot;
36
37/// Stable identifier for an editable node.
38///
39/// # Stability contract
40///
41/// Stable within one `Workspace` instance — that is, across any
42/// sequence of `apply()` calls. IDs are reassigned on fresh `load()`,
43/// which matches spec §10 "Workspace はメモリ上に独立インスタンスを持つ".
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
45#[serde(transparent)]
46pub struct NodeId(pub Uuid);
47
48impl NodeId {
49    pub fn new() -> Self {
50        Self(Uuid::new_v4())
51    }
52}
53
54impl Default for NodeId {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl std::fmt::Display for NodeId {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        self.0.fmt(f)
63    }
64}
65
66/// Complete snapshot of the workspace state.
67///
68/// Shape matches spec §4.2 exactly. Consumed read-only by the GUI;
69/// mutated indirectly via `Workspace::apply`.
70#[derive(Clone, Debug, Serialize)]
71#[non_exhaustive]
72pub struct WorkspaceSnapshot {
73    /// All editable TOML files in the workspace, flattened. Each file
74    /// carries its own list of editable nodes.
75    pub files: Vec<ConfigFileView>,
76    /// Route overview pulled from the routing crate.
77    pub routes: RouteCatalogSnapshot,
78    /// Workspace-scoped issues (e.g. a root file that failed to load).
79    /// Per-node diagnostics live inside each `ConfigNodeView.validation`.
80    pub diagnostics: Vec<Diagnostic>,
81}
82
83impl WorkspaceSnapshot {
84    pub fn empty() -> Self {
85        Self {
86            files: Vec::new(),
87            routes: RouteCatalogSnapshot::empty(),
88            diagnostics: Vec::new(),
89        }
90    }
91}
92
93/// One TOML file inside the workspace.
94#[derive(Clone, Debug, Serialize)]
95#[non_exhaustive]
96pub struct ConfigFileView {
97    /// Absolute path on disk.
98    pub path: PathBuf,
99    /// Display name — typically the file name. Used as a tab title in
100    /// the GUI.
101    pub display_name: String,
102    /// What kind of file this is (root config, rule set, middleware).
103    pub kind: ConfigFileKind,
104    /// Editable nodes extracted from the file.
105    pub nodes: Vec<ConfigNodeView>,
106}
107
108#[derive(Clone, Copy, Debug, Serialize)]
109pub enum ConfigFileKind {
110    Root,
111    RuleSet,
112    Middleware,
113}
114
115/// One editable value inside a `ConfigFileView`.
116///
117/// Each node carries the six fields spec §4.2 makes mandatory.
118#[derive(Clone, Debug, Serialize)]
119#[non_exhaustive]
120pub struct ConfigNodeView {
121    /// Stable identifier — survives moves / renames within a Workspace
122    /// instance.
123    pub id: NodeId,
124    /// File the node was loaded from.
125    pub source_file: PathBuf,
126    /// Dotted TOML path inside `source_file` (e.g. `"listener.port"`,
127    /// `"rules[2].respond"`).
128    pub toml_path: String,
129    /// Human-readable label for UI list rendering (e.g. the rule's
130    /// `url_path` value, or `"Rule #3"` for a rule without one).
131    pub display_name: String,
132    /// Shape of the underlying value.
133    pub kind: NodeKind,
134    /// Per-node validation results.
135    pub validation: NodeValidation,
136}
137
138/// What shape of value a node holds. The variants are what the
139/// spec-defined `EditCommand` variants act on.
140#[derive(Clone, Copy, Debug, Serialize)]
141pub enum NodeKind {
142    /// Root config node — listener / log / service fields.
143    RootSetting,
144    /// One rule set loaded from a referenced TOML file.
145    RuleSet,
146    /// One rule inside a rule set.
147    Rule,
148    /// The `respond` block of a rule.
149    Respond,
150    /// File-based response node (fallback dir entry).
151    FileNode,
152    /// Script / middleware route.
153    Script,
154}
155
156/// Per-node validation result.
157///
158/// # Why validation is a field on the node and not a separate pass
159///
160/// GUIs render validation inline ("this field has a red underline").
161/// Keeping the validation result stapled to the node the GUI is about
162/// to render avoids a second lookup step in every render frame.
163#[derive(Clone, Debug, Default, Serialize)]
164pub struct NodeValidation {
165    /// Convenience flag — true iff `issues` is empty.
166    pub ok: bool,
167    /// Human-readable issues scoped to this node.
168    pub issues: Vec<ValidationIssue>,
169}
170
171impl NodeValidation {
172    pub fn ok() -> Self {
173        Self {
174            ok: true,
175            issues: Vec::new(),
176        }
177    }
178}
179
180#[derive(Clone, Debug, Serialize)]
181pub struct ValidationIssue {
182    pub severity: Severity,
183    pub message: String,
184}
185
186/// Structured edit command applied via `Workspace::apply`.
187///
188/// # Shape comes straight from spec §4.3
189///
190/// Each variant targets a node by NodeId (never by positional index).
191/// This guarantees edits remain well-defined across previous inserts /
192/// removes in the same GUI session.
193#[derive(Clone, Debug)]
194#[non_exhaustive]
195pub enum EditCommand {
196    /// Add a rule set file to the workspace.
197    ///
198    /// `path` is relative to the root config's directory — the same
199    /// convention as the value stored in `service.rule_sets`.
200    AddRuleSet {
201        path: String,
202    },
203    /// Remove a rule set by its NodeId. The underlying TOML file is
204    /// NOT deleted from disk — the workspace only removes the reference.
205    RemoveRuleSet {
206        id: NodeId,
207    },
208    /// Add a rule to an existing rule set.
209    AddRule {
210        parent: NodeId,
211        rule: RulePayload,
212    },
213    /// Update a rule's `when` / `respond` block.
214    UpdateRule {
215        id: NodeId,
216        rule: RulePayload,
217    },
218    /// Remove a rule by NodeId.
219    DeleteRule {
220        id: NodeId,
221    },
222    /// Reorder a rule within its parent rule set.
223    MoveRule {
224        id: NodeId,
225        new_index: usize,
226    },
227    /// Update the `respond` block of a rule.
228    UpdateRespond {
229        id: NodeId,
230        respond: RespondPayload,
231    },
232    /// Update a root-level setting (listener, log, service-level flags).
233    UpdateRootSetting {
234        key: RootSettingKey,
235        value: EditValue,
236    },
237}
238
239/// Payload for `AddRule` / `UpdateRule`.
240#[derive(Clone, Debug, Default)]
241pub struct RulePayload {
242    pub url_path: Option<String>,
243    pub method: Option<String>,
244    pub respond: RespondPayload,
245}
246
247/// Payload for `UpdateRespond`.
248///
249/// The three fields are mutually specialised: exactly one of
250/// `file_path` / `text` / `status` should be populated. Validation
251/// catches cases that violate this.
252#[derive(Clone, Debug, Default)]
253pub struct RespondPayload {
254    pub file_path: Option<String>,
255    pub text: Option<String>,
256    pub status: Option<u16>,
257    pub delay_milliseconds: Option<u32>,
258}
259
260/// Enumerated root-level setting. Typed enum rather than free-form
261/// path so the apply-layer can exhaustively match without parsing.
262#[derive(Clone, Copy, Debug)]
263#[non_exhaustive]
264pub enum RootSettingKey {
265    ListenerIpAddress,
266    ListenerPort,
267    ServiceFallbackRespondDir,
268    ServiceStrategy,
269}
270
271/// Value provided with an edit command.
272#[derive(Clone, Debug)]
273#[non_exhaustive]
274pub enum EditValue {
275    String(String),
276    Integer(i64),
277    Boolean(bool),
278    StringList(Vec<String>),
279    /// For settings whose domain is a small enum value (e.g.
280    /// `ServiceStrategy` → `"first_match"`).
281    Enum(String),
282    /// For completeness — callers can pass a raw JSON value when the
283    /// spec-defined key set is extended by stage-3 tooling. Currently
284    /// reserved; no stage-1 setting uses it.
285    Json(JsonValue),
286}
287
288/// Outcome of a successful `apply`.
289#[derive(Clone, Debug, Serialize)]
290#[non_exhaustive]
291pub struct ApplyResult {
292    /// Node IDs whose content (or position) changed.
293    pub changed_nodes: Vec<NodeId>,
294    /// Issues surfaced by applying the command (validation during apply
295    /// may add diagnostics — e.g. a new rule pointing at a missing file).
296    pub diagnostics: Vec<Diagnostic>,
297    /// `true` iff the server should reload to see this change. An edit
298    /// that changes the listener port needs a restart, not just a
299    /// reload — see `Workspace::save` for the richer `ReloadHint`.
300    pub requires_reload: bool,
301}
302
303/// Outcome of `Workspace::save`.
304#[derive(Clone, Debug, Serialize)]
305#[non_exhaustive]
306pub struct SaveResult {
307    /// TOML files actually written to disk.
308    pub changed_files: Vec<PathBuf>,
309    /// One entry per node that changed since last load.
310    pub diff_summary: Vec<DiffItem>,
311    pub requires_reload: bool,
312}
313
314/// One summary row in a `SaveResult::diff_summary`.
315#[derive(Clone, Debug, Serialize)]
316pub struct DiffItem {
317    pub kind: DiffKind,
318    pub target: NodeId,
319    pub summary: String,
320}
321
322#[derive(Clone, Copy, Debug, Serialize)]
323pub enum DiffKind {
324    Added,
325    Updated,
326    Removed,
327}
328
329/// Workspace-wide validation result. Mirrors spec §4.6.
330#[derive(Clone, Debug, Serialize)]
331pub struct ValidationReport {
332    pub diagnostics: Vec<Diagnostic>,
333    pub is_valid: bool,
334}
335
336impl ValidationReport {
337    pub fn ok() -> Self {
338        Self {
339            diagnostics: Vec::new(),
340            is_valid: true,
341        }
342    }
343}
344
345/// Human-readable notice about the workspace.
346#[derive(Clone, Debug, Serialize)]
347pub struct Diagnostic {
348    /// Target node, if any. `None` means "workspace-wide".
349    pub node_id: Option<NodeId>,
350    /// Target file, if the diagnostic is best reported at file level
351    /// (e.g. "could not read apimock-rule-set.toml"). May be `None` for
352    /// purely in-memory errors.
353    pub file: Option<PathBuf>,
354    pub severity: Severity,
355    pub message: String,
356}
357
358#[derive(Clone, Copy, Debug, Serialize)]
359pub enum Severity {
360    Error,
361    Warning,
362    Info,
363}
364
365// ---------------------------------------------------------------------------
366// Reload hint — spec §9. The same enum shape was defined in 5.0.0;
367// 5.1 reuses it unchanged so existing consumers keep working.
368// ---------------------------------------------------------------------------
369
370/// Advisory indicating what, if anything, the server needs to do in
371/// response to a config change.
372#[derive(Clone, Copy, Debug, Default, Serialize)]
373pub struct ReloadHint {
374    pub requires_reload: bool,
375    pub requires_restart: bool,
376}
377
378impl ReloadHint {
379    pub fn none() -> Self {
380        Self::default()
381    }
382
383    pub fn reload() -> Self {
384        Self {
385            requires_reload: true,
386            requires_restart: false,
387        }
388    }
389
390    pub fn restart() -> Self {
391        Self {
392            requires_reload: false,
393            requires_restart: true,
394        }
395    }
396}