##----------------------------------------------------------------------------
## Getopt::Long with Class - ~/lib/Getopt/Class.pm
## Version 0.1
## Copyright(c) 2020 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2020/04/25
## Modified 2020/04/26
## All rights reserved
## 
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
package Getopt::Class;
BEGIN
{
    use strict;
    use warnings;
    use parent qw( Module::Generic );
    use Getopt::Long;
    use TryCatch;
    use DateTime;
    use DateTime::Format::Strptime;
    use Scalar::Util;
    our $VERSION = '0.1';
};

sub init
{
    my $self  = shift( @_ );
    my $param = shift( @_ ) || return( $self->error( "No hash parameter was provided." ) );
    return( $self->error( "Hash of parameters provided ($param) is not an hash reference." ) ) if( !$self->_is_hash( $param ) );
    $self->SUPER::init( $param );
    $self->{configured} = 0;
    $self->{classes} = {};
    $self->{missing} = [];
    
    my $dict = $param->{dictionary} || return( $self->error( "No dictionary was provided to initiate Getopt::Long" ) );
    return( $self->error( "Dictionary provided is not a hash reference." ) ) if( !$self->_is_hash( $dict ) );
    $self->dictionary( $dict );
    
    $self->{configure_options} = [qw( no_ignore_case no_auto_abbrev auto_version auto_help )];
    my $opts = {};
    my $params = [];
    ## Build the options parameters
    foreach my $k ( sort( keys( %$dict ) ) )
    {
        my $k2 = $k;
        $k2 =~ tr/_/-/;
        my $k2_under = $k;
        $k2_under =~ tr/-/_/;
        my $def = $dict->{ $k };
        my $opt_name = [ $k2 ];
        if( ref( $def->{alias} ) eq 'ARRAY' )
        {
            push( @$opt_name, @{$def->{alias}} ) if( scalar( @{$def->{alias}} ) );
            push( @$opt_name, $k2_under ) if( !scalar( grep( /^$k2_under$/, @{$def->{alias}} ) ) );
        }
        my $opt = join( '|', @$opt_name );
        if( length( $def->{default} ) )
        {
            $opts->{ $k2 } = $def->{default};
        }
        else
        {
            $opts->{ $k2 } = '';
        }
        my $suff = '';
        if( $def->{type} eq 'string' )
        {
            $suff = '=s';
        }
        elsif( $def->{type} eq 'string-hash' )
        {
            $suff = '=s%';
        }
        elsif( $def->{type} eq 'array' )
        {
            $suff = '=s@';
            $opts->{ $k2 } = [] unless( length( $def->{default} ) );
        }
        elsif( $def->{type} eq 'boolean' )
        {
            $suff = '!';
        }
        elsif( $def->{type} eq 'hash' )
        {
            $suff = '=s%';
            $opts->{ $k2 } = {} unless( length( $def->{default} ) );
        }
        elsif( $def->{type} eq 'code' && ref( $def->{code} ) eq 'CODE' )
        {
            $opts->{ $k2 } = $def->{code};
        }
        elsif( $def->{type} eq 'integer' )
        {
            $suff = '=i';
        }
        elsif( $def->{type} eq 'decimal' )
        {
            $suff .= '=f';
        }
        elsif( $def->{type} eq 'date' || $def->{type} eq 'datetime' )
        {
            $suff = '=s';
        }
        elsif( $def->{type} eq 'code' )
        {
            return( $self->error( "Type is code, but there is no property code for this option \"$k\"." ) ) if( !CORE::exists( $def->{code} ) );
            return( $self->error( "Type is code, but the property code is not a code reference for this option \"$k\"." ) ) if( ref( $def->{code} ) ne 'CODE' );
            $opts->{ $k2 } = $def->{code};
        }
        
        if( $def->{min} )
        {
            ## If there is no max, it would be for example s{1,}
            ## 2nd formatter is %s because it could be blank. %d would translate to 0 when blank.
            $suff .= sprintf('{%d,%s}', @$def{ qw( min max ) } );
        }
        
        if( $def->{re} && ref( $def->{re} ) ne 'Regexp' )
        {
            return( $self->error( "Regular expression provided for property \"$k\" ($def->{re}) is not a proper regular expression. I was expecting something like qr// and of type 'Regexp'." ) );
        }
        push( @$params, $opt . $suff );
    }
    $self->options( $opts );
    $self->parameters( $params );
    $self->{getopt} = Getopt::Long::Parser->new;
    return( $self );
}

