=encoding utf-8

=head1 NAME

MOP4Import::Base::CLI_JSON - OO-Modulino with NDJSON args/outputs

=head1 SYNOPSIS

In F<MyScript.pm>

=for code perl

  #!/usr/bin/env perl
  package MyScript;
  
  use MOP4Import::Base::CLI_JSON -as_base,
      [fields => qw/verbose/, [structs => doc => "usage of this field"]];
  
  unless (caller) {
    MY->cli_run(\@ARGV, {h => "help", v => 'verbose', d => 'debug'});
  }
  
  sub cmd_foo : Doc("This prints FOO") {
    print "FOO\n"
  }
  
  sub bar {
    (my MY $self) = @_;
    ([arguments => @ARGV], [structs => $self->{structs}])
  }
  
  1;

From shell:

=for code sh

  % chmod a+x MyScript.pm
  % ./MyScript.pm -h
  Usage: MyScript.pm [--opt=value].. <Command> ARGS...
  
  Commands
    foo        This prints FOO
    help        
  
  
  Options from MyScript:
    --verbose       
    --structs       usage of this field
  
  Options from MOP4Import::Base::CLI_JSON:
    --help          show this help message
    --quiet         to be (somewhat) quiet
    --scalar        evaluate methods in scalar context
    --output        choose output serializer (ndjson/json/tsv/dump)
    --undef-as      serialize undef as this value. used in tsv output
    --no-exit-code  exit with 0(EXIT_SUCCESS) even when result was falsy/empty
    --binary        keep STDIN/OUT/ERR binary friendly
  %
  % ./MyScript.pm foo
  FOO
  %
  % ./MyScript.pm --structs='[1,2,{"x":"y"}]' bar '["baz",{"qux":"quux"}]' '{"other":"arg"}'
  ["arguments",["baz",{"qux":"quux"}],{"other":"arg"}]
  ["structs",[1,2,{"x":"y"}]]

=head1 DESCRIPTION

MOP4Import::Base::CLI_JSON (or CLI_JSON for short in this document)
is my latest boilerplate base class to write B<Object-Oriented Modulinos>
(or OO Modulino),
a variant of Modulino which I propose in this document.
Note: you can write basic OO Modulino without any help of my modules, as I demonstrate soon below.

=head2 What is OO Modulino?

