File Coverage

blib/lib/Object/PadX/Role/AutoMarshal.pm
Criterion Covered Total %
statement 35 36 97.2
branch 3 4 75.0
condition n/a
subroutine 11 12 91.6
pod n/a
total 49 52 94.2


line stmt bran cond sub pod time code
1             package Object::PadX::Role::AutoMarshal;
2              
3 3     3   605478 use v5.36;
  3         11  
4              
5 3     3   16 use Object::Pad ':experimental(custom_field_attr mop)';
  3         6  
  3         19  
6 3     3   1913 use Object::Pad::MOP::FieldAttr;
  3         483  
  3         121  
7 3     3   1377 use Object::Pad::MOP::Field;
  3         523  
  3         107  
8 3     3   15 use Object::Pad::MOP::Class;
  3         5  
  3         17  
9 3     3   1523 use Syntax::Operator::Equ qw/is_strequ/;
  3         17365  
  3         20  
10              
11             # ABSTRACT: Automated nested object creation with Object::Pad
12              
13             our $VERSION = '1.1';
14              
15             # TODO replace these with the real thing as part of the func flags when Object::Pad finally exposes them
16             my $_require_value = sub {
17             my ($field_meta, $value) = @_;
18              
19             die "Missing required attribute value" unless defined $value;
20             return $value;
21             };
22              
23             Object::Pad::MOP::FieldAttr->register( "MarshalTo", permit_hintkey => 'Object::PadX::Role::AutoMarshal', apply => $_require_value );
24              
25             sub import {
26 3     3   28 my @imports = @_;
27 3         21 $^H{'Object::PadX::Role::AutoMarshal'}=1;
28              
29 3 100       9 if (grep {$_ eq '-toplevel'} @imports) {
  4         287  
30 1     1   160 eval "use Object::Pad; use Object::PadX::Role::AutoMarshal; role AutoMarshal :does(Object::PadX::Role::AutoMarshal) {};";
  1     1   10  
  1         2  
  1         8  
  1         238  
  1         6  
  1         7  
31 1 50       123 die $@ if $@;
32             }
33             }
34 0     0   0 sub unimport { delete $^H{'Object::PadX::Role::AutoMarshal'};}
35              
36             role Object::PadX::Role::AutoMarshal {
37 3     3   2298 use Carp qw/croak/;
  3         14  
  3         252  
38 3     3   29 use experimental 'for_list';
  3         9  
  3         13  
39              
40             ADJUST {
41             my $class = __CLASS__;
42              
43             my $classmeta = Object::Pad::MOP::Class->for_class($class);
44             my @metafields = $classmeta->fields;
45              
46             for my $metafield (@metafields) {
47             my $field_name = $metafield->name;
48             my $sigil = $metafield->sigil;
49              
50             my $has_attr = $metafield->has_attribute("MarshalTo");
51              
52             # one of ours!
53             if ($metafield->has_attribute("MarshalTo")) {
54             my $value = $metafield->value($self);
55             my $newvalue;
56              
57             my $newclass = $metafield->get_attribute_value("MarshalTo");
58              
59             if (is_strequ($sigil, '$')) {
60             # TODO more advanced parser?
61             # :KeyValidator?
62             if ($newclass =~ /^\[(.*?)\]$/) {
63             $newclass = $1;
64              
65             my @list = map {$newclass->new($_->%*)} $value->@*;
66              
67             $newvalue = \@list;
68              
69             $metafield->value($self) = $newvalue;
70             } elsif ($newclass =~ /^\{(.*?)\}$/) {
71             $newclass = $1;
72              
73             my %hash = ();
74             for my ($k, $v) ($value->%*) {
75             $hash{$k} = $newclass->new($v->%*);
76             }
77             my $newvalue = \%hash;
78              
79             $metafield->value($self) = $newvalue;
80             } else {
81             $newvalue = $newclass->new($value->%*);
82              
83             $metafield->value($self) = $newvalue;
84             }
85             } elsif (is_strequ($sigil, '%')) {
86             my %hash = ();
87             for my ($k, $v) ($value->%*) {
88             $hash{$k} = $newclass->new($v->%*);
89             }
90             my $newvalue = \%hash;
91              
92             $metafield->value($self) = $newvalue;
93             } elsif (is_strequ($sigil,'@')) {
94             $newclass = $1;
95              
96             my @list = map {$newclass->new($_->%*)} $value->@*;
97             my $newvalue = \@list;
98              
99             $metafield->value($self) = $newvalue;
100             } else {
101             croak "Unable to handle field $class"."->$sigil$field_name";
102             }
103             }
104             }
105             }
106             }
107              
108             =pod
109              
110             =head1 NAME
111              
112             Object::PadX::Role::AutoMarshal - Object::Pad role that tries to automatically create sub-objects during instantiation.
113              
114             =head1 WARNING
115              
116             This module is using the currently experimental Object::Pad::MOP family of packages. They are subject to change due to the MOP being unfinished, and
117             thus this module may fail to work at some point in the future due to an update. This is currently tested with Object::Pad 0.806 released on 2023-11-14
118              
119             =head1 SYNOPSIS
120              
121             use Object::Pad;
122             use Object::PadX::Role::AutoMarshal;
123             use Cpanel::JSON::XS;
124              
125             class Pet {
126             field $name :param;
127             field $species :param = "Dog";
128             }
129              
130             class Person :does(Object::PadX::Role::AutoMarshal) {
131             field $internal_uuid :param;
132              
133             field $first_name :param;
134             field $middle_name :param = undef;
135             field $last_name :param;
136             field $age :param;
137              
138             field $is_alive :param;
139              
140             field $pets :param :MarshalTo([Pet]) = undef;
141             }
142              
143             my $person = Person->new(
144             internal_uuid => "defe205e-833f-11ee-b962-0242ac120002",
145             first_name => "Phillip",
146             last_name => "Fry",
147             age => 3049,
148             is_alive => 1,
149             pets => [
150             {
151             name => "Spot",
152             species => "Dalmation",
153             },
154             {
155             name => "Belle",
156             species => "Bloodhound",
157             }
158             ],
159             );
160              
161             # Now pets is a set of Pet objects
162              
163             =head1 DESCRIPTION
164              
165             This role adds an ADJUST sub that reads the MarshalTo attributes to try to instantiate new objects with the listed class.
166             It doesn't require that the subobjects to be made with Object::Pad but it does require the constructor to be expecting all
167             parameters as a hash, not a hashref or positional arguments.
168              
169             =head2 CAVEATS
170              
171             =over 4
172              
173             =item * This module is VERY opinionated. All constructors of sub-objects must be expecting a hash as their only input.
174              
175             =item * It only handles fields at object creation time. Assignment later does not get considered, so you can overwrite the field with a different type/class.
176              
177             =item * IT DOES NOT CHECK TYPES. Do not use this module if you are expecting type checking.
178              
179             =item * It relies on experimental APIs and will likely break.
180              
181             =back
182              
183             =head2 IMPORTS
184              
185             use Object::PadX::Role::AutoMarshal '-toplevel';
186              
187             class Foo :does(AutoMarshal) {
188             ...
189             }
190              
191             This is the only import right now, it creates a top level namespace role AutoJSON for lazy people (like me).
192             This is a bad idea, don't do it it pollutes this globally since there is no such thing as lexical role imports.
193              
194             =head2 ATTRIBUTES
195              
196             =over 4
197              
198             =item * :MarshalTo(ClassName)
199              
200             Set the type of object to be instantiated during object creation. It'll get called as C<< ClassName->new($field_value->%*) >>, expecting the field to have been
201             set with a hashref on the original ->new call to your class.
202              
203             =item * :MarshalTo([ClassName])
204              
205             Create this as an array ref of ClassName objects. It'll iterate through the field value as an array ref and call C<< ClassName->new($element->%*) >>.
206             All elements of the array are expected to be hash-refs that will be dereferenced for creating the subobjects.
207              
208             =item * :MarshalTo({ClassName})
209              
210             Create this as a hash ref of ClassName objects. It'll iterate through the field value as an hash setting each C<$key> and call C<< ClassName->new($value->%*) >> for each value.
211             All elements of the top level hash-ref are expected to be hash-refs that will be dereferenced for creating the subobjects.
212              
213             =back
214              
215             =head1 TRICKS
216              
217             Since this doesn't actually require the sub-objects to be an Object::Pad class, you can pull some tricks by using a package that just "looks right" to handle more esoteric cases.
218              
219             use Object::PadX::Role::AutoMarshal;
220              
221             class Thing::Vehicle::Car {
222             field $name :param;
223             ...
224             }
225              
226             class Thing::Vehicle::Truck {
227             field $name :param;
228             ...
229             }
230              
231             package Thing::VehicleFactory {
232             sub new {
233             my ($class, %params) = @_;
234              
235             my $type = delete $params{type};
236              
237             if ($type eq "car") {
238             return Thing::Vehicle::Car->new(%params);
239             } elsif ($type eq "truck") {
240             return Thing::Vehicle::Truck->new(%params);
241             } else {
242             die "Unhandled vehicle $type";
243             }
244             }
245             }
246              
247             class Person :does(Object::PadX::Role::AutoMarshal) {
248             field $name :param;
249             field $vehicles :param :MarshalTo([Thing::VehicleFactory]) = undef;
250             }
251              
252             my $peoples = Person->new(
253             name => "Phillip J Fry",
254             vehicles => [
255             {
256             type => "car",
257             name => "Something meaningful here",
258             },
259             {
260             type => "truck",
261             name => "Fry doesn't own a truck!",
262             }
263             ]
264             );
265              
266             Now when you create a Person object and give it a bunch of vehicles, the Thing::VehicleFactory class will take care of creating the correct object types based on the contents of each element.
267              
268             =head1 BUGS
269              
270             No known bugs at this time, if any are found contact simcop2387 on IRC, or email simcop2387 at simcop2387.info
271              
272             =head1 LICENSE
273              
274             This module is available under the Artistic 2.0 License
275              
276             =head1 SEE ALSO
277              
278             L, L
279              
280             =head1 AUTHOR
281              
282             Ryan Voots, L, aka simcop2387
283              
284             =cut
285