sub check_class_data
{
    my $self  = shift( @_ );
    my $class = shift( @_ ) || return( $self->error( "No class was provided to return its definition" ) );
    return( $self->error( "Class provided '$class' is not a string." ) ) if( ref( $class ) );
    $self->message( 3, "Checking data for class '$class'." );
    my $dict = $self->class( $class ) || return;
    $self->message( 3, "Dictionary data for class '$class' is: ", sub{ $self->dumper( $dict ) } );
    my $v = $self->get_class_values( $class ) || return;
    $self->message( 3, "Collected data is: ", sub{ $self->dumper( $v ) } );
    my $error = 
    {
    missing => {},
    regexp => {},
    };
    foreach my $f ( sort( keys( %$dict ) ) )
    {
        my $def = $dict->{ $f };
        my $n = $def->{name} ? $def->{name} : $f;
        $def->{error} ||= "does not match requirements";
        if( !!$p->{required} && $def->{required} )
        {
            if( ( $def->{type} =~ /^(?:boolean|decimal|integer|string)/ && !length( $v->{ $f } ) ) || 
                ( ( $def->{type} eq 'hash' || $def->{type} eq 'string-hash' ) && !scalar( keys( %{$v->{ $f }} ) ) ) ||
                ( $def->{type} eq 'array' && !scalar( @{$v->{ $f }} ) ) )
            {
                $errors->{missing}->{ $f } = "$f ($n) is missing";
                next;
            }
        }
        if( $def->{re} )
        {
            if( $def->{type} eq 'string' && length( $v->{ $f } ) && $v->{ $f } !~ /$def->{re}/ )
            {
                ## $self->message( 3, "Regular expression for $f is $def->{re}" );
                $errors->{regexp}->{ $f } = "$f ($n) " . $def->{error};
            }
            elsif( $def->{type} eq 'array' )
            {
                $self->message( 3, "Checking array data" );
                my $sub_err = [];
                foreach my $this ( @{$v->{ $f }} )
                {
                    if( $this !~ /$def->{re}/ )
                    {
                        push( @$sub_err, $this );
                    }
                }
                $errors->{regexp}->{ $f } = join( ', ', @$sub_err ) . ' ' . $def->{error};
            }
        }
        elsif( $def->{type} eq 'decimal' && $v->{ $f } !~ /^\d+(\.\d{1,12})?$/ )
        {
            $errors->{regexp}->{ $f } = "$f ($n) " . $def->{error};
        }
    }
    return( $errors );
}

sub class
{
    my $self  = shift( @_ );
    my $class = shift( @_ ) || return( $self->error( "No class was provided to return its definition" ) );
    return( $self->error( "Class provided '$class' is not a string." ) ) if( ref( $class ) );
    $self->message( 3, "Getting class dictionary for '$class'." );
    my $classes = $self->classes;
    return( $self->error( "I was expecting an hash reference for the classes dictionaries but got '$classes' instead." ) ) if( !ref( $classes ) );
    return( $self->error( "No class \"$class\" was found." ) ) if( scalar( keys( %$classes ) ) && !exists( $classes->{ $class } ) );
    my $dict = $self->dictionary;
    return( $self->error( "No dictionary data could be found!" ) ) if( !scalar( keys( %$dict ) ) );
    foreach my $k ( sort( keys( %$dict ) ) )
    {
        my $def = $dict->{ $k };
        next if( !exists( $def->{class} ) );
        my $class_names = $def->{class};
        my $k2 = $k;
        $k2 =~ tr/-/_/;
        foreach my $class ( @$class_names )
        {
            ## _message( 3, "Adding class $class" ) if( !exists( $classes->{ $class } ) );
            $classes->{ $class } = {} if( !exists( $classes->{ $class } ) );
            my $this = $classes->{ $class };
            $this->{ $k2 } = $def;
        }
        ## If there are any alias, we add them too
        if( $def->{alias} && scalar( @{$def->{class}} ) )
        {
            foreach my $f ( @{$def->{alias}} )
            {
                my $f2 = $f;
                $f2 =~ tr/-/_/;
                foreach my $class ( @$classes )
                {
                    ## _message( 3, "Adding class $class" ) if( !exists( $classes->{ $class } ) );
                    $classes->{ $class } = {} if( !exists( $classes->{ $class } ) );
                    my $this = $classes->{ $class };
                    $this->{ $f2 } = $def;
                }
            }
        }
    }
    return( $self->error( "No class \"$class\" was found." ) ) if( !exists( $classes->{ $class } ) );
    return( $classes->{ $class } );
}

