1use utoipa::OpenApi;
13
14use crate::error::ErrorBody;
15use crate::routes::{capabilities, config, draft, health, history, opc, runs, stream, templates};
16
17#[derive(OpenApi)]
18#[openapi(
19 info(
20 title = "BHTune API",
21 description = "HTTP API for BHTune: DCS/PLC PID-loop templates, and the tune-run history recorded by the CLI and this server.",
22 license(name = "AGPL-3.0-or-later"),
23 ),
24 paths(
25 health::health,
26 capabilities::capabilities,
27 templates::list_templates,
28 templates::get_template,
29 templates::create_template,
30 templates::update_template,
31 templates::delete_template,
32 history::list_runs,
33 history::last_request,
34 draft::get_draft,
35 draft::put_draft,
36 config::get_config,
37 config::put_config,
38 history::show_run,
39 history::export_run,
40 history::delete_run,
41 runs::start_run,
42 runs::cancel_run,
43 runs::update_notes,
44 runs::delete_notes,
45 runs::write_run,
46 runs::revert_run,
47 stream::stream_run,
48 opc::servers,
49 opc::capabilities,
50 opc::browse,
51 opc::close_browse_session,
52 opc::search,
53 opc::search_index_status,
54 opc::search_index,
55 opc::refresh_search_index,
56 opc::set_search_index_auto_refresh,
57 opc::control_search_index,
58 opc::delete_search_index,
59 opc::read,
60 ),
61 components(schemas(
62 health::Health,
63 templates::TemplateResponse,
64 history::RunSummaryResponse,
65 history::RunListResponse,
66 history::InitialReadingsResponse,
67 history::SampleResponse,
68 history::ResultResponse,
69 history::WriteResponse,
70 history::MvActuationResponse,
71 history::PidConstantTagsResponse,
72 history::PidParameterLabelsResponse,
73 history::RunDetailResponse,
74 bhtune_db::models::EffectiveTuning,
75 history::RunExportFormat,
76 runs::StartRunRequest,
77 draft::NewRunDraft,
78 config::ConfigResponse,
79 config::ConfigValues,
80 config::ConfigTuningValues,
81 config::ConfigTuningSources,
82 config::ConfigTuningTomlValues,
83 config::ConfigTomlValues,
84 config::ConfigSources,
85 config::UpdateConfigRequest,
86 config::UpdateTuningRequest,
87 runs::UpdateNotesRequest,
88 runs::WriteRunRequest,
89 stream::RunStreamDone,
90 opc::OpcServersResponse,
91 opc::OpcCapabilitiesResponse,
92 opc::OpcBrowseNodeKind,
93 opc::OpcBrowseNodeResponse,
94 opc::OpcBrowseResponse,
95 opc::OpcCloseBrowseSessionResponse,
96 opc::OpcIndexedSearchProgressResponse,
97 opc::OpcIndexSchedulerResponse,
98 opc::OpcSearchIndexStatusResponse,
99 opc::OpcIndexedSearchMatchResponse,
100 opc::OpcSearchIndexResponse,
101 opc::OpcReadResponse,
102 capabilities::CapabilitiesResponse,
103 capabilities::CapabilityActions,
104 capabilities::FloatBounds,
105 capabilities::IntegerBounds,
106 capabilities::ProcessControllerCompatibility,
107 capabilities::DemoSimulatorDefaults,
108 capabilities::DemoSimulatorLimits,
109 capabilities::DemoSimulatorCapabilities,
110 capabilities::DemoRestrictions,
111 capabilities::DemoQuotas,
112 capabilities::CookieCapabilities,
113 capabilities::SecurityCapabilities,
114 ErrorBody,
115 )),
116 tags(
117 (name = "health", description = "Liveness probe"),
118 (name = "templates", description = "DCS/PLC template catalog (built-in, community-catalog, and user-created)"),
119 (name = "runs", description = "Start, cancel, and browse the history of tune runs"),
120 (name = "config", description = "Global TOML-backed configuration"),
121 (name = "opc", description = "OPC DA server/tag diagnostics and gateway-owned namespace search"),
122 ),
123)]
124pub struct ApiDoc;
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn generated_spec_is_openapi_3_1() {
132 let spec = ApiDoc::openapi();
133 let json = spec.to_json().expect("spec must serialize to JSON");
134 assert!(json.contains("\"openapi\":\"3.1.0\""));
135 }
136
137 #[test]
138 fn generated_spec_serializes_to_json() {
139 let spec = ApiDoc::openapi();
140 let json = spec.to_json().expect("spec must serialize to JSON");
141 assert!(json.contains("\"title\":\"BHTune API\""));
142 }
143
144 #[test]
145 fn generated_spec_includes_indexed_search_routes_and_schemas() {
146 let spec = ApiDoc::openapi();
147 let document: serde_json::Value =
148 serde_json::from_str(&spec.to_json().expect("spec must serialize to JSON"))
149 .expect("generated spec must be valid JSON");
150
151 for (path, method) in [
152 ("/api/opc/search-index/status", "get"),
153 ("/api/opc/search-index/search", "get"),
154 ("/api/opc/search-index/refresh", "post"),
155 ("/api/opc/search-index/auto-refresh", "post"),
156 ("/api/opc/search-index/control", "post"),
157 ("/api/opc/search-index", "delete"),
158 ] {
159 assert!(
160 document["paths"][path][method].is_object(),
161 "missing {method} {path}"
162 );
163 }
164
165 for schema in [
166 "OpcIndexedSearchProgressResponse",
167 "OpcSearchIndexStatusResponse",
168 "OpcIndexSchedulerResponse",
169 "OpcIndexedSearchMatchResponse",
170 "OpcSearchIndexResponse",
171 ] {
172 assert!(
173 document["components"]["schemas"][schema].is_object(),
174 "missing schema {schema}"
175 );
176 }
177 }
178
179 #[test]
180 fn indexed_and_progressive_search_limits_require_positive_values() {
181 let spec = ApiDoc::openapi();
182 let value = serde_json::to_value(spec).expect("spec must serialize to a JSON value");
183
184 for path in ["/api/opc/search", "/api/opc/search-index/search"] {
185 assert_eq!(
186 value["paths"][path]["get"]["parameters"]
187 .as_array()
188 .and_then(|parameters| {
189 parameters
190 .iter()
191 .find(|parameter| parameter["name"] == "max_results")
192 })
193 .and_then(|parameter| parameter.pointer("/schema/minimum")),
194 Some(&serde_json::json!(1)),
195 "{path} must document a positive max_results minimum"
196 );
197 }
198 }
199}