1use bhtune_db::models::SampleQuality;
2use bhtune_driver::{
3 BrowseNode, BrowseNodeKind, BrowsePage, DriverCapabilities, IndexedSearchMatch,
4 IndexedSearchProgress, OpcDaFeatureCompatibility, OpcDaGatewayCompatibility,
5 SearchIndexResponse, SearchIndexStatus,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use utoipa::{IntoParams, ToSchema};
10
11use super::helpers::{organization_name, source_name};
12use super::validation::{
13 default_index_search_max_results, default_page_size, default_search_match_mode,
14 default_search_max_results,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
19pub struct GatewayProtocolRangeResponse {
20 pub min: u32,
21 pub max: u32,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
26pub struct GatewayFeatureCompatibilityResponse {
27 pub feature: String,
28 pub status: String,
29 pub client_versions: GatewayProtocolRangeResponse,
30 pub gateway_versions: Option<GatewayProtocolRangeResponse>,
31 pub negotiated_version: Option<u32>,
32 pub reason: String,
33}
34
35impl From<&OpcDaFeatureCompatibility> for GatewayFeatureCompatibilityResponse {
36 fn from(feature: &OpcDaFeatureCompatibility) -> Self {
37 Self {
38 feature: feature.feature.as_str().to_string(),
39 status: feature.status.as_str().to_string(),
40 client_versions: GatewayProtocolRangeResponse {
41 min: feature.client_versions.min,
42 max: feature.client_versions.max,
43 },
44 gateway_versions: feature.gateway_versions.as_ref().map(|range| {
45 GatewayProtocolRangeResponse {
46 min: range.min,
47 max: range.max,
48 }
49 }),
50 negotiated_version: feature.negotiated_version,
51 reason: feature.reason.clone(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
63pub struct GatewayCompatibilityResponse {
64 pub client_version: String,
65 pub gateway_version: Option<String>,
66 pub source: String,
67 pub status: String,
68 pub features: Vec<GatewayFeatureCompatibilityResponse>,
69}
70
71impl From<&OpcDaGatewayCompatibility> for GatewayCompatibilityResponse {
72 fn from(report: &OpcDaGatewayCompatibility) -> Self {
73 Self {
74 client_version: report.client_version.clone(),
75 gateway_version: report.gateway_version.clone(),
76 source: report.source.as_str().to_string(),
77 status: report.status.as_str().to_string(),
78 features: report
79 .features
80 .iter()
81 .map(GatewayFeatureCompatibilityResponse::from)
82 .collect(),
83 }
84 }
85}
86
87impl From<OpcDaGatewayCompatibility> for GatewayCompatibilityResponse {
88 fn from(report: OpcDaGatewayCompatibility) -> Self {
89 Self::from(&report)
90 }
91}
92
93#[derive(Debug, Deserialize, IntoParams)]
95#[into_params(parameter_in = Query)]
96pub struct OpcServersQuery {
97 pub bridge_host: Option<String>,
100}
101
102#[derive(Debug, Serialize, ToSchema)]
104pub struct OpcServersResponse {
105 pub servers: Vec<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
109 pub gateway_compatibility: Option<GatewayCompatibilityResponse>,
110}
111
112#[derive(Debug, Deserialize, IntoParams)]
114#[into_params(parameter_in = Query)]
115pub struct OpcServerQuery {
116 pub bridge_host: Option<String>,
117 pub opc_server: Option<String>,
118}
119
120#[derive(Debug, Serialize, ToSchema)]
121pub struct OpcCapabilitiesResponse {
122 pub application_version: String,
123 pub protocol_version: String,
124 pub max_page_size: u32,
125 pub supports_browse_sessions: bool,
126 pub supports_search: bool,
127 pub organization: String,
128 pub source: String,
129 pub supports_indexed_search: bool,
130 pub indexed_search_protocol_version: String,
131 pub max_indexed_search_results: u32,
132 pub search_index_state: String,
133 #[serde(skip_serializing_if = "Option::is_none")]
136 pub gateway_compatibility: Option<GatewayCompatibilityResponse>,
137}
138
139impl From<DriverCapabilities> for OpcCapabilitiesResponse {
140 fn from(capabilities: DriverCapabilities) -> Self {
141 Self {
142 application_version: capabilities.application_version,
143 protocol_version: capabilities.protocol_version,
144 max_page_size: capabilities.max_page_size,
145 supports_browse_sessions: capabilities.supports_browse_sessions,
146 supports_search: capabilities.supports_search,
147 organization: organization_name(capabilities.organization).to_string(),
148 source: source_name(capabilities.source).to_string(),
149 supports_indexed_search: capabilities.supports_indexed_search,
150 indexed_search_protocol_version: capabilities.indexed_search_protocol_version,
151 max_indexed_search_results: capabilities.max_indexed_search_results,
152 search_index_state: capabilities.search_index_state.to_string(),
153 gateway_compatibility: None,
154 }
155 }
156}
157
158#[derive(Debug, Deserialize, IntoParams)]
160#[into_params(parameter_in = Query)]
161pub struct OpcSearchIndexServerQuery {
162 pub bridge_host: Option<String>,
163 pub opc_server: Option<String>,
164}
165
166#[derive(Debug, Serialize, ToSchema)]
167pub struct OpcIndexedSearchProgressResponse {
168 pub branches_visited: u64,
169 pub entries_seen: u64,
170 pub unique_items: u64,
171 pub active_time_ms: u64,
172 pub paused_time_ms: u64,
173 pub items_per_second: f64,
174 pub estimated_remaining_ms: Option<u64>,
175}
176
177impl From<IndexedSearchProgress> for OpcIndexedSearchProgressResponse {
178 fn from(progress: IndexedSearchProgress) -> Self {
179 Self {
180 branches_visited: progress.branches_visited,
181 entries_seen: progress.entries_seen,
182 unique_items: progress.unique_items,
183 active_time_ms: progress.active_time_ms,
184 paused_time_ms: progress.paused_time_ms,
185 items_per_second: progress.items_per_second,
186 estimated_remaining_ms: progress.estimated_remaining_ms,
187 }
188 }
189}
190
191#[derive(Debug, Serialize, ToSchema)]
192pub struct OpcSearchIndexStatusResponse {
193 pub server: String,
194 pub state: String,
195 pub auto_refresh_enabled: bool,
196 pub active_generation: u64,
197 pub entry_count: u64,
198 pub unique_item_count: u64,
199 pub started_at: Option<String>,
200 pub completed_at: Option<String>,
201 pub last_error: Option<String>,
202 pub database_bytes: u64,
203 pub organization: String,
204 pub source: String,
205 pub progress: Option<OpcIndexedSearchProgressResponse>,
206 pub scheduler: OpcIndexSchedulerResponse,
207}
208
209#[derive(Debug, Serialize, ToSchema)]
210pub struct OpcIndexSchedulerResponse {
211 pub next_refresh_at: Option<String>,
212 pub last_attempt_at: Option<String>,
213 pub last_success_at: Option<String>,
214 pub last_success_duration_ms: Option<u64>,
215 pub retry_after: Option<String>,
216 pub consecutive_failures: u32,
217 pub circuit_open: bool,
218}
219
220impl From<SearchIndexStatus> for OpcSearchIndexStatusResponse {
221 fn from(status: SearchIndexStatus) -> Self {
222 Self {
223 server: status.server,
224 state: status.state.to_string(),
225 auto_refresh_enabled: status.auto_refresh_enabled,
226 active_generation: status.active_generation,
227 entry_count: status.entry_count,
228 unique_item_count: status.unique_item_count,
229 started_at: status.started_at,
230 completed_at: status.completed_at,
231 last_error: status.last_error,
232 database_bytes: status.database_bytes,
233 organization: organization_name(status.organization).to_string(),
234 source: source_name(status.source).to_string(),
235 progress: status.progress.map(Into::into),
236 scheduler: OpcIndexSchedulerResponse {
237 next_refresh_at: status.scheduler.next_refresh_at,
238 last_attempt_at: status.scheduler.last_attempt_at,
239 last_success_at: status.scheduler.last_success_at,
240 last_success_duration_ms: status.scheduler.last_success_duration_ms,
241 retry_after: status.scheduler.retry_after,
242 consecutive_failures: status.scheduler.consecutive_failures,
243 circuit_open: status.scheduler.circuit_open,
244 },
245 }
246 }
247}
248
249#[derive(Debug, Serialize, ToSchema)]
250pub struct OpcIndexedSearchMatchResponse {
251 pub item_id: String,
252 pub display_name: String,
253 pub kind: OpcBrowseNodeKind,
254 pub breadcrumbs: Vec<String>,
255}
256
257impl From<IndexedSearchMatch> for OpcIndexedSearchMatchResponse {
258 fn from(found: IndexedSearchMatch) -> Self {
259 Self {
260 item_id: found.item_id,
261 display_name: found.display_name,
262 kind: found.kind.into(),
263 breadcrumbs: found.breadcrumbs,
264 }
265 }
266}
267
268#[derive(Debug, Serialize, ToSchema)]
269pub struct OpcSearchIndexResponse {
270 pub matches: Vec<OpcIndexedSearchMatchResponse>,
271 pub has_more: bool,
272 pub status: OpcSearchIndexStatusResponse,
273}
274
275impl From<SearchIndexResponse> for OpcSearchIndexResponse {
276 fn from(response: SearchIndexResponse) -> Self {
277 Self {
278 matches: response.matches.into_iter().map(Into::into).collect(),
279 has_more: response.has_more,
280 status: response.status.into(),
281 }
282 }
283}
284
285#[derive(Debug, Deserialize, IntoParams)]
288#[into_params(parameter_in = Query)]
289pub struct OpcSearchIndexQuery {
290 pub bridge_host: Option<String>,
291 pub opc_server: Option<String>,
292 pub query: String,
293 #[serde(default = "default_search_match_mode")]
294 pub match_mode: String,
295 #[serde(default = "default_index_search_max_results")]
296 #[param(minimum = 1)]
297 pub max_results: u32,
298}
299
300#[derive(Debug, Deserialize, IntoParams)]
302#[into_params(parameter_in = Query)]
303pub struct OpcSearchIndexRefreshQuery {
304 pub bridge_host: Option<String>,
305 pub opc_server: Option<String>,
306 pub force: Option<bool>,
307}
308
309#[derive(Debug, Deserialize, IntoParams)]
311#[into_params(parameter_in = Query)]
312pub struct OpcSearchIndexAutoRefreshQuery {
313 pub bridge_host: Option<String>,
314 pub opc_server: Option<String>,
315 pub enabled: bool,
316}
317
318#[derive(Debug, Deserialize, IntoParams)]
320#[into_params(parameter_in = Query)]
321pub struct OpcSearchIndexControlQuery {
322 pub bridge_host: Option<String>,
323 pub opc_server: Option<String>,
324 pub action: String,
325}
326
327#[derive(Debug, Deserialize, IntoParams)]
329#[into_params(parameter_in = Query)]
330pub struct OpcBrowseQuery {
331 pub bridge_host: Option<String>,
332 pub opc_server: Option<String>,
333 pub session_id: Option<String>,
334 pub parent_node_key: Option<String>,
335 pub page_token: Option<String>,
336 #[serde(default = "default_page_size")]
337 pub page_size: u32,
338 pub refresh: Option<bool>,
339}
340
341#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
342#[serde(rename_all = "snake_case")]
343pub enum OpcBrowseNodeKind {
344 Unspecified,
345 Branch,
346 Item,
347 BranchAndItem,
348}
349
350impl From<BrowseNodeKind> for OpcBrowseNodeKind {
351 fn from(kind: BrowseNodeKind) -> Self {
352 match kind {
353 BrowseNodeKind::Unspecified => Self::Unspecified,
354 BrowseNodeKind::Branch => Self::Branch,
355 BrowseNodeKind::Item => Self::Item,
356 BrowseNodeKind::BranchAndItem => Self::BranchAndItem,
357 }
358 }
359}
360
361#[derive(Debug, Serialize, ToSchema)]
365pub struct OpcBrowseNodeResponse {
366 pub node_key: String,
367 pub display_name: String,
368 pub kind: OpcBrowseNodeKind,
369 pub item_id: Option<String>,
370}
371
372impl From<BrowseNode> for OpcBrowseNodeResponse {
373 fn from(node: BrowseNode) -> Self {
374 Self {
375 node_key: node.node_key,
376 display_name: node.display_name,
377 kind: node.kind.into(),
378 item_id: node.item_id,
379 }
380 }
381}
382
383#[derive(Debug, Serialize, ToSchema)]
384pub struct OpcBrowseResponse {
385 pub session_id: String,
386 pub nodes: Vec<OpcBrowseNodeResponse>,
387 pub next_page_token: Option<String>,
388 pub complete: bool,
389 pub organization: String,
390 pub source: String,
391 pub warning: Option<String>,
392 #[serde(skip_serializing_if = "Option::is_none")]
395 pub gateway_compatibility: Option<GatewayCompatibilityResponse>,
396}
397
398impl From<BrowsePage> for OpcBrowseResponse {
399 fn from(page: BrowsePage) -> Self {
400 Self {
401 session_id: page.session_id,
402 nodes: page.nodes.into_iter().map(Into::into).collect(),
403 next_page_token: page.next_page_token,
404 complete: page.complete,
405 organization: organization_name(page.organization).to_string(),
406 source: source_name(page.source).to_string(),
407 warning: page.warning,
408 gateway_compatibility: None,
409 }
410 }
411}
412
413#[derive(Debug, Serialize, ToSchema)]
414pub struct OpcCloseBrowseSessionResponse {
415 pub closed: bool,
416}
417
418#[derive(Debug, Deserialize, IntoParams)]
420#[into_params(parameter_in = Query)]
421pub struct OpcSearchQuery {
422 pub bridge_host: Option<String>,
423 pub opc_server: Option<String>,
424 pub query: String,
425 #[serde(default = "default_search_match_mode")]
426 pub match_mode: String,
427 pub session_id: Option<String>,
428 pub scope_node_key: Option<String>,
429 #[serde(default = "default_search_max_results")]
430 #[param(minimum = 1)]
431 pub max_results: u32,
432 pub include_branches: Option<bool>,
433 pub refresh: Option<bool>,
434}
435
436#[derive(Debug, Deserialize, IntoParams)]
438#[into_params(parameter_in = Query)]
439pub struct OpcReadQuery {
440 pub bridge_host: Option<String>,
441 pub opc_server: Option<String>,
442 pub tag: Option<String>,
445}
446
447#[derive(Debug, Serialize, ToSchema)]
455pub struct OpcReadResponse {
456 pub tag: String,
457 pub value: String,
458 pub quality: SampleQuality,
459 pub timestamp: Option<DateTime<Utc>>,
466 #[serde(skip_serializing_if = "Option::is_none")]
469 pub gateway_compatibility: Option<GatewayCompatibilityResponse>,
470}