bilal-fazlani / commanddotnet

A modern framework for building modern CLI apps
https://commanddotnet.bilal-fazlani.com
MIT License
575 stars 29 forks source link

templating engine for help generation #231

Open bilal-fazlani opened 4 years ago

bilal-fazlani commented 4 years ago

Right now, help generation is done via code. For small string generation it worked ok. But with time I feel it is becoming difficult to maintain the help generation code.

Templating engines are built specifically for this. You can look at a template such as this:

<ul id='products'>
  {{ for product in products }}
    <li>
      <h2>{{ product.name }}</h2>
           Price: {{ product.price }}
           {{ product.description | string.truncate 15 }}
    </li>
  {{ end }}
</ul>
");

https://github.com/lunet-io/scriban

and know what is going to get rendered. This approach also makes it easy to intuitively change help text. The way tests are written it should not matter how help got rendered, so ideally tests should not get modified

drewburlingame commented 4 years ago

Agreed, it would bring more clarity. The downside is taking on an external dependency for core functionality. I also am not sure how it will hold up with the new terminal features like virtual terminal sequences. Using those would provide a better format for the help for terminals that support it. I'd been holding off on touching help again until we better understood adoption and how we should expect to interact with them.

drewburlingame commented 4 years ago

How will the template engine ensure text is properly aligned in columns? We generally have to loop through all the rows to get the max length of each field and pad with spaces.

bilal-fazlani commented 4 years ago

How will the template engine ensure text is properly aligned in columns?

That will be an easy problem I think. Similar to asp.net mvc view models, we compute everything before hand and give dumb models to views to render. I can not be sure until I try though.

Agreed, it would bring more clarity. The downside is taking on an external dependency for core functionality. I also am not sure how it will hold up with the new terminal features like virtual terminal sequences. Using those would provide a better format for the help for terminals that support it. I'd been holding off on touching help again until we better understood adoption and how we should expect to interact with them.

yeah, I just created this issue for templated related discussions. even if we decide to do this change it can not be done suddenly. we need to consider the concerns you have mentioned. like being able to colour strings, and making sure alignment is proper, dependency etc.

Lets just use this issue for such discussions and spikes/explorations for now.

drewburlingame commented 4 years ago

If we go this route with an external dependency, it should be a separate package like the other external dependencies. This way we can rev it independently.

drewburlingame commented 4 years ago

It would also be great if there was a way for middleware to inject information into help. I haven't thought of a clean approach for that yet. The template would need to save a place for the data. The problem is that the middleware doesn't know what the template looks like and the template doesn't know what info the middleware wants to output.

bilal-fazlani commented 4 years ago

What middlewares dont have to think about injecting info into templates... What if they are just concerned about creating data. Once a middleware decides that it is the final middleware, it passes that data to help rendering middleware with a template name.

Just some thoughts.

drewburlingame commented 4 years ago

Yes, the middleware doesn't need to know about the template specifically. It needs to be able to specify some information to show and what entity that info belongs to. The command, subcommand, argument, etc.

I wonder if there are some generalized concepts about the help documentation to let the middleware give hints. For example, show before or after arguments. Show at top or bottom. This is a symbol to postfix to the end of an argument.

On Wed, Apr 1, 2020 at 9:55 AM Bilal notifications@github.com wrote:

What middlewares dont have to think about injecting info into templates... What if they are just concerned about creating data. Once a middleware decides that it is the final middleware, it passes that data to help rendering middleware with a template name.

Just some thoughts.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/bilal-fazlani/commanddotnet/issues/231#issuecomment-607123836, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZHMP4LOKIY54QYC55JATRKL6QXANCNFSM4LX4EOPQ .

bilal-fazlani commented 4 years ago

For example, show before or after arguments. Show at top or bottom.

With templates, I can think of following points

  1. Create use different templates instead of using a lot or ifs inside a single template
  2. We don't have to provide settings such as "show at top or bottom" to the devs. We can provide predefined set of templates. And allow devs use their own template if they want to.
  3. The data we provide to all templates can be same. (lots of data) and its up to templates to consume all or minimum data.

This is a symbol to postfix to the end of an argument.

I am not sure if I understand this correctly.


Also, just a thought - may it is worth exploring if we can create html template and then render it on console. Like how browsh or lynx does it? Using some library of course (if there is any). Not sure if that is possible, but if we find any library, then that can take care of colors, as well as table like padded alignments. But this is just too ambitious :P

drewburlingame commented 4 years ago

For example, show before or after arguments. Show at top or bottom.

My point with this is that we have a bit of a race condition. A help template doesn't know what middleware is registered. A middleware doesn't know how to modify or append to the help output. Let's say we're adding a middleware component to support separated arguments. How does that middleware indicate the command supports separated arguments? It should update the Usage section, but it doesn't know the format of the section so it can't just modify it. And the help template doesn't know about this middleware. How do we solve this?

This is a symbol to postfix to the end of an argument.

