Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage message customization (dependency injection and separation of concerns) #530

Closed
stechio opened this issue Oct 26, 2018 · 2 comments
Closed

Comments

@stechio
Copy link

stechio commented Oct 26, 2018

Hi Remko,

first of all, kudos for the huge work put in this project, it feels very accurate and full of dedication!

Despite all the goodies, I've bumped against an annoying issue: its API seems a bit tightly coupled, there's less support to dependency injection than one might expect. Conversely, it seems to lack separation between presentation and data.

Let me explain: in my case, I needed to slightly customize the usage message layout (embellishment tweaks (expansions, indentations...) to make it more pleasing to the eye).
I found out that the suggested route to expansions mixes line separation flags (%n) to actual contents (for example, descriptionHeading="%nDescription:%n%n"), whilst indenting specific contents seems impossible without resorting to hackish tricks (public CommandLine.usage(..) methods don't accept Help objects as argument, they all instantiate their own default implementation; unfortunately, the default Help class implements description() through a join that forces table.indentWrappedLines=0 (sic!)).

The possibility to inject a derived Help object through CommandLine.usage(..) would have spared me the annoying hack to extract the default layout implementation (CommandLine.usage(StringBuilder, Help)) just to tweak its building.
Furthermore, IMO, layout formatting within content attributes (such as surrounding headings with newlines to adjust their vertical spacing, as described above) should be discouraged, since presentation (as you certainly know) should be separate from actual data, delegating it as much as possible to renderers: each and every content member (headings, section contents (header, synopsis, description, footer...)...) should be associated to its own renderer, without hard-coding string manipulation as seen in current Help implementation. About section reordering, I'd throw away the hard-coded members currently exposed by the Help class, in favor of a flexible map of members fluently manipulatable without having to hard code StringBuilder.append(..) call trains (as seen in CommandLine.usage(StringBuilder, Help)).

Please, let me know if I missed/misinterpreted anything.
thank you!

@remkop
Copy link
Owner

remkop commented Oct 27, 2018

That's great feedback, thanks! I haven't looked at this area for a while. Let's see what we can do to improve things.

There's a lot there, let me try to break it down:

  1. allow the CommandLine.usage methods to use a user-specified Help instance. Makes sense. I would prefer not to duplicate the existing 12 methods for usage and getUsageMessage to accept an additional Help parameter object. One idea is, instead of instantiating Help objects directly, to have an IHelpFactory for obtaining Help objects. That allows application authors to inject customized Help objects. Thoughts?
  2. About discouraging formatting shortcuts like %n in headings: we will have to agree to disagree on this one. I think a large number of users appreciate being able to customize this aspect of the layout declaratively in the annotations without needing to learn the details of the Help API.
  3. Create separate renderers for all sections of the usage help message (headings, header, footer, synopsis, description, ...). This would allow for a nicer API for reordering sections of the usage help message. Something like this?
interface ISectionRenderer {
    String renderSection(CommandSpec spec, ColorScheme colorScheme);
}

static class Help {
    // subclasses to override?
    public List<ISectionRenderer> getRenderers() {
        return Arrays.asList(
                // default implementation returns current section ordering
                (spec, scheme) -> headerHeading(),
                (spec, scheme) -> header(),
                (spec, scheme) -> synopsisHeading(),
                (spec, scheme) -> synopsis(synopsisHeadingLength()),
                // ...
        );
    }

    public String getUsageHelpMessage() {
        StringBuilder sb = new StringBuilder();
        for (ISectionRenderer renderer : getRenderers()) {
            sb.append(renderer.renderSection(commandSpec(), colorScheme());
        }
        return sb.toString();
    }
...
}

Then the CommandLine.usage and CommandLine.getUsageMessage methods would look something like this:

// CommandLine
public String getUsageMessage(Help.ColorScheme colorScheme) {
    return helpFactory().createHelp(getCommandSpec(), colorScheme).getUsageHelpMessage();
}

Thoughts?

@stechio
Copy link
Author

stechio commented Oct 29, 2018

In the meantime, following your ideas I've just implemented the whole thing -- it's completely respectful of the existing API and idioms, yet flexible to customization, really sweet!

Hard-coded rendering blocks have been refactored into their respective renderers, wiring back the existing entry points, so it's totally transparent to existing users.

All the existing junit tests passed (in the first run, I only had to fix the headingLength used in the detailedSynopsis algorithm), so I'm confident to send you a pull request quite soon.
cheers

stechio added a commit to stechio/picocli that referenced this issue Oct 30, 2018
stechio added a commit to stechio/picocli that referenced this issue Oct 30, 2018
@remkop remkop modified the milestones: 4.1, 4.0 Nov 10, 2018
@remkop remkop modified the milestones: 4.0, 3.9 Dec 17, 2018
remkop added a commit that referenced this issue Dec 17, 2018
remkop added a commit that referenced this issue Dec 18, 2018
remkop added a commit that referenced this issue Dec 18, 2018
@remkop remkop closed this as completed in d8d0c93 Dec 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants