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