I was thinking about middleware that may want to add some kind of indicator to an argument, like our "" or "(Multiple)". Perhaps, "!" for required or "*" for footnotes in extended help text.

bilal-fazlani commented 4 years ago

Let's say we're adding a middleware component to support separated arguments. How does that middleware indicate the command supports separated arguments?

What about using A ViewModel? So our context class has one more property called view model. This view model has all properties required to render help. All middlewares which want to modify help, keep enriching this view model and keep passing it to next middleware until it finally reaches the help middleware where it gets passed to a view (template) and gets rendered.

In this case, our new middleware component adds relevant info into the view model.

I was thinking about middleware that may want to add some kind of indicator to an argument, like our "" or "(Multiple)". Perhaps, "!" for required or "*" for footnotes in extended help text.

So middleware don't decide how things get rendered. They just enrich information and its up to the template to decide how to render things.

For example, ViewModel contains a command and command contains 2 arguments. 1 non required other required. You can decide to a template which renders required args like this: agr1 ! or you can use a a template which renders required args like this: arg1: REQUIRED

The middleware just collect core domain cli data such as what is required, what is the type of argument, what is the description, etc. It doesn't decide how to display that data. Template decides that.

With this thought, a middleware can enrich the view model with footnote but it's the template that decide where to render that footnote and how.

I hope that makes sense..

bilal-fazlani commented 4 years ago

I am sorry if that was incorrect, I have less knowledge about the framework than you now

drewburlingame commented 4 years ago

What about using A ViewModel? So our context class has one more property called view model. This view model has all properties required to render help. All middlewares which want to modify help, keep enriching this view model and keep passing it to next middleware until it finally reaches the help middleware where it gets passed to a view (template) and gets rendered.

Yes, I was thinking something similar. It's just a matter of representing that model in a way that's as simple as possible while still being expressive enough.

You can decide to a template which renders required args like this: agr1 ! or you can use a a template which renders required args like this: arg1: REQUIRED

The gotcha here is that the template now has to understand "Required". Required is a contrived enough example that it's easy to assume a template should know about it. When we generalize the concept, required is just another feature. How does the template know what to do with "Random Feature H", maybe we'll call it Hamburger. The argument is totally Hamburger.

How would the Hamburger middleware represent itself in the template? Likely, the answer is to update the text of the ViewModel.

bilal-fazlani commented 4 years ago

The gotcha here is that the template now has to understand "Required".

Yes. That is because this template is specifically designed for model "XYZ" and XYX has a property "Required"

How does the template know what to do with "Random Feature H"

That is the one and only job of template. As template developers, we decide what to do with model data and hardcode its presentation logic into the template

How would the Hamburger middleware represent itself in the template? Likely, the answer is to update the text of the ViewModel.

Yes. There are some best practices of doing this. For example it's ok to put bool IsHamburger and int HamburgerPrice properties in the model and assign them values. It's not idiomatic to put int DescriptionLeftOffset properties in model and assign them values in the middleware.

drewburlingame commented 4 years ago

That is the one and only job of template. As template developers, we decide what to do with model data and hardcode its presentation logic into the template

As a template developer, how do you model for data that you don't know about? What about data from custom middleware from an external package or defined in the application?

Yes. There are some best practices of doing this. For example it's ok to put bool IsHamburger and int HamburgerPrice properties in the model and assign them values. It's not idiomatic to put int DescriptionLeftOffset properties in model and assign them values in the middleware.

I agree with this. I'm probably overthinking this. I guess the best we can do is offer a default template and make it easy for users to update when they're adding other middleware that adds new properties to the view model.

bilal-fazlani commented 4 years ago

As a template developer, how do you model for data that you don't know about? What about data from custom middleware from an external package or defined in the application?

This is a good question. If a person is introducing a middleware which wants to introduce new data to help, they would have to add their own template. But they also need a place where that data can be stored. The ViewModel can have a placeholder Dictionary<string, object> customData for such scenarios. I agree that this is not the cleanest solution, but considering the number of people who might want do this, I think that solution should be fine. A more sophisticated solution might be based on generics but I am not sure if the outcome will justify the amount of engineering involved :P

drewburlingame commented 4 years ago

To summarize:

  1. pick a templating library
  2. create CommandDotNet.{library-name} eg. CommandDotNet.Scriban , CommandDotNet.DotLiquid, CommandDotNet.RazerLite ...
  3. create a IHelpTextProvider
  4. add appRunner.Using___() ext method that takes an optional template string or filepath for users to provide customizations.

nice-to-have: users can template the help strings from attributes, like [Command(Description="lala {{ app.name }} lala")]

If, along the way we develop a good ViewModel, we can consider bringing that into CommandDotNet to be available for middleware to update and append to.

drewburlingame commented 4 years ago

Another nice-to-have would be the ability to output all commands to markdown or man page files to be used as documentation for an application. Probably v2 of the package.