Skip to main content

osp_cli/
lib.rs

1#![cfg_attr(
2    not(test),
3    warn(clippy::expect_used, clippy::panic, clippy::unwrap_used)
4)]
5#![deny(missing_docs)]
6
7//! `osp-cli` is the library behind the `osp` CLI and REPL.
8//!
9//! Use it when you want one of these jobs:
10//!
11//! - run the full `osp` host in-process
12//! - build a wrapper crate with site-specific native commands and defaults
13//! - execute the small LDAP service surface without the full host
14//! - render rows or run DSL pipelines in-process
15//!
16//! Most readers only need one of those lanes. You do not need to understand
17//! the whole crate before using it.
18//!
19//! The crate also keeps the full `osp` product surface in one place so the
20//! main concerns stay visible together: host orchestration, config resolution,
21//! rendering, REPL integration, completion, plugins, and the pipeline DSL.
22//! That makes rustdoc a useful architecture map after you have picked the
23//! smallest surface that fits your job.
24//!
25//! Quick starts for the three most common library shapes:
26//!
27//! Full `osp`-style host with captured output:
28//!
29//! ```
30//! use osp_cli::App;
31//! use osp_cli::app::BufferedUiSink;
32//!
33//! let mut sink = BufferedUiSink::default();
34//! let exit = App::new().run_with_sink(["osp", "--help"], &mut sink)?;
35//!
36//! assert_eq!(exit, 0);
37//! assert!(!sink.stdout.is_empty());
38//! assert!(sink.stderr.is_empty());
39//! # Ok::<(), miette::Report>(())
40//! ```
41//!
42//! Lightweight LDAP command execution plus DSL stages:
43//!
44//! ```
45//! use osp_cli::config::RuntimeConfig;
46//! use osp_cli::ports::mock::MockLdapClient;
47//! use osp_cli::services::{ServiceContext, execute_line};
48//!
49//! let ctx = ServiceContext::new(
50//!     Some("oistes".to_string()),
51//!     MockLdapClient::default(),
52//!     RuntimeConfig::default(),
53//! );
54//! let output = execute_line(&ctx, "ldap user oistes | P uid cn")
55//!     .expect("service command should run");
56//! let rows = output.as_rows().expect("expected row output");
57//!
58//! assert_eq!(rows.len(), 1);
59//! assert_eq!(rows[0].get("uid").and_then(|value| value.as_str()), Some("oistes"));
60//! assert!(rows[0].contains_key("cn"));
61//! ```
62//!
63//! Rendering existing rows without bootstrapping the full host:
64//!
65//! ```
66//! use osp_cli::core::output::OutputFormat;
67//! use osp_cli::row;
68//! use osp_cli::ui::{RenderSettings, render_rows};
69//!
70//! let rendered = render_rows(
71//!     &[row! { "uid" => "alice", "mail" => "alice@example.com" }],
72//!     &RenderSettings::test_plain(OutputFormat::Json),
73//! );
74//!
75//! assert!(rendered.contains("\"uid\": \"alice\""));
76//! assert!(rendered.contains("\"mail\": \"alice@example.com\""));
77//! ```
78//!
79//! Building a product-specific wrapper crate:
80//!
81//! - keep site-specific auth, policy, and domain integrations in the wrapper
82//!   crate
83//! - extend the command surface with [`App::with_native_commands`] or
84//!   [`AppBuilder::with_native_commands`]
85//! - keep runtime config bootstrap aligned with
86//!   [`config::RuntimeDefaults`], [`config::RuntimeConfigPaths`], and
87//!   [`config::build_runtime_pipeline`]
88//! - expose a thin product-level `run_process` or builder API on top of this
89//!   crate instead of forking generic host behavior
90//!
91//! Minimal wrapper shape:
92//!
93//! ```
94//! use std::ffi::OsString;
95//!
96//! use anyhow::Result;
97//! use clap::Command;
98//! use osp_cli::app::BufferedUiSink;
99//! use osp_cli::config::ConfigLayer;
100//! use osp_cli::{
101//!     App, AppBuilder, NativeCommand, NativeCommandContext, NativeCommandOutcome,
102//!     NativeCommandRegistry,
103//! };
104//!
105//! struct SiteStatusCommand;
106//!
107//! impl NativeCommand for SiteStatusCommand {
108//!     fn command(&self) -> Command {
109//!         Command::new("site-status").about("Show site-specific status")
110//!     }
111//!
112//!     fn execute(
113//!         &self,
114//!         _args: &[String],
115//!         _context: &NativeCommandContext<'_>,
116//!     ) -> Result<NativeCommandOutcome> {
117//!         Ok(NativeCommandOutcome::Exit(0))
118//!     }
119//! }
120//!
121//! fn site_registry() -> NativeCommandRegistry {
122//!     NativeCommandRegistry::new().with_command(SiteStatusCommand)
123//! }
124//!
125//! fn site_defaults() -> ConfigLayer {
126//!     let mut defaults = ConfigLayer::default();
127//!     defaults.set("extensions.site.enabled", true);
128//!     defaults
129//! }
130//!
131//! #[derive(Clone)]
132//! struct SiteApp {
133//!     inner: App,
134//! }
135//!
136//! impl SiteApp {
137//!     fn builder() -> AppBuilder {
138//!         App::builder()
139//!             .with_native_commands(site_registry())
140//!             .with_product_defaults(site_defaults())
141//!     }
142//!
143//!     fn new() -> Self {
144//!         Self {
145//!             inner: Self::builder().build(),
146//!         }
147//!     }
148//!
149//!     fn run_process<I, T>(&self, args: I) -> i32
150//!     where
151//!         I: IntoIterator<Item = T>,
152//!         T: Into<OsString> + Clone,
153//!     {
154//!         self.inner.run_process(args)
155//!     }
156//! }
157//!
158//! let app = SiteApp::new();
159//! let mut sink = BufferedUiSink::default();
160//! let exit = app.inner.run_process_with_sink(["osp", "--help"], &mut sink);
161//!
162//! assert_eq!(exit, 0);
163//! assert!(sink.stdout.contains("site-status"));
164//! assert_eq!(app.run_process(["osp", "--help"]), 0);
165//! ```
166//!
167//! If you are new here, start with one of these:
168//!
169//! - wrapper crate / downstream product →
170//!   [embedding guide](https://github.com/unioslo/osp-cli-rs/blob/main/docs/EMBEDDING.md)
171//!   and [`App::builder`]
172//! - full in-process host → [`app`]
173//! - smaller service-only integration → [`services`]
174//! - rendering / formatting only → [`ui`]
175//!
176//! Start here depending on what you need:
177//!
178//! - [`app`] exists to turn the lower-level pieces into a running CLI or REPL
179//!   process.
180//! - [`cli`] exists to model the public command-line grammar.
181//! - [`config`] exists to answer what values are legal, where they came from,
182//!   and what finally wins.
183//! - [`completion`] exists to rank suggestions without depending on terminal
184//!   state or editor code.
185//! - [`repl`] exists to own the interactive shell boundary.
186//! - [`dsl`] exists to provide the canonical document-first pipeline language.
187//! - [`ui`] exists to lower structured output into terminal-facing text.
188//! - [`plugin`] exists to treat external command providers as part of the same
189//!   command surface.
190//! - [`services`] and [`ports`] exist for smaller embeddable integrations that
191//!   do not want the whole host stack.
192//!
193//! # Feature Flags
194//!
195//! - `clap` (enabled by default): exposes the clap conversion helpers such as
196//!   [`crate::core::command_def::CommandDef::from_clap`],
197//!   [`crate::core::plugin::DescribeCommandV1::from_clap`], and
198//!   [`crate::core::plugin::DescribeV1::from_clap_command`].
199//!
200//! At runtime, data flows roughly like this:
201//!
202//! ```text
203//! argv / REPL line
204//!      │
205//!      ▼ [ cli ]     parse grammar and flags
206//!      ▼ [ config ]  resolve layered settings (builtin → file → env → cli)
207//!      ▼ [ app ]     dispatch to plugin or native command  ──►  Vec<Row>
208//!      ▼ [ dsl ]     apply pipeline stages to rows         ──►  OutputResult
209//!      ▼ [ ui ]      render structured output to terminal or UiSink
210//! ```
211//!
212//! Architecture contracts worth keeping stable:
213//!
214//! - lower-level modules should not depend on [`app`]
215//! - [`completion`] stays pure and should not start doing network, plugin
216//!   discovery, or terminal I/O
217//! - [`ui`] renders structured input but should not become a config-resolver or
218//!   service-execution layer
219//! - [`cli`] describes the grammar of the program but does not execute it
220//! - [`config`] owns precedence and legality rules so callers do not invent
221//!   their own merge semantics
222//!
223//! Public API shape:
224//!
225//! - semantic payload modules such as [`guide`] and most of [`completion`]
226//!   stay intentionally cheap to compose and inspect
227//! - host machinery such as [`app::App`], [`app::AppBuilder`], and runtime
228//!   state is guided through constructors/builders/accessors rather than
229//!   compatibility shims or open-ended assembly
230//! - each public concept should have one canonical home; duplicate aliases and
231//!   mirrored module paths are treated as API debt
232//!
233//! Guided construction naming:
234//!
235//! - `Type::new(...)` is the exact constructor when the caller already knows
236//!   the required inputs
237//! - `Type::builder(...)` starts guided construction for heavier host/runtime
238//!   objects and returns a concrete `TypeBuilder`
239//! - builder setters use `with_*` and the terminal step is always `build()`
240//! - `Type::from_*` and `Type::detect()` are reserved for derived/probing
241//!   factories
242//! - semantic DSLs may keep domain verbs such as `arg`, `flag`, or
243//!   `subcommand`; the `with_*` rule is for guided host configuration, not for
244//!   every fluent API
245//! - avoid abstract "factory builder" layers in the public API; callers should
246//!   see concrete type-named builders and factories directly
247//!
248//! For embedders, choose the smallest surface that solves the problem you
249//! actually have:
250//!
251//! - "I want a full `osp`-style binary or custom `main`" →
252//!   [`app::App::builder`], [`app::AppBuilder::build`], or
253//!   [`app::App::run_from`]
254//! - "I want to capture rendered stdout/stderr in tests or another host" →
255//!   [`app::App::with_sink`] or [`app::AppBuilder::build_with_sink`]
256//! - "I want parser + service execution + DSL, but not the full host" →
257//!   [`services::ServiceContext`] and [`services::execute_line`]
258//! - "I already have rows and only want pipeline transforms" →
259//!   [`dsl::apply_pipeline`] or [`dsl::apply_output_pipeline`]
260//! - "I need plugin discovery and catalog/policy integration" →
261//!   [`plugin::PluginManager`] on the host side, or [`core::plugin`] when
262//!   implementing the wire protocol itself
263//! - "I need manual runtime/session state" → [`app::AppStateBuilder::new`],
264//!   [`app::UiState::new`], [`app::UiState::from_resolved_config`], and direct
265//!   [`app::LaunchContext`] setters
266//! - "I want to embed the interactive editor loop directly" →
267//!   [`repl::ReplRunConfig::builder`] and [`repl::HistoryConfig::builder`]
268//! - "I need semantic payload generation for help/completion surfaces" →
269//!   [`guide::GuideView`] and [`completion::CompletionTreeBuilder`]
270//!
271//! The root crate module tree is the only supported code path. Older mirrored
272//! layouts have been removed so rustdoc and the source tree describe the same
273//! architecture.
274
275/// Main host-facing entrypoints, runtime state, and session types.
276pub mod app;
277/// Command-line argument types and CLI parsing helpers.
278pub mod cli;
279/// Structured command and pipe completion types.
280pub mod completion;
281/// Layered configuration schema, loading, and resolution.
282pub mod config;
283/// Shared command, output, row, and protocol primitives.
284pub mod core;
285/// Canonical pipeline parsing and execution.
286pub mod dsl;
287/// Structured help/guide view models and conversions.
288pub mod guide;
289/// External plugin discovery, protocol, and dispatch support.
290pub mod plugin;
291/// Service-layer ports used by command execution.
292pub mod ports;
293/// Interactive REPL editor, prompt, history, and completion surface.
294pub mod repl;
295/// Library-level service entrypoints built on the core ports.
296pub mod services;
297/// Rendering, theming, and structured output helpers.
298pub mod ui;
299
300pub use crate::app::{App, AppBuilder, AppRunner, run_from, run_process};
301pub use crate::core::command_policy;
302pub use crate::native::{
303    NativeCommand, NativeCommandCatalogEntry, NativeCommandContext, NativeCommandOutcome,
304    NativeCommandRegistry,
305};
306
307mod native;
308mod normalize;
309
310#[cfg(test)]
311mod tests;