| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{bail, format_err, Context, Error}; |
| use argh::FromArgs; |
| use log::{error, info, LevelFilter}; |
| use std::collections::{HashMap, HashSet}; |
| use std::fs; |
| use std::path::{Path, PathBuf}; |
| use std::process; |
| |
| use rayon::prelude::*; |
| |
| use serde_json::{json, Value}; |
| |
| mod fidljson; |
| use fidljson::{FidlJson, FidlJsonPackageData, TableOfContentsItem}; |
| |
| mod simple_logger; |
| use simple_logger::SimpleLogger; |
| |
| mod templates; |
| use templates::html::HtmlTemplate; |
| use templates::markdown::MarkdownTemplate; |
| use templates::FidldocTemplate; |
| |
| static FIDLDOC_VERSION: &str = "0.0.4"; |
| static SUPPORTED_FIDLJSON: &str = "0.0.1"; |
| static FIDLDOC_CONFIG_PATH: &str = "fidldoc.config.json"; |
| |
| #[derive(Debug)] |
| enum TemplateType { |
| HTML, |
| Markdown, |
| } |
| |
| fn parse_template_type_str(value: &str) -> Result<TemplateType, String> { |
| match &value.to_lowercase()[..] { |
| "html" => Ok(TemplateType::HTML), |
| "markdown" => Ok(TemplateType::Markdown), |
| _ => Err("invalid template type".to_string()), |
| } |
| } |
| |
| #[derive(Debug, FromArgs)] |
| /// FIDL documentation generator. |
| struct Opt { |
| #[argh(option, short = 'c')] |
| /// path to a configuration file to provide additional options |
| config: Option<PathBuf>, |
| #[argh(option, default = "\"master\".to_string()")] |
| /// current commit hash, useful to coordinate doc generation with a specific source code revision |
| tag: String, |
| #[argh(positional)] |
| /// set the input file(s) to use |
| input: Vec<PathBuf>, |
| #[argh(option, short = 'o', default = "\"/tmp/fidldoc/\".to_string()")] |
| /// set the output folder |
| out: String, |
| #[argh(option, short = 'p', default = "\"/\".to_string()")] |
| /// set the base URL path for the generated docs |
| path: String, |
| #[argh( |
| option, |
| short = 't', |
| from_str_fn(parse_template_type_str), |
| default = "TemplateType::Markdown" |
| )] |
| /// select the template to use to render the docs |
| template: TemplateType, |
| #[argh(switch, short = 'v')] |
| /// generate verbose output |
| verbose: bool, |
| #[argh(switch)] |
| /// do not generate any output |
| silent: bool, |
| } |
| |
| static LOGGER: SimpleLogger = SimpleLogger; |
| |
| fn main() { |
| log::set_logger(&LOGGER).unwrap(); |
| let opt: Opt = argh::from_env(); |
| if let Err(e) = run(opt) { |
| error!("Error: {}", e); |
| process::exit(1); |
| } |
| } |
| |
| fn run(opt: Opt) -> Result<(), Error> { |
| let mut input_files = opt.input; |
| normalize_input_files(&mut input_files); |
| |
| let output = &opt.out; |
| let output_path = PathBuf::from(output); |
| |
| let url_path = &opt.path; |
| |
| let template_type = &opt.template; |
| let template = select_template(template_type, &output_path) |
| .with_context(|| format!("Unable to instantiate template {:?}", template_type))?; |
| |
| if opt.silent && opt.verbose { |
| bail!("cannot use --silent and --verbose together"); |
| } |
| |
| if opt.verbose { |
| log::set_max_level(LevelFilter::Info); |
| } else { |
| log::set_max_level(LevelFilter::Error); |
| } |
| |
| // Read in fidldoc.config.json |
| let fidl_config_file = match opt.config { |
| Some(filepath) => filepath, |
| None => get_fidldoc_config_default_path() |
| .with_context(|| format!("Unable to retrieve default config file location"))?, |
| }; |
| info!("Using config file from {}", fidl_config_file.display()); |
| let fidl_config = read_fidldoc_config(&fidl_config_file) |
| .with_context(|| format!("Error parsing {}", &fidl_config_file.display()))?; |
| |
| create_output_dir(&output_path) |
| .with_context(|| format!("Unable to create output directory {}", output_path.display()))?; |
| |
| // Parse input files to get declarations, package set and fidl json map |
| let FidlJsonPackageData { declarations, fidl_json_map } = |
| process_fidl_json_files(input_files.to_vec())?; |
| |
| // The table of contents lists all packages in alphabetical order. |
| let table_of_contents = create_toc(&fidl_json_map); |
| |
| // Modifications to the fidldoc object |
| let main_fidl_doc = json!({ |
| "table_of_contents": table_of_contents, |
| "fidldoc_version": FIDLDOC_VERSION, |
| "config": fidl_config, |
| "search": declarations, |
| "url_path": url_path, |
| }); |
| |
| // Create main page |
| template.render_main_page(&main_fidl_doc).expect("Unable to render main page"); |
| |
| let tag = &opt.tag; |
| let output_path_string = &output_path.display(); |
| fidl_json_map |
| .par_iter() |
| .try_for_each(|(package, package_fidl_json)| { |
| render_fidl_interface( |
| package, |
| package_fidl_json, |
| &table_of_contents, |
| &fidl_config, |
| &tag, |
| &declarations, |
| &url_path, |
| &template_type, |
| &output_path, |
| ) |
| }) |
| .expect("Unable to write FIDL reference files"); |
| |
| if !opt.silent { |
| println!("Generated documentation at {}", &output_path_string); |
| } |
| Ok(()) |
| } |
| |
| fn render_fidl_interface( |
| package: &String, |
| package_fidl_json: &FidlJson, |
| table_of_contents: &Vec<TableOfContentsItem>, |
| fidl_config: &Value, |
| tag: &String, |
| declarations: &Vec<String>, |
| url_path: &String, |
| template_type: &TemplateType, |
| output_path: &PathBuf, |
| ) -> Result<(), Error> { |
| // Modifications to the fidldoc object |
| let fidl_doc = json!({ |
| "version": package_fidl_json.version, |
| "name": package_fidl_json.name, |
| "maybe_attributes": package_fidl_json.maybe_attributes, |
| "library_dependencies": package_fidl_json.library_dependencies, |
| "bits_declarations": package_fidl_json.bits_declarations, |
| "const_declarations": package_fidl_json.const_declarations, |
| "enum_declarations": package_fidl_json.enum_declarations, |
| "interface_declarations": package_fidl_json.interface_declarations, |
| "table_declarations": package_fidl_json.table_declarations, |
| "struct_declarations": package_fidl_json.struct_declarations, |
| "type_alias_declarations": package_fidl_json.type_alias_declarations, |
| "union_declarations": package_fidl_json.union_declarations, |
| "declaration_order": package_fidl_json.declaration_order, |
| "declarations": package_fidl_json.declarations, |
| "table_of_contents": table_of_contents, |
| "fidldoc_version": FIDLDOC_VERSION, |
| "config": fidl_config, |
| "tag": tag, |
| "search": declarations, |
| "url_path": url_path, |
| }); |
| |
| let template = select_template(&template_type, &output_path) |
| .with_context(|| format!("Unable to instantiate template {:?}", template_type)); |
| match template?.render_interface(&package, &fidl_doc) { |
| Err(why) => error!("Unable to render interface {}: {:?}", &package, why), |
| Ok(()) => info!("Generated interface documentation for {}", &package), |
| } |
| |
| Ok(()) |
| } |
| |
| fn select_template( |
| template_type: &TemplateType, |
| output_path: &PathBuf, |
| ) -> Result<Box<dyn FidldocTemplate>, Error> { |
| // Instantiate the template selected by the user |
| let template: Box<dyn FidldocTemplate> = match template_type { |
| TemplateType::HTML => { |
| let template = HtmlTemplate::new(&output_path)?; |
| Box::new(template) |
| } |
| TemplateType::Markdown => { |
| let template = MarkdownTemplate::new(&output_path); |
| Box::new(template) |
| } |
| }; |
| Ok(template) |
| } |
| |
| fn get_fidldoc_config_default_path() -> Result<PathBuf, Error> { |
| // If the fidldoc config file is not available, it should be found |
| // in the same directory as the executable. |
| // This needs to be calculated at runtime. |
| let fidldoc_executable = std::env::current_exe()?; |
| let fidldoc_execution_directory = fidldoc_executable.parent().unwrap(); |
| let fidl_config_default_path = fidldoc_execution_directory.join(FIDLDOC_CONFIG_PATH); |
| Ok(fidl_config_default_path) |
| } |
| |
| fn read_fidldoc_config(config_path: &Path) -> Result<Value, Error> { |
| let fidl_config_str = fs::read_to_string(config_path) |
| .with_context(|| format!("Couldn't open file {}", config_path.display()))?; |
| Ok(serde_json::from_str(&fidl_config_str)?) |
| } |
| |
| fn process_fidl_json_files(input_files: Vec<PathBuf>) -> Result<FidlJsonPackageData, Error> { |
| // Get table of contents as a HashSet of packages |
| let mut package_set: HashSet<String> = HashSet::new(); |
| // Store all of the FidlJson values as we pass through. |
| // There should be one HashMap entry for each package. |
| // Multiple files will be merged together. |
| let mut fidl_json_map: HashMap<String, FidlJson> = HashMap::new(); |
| // Store every `declaration_order` item to populate search |
| let mut declarations: Vec<String> = Vec::new(); |
| |
| for file in input_files { |
| let fidl_file_path = PathBuf::from(&file); |
| let mut fidl_json = match FidlJson::from_path(&fidl_file_path) { |
| Err(why) => { |
| error!("Error parsing {}: {}", file.display(), why); |
| continue; |
| } |
| Ok(json) => json, |
| }; |
| |
| // Check version |
| if fidl_json.version != SUPPORTED_FIDLJSON { |
| error!( |
| "Error parsing {}: fidldoc does not support version {}, only {}", |
| file.display(), |
| fidl_json.version, |
| SUPPORTED_FIDLJSON |
| ); |
| continue; |
| } |
| |
| let package_name = fidl_json.name.clone(); |
| declarations.append(&mut fidl_json.declaration_order); |
| if !package_set.contains(&package_name) { |
| package_set.insert(package_name.clone()); |
| fidl_json_map.insert(package_name, fidl_json); |
| } else { |
| // Merge |
| let mut package_fidl_json: FidlJson = fidl_json_map |
| .get(&package_name) |
| .cloned() |
| .ok_or(format_err!("Package {} not found in FidlJson map", package_name))?; |
| package_fidl_json.maybe_attributes.append(&mut fidl_json.maybe_attributes); |
| package_fidl_json.bits_declarations.append(&mut fidl_json.bits_declarations); |
| package_fidl_json.const_declarations.append(&mut fidl_json.const_declarations); |
| package_fidl_json.enum_declarations.append(&mut fidl_json.enum_declarations); |
| package_fidl_json.interface_declarations.append(&mut fidl_json.interface_declarations); |
| package_fidl_json.struct_declarations.append(&mut fidl_json.struct_declarations); |
| package_fidl_json.table_declarations.append(&mut fidl_json.table_declarations); |
| package_fidl_json |
| .type_alias_declarations |
| .append(&mut fidl_json.type_alias_declarations); |
| package_fidl_json.union_declarations.append(&mut fidl_json.union_declarations); |
| package_fidl_json.declaration_order.append(&mut fidl_json.declaration_order); |
| fidl_json_map.insert(package_name, package_fidl_json); |
| } |
| } |
| |
| // Sort declarations inside each package |
| fidl_json_map.par_iter_mut().for_each(|(_, package_fidl_json)| { |
| package_fidl_json.sort_declarations(); |
| }); |
| |
| Ok(FidlJsonPackageData { declarations, fidl_json_map }) |
| } |
| |
| fn create_toc(fidl_json_map: &HashMap<String, FidlJson>) -> Vec<TableOfContentsItem> { |
| // The table of contents lists all packages in alphabetical order. |
| let mut table_of_contents: Vec<_> = fidl_json_map |
| .par_iter() |
| .map(|(package_name, fidl_json)| TableOfContentsItem { |
| name: package_name.clone(), |
| link: format!("{name}/index", name = package_name), |
| description: get_library_description(&fidl_json.maybe_attributes), |
| }) |
| .collect(); |
| table_of_contents.sort_unstable_by(|a, b| a.name.cmp(&b.name)); |
| table_of_contents |
| } |
| |
| fn get_library_description(maybe_attributes: &Vec<Value>) -> String { |
| for attribute in maybe_attributes { |
| if attribute["name"] == "Doc" { |
| return attribute["value"] |
| .as_str() |
| .expect("Unable to retrieve string value for library description") |
| .to_string(); |
| } |
| } |
| "".to_string() |
| } |
| |
| fn create_output_dir(path: &PathBuf) -> Result<(), Error> { |
| if path.exists() { |
| info!("Directory {} already exists", path.display()); |
| // Clear out the output folder |
| fs::remove_dir_all(path) |
| .with_context(|| format!("Unable to remove output directory {}", path.display()))?; |
| info!("Removed directory {}", path.display()); |
| } |
| |
| // Re-create output folder |
| fs::create_dir_all(path) |
| .with_context(|| format!("Unable to create output directory {}", path.display()))?; |
| info!("Created directory {}", path.display()); |
| |
| Ok(()) |
| } |
| |
| // Pre-processes the list of input files by removing duplicates. |
| fn normalize_input_files(input: &mut Vec<PathBuf>) { |
| input.sort_unstable(); |
| input.dedup(); |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use std::fs::File; |
| use std::io::Write; |
| use tempfile::{tempdir, NamedTempFile}; |
| |
| use std::path::PathBuf; |
| |
| use serde_json::Map; |
| |
| #[test] |
| fn select_template_test() { |
| let path = PathBuf::new(); |
| |
| let html_template = select_template(&TemplateType::HTML, &path).unwrap(); |
| assert_eq!(html_template.name(), "HTML".to_string()); |
| |
| let markdown_template = select_template(&TemplateType::Markdown, &path).unwrap(); |
| assert_eq!(markdown_template.name(), "Markdown".to_string()); |
| } |
| |
| #[test] |
| fn create_toc_test() { |
| let mut fidl_json_map: HashMap<String, FidlJson> = HashMap::new(); |
| fidl_json_map.insert( |
| "fuchsia.media".to_string(), |
| FidlJson { |
| name: "fuchsia.media".to_string(), |
| version: "0.0.1".to_string(), |
| maybe_attributes: Vec::new(), |
| library_dependencies: Vec::new(), |
| bits_declarations: Vec::new(), |
| const_declarations: Vec::new(), |
| enum_declarations: Vec::new(), |
| interface_declarations: Vec::new(), |
| table_declarations: Vec::new(), |
| type_alias_declarations: Vec::new(), |
| struct_declarations: Vec::new(), |
| union_declarations: Vec::new(), |
| declaration_order: Vec::new(), |
| declarations: Map::new(), |
| }, |
| ); |
| fidl_json_map.insert( |
| "fuchsia.auth".to_string(), |
| FidlJson { |
| name: "fuchsia.auth".to_string(), |
| version: "0.0.1".to_string(), |
| maybe_attributes: vec![json!({"name": "Doc", "value": "Fuchsia Auth API"})], |
| library_dependencies: Vec::new(), |
| bits_declarations: Vec::new(), |
| const_declarations: Vec::new(), |
| enum_declarations: Vec::new(), |
| interface_declarations: Vec::new(), |
| table_declarations: Vec::new(), |
| type_alias_declarations: Vec::new(), |
| struct_declarations: Vec::new(), |
| union_declarations: Vec::new(), |
| declaration_order: Vec::new(), |
| declarations: Map::new(), |
| }, |
| ); |
| fidl_json_map.insert( |
| "fuchsia.camera.common".to_string(), |
| FidlJson { |
| name: "fuchsia.camera.common".to_string(), |
| version: "0.0.1".to_string(), |
| maybe_attributes: vec![json!({"some_key": "key", "some_value": "not_description"})], |
| library_dependencies: Vec::new(), |
| bits_declarations: Vec::new(), |
| const_declarations: Vec::new(), |
| enum_declarations: Vec::new(), |
| interface_declarations: Vec::new(), |
| table_declarations: Vec::new(), |
| type_alias_declarations: Vec::new(), |
| struct_declarations: Vec::new(), |
| union_declarations: Vec::new(), |
| declaration_order: Vec::new(), |
| declarations: Map::new(), |
| }, |
| ); |
| |
| let toc = create_toc(&fidl_json_map); |
| assert_eq!(toc.len(), 3); |
| |
| let item0 = toc.get(0).unwrap(); |
| assert_eq!(item0.name, "fuchsia.auth".to_string()); |
| assert_eq!(item0.link, "fuchsia.auth/index".to_string()); |
| assert_eq!(item0.description, "Fuchsia Auth API".to_string()); |
| |
| let item1 = toc.get(1).unwrap(); |
| assert_eq!(item1.name, "fuchsia.camera.common".to_string()); |
| assert_eq!(item1.link, "fuchsia.camera.common/index".to_string()); |
| assert_eq!(item1.description, "".to_string()); |
| |
| let item2 = toc.get(2).unwrap(); |
| assert_eq!(item2.name, "fuchsia.media".to_string()); |
| assert_eq!(item2.link, "fuchsia.media/index".to_string()); |
| assert_eq!(item2.description, "".to_string()); |
| } |
| |
| #[test] |
| fn get_library_description_test() { |
| let maybe_attributes = vec![ |
| json!({"name": "Not Doc", "value": "Not the description"}), |
| json!({"name": "Doc", "value": "Fuchsia Auth API"}), |
| ]; |
| let description = get_library_description(&maybe_attributes); |
| assert_eq!(description, "Fuchsia Auth API".to_string()); |
| } |
| |
| #[test] |
| fn create_output_dir_test() { |
| // Create a temp dir to run tests on |
| let dir = tempdir().expect("Unable to create temp dir"); |
| let dir_path = PathBuf::from(dir.path()); |
| |
| // Add a temp file inside the temp dir |
| let file_path = dir_path.join("temp.txt"); |
| File::create(file_path).expect("Unable to create temp file"); |
| |
| create_output_dir(&dir_path).expect("create_output_dir failed"); |
| assert!(dir_path.exists()); |
| assert!(dir_path.is_dir()); |
| |
| // The temp file has been deleted |
| assert_eq!(dir_path.read_dir().unwrap().count(), 0); |
| } |
| |
| #[test] |
| fn get_fidldoc_config_default_path_test() { |
| // Ensure that I get a valid filepath |
| let default = std::env::current_exe().unwrap().parent().unwrap().join(FIDLDOC_CONFIG_PATH); |
| assert_eq!(default, get_fidldoc_config_default_path().unwrap()); |
| } |
| |
| #[test] |
| fn read_fidldoc_config_test() { |
| // Generate a test config file |
| let fidl_config_sample = json!({ |
| "title": "Fuchsia FIDLs" |
| }); |
| // Write this to a temporary file |
| let mut fidl_config_file = NamedTempFile::new().unwrap(); |
| fidl_config_file |
| .write(fidl_config_sample.to_string().as_bytes()) |
| .expect("Unable to write to temporary file"); |
| |
| // Read in file |
| let fidl_config = read_fidldoc_config(&fidl_config_file.path()).unwrap(); |
| assert_eq!(fidl_config["title"], "Fuchsia FIDLs".to_string()); |
| } |
| |
| #[test] |
| fn normalize_input_files_test() { |
| let mut input_files = vec![ |
| PathBuf::from(r"/tmp/file1"), |
| PathBuf::from(r"/file2"), |
| PathBuf::from(r"/usr/file1"), |
| ]; |
| normalize_input_files(&mut input_files); |
| assert_eq!(input_files.len(), 3); |
| |
| let mut dup_input_files = vec![ |
| PathBuf::from(r"/tmp/file1"), |
| PathBuf::from(r"/file2"), |
| PathBuf::from(r"/tmp/file1"), |
| ]; |
| normalize_input_files(&mut dup_input_files); |
| assert_eq!(dup_input_files.len(), 2); |
| } |
| } |