The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Net::Journyx::Record;
use Moose;

extends 'Net::Journyx::Object';

with 'Net::Journyx::Object::Loadable' => {
    check_on => [qw(update delete)],
    drop_on => [qw(load_from_hash create delete)],
};

use Data::Dumper;

use constant jx_strip_record_suffix => 0;
use constant jx_meta => {};

# class data implementation via inside out
my %jx_columns = ();
has jx_columns => (
    is       => 'ro',
    init_arg => undef,
    isa      => 'ArrayRef[Str]',
    lazy     => 1,
    default  => sub { return $jx_columns{ ref(shift) } || [] },
);
sub _jx_columns { $jx_columns{ ref($_[0]) || $_[0] } = $_[1] }

my %record_template = ();
has record_template => (
    is       => 'ro',
    init_arg => undef,
    isa      => 'HashRef',
    lazy     => 1,
    default  => sub { return $record_template{ ref($_[0]) } ||= $_[0]->_record_template },
);

sub BUILD {
    my $self = shift;
    my $args = shift;

    return if $self->meta->is_immutable;

    my $jx = $args->{'jx'} or die "No JX";

    my %columns = $jx->soap->record_columns( $self->jx_record_class );

    my %type_map = (
        'xsd:boolean' => 'Bool',
        'xsd:int'     => 'Int',
        'xsd:double'  => 'Math::BigFloat',
        'xsd:string'  => 'Str',
    );
    while (my ($name, $schema_type) = each %columns ) {
        my $moose_type = $type_map{ $schema_type }
            or die "No moose type for JX type '$schema_type'";

        #warn "adding column accessor $name to ". ref($self);
        $self->meta->add_attribute(
            $name,
            is => 'ro',
            isa => $moose_type,
            writer => '_'. $name,
        );
    }

    $self->_jx_columns([keys %columns]);

    $self->meta->make_immutable;
}

sub load {
    my $self = shift;
    my %args = @_;

    my $jx_operation = $self->auto_method(
        $self->jx_meta->{'load'},
        'get'
    );

    my $response = $self->jx->soap->basic_call(
        $jx_operation => pattern => \%args
    );
#    warn "loaded $jx_class ". Dumper($response) ." using ". Dumper(\%args);

    my $jx_class = $self->jx_record_class;

    my $record = $response->{ $jx_class };
    unless ( $record ) {
        die "No '$jx_class' in response ". Dumper($response);
    }

    return $self->load_from_hash( %$record );
}

sub load_from_hash {
    my $self = shift;
    my %args = @_;

    foreach my $k ( keys %args ) {
        if ( $k =~ /^_+(.*)$/ && !exists $args{ $1 } ) {
            $args{ $1 } = delete $args{ $k };
        }
    }

    my @missing;
    foreach my $column ( @{ $self->jx_columns } ) {
        my $method = '_'. $column;
        if ( exists $args{ $column } ) {
            $self->$method( delete $args{ $column } );
        } else {
            push @missing, $column;
            # flush as it can be not empty
            $self->$method( undef );
        }
    }

    my @more = keys %args;
    if ( @missing || @more ) {
        my $msg = "Either during load or load_from_hash of ". $self->jx_record_class
            ." incorrect columns have been provided to load_from_hash.";
        $msg .= " Missing columns: ". join(', ', map "'$_'", @missing) ."."
            if @missing;
        $msg .= " Superfluous columns: ". join(', ', map "'$_'", @more) ."."
            if @more;
        $msg .= " It can be either API bug or incorrect call to load_from_hash."
            ." Record *is not* marked as loaded.";
        warn $msg;
    }
    elsif ( $self->can('id') ) {
        # XXX: load may pass us empty record with all defaults
        $self->_is_loaded(1) if $self->id;
    }
    else {
        $self->_is_loaded(1);
    }

    return $self;
}

