Let's pretend the following syntax is valid:
struct Command {
doc: fn() -> impl std::fmt::Display,
}
Library code would internally call the user-supplied Command.doc()
function, and would use its output to build a string and print.
Is there an alternative solution to that syntax that achieves the same goal? That might be too broad, here's some context..
Context
I'm building a library for writing structured text, e.g. CLI Help text. There's a default doc_gen function that builds the help text for every command. Then, each command can build onto it with additional help text.
My idea was to have these doc_gen functions return a set of nestable formatting elements so they can compose, and then the final renderer would take these elements as input and render them into a string. The issue is, I assume my implementation of the formatting elements will be wrong, and want users to be able to come up with better ways of generating their text while my library just expects it to be able to be turned into a string.
Maybe the internal render function could call a user-defined render function, given in some initialization step, so the library wouldn't necessarily need to care about generating the string or printing. But, then I don't know how I'd supply the default rendering function as part of the library..
Does anyone have experience with this sort of problem that could offer some suggestions or alternative patterns?
A core goal of the library is minimum memory usage.
EDIT: A more conceptually complete explanation of the use case below..
The code below is but a conceptual shell to try to explain the core flow of the program with all unnecessary bits removed. If interested, the full code for this project is at https://github.com/slanden/rust-router. A relevant example is in examples/with_docs.rs.
/// A command, as in a CLI command
struct Command {
name: &'static str,
/// A function that modifies the default documentation in some
/// way, such as adding command-specific documentation
doc: fn(c: &Context, doc: DocNodeWithoutSummary) -> DocNode,
}
/// Documentation is built up into a hierarchy where some
/// nodes are meant for text and some are for
/// structure/formatting, like markup
struct DocNode {
text: &'static str,
children: &'static [DocNode],
}
/// When users define their commands, it's ok to not specify
/// a `doc()` method, but when they do, their documentation
/// should include a summary. The logic for enforcing this
/// is not shown here, but they would need to call a method
/// on this struct that adds the summary for their command,
/// then they get `DocNode`s and are free to add whatever
/// else.
pub struct DocNodeWithoutSummary {
blocks: DocNode,
}
/// Like CLI argument parsers, the command an end-user wants
/// is selected. With the selected command, other arguments
/// that were passed with it are stored in a `Context` along with
/// some other information they can use in their docs, such as
/// the name and parameters of the selected command.
struct Context {}
/// Convert the hierarchy to its final string form
impl std::fmt::Display for DocNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// ...
Ok(())
}
}
/// Find which command and arguments the user passed in
fn find_selected_command(
args: std::env::Args,
commands: &Vec<Command>,
) -> usize {
// Just an example
0
}
fn default_docs(
c: &Context,
commands: &Vec<Command>,
) -> DocNodeWithoutSummary {
DocNodeWithoutSummary {
blocks: DocNode {
text: "Here is some default documentation for every command",
children: Vec::new(),
},
}
}
fn render_a_commands_documentation(commands: Vec<Command>) {
let selected_cmd_index =
find_selected_command(std::env::args(), &commands);
let context = Context {};
let doc = default_docs(&context, &commands);
let final_doc = (commands[selected_cmd_index].doc)(&context, doc);
println!("{}", final_doc);
}
fn man() {
// The user defines their program/API
let commands = vec![Command {
name: "example",
doc: |c: &Context, existing_doc_nodes| {
let mut modified_docs = existing_doc_nodes.blocks;
modified_docs.children.push(DocNode {
text: "Some documentation, examples, etc. for the program named 'example'",
children: vec![]
});
modified_docs
},
}];
// This would parse any passed-in arguments, and call
// the real program code defined for the selected
// command, but for this example that logic is removed
// and emphasis is on documentation
render_a_commands_documentation(commands);
}