A Modulino is a Perl file that can act both as an executable or as a module
(See L<these|https://perlmaven.com/modulino-both-script-and-module>
good L<articles|https://www.masteringperl.org/category/chapters/modulinos/>).
With Modulino, you can use some function(s) of the module
from CLI without writing scripts or C<perl -e ...>.
Though which functions you can use depend on how the Modulino is written.
Generally, to use arbitrary public functions from given Modulino,
you may still need to write some scripts.

So, what is OO Modulino? An OO Modulino is a Perl file that can act both as
a class module or as a CLI executable (create an instance and
invoke a specified method). OO Modulino B<standardizes> the CLI behavior
of Modulino about object construction, method invocation, and output handling
from CLI so that you can test-drive almost all public features of the class
by just giving a method name and parameters without worrying about its details.

So, how do you standardize CLI behavior of OO Modulino?
We can mimic CLI behaviors from well-known complex CLI apps.
Just recall C<git> command usage. It has many subcommands like C<add>, C<commit>, C<push>... These can be used as method names.
It also takes global options like C<< --git-dir=<path> >>, C< --no-pager> ...
These can be used as constructor parameters.

For example, suppose you have a module F<Greetings.pm> like followings:

  package Greetings;
  use strict;
  use warnings;
  
  sub new  { my $class = shift; bless +{@_}, $class }
  
  sub hello { my $self = shift; join " ", "Hello", $self->{name} }
  
  sub goodnight { my $self = shift; join(" ", "Good night" => $self->{name}, @_)}
  
  1;

To test above from CLI, you may write:

  % perl -I. -MGreetings -le 'print Greetings->new(name => "world")->hello'


Minimalistic form of OO Modulino of the above could be written
by adding an C<unless (caller) {...}> block like followings:

  #!/usr/bin/env perl
  package Greetings_oo_modulino;
  use strict;
  use warnings;
  
  unless (caller) {
    my $self = __PACKAGE__;
  
    my $cmd = shift
      or die "Usage: $0 COMMAND ARGS...\n";
  
    print $self->new(name => "world")->$cmd(@ARGV), "\n";
  }
  
  sub new  { my $class = shift; bless +{@_}, $class }
  
  sub hello { my $self = shift; join " ", "Hello", $self->{name} }
  
  sub goodnight { my $self = shift; join(" ", "Good night" => $self->{name}, @_)}
  
  1;

You can test OO Modulino like following:

  % ./Greetings_oo_modulino.pm hello

Extending this to pass constructor options is easily achived
by replacing C<unless (caller)> block and rest like followings:

  use fields qw/name/;
  sub MY () {__PACKAGE__}
  
  unless (caller) {
    my $self = MY->new(name => 'world', MY->_parse_posix_opts(\@ARGV));
  
    my $cmd = shift
      or die "Usage: $0 COMMAND ARGS...\n";
  
    print $self->$cmd(@ARGV), "\n";
  }
  
  sub _parse_posix_opts {
    my ($class, $list) = @_;
    my @opts;
    while (@$list and $list->[0] =~ /^--(?:(\w+)(?:=(.*))?)?\z/s) {
      shift @$list;
      last unless defined $1;
      push @opts, $1, $2 // 1;
    }
    @opts;
  }

  sub new  { my MY $self = fields::new(shift); %$self = @_; $self }
  
  sub hello { my MY $self = shift; join " ", "Hello", $self->{name} }
  
  sub goodnight { my MY $self = shift; join " ", "Good night" => $self->{name}, @_}
  
  1;


Now you can test OO Modulino with construction parameters like following:

  % ./Greetings_oo_modulino_with_fields.pm --name=Universe hello
  Hello Universe
  %

With L<fields>, you can reject unknown options at initialization.

  % ./Greetings_oo_modulino_with_fields.pm --foo=bar xx
  Attempt to access disallowed key 'foo' in a restricted hash at ./Greetings_oo_modulino_with_fields.pm line 29.

=head2 About CLI_JSON

Using CLI_JSON as a base class, you can run most methods
directly from the shell. It treats subcommand names and options basically
like following:

=over 4

=item * Subcomands are mapped to methods (basically).

=item * (Posix style long) options before the subcommand are used to create instance(via C<< __PACKAGE__->new(%opts) >>).

=back

You can pass complex structures like arrays and hashes as option
values and arguments to methods in JSON array/object literal syntax.
Results of method invocation are printed with JSON serializer by default.
You can override this behavior by implementing official command
methods C<cmd_$COMMAND>.

Note: design goal of this module is B<*NOT*>
to provide complete feature-rich human-friendly CLI base class.
Instead, it aims to make most methods in developping modules
testable/useable via CLI (and L<perl -d|perldebug>)
from very beginning of its development
so that we can develop perl modules more rapidly via CLI-tested pieces of codes.


=head1 CLASS METHODS

=head2 cli_run (\@ARGV, \%option_shortcuts)

  __PACKAGE__->cli_run(\@ARGV) unless caller;

This method parses arguments, invokes appropriate command
and usually emits its result to STDOUT. Also there is an alias C<run()>
which points to C<cli_run()> so that you do the same thing like below:

  __PACKAGE__->run(\@ARGV) unless caller;

Accepted options are a subset of posix style options (C<--name> and C<--name=value> only). C<--name value> is not allowed,
L<intentionally|MOP4Import::name_space_value_options_considered_harmful>.
If I<value> part of options are recognized as JSON arrays/objects, they are automatically deserialized as perl's arrays/hashes.

If cli_run() gets optional second argument hash, it is used to
accept short name for options like following:

  __PACKAGE__->cli_run(\@ARGV, {h => 'help', v => 'verbose'}) unless caller;

In above, C<-h> is recognized samely as C<--help> and C<-v> as C<--verbose>.

=head3 Command name interpretation rule
X<command-rule>

Then first word in C<@ARGV> is taken and treated as command name.
Command name is resolved to a module method in following ways:

=over 4

=item C<cmd_$COMMAND>

If there is a method named C<cmd_$COMMAND>, it is assumed that
this method is officially designed for CLI invocation.
For official command, C<run()> just invokes it and do nothing else.
Callee method is responsible to handle its output and possibly exit code.

=item C<$COMMAND>

If there is a method exactly matches to specified command,
it is invoked via L<cli_invoke($method, @args)|cli_invoke>.
It invokes the method in array context (unless C<--scalar> option is given).
Results are printed by C<cli_output()> with JSON serializer.

If results are empty list, program exit with code 1
(unless C<--no-exit-code> option is given).

=item ??UNKNOWN??

If the command name does not meet all above,
C<cli_unknown_subcommand> hook is called instead.

=back

=head1 OPTIONS

These options control L<run> method.

=over 4

=item quiet

suppress output of method invocation.

=item scalar

evaluate methods in scalar context

=item output

default 'ndjson'. ndjson stands for Newline Delimited JSON. See L<http://ndjson.org/>.

=item undef-as

default => 'null'

=item no-exit-code

suppress setting exit-code.

=item binary

keep STDIN/OUT/ERR binary friendly. (default 0)

=back

=head1 METHODS

All public APIs are named starting with C<cli_...> prefix.

=head2 cli_output

=head2 cli_invoke

=head2 cli_precmd

=head2 cli_unknown_subcommand

=head1 COMMANDS

=head3 help

This provides default implementation of usage output.

=head1 SEE ALSO

L<App::Cmd> - if your main goal is writing full-fleged CLI.

=head1 LICENSE

Copyright (C) Kobayasi, Hiroaki.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

Kobayasi, Hiroaki E<lt>buribullet@gmail.comE<gt>

=cut