sub create {
    my $self = shift;
    my %args = @_;

    my %private_args = map { $_ => delete $args{"__$_"} }
                       grep { s/^__// } # filter and strip leading __
                       keys %args;

    my $jx_class = $self->jx_record_class;
    my $jx_meta = $self->jx_meta->{'create'};

    if ( my $quick = $jx_meta->{'quick'}
        and join(',', sort keys %args) eq join(',', sort @{$jx_meta->{'quick'}{'columns'}}) 
    ) {
        my $response = $self->jx->soap->basic_call(
            $quick->{'operation'},
            map { $quick->{'rewrite'} || $_ => $args{$_} }
                @{ $quick->{'columns'} }
        );

        # load self to get defaults
        return $self->load( id => $response );
    }

    my $defaults = $self->record_template;
    $args{$_} = $defaults->{$_} foreach
        grep !exists $args{$_}, keys %$defaults;

    $jx_meta->{'defaults'}->( $self, \%args )
        if $jx_meta->{'defaults'};

    my $jx_operation = $self->auto_method( $jx_meta->{'operation'}, 'addFull' );
    my $response = $self->jx->soap->basic_call( $jx_operation, rec => \%args );
    #warn "created $jx_class #$response using ". Dumper( \%args );

    return $response if $private_args{do_not_load};

    # load self to get defaults
    return $self->load( id => $response );
}

sub update {
    my $self = shift;
    my %args = @_;

    my $jx_class = $self->jx_record_class;
    my $jx_meta = $self->jx_meta->{'update'};

    my $leading_column = $jx_meta->{'leading'} || 'id';

    my %op_args = ();
    foreach my $col ( @{ $self->jx_columns } ) {
        if ( $col eq $leading_column ) {
            if ( exists $args{ $col } ) {
                my $val = $args{ $col };
                if ( defined $val && length $val && $val eq $self->$col() ) {
                    die "Can not update column '$col' of $jx_class #". $self->$col();
                } else {
                    # just ignore without complain
                    delete $args{ $col };
                }
            }
            # we must pass leading column anyway
            $op_args{ $col } = $self->$col();
        }
        elsif ( exists $args{ $col } ) {
            $op_args{ $col } = delete $args{ $col };
        } else {
            $op_args{ $col } = $self->$col();
        }
    }

    if ( keys %args ) {
        warn "Ignored ". join( ', ', map "'$_'", keys %args )
            ." arguments on update of $jx_class #". $self->$leading_column()
            ." as these are not columns of the record";
    }

    my $jx_operation = $self->auto_method( $jx_meta->{'operation'}, 'modify' );
    my $response = $self->jx->soap->basic_call(
        $jx_operation,
        $jx_meta->{'leading_argname'} || $leading_column => $self->$leading_column(),
        $jx_meta->{'record_argname'} || 'rec' => \%op_args,
    );

    # reload self just in case Journyx may update values on update
    return $self->load( $leading_column => $op_args{ $leading_column } );
}

sub delete {
    my $self = shift;

    my $jx_class = $self->jx_record_class;
    my $jx_meta = $self->jx_meta->{'delete'};
    my $jx_operation = $self->auto_method( $jx_meta->{'operation'}, 'remove' );
    my $leading_column =  $jx_meta->{'leading'} || 'id';
    my $leading_argname =  $jx_meta->{'leading_argname'} || 'id';

    my $response = $self->jx->soap->basic_call(
        $jx_operation, $leading_argname => $self->$leading_column(),
    );

    return $self;
}

sub list {
    die "not implemented"
}


sub attribute {
    my $self = shift;
    my $attr = shift;
    die "Can get attribute of not loaded record";
}

sub _record_template {
    my $self = shift;

    my $jx_class = $self->jx_record_class;
    my $jx_operation = $self->auto_method(
        $self->jx_meta->{'defaults'},
        'getDefault'
    );

    my $response = $self->jx->soap->basic_call( $jx_operation )
        or die "Couldn't get default field values for $jx_class";

    my $record = $response->{$jx_class};
    unless ( $record && ref($record) eq 'HASH' ) {
        die "Couldn't get default field values for $jx_class";
    }
        
    return $record;
}

sub auto_method {
    my $self = shift;
    my ($val, $prefix, $suffix) = @_;
    return $val if $val;

    my $jx_class = $self->jx_record_class;
    $jx_class =~ s/Record$//
        if $self->jx_strip_record_suffix;

    return join '', grep defined && length,
        $prefix, $jx_class, $suffix;
}

no Moose;

1;