Skip to content

Commit

Permalink
feat: add json_output
Browse files Browse the repository at this point in the history
  • Loading branch information
Janik-Haag committed Jul 31, 2024
1 parent 3712175 commit e49bb1d
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 66 deletions.
72 changes: 39 additions & 33 deletions src/commonmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@
//! This module implements CommonMark output for a struct
//! representing a single entry in the manual.

use std::collections::HashMap;

use std::io::{Result, Write};

use serde::Serialize;

/// Represent a single function argument name and its (optional)
/// doc-string.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct SingleArg {
pub name: String,
pub doc: Option<String>,
}

/// Represent a function argument, which is either a flat identifier
/// or a pattern set.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub enum Argument {
/// Flat function argument (e.g. `n: n * 2`).
Flat(SingleArg),
Expand Down Expand Up @@ -99,16 +99,42 @@ fn handle_indentation(raw: &str) -> String {
}
}

/// Generate the identifier for CommonMark.
/// ident is used as URL Encoded link to the function and has thus stricter rules (i.e. "' " in "lib.map' " is not allowed).
pub(crate) fn get_identifier(prefix: &String, category: &String, name: &String) -> String {
let name_prime = name.replace('\'', "-prime");
vec![prefix, category, &name_prime]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".")
}

/// Generate the title for CommonMark.
/// the title is the human-readable name of the function.
pub(crate) fn get_title(prefix: &String, category: &String, name: &String) -> String {
vec![prefix, category, name]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".")
}

/// Represents a single manual section describing a library function.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct ManualEntry {
/// Prefix for the category (e.g. 'lib' or 'utils').
pub prefix: String,

/// Name of the function category (e.g. 'strings', 'trivial', 'attrsets')
/// Name of the function category (e.g. 'strings', 'trivial', 'attrsets').
pub category: String,

/// Name of the section (used as the title)
/// Location of the function.
pub location: Option<String>,

/// Name of the section (used as the title).
pub name: String,

/// Type signature (if provided). This is not actually a checked
Expand All @@ -122,39 +148,19 @@ pub struct ManualEntry {
/// Usage example for the entry.
pub example: Option<String>,

/// Arguments of the function
/// Arguments of the function.
pub args: Vec<Argument>,
}

impl ManualEntry {
/// Generate the identifier and title for CommonMark.
/// title is the human-readable name of the function.
/// ident is used as URL Encoded link to the function and has thus stricter rules (i.e. "' " in "lib.map' " is not allowed).
pub(crate) fn get_ident_title(&self) -> (String, String) {
let name_prime = self.name.replace('\'', "-prime");

let ident = vec![&self.prefix, &self.category, &name_prime]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".");

let title = vec![&self.prefix, &self.category, &self.name]
.into_iter()
.filter(|x| !x.is_empty())
.cloned()
.collect::<Vec<String>>()
.join(".");

let ident = get_identifier(&self.prefix, &self.category, &self.name);
let title = get_title(&self.prefix, &self.category, &self.name);
(ident, title)
}

