Skip to main content

flodl_cli/
api_ref.rs

1//! API reference generator: extracts the public API surface from flodl source.
2//!
3//! Parses Rust source files to find pub structs, constructors, methods, and
4//! trait implementations. No external dependencies (string-based parsing).
5//!
6//! Used by the `/port` agent skill to understand what flodl offers, and by
7//! anyone who wants a quick reference without building docs.
8
9use std::collections::BTreeMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13
14use crate::util::system::escape_json;
15
16// ---------------------------------------------------------------------------
17// Data model
18// ---------------------------------------------------------------------------
19
20/// A single public function/method signature.
21#[derive(Debug)]
22struct FnSig {
23    name: String,
24    signature: String,
25}
26
27/// A public type extracted from the source.
28#[derive(Debug)]
29struct ApiType {
30    name: String,
31    category: &'static str,
32    file: String,
33    doc_summary: String,
34    doc_examples: Vec<String>,
35    constructors: Vec<FnSig>,
36    methods: Vec<FnSig>,
37    builder_methods: Vec<FnSig>,
38    traits: Vec<String>,
39}
40
41/// Top-level API reference.
42struct ApiRef {
43    version: String,
44    types: Vec<ApiType>,
45}
46
47// ---------------------------------------------------------------------------
48// Source locator
49// ---------------------------------------------------------------------------
50
51/// Find the flodl source directory. Checks (in order):
52/// 1. Explicit path from --path flag
53/// 2. ./flodl/src/ (dev checkout, walk up to 5 levels)
54/// 3. Cargo registry (~/.cargo/registry/src/*/flodl-*/src/)
55/// 4. Cached download (`~/.flodl/api-ref-cache/<tag>/`)
56/// 5. Download from latest GitHub release (cached for next time)
57pub fn find_flodl_src(explicit: Option<&str>) -> Option<PathBuf> {
58    if let Some(p) = explicit {
59        let path = PathBuf::from(p);
60        if path.is_dir() {
61            return Some(path);
62        }
63    }
64
65    // Dev checkout: walk up from cwd looking for flodl/src/lib.rs
66    let mut dir = std::env::current_dir().ok()?;
67    for _ in 0..5 {
68        let candidate = dir.join("flodl/src");
69        if candidate.join("lib.rs").is_file() {
70            return Some(candidate);
71        }
72        if !dir.pop() {
73            break;
74        }
75    }
76
77    // Cargo registry
78    if let Some(home) = home_dir() {
79        let registry = home.join(".cargo/registry/src");
80        if registry.is_dir() {
81            // Find the latest flodl version in registry
82            if let Ok(entries) = fs::read_dir(&registry) {
83                for index_dir in entries.flatten() {
84                    if let Ok(crates) = fs::read_dir(index_dir.path()) {
85                        let mut best: Option<PathBuf> = None;
86                        for entry in crates.flatten() {
87                            let name = entry.file_name().to_string_lossy().to_string();
88                            if name.starts_with("flodl-") && !name.starts_with("flodl-sys") && !name.starts_with("flodl-cli") {
89                                let src = entry.path().join("src");
90                                if src.join("lib.rs").is_file() {
91                                    best = Some(src);
92                                }
93                            }
94                        }
95                        if best.is_some() {
96                            return best;
97                        }
98                    }
99                }
100            }
101        }
102    }
103
104    // Check cached downloads
105    if let Some(tag) = fetch_latest_tag() {
106        if let Some(cache) = cache_dir(&tag) {
107            if let Some(src) = find_src_in_cache(&cache) {
108                return Some(src);
109            }
110        }
111        // Download from GitHub
112        match download_source(&tag) {
113            Ok(src) => return Some(src),
114            Err(e) => eprintln!("warning: could not download source: {}", e),
115        }
116    }
117
118    None
119}
120
121fn home_dir() -> Option<PathBuf> {
122    std::env::var_os("HOME")
123        .or_else(|| std::env::var_os("USERPROFILE"))
124        .map(PathBuf::from)
125}
126
127// ---------------------------------------------------------------------------
128// GitHub source download
129// ---------------------------------------------------------------------------
130
131const REPO: &str = "flodl-labs/flodl";
132
133/// Get the latest release tag from GitHub.
134fn fetch_latest_tag() -> Option<String> {
135    // curl -sI https://github.com/REPO/releases/latest → Location header has the tag
136    let output = Command::new("curl")
137        .args(["-sI", &format!("https://github.com/{}/releases/latest", REPO)])
138        .stdout(Stdio::piped())
139        .stderr(Stdio::null())
140        .output()
141        .ok()?;
142
143    let stdout = String::from_utf8_lossy(&output.stdout);
144    for line in stdout.lines() {
145        let lower = line.to_lowercase();
146        if lower.starts_with("location:") {
147            // https://github.com/flodl-labs/flodl/releases/tag/0.3.0
148            let tag = line.rsplit('/').next()?.trim();
149            if !tag.is_empty() {
150                return Some(tag.to_string());
151            }
152        }
153    }
154    None
155}
156
157/// Cache directory for downloaded source: `~/.flodl/api-ref-cache/<tag>/`
158fn cache_dir(tag: &str) -> Option<PathBuf> {
159    let home = home_dir()?;
160    let flodl_home = std::env::var("FLODL_HOME")
161        .map(PathBuf::from)
162        .unwrap_or_else(|_| home.join(".flodl"));
163    Some(flodl_home.join("api-ref-cache").join(tag))
164}
165
166/// Download and extract flodl source from a GitHub release.
167/// Returns the path to the flodl/src/ directory inside the cache.
168fn download_source(tag: &str) -> Result<PathBuf, String> {
169    let cache = cache_dir(tag)
170        .ok_or_else(|| "cannot determine home directory".to_string())?;
171
172    // Check if already cached
173    let src_dir = find_src_in_cache(&cache);
174    if let Some(src) = src_dir {
175        return Ok(src);
176    }
177
178    eprintln!("Downloading flodl {} source from GitHub...", tag);
179
180    let zip_url = format!(
181        "https://github.com/{}/archive/refs/tags/{}.zip",
182        REPO, tag
183    );
184
185    fs::create_dir_all(&cache)
186        .map_err(|e| format!("cannot create cache dir: {}", e))?;
187
188    let zip_path = cache.join("source.zip");
189    crate::util::http::download_file(&zip_url, &zip_path)?;
190
191    eprintln!("Extracting...");
192    crate::util::archive::extract_zip(&zip_path, &cache)?;
193
194    // Clean up zip
195    let _ = fs::remove_file(&zip_path);
196
197    find_src_in_cache(&cache)
198        .ok_or_else(|| "downloaded archive does not contain flodl/src/lib.rs".to_string())
199}
200
201/// Find flodl/src/lib.rs inside a cache directory.
202/// GitHub archives extract to `<repo-name>-<tag>/` (e.g. `floDl-0.3.0/`).
203fn find_src_in_cache(cache: &Path) -> Option<PathBuf> {
204    if !cache.is_dir() {
205        return None;
206    }
207    // Direct check
208    let direct = cache.join("flodl/src");
209    if direct.join("lib.rs").is_file() {
210        return Some(direct);
211    }
212    // GitHub archive layout: cache/<reponame-tag>/flodl/src/
213    if let Ok(entries) = fs::read_dir(cache) {
214        for entry in entries.flatten() {
215            let path = entry.path();
216            if path.is_dir() {
217                let candidate = path.join("flodl/src");
218                if candidate.join("lib.rs").is_file() {
219                    return Some(candidate);
220                }
221            }
222        }
223    }
224    None
225}
226
227// ---------------------------------------------------------------------------
228// Parser
229// ---------------------------------------------------------------------------
230
231/// Categorize a file path into an API category.
232fn categorize(rel_path: &str) -> &'static str {
233    if rel_path.contains("loss") {
234        "losses"
235    } else if rel_path.contains("optim") {
236        "optimizers"
237    } else if rel_path.contains("scheduler") {
238        "schedulers"
239    } else if rel_path.contains("nn/") || rel_path.starts_with("nn/") {
240        "modules"
241    } else if rel_path.starts_with("tensor") {
242        "tensor"
243    } else if rel_path.starts_with("autograd") {
244        "autograd"
245    } else if rel_path.starts_with("graph") {
246        "graph"
247    } else if rel_path.starts_with("distributed") {
248        "distributed"
249    } else if rel_path.starts_with("data") {
250        "data"
251    } else {
252        "other"
253    }
254}
255
256/// Extract doc comments above a pub item.
257/// Returns (summary_line, code_examples).
258fn extract_docs(lines: &[&str], item_line: usize) -> (String, Vec<String>) {
259    // Walk backwards from the item line to find /// comments
260    let mut doc_lines = Vec::new();
261    let mut i = item_line.saturating_sub(1);
262    loop {
263        let line = lines[i].trim();
264        if line.starts_with("///") {
265            let text = line.trim_start_matches("///");
266            // Keep one leading space if present for indentation
267            let text = text.strip_prefix(' ').unwrap_or(text);
268            doc_lines.push(text.to_string());
269        } else if line.starts_with("#[") || line.is_empty() {
270            if !doc_lines.is_empty() && line.is_empty() {
271                break;
272            }
273        } else {
274            break;
275        }
276        if i == 0 {
277            break;
278        }
279        i -= 1;
280    }
281    doc_lines.reverse();
282
283    let summary = doc_lines.first().cloned().unwrap_or_default();
284
285    // Extract code blocks from doc comments
286    let mut examples = Vec::new();
287    let mut in_code = false;
288    let mut current_block = String::new();
289
290    for line in &doc_lines {
291        if line.starts_with("```") {
292            if in_code {
293                // End of code block
294                if !current_block.trim().is_empty() {
295                    examples.push(current_block.trim().to_string());
296                }
297                current_block.clear();
298                in_code = false;
299            } else {
300                in_code = true;
301            }
302        } else if in_code {
303            if !current_block.is_empty() {
304                current_block.push('\n');
305            }
306            current_block.push_str(line);
307        }
308    }
309
310    (summary, examples)
311}
312
313/// Extract a function signature from a line like `pub fn new(a: i64, b: i64) -> Result<Self> {`
314fn extract_fn_sig(line: &str) -> Option<String> {
315    let trimmed = line.trim();
316    // Find the signature between "pub fn" and the opening brace or "where"
317    let start = if trimmed.contains("pub fn ") {
318        trimmed.find("pub fn ")?
319    } else if trimmed.contains("pub const fn ") {
320        trimmed.find("pub const fn ")?
321    } else {
322        return None;
323    };
324
325    let sig = &trimmed[start..];
326    // Trim trailing { or where
327    let sig = sig.trim_end_matches('{').trim_end_matches("where").trim();
328    Some(sig.to_string())
329}
330
331/// Extract a function name from a signature.
332fn extract_fn_name(sig: &str) -> String {
333    // "pub fn new(...)" -> "new"
334    let after_fn = sig.split("fn ").nth(1).unwrap_or("");
335    let name_end = after_fn.find('(').unwrap_or(after_fn.len());
336    // Handle generic parameters
337    let name_end = name_end.min(after_fn.find('<').unwrap_or(name_end));
338    after_fn[..name_end].to_string()
339}
340
341/// Parse a single Rust source file and extract pub types and their API.
342fn parse_file(src_root: &Path, path: &Path) -> Vec<ApiType> {
343    let content = match fs::read_to_string(path) {
344        Ok(c) => c,
345        Err(_) => return Vec::new(),
346    };
347
348    let rel_path = path
349        .strip_prefix(src_root)
350        .unwrap_or(path)
351        .to_string_lossy()
352        .to_string();
353
354    let category = categorize(&rel_path);
355    let lines: Vec<&str> = content.lines().collect();
356    let mut types: BTreeMap<String, ApiType> = BTreeMap::new();
357
358    // Pass 1: find all pub struct declarations
359    for (i, line) in lines.iter().enumerate() {
360        let trimmed = line.trim();
361        if let Some(after) = trimmed.strip_prefix("pub struct ") {
362            let name_end = after
363                .find(|c: char| !c.is_alphanumeric() && c != '_')
364                .unwrap_or(after.len());
365            let name = after[..name_end].to_string();
366
367            if name.is_empty() || name.starts_with('_') {
368                continue;
369            }
370
371            // Skip test helper types, internal types
372            if name.ends_with("Inner") || name.ends_with("State") && !name.contains("Trained") {
373                continue;
374            }
375
376            let (doc, examples) = extract_docs(&lines, i);
377
378            types.insert(
379                name.clone(),
380                ApiType {
381                    name,
382                    category,
383                    file: rel_path.clone(),
384                    doc_summary: doc,
385                    doc_examples: examples,
386                    constructors: Vec::new(),
387                    methods: Vec::new(),
388                    builder_methods: Vec::new(),
389                    traits: Vec::new(),
390                },
391            );
392        }
393
394        // Also capture pub enum
395        if let Some(after) = trimmed.strip_prefix("pub enum ") {
396            let name_end = after
397                .find(|c: char| !c.is_alphanumeric() && c != '_')
398                .unwrap_or(after.len());
399            let name = after[..name_end].to_string();
400            if !name.is_empty() && !name.starts_with('_') {
401                let (doc, examples) = extract_docs(&lines, i);
402                types.insert(
403                    name.clone(),
404                    ApiType {
405                        name,
406                        category,
407                        file: rel_path.clone(),
408                        doc_summary: doc,
409                        doc_examples: examples,
410                        constructors: Vec::new(),
411                        methods: Vec::new(),
412                        builder_methods: Vec::new(),
413                        traits: Vec::new(),
414                    },
415                );
416            }
417        }
418    }
419
420    // Pass 2: find impl blocks and extract pub methods
421    let mut current_impl: Option<(String, Option<String>)> = None; // (type_name, trait_name)
422    let mut brace_depth: i32 = 0;
423    let mut in_impl = false;
424    let mut in_test = false;
425
426    for line in lines.iter() {
427        let trimmed = line.trim();
428
429        // Skip test modules
430        if trimmed.contains("#[cfg(test)]") {
431            in_test = true;
432        }
433        if in_test {
434            if trimmed == "}" && brace_depth <= 1 {
435                in_test = false;
436            }
437            // Count braces even in test to track depth
438            for c in trimmed.chars() {
439                if c == '{' { brace_depth += 1; }
440                if c == '}' { brace_depth -= 1; }
441            }
442            continue;
443        }
444
445        // Detect impl blocks
446        if trimmed.starts_with("impl ") || trimmed.starts_with("impl<") {
447            let impl_str = trimmed.to_string();
448
449            // Parse: "impl TypeName {" or "impl TraitName for TypeName {"
450            let (type_name, trait_name) = if impl_str.contains(" for ") {
451                // impl Trait for Type
452                let parts: Vec<&str> = impl_str.split(" for ").collect();
453                let trait_part = parts[0]
454                    .trim_start_matches("impl ")
455                    .trim_start_matches("impl<")
456                    .split('>')
457                    .next_back()
458                    .unwrap_or("")
459                    .trim();
460                // Remove generic bounds from trait name
461                let trait_name = trait_part.split('<').next().unwrap_or(trait_part).trim();
462                let type_part = parts.get(1).unwrap_or(&"");
463                let type_name = type_part
464                    .split(|c: char| !c.is_alphanumeric() && c != '_')
465                    .next()
466                    .unwrap_or("")
467                    .trim();
468                (type_name.to_string(), Some(trait_name.to_string()))
469            } else {
470                // impl Type
471                let after_impl = impl_str
472                    .trim_start_matches("impl<")
473                    .split('>')
474                    .next_back()
475                    .unwrap_or(impl_str.strip_prefix("impl ").unwrap_or(&impl_str));
476                let after_impl = after_impl
477                    .strip_prefix("impl ")
478                    .unwrap_or(after_impl.trim());
479                let type_name = after_impl
480                    .split(|c: char| !c.is_alphanumeric() && c != '_')
481                    .next()
482                    .unwrap_or("")
483                    .trim();
484                (type_name.to_string(), None)
485            };
486
487            if types.contains_key(&type_name) {
488                current_impl = Some((type_name, trait_name));
489                in_impl = true;
490            }
491        }
492
493        // Track brace depth
494        for c in trimmed.chars() {
495            if c == '{' {
496                brace_depth += 1;
497            }
498            if c == '}' {
499                brace_depth -= 1;
500                if brace_depth <= 0 && in_impl {
501                    in_impl = false;
502                    current_impl = None;
503                }
504            }
505        }
506
507        // Extract pub fn inside impl blocks
508        if in_impl && (trimmed.starts_with("pub fn ") || trimmed.starts_with("pub const fn ")) {
509            if let Some((ref type_name, ref trait_name)) = current_impl {
510                if let Some(sig) = extract_fn_sig(trimmed) {
511                    let fn_name = extract_fn_name(&sig);
512                    let fn_sig = FnSig {
513                        name: fn_name.clone(),
514                        signature: sig,
515                    };
516
517                    if let Some(api_type) = types.get_mut(type_name) {
518                        // Record trait implementation
519                        if let Some(t) = &trait_name {
520                            if !api_type.traits.contains(t) {
521                                api_type.traits.push(t.clone());
522                            }
523                        }
524
525                        // Categorize the method
526                        if fn_name == "new"
527                            || fn_name == "on_device"
528                            || fn_name == "no_bias"
529                            || fn_name == "no_bias_on_device"
530                            || fn_name == "configure"
531                            || fn_name == "default"
532                        {
533                            api_type.constructors.push(fn_sig);
534                        } else if fn_name.starts_with("with_") || fn_name == "done" || fn_name == "build" {
535                            api_type.builder_methods.push(fn_sig);
536                        } else {
537                            api_type.methods.push(fn_sig);
538                        }
539                    }
540                }
541            }
542        }
543    }
544
545    // Pass 3: collect top-level pub fns (not inside impl blocks).
546    // These are common for losses, init functions, utility functions.
547    let mut free_fns: Vec<FnSig> = Vec::new();
548    let mut depth: i32 = 0;
549    let mut in_test_block = false;
550
551    for (i, line) in lines.iter().enumerate() {
552        let trimmed = line.trim();
553
554        if trimmed.contains("#[cfg(test)]") {
555            in_test_block = true;
556        }
557
558        for c in trimmed.chars() {
559            if c == '{' { depth += 1; }
560            if c == '}' { depth -= 1; }
561        }
562
563        if in_test_block {
564            if depth <= 0 { in_test_block = false; }
565            continue;
566        }
567
568        // Top-level pub fn: depth 0 (module level) or 1 (inside mod block)
569        if depth <= 1 && trimmed.starts_with("pub fn ") {
570            if let Some(sig) = extract_fn_sig(trimmed) {
571                let fn_name = extract_fn_name(&sig);
572                let (doc, _) = extract_docs(&lines, i);
573                free_fns.push(FnSig {
574                    name: format!("{} -- {}", fn_name, doc),
575                    signature: sig,
576                });
577            }
578        }
579    }
580
581    if !free_fns.is_empty() {
582        // Determine a good label from the file name
583        let file_stem = std::path::Path::new(&rel_path)
584            .file_stem()
585            .unwrap_or_default()
586            .to_string_lossy()
587            .to_string();
588
589        let label = match file_stem.as_str() {
590            "mod" => {
591                // Use parent directory name
592                std::path::Path::new(&rel_path)
593                    .parent()
594                    .and_then(|p| p.file_name())
595                    .unwrap_or_default()
596                    .to_string_lossy()
597                    .to_string()
598            }
599            other => other.to_string(),
600        };
601
602        types.insert(
603            format!("{}()", label),
604            ApiType {
605                name: format!("{} (functions)", label),
606                category: categorize(&rel_path),
607                file: rel_path,
608                doc_summary: String::new(),
609                doc_examples: Vec::new(),
610                constructors: Vec::new(),
611                methods: free_fns,
612                builder_methods: Vec::new(),
613                traits: Vec::new(),
614            },
615        );
616    }
617
618    types.into_values().collect()
619}
620
621/// Walk a source tree and parse all .rs files.
622fn parse_source_tree(src_root: &Path) -> Vec<ApiType> {
623    let mut all_types = Vec::new();
624    walk_dir(src_root, src_root, &mut all_types);
625    // Sort by category then name
626    all_types.sort_by(|a, b| a.category.cmp(b.category).then(a.name.cmp(&b.name)));
627    all_types
628}
629
630fn walk_dir(root: &Path, dir: &Path, types: &mut Vec<ApiType>) {
631    let entries = match fs::read_dir(dir) {
632        Ok(e) => e,
633        Err(_) => return,
634    };
635    for entry in entries.flatten() {
636        let path = entry.path();
637        if path.is_dir() {
638            walk_dir(root, &path, types);
639        } else if path.extension().is_some_and(|e| e == "rs") {
640            let mut file_types = parse_file(root, &path);
641            types.append(&mut file_types);
642        }
643    }
644}
645
646// ---------------------------------------------------------------------------
647// Output
648// ---------------------------------------------------------------------------
649
650fn get_version(src_root: &Path) -> String {
651    // Try crate Cargo.toml first, then workspace root
652    let crate_dir = src_root.parent().unwrap_or(src_root);
653    for dir in &[crate_dir, crate_dir.parent().unwrap_or(crate_dir)] {
654        let cargo_toml = dir.join("Cargo.toml");
655        if let Ok(content) = fs::read_to_string(cargo_toml) {
656            // Look for version = "x.y.z" (not version.workspace = true)
657            for line in content.lines() {
658                let trimmed = line.trim();
659                if trimmed.starts_with("version") && trimmed.contains('"') && !trimmed.contains("workspace") {
660                    if let Some(v) = trimmed.split('"').nth(1) {
661                        return v.to_string();
662                    }
663                }
664            }
665        }
666    }
667    "unknown".to_string()
668}
669
670fn print_text(api: &ApiRef) {
671    println!("flodl API Reference v{}", api.version);
672    println!("{}", "=".repeat(40));
673    println!();
674
675    let mut by_category: BTreeMap<&str, Vec<&ApiType>> = BTreeMap::new();
676    for t in &api.types {
677        by_category.entry(t.category).or_default().push(t);
678    }
679
680    for (category, types) in &by_category {
681        println!("## {}", category_title(category));
682        println!();
683
684        for t in types {
685            // Skip types with no public API
686            if t.constructors.is_empty() && t.methods.is_empty() && t.builder_methods.is_empty() {
687                continue;
688            }
689
690            print!("### {}", t.name);
691            if !t.traits.is_empty() {
692                print!("  (implements: {})", t.traits.join(", "));
693            }
694            println!();
695
696            if !t.doc_summary.is_empty() {
697                println!("  {}", t.doc_summary);
698            }
699            println!("  file: {}", t.file);
700
701            if !t.constructors.is_empty() {
702                println!("  constructors:");
703                for f in &t.constructors {
704                    println!("    {}", f.signature);
705                }
706            }
707            if !t.builder_methods.is_empty() {
708                println!("  builder:");
709                for f in &t.builder_methods {
710                    println!("    .{}()", f.name);
711                }
712            }
713            if !t.methods.is_empty() {
714                println!("  methods:");
715                for f in &t.methods {
716                    println!("    {}", f.signature);
717                }
718            }
719            if !t.doc_examples.is_empty() {
720                println!("  examples:");
721                for (ei, ex) in t.doc_examples.iter().enumerate() {
722                    if ei > 0 {
723                        println!();
724                    }
725                    for line in ex.lines() {
726                        println!("    {}", line);
727                    }
728                }
729            }
730            println!();
731        }
732    }
733}
734
735fn print_json(api: &ApiRef) {
736    print!("{{\"version\":\"{}\",\"types\":[", escape_json(&api.version));
737
738    for (i, t) in api.types.iter().enumerate() {
739        if t.constructors.is_empty() && t.methods.is_empty() && t.builder_methods.is_empty() {
740            continue;
741        }
742
743        if i > 0 {
744            print!(",");
745        }
746
747        print!(
748            "{{\"name\":\"{}\",\"category\":\"{}\",\"file\":\"{}\",\"doc\":\"{}\",",
749            escape_json(&t.name),
750            escape_json(t.category),
751            escape_json(&t.file),
752            escape_json(&t.doc_summary),
753        );
754
755        print!("\"traits\":[{}],",
756            t.traits.iter()
757                .map(|s| format!("\"{}\"", escape_json(s)))
758                .collect::<Vec<_>>()
759                .join(",")
760        );
761
762        print!("\"constructors\":[{}],",
763            t.constructors.iter()
764                .map(|f| format!("{{\"name\":\"{}\",\"sig\":\"{}\"}}", escape_json(&f.name), escape_json(&f.signature)))
765                .collect::<Vec<_>>()
766                .join(",")
767        );
768
769        print!("\"builder_methods\":[{}],",
770            t.builder_methods.iter()
771                .map(|f| format!("\"{}\"", escape_json(&f.name)))
772                .collect::<Vec<_>>()
773                .join(",")
774        );
775
776        print!("\"methods\":[{}],",
777            t.methods.iter()
778                .map(|f| format!("{{\"name\":\"{}\",\"sig\":\"{}\"}}", escape_json(&f.name), escape_json(&f.signature)))
779                .collect::<Vec<_>>()
780                .join(",")
781        );
782
783        print!("\"examples\":[{}]",
784            t.doc_examples.iter()
785                .map(|e| format!("\"{}\"", escape_json(e)))
786                .collect::<Vec<_>>()
787                .join(",")
788        );
789
790        print!("}}");
791    }
792
793    println!("]}}");
794}
795
796fn category_title(cat: &str) -> &str {
797    match cat {
798        "modules" => "Modules (nn)",
799        "losses" => "Losses",
800        "optimizers" => "Optimizers",
801        "schedulers" => "Schedulers",
802        "tensor" => "Tensor",
803        "autograd" => "Autograd",
804        "graph" => "Graph",
805        "distributed" => "Distributed",
806        "data" => "Data",
807        other => other,
808    }
809}
810
811// ---------------------------------------------------------------------------
812// Public entry point
813// ---------------------------------------------------------------------------
814
815pub fn run(json: bool, path: Option<&str>) -> Result<(), String> {
816    let src_root = find_flodl_src(path)
817        .ok_or_else(|| {
818            "Could not find flodl source. Run from a flodl checkout, \
819             or pass --path <flodl/src/>."
820                .to_string()
821        })?;
822
823    let version = get_version(&src_root);
824    let types = parse_source_tree(&src_root);
825
826    let api = ApiRef { version, types };
827
828    if json {
829        print_json(&api);
830    } else {
831        print_text(&api);
832    }
833
834    Ok(())
835}