MouseX-OO_Modulino/lib/MouseX/OO_Modulino.pm
#!/usr/bin/env perl
package MouseX::OO_Modulino;
use MouseX::OO_Modulino::MOP4Import -as_base;
our $VERSION = '0.03';
use Carp ();
use Module::Runtime ();
use File::Basename ();
use open ();
use Encode ();
use Data::Dumper ();
use JSON::MaybeXS ();
use constant USING_CPANEL_JSON_XS => JSON::MaybeXS::JSON()->isa("Cpanel::JSON::XS");
#========================================
has output => (is => 'rw', default => 'jsonl');
has binary => (is => 'rw', default => 0);
has scalar => (is => 'rw', default => 0);
has quiet => (is => 'rw', default => 0);
has undef_as => (is => 'rw', default => 'null');
has no_exit_code => (is => 'rw', default => 0);
#========================================
sub cli_run {
my ($class, $arglist, $opt_alias) = @_;
{
my $modFn = Module::Runtime::module_notional_filename($class);
$INC{$modFn} //= 1;
}
my $self = $class->new($class->cli_parse_opts($arglist, undef, $opt_alias));
unless (@$arglist) {
# Invoke help command if no arguments are given.
$self->cmd_help;
return;
}
my $cmd = shift @$arglist;
if (my $sub = $self->can("cmd_$cmd")) {
# Invoke official command.
$self->cli_precmd($cmd);
$sub->($self, @$arglist);
}
elsif ($self->can($cmd)) {
# Invoke unofficial internal methods. Development aid.
$self->cli_invoke($cmd, @$arglist);
}
else {
# Last resort. You can implement your own subcommand interpretations here.
$self->cli_unknown_subcommand($cmd, $arglist);
}
}
sub cli_unknown_subcommand {
my ($self, $cmd, $arglist) = @_;
$self->cmd_help("Error: No such subcommand '$cmd'\n");
}
sub cli_invoke {
my ($self, $method, @args) = @_;
my $no_exit_code = $self->can('no_exit_code') && $self->no_exit_code;
$self->cli_precmd($method);
my $sub = $self->can($method)
or Carp::croak "No such method: $method";
my $list = $self->cli_invoke_sub($sub, $self, @args);
unless ($no_exit_code) {
$self->cli_exit_for_result($list);
}
}
sub cli_invoke_sub {
my ($self, $sub, $receiver, @args) = @_;
my @res;
if ($self->scalar) {
$res[0] = $sub->($receiver, @args);
$self->cli_output($res[0]) if not $self->quiet and $res[0];
} else {
@res = $sub->($receiver, @args);
$self->cli_output(\@res) if not $self->quiet and @res;
}
\@res;
}
sub cli_output {
my ($self, $item) = @_;
my $format = $self->output // "jsonl";
my $emitter = $self->can("cli_write_fh_as_$format")
or Carp::croak "No such output format: $format";
if ($self->scalar) {
$emitter->($self, \*STDOUT, $item);
} else {
$emitter->($self, \*STDOUT, $_) for @$item;
}
}
*cli_write_fh_as_json = *cli_write_fh_as_jsonl; *cli_write_fh_as_json = *cli_write_fh_as_jsonl;
sub cli_write_fh_as_jsonl {
my ($self, $outFH, $item) = @_;
print $outFH (
ref $item ? $self->cli_encode_json($item) : $item // $self->undef_as
), "\n";
}
sub cli_encode_json {
my ($self, $obj) = @_;
my $json = $self->cli_encode_json_as_bytes($obj);
Encode::_utf8_on($json) unless $self->binary;
$json;
}
sub cli_encode_json_as_bytes {
my ($self, $obj) = @_;
$self->cli_json->encode($obj);
}
sub cli_write_fh_as_dump {
my ($self, $outFH, $item) = @_;
print $outFH $self->cli_encode_dump($item), "\n";
}
sub cli_exit_for_result {
my ($self, $list) = @_;
exit($self->cli_examine_result($list) ? 0 : 1);
}
sub cli_examine_result {
my ($self, $list) = @_;
if ($self->scalar) {
$list->[0];
} else {
@$list;
}
}
sub cmd_help {
my $self = shift;
my $pack = ref $self || $self;
# Invoke precmd (mainly for binmode handling)
$self->cli_precmd();
my @msg = (join("\n", @_, <<END));
Usage: @{[File::Basename::basename($0)]} [--opt=value].. <Command | Method> ARGS...
END
my @cmds = $self->cli_describe_commands;
my @opts = $self->cli_describe_options;
push @msg, "\nCommands:\n", @cmds if @cmds;
push @msg, "\n" if @cmds && @opts;
push @msg, "\nOptions:\n", @opts if @opts;
$! = 0; $? = 0;
die join("", @msg);
}
sub cli_describe_commands {
my ($self) = @_;
my $meta = $self->meta;
map {
my $subName = $_;
(my $name = $subName) =~ s/^cmd_//;
my $rawAtts = $meta->get_method($subName)->attributes;
my $attsDict = $self->cli_parse_attributes(@$rawAtts);
" $name" . (defined $attsDict->{Doc} ? " -- $attsDict->{Doc}" : "");
} sort grep {/^cmd_/} $meta->get_method_list;
}
sub cli_parse_attributes {
my ($self, @atts) = @_;
my $atts = +{};
foreach my $attDesc (@atts) {
my ($name, $value) = $attDesc =~ m{^([^\(]+)([\(].*)?\z}
or Carp::croak "Can't parse attribute $attDesc";
$value =~ s/^\(|\)\z//g if defined $value;
$atts->{$name} = $value // 1;
}
$atts;
}
sub cli_describe_options {
my ($self) = @_;
my $meta = $self->meta;
my $maxLen = 0;
my @spec = map {
my $att = $_;
if ($att->associated_class == $meta) {
my $doc = $att->{documentation};
my $name = $att->name;
$maxLen = length($name) > $maxLen ? length($name): $maxLen;
[$name, $doc];
} else {
()
}
} $meta->get_all_attributes;
map {
my ($name, $doc) = @$_;
sprintf " --%-${maxLen}s %s\n", $name, $doc // ""
} @spec;
}
sub cli_precmd {
my ($self) = @_;
#
# cli_precmd() may be called from $class->cmd_help.
#
unless (ref $self and $self->can("binary") and $self->binary) {
'open'->import(qw/:locale :std/);
}
}
sub cli_parse_opts {
my ($class, $list, $result, $alias) = @_;
my $wantarray = wantarray;
unless (defined $result) {
$result = $wantarray ? [] : {};
}
while (@$list and defined $list->[0] and my ($n, $v) = $list->[0] =~ m{
^--$
| ^(?:--? ([\w:\-\.]+) (?: =(.*))?)$
}xs) {
shift @$list;
last unless defined $n;
$n = $alias->{$n} if $alias and $alias->{$n};
$v = 1 unless defined $v;
if (ref $result eq 'HASH') {
$result->{$n} = $class->cli_decode_argument($v);
} else {
push @$result, $n, $class->cli_decode_argument($v);
}
}
$_ = $class->cli_decode_argument($_) for @$list;
$wantarray && ref $result ne 'HASH' ? @$result : $result;
}
sub cli_decode_argument {
if ($_[1] =~ /^(?:\[.*?\]|\{.*?\})\z/s) {
my $copy = $_[1];
Encode::_utf8_off($copy) if Encode::is_utf8($copy);
$_[0]->cli_json->utf8->relaxed->decode($copy);
}
elsif (not Encode::is_utf8($_[1]) and $_[1] =~ /\P{ASCII}/) {
Encode::decode(utf8 => $_[1]);
}
else {
$_[1];
}
}
sub cli_json {
JSON::MaybeXS::JSON()->new;
}
__PACKAGE__->cli_run(\@ARGV) unless caller;
1;