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             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   604861 use 5.6.0;
  8         34  
154              
155 8     8   871 use Moo::Role;
  8         30921  
  8         42  
156              
157 8     8   5482 use Carp;
  8         18  
  8         406  
158 8     8   3140 use JSON::MaybeXS;
  8         49012  
  8         390  
159 8     8   850 use MRO::Compat;
  8         2924  
  8         216  
160 8         79 use Types::Standard qw[ArrayRef HashRef InstanceOf Str is_CodeRef is_HashRef
161 8     8   3298 is_ArrayRef is_Ref is_Object];
  8         457798  
162              
163             our $VERSION = '1.0.0';
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   87 my ($self) = @_;
175 7   33     28 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   227 return 'http://schema.org/';
187             }
188              
189             sub _resolve_nested {
190 76     76   110 my ($val) = @_;
191              
192 76 100       130 if (is_ArrayRef($val)) {
193             return [
194 3 50 33     11 map { is_Object($_) && $_->can('json_ld_data')
  6         9  
195             ? $_->json_ld_data
196             : $_; } @$val
197             ];
198             }
199              
200 73 100 66     273 is_Object($val) && $val->can('json_ld_data')
201             ? $val->json_ld_data
202             : $val;
203             }
204              
205             sub json_ld_data {
206 29     29 0 33722 my $self = shift;
207              
208 29         457 my $data = {
209             '@context' => $self->context,
210             '@type' => $self->json_ld_type,
211             };
212              
213 29         3227 foreach my $field (@{$self->json_ld_fields}) {
  29         296  
214              
215 72 100       365 if (is_Ref($field)) {
216              
217 56 50       221 if (is_HashRef($field)) {
218              
219 56         218 my @keys = keys %$field;
220 56         103 my @vals = values %$field;
221              
222             # Originally, this code used 'each', but there seemed
223             # to be some circumstances where the internet iterator
224             # got confused - particularly when an object contained
225             # a sub-object of the same type.
226 56         113 for my $x (0 .. $#keys) {
227 64         116 my $key = $keys[$x];
228 64         81 my $val = $vals[$x];
229              
230 64 100       115 if (defined (my $res = is_CodeRef($val)
    100          
231             ? $val->($self)
232             : $self->$val)) {
233 60         651 $data->{$key} = _resolve_nested($res);
234             }
235             }
236             }
237             else {
238 0         0 carp "Weird JSON-LD reference: " . ref $field;
239 0         0 next;
240             }
241              
242             }
243             else {
244              
245 16 50       235 if (defined (my $res = $self->$field)) {
246 16         86 $data->{$field} = _resolve_nested($res);
247             }
248              
249             }
250              
251             }
252              
253 29         333 return $data;
254             }
255              
256             sub json_ld {
257 9     9 0 1998 my $self = shift;
258              
259 9         160 return $self->json_ld_encoder->encode($self->json_ld_data);
260             }
261              
262             1;
263              
264             =head1 AUTHOR
265              
266             Dave Cross <dave@perlhacks.com>
267              
268             =head1 SEE ALSO
269              
270             perl(1), Moo, Moose, L<https://json-ld.org/>, L<https://schema.org/>
271              
272             L<MooX::JSON_LD> is included in this distribution and provides an alternative
273             interface to the same functionality.
274              
275             =head1 COPYRIGHT AND LICENSE
276              
277             Copyright (C) 2018, Magnum Solutions Ltd. All Rights Reserved.
278              
279             This script is free software; you can redistribute it and/or modify it
280             under the same terms as Perl itself.
281              
282             =cut