The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package RPC::ExtDirect::Config;

use strict;
use warnings;
no  warnings 'uninitialized';           ## no critic

use Carp;

use RPC::ExtDirect::Util::Accessor;
use RPC::ExtDirect::Util qw/ parse_global_flags /;

### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
#
# Create a new Config instance
#

sub new {
    my $class = shift;
    
    my %arg;
    
    if ( @_ == 1 and 'HASH' eq ref $_[0] ) {
        %arg = %{ $_[0] };
    }
    elsif ( @_ % 2 == 0 ) {
        %arg = @_;
    }
    elsif ( @_ != 0 ) {
        croak "Odd number of arguments in RPC::ExtDirect::Config->new()";
    }
    
    my $self = bless {}, $class;
    
    $self->_init();
    $self->set_options(%arg);
    
    return $self;
}

### PUBLIC INSTANCE METHOD (CONSTRUCTOR) ###
#
# Create a new Config instance from existing one (clone it)
# We're only doing shallow copying here.
#

sub clone {
    my ($self) = @_;
    
    my $clone = bless {}, ref $self;
    
    @$clone{ keys %$self } = values %$self;
    
    return $clone;
}

### PUBLIC INSTANCE METHOD ###
#
# Re-parse the global vars
#

sub read_global_vars {
    my ($self) = @_;
    
    $self->_parse_global_vars();
    
    return $self;
}

### PUBLIC INSTANCE METHOD ###
#
# Add specified accessors to the Config instance class
#

sub add_accessors {
    my ($self, %arg) = @_;
    
    RPC::ExtDirect::Util::Accessor->mk_accessors(
        class  => ref $self || $self, # Class method, too
        ignore => 1,
        %arg,
    );
    
    return $self;
}

### PUBLIC INSTANCE METHOD ###
#
# Set the options in bulk by calling relevant setters
#

sub set_options {
    my $self = shift;
    
    my $debug = $self->debug;
    
    my %options = @_ == 1 && 'HASH' eq ref($_[0]) ? %{ $_[0] } : @_;
    
    foreach my $option (keys %options) {
        my $value = $options{$option};
        
        # We may as well be passed some options that we don't support;
        # that may happen by accident, or the options hash may be passed
        # on from unknown upper level. This does not represent a problem
        # per se, so rather than bomb out with a cryptic error if a setter
        # happens not to be defined, we warn in debug and silently ignore
        # such occurences when not debugging.
        if ( $self->can($option) ) {
            $self->$option($value);
        }
        elsif ( $debug ) {
            warn ref($self)." instance was passed a config option $option ".
                            "for which there is no setter. A mistake?";
        }
    }
    
    return $self;
}

#
# Note to self: the four deprecated methods below are *intentionally*
# left verbose and not collapsed to some helper sub.
#

### PUBLIC CLASS METHOD ###
#
# Return the default router path; provided for compatibility with 2.x
#
# DEPRECATED. Use `router_path` method on a Config instance instead.
#

sub get_router_path {
    warn __PACKAGE__."->get_router_path class method is deprecated; " .
                     "use router_path instance method instead";
    
    return __PACKAGE__->new->router_path;
}

### PUBLIC CLASS METHOD ###
#
# Return the default poll path; provided for compatibility with 2.x
#
# DEPRECATED. Use `poll_path` method on a Config instance instead.
#

sub get_poll_path {
    warn __PACKAGE__."->get_poll_path class method is deprecated; " .
                     "use poll_path instance method instead";
    
    return __PACKAGE__->new->poll_path;
}

### PUBLIC CLASS METHOD ###
#
# Return the default remoting variable name; provided for
# compatibility with 2.x
#
# DEPRECATED. Use `remoting_var` method on a Config instance instead.
#

sub get_remoting_var {
    warn __PACKAGE__."->get_remoting_var class method is deprecated; " .
                     "use remoting_var instance method instead";

    return __PACKAGE__->new->remoting_var;
}

### PUBLIC CLASS METHOD ###
#
# Return the default polling variable name; provided for
# compatibility with 2.x
#
# DEPRECATED. Use `polling_var` method on a Config instance instead.
#

sub get_polling_var {
    warn __PACKAGE__."->get_polling_var class method is deprecated; " .
                     "use polling_var instance method instead";
    
    return __PACKAGE__->new->polling_var;
}

############## PRIVATE METHODS BELOW ##############

