File Coverage

blib/lib/Object/PadX/Role/AutoJSON.pm
Criterion Covered Total %
statement 61 62 98.3
branch 17 20 85.0
condition 2 3 66.6
subroutine 19 20 95.0
pod 0 1 0.0
total 99 106 93.4


line stmt bran cond sub pod time code
1             package Object::PadX::Role::AutoJSON;
2              
3 3     3   795942 use v5.26;
  3         15  
4              
5 3     3   22 use strict;
  3         6  
  3         92  
6 3     3   43 use warnings;
  3         7  
  3         184  
7              
8 3     3   21 use Object::Pad ':experimental(custom_field_attr mop)';
  3         7  
  3         22  
9 3     3   2357 use Object::Pad::MOP::FieldAttr;
  3         586  
  3         195  
10 3     3   1751 use Object::Pad::MOP::Field;
  3         591  
  3         129  
11 3     3   21 use Object::Pad::MOP::Class;
  3         12  
  3         22  
12              
13             # TODO replace these with the real thing as part of the func flags when Object::Pad finally exposes them
14             my $_require_value = sub {
15             my ($field_meta, $value) = @_;
16              
17             die "Missing required attribute value" unless defined $value;
18             return $value;
19             };
20              
21             my $_disallow_value = sub {
22             my ($field_meta, $value) = @_;
23              
24             die "Missing required attribute value" if defined $value;
25             };
26              
27             Object::Pad::MOP::FieldAttr->register( "JSONExclude", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_disallow_value );
28             # Set a new name when going to JSON
29             Object::Pad::MOP::FieldAttr->register( "JSONKey", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_require_value );
30             # Allow this to get sent as null, rather than leaving it off
31             Object::Pad::MOP::FieldAttr->register( "JSONNull", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_disallow_value );
32             # Force boolean or num or str
33             Object::Pad::MOP::FieldAttr->register( "JSONBool", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_disallow_value );
34             Object::Pad::MOP::FieldAttr->register( "JSONNum", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_disallow_value );
35             Object::Pad::MOP::FieldAttr->register( "JSONStr", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_disallow_value );
36              
37             Object::Pad::MOP::FieldAttr->register( "JSONList", permit_hintkey => 'Object::PadX::Role::AutoJSON', apply => $_require_value );
38              
39             # ABSTRACT: Role for Object::Pad that dynamically handles a TO_JSON serialization based on the MOP
40             our $VERSION = '1.2';
41              
42             sub import {
43 5     5   36 my @imports = @_;
44 5         72 $^H{'Object::PadX::Role::AutoJSON'}=1;
45              
46 5 100       41 if (grep {$_ eq '-toplevel'} @imports) {
  7         486  
47 2     2   275 eval "use Object::Pad; use Object::PadX::Role::AutoJSON; role AutoJSON :does(Object::PadX::Role::AutoJSON) {};";
  2     2   20  
  2         5  
  2         8  
  2         369  
  2         5  
  2         11  
48 2 50       151 die $@ if $@;
49             }
50             }
51              
52             sub unimport {
53 0     0   0 delete $^H{'Object::PadX::Role::AutoJSON'};
54              
55             # Don't try to undo -toplevel, madness may ensue
56             }
57              
58             role Object::PadX::Role::AutoJSON {
59 3     3   3435 use feature 'signatures';
  3         31  
  3         5745  
60              
61             my $_to_str = sub ($x) {
62             return "".$x;
63             };
64              
65             my $_to_num = sub ($x) {
66             return 0+$x;
67             };
68              
69             my $_to_bool = sub ($x) {
70             return !!$x ? \1 : \0;
71             };
72              
73             my $_to_list = sub ($ref, $type) {
74             my $sub = $type eq 'JSONNum' ? $_to_num :
75             $type eq 'JSONStr' ? $_to_str :
76             $type eq 'JSONBool' ? $_to_bool :
77             sub {die "Wrong type $type in json conversion"};
78             return [map {$sub->($_)} $ref->@*]
79             };
80              
81 10     10 0 115 method TO_JSON() {
  10     10   35  
  10     8   20  
        8      
        8      
        8      
        8      
        8      
82 10         27 my $class = __CLASS__;
83 10         79 my $classmeta = Object::Pad::MOP::Class->for_class($class);
84 10         767 my @metafields = $classmeta->fields;
85              
86 10         31 my %json_out = ();
87              
88 10         29 for my $metafield (@metafields) {
89 11         55 my $field_name = $metafield->name;
90 11         45 my $sigil = $metafield->sigil;
91              
92 11         104 my $has_exclude = $metafield->has_attribute("JSONExclude");
93              
94 11 100       48 next if $has_exclude;
95              
96 10 50       37 next if $sigil ne '$'; # Don't try to handle anything but scalars
97              
98 10         51 my $has_null = $metafield->has_attribute("JSONNull");
99              
100 10         48 my $value = $metafield->value($self);
101 10 50 66     53 next unless (defined $value || $has_null);
102              
103 10         66 my $key = $field_name =~ s/^\$//r;
104 10 100       63 $key = $metafield->get_attribute_value("JSONKey") if $metafield->has_attribute("JSONKey");
105              
106 10 100       133 if ($metafield->has_attribute('JSONBool')) {
    100          
    100          
    100          
107 2         8 $value = $_to_bool->($value);
108             } elsif ($metafield->has_attribute('JSONNum')) {
109             # Force numification
110 1         8 $value = $_to_num->($value);
111             } elsif ($metafield->has_attribute('JSONStr')) {
112             # Force stringification
113 1         36 $value = $_to_str->($value);
114             } elsif ($metafield->has_attribute('JSONList')) {
115 1         7 my $type = $metafield->get_attribute_value('JSONList');
116 1         4 $value = $_to_list->($value, $type);
117             }
118              
119 10         51 $json_out{$key} = $value;
120             }
121              
122 10         107 return \%json_out;
123             }
124             }
125              
126             =pod
127              
128             =head1 NAME
129              
130             Object::PadX::Role::AutoJSON - Object::Pad role that creates an automatic TO_JSON() method that serializes properly with JSON::XS or Cpanel::JSON::XS
131              
132             =head1 WARNING
133              
134             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
135             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
136              
137             =head1 SYNOPSIS
138              
139             use Object::Pad;
140             use Object::PadX::Role::AutoJSON;
141             use Cpanel::JSON::XS;
142              
143             class Person :does(Object::PadX::Role::AutoJSON) {
144             field $internal_uuid :param :JSONStr :JSONKey(uuid);
145              
146             field $first_name :param;
147             field $middle_name :param :JSONNull = undef;
148             field $last_name :param;
149             field $age :param :JSONNum;
150              
151             field $is_alive :param :JSONBool;
152              
153             field $private_information :param :JSONExclude = undef;
154             }
155              
156             my $person = Person->new(
157             internal_uuid => "defe205e-833f-11ee-b962-0242ac120002",
158             first_name => "Phillip",
159             last_name => "Fry",
160             age => 3049,
161             is_alive => 1,
162             private_information => {"pin number": "1077"}
163             );
164              
165             my $json = Cpanel::JSON::XS->new->convert_blessed(1);
166              
167             my $output = $json->encode($person);
168              
169             $output eq '{
170             "uuid": "defe205e-833f-11ee-b962-0242ac120002",
171             "first_name": "Phillip",
172             "middle_name": null,
173             "last_name": "Fry",
174             "age": 3049,
175             "is_alive": true,
176             }'
177              
178             =head1 DESCRIPTION
179              
180             This module creates an automatic serialization function named C on your Object::Pad classes. The purpose
181             of which is to automatically look up all fields in the object and give them out to be serialized by a JSON module.
182             It also provides a series of attributes, C<:JSONExclude> and such, to allow you to do some basic customization of
183             how the fields will be output, without affecting how the fields themselves work.
184              
185             =head2 IMPORTS
186              
187             use Object::PadX::Role::AutoJSON '-toplevel';
188              
189             class Foo :does(AutoJSON) {
190             ...
191             }
192              
193             This is the only import right now, it creates a top level namespace role AutoJSON for lazy people (like me).
194             This is a bad idea, don't do it it pollutes this globally since there is no such thing as lexical role imports.
195              
196             =head2 ATTRIBUTES
197              
198             =over 4
199              
200             =item * :JSONExclude
201              
202             This attribute on a field tells the serializier to ignore the field and never output it. This is useful for internal
203             fields or fields to other objects that shouldn't be kept as part of the object when serializing, such as a database handle
204             or private information.
205              
206             =item * :JSONKey(name)
207              
208             This attribute lets you change the name that is output when serializing, so that you can use a more descriptive name on the class
209             but give a shorter one when serializing, or to help multiple classes look the same when output as JSON even if they're different internally.
210              
211             =item * :JSONNull
212              
213             Normally fields that have no value will be excluded from output, to prevent accidental nulls being given and breaking other expectations.
214             This attribute lets you force those fields to be output when appropriate.
215              
216             =item * :JSONBool
217              
218             This attribute forces the value to be re-interpreted as a boolean value, regardless of how perl sees it. This way you can get a proper 'true' and 'false'
219             in the resulting JSON without having to massage the value yourself through other means.
220              
221             =item * :JSONNum
222              
223             This attribute forces the value to be re-interpreted as a numeric value, regardless of how perl sees it. This will help handle dual-vars or places where a number
224             came as a string and perl wouldn't care but JSON does.
225              
226             =item * :JSONStr
227              
228             This attribute forces the value to be re-interpreted as a string value, regardless of how perl sees it. That way numbers, or other value types that were present will
229             be properly stringified, such as nested objects that override stringification.
230              
231             =item * :JSONList(type)
232              
233             This attribute forces the list in the field to have all of it's elements processed as C. Where C is one of C, C, or C. See above for any
234             notes about each type, they match the attributes
235              
236             =back
237              
238             =head1 BUGS
239              
240             No known bugs at this time, if any are found contact simcop2387 on IRC, or email simcop2387 at simcop2387.info
241              
242             =head1 LICENSE
243              
244             This module is available under the Artistic 2.0 License
245              
246             =head1 SEE ALSO
247              
248             L, L
249              
250             =head1 AUTHOR
251              
252             Ryan Voots, L, aka simcop2387
253              
254             =cut
255