Skip to main content

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}