Skip to main content

tldr_cli/commands/
api_surface.rs

1//! API Surface command - Extract machine-readable API surface (structural contracts).
2//!
3//! Extracts the complete public API surface of a library or package, including:
4//! - Function and method signatures with typed parameters
5//! - Class definitions with constructor signatures
6//! - Constants and type aliases
7//! - Usage examples (templated from types)
8//! - Trigger keywords for intent-based retrieval
9//!
10//! # Usage
11//! ```bash
12//! # Extract API surface for an installed Python package
13//! tldr surface json --lang python --format json
14//!
15//! # Extract from a directory
16//! tldr surface ./src/mylib/ --format text
17//!
18//! # Look up a specific API
19//! tldr surface json --lookup json.loads --format text
20//! ```
21
22use std::path::PathBuf;
23
24use anyhow::Result;
25use clap::Args;
26
27use tldr_core::surface::{extract_api_surface, format_api_surface_text};
28use tldr_core::Language;
29
30use crate::output::{OutputFormat, OutputWriter};
31
32/// Extract machine-readable API surface (structural contracts) for a library/package
33#[derive(Debug, Args)]
34pub struct ApiSurfaceArgs {
35    /// Package name (e.g., "json", "flask") or directory path
36    pub target: String,
37
38    /// Lookup a specific API by qualified name (e.g., "json.loads")
39    #[arg(long)]
40    pub lookup: Option<String>,
41
42    /// Include private/internal APIs (default: public only)
43    #[arg(long)]
44    pub include_private: bool,
45
46    /// Maximum APIs to extract (default: unlimited)
47    #[arg(long)]
48    pub limit: Option<usize>,
49
50    /// Path to Cargo.toml for Rust crate resolution
51    #[arg(long)]
52    pub manifest_path: Option<PathBuf>,
53}
54
55impl ApiSurfaceArgs {
56    /// Run the API surface extraction command.
57    ///
58    /// The language is provided by the global `--lang` flag from CLI.
59    pub fn run(&self, format: OutputFormat, quiet: bool, lang: Option<Language>) -> Result<()> {
60        let writer = OutputWriter::new(format, quiet);
61
62        writer.progress(&format!(
63            "Extracting API surface for '{}'...",
64            self.target
65        ));
66
67        // Convert Language enum to string for the surface module
68        let lang_str = lang.map(language_to_string);
69
70        // If --manifest-path is provided and lang is Rust, use the parent directory
71        // of the manifest as the target for crate resolution.
72        let effective_target = if let Some(ref manifest) = self.manifest_path {
73            let is_rust = lang_str.as_deref() == Some("rust");
74            if is_rust {
75                manifest
76                    .parent()
77                    .map(|p| p.to_string_lossy().into_owned())
78                    .unwrap_or_else(|| self.target.clone())
79            } else {
80                self.target.clone()
81            }
82        } else {
83            self.target.clone()
84        };
85
86        let surface = extract_api_surface(
87            &effective_target,
88            lang_str.as_deref(),
89            self.include_private,
90            self.limit,
91            self.lookup.as_deref(),
92        )?;
93
94        if writer.is_text() {
95            writer.write_text(&format_api_surface_text(&surface))?;
96        } else {
97            writer.write(&surface)?;
98        }
99
100        Ok(())
101    }
102}
103
104/// Convert a Language enum to the string expected by the contracts module.
105fn language_to_string(lang: Language) -> String {
106    match lang {
107        Language::Python => "python".to_string(),
108        Language::TypeScript => "typescript".to_string(),
109        Language::JavaScript => "javascript".to_string(),
110        Language::Go => "go".to_string(),
111        Language::Rust => "rust".to_string(),
112        Language::Java => "java".to_string(),
113        Language::C => "c".to_string(),
114        Language::Cpp => "cpp".to_string(),
115        Language::Ruby => "ruby".to_string(),
116        Language::Kotlin => "kotlin".to_string(),
117        Language::Swift => "swift".to_string(),
118        Language::CSharp => "csharp".to_string(),
119        Language::Scala => "scala".to_string(),
120        Language::Php => "php".to_string(),
121        Language::Lua => "lua".to_string(),
122        Language::Luau => "luau".to_string(),
123        Language::Elixir => "elixir".to_string(),
124        Language::Ocaml => "ocaml".to_string(),
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use std::path::PathBuf;
132
133    #[test]
134    fn test_api_surface_args_has_manifest_path_field() {
135        let args = ApiSurfaceArgs {
136            target: "my-crate".to_string(),
137            lookup: None,
138            include_private: false,
139            limit: None,
140            manifest_path: Some(PathBuf::from("./Cargo.toml")),
141        };
142        assert_eq!(
143            args.manifest_path,
144            Some(PathBuf::from("./Cargo.toml"))
145        );
146    }
147
148    #[test]
149    fn test_api_surface_args_manifest_path_defaults_to_none() {
150        let args = ApiSurfaceArgs {
151            target: "json".to_string(),
152            lookup: None,
153            include_private: false,
154            limit: None,
155            manifest_path: None,
156        };
157        assert!(args.manifest_path.is_none());
158    }
159}