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