Skip to main content

Crate osp_cli

Crate osp_cli 

Source
Expand description

osp-cli is the library behind the osp CLI and REPL.

Use it when you want one of these jobs:

  • run the full osp host in-process
  • build a wrapper crate with site-specific native commands and defaults
  • execute the small LDAP service surface without the full host
  • render rows or run DSL pipelines in-process

Most readers only need one of those lanes. You do not need to understand the whole crate before using it.

The crate also keeps the full osp product surface in one place so the main concerns stay visible together: host orchestration, config resolution, rendering, REPL integration, completion, plugins, and the pipeline DSL. That makes rustdoc a useful architecture map after you have picked the smallest surface that fits your job.

Quick starts for the three most common library shapes:

Full osp-style host with captured output:

use osp_cli::App;
use osp_cli::app::BufferedUiSink;

let mut sink = BufferedUiSink::default();
let exit = App::new().run_with_sink(["osp", "--help"], &mut sink)?;

assert_eq!(exit, 0);
assert!(!sink.stdout.is_empty());
assert!(sink.stderr.is_empty());

Lightweight LDAP command execution plus DSL stages:

use osp_cli::config::RuntimeConfig;
use osp_cli::ports::mock::MockLdapClient;
use osp_cli::services::{ServiceContext, execute_line};

let ctx = ServiceContext::new(
    Some("oistes".to_string()),
    MockLdapClient::default(),
    RuntimeConfig::default(),
);
let output = execute_line(&ctx, "ldap user oistes | P uid cn")
    .expect("service command should run");
let rows = output.as_rows().expect("expected row output");

assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("uid").and_then(|value| value.as_str()), Some("oistes"));
assert!(rows[0].contains_key("cn"));

Rendering existing rows without bootstrapping the full host:

use osp_cli::core::output::OutputFormat;
use osp_cli::row;
use osp_cli::ui::{RenderSettings, render_rows};

let rendered = render_rows(
    &[row! { "uid" => "alice", "mail" => "alice@example.com" }],
    &RenderSettings::test_plain(OutputFormat::Json),
);

assert!(rendered.contains("\"uid\": \"alice\""));
assert!(rendered.contains("\"mail\": \"alice@example.com\""));

Building a product-specific wrapper crate:

Minimal wrapper shape:

use std::ffi::OsString;

use anyhow::Result;
use clap::Command;
use osp_cli::app::BufferedUiSink;
use osp_cli::config::ConfigLayer;
use osp_cli::{
    App, AppBuilder, NativeCommand, NativeCommandContext, NativeCommandOutcome,
    NativeCommandRegistry,
};

struct SiteStatusCommand;

impl NativeCommand for SiteStatusCommand {
    fn command(&self) -> Command {
        Command::new("site-status").about("Show site-specific status")
    }

    fn execute(
        &self,
        _args: &[String],
        _context: &NativeCommandContext<'_>,
    ) -> Result<NativeCommandOutcome> {
        Ok(NativeCommandOutcome::Exit(0))
    }
}

fn site_registry() -> NativeCommandRegistry {
    NativeCommandRegistry::new().with_command(SiteStatusCommand)
}

fn site_defaults() -> ConfigLayer {
    let mut defaults = ConfigLayer::default();
    defaults.set("extensions.site.enabled", true);
    defaults
}

#[derive(Clone)]
struct SiteApp {
    inner: App,
}

impl SiteApp {
    fn builder() -> AppBuilder {
        App::builder()
            .with_native_commands(site_registry())
            .with_product_defaults(site_defaults())
    }

    fn new() -> Self {
        Self {
            inner: Self::builder().build(),
        }
    }

    fn run_process<I, T>(&self, args: I) -> i32
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone,
    {
        self.inner.run_process(args)
    }
}

let app = SiteApp::new();
let mut sink = BufferedUiSink::default();
let exit = app.inner.run_process_with_sink(["osp", "--help"], &mut sink);

assert_eq!(exit, 0);
assert!(sink.stdout.contains("site-status"));
assert_eq!(app.run_process(["osp", "--help"]), 0);

If you are new here, start with one of these:

  • wrapper crate / downstream product → [docs/EMBEDDING.md in the repo] and App::builder
  • full in-process host → app
  • smaller service-only integration → services
  • rendering / formatting only → ui

