File Coverage

blib/lib/MooX/Role/JSON_LD.pm
Criterion Covered Total %
statement 43 45 95.5
branch 13 16 81.2
condition 4 9 44.4
subroutine 11 11 100.0
pod 0 2 0.0
total 71 83 85.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             MooX::Role::JSON_LD - Easily provide JSON-LD mark-up for your objects.
4              
5             =head1 SYNOPSIS
6              
7             # Your Moo (or Moose) Class
8             package::My::Moo::Class
9              
10             use Moo;
11             with 'MooX::Role::JSON_LD';
12              
13             # define your attributes
14             has first_name => ( ... );
15             has last_name => ( ... );
16             has birth_date => ( ... );
17              
18             # Add two required methods
19             sub json_ld_type { 'Person' };
20              
21             sub json_ld_fields { [ qw[ first_name last_name birth_date ] ] };
22              
23             # Then, in a program somewhere...
24             use My::Moo::Class;
25              
26             my $obj = My::Moo::Class->new({
27             first_name => 'David',
28             last_name => 'Bowie',
29             birth_date => '1947-01-08',
30             });
31              
32             # print a text representation of the JSON-LD
33             print $obj->json_ld;
34              
35             # print the raw data structure for the JSON-LD
36             use Data::Dumper;
37             print Dumper $obj->json_ld_data;
38              
39             =head1 DESCRIPTION
40              
41             This role allows you to easily add a method to your class that produces
42             JSON-LD representing an instance of your class.
43              
44             To do this, you need to do three things:
45              
46             =over 4
47              
48             =item 1. Add the role to your class
49              
50             with 'MooX::Role::JSON_LD';
51              
52             =item 2. Add a method telling the role which JSON-LD type to use in the output
53              
54             sub json_ld_type { 'Person' }
55              
56             =item 3. Add a method defining the fields you want to appear in the JSON-LD
57              
58             sub json_ld_fields { [ qw[ first_name last_name birth_date ] ] };
59              
60             =back
61              
62             =head2 Using the role
63              
64             C<MooX::Role::JSON_LD> can be loaded into your class using the C<with>
65             keyword, just like any other role. The role has been written so that it
66             works in both L<Moo> and L<Moose> classes.
67              
68             =head2 Defining your type
69              
70             JSON-LD can be used to model many different types of object. The current list
71             can be found at L<https://schema.org/>. Once you have chosen one of the types
72             you want to use in your JSON-LD, simply add a method called C<json_ld_type>
73             which returns the name of your type as a string. This string will be used
74             in the C<@type> field of the JSON-LD.
75              
76             =head2 Defining your fields
77              
78             You also need to define the fields that are to be included in your JSON-LD.
79             To do this, you need to add a method called C<json_ld_fields> which returns
80             an array reference containing details of the fields you want.
81              
82             The simplest approach is for each element of the array to be the name of
83             a method on your object. In our example above, we call the three methods,
84             C<first_name>, C<last_name> and C<birth_date>. The names of the methods are
85             used as keys in the JSON-LD and the values returned will be the matching values.
86             So in our example, we would get the following as part of our output:
87              
88             "birth_date" : "1947-01-08",
89             "first_name" : "David",
90             "last_name" : "Bowie",
91              
92             Unfortunately, these aren't valid keys in the "Person" type, so we need to
93             use a slightly more complicated version of the C<json_ld_fields> method, one
94             that enables us to rename fields.
95              
96             sub json_ld_fields {
97             [
98             qw[ first_name last_name],
99             { birthDate => 'birth_date' },
100             ]
101             }
102              
103             In this version, the last element of the array is a hash reference. The key
104             in the hash will be used as the key in the JSON-LD and the value is the name
105             of a method to call. If we make this change, our JSON will look like this:
106              
107             "birthDate" : "1947-01-08",
108             "first_name" : "David",
109             "last_name" : "Bowie",
110              
111             The C<birthDate> key is now a valid key in the JSON-LD representation of a
112             person.
113              
114             But our C<first_name> and C<last_name> keys are still wrong. We could take
115             the same approach as we did with C<birthDate> and translate them to
116             C<givenName> and C<familyName>, but what if we want to combine them into the
117             single C<name> key. We can do that by using another version of
118             C<json_ld_fields> where the value of the definition hash is a subroutine
119             reference. That subroutine is called, passing it the object, so it can build
120             anything you want. We can use that to get the full name of our person.
121              
122             sub json_ld_fields {
123             [
124             { birthDate => 'birthDate'},
125             { name => sub{ $_[0]-> first_name . ' ' . $_[0]->last_name} },
126             ]
127             }
128              
129             That configuration will give us the following output:
130              
131             "birthDate" : "1974-01-08",
132             "name" : "David Bowie",
133              
134             =head2 Other contexts
135              
136             By default, this role uses the URL L<http://schema.org>, but you can change
137             this. This role adds an attribute (called C<context>) which can be used to
138             change the context.
139              
140             =cut
141              
142             package MooX::Role::JSON_LD;
143              
144 8     8   626719 use 5.6.0;
  8         34  
