summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2015-01-02 14:00:56 +0100
committerPaul Buetow <paul@buetow.org>2015-01-02 14:00:56 +0100
commit4f27f3ea59baa6cfca0ac1df96b1dfedbd83706c (patch)
tree79754e3c49216f80cb3ad5579851fca0bf1a31c9 /lib
initial
Diffstat (limited to 'lib')
-rw-r--r--lib/MON/Cache.pm55
-rw-r--r--lib/MON/Config.pm176
-rw-r--r--lib/MON/Display.pm360
-rw-r--r--lib/MON/Filter.pm166
-rw-r--r--lib/MON/JSON.pm51
-rw-r--r--lib/MON/Options.pm163
-rw-r--r--lib/MON/Query.pm557
-rw-r--r--lib/MON/QueryBase.pm232
-rw-r--r--lib/MON/RESTlos.pm471
-rw-r--r--lib/MON/Syslogger.pm77
-rw-r--r--lib/MON/Utils.pm80
11 files changed, 2388 insertions, 0 deletions
diff --git a/lib/MON/Cache.pm b/lib/MON/Cache.pm
new file mode 100644
index 0000000..21b59f5
--- /dev/null
+++ b/lib/MON/Cache.pm
@@ -0,0 +1,55 @@
+package MON::Cache;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->clear();
+
+ return undef;
+}
+
+sub clear {
+ my ($self) = @_;
+
+ $self->{cache} = {};
+
+ return undef;
+}
+
+sub magic {
+ my ( $self, $key, $sub ) = @_;
+
+ my $cache = $self->{cache};
+
+ if ( exists $cache->{$key} ) {
+ $self->verbose("Delivering '$key' from cache");
+ return $cache->{$key};
+ }
+
+ return $cache->{$key} = $sub->();
+}
+
+1;
diff --git a/lib/MON/Config.pm b/lib/MON/Config.pm
new file mode 100644
index 0000000..dc83911
--- /dev/null
+++ b/lib/MON/Config.pm
@@ -0,0 +1,176 @@
+package MON::Config;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use IO::File;
+use Data::Dumper;
+
+use MON::Display;
+use MON::Utils;
+
+#use MON::Options;
+
+use MIME::Base64 qw( decode_base64 );
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+ my $options = $self->{options};
+
+ $options->store_first($self);
+
+ $self->SUPER::init(%opts);
+
+ for ( @{ $options->{unknown} } ) {
+ $self->error("Unknown option: $_");
+ }
+
+ if ( $self->{'config'} ne '' ) {
+ $self->read_config( $self->{'config'} );
+
+ }
+ elsif ( exists $ENV{MON_CONFIG} ) {
+ $self->read_config( $ENV{MON_CONFIG} );
+
+ }
+ else {
+ $self->read_config('/etc/mon.conf');
+ $self->read_config($_) for sort glob("/etc/mon.d/*.conf");
+
+ $self->read_config("$ENV{HOME}/.mon.conf");
+ $self->read_config($_) for sort glob("$ENV{HOME}/.mon.d/*.conf");
+ }
+
+ $options->store_after($self);
+
+ unless ( exists $self->{config_was_read} ) {
+ $self->verbose("No config file found, but this might be OK");
+ }
+
+ $self->_set_defaults();
+
+ return $self;
+}
+
+sub _set_defaults {
+ my ($self) = @_;
+
+ my $set_default = sub {
+ my ( $key, $val ) = @_;
+
+ unless ( exists $self->{$key} ) {
+ $self->{$key} = $val;
+ $self->verbose(
+ "Since $key is not specified setting its default value to $val");
+ }
+ };
+
+ $set_default->( 'backups.dir' => "$ENV{HOME}/.mon" );
+ $set_default->( 'backups.disable' => 1 );
+ $set_default->( 'backups.keep.days' => 7 );
+ $set_default->( 'restlos.api.port' => '443' );
+ $set_default->( 'restlos.api.protocol' => 'https' );
+ $set_default->( 'restlos.auth.realm' => 'Login Required' );
+ $set_default->( 'restlos.auth.username' => $ENV{USER} );
+}
+
+sub read_config {
+ my ( $self, $config_file ) = @_;
+
+ return undef if not defined $config_file or not -f $config_file;
+
+ my $fh = IO::File->new( $config_file, 'r' );
+ $self->error("Could not open file $config_file") unless defined $fh;
+
+ $self->verbose("Reading config $config_file");
+
+ while ( my $line = $fh->getline() ) {
+ next if $line =~ /^#/;
+
+ # Ignore comments
+ $line =~ s/(.*);.*/$1/;
+
+ # Parse only matching lines
+ if ( $line =~ /^(.*):(.*)/ ) {
+ my ( $key, $val ) = ( lc trim $1, trim $2);
+ $self->verbose("Reading conf value $key");
+
+ # Handle ~
+ $val =~ s/~/$ENV{HOME}/g;
+ $self->set( $key, $val );
+ }
+ }
+
+ $fh->close();
+ $self->{config_was_read} = 1;
+
+ return undef;
+}
+
+sub get {
+ my ( $self, $key ) = @_;
+ $key = lc $key;
+
+ $self->{$key} //= do {
+ my $key = uc $key;
+ $key =~ s/\./_/g;
+
+ exists $ENV{$key} ? $ENV{$key} : undef;
+ };
+
+ if ( not exists $self->{$key}
+ or not defined $self->{$key}
+ or $self->{$key} eq '' )
+ {
+ $self->error("$key not configured");
+ }
+
+ return $self->{$key};
+}
+
+sub get_maybe_encoded {
+ my ( $self, $key ) = @_;
+
+ return $self->get($key) if exists $self->{$key};
+
+ $self->error("$key or $key.enc not configured")
+ unless exists $self->{"$key.enc"};
+
+ my $enc = $self->get("$key.enc");
+
+ return decode_base64($enc);
+}
+
+sub bool {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return $val != 0;
+}
+
+sub array {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return map { trim $_ } split ',', $val;
+}
+
+sub set {
+ my ( $self, $key, $val ) = @_;
+ $key = lc $key;
+
+ $self->verbose("$key already configured, overwriting it with its new value")
+ if exists $self->{$key};
+
+ return $self->{$key} = $val;
+}
+
+1;
diff --git a/lib/MON/Display.pm b/lib/MON/Display.pm
new file mode 100644
index 0000000..9bf8115
--- /dev/null
+++ b/lib/MON/Display.pm
@@ -0,0 +1,360 @@
+package MON::Display;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Term::ANSIColor;
+
+use MON::Config;
+use MON::JSON;
+use MON::Utils;
+
+our $VERBOSE = 0;
+our $DEBUG = 0;
+our $COLORFUL = 0;
+our $QUIET = 0;
+our $LOGGER = undef;
+our $INTERACTIVE = undef;
+
+sub init {
+ my ( $self, %opts ) = @_;
+
+ $VERBOSE = $self->{'verbose'} == 1;
+ $DEBUG = $self->{'debug'} == 1;
+ $QUIET = $self->{'quiet'} == 1;
+ $LOGGER = $opts{logger};
+ $INTERACTIVE = $opts{interactive};
+
+ $self->{logglevel} = 'info';
+
+ if ( $self->{'nocolor'} == 1 ) {
+ $COLORFUL = 0;
+ }
+ else {
+ $COLORFUL = $ENV{MON_COLORFUL} // 1;
+ }
+
+ $VERBOSE = $DEBUG = $COLORFUL = 0 if $QUIET == 1;
+
+ return undef;
+}
+
+sub is_verbose {
+ my ($self) = @_;
+
+ return $VERBOSE == 1;
+}
+
+sub is_debug {
+ my ($self) = @_;
+
+ return $DEBUG == 1;
+}
+
+sub is_quiet {
+ my ($self) = @_;
+
+ return $QUIET == 1;
+}
+
+sub _display {
+ my ( $self, $msg, $fh, $level ) = @_;
+
+ return undef unless defined $msg;
+
+ $LOGGER->logg( $self->{logglevel}, $msg ) if defined $LOGGER;
+
+ return undef if $QUIET;
+
+ $fh = *STDERR unless defined $fh;
+
+ print $fh $msg;
+
+ return undef;
+}
+
+sub info_no_nl {
+ my ( $self, $msg ) = @_;
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($msg);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_json {
+ my ( $self, $out ) = @_;
+
+ return undef unless defined $out;
+ my $config = $self->{config};
+
+ local $, = "\n";
+
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ # Don't _display meta aka custom variables unless -m or --meta is specified
+ unless ( $config->{'meta'} ) {
+ if ( ref $json eq 'ARRAY' ) {
+ @$json = map {
+ if ( ref $_ eq 'HASH' )
+ {
+ my $h = $_;
+ delete $h->{$_} for grep /^_/, keys %$h;
+ $h;
+ }
+ else {
+ $_;
+ }
+ } @$json;
+ }
+ }
+
+ # Sort and pretty print all the JSON pretty pretty please
+ unless ( defined $config->{outfile} ) {
+ print MON::JSON->new()->encode_canonical($json) unless $QUIET;
+ }
+ else {
+ my $outfile = $config->{outfile};
+ print $outfile MON::JSON->new()->encode_canonical($json);
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Wrote JSON to file\n");
+ print STDERR color 'reset' if $COLORFUL;
+ }
+
+ $LOGGER->logg( 'info', JSON->new()->encode($json) ) if defined $LOGGER;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub out_format {
+ my ( $self, $format, $out ) = @_;
+
+ return undef unless defined $out;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+ my $json = MON::JSON->new()->decode($out);
+ my $num_results = ref $json eq 'ARRAY' ? @$json : undef;
+
+ $self->error("Expected an JSON Array") if ref $json ne 'ARRAY';
+
+ my @vars1 = $format =~ /\$(\w+)/g;
+ my @vars2 = $format =~ /\$\{(\w+)\}/g;
+ my @vars3 = $format =~ /\@(\w+)/g;
+ my @vars4 = $format =~ /\@\{(\w+)\}/g;
+
+ my %vars;
+ $vars{$_} = '' for @vars1, @vars2, @vars3, @vars4;
+ my @out;
+ my %empty;
+
+ for my $obj (@$json) {
+ my %obj_vars = %vars;
+ my $obj_format = $format;
+
+ for my $var ( keys %obj_vars ) {
+ if ( $var eq 'HOSTNAME' ) {
+ my $val = exists $obj->{host_name} ? $obj->{host_name} : '';
+
+ if ( $val eq '' ) {
+ $empty{$var} = 1;
+ }
+ else {
+ $val =~ s/\..*//;
+ }
+
+ $obj_format =~ s/\$$var/$val/g;
+
+ }
+ else {
+ my $val = exists $obj->{$var} ? $obj->{$var} : '';
+ $empty{$var} = 1 if $val eq '';
+
+ $obj_format =~ s/\$$var/$val/g;
+ $obj_format =~ s/\$\{$var\}/$val/g;
+ $obj_format =~ s/\@$var/$val/g;
+ $obj_format =~ s/\@\{$var\}/$val/g;
+ }
+ }
+
+ push @out, $obj_format if $obj_format =~ /^.*\w+.*$/;
+ }
+
+ if (@out) {
+
+ if ( $config->{'unique'} ) {
+ my %lines;
+ @out = grep { exists $lines{$_} ? 0 : ( $lines{$_} = 1 ) } sort @out;
+ $num_results = @out;
+ }
+ else {
+ @out = sort @out;
+ }
+
+ if ( $QUIET == 0 ) {
+ local $, = "\n";
+ print @out;
+ say '';
+ }
+ elsif ( defined $LOGGER ) {
+ $LOGGER->logg( 'info', $_ ) for @out;
+ }
+ }
+
+ $self->warning( "Some objects dont have such a field or have empty strings: "
+ . join( ' ', sort keys %empty ) )
+ if keys %empty;
+
+ print STDERR color 'bold green' if $COLORFUL;
+ $self->_display("Found $num_results entries\n") if defined $num_results;
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub info {
+ my ( $self, $msg ) = @_;
+
+ my $str = "$msg\n";
+ $self->{logglevel} = 'info';
+
+ print STDERR color 'bold blue' if $COLORFUL;
+ $self->_display($str);
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub nl {
+ my ($self) = @_;
+
+ $self->_display("\n");
+
+ return undef;
+}
+
+sub error {
+ my ( $self, $msg ) = @_;
+
+ $self->error_no_exit($msg);
+
+ exit 3 unless $INTERACTIVE;
+}
+
+sub error_no_exit {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ print STDERR color 'bold red' if $COLORFUL;
+ $self->_display( "! ERROR: $msg\n", *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub possible {
+ my ( $self, @params ) = @_;
+
+ my $config = $self->{config};
+ my $options = $self->{options};
+
+ push @params, $options->get_keys()
+ if $config->{'help'};
+
+ my $msg = '';
+
+ if (@params) {
+ for ( grep !/^V_ALIAS/, @params ) {
+ if ( ref $_ eq 'ARRAY' ) {
+ $msg .= join "\n", @$_;
+ $msg .= "\n";
+ }
+ else {
+ $msg .= "$_\n";
+ }
+ }
+ }
+ else {
+ $msg .= "\n";
+ }
+
+ $self->{logglevel} = 'info';
+ $self->_display($msg);
+
+ exit 0 unless $INTERACTIVE;
+}
+
+sub warning {
+ my ( $self, $msg ) = @_;
+
+ my $str = "! $msg\n";
+
+ print STDERR color 'red' if $COLORFUL;
+ $self->_display( $str, *STDERR );
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub verbose {
+ my ( $self, @msgs ) = @_;
+
+ print STDERR color 'cyan' if $COLORFUL;
+ $self->{logglevel} = 'info';
+
+ if ( $self->is_verbose() ) {
+ for my $msg (@msgs) {
+ if ( $self->is_debug() ) {
+ my @caller = caller;
+ $self->_display("@caller: $msg\n");
+ }
+ else {
+ $self->_display("$msg\n");
+ }
+ }
+ }
+
+ print STDERR color 'reset' if $COLORFUL;
+
+ return undef;
+}
+
+sub dump {
+ my ( $self, $msg ) = @_;
+
+ $self->{logglevel} = 'warning';
+ $self->_display( Dumper $msg );
+
+ return undef;
+}
+
+sub debug {
+ my ( $self, @msgs ) = @_;
+
+ my @caller = caller;
+
+ if ( $self->is_debug() ) {
+ for my $msg (@msgs) {
+ $msg = Dumper $msg if ref $msg ne '';
+
+ my $str = "@caller: $msg\n";
+
+ $self->{logglevel} = 'debug';
+ $self->_display($str);
+ }
+ }
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/MON/Filter.pm b/lib/MON/Filter.pm
new file mode 100644
index 0000000..d16d1c5
--- /dev/null
+++ b/lib/MON/Filter.pm
@@ -0,0 +1,166 @@
+package MON::Filter;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{query_string} = '';
+ $self->{filters} = {};
+ $self->{num_filters} = 0;
+ $self->{is_computed} = 0;
+ $self->{or} = [];
+
+ return undef;
+}
+
+# Create filters with params
+sub compute {
+ my ( $self, $params ) = @_;
+
+ $self->debug( 'Computing filter using', $params );
+ return undef if $self->{is_computed};
+
+ my %likes;
+
+ if ( defined $params and ref $params eq 'ARRAY' ) {
+ while (@$params) {
+ my $op_token = pop @$params;
+ given ($op_token) {
+ when (/^OP_LIKE$/) {
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+
+ if ( exists $likes{$arg1} ) {
+ $self->error(
+"Can not run multiple 'like's on '$arg1', since it is used for the API query_string"
+ );
+ }
+ else {
+ $likes{$arg1} = "$arg1=$arg2";
+ }
+
+ }
+ when (/^OP_/) {
+ $self->{filters}{$_} = [] unless exists $self->{filters}{$_};
+ my $arg2 = pop @$params;
+ my $arg1 = pop @$params;
+ push @{ $self->{filters}{$_} }, [ $arg1, $arg2 ];
+ $self->{num_filters}++;
+ }
+ default {
+ $self->error("Inernal error: Operator expected instead of $_");
+ }
+ }
+ }
+ }
+
+ $self->{query_string} = '?' . join( '&', values %likes );
+ $self->{is_computed} = 1;
+
+ $self->debug( 'Computed filter:', $self->{filters} );
+ $self->verbose( "Computed query string is: " . $self->{query_string} );
+
+ return undef;
+}
+
+sub filter {
+ my ( $self, $objects ) = @_;
+
+ my $config = $self->{config};
+ my $json = $self->{json};
+
+ return $objects unless $self->{num_filters};
+
+ my $num = sub {
+ my $str = shift;
+ $str =~ s/\D//g;
+ $str = 0 if $str eq '';
+ return int $str;
+ };
+
+ while ( my ( $op, $vals ) = each %{ $self->{filters} } ) {
+ for my $val (@$vals) {
+ my ( $key, $val ) = @$val;
+
+ @$objects = grep {
+ my $object = $_;
+
+ if ( exists $object->{$key} ) {
+ if ( $op eq 'OP_MATCHES' and $object->{$key} =~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NMATCHES' and $object->{$key} !~ /$val/ ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_EQ' and $object->{$key} eq $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_NE' and $object->{$key} ne $val ) {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LT'
+ and $num->( $object->{$key} ) < $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_LE'
+ and $num->( $object->{$key} ) <= $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GT'
+ and $num->( $object->{$key} ) > $num->($val) )
+ {
+ 1;
+
+ }
+ elsif ( $op eq 'OP_GE'
+ and $num->( $object->{$key} ) >= $num->($val) )
+ {
+ 1;
+
+ }
+ else {
+ 0;
+ }
+ }
+ else {
+ 0;
+ }
+ } @$objects;
+ }
+ }
+
+ return $objects;
+}
+
+1;
+
diff --git a/lib/MON/JSON.pm b/lib/MON/JSON.pm
new file mode 100644
index 0000000..e12b1ce
--- /dev/null
+++ b/lib/MON/JSON.pm
@@ -0,0 +1,51 @@
+package MON::JSON;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use JSON;
+
+use MON::Display;
+use MON::Utils;
+
+our @ISA = ('MON::Display');
+
+our $JSON_XS = JSON::XS->new();
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ return undef;
+}
+
+sub decode {
+ my ( $self, $json ) = @_;
+
+ return $JSON_XS->allow_nonref()->decode($json);
+}
+
+sub encode {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->pretty()->encode($vals);
+}
+
+sub encode_canonical {
+ my ( $self, $vals ) = @_;
+
+ return $JSON_XS->canonical()->pretty()->encode($vals);
+}
+
+1;
diff --git a/lib/MON/Options.pm b/lib/MON/Options.pm
new file mode 100644
index 0000000..b798f56
--- /dev/null
+++ b/lib/MON/Options.pm
@@ -0,0 +1,163 @@
+package MON::Options;
+
+use strict;
+use warnings;
+use v5.10;
+use autodie;
+
+use Data::Dumper;
+use Scalar::Util qw(looks_like_number);
+
+use MON::Utils;
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+ $self->parse();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ my %opts = (
+ opts => {
+ config => '',
+ debug => 0,
+ dry => 0,
+ help => 0,
+ interactive => 0,
+ meta => 0,
+ nocolor => 0,
+ quiet => 0,
+ syslog => 0,
+ unique => 0,
+ verbose => 0,
+ version => 0,
+ errfile => '',
+ },
+ opts_short => {
+ c => 'config',
+ D => 'debug',
+ d => 'dry',
+ i => 'interactive',
+ h => 'help',
+ m => 'meta',
+ n => 'nocolor',
+ q => 'quiet',
+ s => 'syslog',
+ u => 'unique',
+ v => 'verbose',
+ V => 'version',
+ R => 'errfile',
+ },
+ unknown => [],
+ );
+
+ $self->{$_} = $opts{$_} for keys %opts;
+
+ return undef;
+}
+
+sub parse {
+ my ($self) = @_;
+
+ my $opts_passed = $self->{opts_passed};
+
+ for my $opt (@$opts_passed) {
+ my ( $k, $v ) = split /=/, $opt;
+
+ # Longopt
+ if ( $k =~ s/^--// && isin $k, keys %{ $self->{opts} } ) {
+ if ( defined $v ) {
+ $self->{opts}{$k} = $v;
+ }
+ else {
+ $self->{opts}{$k} = 1;
+ }
+ }
+
+ # Shortopt
+ elsif ( $k =~ s/^-// && isin $k, keys %{ $self->{opts_short} } ) {
+ if ( defined $v ) {
+ $self->{opts}{ $self->{opts_short}{$k} } = $v;
+ }
+ else {
+ $self->{opts}{ $self->{opts_short}{$k} } = 1;
+ }
+
+ }
+ elsif ( $k !~ /\./ ) {
+
+ # If key is not separated by dot, it is unknown
+ push @{ $self->{unknown} }, $opt;
+
+ }
+ else {
+
+ # Otherise it might overwrite a value of mon.conf
+ $self->{opts}{$k} = $v;
+ }
+ }
+
+ # Help implies dry mode
+ $self->{opts}{dry} = 1 if $self->{opts}{help};
+
+ # Debug implies verbose mode
+ $self->{opts}{verbose} = 1 if $self->{opts}{debug};
+
+ return undef;
+}
+
+sub get_keys {
+ my ($self) = @_;
+ my @keys;
+
+ while ( my ( $k, $v ) = each %{ $self->{opts_short} } ) {
+ if ( looks_like_number( $self->{opts}{$v} ) ) {
+ push @keys, "--$v -$k";
+ }
+ else {
+ push @keys, "--$v=VAL -$k=VAL";
+ }
+ }
+
+ return @keys;
+}
+
+sub store {
+ my ( $self, $config ) = @_;
+
+ $self->store_first($config);
+ $self->store_after($config);
+
+ return undef;
+}
+
+# Only store values which are not separated by dots
+sub store_first {
+ my ( $self, $config ) = @_;
+
+ for ( grep !/\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+# Only store values which are separated by dots
+sub store_after {
+ my ( $self, $config ) = @_;
+
+ for ( grep /\./, keys %{ $self->{opts} } ) {
+ $config->{$_} = $self->{opts}{$_};
+ }
+
+ return undef;
+}
+
+1;
diff --git a/lib/MON/Query.pm b/lib/MON/Query.pm
new file mode 100644
index 0000000..d7e223b
--- /dev/null
+++ b/lib/MON/Query.pm
@@ -0,0 +1,557 @@
+package MON::Query;
+
+use strict;
+use warnings;
+use v5.10;
+
+use Data::Dumper;
+
+use MON::Display;
+use MON::Config;
+use MON::Utils;
+use MON::QueryBase;
+
+our @ISA = ('MON::QueryBase');
+
+sub new {
+ my ( $class, %opts ) = @_;
+
+ my $self = bless \%opts, $class;
+
+ $self->init();
+
+ return $self;
+}
+
+sub init {
+ my ($self) = @_;
+
+ $self->{querystack} = [];
+ $self->{args} = [ map { s/^V_/:V_/; $_ } @{ $self->{args} } ];
+
+ return undef;
+}
+
+sub tree {
+ my ($self) = @_;
+
+ my $api = $self->{api};
+ my $paths = $api->get_possible_paths();
+
+ my ( $s, $r ) = ( $self, $api );
+
+ my $arr = sub {
+ my ( $keys, $vals ) = @_;
+ map { $_ => shift @$vals } @$keys;
+ };
+
+# _ => By default to run anonymous sub if no other key is specified in command line
+# __ => Always to run anonymous sub in the beginning of the current recursion
+# ___ => Always to run anonymous sub before next recursion
+# __DO => Process recursion right away, only do __ if exists
+# V_FOO => Declare variable FOO
+
+ my $where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_KEY => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_KEY}, $r->get_path_params( $d->{V_PATH} ) );
+ },
+ like => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LIKE') }
+ },
+ matches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_MATCHES') }
+ },
+ nmatches => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NMATCHES') }
+ },
+ eq => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_EQ') }
+ },
+ ne => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_NE') }
+ },
+ lt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LT') }
+ },
+ le => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_LE') }
+ },
+ gt => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GT') }
+ },
+ ge => {
+ V_VAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+ $s->out_json( $d->{where_action}($path) );
+ },
+ },
+ ___ => sub { $s->push_querystack('OP_GE') }
+ },
+ },
+ };
+
+ for my $op ( sort qw(like matches nmatches eq ne lt le gt ge) ) {
+ $where->{V_KEY}{$op}{V_VAL}{and}{__DO} = $where;
+ $where->{V_KEY}{$op}{V_VAL}{'V_ALIAS:a'} = $where->{V_KEY}{$op}{V_VAL}{and};
+ }
+
+ $where->{V_KEY}{'V_ALIAS:l'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:~'} = $where->{V_KEY}{like};
+ $where->{V_KEY}{'V_ALIAS:=='} = $where->{V_KEY}{eq};
+ $where->{V_KEY}{'V_ALIAS:!='} = $where->{V_KEY}{ne};
+ $where->{V_KEY}{'V_ALIAS:=~'} = $where->{V_KEY}{matches};
+ $where->{V_KEY}{'V_ALIAS:!~'} = $where->{V_KEY}{nmatches};
+
+ my $set_where = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{set_action};
+ },
+ where => $where,
+ },
+ },
+ },
+ };
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and} = $set_where;
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS::'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{where};
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set_where->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $set = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_SETKEY => {
+ '=' => {
+ V_SETVAL => {
+ _ => sub {
+ my $d = shift;
+ my $path = $d->{V_PATH};
+
+ $s->out_json( $d->{set_action}($path) );
+ },
+ },
+ },
+ },
+ };
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and} = $set;
+ $set->{V_SETKEY}{'='}{V_SETVAL}{'V_ALIAS:a'} =
+ $set->{V_SETKEY}{'='}{V_SETVAL}{and};
+
+ my $remove = {
+ _ => sub {
+ my $d = shift;
+ $s->possible( $r->get_path_params( $d->{V_PATH} ) );
+ },
+ V_REMOVEKEY => {
+ __ => sub {
+ my $d = shift;
+ $d->{where_action} = $d->{remove_action};
+ },
+ where => $where,
+ },
+ };
+ $remove->{V_REMOVEKEY}{and} = $remove;
+ $remove->{V_REMOVEKEY}{'V_ALIAS:a'} = $remove->{V_REMOVEKEY}{and};
+
+ my $tree = {
+ get => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH => {
+ __ => sub {
+ my $d = shift;
+ $s->check_has( $d->{V_PATH}, $paths );
+ $d->{where_action} = sub {
+ my ($path) = @_;
+ $r->fetch_path_json( $path, $s->get_querystack() );
+ };
+ },
+ _ => sub {
+ my $d = shift;
+ $s->out_json(
+ $r->fetch_path_json( $d->{V_PATH}, $s->get_querystack() ) );
+ },
+ where => $where,
+ },
+ },
+ getfmt => {
+ V_FORMAT => {
+ _ => sub { $s->possible(@$paths) },
+ V_PATH =