File Coverage

blib/lib/Stancer/Core/Object.pm
Criterion Covered Total %
statement 216 216 100.0
branch 93 96 96.8
condition 26 27 96.3
subroutine 39 39 100.0
pod 12 13 92.3
total 386 391 98.7


line stmt bran cond sub pod time code
1             package Stancer::Core::Object;
2              
3 36     36   933389 use 5.020;
  36         175  
4 36     36   246 use strict;
  36         108  
  36         963  
5 36     36   175 use warnings;
  36         73  
  36         3046  
6              
7             # ABSTRACT: Basic API object
8             our $VERSION = '1.0.3'; # VERSION
9              
10 36     36   2658 use Stancer::Core::Types qw(coerce_datetime ArrayRef Bool Char HashRef InstanceOf Int Maybe Str);
  36         84  
  36         4197  
11              
12 36     36   283 use Carp;
  36         81  
  36         2849  
13 36     36   10864 use Stancer::Config;
  36         186  
  36         1766  
14 36     36   25376 use Stancer::Core::Request;
  36         156  
  36         1795  
15 36     36   280 use List::MoreUtils qw(any first_index);
  36         89  
  36         523  
16 36     36   40170 use Log::Any qw($log);
  36         92  
  36         241  
17 36     36   8475 use JSON;
  36         113  
  36         302  
18 36     36   6024 use Scalar::Util qw(blessed);
  36         89  
  36         2580  
19 36     36   253 use Storable qw(dclone);
  36         87  
  36         2032  
20              
21 36     36   236 use Moo;
  36         76  
  36         251  
22 36     36   20557 use namespace::clean;
  36         99  
  36         225  