sub classes { return( shift->_set_get_hash( 'classes', @_ ) ); }

sub class_properties
{
    my $self  = shift( @_ );
    my $class = shift( @_ );
    return( $self->error( "No class was provided to list its properties." ) ) if( !length( $class ) );
    $self->message( "Showing all properties for class \"$class\"." );
    my $fields = [];
    my $ref = $self->class( $class );
    my $props = [ sort( keys( %$ref ) ) ];
    return( Module::Generic::Array->new( $props ) );
}

sub configure
{
    my $self = shift( @_ );
    return( $self ) if( $self->{configured} );
    $self->message( 3, "Called to configure Getopt::Long" );
    my $conf = [];
    $conf = shift( @_ ) if( ref( $_[0] ) );
    $conf = $self->configure_options if( !scalar( @$conf ) );
    $self->message( 3, "Using configuration parameters: '", join( "', '", @$conf ), "'." );
    my $getopt = $self->getopt || return( $self->error( "No Getopt::Long::Parser object found." ) );
    try
    {
        $getopt->configure( @$conf );
        $self->{configured} = 1;
    }
    catch( $e )
    {
        return( $self->error( "An error occurred while configuration Getlong::Opt: $e" ) );
    }
    return( $self );
}

sub configure_errors { return( shift->_set_get_array_as_object( 'configure_errors', @_ ) ); }

sub configure_options { return( shift->_set_get_array_as_object( 'configure_options', @_ ) ); }

sub dictionary { return( shift->_set_get_hash( 'dictionary', @_ ) ); }

