File Coverage

blib/lib/DBIx/Class/InflateColumn/JSON2Object.pm
Criterion Covered Total %
statement 89 95 93.6
branch 12 22 54.5
condition 5 15 33.3
subroutine 19 19 100.0
pod 3 4 75.0
total 128 155 82.5


line stmt bran cond sub pod time code
1             package DBIx::Class::InflateColumn::JSON2Object;
2              
3             # ABSTRACT: convert JSON columns to Perl objects
4             our $VERSION = '0.907'; # VERSION
5              
6 5     5   970308 use strict;
  5         38  
  5         144  
7 5     5   28 use warnings;
  5         10  
  5         113  
8 5     5   84 use 5.014;
  5         21  
9 5     5   1955 use JSON::MaybeXS qw(encode_json decode_json );
  5         23111  
  5         328  
10 5     5   42 use Encode qw(encode_utf8 decode_utf8);
  5         11  
  5         427  
11 5     5   32 use Module::Runtime 'use_module';
  5         12  
  5         62  
12 5     5   308 use Scalar::Util qw(blessed);
  5         11  
  5         5684  
13              
14             sub class_in_column {
15 5     5 1 3327 my ( $class, @args ) = @_;
16 5         21 my $caller = caller(0);
17              
18 5         20 foreach my $def (@args) {
19              
20 5         15 my $class_column = $def->{class_column};
21 5         15 my $data_column = $def->{data_column};
22 5         17 my $namespace = $def->{namespace};
23 5   33     35 my $result_source = $def->{result_source} || $caller;
24              
25 5         30 use_module($namespace);
26              
27             $result_source->inflate_column(
28             $data_column,
29             { inflate => sub {
30 4     4   53609 my ( $data, $self ) = @_;
31 4         94 my $package = $namespace->package( $self->$class_column );
32 4         99 return $package->thaw($data);
33             },
34             deflate => sub {
35 4     4   29259 my ( $data, $self ) = @_;
36 4 100       25 if ( blessed $data) {
37 2 50       16 if ( $data->isa($namespace) ) {
38 2         19 $self->$class_column( $data->moniker );
39             }
40             else {
41 0         0 die( 'Supplied args object is not a '
42             . $namespace );
43             }
44             }
45             else {
46 2         59 my $package =
47             $namespace->package( $self->$class_column );
48 2         37 $data = $package->thaw($data);
49             }
50 4         674 return $data->freeze;
51             },
52             }
53 5         85999 );
54             }
55             }
56              
57             sub fixed_class {
58 6     6 1 8210 my ( $class, @args ) = @_;
59 6         31 my $caller = caller(0);
60              
61 6         23 foreach my $def (@args) {
62              
63 6         30 my $data_column = $def->{column};
64 6         17 my $package = $def->{class};
65 6   33     227 my $result_source = $def->{result_source} || $caller;
66              
67 6         46 use_module($package);
68              
69             $result_source->inflate_column(
70             $data_column,
71             { inflate => sub {
72 8     8   120222 my ( $data, $self ) = @_;
73 8         53 return $package->thaw($data);
74             },
75             deflate => sub {
76 8     8   35207 my ( $data, $self ) = @_;
77 8 50       44 if ( blessed $data) {
78 8 50       49 if ( !$data->isa($package) ) {
79 0         0 die('Supplied args object is not a ' . $package );
80             }
81             }
82             else {
83 0         0 $data = $package->thaw($data);
84             }
85 8         42 return $data->freeze;
86             },
87             }
88 6         239613 );
89             }
90             }
91              
92             sub array_of_class {
93 5     5 0 4233 my ( $class, @args ) = @_;
94 5         22 my $caller = caller(0);
95              
96 5         17 foreach my $def (@args) {
97              
98 5         16 my $data_column = $def->{column};
99 5         13 my $package = $def->{class};
100 5   33     41 my $result_source = $def->{result_source} || $caller;
101              
102 5         30 use_module($package);
103              
104             $result_source->inflate_column(
105             $data_column,
106             { inflate => sub {
107 4     4   59892 my ( $raw_list, $self ) = @_;
108 4 50       27 utf8::encode($raw_list) if utf8::is_utf8($raw_list);
109 4         37 my $list = decode_json($raw_list);
110 4 50       21 die('Can only work with arrayref')
111             unless ref($list) eq 'ARRAY';
112 4         9 my @objects;
113 4         9 foreach my $data (@$list) {
114 12         559 push( @objects, $package->new($data) );
115             }
116 4         174 return \@objects;
117             },
118             deflate => sub {
119 2     2   14267 my ( $list, $self ) = @_;
120 2 50       12 die('Supplied args object is not a arrayref')
121             unless ref($list) eq 'ARRAY';
122 2         7 my @frozen;
123 2         7 foreach my $data (@$list) {
124 7 50       38 if ( blessed $data) {
125 7 50       37 if ( !$data->isa($package) ) {
126 0         0 die( 'Supplied args object is not a '
127             . $package );
128             }
129             }
130             else {
131 0         0 $data = $package->thaw($data);
132             }
133 7         33 push( @frozen, $data->freeze );
134             }
135 2         69 return '[' . ( join( ',', @frozen ) ) . ']';
136             },
137             }
138 5         88269 );
139             }
140             }
141              
142             sub no_class {
143 5     5 1 463416 my ( $class, @args ) = @_;
144 5         33 my $caller = caller(0);
145              
146 5         20 foreach my $def (@args) {
147              
148 5         33 my $data_column = $def->{column};
149 5   33     41 my $result_source = $def->{result_source} || $caller;
150              
151             $result_source->inflate_column(
152             $data_column,
153             { inflate => sub {
154 3     3   54905 my ( $data, $self ) = @_;
155             return {}
156 3 50 33     43 if !defined $data
157             || $data =~ m/^\s*$/;
158 3         24 return decode_json( encode_utf8($data) );
159             },
160             deflate => sub {
161 2     2   14780 my ( $data, $self ) = @_;
162 2 50       23 if ( ref($data) =~ m/^(HASH|ARRAY)$/ ) {
163 2         37 return decode_utf8( encode_json($data) );
164             }
165 0           return $data;
166             },
167             }
168 5         106 );
169             }
170             }
171              
172             1;
173              
174             __END__
175              
176             =pod
177              
178             =encoding UTF-8
179              
180             =head1 NAME
181              
182             DBIx::Class::InflateColumn::JSON2Object - convert JSON columns to Perl objects
183              
184             =head1 VERSION
185              
186             version 0.907
187              
188             =head1 SYNOPSIS
189              
190             # In a DBIx::Class Result
191             package MyApp::Schema::Result::SomeTable;
192             ... lots of DBIx::Class code...
193              
194             use DBIx::Class::InflateColumn::JSON2Object;
195             DBIx::Class::InflateColumn::JSON2Object->fixed_class({
196             column=>'data', # a column storing JSON
197             class=>'MyApp::SomeClass',
198             });
199              
200             # later, in some code far, far away...
201             my $row = $schema->resultset('SomeTable')->find(42);
202             my $obj = $row->data;
203             $obj->foo; # $obj ISA MyApp::SomeClass
204              
205             # store a hash as JSON after "validating" it through MyApp::SomeClass
206             $schema->resultset('SomeTable')->create({
207             data => {
208             foo=>'bar',
209             };
210             })
211              
212             # you can also use it to just deflate/inflate JSON to a Perl hash
213             DBIx::Class::InflateColumn::JSON2Object->no_class({
214             column=>'args',
215             });
216              
217             # or have a complex set of objects
218             DBIx::Class::InflateColumn::JSON2Object->class_in_column({
219             data_column => 'object', # some JSON
220             class_column => 'type', # here we store the name of the object
221             namespace => 'MyApp::Object', # the namespace of the objects
222             });
223              
224             # or store a list of objects (of one class!) into an array
225             DBIx::Class::InflateColumn::JSON2Object->array_of_class({
226             column=>'data', # a column storing a JSON list
227             class=>'MyApp::SomeElementClass',
228             });
229              
230             =head1 DESCRIPTION
231              
232             TODO: short overview
233              
234             =head2 Booleans, JSON, oh my...
235              
236             TODO: describe problem and the (hacky/crappy?) solution
237              
238             =head2 METHODS
239              
240             Please note that you can pass more than one HASHREF per method to
241             install several inflator/deflators at once.
242              
243             =head3 no_class
244              
245             Install a JSON inflator/deflator for each column.
246              
247             DBIx::Class::InflateColumn::JSON2Object->no_class({
248             column=>'args'
249             });
250              
251             You can pass a Perl datastructure to the row and it will be stored as JSON:
252              
253             $resultset->create( {
254             id => 123,
255             args => {
256             some => 'data',
257             more => [1,1,2,3,5,8]
258             }
259             });
260             # will be stored as '{"some":"data","more":[1,1,2,3,5,8]}'
261              
262             You can also access the data directly as a Perl hash:
263              
264             my $row = $resultset->find(123);
265             $row->args->{some}; # 'data'
266             $row->args->{more}[5]; # 8
267              
268             =head3 fixed_class
269              
270             If plain JSON is to wobbly for you, you can define Moose objects and
271             have them serialized to JSON. Not only can you now add some custom
272             methods to the objects, but you can (ab)use the object initalisation
273             and all features Moose provides to define your objects.
274              
275             # Define a class
276             package YourApp::Sweets;
277             use Moose;
278             with 'DBIx::Class::InflateColumn::JSON2Object::Role::Storable';
279              
280             has 'name' => ( is=>'ro', isa=>'Str');
281             has 'amount => ( is=>'ro', isa=>'Int');
282              
283             __PACKAGE__->meta->make_immutable;
284              
285              
286             # In your Result class, map the class to a column
287             package YourApp::Schema::Result::Thing;
288             __PACKAGE__->add_columns(
289             "sweets",
290             { data_type => "jsonb", is_nullable => 1 }
291             );
292             use DBIx::Class::InflateColumn::JSON2Object;
293              
294             DBIx::Class::InflateColumn::JSON2Object->fixed_class({
295             column=>'sweets',
296             class=>'YourApp::Sweets',
297             });
298              
299             Now you can pass a plain hash, a raw JSON string, or an initiated
300             object to the row, and always get back the object:
301              
302             my $thing = $schema->resultset('Thing')->create({
303             sweets => {
304             name => 'Manner Schnitten',
305             amount => 10,
306             }
307             });
308             ref($thing->sweets); # 'YourApp::Sweets'
309              
310             =head3 class_in_column
311              
312             Sometimes you have a set of similar objects you want to store. TODO: explain oe1.article.paragraph
313              
314             TODO ->class_in_column(..)
315              
316             You can pass an object to the row and the infered type will be stored together with the JSON-payload:
317              
318             TODO
319              
320             To get back the object:
321              
322             TODO
323              
324             =head3 array_in_class
325              
326             Sometimes you want to store a list of objects without packing them into a "Set"-Object. This only works if all objects are of the same class, as we do not want to include the class name inside the object (c.f. MooseX::Storage, KiokuDB).
327              
328             TODO ->array_in_class(..)
329              
330             Just pass an arrayref of object to the row and it will be stored as JSON:
331              
332             TODO
333              
334             And you get back the initated list of objects:
335              
336             TODO
337              
338             =head1 THANKS
339              
340             =over
341              
342             =item *
343              
344             Parts of this code were orginally developed for L<validad.com|https://www.validad.com/> and released as Open Source.
345              
346             =item * L<Maroš Kollár|https://metacpan.org/author/MAROS> wrote the prototype of C<class_in_column>, orginally developed for L<http://oe1.orf.at>.
347              
348             =back
349              
350             =head1 AUTHOR
351              
352             Thomas Klausner <domm@plix.at>
353              
354             =head1 COPYRIGHT AND LICENSE
355              
356             This software is copyright (c) 2017 - 2021 by Thomas Klausner.
357              
358             This is free software; you can redistribute it and/or modify it under
359             the same terms as the Perl 5 programming language system itself.
360              
361             =cut