apimock_config/view.rs
1//! Read-only + edit-command views on config state for GUI tooling.
2//!
3//! # Stage-1 scope (5.0.0)
4//!
5//! Per the 5.0.0 brief (§4.1), this module defines the *shape* of the
6//! editable API a future GUI will depend on. Types are declared with
7//! their fields and rustdoc-annotated responsibilities. Populating them
8//! from a live `Config` — and mutating a `Config` via `EditCommand` — is
9//! stage-2 work.
10//!
11//! Every type here is `#[non_exhaustive]` so later stages can add
12//! fields / variants without breaking downstream code that depends on
13//! the stage-1 shape.
14//!
15//! # The model this API assumes
16//!
17//! A GUI sees a *workspace* — the root `apimock.toml` plus every file
18//! referenced from it (rule sets, middlewares). Each editable value in
19//! the workspace lives in exactly one of those files; tracking the
20//! origin is essential so the GUI knows which file a "Save" button
21//! should write.
22
23use serde::Serialize;
24
25use std::path::PathBuf;
26
27/// A snapshot of the whole workspace — every loaded file and the
28/// editable nodes inside each.
29#[derive(Clone, Debug, Serialize)]
30#[non_exhaustive]
31pub struct WorkspaceSnapshot {
32 /// The root `apimock.toml` file.
33 pub root: ConfigFileView,
34 /// Rule-set files referenced from `root`, in the same order they
35 /// appear in `service.rule_sets`.
36 pub rule_sets: Vec<ConfigFileView>,
37 /// Middleware files referenced from `root`.
38 pub middlewares: Vec<ConfigFileView>,
39 /// Issues found during the most recent load. `ok` iff empty.
40 pub diagnostics: Vec<Diagnostic>,
41}
42
43impl WorkspaceSnapshot {
44 pub fn empty() -> Self {
45 Self {
46 root: ConfigFileView::empty(PathBuf::new()),
47 rule_sets: Vec::new(),
48 middlewares: Vec::new(),
49 diagnostics: Vec::new(),
50 }
51 }
52}
53
54/// One TOML file inside the workspace.
55#[derive(Clone, Debug, Serialize)]
56#[non_exhaustive]
57pub struct ConfigFileView {
58 /// Absolute path on disk.
59 pub path: PathBuf,
60 /// Path as written inside the parent config (e.g.
61 /// `"apimock-rule-set.toml"`). Useful for displaying without the
62 /// full absolute path noise.
63 pub source_relative: Option<String>,
64 /// The editable nodes extracted from the file, flat for list-rendering.
65 pub nodes: Vec<ConfigNodeView>,
66}
67
68impl ConfigFileView {
69 pub fn empty(path: PathBuf) -> Self {
70 Self {
71 path,
72 source_relative: None,
73 nodes: Vec::new(),
74 }
75 }
76}
77
78/// One editable value inside a `ConfigFileView`.
79///
80/// # Why a flat list of nodes instead of a tree
81///
82/// The TOML the user writes is nested, but the UI surface a GUI renders
83/// is usually flat (a properties panel, a form). Keeping the API flat
84/// with a dotted `path` string trades a tiny bit of GUI cleverness for
85/// a much simpler cross-language contract.
86#[derive(Clone, Debug, Serialize)]
87#[non_exhaustive]
88pub struct ConfigNodeView {
89 /// Dotted TOML path (e.g. `"listener.port"`, `"service.rule_sets"`).
90 pub path: String,
91 /// Current value, stringified for display. A GUI can format this
92 /// however it wants; the value's original type is in `kind`.
93 pub display_value: String,
94 /// What shape of value this node holds.
95 pub kind: NodeKind,
96 /// Whether the GUI should allow editing this field.
97 pub editable: bool,
98}
99
100#[derive(Clone, Copy, Debug, Serialize)]
101pub enum NodeKind {
102 String,
103 Integer,
104 Boolean,
105 StringList,
106 Table,
107}
108
109/// A structured edit to apply to the workspace.
110///
111/// # Why commands instead of free-form text edits
112///
113/// The brief (§6.1, §6.2) spells this out: free-form text edits are too
114/// coarse for a GUI that needs to reason about the change. Commands
115/// carry exactly the intent — "add rule set", "update field at path" —
116/// which the apply-layer can validate before writing anything.
117#[derive(Clone, Debug)]
118#[non_exhaustive]
119pub enum EditCommand {
120 /// Add a rule-set file to the workspace.
121 AddRuleSet { path: String },
122 /// Remove a rule-set file by its index in the list.
123 RemoveRuleSet { index: usize },
124 /// Update an editable field. `target` identifies which file; `path`
125 /// is the dotted TOML path inside that file.
126 UpdateField {
127 target: EditTarget,
128 path: String,
129 value: EditValue,
130 },
131 /// Change the fallback respond dir.
132 SetFallbackRespondDir { path: String },
133}
134
135/// Which file inside the workspace an edit applies to.
136#[derive(Clone, Debug)]
137pub enum EditTarget {
138 Root,
139 RuleSet { index: usize },
140 Middleware { index: usize },
141}
142
143/// A value in an `UpdateField` edit. Kept intentionally small — this is
144/// the union of types our TOML config actually uses.
145#[derive(Clone, Debug)]
146pub enum EditValue {
147 String(String),
148 Integer(i64),
149 Boolean(bool),
150 StringList(Vec<String>),
151}
152
153/// What happened when an `EditCommand` was applied.
154#[derive(Clone, Debug, Serialize)]
155#[non_exhaustive]
156pub struct ApplyResult {
157 /// Whether the edit succeeded.
158 pub ok: bool,
159 /// Files whose in-memory model was changed. Use this to drive the
160 /// GUI's "unsaved changes" indicator.
161 pub modified_files: Vec<PathBuf>,
162 /// Problems found during apply. Non-empty when `ok` is false.
163 pub diagnostics: Vec<Diagnostic>,
164 /// Whether the running server would need a reload / restart to see
165 /// this change. Informational — the server control layer decides
166 /// what to do with the hint.
167 pub reload_hint: ReloadHint,
168}
169
170/// How much of the server needs to restart after a change.
171///
172/// A GUI can show this as a banner ("restart required") and a supervisor
173/// can use it to decide between reload-in-place and full restart.
174#[derive(Clone, Copy, Debug, Serialize)]
175pub enum ReloadHint {
176 /// No reload needed — change was purely cosmetic or to comments.
177 None,
178 /// Rule-set / middleware reload is sufficient.
179 Reload,
180 /// A full restart is needed (e.g. listener port changed).
181 Restart,
182}
183
184/// Result of writing in-memory changes back to disk.
185#[derive(Clone, Debug, Serialize)]
186#[non_exhaustive]
187pub struct SaveResult {
188 pub ok: bool,
189 /// Files actually written.
190 pub written: Vec<PathBuf>,
191 pub diagnostics: Vec<Diagnostic>,
192}
193
194/// Human-readable notice about something in the workspace.
195///
196/// Severities follow the same convention as `RouteValidationIssue` in
197/// the routing crate so a GUI can render them uniformly.
198#[derive(Clone, Debug, Serialize)]
199#[non_exhaustive]
200pub struct Diagnostic {
201 pub severity: DiagnosticSeverity,
202 /// Which file the diagnostic is about, if applicable.
203 pub file: Option<PathBuf>,
204 pub message: String,
205}
206
207#[derive(Clone, Copy, Debug, Serialize)]
208pub enum DiagnosticSeverity {
209 Error,
210 Warning,
211 Info,
212}