File Coverage

blib/lib/MooX/Role/JSON_LD.pm
Criterion Covered Total %
statement 46 48 95.8
branch 13 16 81.2
condition 4 9 44.4
subroutine 12 12 100.0
pod 0 3 0.0
total 75 88 85.2


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 a method to your class that produces
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 => 'birthDate'},
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             =head2 Other contexts
144              
145             By default, this role uses the URL L<http://schema.org>, but you can change
146             this. This role adds an attribute (called C<context>) which can be used to
147             change the context.
148              
149             =cut
150              
151             package MooX::Role::JSON_LD;
152              
153 8     8   828241 use 5.6.0;
  8         64  
154              
155 8     8   1151 use Moo::Role;
  8         40704  
  8         48  
156              
157 8     8   7418 use Carp;
  8         28  
  8         541  
158 8     8   4337 use JSON::MaybeXS;
  8         63482  
  8         505  
159 8     8   1061 use MRO::Compat;
  8         3510  
  8         283  
160 8         112 use Types::Standard qw[ArrayRef HashRef InstanceOf Str is_CodeRef is_HashRef
161 8     8   4843 is_ArrayRef is_Ref is_Object];
  8         583282  
162              
163             our $VERSION = '1.0.1';
164              
165             requires qw[json_ld_type json_ld_fields];
166              
167             has json_ld_encoder => (
168             isa => InstanceOf[ qw/ Cpanel::JSON::XS JSON JSON::PP JSON::XS /],
169             is => 'lazy',
170             builder => '_build_json_ld_encoder',
171             );
172              
173             sub _build_json_ld_encoder {
174 7     7   135 my ($self) = @_;
175 7   33     49 return $self->maybe::next::method ||
176             JSON->new->canonical->utf8->space_after->indent->pretty->convert_blessed;
177             };
178              
179             has context => (
180             isa => Str | HashRef | ArrayRef,
181             is => 'lazy',
182             builder => '_build_context',
183             );
184              
185             sub _build_context {
186 11     11   309 return 'http://schema.org/';
187             }
188              
189             sub _resolve_nested {
190 76     76   135 my ($val) = @_;
191              
192 76 100       169 if (is_ArrayRef($val)) {
193             return [
194 3 50 33     15 map { is_Object($_) && $_->can('json_ld_data')
  6         14  
195             ? $_->json_ld_data
196             : $_; } @$val
197             ];
198             }
199              
200 73 100 66     342 is_Object($val) && $val->can('json_ld_data')
201             ? $val->json_ld_data
202             : $val;
203             }
204              
205             sub process_hash {
206 56     56 0 97 my $self = shift;
207 56         114 my ($data, $field) = @_;
208              
209 56         150 my @keys = keys %$field;
210 56         120 my @vals = values %$field;
211              
212             # Originally, this code used 'each', but there seemed
213             # to be some circumstances where the internet iterator
214             # got confused - particularly when an object contained
215             # a sub-object of the same type.
216 56         158 for my $x (0 .. $#keys) {
217 64         145 my $key = $keys[$x];
218 64         94 my $val = $vals[$x];
219              
220 64 100       151 if (defined (my $res = is_CodeRef($val)
    100          
221             ? $val->($self)
222             : $self->$val)) {
223 60         838 $data->{$key} = _resolve_nested($res);
224             }
225             }
226             }
227              
228             sub json_ld_data {
229 29     29 0 43125 my $self = shift;
230              
231 29         581 my $data = {
232             '@context' => $self->context,
233             '@type' => $self->json_ld_type,
234             };
235              
236 29         4314 foreach my $field (@{$self->json_ld_fields}) {
  29         412  
237              
238 72 100       461 if (is_Ref($field)) {
239              
240 56 50       283 if (is_HashRef($field)) {
241 56         282 $self->process_hash($data, $field);
242             } else {
243 0         0 carp "Weird JSON-LD reference: " . ref $field;
244 0         0 next;
245             }
246              
247             }
248             else {
249              
250 16 50       294 if (defined (my $res = $self->$field)) {
251 16         107 $data->{$field} = _resolve_nested($res);
252             }
253              
254             }
255              
256             }
257              
258 29         461 return $data;
259             }
260              
261             sub json_ld {
262 9     9 0 2583 my $self = shift;
263              
264 9         243 return $self->json_ld_encoder->encode($self->json_ld_data);
265             }
266              
267             1;
268              
269             =head1 AUTHOR
270              
271             Dave Cross <dave@perlhacks.com>
272              
273             =head1 SEE ALSO
274              
275             perl(1), Moo, Moose, L<https://json-ld.org/>, L<https://schema.org/>
276              
277             L<MooX::JSON_LD> is included in this distribution and provides an alternative
278             interface to the same functionality.
279              
280             =head1 COPYRIGHT AND LICENSE
281              
282             Copyright (C) 2018, Magnum Solutions Ltd. All Rights Reserved.
283              
284             This script is free software; you can redistribute it and/or modify it
285             under the same terms as Perl itself.
286              
287             =cut