dioxus_openapi/
runtime.rs1use utoipa::openapi::{
4 info::InfoBuilder,
5 path::{PathItem, PathItemBuilder, PathsBuilder},
6 schema::ComponentsBuilder,
7 tag::TagBuilder,
8 OpenApi, OpenApiBuilder,
9};
10
11pub struct RegisteredPath {
16 pub append: fn(PathsBuilder) -> PathsBuilder,
17 pub schemas: fn(
18 &mut Vec<(
19 String,
20 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
21 )>,
22 ),
23}
24
25inventory::collect!(RegisteredPath);
26
27#[derive(Debug, Clone, Copy)]
29pub struct TagMeta {
30 pub name: &'static str,
31 pub description: Option<&'static str>,
32}
33
34#[derive(Debug, Clone, Copy)]
36pub struct SpecOptions {
37 pub title: &'static str,
38 pub version: &'static str,
39 pub description: Option<&'static str>,
40 pub tags: &'static [TagMeta],
41}
42
43pub fn append_tagged_path<P>(paths: PathsBuilder) -> PathsBuilder
50where
51 P: utoipa::Path,
52 P: for<'t> utoipa::__dev::Tags<'t>,
53{
54 let mut operation = P::operation();
55 let tags: Vec<String> = P::tags().into_iter().map(str::to_string).collect();
56 if !tags.is_empty() {
57 operation.tags = Some(tags);
58 }
59
60 let methods = P::methods();
61 let path_item = if methods.len() == 1 {
62 PathItem::new(
63 methods
64 .into_iter()
65 .next()
66 .expect("path must declare at least one method"),
67 operation,
68 )
69 } else {
70 methods
71 .into_iter()
72 .fold(PathItemBuilder::new(), |item, method| {
73 item.operation(method, operation.clone())
74 })
75 .build()
76 };
77
78 paths.path(P::path(), path_item)
79}
80
81pub fn build_openapi(opts: &SpecOptions) -> OpenApi {
83 let mut paths = PathsBuilder::new();
84 let mut schema_list: Vec<(
85 String,
86 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
87 )> = Vec::new();
88
89 for reg in inventory::iter::<RegisteredPath> {
90 paths = (reg.append)(paths);
91 (reg.schemas)(&mut schema_list);
92 }
93
94 let mut seen = std::collections::HashSet::new();
96 schema_list.retain(|(name, _)| seen.insert(name.clone()));
97
98 let mut info = InfoBuilder::new()
99 .title(opts.title)
100 .version(opts.version);
101 if let Some(desc) = opts.description {
102 info = info.description(Some(desc));
103 }
104
105 let tags: Vec<_> = opts
106 .tags
107 .iter()
108 .map(|t| {
109 let mut b = TagBuilder::new().name(t.name);
110 if let Some(d) = t.description {
111 b = b.description(Some(d));
112 }
113 b.build()
114 })
115 .collect();
116
117 OpenApiBuilder::new()
118 .info(info.build())
119 .paths(paths.build())
120 .components(Some(
121 ComponentsBuilder::new()
122 .schemas_from_iter(schema_list)
123 .build(),
124 ))
125 .tags(if tags.is_empty() { None } else { Some(tags) })
126 .build()
127}
128
129#[derive(Debug, Clone)]
131pub struct ScalarOptions<'a> {
132 pub title: &'a str,
133 pub openapi_url: &'a str,
135 pub custom_css: &'a str,
138 pub dark_mode: bool,
139 pub hide_agent: bool,
140 pub hide_mcp: bool,
141 pub hide_client_button: bool,
142 pub hide_clients: bool,
143}
144
145impl Default for ScalarOptions<'static> {
146 fn default() -> Self {
147 Self {
148 title: "API Reference",
149 openapi_url: "/api/openapi.json",
150 custom_css: "",
151 dark_mode: true,
152 hide_agent: true,
153 hide_mcp: true,
154 hide_client_button: true,
155 hide_clients: true,
156 }
157 }
158}
159
160pub fn scalar_html(opts: &ScalarOptions<'_>) -> String {
162 let css_for_js = opts
163 .custom_css
164 .replace('\\', "\\\\")
165 .replace('`', "\\`")
166 .replace("${", "\\${");
167
168 let dark_class = if opts.dark_mode { "dark-mode" } else { "" };
169 let color_scheme = if opts.dark_mode { "dark" } else { "light" };
170
171 format!(
172 r#"<!doctype html>
173<html class="{dark_class}">
174<head>
175 <title>{title}</title>
176 <meta charset="utf-8"/>
177 <meta name="viewport" content="width=device-width, initial-scale=1"/>
178 <meta name="color-scheme" content="{color_scheme}"/>
179 <style>{css}</style>
180</head>
181<body>
182 <div id="app"></div>
183 <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
184 <script>
185 Scalar.createApiReference('#app', {{
186 url: '{openapi_url}',
187 theme: 'none',
188 darkMode: {dark_mode},
189 forceDarkModeState: '{force_dark}',
190 hideDarkModeToggle: {hide_toggle},
191 withDefaultFonts: false,
192 agent: {{ disabled: {hide_agent} }},
193 mcp: {{ disabled: {hide_mcp} }},
194 hideClientButton: {hide_client_button},
195 hiddenClients: {hide_clients},
196 customCss: `{css_js}`,
197 }});
198 </script>
199</body>
200</html>
201"#,
202 dark_class = dark_class,
203 title = opts.title,
204 color_scheme = color_scheme,
205 css = opts.custom_css,
206 openapi_url = opts.openapi_url,
207 dark_mode = opts.dark_mode,
208 force_dark = if opts.dark_mode { "dark" } else { "light" },
209 hide_toggle = opts.dark_mode,
210 hide_agent = opts.hide_agent,
211 hide_mcp = opts.hide_mcp,
212 hide_client_button = opts.hide_client_button,
213 hide_clients = opts.hide_clients,
214 css_js = css_for_js,
215 )
216}