Start here depending on what you need:

  • app exists to turn the lower-level pieces into a running CLI or REPL process.
  • cli exists to model the public command-line grammar.
  • config exists to answer what values are legal, where they came from, and what finally wins.
  • completion exists to rank suggestions without depending on terminal state or editor code.
  • repl exists to own the interactive shell boundary.
  • dsl exists to provide the canonical document-first pipeline language.
  • ui exists to lower structured output into terminal-facing text.
  • plugin exists to treat external command providers as part of the same command surface.
  • services and ports exist for smaller embeddable integrations that do not want the whole host stack.

§Feature Flags

At runtime, data flows roughly like this:

argv / REPL line
     │
     ▼ [ cli ]     parse grammar and flags
     ▼ [ config ]  resolve layered settings (builtin → file → env → cli)
     ▼ [ app ]     dispatch to plugin or native command  ──►  Vec<Row>
     ▼ [ dsl ]     apply pipeline stages to rows         ──►  OutputResult
     ▼ [ ui ]      render structured output to terminal or UiSink

Architecture contracts worth keeping stable:

  • lower-level modules should not depend on app
  • completion stays pure and should not start doing network, plugin discovery, or terminal I/O
  • ui renders structured input but should not become a config-resolver or service-execution layer
  • cli describes the grammar of the program but does not execute it
  • config owns precedence and legality rules so callers do not invent their own merge semantics

Public API shape:

  • semantic payload modules such as guide and most of completion stay intentionally cheap to compose and inspect
  • host machinery such as app::App, app::AppBuilder, and runtime state is guided through constructors/builders/accessors rather than compatibility shims or open-ended assembly
  • each public concept should have one canonical home; duplicate aliases and mirrored module paths are treated as API debt

Guided construction naming:

  • Type::new(...) is the exact constructor when the caller already knows the required inputs
  • Type::builder(...) starts guided construction for heavier host/runtime objects and returns a concrete TypeBuilder
  • builder setters use with_* and the terminal step is always build()
  • Type::from_* and Type::detect() are reserved for derived/probing factories
  • semantic DSLs may keep domain verbs such as arg, flag, or subcommand; the with_* rule is for guided host configuration, not for every fluent API
  • avoid abstract “factory builder” layers in the public API; callers should see concrete type-named builders and factories directly

For embedders, choose the smallest surface that solves the problem you actually have:

The root crate module tree is the only supported code path. Older mirrored layouts have been removed so rustdoc and the source tree describe the same architecture.

Re-exports§

pub use crate::app::App;
pub use crate::app::AppBuilder;
pub use crate::app::AppRunner;
pub use crate::app::run_from;
pub use crate::app::run_process;
pub use crate::core::command_policy;

Modules§

app
Main host-facing entrypoints, runtime state, and session types. The app module exists to turn the library pieces into a running program.
cli
Command-line argument types and CLI parsing helpers. The CLI module exists to define the public command-line grammar of osp.
completion
Structured command and pipe completion types. Completion exists to turn a partially typed line plus cursor position into a ranked suggestion set.
config
Layered configuration schema, loading, and resolution. Configuration exists so the app can answer three questions consistently: what keys are legal, which source wins, and what file edits are allowed.
core
Shared command, output, row, and protocol primitives. Core primitives shared across the rest of the crate.
dsl
Canonical pipeline parsing and execution. Canonical document-first pipeline DSL.
guide
Structured help/guide view models and conversions. Structured help and guide payload model.
plugin
External plugin discovery, protocol, and dispatch support. The plugin module exists so external commands can participate in osp without becoming a separate execution model everywhere else in the app.
ports
Service-layer ports used by command execution. Small service-layer ports and helpers.
repl
Interactive REPL editor, prompt, history, and completion surface. The REPL module exists to own interactive shell behavior that the ordinary CLI host should not know about.
services
Library-level service entrypoints built on the core ports. Small embeddable LDAP service surface with optional DSL pipeline support.
ui
Rendering, theming, and structured output helpers. The UI module turns structured output into predictable terminal text while keeping rendering decisions separate from business logic.

Macros§

row
Builds a Row from literal key/value pairs.

Structs§

NativeCommandCatalogEntry
Public metadata snapshot for one registered native command.
NativeCommandContext
Runtime context passed to native command implementations.
NativeCommandRegistry
Registry of in-process native commands exposed alongside plugin commands.

Enums§

NativeCommandOutcome
Result of executing a native command.

Traits§

NativeCommand
Trait implemented by in-process commands registered alongside plugins.