File Coverage

blib/lib/Model/Envoy.pm
Criterion Covered Total %
statement 137 155 88.3
branch 53 66 80.3
condition 11 18 61.1
subroutine 32 37 86.4
pod 9 9 100.0
total 242 285 84.9


line stmt bran cond sub pod time code
1             package Model::Envoy;
2              
3 13     13   4971043 use MooseX::Role::Parameterized;
  13         1284124  
  13         90  
4 13     13   382802 use Module::Runtime 'use_module';
  13         40  
  13         106  
5 13     13   9182 use List::AllUtils qw( first first_result );
  13         137455  
  13         24591  
6              
7             our $VERSION = '0.5.3';
8              
9             =head1 Model::Envoy
10              
11             A Moose Role that can be used to build a model layer which keeps business logic separate from your storage layer.
12              
13             =head2 Synopsis
14              
15             package My::Model::Widget;
16              
17             use Moose;
18             with 'Model::Envoy' => { storage => {
19             'DBIC' => {
20             schema => sub {
21             my ( $class ) = @_;
22             My::DB->db_connect(...);
23             }
24             },
25             };
26              
27             sub dbic { 'My::DB::Result::Widget' }
28              
29              
30             has 'id' => (
31             is => 'ro',
32             isa => 'Num',
33             traits => ['Envoy','DBIC'],
34             primary_key => 1,
35              
36             );
37              
38             has 'name' => (
39             is => 'rw',
40             isa => 'Maybe[Str]',
41             traits => ['Envoy','DBIC'],
42             );
43              
44             has 'no_storage' => (
45             is => 'rw',
46             isa => 'Maybe[Str]',
47             traits => ['Envoy'],
48             );
49              
50             has 'parts' => (
51             is => 'rw',
52             isa => 'ArrayRef[My::Model::Part]',
53             traits => ['Envoy','DBIC'],
54             rel => 'has_many',
55             );
56              
57             has 'envoy_ignores_me' => (
58             is => 'rw',
59             isa => 'Str',
60             );
61              
62             package My::Models;
63              
64             use Moose;
65             with 'Model::Envoy::Set' => { namespace => 'My::Envoy' };
66              
67              
68             ....then later...
69              
70             my $widget = My::Models->m('Widget')->build({
71             id => 1
72             name => 'foo',
73             no_storage => 'bar',
74             parts => [
75             {
76             id => 2,
77             name => 'baz',
78             },
79             ],
80             envoy_ignores_me => 'hi there',
81             });
82              
83             $widget->name('renamed');
84             $widget->save;
85              
86             Mixing database logic with business rules is a common hazard when building an application's model layer. Beyond
87             the violation of the ideal separation of concerns, it also ties a developer's hands when needing to transition
88             between different storage mechanisms, or support more than one.
89              
90             Model::Envoy provides an Moose-based object layer to manage business logic, and a plugin system to add one or more
91             persistence methods under the hood.
92              
93             =head2 Setting up storage
94              
95             Indicating which storage back ends you are using for your models is done when you include the role. It makes the most sense to
96             do this in a base class which your models can inherit from:
97              
98             package My::Model;
99              
100             use Moose;
101             use My::DB;
102              
103             my $schema;
104              
105             with 'Model::Envoy' => { storage => {
106             'DBIC' => {
107             schema => sub {
108             my ( $class ) = @_;
109             $schema ||= My::DB->db_connect(...);
110             }
111             },
112             ...
113             } };
114            
115             # then....
116              
117             package My::Model::Widget;
118              
119             use Moose;
120              
121             extends 'My::Model'
122              
123             ...
124              
125             =head2 Model attributes
126              
127             Model::Envoy classes use normal Moose attribute declarations. Depending on the storage layer plugin, they may add attribute traits or other methods
128             your models need to implement to indicate how each attribute finds its way into and out of storage. All attributes that you want Model::Envoy to manage
129             do need to have at least this one trait specified:
130              
131             =head3 The 'Envoy' trait
132              
133             This trait indicates an attribute is part of your model's data, rather than being a secondary or utility attribute (such as a handle to a logging utility).
134             It also provides some automatic coercion for class attributes that represent other Model::Envoy
135             enabled classes (or arrays of them). It will allow you to pass in hashrefs
136             (or arrays of hashrefs) to those attributes and have them automagically elevated
137             into an instance of the intended class.
138              
139             has 'parts' => (
140             is => 'rw',
141             isa => 'ArrayRef[My::Model::Part]',
142             traits => ['Envoy'],
143             );
144              
145             =head2 Adding caching
146              
147             The role inclusion can also specify a cache plugin. These operate just like storage plugins, but are checked before your storage plugins on fetches,
148             and updated with results from your storage plugins on a cache miss.
149              
150             with 'Model::Envoy' => {
151             storage => {
152             'DBIC' => { ... },
153             },
154             cache => {
155             'Memory' => { ... }
156             }
157             };
158              
159             =head2 Class Methods
160              
161             =head3 build()
162              
163             C<build> is a more flexible version of C<new>. It can take a standard hashref of properties just as C<new> would, but it can also take
164             classes that are used by your storage layer plugins and, if those plugins support this, convert them into an instance of your model object.
165              
166             =head3 get_storage('Plugin')
167              
168             Passes back the storage plugin specified by C<Plugin> being used by the class. Follows the same namespace resolution
169             process as the instance method below.
170              
171             =head3 get_cache('Plugin')
172              
173             Passes back the cache plugin specified by C<Plugin> being used by the class. Follows the same namespace resolution
174             process as the C<get_storage> instance method below.
175              
176             =head2 Instance Methods
177              
178             =head3 save()
179              
180             Save the instance to your persistent storage layer.
181              
182             =head3 update($hashref)
183              
184             Given a supplied hashref of attributes, bulk update the attributes of your instance object.
185              
186             =head3 delete()
187              
188             Remove the instance from your persistent storage layer.
189              
190             =head3 dump()
191              
192             Provide an unblessed copy of the datastructure of your instance object. If any attributes are an instance of a Model::Envoy-using class (or an array of same),
193             they will be recursively dumped. Other blessed objects will return undef unless they have a C<stringify> or C<to_string> method, in which case those will be
194             used to represent the object.
195              
196             =head3 get_storage('Plugin')
197              
198             Given the namespace of a storage plugin, return the instance of it that's backing the current object. If the plugin is in the
199             C<Model::Envoy::Storage::> namespace, it can be abbreviated:
200              
201             $model->get_storage('DBIC') # looks for Model::Envoy::Storage::DBIC
202              
203             otherwise, prefix your plugin name with a C<+> to get something outside of the default namespace:
204              
205             $model->get_storage('+My::Storage::WhatsIt');
206              
207             =head3 get_cache('Plugin')
208              
209             Works just like C<get_storage> but looks for a cache plugin instead of a storage plugin
210              
211             =head3 in_storage('Plugin')
212              
213             Returns true if the storage plugin reports your model is saved in its storage mechanism.
214              
215             =head3 in_cache('Plugin')
216              
217             Returns true if the cache plugin reports your model is saved in its storage mechanism.
218              
219             =head2 Aggregate methods
220              
221             For operations that fetch and search for one or more models, see C<Model::Envoy::Set>.
222              
223             =cut
224              
225             parameter storage => (
226             isa => 'HashRef',
227             required => 1,
228             );
229              
230             parameter cache => (
231             isa => 'HashRef',
232             required => 0,
233             );
234              
235             my $abs_module_prefix = qr/^\+/;
236              
237             role {
238             my $p = shift;
239              
240             my %plugins;
241             my @stores = qw( storage cache );
242              
243             for my $store ( @stores ) {
244              
245             if ( my $storage = $p->$store ) {
246              
247             while ( my ( $package, $conf ) = each %$storage ) {
248              
249             my $role = _resolve_namespace($package);
250              
251             use_module( $role );
252              
253             $plugins{$store}{$role} = $conf;
254             }
255             }
256             }
257              
258             has '_storage' => (
259             isa => 'HashRef',
260             is => 'ro',
261             default => sub {
262              
263             my $self = shift;
264             my $instances = {};
265              
266             for my $store ( @stores ) {
267             next unless $plugins{$store};
268             $instances->{$store} = { map { $_ => undef } keys %{$plugins{$store}} };
269             }
270              
271             return $instances;
272             },
273             );
274              
275             method '_plugins' => sub {
276              
277 337     337   1017 \%plugins;
278             };
279              
280             };
281              
282             sub _resolve_namespace {
283 149     149   3062 my ( $namespace ) = @_;
284              
285 149         516 $namespace =~ s/^Model::Envoy::Storage:://;
286              
287             return $namespace =~ $abs_module_prefix
288 149 100       892 ? do { $namespace =~ s/$abs_module_prefix//; $namespace }
  2         12  
  2         10  
289             : 'Model::Envoy::Storage::' . $namespace;
290             }
291              
292             sub get_storage {
293 65     65 1 4716 my ( $self, $package ) = @_;
294              
295 65 100       198 if ( ! ref $self ) {
296 2         36 return $self->_get_configured_plugin_class('storage', $package);
297             }
298             else {
299 63         208 return $self->_plugin_instance('storage',$package);
300             }
301             }
302              
303             sub get_cache {
304 3     3 1 4761 my ( $self, $package ) = @_;
305              
306 3 50       11 if ( ! ref $self ) {
307 0         0 return $self->_get_configured_plugin_class('cache', $package);
308             }
309             else {
310 3         10 return $self->_plugin_instance('cache',$package);
311             }
312             }
313              
314             sub in_storage {
315 13     13 1 3302 my ( $self, $package ) = @_;
316              
317 13 50       34 if( my $storage = $self->get_storage($package) ) {
318              
319 12         46 return $storage->in_storage;
320             }
321              
322 0         0 die "model does not use '$package' to persist data";
323             }
324              
325             sub in_cache {
326 9     9 1 851 my ( $self, $package ) = @_;
327              
328 9 50       21 if( my $storage = $self->get_storage($package) ) {
329              
330 9         40 return $storage->in_storage;
331             }
332              
333 0         0 die "model does not use '$package' to persist data";
334             }
335              
336             sub build {
337 35     35 1 2770 my( $class, $params, $no_rel ) = @_;
338              
339 35 100 33     529 if ( ! ref $params ) {
    100          
    50          
    50          
    50          
340              
341 1 50       7 return undef unless defined $params;
342              
343 0         0 die "Cannot build a ". $class ." from '$params'";
344             }
345             elsif( ref $params eq 'HASH' ) {
346 6         70 return $class->new(%$params);
347             }
348             elsif( ref $params eq 'ARRAY' ) {
349 0         0 die "Cannot build a ". $class ." from an Array Ref";
350             }
351             elsif( blessed $params && $params->isa( $class ) ) {
352 0         0 return $params;
353             }
354             elsif( my $model = $class->_dispatch('build', $params, $no_rel ) ) {
355 28         286 return $model;
356             }
357             else {
358 0         0 die "Cannot coerce a " . ( ref $params ) . " into a " . $class;
359             }
360             }
361              
362             sub save {
363 7     7 1 9549 my $self = shift;
364              
365 7         53 $self->_dispatch('save', @_ );
366              
367 7         23 return $self;
368             }
369              
370             sub update {
371 15     15 1 9587 my ( $self, $hashref ) = @_;
372              
373 15         124 foreach my $attr ( grep { $_->get_write_method } $self->_get_all_attributes ) {
  45         4276  
374              
375 30         5526 my $name = $attr->name;
376              
377 30 100       111 if ( exists $hashref->{$name} ) {
378              
379 24         601 $self->$name( $hashref->{$name} );
380             }
381             }
382              
383 15         535 return $self;
384             }
385              
386              
387             sub delete {
388 2     2 1 8316 my ( $self ) = @_;
389              
390 2         10 $self->_dispatch('delete', @_ );
391              
392 2         7 return 1;
393             }
394              
395             sub dump {
396 34     34 1 29730 my ( $self ) = @_;
397              
398             return {
399 85         2002 map { $_ => $self->_dump_property( $self->$_ ) }
400 103         2700 grep { defined $self->$_ }
401 34         133 map { $_->name }
  103         8674  
402             $self->_get_all_attributes
403             };
404             }
405              
406              
407             sub _dump_property {
408 92     92   631 my ( $self, $value ) = @_;
409              
410 92 100       669 return $value if ! ref $value;
411              
412 24 100       128 return [ map { $self->_dump_property($_) } @$value ] if ref $value eq 'ARRAY';
  7         45  
413              
414 12 50       43 return { map { $_ => $self->_dump_property( $value->{$_} ) } keys %$value } if ref $value eq 'HASH';
  0         0  
415              
416 12 100 66     96 return $value->dump if $value->can('does') && $value->does('Model::Envoy');
417              
418 5 100   14   1258 if ( my $method = first { $value->can($_) } qw( stringify to_string as_string to_str ) ) {
  14         65  
419              
420 4         17 return $value->$method;
421             }
422              
423 1         5 return undef;
424             }
425              
426             sub _get_all_attributes {
427 50     50   5449 my ( $self ) = @_;
428              
429 50         206 return grep { $_->does('Envoy') } $self->meta->get_all_attributes;
  220         43709  
430             }
431              
432             sub _dispatch {
433 56     56   197 my ( $self, $method, @params ) = @_;
434              
435 56 100       269 return $self->_class_dispatch($method,@params) unless ref $self;
436              
437 9         44 for my $store ( qw( storage cache ) ) {
438 18 100       630 next unless $self->_storage->{$store};
439              
440 12         59 for my $package ( keys %{$self->_storage->{$store}} ) {
  12         302  
441 15         72 $self->_plugin_instance($store,$package)->$method();
442             }
443             }
444             }
445              
446             sub _class_dispatch {
447 47     47   112 my ( $self, $method, @params ) = @_;
448              
449             my $result =
450 6     6   16 first_result { $self->_get_configured_plugin_class('cache',$_)->$method( $self, @params ) }
451 47         225 keys %{$self->_plugins->{cache}};
  47         182  
452              
453 47 100       2717 return $result if $result;
454              
455             $result =
456 44     44   123 first_result { $self->_get_configured_plugin_class('storage',$_)->$method( $self, @params ) }
457 44         155 keys %{$self->_plugins->{storage}};
  44         103  
458              
459 42 50 66     6768 if ( blessed $result && $result->can('does') && $result->does('Model::Envoy') ) {
      66        
460 33         11869 for my $plugin ( keys %{$self->_plugins->{cache}} ) {
  33         115  
461 2         7 $result->get_cache($plugin)->save();
462             }
463              
464             }
465              
466 42         367 return $result;
467             }
468              
469             sub _plugin_instance {
470 81     81   187 my ( $self, $store, $package ) = @_;
471              
472 81         237 $package = $self->_get_configured_plugin_class($store,$package);
473 80         187 my $conf = $self->_plugins->{$store}{$package};
474              
475             $self->_storage->{$store}{$package} = $package->new( %$conf, model => $self )
476 80 100       2307 unless $self->_storage->{$store}{$package};
477              
478 80         2017 return $self->_storage->{$store}{$package};
479             }
480              
481             sub _get_configured_plugin_class {
482 133     133   267 my ( $self, $store, $package ) = @_;
483              
484 133         298 $package = _resolve_namespace($package);
485              
486 133         407 my $conf = $self->_plugins->{$store}{$package};
487 133 100       372 if ( ! $conf->{_configured} ) {
488 24         162 $package->configure($self,$conf);
489             }
490              
491 132         534 return $package;
492             }
493              
494             package Model::Envoy::Meta::Attribute::Trait::Envoy;
495 13     13   137 use Moose::Role;
  13         26  
  13         148  
496             Moose::Util::meta_attribute_alias('Envoy');
497              
498 13     13   72516 use Moose::Util::TypeConstraints;
  13         86  
  13         123  
499 13     13   29335 use List::AllUtils 'any';
  13         43  
  13         12808  
500              
501             has moose_class => (
502             is => 'ro',
503             isa => 'Str',
504             );
505              
506             around '_process_options' => sub { _install_types(@_) };
507              
508             sub _install_types {
509 64     64   238 my ( $orig, $self, $name, $options ) = @_;
510              
511 64 50       106 return unless grep { $_ eq __PACKAGE__ } @{$options->{traits} || []};
  112 50       400  
  64         261  
512              
513 64 50       220 unless( find_type_constraint('Array_of_Hashref') ) {
514              
515 64         3999 subtype 'Array_of_HashRef',
516             as 'ArrayRef[HashRef]';
517             }
518 64 100       232134 unless ( find_type_constraint('Array_of_Object') ) {
519              
520 12         675 subtype 'Array_of_Object',
521             as 'ArrayRef[Object]';
522             }
523 64 100       81400 if ( $options->{isa} =~ / ArrayRef \[ (.+?) \]/x ) {
    100          
    50          
524 9         41 $options->{moose_class} = $1;
525 9         66 $options->{isa} = $self->_coerce_array($1);
526             }
527             elsif( $options->{isa} =~ / Maybe \[ (.+?) \]/x ) {
528 31         122 $options->{moose_class} = $1;
529 31         139 $options->{isa} = $self->_coerce_maybe($1);
530             }
531             elsif( ! find_type_constraint( $options->{isa} ) ) {
532 0         0 $self->_coerce_class($options->{isa});
533             }
534             else {
535 24         2374 return $self->$orig($name,$options);
536             }
537              
538 40 100 66 78   361 $options->{coerce} = 1 unless $options->{moose_class} && any { $options->{moose_class} eq $_ } qw( HashRef ArrayRef );
  78         347  
539              
540 40         286 return $self->$orig($name,$options);
541             }
542              
543             sub _coerce_array {
544 9     9   38 my ( $self, $class ) = @_;
545              
546 9         65 my $type = ( $class =~ / ( [^:]+ ) $ /x )[0];
547              
548 9 100       48 unless( find_type_constraint("Array_of_$type") ) {
549              
550 7         399 subtype "Array_of_$type",
551             as "ArrayRef[$class]";
552              
553             coerce "Array_of_$type",
554             from "Array_of_Object",
555 0     0   0 via { [ map { $class->build($_) } @{$_} ] },
  0         0  
  0         0  
556              
557             from 'Array_of_HashRef',
558 7     2   62843 via { [ map { $class->new($_) } @{$_} ] };
  2         1199  
  3         1455  
  2         8  
559             }
560              
561 9         7257 return "Array_of_$type";
562             }
563              
564             sub _coerce_maybe {
565 31     31   115 my ( $self, $class ) = @_;
566 31   66     247 my $type = ( $class =~ /\:\:([^:]+)$/ )[0] || $class;
567              
568 31 100       140 unless( find_type_constraint("Maybe_$type") ) {
569              
570 17         950 subtype "Maybe_$type",
571             as "Maybe[$class]";
572              
573             coerce "Maybe_$type",
574             from 'Object',
575 5     5   343 via { $class->build($_) },
576              
577             from 'HashRef',
578 17     0   132519 via { $class->new($_) };
  0         0  
579             }
580              
581 31         18640 return "Maybe_$type";
582             }
583              
584             sub _coerce_class {
585 0     0     my ( $self, $class ) = @_;
586 0           my $type = ( $class =~ /\:\:([^:]+)$/ )[0];
587              
588             coerce $class,
589             from 'Object',
590 0     0     via { $class->build($_) },
591              
592             from 'HashRef',
593 0     0     via { $class->new($_) };
  0            
594             }
595              
596             package Moose::Meta::Attribute::Custom::Trait::Envoy;
597             sub register_implementation {
598             'MooseX::Meta::Attribute::Trait::Envoy'
599             };
600              
601             1;