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!("Extracting API surface for '{}'...", self.target));
63
64        // Convert Language enum to string for the surface module
65        let lang_str = lang.map(language_to_string);
66
67        // If --manifest-path is provided and lang is Rust, use the parent directory
68        // of the manifest as the target for crate resolution.
69        let effective_target = if let Some(ref manifest) = self.manifest_path {
70            let is_rust = lang_str.as_deref() == Some("rust");
71            if is_rust {
72                manifest
73                    .parent()
74                    .map(|p| p.to_string_lossy().into_owned())
75                    .unwrap_or_else(|| self.target.clone())
76            } else {
77                self.target.clone()
78            }
79        } else {
80            self.target.clone()
81        };
82
83        let surface = extract_api_surface(
84            &effective_target,
85            lang_str.as_deref(),
86            self.include_private,
87            self.limit,
88            self.lookup.as_deref(),
89        )?;
90
91        if writer.is_text() {
92            writer.write_text(&format_api_surface_text(&surface))?;
93        } else {
94            writer.write(&surface)?;
95        }
96
97        Ok(())
98    }
99}
100
101/// Convert a Language enum to the string expected by the contracts module.
102fn language_to_string(lang: Language) -> String {
103    match lang {
104        Language::Python => "python".to_string(),
105        Language::TypeScript => "typescript".to_string(),
106        Language::JavaScript => "javascript".to_string(),
107        Language::Go => "go".to_string(),
108        Language::Rust => "rust".to_string(),
109        Language::Java => "java".to_string(),
110        Language::C => "c".to_string(),
111        Language::Cpp => "cpp".to_string(),
112        Language::Ruby => "ruby".to_string(),
113        Language::Kotlin => "kotlin".to_string(),
114        Language::Swift => "swift".to_string(),
115        Language::CSharp => "csharp".to_string(),
116        Language::Scala => "scala".to_string(),
117        Language::Php => "php".to_string(),
118        Language::Lua => "lua".to_string(),
119        Language::Luau => "luau".to_string(),
120        Language::Elixir => "elixir".to_string(),
121        Language::Ocaml => "ocaml".to_string(),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::path::PathBuf;
129
130    #[test]
131    fn test_api_surface_args_has_manifest_path_field() {
132        let args = ApiSurfaceArgs {
133            target: "my-crate".to_string(),
134            lookup: None,
135            include_private: false,
136            limit: None,
137            manifest_path: Some(PathBuf::from("./Cargo.toml")),
138        };
139        assert_eq!(args.manifest_path, Some(PathBuf::from("./Cargo.toml")));
140    }
141
142    #[test]
143    fn test_api_surface_args_manifest_path_defaults_to_none() {
144        let args = ApiSurfaceArgs {
145            target: "json".to_string(),
146            lookup: None,
147            include_private: false,
148            limit: None,
149            manifest_path: None,
150        };
151        assert!(args.manifest_path.is_none());
152    }
153}