sub exec
{
    my $self = shift( @_ );
    $self->configure || return;
    my $errors = [];
    my $missing = [];
    my $dict = $self->dictionary;
    return( $self->error( "The data returned by dictionary() is not an hash reference." ) ) if( !$self->_is_hash( $dict ) );
    return( $self->error( "Somehow, the dictionary hash is empty!" ) ) if( !scalar( keys( %$dict ) ) );
    my $opts = $self->options;
    return( $self->error( "The data returned by options() is not an hash reference." ) ) if( !$self->_is_hash( $opts ) );
    return( $self->error( "Somehow, the options hash is empty!" ) ) if( !scalar( keys( %$opts ) ) );
    my $params = $self->parameters;
    return( $self->error( "Data returned by parameters() is not an array reference" ) ) if( !$self->_is_array( $params ) );
    return( $self->error( "Somehow, the parameters array is empty!" ) ) if( !scalar( @$params ) );
    my $getopt = $self->getopt || return( $self->error( "No Getopt::Long object found." ) );
    my $required = $self->required;
    return( $self->error( "Data returned by required() is not an array reference" ) ) if( !$self->_is_array( $required ) );
    
    local $Getopt::Long::SIG{ '__DIE__' } = sub
    {
        push( @$errors, join( '', @_ ) );
    };
    local $Getopt::Long::SIG{ '__WARN__' } = sub
    {
        push( @$errors, join( '', @_ ) );
    };
    $self->configure_errors( $errors );
    
    $getopt->getoptions( $opts, @$params ) || do
    {
        my $usage = $self->usage;
        return( $usage->() ) if( ref( $usage ) eq 'CODE' );
        return;
    };
    $self->message( 3, "Options data is: ", sub{ $self->dumper( $opts ) } );
    
    foreach my $key ( @$required )
    {
        if( exists( $opts->{ $key } ) &&
            ( !defined( $opts->{ $key } ) || 
              !length( $opts->{ $key } ) || 
              $opts->{ $key } =~ /^[[:blank:]]*$/ ||
              ( ref( $opts->{ $key } ) eq 'SCALAR' && 
                ( !length( ${$opts->{ $key }} ) || ${$opts->{ $key }} =~ /^[[:blank:]]*$/ ) 
              ) ||
              (
                ref( $opts->{ $key } ) eq 'ARRAY' &&
                !scalar( @{$opts->{ $key }} )
              )
            ) 
        )
        {
            push( @$missing, $key );
        }
    }
    $self->missing( $missing );
    
    foreach my $k ( sort( keys( %$opts ) ) )
    {
        next if( index( $k, '-' ) == -1 );
        my $k2 = $k;
        $k2 =~ tr/-/_/;
        $opts->{ $k2 } = $opts->{ $k };
    }
    
    ## This only process data and does not check their validity beyond what Getopt::Long has already done
    foreach my $k ( keys( %$dict ) )
    {
        next if( !length( $opts->{ $k } ) );
        my $def = $dict->{ $k };
        return( $self->error( "Dictionary is malformed with entry $k value not being an hash reference." ) ) if( ref( $def ) ne 'HASH' );
        my $k2_dash = $k;
        $k2_dash =~ tr/_/-/;
        my $k2_under = $k;
        $k2_under =~ tr/-/_/;
        ## If there are aliases, make sure the value submitted is also available with the aliases
        if( $def->{alias} )
        {
            ## _message( 3, "Processing optiona $k with value \"$opts->{$k}\" with aliases: '", join( "', '", @{$def->{alias}} ), "'." );
            foreach my $f ( @{$def->{alias}} )
            {
                my $f2_dash = $f;
                my $f2_under = $f;
                $f2_dash =~ tr/_/-/;
                $f2_under =~ tr/-/_/;
                $opts->{ $f2_dash } = $opts->{ $k } unless( length( $opts->{ $f2_dash } ) );
                $opts->{ $f2_under } = $opts->{ $k } unless( length( $opts->{ $f2_under } ) );
            }
        }
        
        if( ref( $def->{default} ) eq 'SCALAR' )
        {
            $opts->{ $k2_dash } = $opts->{ $k2_under } = $opts->{ $k } = ${$opts->{ $k }};
        }
        
        if( ( $def->{type} eq 'date' || $def->{type} eq 'datetime' ) && length( $opts->{ $k } ) )
        {
            $self->message( 3, "Found a date/datetime field \"$k\"." );
            if( $opts->{ $k } =~ /^\d+$/ )
            {
                try
                {
                    my $dt = DateTime->from_epoch( epoch => $opts->{ $k } );
                    $opts->{ $k2_dash } = $dt;
                    $opts->{ $k2_under } = $dt;
                }
                catch( $e )
                {
                    return( $self->error( "An unexpected error occurred while converting date provided for \"$k\" (", $v->{ $k }, "): $e" ) );
                }
            }
            elsif( $def->{type} eq 'date' && $opts->{ $k } =~ /^(?<year>\d{4})-(?<month>\d{1,2})-(?<day>\d{1,2})$/ )
            {
                try
                {
                    my $dt = DateTime->new(
                        year => int( $+{year} ),
                        month => int( $+{month} ),
                        day => int( $+{day} ),
                        hour => 0,
                        minute => 0,
                        second => 0,
                        time_zone => 'local',
                    );
                    $opts->{ $k2_dash } = $dt;
                    $opts->{ $k2_under } = $dt;
                    ## my $ts = $dt->epoch;
                }
                catch( $e )
                {
                    return( $self->error( "An unexpected error occurred while converting date provided for \"$k\" (", $v->{ $k }, "): $e" ) );
                }
            }
            elsif( $def->{type} eq 'datetime' && $opts->{ $k } =~ /^(?<year>\d{4})-(?<month>\d{1,2})-(?<day>\d{1,2})(?:T|\s)(?<hour>\d{1,2})\:(?<minute>\d{1,2})\:(?<second>\d{1,2})$/ )
            {
                try
                {
                    my $dt = DateTime->new(
                        year => int( $+{year} ),
                        month => int( $+{month} ),
                        day => int( $+{day} ),
                        hour => int( $+{hour} ),
                        minute => int( $+{minute} ),
                        second => int( $+{second} ),
                        time_zone => 'local',
                    );
                    $opts->{ $k2_dash } = $dt;
                    $opts->{ $k2_under } = $dt;
                    ## my $ts = $dt->epoch;
                }
                catch( $e )
                {
                    return( $self->error( "An unexpected error occurred while converting date provided for \"$k\" (", $v->{ $k }, "): $e" ) );
                }
            }
            elsif( $opts->{ $k } eq 'now' )
            {
                my $dt = DateTime->now( time_zone => 'local' );
                $opts->{ $k2_dash } = $dt;
                $opts->{ $k2_under } = $dt;
            }
            else
            {
                return( $self->error( "Unsuported value for date or datetime \"$f\": ", $v->{ $k }, ". Please use a unix timestamp (i.e. an integer) or an iso 8601 type like 2019-12-25" ) );
            }
            ## Default format for stringification is the unix timestamp
            my $fmt = DateTime::Format::Strptime->new(
                pattern => '%s',
                locale => 'en_GB',
                time_zone => 'local',
            );
            $opts->{ $k2_dash }->set_formatter( $fmt );
            $opts->{ $k2_under }->set_formatter( $fmt );
        }
    }
    return( $opts );
}

