File Coverage

blib/lib/MooX/Role/JSON_LD.pm
Criterion Covered Total %
statement 54 58 93.1
branch 17 20 85.0
condition 4 9 44.4
subroutine 12 13 92.3
pod 3 4 75.0
total 90 104 86.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             is => 'ro',
16             # Various other properties
17             );
18             has last_name => (
19             is => 'ro',
20             # Various other properties
21             );
22             has birth_date => (
23             is => 'ro',
24             # Various other properties
25             );
26              
27             # Add two required methods
28             sub json_ld_type { 'Person' };
29              
30             sub json_ld_fields { [ qw[ first_name last_name birth_date ] ] };
31              
32             # Then, in a program somewhere...
33             use My::Moo::Class;
34              
35             my $obj = My::Moo::Class->new({
36             first_name => 'David',
37             last_name => 'Bowie',
38             birth_date => '1947-01-08',
39             });
40              
41             # print a text representation of the JSON-LD
42             print $obj->json_ld;
43              
44             # print the raw data structure for the JSON-LD
45             use Data::Dumper;
46             print Dumper $obj->json_ld_data;
47              
48             =head1 DESCRIPTION
49              
50             This role allows you to easily add methods to your class that produce
51             JSON-LD representing an instance of your class.
52              
53             To do this, you need to do three things:
54              
55             =over 4
56              
57             =item 1. Add the role to your class
58              
59             with 'MooX::Role::JSON_LD';
60              
61             =item 2. Add a method telling the role which JSON-LD type to use in the output
62              
63             sub json_ld_type { 'Person' }
64              
65             =item 3. Add a method defining the fields you want to appear in the JSON-LD
66              
67             sub json_ld_fields { [ qw[ first_name last_name birth_date ] ] };
68              
69             =back
70              
71             =head2 Using the role
72              
73             C<MooX::Role::JSON_LD> can be loaded into your class using the C<with>
74             keyword, just like any other role. The role has been written so that it
75             works in both L<Moo> and L<Moose> classes.
76              
77             =head2 Defining your type
78              
79             JSON-LD can be used to model many different types of object. The current list
80             can be found at L<https://schema.org/>. Once you have chosen one of the types
81             you want to use in your JSON-LD, simply add a method called C<json_ld_type>
82             which returns the name of your type as a string. This string will be used
83             in the C<@type> field of the JSON-LD.
84              
85             =head2 Defining your fields
86              
87             You also need to define the fields that are to be included in your JSON-LD.
88             To do this, you need to add a method called C<json_ld_fields> which returns
89             an array reference containing details of the fields you want.
90              
91             The simplest approach is for each element of the array to be the name of
92             a method on your object. In our example above, we call the three methods,
93             C<first_name>, C<last_name> and C<birth_date>. The names of the methods are
94             used as keys in the JSON-LD and the values returned will be the matching values.
95             So in our example, we would get the following as part of our output:
96              
97             "birth_date" : "1947-01-08",
98             "first_name" : "David",
99             "last_name" : "Bowie",
100              
101             Unfortunately, these aren't valid keys in the "Person" type, so we need to
102             use a slightly more complicated version of the C<json_ld_fields> method, one
103             that enables us to rename fields.
104              
105             sub json_ld_fields {
106             [
107             qw[ first_name last_name],
108             { birthDate => 'birth_date' },
109             ]
110             }
111              
112             In this version, the last element of the array is a hash reference. The key
113             in the hash will be used as the key in the JSON-LD and the value is the name
114             of a method to call. If we make this change, our JSON will look like this:
115              
116             "birthDate" : "1947-01-08",
117             "first_name" : "David",
118             "last_name" : "Bowie",
119              
120             The C<birthDate> key is now a valid key in the JSON-LD representation of a
121             person.
122              
123             But our C<first_name> and C<last_name> keys are still wrong. We could take
124             the same approach as we did with C<birthDate> and translate them to
125             C<givenName> and C<familyName>, but what if we want to combine them into the
126             single C<name> key. We can do that by using another version of
127             C<json_ld_fields> where the value of the definition hash is a subroutine
128             reference. That subroutine is called, passing it the object, so it can build
129             anything you want. We can use that to get the full name of our person.
130              
131             sub json_ld_fields {
132             [
133             { birthDate => 'birth_date'},
134             { name => sub{ $_[0]-> first_name . ' ' . $_[0]->last_name} },
135             ]
136             }
137              
138             That configuration will give us the following output:
139              
140             "birthDate" : "1974-01-08",
141             "name" : "David Bowie",
142              
143             To summarise, the C<json_ld_fields> subroutine should return an array
144             reference. Each element of the array is either a string or a hash reference.
145             If it is a string, that string is used as both the key in the JSON-LD and
146             the method to call on an object to get the value. If the array contains
147             a hash reference, then the hash key is used in the JSON-LD and the hash value
148             is either a string, which is the name of the method to call on an object to
149             get the value, or a code reference which is called (passing in the object)
150             and which is expected to return the value to use in the JSON-LD.
151              
152             =head2 Other contexts
153              
154             By default, this role uses the URL L<https://schema.org>, but you can change
155             this. This role adds an attribute (called C<context>) which can be used to
156             change the context.
157              
158             =cut
159              
160             package MooX::Role::JSON_LD;
161              
162 8     8   881261 use 5.6.0;
  8         2117  
163              
164 8     8   1116 use Moo::Role;
  8         46220  
  8         50  
165              
166 8     8   8607 use Carp;
  8         16  
  8         576  
167 8     8   3783 use JSON::MaybeXS;
  8         114682  
  8         1107  
