File Coverage

blib/lib/PickLE/Document.pm
Criterion Covered Total %
statement 119 142 83.8
branch 32 44 72.7
condition n/a
subroutine 22 26 84.6
pod 10 10 100.0
total 183 222 82.4


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3             =head1 NAME
4              
5             C<PickLE::Document> - Component pick list document abstraction
6              
7             =cut
8              
9              
10             use 5.010;
11 1     1   581 use strict;
  1         3  
12 1     1   4 use warnings;
  1         1  
  1         15  
13 1     1   4 use autodie;
  1         2  
  1         16  
14 1     1   479 use Moo;
  1         12215  
  1         4  
15 1     1   9549 use Carp;
  1         3  
  1         8  
16 1     1   522 use List::Util qw(any);
  1         2  
  1         69  
17 1     1   5 use Scalar::Util qw(reftype);
  1         2  
  1         70  
18 1     1   6  
  1         2  
  1         46  
19             use PickLE::Property;
20 1     1   517 use PickLE::Category;
  1         2  
  1         25  
21 1     1   6 use PickLE::Component;
  1         1  
  1         17  
22 1     1   5  
  1         1  
  1         1360  
23             =head1 SYNOPSIS
24              
25             use PickLE::Document;
26              
27             # Start from scratch.
28             my $doc = PickLE::Document->new;
29             $doc->add_category($category);
30             $doc->save("example.pkl");
31              
32             # Load from file.
33             $doc = PickLE::Document->load("example.pkl");
34              
35             # List all document properties.
36             $doc->foreach_property(sub {
37             my $property = shift;
38             say $property->name . ': ' . $property->value;
39             });
40              
41             # List all components in each category.
42             $doc->foreach_category(sub {
43             my $category = shift;
44             $category->foreach_component(sub {
45             my ($component) = @_;
46             say $component->name;
47             });
48             });
49              
50             =head1 ATTRIBUTES
51              
52             =over 4
53              
54             =item I<properties>
55              
56             List of all of the pick list properties in the document.
57              
58             =cut
59              
60             has properties => (
61             is => 'ro',
62             lazy => 1,
63             default => sub { [] },
64             writer => '_set_properties'
65             );
66              
67             =item I<categories>
68              
69             List of all of the categories available in the document.
70              
71             =cut
72              
73             has categories => (
74             is => 'ro',
75             lazy => 1,
76             default => sub { [] },
77             writer => '_set_categories'
78             );
79              
80             =back
81              
82             =head1 METHODS
83              
84             =over 4
85              
86             =item I<$doc> = C<PickLE::Document>->C<new>()
87              
88             Initializes an empty document object.
89              
90             =item I<$doc> = C<PickLE::Document>->C<load>(I<$filename>)
91              
92             =item I<$doc>->C<load>(I<$filename>)
93              
94             Parses a component pick list file located at I<$filename>. This method can be
95             called statically, or as an object method. In both cases a brand new object will
96             be contructed.
97              
98             =cut
99              
100             my ($proto, $filename) = @_;
101             my $self = (ref $proto) ? $proto : $proto->new;
102 1     1 1 556  
103 1 50       24 # Parse the file.
104             open my $fh, '<:encoding(UTF-8)', $filename;
105             $self->_parse($fh) or return undef;
106 1         20 close $fh;
107 1 50       15970  
108 1         5 return $self;
109             }
110 1         867  
111             =item I<$doc> = C<PickLE::Document>->C<from_string>(I<$str>)
112              
113             =item I<$doc>->C<from_string>(I<$str>)
114              
115             Parses a component pick list document from a string I<$str>. This method can be
116             called statically, or as an object method. In both cases a brand new object will
117             be contructed.
118              
119             =cut
120              
121             my ($proto, $str) = @_;
122             my $self = (ref $proto) ? $proto : $proto->new;
123              
124 1     1 1 588 # Parse the document.
125 1 50       25 open my $fh, '<:encoding(UTF-8)', \$str;
126             $self->_parse($fh) or return undef;
127             close $fh;
128 1         17  
129 1 50       733 return $self;
130 1         4 }
131              
132 1         80 =item I<$doc>->C<add_property>(I<@property>)
133              
134             Adds any number of proprerties in the form of L<PickLE::Property> objects to the
135             document.
136              
137             =cut
138              
139             my $self = shift;
140              
141             # Go through properties adding them to the properties list.
142             foreach my $property (@_) {
143 15     15 1 1069 push @{$self->properties}, $property;
144             }
145             }
146 15         24  
147 15         17 =item I<$doc>->C<add_category>(I<@category>)
  15         213  
148              
149             Adds any number of categories in the form of L<PickLE::Category> objects to the
150             document.
151              
152             =cut
153              
154             my $self = shift;
155              
156             # Go through categories adding them to the categories list.
157             foreach my $category (@_) {
158             push @{$self->categories}, $category;
159 24     24 1 2164 }
160             }
161              
162 24         32 =item I<$doc>->C<foreach_property>(I<$coderef>)
163 24         28  
  24         344  
164             Executes a block of code (I<$coderef>) for each proprety. The property object
165             will be passed as the first argument.
166              
167             =cut
168              
169             my ($self, $coderef) = @_;
170              
171             # Go through the properties.
172             foreach my $property (@{$self->properties}) {
173             # Call the coderef given by the caller.
174             $coderef->($property);
175 10     10 1 15 }
176             }
177              
178 10         13 =item I<$doc>->C<foreach_category>(I<$coderef>)
  10         138  
179              
180 20         65 Executes a block of code (I<$coderef>) for each category available in the
181             document. The category object will be passed as the first argument.
182              
183             =cut
184              
185             my ($self, $coderef) = @_;
186              
187             # Go through the categories.
188             foreach my $category (@{$self->categories}) {
189             $coderef->($category);
190             }
191             }
192 1     1 1 2  
193             =item I<$property> = I<$self>->C<get_property>(I<$name>)
194              
195 1         1 Gets a property of the document by its I<$name> and returns it if found,
  1         16  
196 0         0 otherwise returns C<undef>.
197              
198             =cut
199              
200             my ($self, $name) = @_;
201             my $found = undef;
202              
203             # Go through the properties checking their names for a match.
204             $self->foreach_property(sub {
205             my $property = shift;
206             if ($property->name eq $name) {
207             $found = $property;
208 9     9 1 16 }
209 9         10 });
210              
211             return $found;
212             }
213 17     17   20  
214 17 100       40 =item I<$category> = I<$self>->C<get_category>(I<$name>)
215 6         10  
216             Gets a category in the document by its I<$name> and returns it if found,
217 9         34 otherwise returns C<undef>.
218              
219 9         43 =cut
220              
221             my ($self, $name) = @_;
222             my $found = undef;
223              
224             # Go through the categories checking their names for a match.
225             $self->foreach_category(sub {
226             my $category = shift;
227             if ($category->name eq $name) {
228             $found = $category;
229             }
230 0     0 1 0 });
231 0         0  
232             return $found;
233             }
234              
235 0     0   0 =item I<$doc>->C<save>(I<$filename>)
236 0 0       0  
237 0         0 Saves the document object to a file.
238              
239 0         0 =cut
240              
241 0         0 my ($self, $filename) = @_;
242              
243             # Write object to file.
244             open my $fh, '>:encoding(UTF-8)', $filename;
245             print $fh $self->as_string;
246             close $fh;
247             }
248              
249             =item I<$doc>->C<as_string>()
250              
251 0     0 1 0 String representation of this object, just like it is representated in the file.
252              
253             =cut
254 0         0  
255 0         0 my ($self) = @_;
256 0         0 my $str = "";
257              
258             # Check if we have the required Name property.
259             if (not defined $self->get_property('Name')) {
260             carp "Document can't be represented because the required 'Name' " .
261             'property is not defined';
262             return '';
263             }
264              
265             # Check if we have the required Revision property.
266 4     4 1 1854 if (not defined $self->get_property('Revision')) {
267 4         7 carp "Document can't be represented because the required 'Revision' " .
268             'property is not defined';
269             return '';
270 4 100       8 }
271 1         161  
272             # Check if we have the required Description property.
273 1         52 if (not defined $self->get_property('Description')) {
274             carp "Document can't be represented because the required 'Description' " .
275             'property is not defined';
276             return '';
277 3 100       7 }
278 1         77  
279             # Go through properties getting their string representations.
280 1         60 $self->foreach_property(sub {
281             my $property = shift;
282             $str .= $property->as_string . "\n";
283             });
284 2 100       8  
285 1         77 # Add the header section separator.
286             $str .= "\n---\n\n";
287 1         75  
288             # Go through categories getting their string representations.
289             $self->foreach_category(sub {
290             my $category = shift;
291             $str .= $category->as_string . "\n";
292 3     3   4  
293 3         11 # Go through components getting their string representations.
294 1         11 $category->foreach_component(sub {
295             my $component = shift;
296             $str .= $component->as_string . "\n\n";
297 1         4 });
298             });
299              
300             return $str;
301 0     0   0 }
302 0         0  
303             =back
304              
305             =head1 PRIVATE METHODS
306 0         0  
307 0         0 =over 4
308 0         0  
309 1         6 =item I<$status> = I<$self>->C<_parse>(I<$fh>)
310              
311 1         6 Parses the contents of a file or scalar handle (I<$fh>) and populates the
312             object. Returns C<0> if there were parsing errors.
313              
314             =cut
315              
316             my ($self, $fh) = @_;
317             my $status = 1;
318             my $phases = {
319             empty => 0,
320             property => 1,
321             descriptor => 2,
322             refdes => 3,
323             };
324             my $phase = $phases->{property};
325             my $component = undef;
326             my $category = undef;
327              
328 2     2   16 # Go through the file line-by-line.
329 2         8 while (my $line = <$fh>) {
330 2         8 # Clean up the line string.
331             $line =~ s/^\s+|[\s\r\n]+$//g;
332              
333             # Check if we are about to parse a descriptor.
334             if ($phase == $phases->{empty}) {
335             if (substr($line, 0, 1) eq '[') {
336 2         4 # Looks like we have to parse a descriptor line.
337 2         4 $phase = $phases->{descriptor};
338 2         3 } elsif (substr($line, -1, 1) eq ':') {
339             # Got a category line.
340             if (defined $category) {
341 2         64 # Append the last category we parsed to the list.
342             $self->add_category($category);
343 206         1249 }
344              
345             # Parse the new category.
346 206 100       419 $category = PickLE::Category->from_line($line);
    100          
    50          
347 134 100       383 if (not defined $category) {
    100          
    50          
348             # Looks like the category line was malformed.
349 56         70 carp "Error parsing category '$line'";
350             $status = 0;
351             }
352 22 100       29  
353             next;
354 20         39 } elsif ($line eq '') {
355             # Just another empty line...
356             next;
357             }
358 22         187 } elsif ($phase == $phases->{refdes}) {
359 22 50       43 # Parse the reference designators.
360             $component->parse_refdes_line($line);
361 0         0  
362 0         0 # Append the component to the pick list and go to the next line.
363             $category->add_component($component);
364             $component = undef;
365 22         81 $phase = $phases->{empty};
366             next;
367             } elsif ($phase == $phases->{property}) {
368 56         196 # Looks like we are in the properties header.
369             if ($line eq '') {
370             # Just another empty line...
371             next;
372 56         128 } elsif ($line eq '---') {
373             # We've finished parsing the properties header.
374             $phase = $phases->{empty};
375 56         149 next;
376 56         242 }
377 56         76
378 56         164 # Parse the property.
379             my $prop = PickLE::Property->from_line($line);
380             if (not defined $prop) {
381 16 100       31 # Looks like the property line was malformed.
    100          
382             carp "Error parsing property '$line'";
383 2         4 $status = 0;
384             }
385              
386 2         4 # Append the property to the properties list of the document.
387 2         4 $self->add_property($prop);
388             next;
389             }
390              
391 12         32 # Parse the descriptor line into a component.
392 12 50       21 $component = PickLE::Component->from_line($line);
393             $phase = $phases->{refdes};
394 0         0 if (not defined $component) {
395 0         0 # Looks like the descriptor line was malformed.
396             carp "Error parsing component descriptor '$line'";
397             $status = 0;
398             }
399 12         23 }
400 12         88  
401             # Make sure we get that last category.
402             if (defined $category) {
403             # Append the last category we parsed to the list.
404 56         126 $self->add_category($category);
405 56         82 }
406 56 50       191  
407             return $status;
408 0         0 }
409 0         0  
410             1;
411              
412              
413             =back
414 2 50       7  
415             =head1 AUTHOR
416 2         5  
417             Nathan Campos <nathan@innoveworkshop.com>
418              
419 2         20 =head1 COPYRIGHT
420              
421             Copyright (c) 2022- Nathan Campos.
422              
423             =cut