sub get_class_values
{
    my $self  = shift( @_ );
    my $class = shift( @_ ) || return( $self->error( "No class was provided to return its definition" ) );
    return( $self->error( "Class provided '$class' is not a string." ) ) if( ref( $class ) );
    my $this_dict = $self->class( $class ) || return;
    my $opts = $self->options;
    return( $self->error( "The data returned by options() is not an hash reference." ) ) if( !$self->_is_hash( $opts ) );
    return( $self->error( "Somehow, the options hash is empty!" ) ) if( !scalar( keys( %$opts ) ) );
    my $v = {};
    foreach my $f ( sort( keys( %$this_dict ) ) )
    {
        my $ref = lc( Scalar::Util::reftype( $opts->{ $f } ) );
        if( $ref eq 'hash' )
        {
            $v->{ $f } = $opts->{ $f } if( scalar( keys( %{$opts->{ $f }} ) ) > 0 );
        }
        elsif( $ref eq 'array' )
        {
            $v->{ $f } = $opts->{ $f } if( scalar( @{$opts->{ $f }} ) > 0 );
        }
        elsif( !length( $ref ) )
        {
            $v->{ $f } = $opts->{ $f } if( length( $opts->{ $f } ) );
        }
    }
    return( $v );
}

sub getopt { return( shift->_set_get_object( 'getopt', 'Getopt::Long::Parser', @_ ) ); }

sub missing { return( shift->_set_get_array_as_object( 'missing', @_ ) ); }

sub options { return( shift->_set_get_hash( 'options', @_ ) ); }

sub parameters { return( shift->_set_get_array_as_object( 'parameters', @_ ) ); }

sub required { return( shift->_set_get_array_as_object( 'required', @_ ) ); }

sub usage { return( shift->_set_get_code( 'usage', @_ ) ); }

1;

__END__

=encoding utf-8

=head1 NAME

Getopt::Class - Extended dictionary version of Getopt::Long

