Skip to main content

obz_core/
registry.rs

1//! Provider registry.
2//!
3//! The registry is the runtime catalog of available providers.
4//! It is analogous to `DuckDB`'s catalog — providers register their
5//! metadata and factory functions here at startup. The obz shell then
6//! looks up a provider by name and instantiates it on demand with the
7//! user-supplied endpoint and credentials.
8//!
9//! # Why lazy instantiation?
10//!
11//! Different providers use different default ports and authentication
12//! schemes. The endpoint is supplied by the user at invocation time via
13//! `--endpoint`, so providers cannot be instantiated until after argument
14//! parsing. The registry stores factory functions; the shell calls the
15//! right one after resolving `--provider` and `--endpoint`.
16//!
17//! # Registration flow (performed by the obz shell at startup)
18//!
19//! ```text
20//! obz main()
21//!   → build ProviderRegistry
22//!   → registry.register(ProviderMeta { name: "<provider>", aliases: &[...], ... })
23//!   → ... (repeat for each built-in provider)
24//!   → clap parses args
25//!   → registry.get("<alias>")? → &ProviderMeta
26//!   → meta.build(config)? → BuiltProvider { metric: Some(Box<dyn MetricProvider>), ... }
27//! ```
28
29use std::collections::BTreeMap;
30use std::future::Future;
31use std::pin::Pin;
32use std::time::Duration;
33
34use crate::cmd_path::StandardCommand;
35use crate::descriptor::{CommandDescriptor, FlagDescriptor};
36use crate::model::error::{ErrorCode, ObzError};
37use crate::provider::traits::{ExtensionProvider, LogProvider, MetricProvider, TraceProvider};
38use crate::provider::ProviderConfig;
39
40/// A fully instantiated provider ready to execute commands.
41///
42/// Built by calling [`ProviderMeta::build`] after the user's endpoint
43/// and credentials are known. Each signal capability is `None` if the
44/// provider does not support that signal.
45pub struct BuiltProvider {
46    /// The canonical provider name (for use in response metadata).
47    pub name: &'static str,
48    /// Query language for metric queries (e.g. `"MetricsQL"`).
49    pub metric_query_language: Option<&'static str>,
50    /// Query language for log queries (e.g. `"LogsQL"`).
51    pub log_query_language: Option<&'static str>,
52    /// Metric provider implementation.
53    pub metric: Option<Box<dyn MetricProvider>>,
54    /// Log provider implementation.
55    pub log: Option<Box<dyn LogProvider>>,
56    /// Trace provider implementation.
57    pub trace: Option<Box<dyn TraceProvider>>,
58    /// Extension command provider implementation.
59    pub extension: Option<Box<dyn ExtensionProvider>>,
60}
61
62/// Type alias for the provider factory function.
63///
64/// Takes connection config and returns a fully built provider or an error
65/// if the HTTP client cannot be constructed (e.g. invalid TLS config).
66pub type ProviderFactory = fn(&ProviderConfig) -> Result<BuiltProvider, ObzError>;
67
68/// Result of a provider-specific health check.
69#[derive(Debug, Clone)]
70pub struct CheckResult {
71    /// Severity level of the check outcome.
72    pub severity: CheckSeverity,
73    /// Human-readable status message.
74    pub message: String,
75    /// What the check verified.
76    pub scope: CheckScope,
77    /// End-to-end latency for the live check when a request was attempted.
78    pub latency: Option<Duration>,
79}
80
81/// Severity level for a provider check result.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum CheckSeverity {
84    /// Check passed successfully.
85    Ok,
86    /// Check completed but with a warning (e.g. server error, degraded).
87    Warn,
88    /// Check failed.
89    Fail,
90}
91
92/// What a provider check verified.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum CheckScope {
95    /// Only network connectivity was verified.
96    Connectivity,
97    /// Both connectivity and authentication were verified.
98    ConnectivityAndAuth,
99    /// Credentials are configured but the check could not verify them.
100    ConfiguredNotVerifiable,
101}
102
103/// Type alias for an async provider check function.
104///
105/// The function takes the provider's resolved config and returns a `CheckResult`.
106pub type ProviderCheckFn =
107    fn(&ProviderConfig) -> Pin<Box<dyn Future<Output = CheckResult> + Send + '_>>;
108
109/// Which standard (core) commands a provider supports.
110///
111/// Used by the obz shell to generate accurate `--help` output showing
112/// provider support tags next to each core command.
113/// Set a field to `true` if the provider implements that operation.
114#[derive(Debug, Clone, Copy, Default)]
115pub struct SupportedCommands {
116    // --- metric ---
117    pub metric_query: bool,
118    pub metric_list: bool,
119    pub metric_info: bool,
120    pub metric_labels: bool,
121    pub metric_label_values: bool,
122    pub metric_series: bool,
123    // --- log ---
124    pub log_search: bool,
125    // --- trace ---
126    pub trace_search: bool,
127    pub trace_get: bool,
128}
129
130/// Static metadata and factory for a single provider.
131///
132/// Stored in the registry; does not hold any live connections or state.
133pub struct ProviderMeta {
134    /// Canonical name used as the primary `--provider` value.
135    pub name: &'static str,
136    /// Human-readable display name shown in help and diagnostics.
137    pub display_name: &'static str,
138    /// All accepted `--provider` aliases, including the canonical name.
139    /// Insertion order is preserved for deterministic `--help` output.
140    pub aliases: &'static [&'static str],
141    /// Which standard core commands this provider supports.
142    /// Used to generate provider support tags in `--help` output.
143    pub supported_commands: SupportedCommands,
144    /// Factory function — instantiates the provider from a `ProviderConfig`.
145    pub build: ProviderFactory,
146    /// Optional health check function registered by the provider.
147    ///
148    /// When `Some`, `provider check` calls this to verify connectivity
149    /// and (where possible) authentication. When `None`, the check is skipped.
150    pub check: Option<ProviderCheckFn>,
151    /// Provider-specific flags per standard command.
152    /// Key is the standard command identifier for the target core command.
153    pub command_flags: &'static [(StandardCommand, &'static [FlagDescriptor])],
154    /// Extension commands registered by this provider.
155    ///
156    /// Each entry is a `(signal, descriptor)` pair where `signal` is the
157    /// signal group the command belongs to (e.g. `"trace"`, `"metric"`).
158    /// Unlike `command_flags` (which uses [`StandardCommand`] identifiers), extension commands
159    /// use a bare signal name because the command name itself lives in
160    /// the descriptor.
161    pub extension_commands: &'static [(&'static str, CommandDescriptor)],
162}
163
164/// Runtime catalog of available providers.
165///
166/// Built once at startup by the obz shell. Providers are not instantiated
167/// until the user's `--provider` and `--endpoint` are known.
168#[derive(Default)]
169pub struct ProviderRegistry {
170    /// Alias → index into `metas`. `BTreeMap` for deterministic ordering.
171    by_alias: BTreeMap<String, usize>,
172    /// Insertion-ordered list of provider metadata.
173    metas: Vec<ProviderMeta>,
174}
175
176impl ProviderRegistry {
177    /// Create an empty registry.
178    pub fn new() -> Self {
179        Self::default()
180    }
181
182    /// Register a provider.
183    ///
184    /// # Panics
185    ///
186    /// Panics if any alias conflicts with an already-registered provider.
187    /// This is a programming error caught at startup, not a runtime error.
188    pub fn register(&mut self, meta: ProviderMeta) {
189        let idx = self.metas.len();
190        for &alias in meta.aliases {
191            assert!(
192                self.by_alias.insert(alias.to_string(), idx).is_none(),
193                "provider alias '{}' is already registered",
194                alias
195            );
196        }
197        self.metas.push(meta);
198    }
199
200    /// Look up provider metadata by name or alias.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`ObzError::InvalidArgument`] if the name is not registered.
205    pub fn get(&self, name: &str) -> Result<&ProviderMeta, ObzError> {
206        self.by_alias
207            .get(name)
208            .map(|&i| &self.metas[i])
209            .ok_or_else(|| ObzError::InvalidArgument {
210                code: ErrorCode::InvalidFlag,
211                message: format!(
212                    "unknown provider '{}' — run `obz provider list` to see available providers",
213                    name
214                ),
215                suggestion: None,
216            })
217    }
218
219    /// All registered provider metadata in insertion order.
220    pub fn all(&self) -> &[ProviderMeta] {
221        &self.metas
222    }
223
224    /// All accepted alias strings in sorted order (for clap validation).
225    pub fn all_aliases(&self) -> Vec<&str> {
226        self.by_alias.keys().map(String::as_str).collect()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// Build a minimal dummy `ProviderMeta` for testing.
235    ///
236    /// Uses `SupportedCommands::default()` and no extension commands — the
237    /// registry tests only care about name/alias resolution, not capabilities.
238    fn dummy_meta(name: &'static str, aliases: &'static [&'static str]) -> ProviderMeta {
239        fn dummy_build(_: &ProviderConfig) -> Result<BuiltProvider, ObzError> {
240            Ok(BuiltProvider {
241                name: "dummy",
242                metric_query_language: None,
243                log_query_language: None,
244                metric: None,
245                log: None,
246                trace: None,
247                extension: None,
248            })
249        }
250
251        ProviderMeta {
252            name,
253            display_name: name,
254            aliases,
255            supported_commands: SupportedCommands::default(),
256            build: dummy_build,
257            check: None,
258            command_flags: &[],
259            extension_commands: &[],
260        }
261    }
262
263    #[test]
264    fn test_get_resolves_provider_by_name_and_alias() {
265        let mut registry = ProviderRegistry::new();
266        registry.register(dummy_meta("victoriametrics", &["vm", "victoriametrics"]));
267
268        let meta = registry.get("vm").unwrap();
269        assert_eq!(meta.name, "victoriametrics");
270
271        let meta2 = registry.get("victoriametrics").unwrap();
272        assert_eq!(meta2.name, "victoriametrics");
273    }
274
275    #[test]
276    fn test_get_returns_error_for_unknown_provider() {
277        let registry = ProviderRegistry::new();
278        match registry.get("nonexistent") {
279            Err(ObzError::InvalidArgument { code, message, .. }) => {
280                assert_eq!(code, ErrorCode::InvalidFlag);
281                assert!(message.contains("nonexistent"));
282            }
283            Err(other) => panic!("expected InvalidArgument, got {other:?}"),
284            Ok(_) => panic!("expected error, got Ok"),
285        }
286    }
287
288    #[test]
289    fn test_all_preserves_insertion_order() {
290        let mut registry = ProviderRegistry::new();
291        registry.register(dummy_meta("alpha", &["alpha"]));
292        registry.register(dummy_meta("beta", &["beta"]));
293        registry.register(dummy_meta("gamma", &["gamma"]));
294
295        let names: Vec<&str> = registry.all().iter().map(|m| m.name).collect();
296        assert_eq!(names, vec!["alpha", "beta", "gamma"]);
297    }
298
299    #[test]
300    fn test_all_aliases_returns_sorted_list() {
301        let mut registry = ProviderRegistry::new();
302        registry.register(dummy_meta("victoriametrics", &["vm", "victoriametrics"]));
303        registry.register(dummy_meta("victorialogs", &["vl", "victorialogs"]));
304
305        let aliases = registry.all_aliases();
306        // BTreeMap is sorted, so aliases should be in alphabetical order.
307        assert_eq!(aliases, vec!["victorialogs", "victoriametrics", "vl", "vm"]);
308    }
309
310    #[test]
311    #[should_panic(expected = "provider alias 'vm' is already registered")]
312    fn test_register_panics_on_duplicate_alias() {
313        let mut registry = ProviderRegistry::new();
314        registry.register(dummy_meta("provider1", &["vm"]));
315        registry.register(dummy_meta("provider2", &["vm"])); // should panic
316    }
317
318    #[test]
319    fn test_new_registry_is_empty() {
320        let registry = ProviderRegistry::new();
321        assert!(registry.all().is_empty());
322        assert!(registry.all_aliases().is_empty());
323    }
324
325    #[test]
326    fn test_registry_with_extension_commands() {
327        static EXT_CMDS: &[(&str, CommandDescriptor)] = &[
328            (
329                "trace",
330                CommandDescriptor {
331                    name: "services",
332                    description: "List services",
333                    flags: &[],
334                },
335            ),
336            (
337                "metric",
338                CommandDescriptor {
339                    name: "top-queries",
340                    description: "Top queries",
341                    flags: &[],
342                },
343            ),
344        ];
345
346        fn build_with_ext(_: &ProviderConfig) -> Result<BuiltProvider, ObzError> {
347            Ok(BuiltProvider {
348                name: "test",
349                metric_query_language: None,
350                log_query_language: None,
351                metric: None,
352                log: None,
353                trace: None,
354                extension: None,
355            })
356        }
357
358        let meta = ProviderMeta {
359            name: "testprov",
360            display_name: "TestProvider",
361            aliases: &["tp"],
362            supported_commands: SupportedCommands::default(),
363            build: build_with_ext,
364            check: None,
365            command_flags: &[],
366            extension_commands: EXT_CMDS,
367        };
368
369        let mut registry = ProviderRegistry::new();
370        registry.register(meta);
371
372        let retrieved = registry.get("tp").unwrap();
373        assert_eq!(retrieved.extension_commands.len(), 2);
374        assert_eq!(retrieved.extension_commands[0].0, "trace");
375        assert_eq!(retrieved.extension_commands[0].1.name, "services");
376        assert_eq!(retrieved.extension_commands[1].0, "metric");
377        assert_eq!(retrieved.extension_commands[1].1.name, "top-queries");
378    }
379}