The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Class::Accessor::TrackDirty;
use 5.008_001;
use strict;
use warnings;
use List::MoreUtils qw(any);
use Storable qw(dclone freeze);
our $VERSION = '0.10';

our $RESERVED_FIELD = '_original';
our $NEW = 'new';
our $FROM_HASH = 'from_hash';
our $RAW = 'raw';
our $TO_HASH = 'to_hash';
our $IS_MODIFIED = 'is_dirty';
our $MODIFIED_FIELDS = 'dirty_fields';
our $IS_NEW = 'is_new';
our $REVERT = 'revert';

{
    my %package_info;
    sub _package_info($) {
        my $package = shift;
        $package_info{$package} ||= {tracked_fields => {}, fields => {}};
    }
}

sub _is_different_deeply($$) {
    my ($ref_x, $ref_y) = @_;
    (freeze $ref_x) ne (freeze $ref_y);
}

sub _is_different($$) {
    my ($x, $y) = @_;
    if (defined $x && defined $y) {
        if (ref $x && ref $y) {
            return _is_different_deeply $x, $y;
        } else {
            return ref $x || ref $y || $x ne $y;
        }
    } else {
        return defined $x || defined $y;
    }
}

sub _make_tracked_accessor($$) {
    no strict 'refs';
    my ($package, $name) = @_;

    *{"$package\::$name"} = sub {
        my $self = shift;

        # getter
        my $value;
        if (exists $self->{$name}) {
            $value = $self->{$name};
        } elsif (defined $self->{$RESERVED_FIELD})  {
            $value = $self->{$RESERVED_FIELD}{$name};

            # Defensive copying
            $value = ($self->{$name} = dclone $value) if ref $value;
        }

        # setter
        $self->{$name} = $_[0] if @_;

        return $value;
    };
}

sub _make_accessor($$) {
    no strict 'refs';
    my ($package, $name) = @_;

    *{"$package\::$name"} = sub {
        my $self = shift;
        my $value = $self->{$name};
        $self->{$name} = $_[0] if @_;
        $value;
    };
}

sub _mk_tracked_accessors($@) {
    my $package = shift;
    _make_tracked_accessor $package => $_ for @_;
    @{(_package_info $package)->{tracked_fields}}{@_} = (1,) x @_;
}

sub _mk_helpers($) {
    no strict 'refs';
    my $package = shift;
    my ($tracked_fields, $fields) =
        @{_package_info $package}{qw(tracked_fields fields)};

    # cleate helper methods
    *{"$package\::$FROM_HASH"} = sub {
        my $package = shift;
        my %modified = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;

        my %origin;
        for my $name (keys %$tracked_fields) {
            $origin{$name} = delete $modified{$name} if exists $modified{$name};
        }

        $modified{$RESERVED_FIELD} = \%origin;
        bless \%modified, $package;
    };

    *{"$package\::$RAW"} = sub {
        my ($self) = @_;

        my %hash = (
            (map {
                # Don't store undefined values.
                my $v = $self->$_;
                defined $v ? ($_ => $v) : ();
            } keys %$tracked_fields, keys %$fields),
        );

        return \%hash;
    };

    *{"$package\::$TO_HASH"} = sub {
        my ($self) = @_;
        my $raw = $self->$RAW;

        # Move published data for cleaning.
        $self->{$RESERVED_FIELD} ||= {};
        $self->{$RESERVED_FIELD}{$_} = delete $self->{$_}
                         for grep { exists $self->{$_} } keys %$tracked_fields;

        return $raw;
    };

    *{"$package\::$IS_MODIFIED"} = sub {
        my ($self, $field) = @_;
        return any { $self->$IS_MODIFIED($_) } keys %$tracked_fields
                                                                 unless $field;

        return unless $tracked_fields->{$field};
        return defined $self->{$field} unless defined $self->{$RESERVED_FIELD};

        exists $self->{$field} &&
               _is_different $self->{$field}, $self->{$RESERVED_FIELD}{$field};
    };

    *{"$package\::$MODIFIED_FIELDS"} = sub {
        my $self = shift;
        grep { $self->$IS_MODIFIED($_) } keys %$tracked_fields;
    };

    *{"$package\::$IS_NEW"} = sub {
        my $self = shift;
        exists $self->{$RESERVED_FIELD} ? 0 : 1;
    };

    *{"$package\::$REVERT"} = sub {
        my $self = shift;
        delete $self->{$_} for keys %$tracked_fields;
    };
}

sub _mk_accessors($@) {
    my $package = shift;
    _make_accessor $package => $_ for @_;
    @{(_package_info $package)->{fields}}{@_} = (1,) x @_;
}

sub _mk_new($) {
    no strict 'refs';
    my $package = shift;

    *{"$package\::$NEW"} = sub {
        my $package = shift;
        my %modified = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;

        bless \%modified => $package;
    };
}

sub mk_tracked_accessors {
    (undef, my @tracked_fields) = @_;
    my $package = caller(0);
    _mk_tracked_accessors $package => @tracked_fields;
    _mk_helpers $package;
}

