File Coverage

blib/lib/Catmandu/Plugin/Versioning.pm
Criterion Covered Total %
statement 52 60 86.6
branch 8 16 50.0
condition 3 7 42.8
subroutine 17 18 94.4
pod 5 5 100.0
total 85 106 80.1


line stmt bran cond sub pod time code
1             package Catmandu::Plugin::Versioning;
2              
3 2     2   1515 use Catmandu::Sane;
  2         6  
  2         12  
4              
5             our $VERSION = '1.2020';
6              
7 2     2   21 use Catmandu::Util qw(is_value is_array_ref check_value check_positive);
  2         13  
  2         134  
8 2     2   14 use Data::Compare;
  2         5  
  2         23  
9 2     2   8998 use Moo::Role;
  2         4  
  2         16  
10 2     2   811 use MooX::Aliases;
  2         5  
  2         20  
11 2     2   798 use namespace::clean;
  2         4  
  2         15  
12              
13             has version_bag_name => (is => 'lazy', init_arg => 'version_bag');
14             has version_bag => (is => 'lazy', init_arg => undef);
15             has version_key => (is => 'lazy', alias => 'version_field');
16              
17             has version_compare_ignore => (
18             is => 'lazy',
19             coerce => sub {
20             my $keys = $_[0];
21             $keys = [@$keys] if is_array_ref $keys;
22             $keys = [split /,/, $keys] if is_value $keys;
23             $keys;
24             },
25             );
26              
27             has version_transfer => (
28             is => 'lazy',
29             coerce => sub {
30             my $keys = $_[0];
31             $keys = [@$keys] if is_array_ref $keys;
32             $keys = [split /,/, $keys] if is_value $keys;
33             $keys;
34             },
35             );
36              
37             sub _build_version_bag_name {
38 2     2   30 $_[0]->name . '_version';
39             }
40              
41             sub _build_version_bag {
42 3     3   110 $_[0]->store->bag($_[0]->version_bag_name);
43             }
44              
45             sub _build_version_key {
46 4     4   71 $_[0]->store->key_for('version');
47             }
48              
49             sub _build_version_compare_ignore {
50 2     2   65 [$_[0]->version_key];
51             }
52              
53             sub _trigger_version_compare_ignore {
54 0     0   0 my ($self, $keys) = @_;
55 0         0 my $version_key = $self->version_key;
56 0 0       0 push @$keys, $version_key unless grep /^$version_key$/, @$keys;
57             }
58              
59             sub _build_version_transfer {
60 3     3   58 [];
61             }
62              
63             sub _version_id {
64 19     19   40 my ($self, $id, $version) = @_;
65 19         58 "$id.$version";
66             }
67              
68             around add => sub {
69             my ($sub, $self, $data) = @_;
70             my $id_key = $self->id_key;
71             my $version_key = $self->version_key;
72             if (defined $data->{$id_key} and my $d = $self->get($data->{$id_key})) {
73             $data->{$version_key} = $d->{$version_key} ||= 1;
74             for my $key (@{$self->version_transfer}) {
75             next if exists $data->{$key} || !exists $d->{$key};
76             $data->{$key} = $d->{$key};
77             }
78             return $data
79             if Compare($data, $d,
80             {ignore_hash_keys => $self->version_compare_ignore});
81             my $version_id
82             = $self->_version_id($data->{$id_key}, $data->{$version_key});
83             $self->version_bag->add(
84             {$self->version_bag->id_key => $version_id, data => $d});
85             $data->{$version_key}++;
86             }
87             else {
88             $data->{$version_key} ||= 1;
89             }
90             $sub->($self, $data);
91             };
92              
93             sub get_history {
94 3     3 1 11 my ($self, $id, %opts) = @_;
95 3 50       59 if (my $data = $self->get($id)) {
96 3         8 my $history = [$data];
97 3   50     53 my $version = $data->{$self->version_key} || 1;
98 3         29 while (--$version) {
99 3         12 push @$history, $self->get_version($id, $version);
100             }
101 3         15 return $history;
102             }
103 0         0 return;
104             }
105              
106             sub get_version {
107 12     12 1 31 my ($self, $id, $version) = @_;
108 12         37 check_value($id);
109 12         316 check_positive($version);
110 12         45 my $data;
111 12         37 my $version_id = $self->_version_id($id, $version);
112 12 100       191 if ($data = $self->version_bag->get($version_id)) {
113 9         68 return $data->{data};
114             }
115 3 50 33     102 if ($data = $self->get($id) and $data->{$self->version_key} == $version) {
116 3         59 return $data;
117             }
118 0         0 return;
119             }
120              
121             sub restore_version {
122 1     1 1 6 my ($self, $id, $version) = @_;
123 1 50       4 if (my $data = $self->get_version($id, $version)) {
124 1         23 return $self->add($data);
125             }
126 0         0 return;
127             }
128              
129             sub get_previous_version {
130 3     3 1 14 my ($self, $id) = @_;
131 3 50       62 if (my $data = $self->get($id)) {
132 3   50     54 my $version = $data->{$self->version_key} || 1;
133 3 50       33 if ($version > 1) {
134 3         11 return $self->get_version($id, $version - 1);
135             }
136             }
137 0         0 return;
138             }
139              
140             sub restore_previous_version {
141 1     1 1 5 my ($self, $id) = @_;
142 1 50       4 if (my $data = $self->get_previous_version($id)) {
143 1         19 return $self->add($data);
144             }
145 0           return;
146             }
147              
148             1;
149              
150             __END__
151              
152             =pod
153              
154             =head1 NAME
155              
156             Catmandu::Plugin::Versioning - Automatically adds versioning to Catmandu::Store records
157              
158             =head1 SYNOPSIS
159              
160             # Using configuration files
161              
162             $ cat catmandu.yml
163             ---
164             store:
165             test:
166             package: MongoDB
167             options:
168             database_name: test
169             bags:
170             data:
171             plugins:
172             - Versioning
173              
174             # Add two version of record 001 to the store
175             $ echo '{"_id":"001",hello":"world"}' | catmandu import JSON to test
176             $ echo '{"_id":"001",hello":"world2"}' | catmandu import JSON to test
177              
178             # In the store we see only the latest version
179             $ catmandu export test to YAML
180             ---
181             _id: '001'
182             _version: 2
183             hello: world2
184              
185             # In the '_version' store we'll find all the previous versions
186             $ catmandu export test --bag data_version to YAML
187             ---
188             _id: '001.1'
189             data:
190             _id: '001'
191             _version: 1
192             hello: world
193              
194             # Or in your Perl program
195             my $store = Catmandu->store('MongoDB',
196             database_name => 'test' ,
197             bags => {
198             data => {
199             plugins => [qw(Versioning)]
200             }
201             });
202              
203             $store->bag->add({ _id => '001' , hello => 'world'});
204             $store->bag->add({ _id => '001' , hello => 'world2'});
205              
206             print "Versions:\n";
207              
208             for (@{$store->bag->get_history('001')}) {
209             print Dumper($_);
210             }
211              
212             =head1 DESCRIPTION
213              
214             The Catmandu::Plugin::Versioning plugin automatically adds a new 'version' bag to your Catmandu::Store
215             containing previous versions of newly created records. The name of the version is created by appending
216             '_version' to your original bag name. E.g. when add the Versioning plugin to a 'test' bag then 'test_version'
217             will contain the previous version of all your records.
218              
219             When using Catmandu::Store-s that don't have dynamic schema's (e.g. Solr , DBI) these new bags need to be
220             predefined (e.g. create new Solr cores or database tables).
221              
222             =head1 CONFIGURATION
223              
224             =over
225              
226             =item version_compare_ignore
227              
228             By default every change to a record with trigger the creation of a new version. Use the version_compare_ignore option
229             to specify fields that should be ignored when testing for new updates. E.g. in the example below we configured the
230             MongoDB store to add versioning to the default 'data' bag. We want to ignore changes to the 'date_updated' field
231             when creating new version records
232              
233             # catmandu.yml
234             ---
235             store:
236             test:
237             package: MongoDB
238             options:
239             database_name: test
240             bags:
241             data:
242             plugins:
243             - Versioning
244             version_compare_ignore:
245             - date_updated
246              
247             # In your perl
248              
249             # First version
250             $store->bag->add({ _id => '001' , name => 'test' , date_updated => '10:00' });
251              
252             # Second version (name has changed)
253             $store->bag->add({ _id => '001' , name => 'test123' , date_updated => '10:00' });
254              
255             # Second version (date_updated has changed but we ignored that in our configuration)
256             $store->bag->add({ _id => '001' , name => 'test123' , date_updated => '10:15' });
257              
258             =item version_transfer
259              
260             This option autmatically copies the configured fields from the previous version of a record to the new version of the
261             record. E.g. in the example below we will create a versioning on the default bag and add a rights statement that can
262             not be deleted.
263              
264             # catmandu.yml
265             ---
266             store:
267             test:
268             package: MongoDB
269             options:
270             database_name: test
271             bags:
272             data:
273             plugins:
274             - Versioning
275             version_transfer:
276             - rights:
277              
278             # In your perl
279              
280             # First version
281             $store->bag->add({ _id => '001' , name => 'test' , rights => 'Acme Corp.' });
282              
283             # Second version we will try you delete rights but this is copied to the new version
284             $store->bag->add({ _id => '001' , name => 'test'});
285              
286             print "Rights: %s\n" , $store->bag->get('001')->{rights}; # Rights: Acme Corp.
287              
288             =item version_bag
289              
290             The name of the bag that stores the versions. Default is the name of the
291             versioned bag with '_version' appended.
292              
293             my $store = Catmandu::Store::MyDB->new(bags => {book => {plugins =>
294             ['Versioning'], version_bag => 'book_history'}});
295             $store->bag('book')->version_bag->name # returns 'book_history'
296              
297             =item version_key
298              
299             Use a custom key to hold the version number in this bag. Default is '_version'
300             unless the store has a custom C<key_prefix>. Also aliased as C<version_field>.
301              
302             =back
303              
304             =head1 METHODS
305              
306             Every bag that is configured with the Catmandu::Plugin::Versioning plugin can use the following methods:
307              
308             =head2 get_version(ID,VERSION)
309              
310             Retrieve a record with identifier ID and version identifier VERSION. E.g.
311              
312             my $obj = $store->bag('test')->get_version('001',1);
313              
314             =head2 get_previous_version(ID)
315              
316             Retrieve the previous version of a record with identifier ID. E.g.
317              
318             =head2 get_history(ID)
319              
320             Returns an ARRAY reference with all the versions of the record with identifier ID.
321              
322             =head2 restore_version(ID,VERSION)
323              
324             Overwrites the current version of the stored record with identifier ID with a version with identifier VERSION.
325              
326             =head2 restore_previous_version(ID)
327              
328             Overwrites the current version of the stored record with identifier ID with its previous version.
329              
330             =head1 SEE ALSO
331              
332             L<Catmandu::Store>, L<Catmandu::Bag>
333              
334             =cut