145              
146 8     8   874 use Moo::Role;
  8         31101  
  8         40  
147              
148 8     8   5455 use Carp;
  8         17  
  8         389  
149 8     8   3150 use JSON::MaybeXS;
  8         48833  
  8         428  
150 8     8   837 use MRO::Compat;
  8         2953  
  8         211  
151 8         82 use Types::Standard qw[ArrayRef HashRef InstanceOf Str is_CodeRef is_HashRef
152 8     8   3270 is_ArrayRef is_Ref is_Object];
  8         452484  
153              
154             our $VERSION = '0.0.18';
155              
156             requires qw[json_ld_type json_ld_fields];
157              
158             has json_ld_encoder => (
159             isa => InstanceOf[ qw/ Cpanel::JSON::XS JSON JSON::PP JSON::XS /],
160             is => 'lazy',
161             builder => '_build_json_ld_encoder',
162             );
163              
164             sub _build_json_ld_encoder {
165 7     7   87 my ($self) = @_;
166 7   33     33 return $self->maybe::next::method ||
167             JSON->new->canonical->utf8->space_after->indent->pretty->convert_blessed;
168             };
169              
170             has context => (
171             isa => Str | HashRef | ArrayRef,
172             is => 'lazy',
173             builder => '_build_context',
174             );
175              
176             sub _build_context {
177 11     11   231 return 'http://schema.org/';
178             }
179              
180             sub _resolve_nested {
181 76     76   111 my ($val) = @_;
182              
183 76 100       130 if (is_ArrayRef($val)) {
184             return [
185 3 50 33     14 map { is_Object($_) && $_->can('json_ld_data')
  6         11  
186             ? $_->json_ld_data
187             : $_; } @$val
188             ];
189             }
190              
191 73 100 66     279 is_Object($val) && $val->can('json_ld_data')
192             ? $val->json_ld_data
193             : $val;
194             }
195              
196             sub json_ld_data {
197 29     29 0 33891 my $self = shift;
198              
199 29         430 my $data = {
200             '@context' => $self->context,
201             '@type' => $self->json_ld_type,
202             };
203              
204 29         3195 foreach my $field (@{$self->json_ld_fields}) {
  29         298  
205              
206 72 100       364 if (is_Ref($field)) {
207              
208 56 50       232 if (is_HashRef($field)) {
209              
210 56         219 my @keys = keys %$field;
211 56         95 my @vals = values %$field;
212              
213             # Originally, this code used 'each', but there seemed
214             # to be some circumstances where the internet iterator
215             # got confused - particularly when an object contained
216             # a sub-object of the same type.
217 56         119 for my $x (0 .. $#keys) {
218 64         116 my $key = $keys[$x];
219 64         76 my $val = $vals[$x];
220              
221 64 100       120 if (defined (my $res = is_CodeRef($val)
    100          
222             ? $val->($self)
223             : $self->$val)) {
224 60         659 $data->{$key} = _resolve_nested($res);
225             }
226             }
227             }
228             else {
229 0         0 carp "Weird JSON-LD reference: " . ref $field;
230 0         0 next;
231             }
232              
233             }
234             else {
235              
236 16 50       237 if (defined (my $res = $self->$field)) {
237 16         86 $data->{$field} = _resolve_nested($res);
238             }
239              
240             }
241              
242             }
243              
244 29         354 return $data;
245             }
246              
247             sub json_ld {
248 9     9 0 2035 my $self = shift;
249              
250 9         156 return $self->json_ld_encoder->encode($self->json_ld_data);
251             }
252              
253             1;
254              
255             =head1 AUTHOR
256              
257             Dave Cross <dave@perlhacks.com>
258              
259             =head1 SEE ALSO
260              
261             perl(1), Moo, Moose, L<https://json-ld.org/>, L<https://schema.org/>
262              
263             L<MooX::JSON_LD> is included in this distribution and provides an alternative
264             interface to the same functionality.
265              
266             =head1 COPYRIGHT AND LICENSE
267              
268             Copyright (C) 2018, Magnum Solutions Ltd. All Rights Reserved.
269              
270             This script is free software; you can redistribute it and/or modify it
271             under the same terms as Perl itself.
272              
273             =cut