23              
24              
25             around BUILDARGS => sub {
26             my ($orig, $class, @args) = @_;
27              
28             if (scalar @args == 1 && !ref $args[0]) {
29             return { id => $args[0] } if defined $args[0];
30             return {};
31             }
32              
33             my $data;
34              
35             if (ref $args[0] eq 'HASH') {
36             $data = $args[0];
37             } else {
38             $data = {@args};
39             }
40              
41             foreach my $key (keys %{$data}) {
42             delete $data->{$key} unless defined $data->{$key};
43             }
44              
45             return $class->$orig($data);
46             };
47              
48              
49             sub BUILD {
50 496     496 0 19281 my ($this, $args) = @_;
51              
52             # Force modified list
53 496         1011 for my $key (keys %{$args}) {
  496         2300  
54 812 100 100     4023 $this->_add_modified($key) if $key ne 'id' && $key ne 'created';
55             }
56              
57 496         9925 return $this;
58             }
59              
60             has _api_data => (
61             is => 'rw',
62             isa => HashRef,
63             );
64              
65              
66             sub _attribute_builder {
67 718     718   3253 my ($this, $attr) = @_;
68 718         1809 my $has = 'has_' . $attr;
69              
70 718 100       2590 if ($this->populate()->$has()){
71 38         921 return $this->$attr;
72             }
73              
74 680         11715 return undef;
75             }
76              
77              
78             has _boolean => (
79             is => 'ro',
80             isa => ArrayRef[Str],
81             default => sub{ [] },
82             );
83              
84              
85             has _date_only => (
86             is => 'ro',
87             isa => ArrayRef[Str],
88             default => sub{ [] },
89             );
90              
91              
92             has _inner_objects => (
93             is => 'ro',
94             isa => ArrayRef[Str],
95             default => sub{ [] },
96             );
97              
98              
99             has _integer => (
100             is => 'ro',
101             isa => ArrayRef[Str],
102             default => sub{ [] },
103             );
104              
105              
106             has _json_ignore => (
107             is => 'ro',
108             isa => ArrayRef[Str],
109             default => sub{ [qw(endpoint created populated)] },
110             );
111              
112              
113             has _modified => (
114             is => 'rwp',
115             isa => HashRef[Int],
116             default => sub{ return {} },
117             );
118              
119             sub _add_modified {
120 1555     1555   15154 my ($this, $name) = @_;
121              
122 1555 50       6781 $this->_set__modified({}) unless defined $this->_modified; # I don't know why but I sometimes get undef
123 1555         4832 $this->_modified->{$name} = 1;
124              
125 1555         12828 return $this;
126             }
127              
128             sub _reset_modified { ## no critic (RequireFinalReturn)
129 139     139   77509 my $this = shift;
130              
131 139         4270 $this->_set__modified({});
132              
133 139         4935 for my $attr (@{$this->_inner_objects}) {
  139         871  
134 98 100       2701 $this->$attr->_reset_modified() if defined $this->$attr;
135             }
136             }
137              
138              
139             has _process_hydratation => (
140             is => 'rwp',
141             isa => Bool,
142             default => 0,
143             writer => '_set_process_hydratation',
144             );
145              
146              
147             has id => (
148             is => 'rwp',
149             isa => Char[29],
150             clearer => 1,
151             predicate => 1,
152             );
153              
154              
155             has created => (
156             is => 'rwp',
157             isa => Maybe[InstanceOf['DateTime']],
158 74     74   8747 builder => sub { $_[0]->_attribute_builder('created') },
159             coerce => coerce_datetime(),
160             lazy => 1,
161             predicate => 1,
162             );
163              
164              
165             has endpoint => (
166             is => 'ro',
167             isa => Str,
168             default => q//,
169             );
170              
171              
172             sub is_modified {
173 113     113 1 1265 my $this = shift;
174 113         267 my $is_modified = scalar keys %{$this->_modified} > 0;
  113         601  
175              
176 113 100       630 return $is_modified if $is_modified;
177              
178 64         142 for my $attr (@{$this->_inner_objects}) {
  64         390  
179 62 100 100     2183 return 1 if defined $this->$attr && $this->$attr->is_modified;
180             }
181              
182 62         878 return !1;
183             }
184              
185             sub is_not_modified {
186 89     89 1 62784 my $this = shift;
187              
188 89         479 return !$this->is_modified;
189             }
190              
191              
192             has populated => (
193             is => 'rwp',
194             isa => Bool,
195             default => 0,
196             writer => '_set__populated',
197             );
198              
199             sub _set_populated {
200 81     81   770 my ($this, $value) = @_;
201              
202 81         3461 $this->_set__populated($value);
203              
204 81         2914 for my $attr (@{$this->_inner_objects}) {
  81         813  
205 98 100       4517 $this->$attr->_set_populated($value) if defined $this->$attr;
206             }
207              
208 81         1235 return $this;
209             }
210              
211              
212             sub uri {
213 144     144 1 53598 my $this = shift;
214 144         1804 my $config = Stancer::Config->init();
215 144         885 my @args = (
216             $config->uri,
217             );
218              
219 144 100       10398 if ($this->endpoint) {
220 121         508 push @args, $this->endpoint;
221             }
222              
223 144 100       812 if ($this->id) {
224 69         261 push @args, $this->id;
225             }
226              
227 144         1576 return join q!/!, @args;
228             }
229              
230              
231             sub del {
232 3     3 1 2128 my $this = shift;
233              
234 3 100       26 return $this unless defined $this->id;
235              
236 2         26 my $data = Stancer::Core::Request->new->del($this);
237              
238 2 100       219 if ($data) {
239 1         18 $this->hydrate(decode_json $data);
240             }
241              
242 2         15 my @parts = split m/::/sm, ref $this;
243 2         5 my $class = $parts[-1];
244              
245 2         30 $log->info(sprintf '%s %s deleted', $class, $this->id);
246              
247 2         197 $this->clear_id;
248              
249 2         31 return $this;
250             }
251              
252              
253             sub get {
254 22     22 1 5802 my ($this, $target) = @_;
255              
256 22 100       892 return undef unless defined $this->_api_data;
257 15 100       430 return dclone($this->_api_data) unless defined $target;
258              
259 9         209 my $data = $this->_api_data->{$target};
260              
261 9 100       266 return dclone($data) if ref $data ne q//;
262 6         41 return $data;
263             }
264              
265              
266             sub hydrate {
267 140     140 1 36881 my ($this, @args) = @_;
268 140         319 my $data;
269              
270 140 100       614 if (scalar @args == 1) {
271 100         274 $data = $args[0];
272             } else {
273 40         254 $data = {@args};
274             }
275              
276 140         4545 $this->_set_process_hydratation(1);
277              
278 140         4802 foreach my $key (keys %{$data}) {
  140         887  
279 971 100       16596 next if not defined $data->{$key};
280              
281 837         2054 my $setter = '_set_' . $key;
282              
283 837 100       3733 if (JSON::is_bool($data->{$key})) {
284 83         860 my $tmp = $data->{$key};
285              
286 83         1188 $data->{$key} = "$tmp";
287 83 50       854 $data->{$key} = 1 if "$tmp" eq 'true';
288 83 50       641 $data->{$key} = 0 if "$tmp" eq 'false';
289             }
290              
291 837 100 100     30594 if ($this->can($key) && blessed($this->$key) && $this->$key->can('hydrate')) {
    100 100        
    100          
292 20 100       1022 if (ref $data->{$key} eq 'HASH') {
293 15         308 $this->$key->hydrate($data->{$key});
294             } else {
295 5         134 $this->$key->hydrate(id => $data->{$key});
296             }
297             } elsif ($this->can($setter)) {
298 396         22933 $this->$setter($data->{$key});
299             } elsif ($this->can($key)) {
300 351         20586 $this->$key($data->{$key});
301             }
302             }
303              
304 140         6217 $this->_set_process_hydratation(0);
305              
306 140         4651 return $this;
307             }
308              
309              
310             sub populate {
311 741     741 1 8183 my $this = shift;
312              
313 741 100 100     9712 return $this if !$this->id || $this->populated || !$this->endpoint;
      100        
314              
315 46         719 my $request = Stancer::Core::Request->new();
316 46         1754 my $data = $request->get($this);
317              
318 44         4740 $this->_set_populated(1);
319              
320 44 100       177 if ($data) {
321 42         1467 my $decoded = decode_json $data;
322              
323 42         1339 $this->_api_data($decoded);
324 42         1655 $this->hydrate($decoded);
325              
326 42         120 for my $attr (@{$this->_inner_objects}) {
  42         316  
327 48 100 66     927 if (defined $this->{$attr} && defined $decoded->{$attr}) {
328 17         674 $this->{$attr}->_api_data($decoded->{$attr});
329             }
330             }
331             }
332              
333 44         307 $this->_reset_modified();
334              
335 44         546 return $this;
336             }
337              
338              
339             sub save {
340 1     1 1 599 my $this = shift;
341              
342 1         25 carp '"save" method is deprecated and will be removed in a later release, use the "send" method instead';
343              
344 1         787 return $this->send();
345             }
346              
347              
348             sub send { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
349 30     30 1 20556 my $this = shift;
350              
351 30 100       356 return $this if $this->is_not_modified;
352              
353 25         393 my $request = Stancer::Core::Request->new();
354 25         626 my $data;
355             my $verb;
356              
357 25 100       176 if (defined $this->id) {
358 5         27 $data = $request->patch($this);
359 5         494 $verb = 'updated';
360             } else {
361 20         280 $data = $request->post($this);
362 20         2220 $verb = 'created';
363             }
364              
365 25         210 $this->_set_populated(1);
366              
367 25 100       99 if ($data) {
368 24         1289 $this->hydrate(decode_json $data);
369             }
370              
371 25         321 $this->_reset_modified();
372              
373 25         262 my @parts = split m/::/sm, ref $this;
374 25         114 my $class = $parts[-1];
375              
376 25         642 $log->info(sprintf '%s %s %s', $class, $this->id, $verb);
377              
378 25         2177 return $this;
379             }
380              
381              
382             ## no critic (Capitalization)
383             sub toJSON {
384 194     194 1 408293 my $this = shift;
385              
386 194         4749 return JSON->new->convert_blessed->canonical->encode($this);
387             }
388             ## use critic
389              
390              
391             sub to_hash {
392 9     9 1 973 my $this = shift;
393 9         25 my $attrs = {};
394 9         22 my @properties = keys %{$this->populate()};
  9         34  
395              
396 9         68 foreach my $attr (sort @properties) {
397 111 100   306   383 next if any { $_ eq $attr } qw(endpoint populated refunds); # remove ignored attributes
  306         553  
398 93 100       320 next if $attr =~ m/^_/sm; # remove private attributes
399              
400 29 100 100     758 if (blessed($this->$attr) && $this->$attr->isa(__PACKAGE__)) {
401 3         201 $attrs->{$attr} = $this->$attr->to_hash;
402             } else {
403 26         873 $attrs->{$attr} = $this->$attr;
404             }
405              
406 29 100   54   217 if (any { $_ eq $attr } @{$this->_boolean}) { # Parse boolean
  54         124  
  29         111  
407 8         176 my $tmp = $this->$attr;
408              
409 8 100       67 $attrs->{$attr} = \1 if "$tmp" eq '1';
410 8 100       31 $attrs->{$attr} = \0 if "$tmp" eq '0';
411             }
412              
413 29 100   54   104 if (any { $_ eq $attr } @{$this->_integer}) { # Force integer
  54         161  
  29         87  
414 8         27 $attrs->{$attr} *= 1;
415             }
416             }
417              
418 9         72 return $attrs;
419             }
420              
421              
422             sub TO_JSON {
423 261     261 1 17304 my $this = shift;
424 261         1241 my $attrs = {};
425 261         484 my @properties = keys %{$this};
  261         2118  
426              
427 261 100       1608 if ($this->id) {
428 28         72 @properties = keys %{$this->_modified};
  28         206  
429              
430 28 100       168 return $this->id() if $this->is_not_modified;
431             }
432              
433 253         1800 foreach my $attr (sort @properties) {
434 2669 100   11461   10708 next if any { $_ eq $attr } @{$this->_json_ignore}; # remove ignored attributes
  11461         18776  
  2669         7409  
435 2109 100       7416 next if $attr =~ m/^_/sm; # remove private attributes
436 478 100       1523 next if $attr eq 'id';
437 477 100       16175 next unless defined $this->$attr;
438              
439 447         11973 $attrs->{$attr} = $this->$attr;
440              
441 447 100   379   4556 if (any { $_ eq $attr } @{$this->_boolean}) { # Parse boolean
  379         977  
  447         2199  
442 15         316 my $tmp = $this->$attr;
443              
444 15 100       157 $attrs->{$attr} = \1 if "$tmp" eq '1';
445 15 100       115 $attrs->{$attr} = \0 if "$tmp" eq '0';
446             }
447              
448 447 100   318   1721 if (any { $_ eq $attr } @{$this->_integer}) { # Force integer
  318         657  
  447         1776  
449 51         145 $attrs->{$attr} *= 1;
450             }
451              
452 447 100 100     9527 if (defined blessed($this->$attr) && blessed($this->$attr) eq 'DateTime') {
453 15 100   14   543 if (any { $_ eq $attr } @{$this->_date_only}) { # Force date only
  14         74  
  15         85  
454 9         159 $attrs->{$attr} = $this->$attr->ymd();
455             } else {
456 6         145 $attrs->{$attr} = $this->$attr->epoch();
457             }
458             }
459             }
460              
461 253         5803 return $attrs;
462             }
463              
464             1;
465              
466             __END__
467              
468             =pod
469              
470             =encoding UTF-8
471              
472             =head1 NAME
473              
474             Stancer::Core::Object - Basic API object
475              
476             =head1 VERSION
477              
478             version 1.0.3
479              
480             =head1 DESCRIPTION
481              
482             You should not use this class directly.
483              
484             This module is an internal class, regouping method for every API object.
485              
486             =head1 ATTRIBUTES
487              
488             =head2 C<id>
489              
490             Read-only 29 characters string.
491              
492             Current object identifier.
493              
494             =head2 C<created>
495              
496             Read-only instance of C<DateTime>.
497              
498             A DateTime object representing the creation date.
499              
500             Value is only present for object returned by the API, so the method can return C<undef>.
501              
502             =head2 C<endpoint>
503              
504             Read-only string.
505              
506             API endpoint.
507              
508             =head2 C<is_modified>
509              
510             =head2 C<is_not_modified>
511              
512             Read/Write boolean.
513              
514             Indicate if the current object is modified or not.
515              
516             =head2 C<populated>
517              
518             Read-only boolean.
519              
520             Indicate if the current object has been populated from API data.
521              
522             =head2 C<uri>
523              
524             Read-only String.
525              
526             Return a complete location for the current object.
527              
528             =head1 METHODS
529              
530             =head2 C<< Stancer::Core::Object->new() : I<self> >>
531              
532             =head2 C<< Stancer::Core::Object->new(I<$token>) : I<self> >>
533              
534             =head2 C<< Stancer::Core::Object->new(I<%args>) : I<self> >>
535              
536             =head2 C<< Stancer::Core::Object->new(I<\%args>) : I<self> >>
537              
538             This method accept an optional string, it will be used as an entity ID for API calls.
539              
540             =head2 C<< $obj->del() : I<self> >>
541              
542             Delete the current object.
543              
544             =head2 C<< $obj->get() : I<hash> | I<undef> >>
545              
546             =head2 C<< $obj->get( I<$attr> ) : I<mixed> >>
547              
548             Return raw data from the API.
549              
550             C<$attr> must be a string, keys should be an object property.
551              
552             =head2 C<< $obj->hydrate( \%data ) : I<self> >>
553              
554             =head2 C<< $obj->hydrate( %data ) : I<self> >>
555              
556             Hydrate the current object.
557              
558             C<%data> must be an hash or a hashref, keys should be an object property.
559              
560             =head2 C<< $obj->populate() : I<self> >>
561              
562             Contact the API to populate current object.
563              
564             =head2 C<< $obj->save() : I<self> >>
565              
566             Save the current object.
567              
568             =head2 C<< $obj->send() : I<self> >>
569              
570             Send the current object.
571              
572             =head2 C<< $obj->toJSON() : I<string> >>
573              
574             Return a JSON representation of current object.
575              
576             =head2 C<< $obj->to_hash() : I<hash> >>
577              
578             Return an hash representing the current object.
579              
580             =head2 C<< $obj->TO_JSON() : I<hash> >>
581              
582             Return an hash representing the current object.
583              
584             This method is used by L<JSON module|JSON/"OBJECT-SERIALISATION"> for convertions.
585              
586             =for Pod::Coverage BUILD
587              
588             =for comment Not supposed to be public, but protected is not possible.
589             We let it in "public area" but without documentation.
590             We use hydratation to force modified list to be accurate on new instance.
591              
592             =for comment Inner callback call before accessing a property to make a populate call before.
593              
594             =for comment List of property that must be transtyped in boolean in JSON export.
595              
596             =for comment List of property where DateTime should be concidered as a date only.
597              
598             =begin comment
599              
600             List of property with object inside.
601              
602             Used to propagate modification on modified properties list.
603              
604             =end comment
605              
606             =for comment List of property that must be transtyped in integer in JSON export.
607              
608             =for comment List of property that must be ignored in JSON export.
609              
610             =begin comment
611              
612             Read/Write hashref of string.
613              
614             Indicate if the current object has been modified.
615              
616             Use with care.
617              
618             =end comment
619              
620             =begin comment
621              
622             Read/Write boolean.
623              
624             Indicate if we are currently in an hydratation.
625              
626             =end comment
627              
628             =head1 USAGE
629              
630             =head2 Logging
631              
632              
633              
634             We use the L<Log::Any> framework for logging events.
635             You may tell where it should log using any available L<Log::Any::Adapter> module.
636              
637             For example, to log everything to a file you just have to add a line to your script, like this:
638             #! /usr/bin/env perl
639             use Log::Any::Adapter (File => '/var/log/payment.log');
640             use Stancer::Core::Object;
641              
642             You must import C<Log::Any::Adapter> before our libraries, to initialize the logger instance before use.
643              
644             You can choose your log level on import directly:
645             use Log::Any::Adapter (File => '/var/log/payment.log', log_level => 'info');
646              
647             Read the L<Log::Any> documentation to know what other options you have.
648              
649             =cut
650              
651             =head1 SECURITY
652              
653             =over
654              
655             =item *
656              
657             Never, never, NEVER register a card or a bank account number in your database.
658              
659             =item *
660              
661             Always uses HTTPS in card/SEPA in communication.
662              
663             =item *
664              
665             Our API will never give you a complete card/SEPA number, only the last four digits.
666             If you need to keep track, use these last four digit.
667              
668             =back
669              
670             =cut
671              
672             =head1 BUGS
673              
674             Please report any bugs or feature requests on the bugtracker website
675             L<https://gitlab.com/wearestancer/library/lib-perl/-/issues> or by email to
676             L<bug-stancer@rt.cpan.org|mailto:bug-stancer@rt.cpan.org>.
677              
678             When submitting a bug or request, please include a test-file or a
679             patch to an existing test-file that illustrates the bug or desired
680             feature.
681              
682             =head1 AUTHOR
683              
684             Joel Da Silva <jdasilva@cpan.org>
685              
686             =head1 COPYRIGHT AND LICENSE
687              
688             This software is Copyright (c) 2018-2024 by Stancer / Iliad78.
689              
690             This is free software, licensed under:
691              
692             The Artistic License 2.0 (GPL Compatible)
693              
694             =cut