=head1 SYNOPSIS

    use Getopt::Class;
    our $DEBUG = 0;
    our $VERBOSE = 0;
    our $VERSION = '0.1';
    my $dict =
    {
        create_user     => { type => 'boolean', alias => [qw(create_person create_customer)], action => 1 },
        create_product  => { type => 'boolean', action => 1 },
        debug           => { type => 'integer', default => \$DEBUG },
        help            => { type => 'code', code => sub{ pod2usage(1); }, alias => '?', action => 1 },
        man             => { type => 'code', code => sub{ pod2usage( -exitstatus => 0, -verbose => 2 ); }, action => 1 },
        quiet           => { type => 'boolean', default => 0, alias => 'silent' },
        verbose         => { type => 'boolean', default => \$VERBOSE, alias => 'v' },
        version         => { type => 'code', code => sub{ printf( "v%.2f\n", $VERSION ); }, action => 1 },
    
        api_server      => { type => 'string', default => 'api.example.com' },
        api_version     => { type => 'string', default => 1 },
        as_admin        => { type => 'boolean' },
        dry_run         => { type => 'boolean', default => 0 },
    
        name            => { type => 'string', class => [qw( person product )] },
        created         => { type => 'datetime', class => [qw( person product )] },
        define          => { type => 'string-hash', default => {} },
        langs           => { type => 'array', class => [qw( person product )], re => qr/^[a-z]{2}([_|-][A-Z]{2})?/, min => 1, default => [qw(en)] },
        currency        => { type => 'string', class => [qw(product)], name => 'currency', re => qr/^[a-z]{3}$/, error => "must be a three-letter iso 4217 value" },
        age             => { type => 'integer', class => [qw(person)], name => 'age', },
    };
    
    # Assuming command line arguments like:
    prog.pl --create-user --name Bob --langs fr ja --age 30 --created now --debug 3

    my $opt = Getopt::Class->new({
        dictionary => $dict,
    }) || die( Getopt::Class->error, "\n" );
    my $opts = $opt->exec || die( $opt->error, "\n" );
    $opt->required( [qw( name langs )] );
    my $err = $opt->check_class_data( 'person' );
    printf( "User is %s and is %d years old\n", $opts{qw( name age )} ) if( $opts->{debug} );

    # Get all the properties for class person
    my $props = $opt->class_properties( 'person' );

    # Get values collected for class 'person'
    if( $opts->{create_user} )
    {
        my $values = $opt->get_class_values( 'person' );
        # Having collected the values for our class of properties, and making sure all 
        # required are here, we can add them to database or make api calls, etc
    }
    elsif( $opts->{create_product} )
    {
        # etc...
    }

=head1 VERSION

    v0.1

=head1 DESCRIPTION

L<Getopt::Class> is a lightweight wrapper around L<Getopt::Long> that implements the idea of class of properties and makes it easier and powerful to set up L<Getopt::Long>. This module is particularly useful if you want to provide several sets of options for different features or functions of your program. For example, you may have a part of your program that deals with user while another deals with product. Each of them needs their own properties to be provided.

=head1 CONSTRUCTOR

=head2 new

To instantiate a new L<Getopt::Class> object, pass an hash reference of following parameters:

=over 4

=item I<dictionary>

This is required. It must contain a key value pair where the value is an anonymous hash reference that can contain the following parameters:

=over 8

=item I<alias>

This is an array reference of alternative options that can be used in an interchangeable way

    my $dict =
    {
    last_name => { type => 'string', alias => [qw( family_name surname )] },
    };
    # would make it possible to use either of the following combinations
    --last-name Doe
    # or
    --surname Doe
    # or
    --family-name Doe

=item I<default>

This contains the default value. For a string, this could be anything, and also a reference to a scalar, such as:

    our $DEBUG = 0;
    my $dict =
    {
    debug => { type => 'integer', default => \$DEBUG },
    };

It can also be used to provide default value for an array, such as:

    my $dict =
    {
    langs => { type => 'array', class => [qw( person product )], re => qr/^[a-z]{2}([_|-][A-Z]{2})?/, min => 1, default => [qw(en)] },
    };

But beware that if you provide a value, it will not superseed the existing default value, but add it on top of it, so

    --langs en fr ja

would not produce an array with C<en>, C<fr> and C<ja> entries, but an array such as:

    ['en', 'en', 'fr', 'ja' ]

because the initial default value is not replaced when one is provided. This is a design from L<Getopt::Long> and although I could circumvent this, I a not sure I should.

=item I<error>

A string to be used to set an error by L</"check_class_data">. Typically the string should provide meaningful information as to what the data should normally be. For example:

    my $dict =
    {
    currency => { type => 'string', class => [qw(product)], name => 'currency', re => qr/^[a-z]{3}$/, error => "must be a three-letter iso 4217 value" },
    };

=item I<max>

This is well explained in L<Getopt::Long/"Options with multiple values">

It serves "to specify the minimal and maximal number of arguments an option takes".

=item I<min>

Same as above

=item I<re>

This must be a regular expression and is used by L</"check_class_data"> to check the sanity of the data provided by the user.
So, for example:

    my $dict =
    {
    currency => { type => 'string', class => [qw(product)], name => 'currency', re => qr/^[a-z]{3}$/, error => "must be a three-letter iso 4217 value" },
    };

then the user calls your program with, among other options:

    --currency euro

would set an error that can be retrieved as an output of L</"check_class_data">

=item I<required>

Set this to true or false (1 or 0) to instruct L</"check_class_data"> whether to check if it is missing or not.

This is an alternative to the L</"required"> method which is used at an earlier stage, during L</"exec">

=item I<type>

Type can be I<array>, I<boolean>, I<code>, I<decimal>, I<hash>, I<integer>, I<string>, I<string-hash>

Type I<hash> is convenient for free key-value pair such as:

    --define customer_id=10 --define transaction_id 123

would result for C<define> with an anonymous hash as value containing C<customer_id> with value C<10> and C<transaction_id> with value C<123>

Type code implies an anonymous sub routine and should be accompanied with the attribute I<code>, such as:

    { type => 'code', code => sub{ pod2usage(1); exit( 0 ) }, alias => '?', action => 1 },

Also as seen in the example above, you can add additional properties to be used in your program, here such as I<action> that could be used to identify all options that are used to trigger an action or a call to a sub routine.

=back

=item I<debug>

This takes an integer, and is used to set the level of debugging. Anything under 3 will not provide anything meaningful.

=back

=head1 METHODS

=head2 check_class_data

Provided with a string corresponding to a class name, this will check the data provided by the user.

Currently this means it checks if the data is present when the attribute I<required> is set, and it checks the data against a regular expression if one is provided with the attribute I<re>

It returns an hash reference with 2 keys: I<missing> and I<regexp>. Each with an anonymous hash reference with key matching the option name and the value the error string. So:

    my $dict =
    {
    name => { type => 'string', class => [qw( person product )], required => 1 },
    langs => { type => 'array', class => [qw( person product )], re => qr/^[a-z]{2}([_|-][A-Z]{2})?/, min => 1, default => [qw(en)] },
    };

Assuming your user calls your program without C<--name> and with C<--langs FR EN> this would have L</"check_class_data"> return the following data structure:

    $errors =
    {
    missing => { name => "name (name) is missing" },
    regexp => { langs => "langs (langs) does not match requirements" },
    };

=head2 class

Provided with a string representing a property class, and this returns an hash reference of all the dictionary entries matching this class

=head2 classes

This returns an hash reference containing class names, each of which has an anonymous hash reference with corresponding dictionary entries

=head2 class_properties

Provided with a string representing a class name, this returns an array reference of options, a.k.a. class properties.

The array reference is a L<Module::Generic::Array> object.

=head2 configure

This calls L<Getopt::Long::configure> with the L</"configure_options">.

It can be overriden by calling L</"configure"> with an array reference.

If there is an error, it will return undef and set an L</"error"> accordingly.

Otherwise, it returns the L<Getopt::Class> object, so it can be chained.

=head2 configure_errors

This returns an array reference of the errors generated by L<Getopt::Long> upon calling L<Getopt::Long/"getoptions"> by L</"exec">

The array is an L<Module::Generic::Array> object

=head2 configure_options

This returns an array reference of the L<Getopt::Long> configuration options upon calling L<Getopt::Long/"configure"> by method L</"configure">

The array is an L<Module::Generic::Array> object

=head2 dictionary

This returns the hash reference representing the dictionary set when the object was instantiated. See L</"new"> method.

=head2 error

Return the last error set as a L<Module::Generic::Exception> object. Because the object can be stringified, you can do directly:

    die( $opt->error, "\n" ); # with a stack trace

or

    die( sprintf( "Error occurred at line %d in file %s with message %s\n", $opt->error->line, $opt->error->file, $opt->error->message ) );

=head2 exec

