doxa_docs/ui/config.rs
1//! Typed configuration for the [Scalar API Reference](https://github.com/scalar/scalar) UI.
2//!
3//! [`ScalarConfig`] mirrors the subset of Scalar's `data-configuration`
4//! options the crate surfaces, with strongly typed enums for fields
5//! whose values come from a fixed vocabulary. The default values are
6//! chosen to render the docs page exactly as the crate has shipped it
7//! historically — a three-pane `modern` layout with dark mode on, the
8//! schemas index hidden, the codegen sidebar suppressed, and Scalar's
9//! paid product upsells (Agent / MCP) disabled.
10//!
11//! Pass an instance to [`MountOpts::scalar`](crate::MountOpts::scalar)
12//! to override individual fields. The whole struct serializes to the
13//! attribute-encoded JSON Scalar reads at boot, so adding a field here
14//! is the only change required to expose a new toggle.
15
16use serde::Serialize;
17
18/// Scalar UI rendering options.
19///
20/// All fields default to the values the crate has shipped since the
21/// initial Scalar adoption — constructing `ScalarConfig::default()` and
22/// passing it to [`MountOpts::scalar`](crate::MountOpts::scalar) is a
23/// no-op compared to omitting the call entirely.
24///
25/// # Example
26///
27/// ```
28/// use doxa::{ScalarConfig, ScalarLayout, ScalarTheme};
29///
30/// let cfg = ScalarConfig::default()
31/// .layout(ScalarLayout::Classic)
32/// .theme(ScalarTheme::Solarized)
33/// .dark_mode(false);
34/// # let _ = cfg;
35/// ```
36#[derive(Debug, Clone, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ScalarConfig {
39 /// Visual theme. Defaults to [`ScalarTheme::Default`].
40 pub theme: ScalarTheme,
41
42 /// Initial dark mode state. Defaults to `true`. The user-facing
43 /// toggle remains visible unless [`Self::hide_dark_mode_toggle`] is
44 /// set.
45 pub dark_mode: bool,
46
47 /// Hide the search box. Defaults to `false`.
48 pub hide_search: bool,
49
50 /// Hide the dark-mode toggle in the header. Defaults to `false`.
51 pub hide_dark_mode_toggle: bool,
52
53 /// Show the left sidebar. Defaults to `true`.
54 pub show_sidebar: bool,
55
56 /// Page layout. Defaults to [`ScalarLayout::Modern`] (three-pane
57 /// nav / description / playground).
58 pub layout: ScalarLayout,
59
60 /// Hide the standalone "Models" / schemas index. Defaults to `true`
61 /// — referenced schemas still render inline under each operation.
62 pub hide_models: bool,
63
64 /// Hide the "copy as curl/node/..." codegen button row. Defaults to
65 /// `true`. The interactive try-it-out panel is unaffected.
66 pub hide_client_button: bool,
67
68 /// Format offered by the header "Download OpenAPI" button. Defaults
69 /// to [`DocumentDownload::None`] — the spec is still reachable at
70 /// the mounted JSON path.
71 #[serde(rename = "documentDownloadType")]
72 pub document_download: DocumentDownload,
73
74 /// When the developer-tools drawer is exposed. Defaults to
75 /// [`DeveloperTools::Never`].
76 pub show_developer_tools: DeveloperTools,
77
78 /// Enable Scalar's "Ask AI" assistant. Defaults to `false`. Scalar
79 /// charges for production use of this feature; leaving it off keeps
80 /// the docs UI free of upsell surface.
81 #[serde(serialize_with = "serialize_agent", rename = "agent")]
82 pub agent_enabled: bool,
83
84 /// Enable Scalar's "Generate MCP" integration. Defaults to `false`
85 /// for the same reason as [`Self::agent_enabled`].
86 #[serde(serialize_with = "serialize_mcp", rename = "mcp")]
87 pub mcp_enabled: bool,
88
89 /// Override the Scalar CDN URL. `None` keeps the crate's default
90 /// (`https://cdn.jsdelivr.net/npm/@scalar/api-reference`). Useful
91 /// for air-gapped deployments, CDN mirrors, or self-hosted Scalar
92 /// bundles — set this to the URL of the `@scalar/api-reference`
93 /// script the browser should load.
94 ///
95 /// This field is skipped during JSON serialization — it is a
96 /// server-side concern only (the URL is written into the HTML
97 /// template, not handed to Scalar as configuration).
98 #[serde(skip_serializing)]
99 pub cdn_url: Option<String>,
100}
101
102impl Default for ScalarConfig {
103 fn default() -> Self {
104 Self {
105 theme: ScalarTheme::Default,
106 layout: ScalarLayout::Modern,
107 dark_mode: true,
108 hide_dark_mode_toggle: false,
109 hide_search: false,
110 show_sidebar: true,
111 hide_models: true,
112 hide_client_button: true,
113 document_download: DocumentDownload::None,
114 show_developer_tools: DeveloperTools::Never,
115 agent_enabled: false,
116 mcp_enabled: false,
117 cdn_url: None,
118 }
119 }
120}
121
122impl ScalarConfig {
123 /// Override the visual theme.
124 pub fn theme(mut self, theme: ScalarTheme) -> Self {
125 self.theme = theme;
126 self
127 }
128
129 /// Override the page layout.
130 pub fn layout(mut self, layout: ScalarLayout) -> Self {
131 self.layout = layout;
132 self
133 }
134
135 /// Set the initial dark mode state.
136 pub fn dark_mode(mut self, on: bool) -> Self {
137 self.dark_mode = on;
138 self
139 }
140
141 /// Hide or show the dark-mode toggle.
142 pub fn hide_dark_mode_toggle(mut self, hide: bool) -> Self {
143 self.hide_dark_mode_toggle = hide;
144 self
145 }
146
147 /// Hide or show the search box.
148 pub fn hide_search(mut self, hide: bool) -> Self {
149 self.hide_search = hide;
150 self
151 }
152
153 /// Show or hide the left sidebar.
154 pub fn show_sidebar(mut self, show: bool) -> Self {
155 self.show_sidebar = show;
156 self
157 }
158
159 /// Hide or show the standalone schemas index.
160 pub fn hide_models(mut self, hide: bool) -> Self {
161 self.hide_models = hide;
162 self
163 }
164
165 /// Hide or show the codegen button row.
166 pub fn hide_client_button(mut self, hide: bool) -> Self {
167 self.hide_client_button = hide;
168 self
169 }
170
171 /// Configure the header download button format.
172 pub fn document_download(mut self, format: DocumentDownload) -> Self {
173 self.document_download = format;
174 self
175 }
176
177 /// Configure when the developer-tools drawer is exposed.
178 pub fn show_developer_tools(mut self, when: DeveloperTools) -> Self {
179 self.show_developer_tools = when;
180 self
181 }
182
183 /// Enable or disable Scalar's "Ask AI" agent.
184 pub fn agent_enabled(mut self, on: bool) -> Self {
185 self.agent_enabled = on;
186 self
187 }
188
189 /// Enable or disable Scalar's "Generate MCP" integration.
190 pub fn mcp_enabled(mut self, on: bool) -> Self {
191 self.mcp_enabled = on;
192 self
193 }
194
195 /// Override the Scalar CDN URL. See [`Self::cdn_url`] for when to
196 /// use this.
197 pub fn cdn_url(mut self, url: impl Into<String>) -> Self {
198 self.cdn_url = Some(url.into());
199 self
200 }
201}
202
203/// Visual theme presets recognized by Scalar.
204///
205/// The string each variant serializes to matches Scalar's documented
206/// theme keys. New themes published upstream can be added without a
207/// breaking change.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
209#[serde(rename_all = "camelCase")]
210pub enum ScalarTheme {
211 /// The Scalar default theme.
212 Default,
213 /// Alternate light theme.
214 Alternate,
215 /// Moon (low-contrast dark) theme.
216 Moon,
217 /// Purple accent theme.
218 Purple,
219 /// Solarized theme.
220 Solarized,
221 /// Blue Planet theme.
222 BluePlanet,
223 /// Saturn theme.
224 Saturn,
225 /// Kepler theme.
226 Kepler,
227 /// Mars theme.
228 Mars,
229 /// Deep Space theme.
230 DeepSpace,
231 /// No theme — render with Scalar's bare defaults.
232 None,
233}
234
235/// Page layout variants.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
237#[serde(rename_all = "lowercase")]
238pub enum ScalarLayout {
239 /// Three-pane layout (nav / description / always-on
240 /// request-response playground). The historical default.
241 Modern,
242 /// Single-column Redoc-style layout.
243 Classic,
244}
245
246/// Format(s) offered by the header "Download OpenAPI" button.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
248#[serde(rename_all = "lowercase")]
249pub enum DocumentDownload {
250 /// Hide the download button entirely. The spec is still reachable
251 /// at the mounted JSON path.
252 None,
253 /// Offer JSON download.
254 Json,
255 /// Offer YAML download.
256 Yaml,
257 /// Offer both JSON and YAML downloads.
258 Both,
259}
260
261/// Visibility of Scalar's developer-tools drawer.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
263#[serde(rename_all = "camelCase")]
264pub enum DeveloperTools {
265 /// Never expose the drawer.
266 Never,
267 /// Always expose the drawer.
268 Always,
269 /// Expose the drawer on hover.
270 OnHover,
271}
272
273// Scalar represents agent and MCP toggles as nested objects rather
274// than top-level booleans: `agent: { disabled: true }` /
275// `mcp: { disabled: true }`. Encode that shape from a single bool so
276// the public surface stays flat.
277fn serialize_agent<S: serde::Serializer>(enabled: &bool, ser: S) -> Result<S::Ok, S::Error> {
278 serialize_disabled_object(*enabled, ser)
279}
280
281fn serialize_mcp<S: serde::Serializer>(enabled: &bool, ser: S) -> Result<S::Ok, S::Error> {
282 serialize_disabled_object(*enabled, ser)
283}
284
285fn serialize_disabled_object<S: serde::Serializer>(
286 enabled: bool,
287 ser: S,
288) -> Result<S::Ok, S::Error> {
289 use serde::ser::SerializeMap;
290 let mut map = ser.serialize_map(Some(1))?;
291 map.serialize_entry("disabled", &!enabled)?;
292 map.end()
293}