File Coverage

blib/lib/Net/Duo/Object.pm
Criterion Covered Total %
statement 125 133 93.9
branch 39 50 78.0
condition 4 5 80.0
subroutine 16 17 94.1
pod 5 5 100.0
total 189 210 90.0


line stmt bran cond sub pod time code
1             # Helper base class for Duo objects.
2             #
3             # The Duo API contains a variety of objects, represented as JSON objects with
4             # multiple fields. This objects often embed other objects inside them. To
5             # provide a nice Perl API with getters, setters, and commit and delete methods
6             # on individual objects, we want to wrap these Duo REST API objects in Perl
7             # classes.
8             #
9             # This module serves as a base class for such objects and does the dirty work
10             # of constructing an object from decoded JSON data and building the accessors
11             # automatically from a field specification.
12              
13             package Net::Duo::Object 1.01;
14              
15 6     6   3702 use 5.014;
  6         19  
16 6     6   29 use strict;
  6         9  
  6         144  
17 6     6   27 use warnings;
  6         10  
  6         151  
18              
19 6     6   29 use Carp qw(croak);
  6         25  
  6         317  
20 6     6   27 use JSON ();
  6         11  
  6         160  
21 6     6   4217 use Sub::Install;
  6         9896  
  6         26  
22              
23             # Helper function to parse the data for a particular field specification.
24             #
25             # $spec - The field specification (a value in the hash from _fields)
26             #
27             # Returns: The type in scalar context
28             # The type and then a reference to a hash of flags in array context
29             sub _field_type {
30 561     561   754 my ($spec) = @_;
31 561         637 my ($type, @flags);
32              
33             # If the specification is a reference, it's an array, with the first value
34             # as type and the rest as flags. Otherwise, it's a simple type.
35 561 100       1166 if (ref($spec) eq 'ARRAY') {
36 250         273 ($type, @flags) = @{$spec};
  250         594  
37             } else {
38 311         431 $type = $spec;
39             }
40              
41             # Return the appropriate value or values.
42 561 100       1635 return wantarray ? ($type, { map { $_ => 1 } @flags }) : $type;
  188         784  
43             }
44              
45             # Helper function to do the data translation from the results of JSON parsing
46             # to our internal representation. This mostly consists of converting nested
47             # objects into proper objects, but it also makes a deep copy of the data.
48             #
49             # This is broken into a separate function so that it can be used by both new()
50             # and commit().
51             #
52             # $self - Class of object we're creating or an object of the right type
53             # $data_ref - Reference to parsed data from JSON
54             # $duo - Net::Duo object to use for subobjects
55             #
56             # Returns: Reference to hash suitable for blessing as an object
57             sub _convert_data {
58 18     18   35 my ($self, $data_ref, $duo) = @_;
59              
60             # Retrieve the field specification for this object.
61 18         75 my $fields = $self->_fields;
62              
63             # Make a deep copy of the data following the field specification.
64 18         37 my %result;
65             FIELD:
66 18         23 for my $field (keys %{$fields}) {
  18         69  
67 185 100       438 next FIELD if (!exists($data_ref->{$field}));
68 135         258 my $type = _field_type($fields->{$field});
69 135         231 my $value = $data_ref->{$field};
70 135 100       259 if ($type eq 'simple') {
    100          
    50          
71 120         266 $result{$field} = $value;
72             } elsif ($type eq 'array') {
73 9         17 $result{$field} = [@{$value}];
  9         77  
74             } elsif (defined($value)) {
75 6         8 my @objects;
76 6         6 for my $object (@{$value}) {
  6         13  
77 9         54 push(@objects, $type->new($duo, $object));
78             }
79 6         20 $result{$field} = \@objects;
80             }
81             }
82              
83             # Return the new data structure.
84 18         98 return \%result;
85             }
86              
87             # Create a new Net::Duo object. This constructor can be inherited by all
88             # object classes. It takes the decoded JSON and uses the field specification
89             # for the object to construct an object via deep copying.
90             #
91             # The child class must provide a static method fields() that returns a field
92             # specification. See the documentation for more details.
93             #
94             # $class - Class of object to create
95             # $duo - Net::Duo object to use for further API calls on this object
96             # $data_ref - Object data as a reference to a hash (usually decoded from JSON)
97             #
98             # Returns: Newly-created object
99             sub new {
100 14     14 1 25 my ($class, $duo, $data_ref) = @_;
101 14         61 my $self = $class->_convert_data($data_ref, $duo);
102 14         30 $self->{_duo} = $duo;
103 14         21 bless($self, $class);
104 14         50 return $self;
105             }
106              
107             # Create a new object in Duo. This constructor must be overridden by
108             # subclasses to pass in the additional URI parameter for the Duo API endpoint.
109             # It takes a reference to a hash representing the object values and returns
110             # the new object as an appropriately-blessed object. Currently, no local data
111             # checking is performed on the provided data.
112             #
113             # The child class must provide a static method fields() that returns a field
114             # specification. See the documentation for more details.
115             #
116             # $class - Class of object to create
117             # $duo - Net::Duo object to use to create the object
118             # $uri - Duo endpoint to use for creation
119             # $data_ref - Data for new object as a reference to a hash
120             #
121             # Returns: Newly-created object
122             # Throws: Net::Duo::Exception on any problem creating the object
123             sub create {
124 5     5 1 16 my ($class, $duo, $uri, $data_ref) = @_;
125              
126             # Retrieve the field specification for this object.
127 5         23 my $fields = $class->_fields;
128              
129             # Make a copy of the data and convert all boolean values.
130 5         12 my %data = %{$data_ref};
  5         29  
131             FIELD:
132 5         20 for my $field (keys %data) {
133 22         76 my ($type, $flags) = _field_type($fields->{$field});
134 22 100       100 if ($flags->{boolean}) {
    100          
135 3 100       13 $data{$field} = $data{$field} ? 'true' : 'false';
136             } elsif ($flags->{zero_or_one}) {
137 3 100       12 $data{$field} = $data{$field} ? 1 : 0;
138             }
139             }
140              
141             # Create the object in Duo.
142 5         37 my $self = $duo->call_json('POST', $uri, \%data);
143              
144             # Add the Net::Duo object.
145 5         17 $self->{_duo} = $duo;
146              
147             # Bless and return the new object.
148 5         22 bless($self, $class);
149 5         48 return $self;
150             }
151              
152             # Commit changes to the object to Duo. This method must be overridden by
153             # subclasses to pass in the additional URI parameter for the Duo API endpoint.
154             # It sends all of the fields that have been modified by setters, and then
155             # clears the flags that track modifications if the commit was successful.
156             #
157             # The child class must provide a static method fields() that returns a field
158             # specification. See the documentation for more details.
159             #
160             # $self - Subclass of Net::Duo::Object
161             # $uri - Duo endpoint to use for updates
162             #
163             # Returns: undef
164             # Throws: Net::Duo::Exception on any problem modifying the object in Duo
165             sub commit {
166 4     4 1 9 my ($self, $uri) = @_;
167              
168             # Retrieve the field specification for this object.
169 4         15 my $fields = $self->_fields;
170              
171             # Iterate through the changed fields to build the data for Duo. Remap
172             # boolean fields to true or false here.
173 4         9 my %data;
174 4         6 for my $field (keys %{ $self->{_changed} }) {
  4         92  
175 14         35 my ($type, $flags) = _field_type($fields->{$field});
176 14 50       46 if ($flags->{boolean}) {
    50          
177 0 0       0 $data{$field} = $self->{$field} ? 'true' : 'false';
178             } elsif ($flags->{zero_or_one}) {
179 0 0       0 $data{$field} = $self->{$field} ? 1 : 0;
180             } else {
181 14         40 $data{$field} = $self->{$field};
182             }
183             }
184              
185             # Modify the object in Duo. Duo will return the resulting new object,
186             # which we want to convert back to our internal representation.
187 4         22 my $new_data_ref = $self->{_duo}->call_json('POST', $uri, \%data);
188 4         22 $new_data_ref = $self->_convert_data($new_data_ref, $self->{_duo});
189              
190             # Duo may have changed or canonicalized the data, or someone else may have
191             # changed other parts of the object, so replace all of our data with what
192             # Duo now says the object looks like. Save our private fields. This is
193             # more extensible than having a whitelist of private fields.
194 4         18 delete $self->{_changed};
195 4         8 for my $field (keys %{$self}) {
  4         15  
196 38 100       92 next if ($field !~ m{ \A _ }xms);
197 4         10 $new_data_ref->{$field} = $self->{$field};
198             }
199 4         10 %{$self} = %{$new_data_ref};
  4         32  
  4         14  
200 4         41 return;
201             }
202              
203             # Create all the accessor methods for the object fields. This method is
204             # normally called via code outside of any method in the object class so that
205             # it is run when the class is first imported.
206             #
207             # The child class must provide a static method fields() that returns a field
208             # specification. See the documentation for more details.
209             #
210             # $class - Class whose accessors we're initializing
211             #
212             # Returns: undef
213             sub install_accessors {
214 30     30 1 63 my ($class) = @_;
215              
216             # Retrieve the field specification for this object.
217 30         103 my $fields = $class->_fields;
218              
219             # Create an accessor for each one.
220 30         61 for my $field (keys %{$fields}) {
  30         143  
221 318         3404 my ($type, $flags) = _field_type($fields->{$field});
222              
223             # For fields containing arrays, return a copy of the array instead
224             # of the reference to the internal data structure in the object,
225             # preventing client manipulation of our internals.
226 318         480 my $code;
227 318 100       605 if ($type eq 'simple') {
228 270     199   808 $code = sub { my $self = shift; return $self->{$field} };
  199         67641  
  199         1003  
229             } else {
230             $code = sub {
231 30     30   5835 my $self = shift;
232 30 100       159 return if !$self->{$field};
233 19         28 return @{ $self->{$field} };
  19         135  
234 48         156 };
235             }
236              
237             # Create and install the accessor.
238 318         908 my $spec = { code => $code, into => $class, as => $field };
239 318         802 Sub::Install::install_sub($spec);
240              
241             # If the "set" flag is set, also generate a setter.
242 318 100       14718 if ($flags->{set}) {
243 72 50       159 if ($type eq 'simple') {
244             $code = sub {
245 14     14   3960 my ($self, $value) = @_;
246 14         29 $self->{$field} = $value;
247 14         31 $self->{_changed}{$field} = 1;
248 14         27 return;
249 72         244 };
250             } else {
251             $code = sub {
252 0     0   0 my ($self, @values) = @_;
253 0         0 $self->{$field} = [@values];
254 0         0 $self->{_changed}{$field} = 1;
255 0         0 return;
256 0         0 };
257             }
258 72         329 $spec = { code => $code, into => $class, as => "set_$field" };
259 72         244 Sub::Install::install_sub($spec);
260             }
261             }
262 30         447 return;
263             }
264              
265             # Returns the current contents of the object as JSON. The json() method of
266             # nested objects is called to convert them in turn.
267             #
268             # $self - The object to convert to JSON
269             #
270             # Returns: JSON representation of the object using the Duo data model
271             sub json {
272 11     11 1 746 my ($self) = @_;
273              
274             # Create a JSON encoder and decoder.
275 11         81 my $json = JSON->new->utf8(1);
276              
277             # Retrieve the field specification for this object.
278 11         48 my $fields = $self->_fields;
279              
280             # Iterate through the fields to build the data structure we'll convert to
281             # JSON. We have to do some data mapping and call the json() method on any
282             # embedded objects. This is unnecessarily inefficient since it converts
283             # the children to JSON and then back again, purely for coding convenience.
284 11         20 my %data;
285             FIELD:
286 11         30 for my $field (keys %{$self}) {
  11         106  
287 83 100       228 next FIELD if ($field =~ m{ \A _ }xms);
288 72         155 my ($type, $flags) = _field_type($fields->{$field});
289 72 100 100     225 if ($type eq 'simple' || $type eq 'array') {
290 69 100       173 if ($flags->{boolean}) {
    50          
291 3 100       51 $data{$field} = $self->{$field} ? JSON::true : JSON::false;
292             } elsif ($flags->{zero_or_one}) {
293 0 0       0 $data{$field} = $self->{$field} ? 1 : 0;
294             } else {
295 66         185 $data{$field} = $self->{$field};
296             }
297             } else {
298 3   50     4 my @children = map { $_->json } @{ $self->{$field} // [] };
  6         28  
  3         10  
299 3         6 $data{$field} = [map { $json->decode($_) } @children];
  6         48  
300             }
301             }
302              
303             # Convert the result to JSON and return it.
304 11         267 return $json->encode(\%data);
305             }
306              
307             1;
308             __END__