This calls L<Getopt::Long/"getoptions"> with the L</"options"> hash reference and the L</"parameters"> array reference and after having called L</"configure"> to configure L<Getopt::Long> with the proper parameters according to the dictionary provided at the time of object instantiation.

If there are any L<Getopt::Long> error, they can be retrieved with method L</"configure_errors">

    my $opt = Getopt::Class->new({ dictionary => $dict }) || die( Getopt::Class->error );
    my $opts = $opt->exec || die( $opt->error );
    if( $opt->configure_errors->length > 0 )
    {
        # do something about it
    }

If any required options have been specified with the method L</"required">, it will check any missing option then and set an array of those missing options that can be retrieved with method L</"missing">

This method makes sure that any option can be accessed with underscore or dash whichever, so a dictionary entry such as:

    my $dict =
    {
    create_customer => { type => 'boolean', alias => [qw(create_client create_user)], action => 1 },
    };

can be called by your user like:

    ---create-customer
    # or
    --create-client
    # or
    --create-user

because a duplicate entry with the underscore replaced by a dash is created. So you can say in your program:

    my $opts = $opt->exec || die( $opt->error );
    if( $opts->{create_user} )
    {
        # do something
    }

=head2 get_class_values

Provided with a string representing a property class, and this returns an hash reference of all the key-value pairs provided by your user. So:

    my $dict =
    {
    create_customer => { type => 'boolean', alias => [qw(create_client create_user)], action => 1 },
    name        => { type => 'string', class => [qw( person product )] },
    created     => { type => 'datetime', class => [qw( person product )] },
    define      => { type => 'string-hash', default => {} },
    langs       => { type => 'array', class => [qw( person product )], re => qr/^[a-z]{2}([_|-][A-Z]{2})?/, min => 1, default => [] },
    currency    => { type => 'string', class => [qw(product)], name => 'currency', re => qr/^[a-z]{3}$/, error => "must be a three-letter iso 4217 value" },
    age         => { type => 'integer', class => [qw(person)], name => 'age', },
    };

Then the user calls your program with:

    --create-user --name Bob --age 30 --langs en ja --created now

    # In your app
    my $opt = Getopt::Class->new({ dictionary => $dict }) || die( Getopt::Class->error );
    my $opts = $opt->exec || die( $opt->error );
    # $vals being an hash reference as a subset of all the values returned in $opts above
    my $vals = $opt->get_class_values( 'person' )
    # returns an hash only with keys name, age, langs and created

=head2 getopt

Sets or get the L<Getopt::Long::Parser> object. You can provide yours if you want but beware that certain options are necessary for L<Getopt::Class> to work. You can check those options with the method L</"configure_options">

=head2 missing

Returns an array of missing options. The array reference returned is a L<Module::Generic::Array> object, so you can do thins like

    if( $opt->missing->length > 0 )
    {
        # do something
    }

=head2 options

Returns an hash reference of options created by L</"new"> based on the dictionary you provide. This hash reference is used by L</"exec"> to call L<Getopt::Long/"getoptions">

=head2 parameters

Returns an array reference of parameters created by L</"new"> based on the dictionary you provide. This hash reference is used by L</"exec"> to call L<Getopt::Long/"getoptions">

This array reference is a L<Module::Generic::Array> object

=head2 required

Set or get the array reference of required options. This returns a L<Module::Generic::Array> object.

=head2 usage

Set or get the anonymous subroutine or sub routine reference used to show the user the proper usage of your program.

This is called by L</"exec"> after calling L<Getopt::Long/"getoptions"> if there is an error, i.e. if L<Getopt::Long/"getoptions"> does not return a true value.

If you use object to call the sub routine usage, I recommend using the module L<curry>

If this is not set, L</"exec"> will simply return undef or an empty list depending on the calling context.

=head1 ERROR HANDLING

This module never dies, or at least not by design. If an error occurs, each method returns undef and sets an error that can be retrieved with the method L</"error">

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 SEE ALSO

L<Net::API::Stripe::Number::Format>

=head1 COPYRIGHT & LICENSE

Copyright (c) 2019-2020 DEGUEST Pte. Ltd.

You can use, copy, modify and redistribute this package and associated
files under the same terms as Perl itself.

=cut