sub mk_accessors {
    (undef, my @fields) = @_;
    my $package = caller(0);
    _mk_accessors $package => @fields;
}

sub mk_new {
    my $package = caller(0);
    _mk_new $package;
}

sub mk_new_and_tracked_accessors {
    (undef, my @tracked_fields) = @_;
    my $package = caller(0);
    _mk_tracked_accessors $package => @tracked_fields;
    _mk_helpers $package;
    _mk_new $package;
}

1;
__END__

=encoding utf-8

=head1 NAME

Class::Accessor::TrackDirty - Define simple entities stored in some places.

=head1 SYNOPSIS

    package UserInfo;
    use Class::Accessor::TrackDirty;
    Class::Accessor::TrackDirty->mk_new_and_tracked_accessors("name", "password");
    Class::Accessor::TrackDirty->mk_accessors("modified");

    package main;
    my $user = UserInfo->new({name => 'honma', password => 'F!aS3l'});
    store_into_someplace($user->to_hash) if $user->is_dirty;
    # ...
    $user = UserInfo->from_hash(restore_from_someplace());
    $user->name('hiratara');
    $user->revert; # but decided not to
    $user->name('honma');
    $user->name('hiratara');
    $user->name('honma'); # I can't make up my mind...
    # ... blabla ...

    # Check the status of fields if needed
    $user->is_dirty('name') and warn "Did you change name?";
    my @dirty_fields = $user->dirty_fields;

    # Store it only if $user was really modified.
    store_into_someplace($user->to_hash) if $user->is_dirty;

=head1 DESCRIPTION

Class::Accessor::TrackDirty defines simple entities stored in files, RDBMS,
KVS, and so on. It tracks dirty columns and you can store it only when the
instance was really modified.

=head1 INTERFACE

=head2 Functions

=head3 C<< Class::Accessor::TrackDirty->mk_new; >>

Create the C<<new>> methods in your class.
You can pass a hash-ref or hash-like list to C<<new>> method.

=over 4

=item C<< my $object = YourClass->new({name1 => "value1", ...}); >>

The instance created by C<<new>> is regarded as `dirty' if it has some nonempty
fields. It's because it hasn't been stored yet.

=back

=head3 C<< Class::Accessor::TrackDirty->mk_tracked_accessors("name1", "name2", ...); >>

Create accessor methods and helper methods in your class.
Following helper methods will be created automatically.

=over 4

=item C<< $your_object->is_dirty; >>
=item C<< $your_object->is_dirty("field_name"); >>

Check that the instance is modified. If it's true, you should store this
instance into some place through using C<<to_hash>> method.

When you pass the name of a field, you can know if the field contains the same
value as the stored object.

=item C<< my @fields = $your_object->dirty_fields; >>

Gets the name of all dirty fields of C<$your_object>.

=item C<< $your_object->is_new; >>

Checks if the instance might be in a storage. Returns false value when
the instance comes from C<from_hash> method, or after you call
C<to_hash> method.

=item C<< my $hash_ref = $your_object->to_hash; >>

Eject data from this instance as plain hash-ref format.
C<<$your_object>> is regarded as `clean' after calling this method.

You'd better store C<<$hash_ref>> into some place ASAP. It's up to you how
C<<$hash_ref>> should be serialized.

=item C<< $your_object->raw; >>

Retrieves the row data from the instance. The return value is the same as
C<to_hash> method, but this method doesn't change the state of the
instance.

=item C<< my $object = YourClass->from_hash({name1 => "value1", ...}); >>

Rebuild the instance from a hash-ref ejected by C<<to_hash>> method.
The instance constructed by C<<from_hash>> is regarded as `clean'.

=item C<< $your_object->revert; >>

Revert all `dirty' changes. Fields created by C<<mk_tracked_accessors>> returns to
the point where you call C<<new>>, C<<to_hash>>, or C<<from_hash>>.

The volatile fields will be never reverted.

=back

You'd better *NOT* store references in tracked fields. Though following codes
work well, to make C<revert> work well, we'll have to copy references deeply
when you call getter.

  my $your_object = YourClass->new(some_refs => {key => 'value'});
  # some_refs are copyied deeply :(
  $your_object->some_refs->{key} = '<censored>';

  $your_object->revert;
  print $your_object->some_refs, "\n"; # printed "value"

=head3 C<< Class::Accessor::TrackDirty->mk_accessors("name1", "name2", ...); >>

Define the field which isn't tracked. You can freely change these fields,
and it will never be marked as `dirty'.

=head3 C<< Class::Accessor::TrackDirty->mk_new_and_tracked_accessors("name1", "name2", ...); >>

This method is a combination of C<<mk_tracked_accessors>> and C<<mk_new>>.

=head1 SEE ALSO

L<Class::Accessor>, L<Class::Accessor::Lite>, L<MooseX::TrackDirty::Attributes>, L<Hash::Dirty>

=head1 AUTHOR

Masahiro Honma E<lt>hiratara@cpan.orgE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2013, Masahiro Honma. All rights reserved.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut