File Coverage

blib/lib/Poz/Types/object.pm
Criterion Covered Total %
statement 82 82 100.0
branch 17 18 94.4
condition 8 9 88.8
subroutine 18 18 100.0
pod 6 6 100.0
total 131 133 98.5


line stmt bran cond sub pod time code
1             package Poz::Types::object;
2 11     11   210 use 5.032;
  11         41  
3 11     11   59 use strict;
  11         21  
  11         283  
4 11     11   47 use warnings;
  11         22  
  11         565  
5 11     11   64 use Carp ();
  11         21  
  11         350  
6 11     11   57 use Try::Tiny;
  11         20  
  11         1041  
7 11     11   62 use parent 'Poz::Types';
  11         20  
  11         78  
8              
9             sub new {
10 13     13 1 22 my ($class, $struct) = @_;
11 13         67 my $self = bless {
12             __struct__ => {},
13             __as__ => undef,
14             __is__ => undef,
15             }, $class;
16 13         41 for my $key (keys %$struct) {
17 24         35 my $v = $struct->{$key};
18 24 50       80 if ($v->isa('Poz::Types')) {
19 24         57 $self->{__struct__}{$key} = $v;
20             }
21             }
22 13         48 return $self;
23             }
24              
25             sub as {
26 7     7 1 12 my ($self, $typename) = @_;
27 7         11 $self->{__as__} = $typename;
28 7         17 return $self;
29             }
30              
31             sub is {
32 1     1 1 3 my ($self, $typename) = @_;
33 1         3 $self->{__is__} = $typename;
34 1         3 return $self;
35             }
36              
37             sub constructor {
38 1     1 1 3 my ($self) = @_;
39 1         2 my $caller_class = caller();
40 11     11   3801 no strict 'refs';
  11         22  
  11         9079  
41 1         10 *{"$caller_class\::new"} = sub {
42 1     1   14 my ($class, %args) = @_;
43 1         7 return $self
44             ->as($caller_class)
45             ->parse({%args});
46             }
47 1         16 }
48              
49             sub parse {
50 14     14 1 4821 my ($self, $data) = @_;
51 14         77 my ($valid, $errors) = $self->safe_parse($data);
52 14 100       29 if ($errors) {
53 3         10 my $error_message = _errors_to_string($errors);
54 3         49 Carp::croak($error_message);
55             }
56 11         36 return $valid;
57             }
58              
59             sub safe_parse {
60 19 100   19 1 4843 Carp::croak "Must handle error" unless wantarray;
61              
62 18         32 my ($self, $data) = @_;
63 18         29 my @errors = ();
64 18         29 my $valid = {};
65 18 100       41 if (!_is_hashref_or_object($data)) {
66 1         6 push @errors, {
67             key => '(root)',
68             error => "Invalid data: is not hashref"
69             };
70             } else {
71 17 100       50 if ($self->{__is__}) {
72 2         4 my $is = $self->{__is__};
73 2 100       11 if (!$data->isa($is)) {
74 1         8 push @errors, {
75             key => '(root)',
76             error => "Invalid data: is not $is"
77             };
78             }
79             }
80 17         30 for my $key (sort keys %{$self->{__struct__}}) {
  17         76  
81 47         473 my $v = $self->{__struct__}{$key};
82 47         95 my $val = $data->{$key};
83            
84             # if the value is not defined, try to transform it
85 47 100 100     108 if (!defined $val && scalar(@{$v->{transform}}) > 0) {
  14         50  
86 13         14 for my $transformer (@{$v->{transform}}) {
  13         28  
87 13         28 $val = $transformer->($v, $val);
88             }
89             }
90              
91             try {
92 47     47   2864 my $_parsed = $v->parse($val);
93 45         150 $valid->{$key} = $_parsed;
94             } catch {
95 2     2   948 my $error_message = $_;
96 2         18 $error_message =~ s/ at .+ line [0-9]+\.\n//;
97 2         27 push @errors, {
98             key => $key,
99             error => $error_message,
100             };
101 47         2491 };
102             }
103             }
104 18 100       271 if (scalar(@errors) > 0) {
105 4         22 return (undef, [@errors])
106             }
107 14   100     47 my $classname = $self->{__as__} || $self->{__is__};
108 14 100       96 $valid = bless $valid, $classname if $classname;
109 14         39 return ($valid, undef);
110             }
111              
112             sub _is_hashref_or_object {
113 18     18   26 my $data = shift;
114 18   66     155 return defined $data && (ref($data) eq 'HASH' || $data->isa('HASH'));
115             }
116              
117             sub _errors_to_string {
118 3     3   6 my $errors = shift;
119 3         5 my @error_strings = ();
120 3         7 for my $error (@$errors) {
121 3         15 push @error_strings, sprintf("%s on key `%s`", $error->{error}, $error->{key});
122             }
123 3         11 return join(", and ", @error_strings);
124             }
125              
126             1;
127              
128             =head1 NAME
129              
130             Poz::Types::object - A module for handling structured data with type validation
131              
132             =head1 SYNOPSIS
133              
134             use Poz qw/z/;
135              
136             # Schema for a person, cast to Some::Class when valid
137             my $object = z->object({
138             name => z->string,
139             age => z->number,
140             })->as('Some::Class');
141             my $data = {
142             name => 'John Doe',
143             age => 30,
144             };
145             my $parsed_data = $object->parse($data); # isa Some::Class
146              
147             # Schema for a person, validate that the data is an instance of Some::Class
148             my $another_object = z->object({
149             name => z->string,
150             age => z->number,
151             })->is('Another::Class');
152             my $other = bless {
153             name => 'John Doe',
154             age => 30,
155             }, 'Another::Class';
156             my $someone = bless {
157             name => 'Jane Doe',
158             age => 25,
159             }, 'Some::Class';
160             my $parsed_data = $another_object->parse($other); # isa Another::Class
161             my $someone_else = $another_object->parse($someone); # throws an exception, because not an instance of Another::Class
162              
163             # or use Poz as your class constructor
164             {
165             package My::Class;
166             use Poz qw/z/;
167             z->object({
168             name => z->string,
169             age => z->number,
170             })->constructor;
171             }
172             my $instance = My::Class->new(
173             name => 'Alice',
174             age => 20,
175             );
176              
177              
178             =head1 DESCRIPTION
179              
180             Poz::Types::object is a module for handling structured data with type validation. It allows you to define a structure with specific types and validate data against this structure.
181              
182             =head1 METHODS
183              
184             =head2 as
185              
186             $object->as($typename);
187              
188             Sets the class name to bless the parsed data into. The C<$typename> parameter should be a string representing the class name.
189              
190             =head2 is
191              
192             $object->is($typename);
193              
194             Validates that the parsed data is an instance of the given class. The C<$typename> parameter should be a string representing the class name.
195              
196             =head2 constructor
197              
198             $object->constructor;
199              
200             Creates a constructor method into your class.
201              
202             =head2 parse
203              
204             my $parsed_data = $object->parse($data);
205              
206             Parses and validates the given data against the structure. If the data is valid, it returns the parsed data. If the data is invalid, it throws an exception with the validation errors.
207              
208             =head2 safe_parse
209              
210             my ($valid, $errors) = $object->safe_parse($data);
211              
212             Parses and validates the given data against the structure. If the data is valid, it returns the parsed data and undef for errors. If the data is invalid, it returns undef for valid data and an array reference of errors.
213              
214             =head1 LICENSE
215              
216             Copyright (C) ytnobody.
217              
218             This library is free software; you can redistribute it and/or modify
219             it under the same terms as Perl itself.
220              
221             =head1 AUTHOR
222              
223             ytnobody E<lt>ytnobody@gmail.comE<gt>
224              
225             =cut