#
# This humongous hashref holds definitions for all fields,
# accessors, default values and global variables involved
# with config objects.
# It's just easier to keep all this stuff in one place
# and pluck the pieces needed for various purposes.
#
my $DEFINITIONS = [{
    accessor => 'api_action_class',
    default  => 'RPC::ExtDirect::API::Action',
}, {
    accessor => 'api_method_class',
    default  => 'RPC::ExtDirect::API::Method',
}, {
    accessor => 'api_hook_class',
    default  => 'RPC::ExtDirect::API::Hook',
}, {
    accessor => 'api_full_action_names',
    default  => !1,
}, {
    accessor => 'debug',
    default  => !1,
}, {
    package  => 'RPC::ExtDirect::API',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_api',
    fallback => 'debug',
}, {
    package  => 'RPC::ExtDirect::EventProvider',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_eventprovider',
    fallback => 'debug',
}, {
    package  => 'RPC::ExtDirect::Serialize',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_serialize',
    fallback => 'debug',
}, {
    package  => 'RPC::ExtDirect::Deserialize',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_deserialize',
    fallback => 'debug',
}, {
    package  => 'RPC::ExtDirect::Request',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_request',
    fallback => 'debug',
}, {
    package  => 'RPC::ExtDirect::Router',
    var      => 'DEBUG',
    type     => 'scalar',
    setter   => 'debug_router',
    fallback => 'debug',
}, {
    accessor => 'exception_class',
    default  => 'RPC::ExtDirect::Exception',
}, {
    package  => 'RPC::ExtDirect::Serialize',
    var      => 'EXCEPTION_CLASS',
    type     => 'scalar',
    setter   => 'exception_class_serialize',
    fallback => 'exception_class',
}, {
    package  => 'RPC::ExtDirect::Deserialize',
    var      => 'EXCEPTION_CLASS',
    type     => 'scalar',
    setter   => 'exception_class_deserialize',
    fallback => 'exception_class',
}, {
    package  => 'RPC::ExtDirect::Request',
    var      => 'EXCEPTION_CLASS',
    type     => 'scalar',
    setter   => 'exception_class_request',
    fallback => 'exception_class',
}, {
    accessor => 'request_class',
    default  => 'RPC::ExtDirect::Request',
}, {
    package  => 'RPC::ExtDirect::Deserialize',
    var      => 'REQUEST_CLASS',
    type     => 'scalar',
    setter   => 'request_class_deserialize',
    fallback => 'request_class',
}, {
    # This is a special case - can be overridden
    # but doesn't fall back to request_class
    accessor => 'request_class_eventprovider',
    default  => 'RPC::ExtDirect::Request::PollHandler',
}, {
    accessor => 'serializer_class',
    default  => 'RPC::ExtDirect::Serializer',
}, {
    setter   => 'serializer_class_api',
    fallback => 'serializer_class',
}, {
    package  => 'RPC::ExtDirect::Router',
    var      => 'SERIALIZER_CLASS',
    type     => 'scalar',
    setter   => 'serializer_class_router',
    fallback => 'serializer_class',
}, {
    package  => 'RPC::ExtDirect::EventProvider',
    var      => 'SERIALIZER_CLASS',
    type     => 'scalar',
    setter   => 'serializer_class_eventprovider',
    fallback => 'serializer_class',
}, {
    accessor => 'deserializer_class',
    default  => 'RPC::ExtDirect::Serializer',
}, {
    package  => 'RPC::ExtDirect::Router',
    var      => 'DESERIALIZER_CLASS',
    type     => 'scalar',
    setter   => 'deserializer_class_router',
    fallback => 'deserializer_class',
}, {
    accessor => 'json_options',
}, {
    setter   => 'json_options_serialize',
    fallback => 'json_options',
}, {
    package  => 'RPC::ExtDirect::Deserialize',
    var      => 'JSON_OPTIONS',
    type     => 'hash',
    setter   => 'json_options_deserialize',
    fallback => 'json_options',
}, {
    accessor => 'router_class',
    default  => 'RPC::ExtDirect::Router',
}, {
    accessor => 'timeout'
}, {
    accessor => 'max_retries'
}, {
    accessor => 'eventprovider_class',
    default  => 'RPC::ExtDirect::EventProvider',
}, {
    accessor => 'verbose_exceptions',
    default  => !1,  # In accordance with Ext.Direct spec
}, {
    accessor => 'api_path',
    default  => '/extdirectapi',
}, {
    accessor => 'router_path',
    default  => '/extdirectrouter',
}, {
    accessor => 'poll_path',
    default  => '/extdirectevents',
}, {
    accessor => 'remoting_var',
    default  => 'Ext.app.REMOTING_API',
}, {
    accessor => 'polling_var',
    default  => 'Ext.app.POLLING_API',
}, {
    accessor => 'namespace',
}, {
    accessor => 'auto_connect',
    default  => !1,
}, {
    accessor => 'no_polling',
    default  => !1,
}];

my @simple_accessors = map  { $_->{accessor} }
                       grep { $_->{accessor} }
                            @$DEFINITIONS;

my @complex_accessors = grep { $_->{fallback} } @$DEFINITIONS;

# Package globals are handled separately, this is only for
# accessors with default values
my %field_defaults = map  { $_->{accessor} => $_ }
                     grep { defined $_->{default} and !exists $_->{var} }
                          @$DEFINITIONS;

my @package_globals = grep { $_->{var} } @$DEFINITIONS;

### PRIVATE INSTANCE METHOD ###
#
# Return the default value for a field.
#

sub _get_default {
    my ($self, $field) = @_;
    
    my $def = $field_defaults{$field};
    
    return $def ? $def->{default} : undef;
}

### PRIVATE INSTANCE METHOD ###
#
# Return true if the current field value is the default.
#

sub _is_default {
    my ($self, $field) = @_;
    
    my $value   = $self->$field();
    my $default = $self->_get_default($field);
    
    return $value eq $default;
}

### PRIVATE INSTANCE METHOD ###
#
# Parse global package variables
#

sub _parse_global_vars {
    my ($self) = @_;
    
    parse_global_flags(\@package_globals, $self);
}

### PRIVATE INSTANCE METHOD ###
#
# Parse global package variables and apply default values
#

sub _init {
    my ($self) = @_;
    
    $self->_parse_global_vars();
    
    # Apply the defaults
    foreach my $field (keys %field_defaults) {
        my $def = $field_defaults{$field};
        my $default = $def->{default};
        
        $self->$field($default) unless defined $self->$field();
    }
}

### PRIVATE PACKAGE SUBROUTINE ###
#
# Export a deep copy of the definitions for testing
#

sub _get_definitions {
    return [ map { +{ %$_ } } @$DEFINITIONS ];
}

RPC::ExtDirect::Util::Accessor::mk_accessors(
    simple    => \@simple_accessors,
    complex   => \@complex_accessors,
    overwrite => 1,
);

1;