File Coverage

blib/lib/Whelk/Schema/Definition.pm
Criterion Covered Total %
statement 92 99 92.9
branch 21 24 87.5
condition 9 11 81.8
subroutine 23 25 92.0
pod 10 11 90.9
total 155 170 91.1


line stmt bran cond sub pod time code
1             package Whelk::Schema::Definition;
2             $Whelk::Schema::Definition::VERSION = '1.04';
3 25     25   12639 use Whelk::StrictBase;
  25         116  
  25         253  
4 25     25   212 use Carp;
  25         60  
  25         2114  
5 25     25   206 use Kelp::Util;
  25         198  
  25         1067  
6 25     25   148 use Scalar::Util qw(blessed);
  25         104  
  25         1807  
7 25     25   3279 use Clone qw();
  25         3533  
  25         808  
8 25     25   141 use Data::Dumper;
  25         40  
  25         11636  
9 25     25   4160 use JSON::PP;
  25         97383  
  25         2308  
10              
11 25     25   15200 use Whelk::Schema::ExtraRule;
  25         127  
  25         155  
12              
13             # no import loop, load Whelk::Schema for child classes
14             require Whelk::Schema;
15              
16             our @CARP_NOT = qw(Whelk::Schema);
17              
18             attr name => undef;
19             attr '?required' => !!1;
20             attr '?nullable' => !!0;
21             attr '?description' => undef;
22             attr '?rules' => sub { [] };
23              
24             sub create
25             {
26 302     302 1 755 my ($class, $args) = @_;
27              
28 302         831 return $class->_build($args);
29             }
30              
31             sub new
32             {
33 522     522 0 5926 my ($class, %args) = @_;
34              
35             # don't allow to set the name through the constructor. This would not
36             # correctly register the schema. Schemas are correctly built and registered
37             # through Whelk::Schema factory.
38 522         953 delete $args{name};
39              
40 522         1831 my $self = $class->SUPER::new(%args);
41 518   50     3555 $self->rules([map { Whelk::Schema::ExtraRule->new(%$_) } @{$self->rules // []}]);
  8         151  
  518         1630  
42              
43 518         3494 $self->_resolve;
44 518         2689 return $self;
45             }
46              
47             sub _bool
48             {
49 13 100   13   158 return pop() ? JSON::PP::true : JSON::PP::false;
50             }
51              
52       332     sub _resolve { }
53              
54             sub _build
55             {
56 600     600   1386 my ($self, $item) = @_;
57              
58 600 100 66     1791 if (blessed $item && $item->isa(__PACKAGE__)) {
59 31         124 return $item;
60             }
61 569 100       2113 if (ref $item eq 'SCALAR') {
    100          
    50          
62 47         234 return Whelk::Schema->get_by_name($$item);
63             }
64             elsif (ref $item eq 'ARRAY') {
65 25         67 my ($type, @rest) = @$item;
66 25         72 my $ret = $self->_build($type)->clone(@rest);
67 25         122 return $ret;
68             }
69             elsif (ref $item eq 'HASH') {
70 497         769 my %data = %{$item};
  497         1811  
71 497         1259 my $type = delete $data{type};
72 497 50       1288 croak 'no schema definition type specified'
73             unless defined $type;
74              
75 497         829 my $class = __PACKAGE__;
76 497         1084 $type = ucfirst $type;
77              
78 497         2066 return Kelp::Util::load_package("${class}::${type}")->new(%data);
79             }
80             else {
81 0         0 croak 'can only build a definition from SCALAR, ARRAY or HASH';
82             }
83             }
84              
85             sub _valid_nullable
86             {
87             # this will get executed a lot, so skip unpacking @_
88 719   100 719   2717 return !defined $_[1] && $_[0]->nullable;
89             }
90              
91             sub _inhale_extra_rules
92             {
93 322     322   662 my ($self, $value) = @_;
94              
95 322         493 foreach my $rule (@{$self->rules}) {
  322         1635  
96 30         221 my $error = $rule->inhale($value);
97 30 100       440 return $error if defined $error;
98             }
99              
100 313         2297 return undef;
101             }
102              
103             sub _openapi_dump_extra_rules
104             {
105 76     76   141 my ($self) = @_;
106              
107 76         119 my %result;
108 76         111 foreach my $rule (@{$self->rules}) {
  76         185  
109 1         8 %result = (%result, %{$rule->openapi});
  1         56  
110             }
111              
112 76         628 return \%result;
113             }
114              
115             sub clone
116             {
117 25     25 1 91 my ($self, %more_data) = @_;
118 25         59 my $class = ref $self;
119              
120             # NOTE: since cloning uses the constructor, the name is automatically
121             # removed from the resulting object.
122              
123 25         40 my $data = Clone::clone({%{$self}});
  25         510  
124 25         158 $data = Kelp::Util::merge($data, \%more_data, 1);
125 25         1307 return $class->new(%$data);
126             }
127              
128             sub empty
129             {
130 258     258 1 1553 return !!0;
131             }
132              
133             sub has_default
134             {
135 4     4 1 16 return !!0;
136             }
137              
138             sub inhale_exhale
139             {
140 52     52 1 30495 my ($self, $data, $error_sub) = @_;
141              
142 52         211 $self->inhale_or_error($data, $error_sub);
143 39         205 return $self->exhale($data);
144             }
145              
146             sub inhale_or_error
147             {
148 90     90 1 1245 my ($self, $data, $error_sub) = @_;
149              
150 90         378 my $inhaled = $self->inhale($data);
151 90 100       319 if (defined $inhaled) {
152 23 50       139 $error_sub->($inhaled)
153             if ref $error_sub eq 'CODE';
154              
155             # generic error in case $error_sub was not passed or did not throw
156 0         0 local $Data::Dumper::Sortkeys = 1;
157 0         0 die "incorrect data: " . Dumper({schema => $self, data => $data, hint => $inhaled});
158             }
159              
160 67         139 return undef;
161             }
162              
163             sub openapi_schema
164             {
165 129     129 1 849 my ($self, $openapi_obj, %hints) = @_;
166              
167 129 100 100     380 if ($self->name && !$hints{full}) {
168             return {
169 45         404 '$ref' => $openapi_obj->location_for_schema($self->name),
170             };
171             }
172             else {
173 84         820 return $self->openapi_dump($openapi_obj, %hints);
174             }
175             }
176              
177             sub openapi_dump
178             {
179 76     76 1 190 my ($self, $obj, %hints) = @_;
180              
181             # incomplete, must be complimented in child classes
182             my $res = {
183 76         132 %{$self->_openapi_dump_extra_rules},
  76         213  
184             };
185              
186 76 100       235 if (defined $self->description) {
187 2         15 $res->{description} = $self->description;
188             }
189              
190 76 100       758 if ($self->nullable) {
191 1         9 $res->{nullable} = JSON::PP::true;
192             }
193              
194 76         627 return $res;
195             }
196              
197             sub exhale
198             {
199 0     0 1   my ($self, $value) = @_;
200 0           ...;
201             }
202              
203             sub inhale
204             {
205 0     0 1   my ($self, $value) = @_;
206 0           ...;
207             }
208              
209             1;
210              
211             __END__
212              
213             =pod
214              
215             =head1 NAME
216              
217             Whelk::Schema::Definition - Base class for a Whelk type
218              
219             =head1 SYNOPSIS
220              
221             my $definition = Whelk::Schema->build(
222             name => {
223             type => 'integer',
224             }
225             );
226              
227             =head1 DESCRIPTION
228              
229             Definition is a base class for schemas. L<Whelk::Schema> is just a factory and
230             register for definitions. This class is abstract and does not do anything by
231             itself, but a number of subclasses exist which implement different OpenAPI
232             types.
233              
234             Definitions use names I<inhale> to describe data validation and I<exhale> for
235             data coercion. Inhaling is recursively checking the entire input to see if it
236             conforms to the definition. Exhaling is recursively adjusting the entire output
237             structure so that it has all the values in line with the definition (for
238             example, changing a C<boolean> field to a real boolean on endpoint output).
239             Exhaling is not a standalone process, as it assumes data was inhaled previously
240             - exhaling without inhaling can lead to problems like warnings or even fatal
241             errors.
242              
243             Inhaling will short-circuit if it encounters an error and return a string which
244             describes where and what type of problem it encountered. For example, it may
245             return C<'boolean'> if a definition was boolean and the value was a string. For
246             the same boolean definition, the error may also be C<'defined'> if the value
247             was not defined - the string is not always the name of the type, but rather
248             which Whelk assumption has failed (which may be a more basic assumption than
249             the actual type). For nested types like objects it will return something like
250             C<< 'object[key]->boolean' >>. It should be pretty obvious where the problem is
251             based on those strings, but since it short circuits it may require a couple of
252             runs to weed out all the errors.
253              
254             =head1 ATTRIBUTES
255              
256             There attributes are common for all definitions.
257              
258             =head2 name
259              
260             Name of this schema, cannot be set in the constructor - is only set through
261             creating a named schema in L<Whelk::Schema/build>.
262              
263             =head2 required
264              
265             Whether this definition is required. It's needed for cases where it is nested
266             inside an object or inside C<parameters> for an endpoint.
267              
268             =head2 nullable
269              
270             Whether this definiton can be C<null> regardless of type. False by default.
271              
272             =head2 description
273              
274             OpenAPI description of this definition.
275              
276             =head2 rules
277              
278             An array reference of extra validation rules.
279              
280             =head2 code
281              
282             A status code number used in the response. Only used for response schema
283             definitions.
284              
285             =head1 METHODS
286              
287             =head2 create
288              
289             Constructs a definition. Unlike C<new> which only accepts a hash, it does all
290             the tricks described in L<Whelk::Schema/Defining a schema>. Should not be
291             called directly, use L<Whelk::Schema/build> instead.
292              
293             =head2 clone
294              
295             my $new = $definition->clone(%more_data);
296              
297             Clones this definition and optionally merges its contents with C<%more_data>,
298             if present. The merge is recursive is the done in the same way Kelp config
299             files are merged. It's used for extending schemas using C<< [\'schema_name',
300             %args] >> syntax. There should be no need to ever call this directly.
301              
302             =head2 empty
303              
304             my $is_empty = $definition->empty;
305              
306             Whether this definition is empty. It is a special measure to check for
307             C<Whelk::Schema::Definition::Empty>, which is implementing C<204 No Content>
308             responses.
309              
310             =head2 has_default
311              
312             Whether this definition has a default value.
313              
314             =head2 inhale
315              
316             my $error_or_undef = $definition->inhale($data);
317              
318             Must be implemented in a subclass.
319              
320             Inhales data to see if it likes it. See L</DESCRIPTION> for more data on
321             inhaling and exhaling.
322              
323             =head2 exhale
324              
325             my $adjusted_data = $definition->exhale($data);
326              
327             Must be implemented in a subclass.
328              
329             Exhales the data in form described in the definition. See L</DESCRIPTION> for
330             more data on inhaling and exhaling.
331              
332             =head2 inhale_or_error
333              
334             $definition->inhale_or_error($data, $error_sub = sub {});
335              
336             Calls L</inhale> and calls C<$error_sub> if it failed. The sub will get passed
337             the return value of C<inhale> as its only argument. If the sub is not passed or
338             does not throw an exception, a stock exception will be thrown with the error
339             and dumped C<$data>.
340              
341             =head2 inhale_exhale
342              
343             my $adjusted_data = $definition->inhale_exhale($data, $error_sub = sub {});
344              
345             Both L</inhale> and L</exhale> in one call. Uses L</inhale_or_error> under the
346             hood, so inhaling errors will throw an exception.
347              
348             =head2 openapi_dump
349              
350             my $perl_struct = $definition->openapi_dump($obj, %hints);
351              
352             Must be implemented in a subclass.
353              
354             Returns the structure which describes this type for the OpenAPI document.
355             Should not be called directly, as it is called by L</openapi_schema>.
356              
357             Base class implementation returns some partial data, so it may be handy to call
358             it while reimplementing.
359              
360             =head2 openapi_schema
361              
362             my $perl_struct = $definition->openapi_schema($obj, %hints);
363              
364             Returns the structure which describes this type for the OpenAPI document. It
365             usually just calls L</openapi_dump>.
366              
367             C<$obj> should be an object of L<Whelk::OpenAPI> or similar. It should at least
368             implement method C<location_for_schema>.
369              
370             C<%hints> are special hints which change how the schema is produced. Currently,
371             just a couple hints are defined:
372              
373             If C<full> hint is present and true, the top level definition will be dumped in
374             full, even if it is a named schema. If not, it will be made a reference to a
375             predefined schema.
376              
377             Special C<parameters> hint will change how C<object> is treated, since objects
378             are used to define all types of parameters of OpenAPI.
379