168 8     8   2672 use MRO::Compat;
  8         3780  
  8         343  
169 8         110 use Types::Standard qw[ArrayRef HashRef InstanceOf Str is_CodeRef is_HashRef
170 8     8   4534 is_ArrayRef is_Ref is_Object];
  8         954422  
171              
172             our $VERSION = '2.1.0';
173              
174             requires qw[json_ld_type json_ld_fields];
175              
176             has json_ld_encoder => (
177             isa => InstanceOf[ qw/ Cpanel::JSON::XS JSON JSON::PP JSON::XS /],
178             is => 'lazy',
179             builder => '_build_json_ld_encoder',
180             );
181              
182             sub _build_json_ld_encoder {
183 7     7   115 my ($self) = @_;
184 7   33     48 return $self->maybe::next::method ||
185             JSON->new->canonical->utf8->space_after->indent->pretty->convert_blessed;
186             };
187              
188             has context => (
189             isa => Str | HashRef | ArrayRef,
190             is => 'lazy',
191             builder => '_build_context',
192             );
193              
194             sub _build_context {
195 7     7   264 return 'https://schema.org/';
196             }
197              
198             sub _resolve_nested {
199 76     76   162 my ($val) = @_;
200              
201 76 100       258 if (is_ArrayRef($val)) {
202             return [
203 3 50 33     7 map { is_Object($_) && $_->can('json_ld_data')
  6         69  
204             ? $_->json_ld_data
205             : $_; } @$val
206             ];
207             }
208              
209 73 100 66     562 is_Object($val) && $val->can('json_ld_data')
210             ? $val->json_ld_data
211             : $val;
212             }
213              
214             sub process_hash {
215 56     56 0 117 my $self = shift;
216 56         116 my ($data, $field) = @_;
217              
218 56         152 my @keys = keys %$field;
219 56         145 my @vals = values %$field;
220              
221             # Originally, this code used 'each', but there seemed
222             # to be some circumstances where the internal iterator
223             # got confused - particularly when an object contained
224             # a sub-object of the same type.
225 56         145 for my $x (0 .. $#keys) {
226 64         131 my $key = $keys[$x];
227 64         127 my $val = $vals[$x];
228              
229 64 100       594 if (defined (my $res = is_CodeRef($val)
    100          
230             ? $val->($self)
231             : $self->$val)) {
232 60         666 $data->{$key} = _resolve_nested($res);
233             }
234             }
235             }
236              
237             sub json_ld_data {
238 29     29 1 1383623 my $self = shift;
239              
240             # We need to know if there's another call to json_ld_data somewhere
241             # in the call stack. If there is, we're not the top-level object and
242             # we should omit the @context;
243              
244 29         62 my $already_in_stack = 0;
245 29         214 my $this_sub = (caller(0))[3];
246 29         79 my $i = 1;
247              
248 29         181 while (my @call = caller $i++) {
249 39 100       201 if ($call[3] eq $this_sub) {
250 10         19 $already_in_stack = 1;
251 10         43 last;
252             }
253             }
254              
255 29         500 my $data = {
256             '@type' => $self->json_ld_type,
257             };
258              
259 29 100       4891 $data->{'@context'} = $self->context unless $already_in_stack;
260              
261 29         367 foreach my $field (@{$self->json_ld_fields}) {
  29         493  
262              
263 72 100       297 if (is_Ref($field)) {
264              
265 56 50       156 if (is_HashRef($field)) {
266 56         156 $self->process_hash($data, $field);
267             } else {
268 0         0 carp "Weird JSON-LD reference: " . ref $field;
269 0         0 next;
270             }
271              
272             }
273             else {
274              
275 16 50       384 if (defined (my $res = $self->$field)) {
276 16         98 $data->{$field} = _resolve_nested($res);
277             }
278              
279             }
280              
281             }
282              
283 29         481 return $data;
284             }
285              
286             sub json_ld {
287 9     9 1 3587 my $self = shift;
288              
289 9         278 return $self->json_ld_encoder->encode($self->json_ld_data);
290             }
291              
292             sub json_ld_wrapped {
293 0     0 1   my $self = shift;
294              
295 0           return qq[<script type="application/ld+json">\n]
296             . $self->json_ld . "\n"
297             . qq[</script>\n];
298             }
299              
300             1;
301              
302             =head1 METHODS
303              
304             This role adds three methods to your class. You can call these methods to
305             output the object's JSON-LD in various formats.
306              
307             =over 4
308              
309             =item json_ld_data
310              
311             Returns a Perl data structure containing the data that will be encoded into
312             JSON.
313              
314             =item json_ld
315              
316             Returns the data from C<json_ld_data> encoded into JSON.
317              
318             =item json_ld_wrapped
319              
320             Returns the JSON string from C<json_ld> wrapped in the
321             C<< <script> .. </script> >> element you will usually need in order to
322             embed the JSON in an HTML document.
323              
324             =back
325              
326             =head1 AUTHOR
327              
328             Dave Cross <dave@perlhacks.com>
329              
330             =head1 SEE ALSO
331              
332             perl(1), Moo, Moose, L<https://json-ld.org/>, L<https://schema.org/>
333              
334             L<MooX::JSON_LD> is included in this distribution and provides an alternative
335             interface to the same functionality.
336              
337             =head1 COPYRIGHT AND LICENSE
338              
339             Copyright (C) 2018, Magnum Solutions Ltd. All Rights Reserved.
340              
341             This script is free software; you can redistribute it and/or modify it
342             under the same terms as Perl itself.
343              
344             =cut