Skip to main content

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}