/// Write a single CommonMark entry for a documented Nix function.
pub fn write_section<W: Write>(
self,
locs: &HashMap<String, String>,
writer: &mut W,
) -> Result<()> {
pub fn write_section<W: Write>(self, writer: &mut W) -> Result<()> {
let (ident, title) = self.get_ident_title();
writeln!(writer, "## `{}` {{#function-library-{}}}\n", title, ident)?;

Expand Down Expand Up @@ -194,7 +200,7 @@ impl ManualEntry {
writeln!(writer, "```nix\n{}\n```\n:::\n", example.trim())?;
}

if let Some(loc) = locs.get(&ident) {
if let Some(loc) = self.location {
writeln!(writer, "Located at {loc}.\n")?;
}

Expand Down
17 changes: 15 additions & 2 deletions src/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ use rnix::{
SyntaxKind, SyntaxNode,
};
use rowan::ast::AstNode;
use std::collections::HashMap;

use crate::{
commonmark::{Argument, ManualEntry, SingleArg},
format::handle_indentation,
retrieve_doc_comment, DocComment,
get_identifier, retrieve_doc_comment, DocComment,
};

#[derive(Debug)]
Expand All @@ -18,10 +19,22 @@ pub struct LegacyDocItem {
}

impl LegacyDocItem {
pub fn into_entry(self, prefix: &str, category: &str) -> ManualEntry {
pub fn into_entry(
self,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
) -> ManualEntry {
let ident = get_identifier(
&prefix.to_string(),
&category.to_string(),
&self.name.to_string(),
);

ManualEntry {
prefix: prefix.to_string(),
category: category.to_string(),
location: locs.get(&ident).cloned(),
name: self.name,
description: self
.comment
Expand Down
45 changes: 33 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ struct Options {
#[arg(short, long, default_value_t = String::from("lib"))]
prefix: String,

/// Prefix for the category (e.g. 'lib' or 'utils').
#[arg(short, long, default_value_t = false)]
json_output: bool,

/// Name of the function category (e.g. 'strings', 'attrsets').
#[arg(short, long)]
category: String,
Expand Down Expand Up @@ -222,6 +226,7 @@ fn collect_bindings(
node: &SyntaxNode,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
scope: HashMap<String, ManualEntry>,
) -> Vec<ManualEntry> {
for ev in node.preorder() {
Expand All @@ -232,7 +237,7 @@ fn collect_bindings(
if let Some(apv) = AttrpathValue::cast(child.clone()) {
entries.extend(
collect_entry_information(apv)
.map(|di| di.into_entry(prefix, category)),
.map(|di| di.into_entry(prefix, category, locs)),
);
} else if let Some(inh) = Inherit::cast(child) {
// `inherit (x) ...` needs much more handling than we can
Expand All @@ -259,7 +264,12 @@ fn collect_bindings(

// Main entrypoint for collection
// TODO: document
fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec<ManualEntry> {
fn collect_entries(
root: rnix::Root,
prefix: &str,
category: &str,
locs: &HashMap<String, String>,
) -> Vec<ManualEntry> {
// we will look into the top-level let and its body for function docs.
// we only need a single level of scope for this.
// since only the body can export a function we don't need to implement
Expand All @@ -271,15 +281,16 @@ fn collect_entries(root: rnix::Root, prefix: &str, category: &str) -> Vec<Manual
LetIn::cast(n.clone()).unwrap().body().unwrap().syntax(),
prefix,
category,
locs,
n.children()
.filter_map(AttrpathValue::cast)
.filter_map(collect_entry_information)
.map(|di| (di.name.to_string(), di.into_entry(prefix, category)))
.map(|di| (di.name.to_string(), di.into_entry(prefix, category, locs)))
.collect(),
);
}
WalkEvent::Enter(n) if n.kind() == SyntaxKind::NODE_ATTR_SET => {
return collect_bindings(&n, prefix, category, Default::default());
return collect_bindings(&n, prefix, category, locs, Default::default());
}
_ => (),
}
Expand All @@ -303,7 +314,6 @@ fn retrieve_description(nix: &rnix::Root, description: &str, category: &str) ->
}

fn main() {
let mut output = io::stdout();
let opts = Options::parse();
let src = fs::read_to_string(&opts.file).unwrap();
let locs = match opts.locs {
Expand All @@ -316,12 +326,23 @@ fn main() {
let nix = rnix::Root::parse(&src).ok().expect("failed to parse input");
let description = retrieve_description(&nix, &opts.description, &opts.category);

// TODO: move this to commonmark.rs
writeln!(output, "{}", description).expect("Failed to write header");

for entry in collect_entries(nix, &opts.prefix, &opts.category) {
entry
.write_section(&locs, &mut output)
.expect("Failed to write section")
let entries = collect_entries(nix, &opts.prefix, &opts.category, &locs);

if opts.json_output {
let json_string = match serde_json::to_string(&entries) {
Ok(json) => json,
Err(error) => panic!("Problem converting entries to json: {error:?}"),
};
println!("{}", json_string);
} else {
// TODO: move this to commonmark.rs
let mut output = io::stdout();
writeln!(output, "{}", description).expect("Failed to write header");

for entry in entries {
entry
.write_section(&mut output)
.expect("Failed to write section")
}
}
}
41 changes: 22 additions & 19 deletions src/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use rnix;
use std::collections::HashMap;
use std::fs;

use std::io::Write;
Expand All @@ -9,7 +10,8 @@ use crate::{collect_entries, format::shift_headings, retrieve_description, Manua
fn test_main() {
let mut output = Vec::new();
let src = fs::read_to_string("test/strings.nix").unwrap();
let locs = serde_json::from_str(&fs::read_to_string("test/strings.json").unwrap()).unwrap();
let locs: HashMap<String, String> =
serde_json::from_str(&fs::read_to_string("test/strings.json").unwrap()).unwrap();
let nix = rnix::Root::parse(&src).ok().expect("failed to parse input");
let desc = "string manipulation functions";
let prefix = "lib";
Expand All @@ -23,9 +25,9 @@ fn test_main() {
)
.expect("Failed to write header");

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &locs) {
entry
.write_section(&locs, &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -44,9 +46,9 @@ fn test_description_of_lib_debug() {
let desc = retrieve_description(&nix, &"Debug", category);
writeln!(output, "{}", desc).expect("Failed to write header");

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -63,9 +65,9 @@ fn test_arg_formatting() {
let prefix = "lib";
let category = "options";

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -82,9 +84,9 @@ fn test_inherited_exports() {
let prefix = "lib";
let category = "let";

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -101,9 +103,9 @@ fn test_line_comments() {
let prefix = "lib";
let category = "let";

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -120,9 +122,9 @@ fn test_multi_line() {
let prefix = "lib";
let category = "let";

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -139,9 +141,9 @@ fn test_doc_comment() {
let prefix = "lib";
let category = "debug";

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand Down Expand Up @@ -178,9 +180,9 @@ fn test_doc_comment_section_description() {
let desc = retrieve_description(&nix, &"Debug", category);
writeln!(output, "{}", desc).expect("Failed to write header");

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -199,9 +201,9 @@ fn test_doc_comment_no_duplicate_arguments() {
let desc = retrieve_description(&nix, &"Debug", category);
writeln!(output, "{}", desc).expect("Failed to write header");

for entry in collect_entries(nix, prefix, category) {
for entry in collect_entries(nix, prefix, category, &Default::default()) {
entry
.write_section(&Default::default(), &mut output)
.write_section(&mut output)
.expect("Failed to write section")
}

Expand All @@ -215,6 +217,7 @@ fn test_empty_prefix() {
let test_entry = ManualEntry {
args: vec![],
category: "test".to_string(),
location: None,
description: vec![],
example: None,
fn_type: None,
Expand Down

0 comments on commit e49bb1d

Please sign in to comment.