File Coverage

blib/lib/Config/JSON.pm
Criterion Covered Total %
statement 155 170 91.1
branch 37 42 88.1
condition n/a
subroutine 24 25 96.0
pod 13 15 86.6
total 229 252 90.8


line stmt bran cond sub pod time code
1             package Config::JSON;
2             $Config::JSON::VERSION = '1.5202';
3 5     5   106520 use strict;
  5         9  
  5         146  
4 5     5   2302 use Moo;
  5         55595  
  5         23  
5 5     5   5961 use Carp;
  5         8  
  5         276  
6 5     5   21 use File::Spec;
  5         8  
  5         148  
7 5     5   2393 use JSON 2.0;
  5         36395  
  5         26  
8 5     5   580 use List::Util;
  5         14  
  5         301  
9              
10 5     5   21 use constant FILE_HEADER => "# config-file-type: JSON 1\n";
  5         7  
  5         8134  
11              
12             #-------------------------------------------------------------------
13             has config => (
14             is => 'rw',
15             default => sub {{}},
16             );
17              
18             #-------------------------------------------------------------------
19             sub getFilePath {
20 2     2 0 810 my $self = shift;
21 2         12 return $self->pathToFile;
22             }
23              
24             #-------------------------------------------------------------------
25             has pathToFile => (
26             is => 'ro',
27             required => 1,
28             trigger => sub {
29             my ($self, $pathToFile, $old) = @_;
30             if (open(my $FILE, "<", $pathToFile)) {
31             # slurp
32             local $/ = undef;
33             my $json = <$FILE>;
34             close($FILE);
35             my $conf = eval { JSON->new->relaxed->utf8->decode($json); };
36             confess "Couldn't parse JSON in config file '$pathToFile'\n" unless ref $conf;
37             $self->config($conf);
38            
39             # process includes
40             my @includes = map { glob $_ } @{ $self->get('includes') || [] };
41             my @loadedIncludes;
42             foreach my $include (@includes) {
43             push @loadedIncludes, __PACKAGE__->new(pathToFile=>$include, isInclude=>1);
44             }
45             $self->includes(\@loadedIncludes);
46             }
47             else {
48             confess "Cannot read config file: ".$pathToFile;
49             }
50             },
51             );
52              
53             #-------------------------------------------------------------------
54             has isInclude => (
55             is => 'ro',
56             default => sub {0},
57             );
58              
59             #-------------------------------------------------------------------
60             has includes => (
61             is => 'rw',
62             default => sub {[]},
63             );
64              
65             #-------------------------------------------------------------------
66             sub getIncludes {
67 0     0 0 0 my $self = shift;
68 0         0 return $self->includes;
69             }
70              
71             #-------------------------------------------------------------------
72             around BUILDARGS => sub {
73             my $orig = shift;
74             my $class = shift;
75             if ( @_ == 1 && ! ref $_[0] ) {
76             return $class->$orig(pathToFile => $_[0]);
77             }
78             else {
79             return $class->$orig(@_);
80             }
81             };
82              
83             #-------------------------------------------------------------------
84             sub addToArray {
85 2     2 1 6 my ($self, $property, $value) = @_;
86 2         6 my $array = $self->get($property);
87 2 50   6   8 unless (defined List::Util::first { $value eq $_ } @{$array}) { # check if it already exists
  6         10  
  2         9  
88             # add it
89 2         3 push(@{$array}, $value);
  2         3  
90 2         5 $self->set($property, $array);
91             }
92             }
93              
94             #-------------------------------------------------------------------
95             sub addToArrayAfter {
96 3     3 1 6 my ($self, $property, $afterValue, $value) = @_;
97 3         7 my $array = $self->get($property);
98 3 100   10   12 unless (defined List::Util::first { $value eq $_ } @{ $array }) { # check if it already exists
  10         15  
  3         11  
99 2         2 my $idx = 0;
100 2         3 for (; $idx < $#{ $array }; $idx++) {
  6         13  
101 5 100       13 if ($array->[$idx] eq $afterValue) {
102 1         2 last;
103             }
104             }
105 2         2 splice @{ $array }, $idx + 1, 0, $value;
  2         6  
106 2         6 $self->set($property, $array);
107             }
108             }
109              
110             #-------------------------------------------------------------------
111             sub addToArrayBefore {
112 3     3 1 8 my ($self, $property, $beforeValue, $value) = @_;
113 3         5 my $array = $self->get($property);
114 3 100   9   10 unless (defined List::Util::first { $value eq $_ } @{ $array }) { # check if it already exists
  9         13  
  3         8  
115 2         2 my $idx = $#{ $array };
  2         4  
116 2         6 for (; $idx > 0; $idx--) {
117 5 100       14 if ($array->[$idx] eq $beforeValue) {
118 1         2 last;
119             }
120             }
121 2         3 splice @{ $array }, $idx , 0, $value;
  2         5  
122 2         4 $self->set($property, $array);
123             }
124             }
125              
126             #-------------------------------------------------------------------
127             sub addToHash {
128 3     3 1 7 my ($self, $property, $key, $value) = @_;
129 3         10 $self->set($property."/".$key, $value);
130             }
131              
132             #-------------------------------------------------------------------
133             sub create {
134 7     7 1 92436 my ($class, $filename) = @_;
135 7 50       365 if (open(my $FILE,">",$filename)) {
136 7         51 print $FILE FILE_HEADER."\n{ }\n";
137 7         249 close($FILE);
138             }
139             else {
140 0         0 warn "Can't write to config file ".$filename;
141             }
142 7         159 return $class->new(pathToFile=>$filename);
143             }
144              
145             #-------------------------------------------------------------------
146             sub delete {
147 29     29 1 51 my ($self, $param) = @_;
148            
149             # inform the includes
150 29         24 foreach my $include (@{$self->includes}) {
  29         77  
151 16         58 $include->delete($param);
152             }
153            
154             # find the directive
155 29         49 my $directive = $self->config;
156 29         46 my @parts = $self->splitKeyParts($param);
157 29         57 my $lastPart = pop @parts;
158 29         34 foreach my $part (@parts) {
159 22         36 $directive = $directive->{$part};
160             }
161            
162             # only delete it if it exists
163 29 100       73 if (exists $directive->{$lastPart}) {
164 18         37 delete $directive->{$lastPart};
165 18         35 $self->write;
166             }
167             }
168              
169             #-------------------------------------------------------------------
170             sub deleteFromArray {
171 2     2 1 4 my ($self, $property, $value) = @_;
172 2         4 my $array = $self->get($property);
173 2         5 for (my $i = 0; $i < scalar(@{$array}); $i++) {
  8         14  
174 8 100       19 if ($array->[$i] eq $value) {
175 2         3 splice(@{$array}, $i, 1);
  2         5  
176 2         3 last;
177             }
178             }
179 2         7 $self->set($property, $array);
180             }
181              
182             #-------------------------------------------------------------------
183             sub deleteFromHash {
184 2     2 1 252 my ($self, $property, $key) = @_;
185 2         5 $self->delete($property."/".$key);
186             }
187              
188             #-------------------------------------------------------------------
189             sub get {
190 122     122 1 1002 my ($self, $property) = @_;
191              
192             # they want a specific property
193 122 50       256 if (defined $property) {
194              
195             # look in this config
196 122         199 my $value = $self->config;
197 122         205 foreach my $part ($self->splitKeyParts($property)) {
198 163         151 $value = eval{$value->{$part}};
  163         252  
199 163 100       373 if ($@) {
200 1         296 confess "Can't access $property. $@";
201             }
202             }
203 121 100       437 return $value if (defined $value);
204              
205             # look through includes
206 59         52 foreach my $include (@{$self->includes}) {
  59         121  
207 34         69 my $value = $include->get($property);
208 34 100       114 return $value if (defined $value);
209             }
210              
211             # didn't find it
212 47         210 return undef;
213             }
214            
215             # they want the whole properties list
216 0         0 my %whole = ();
217 0         0 foreach my $include (@{$self->includes}) {
  0         0  
218 0         0 %whole = (%whole, %{$include->get});
  0         0  
219             }
220 0         0 %whole = (%whole, %{$self->config});
  0         0  
221 0         0 return \%whole;
222             }
223              
224             #-------------------------------------------------------------------
225             sub getFilename {
226 2     2 1 4 my $self = shift;
227 2         15 my @path = split "/", $self->pathToFile;
228 2         10 return pop @path;
229             }
230              
231             #-------------------------------------------------------------------
232             sub set {
233 35     35 1 440 my ($self, $property, $value) = @_;
234              
235             # see if the directive exists in this config
236 35         64 my $directive = $self->config;
237 35         58 my @parts = $self->splitKeyParts($property);
238 35         38 my $numParts = scalar @parts;
239 35         84 for (my $i=0; $i < $numParts; $i++) {
240 41         43 my $part = $parts[$i];
241 41 100       77 if (exists $directive->{$part}) { # exists so we continue
242 23 100       39 if ($i == $numParts - 1) { # we're on the last part
243 17         24 $directive->{$part} = $value;
244 17         36 $self->write;
245 17         56 return 1;
246             }
247             else {
248 6         16 $directive = $directive->{$part};
249             }
250             }
251             else { # doesn't exist so we quit
252 18         20 last;
253             }
254             }
255              
256             # see if any of the includes have this directive
257 18         17 foreach my $include (@{$self->includes}) {
  18         48  
258 9         21 my $found = $include->set($property, $value);
259 9 100       25 return 1 if ($found);
260             }
261              
262             # let's create the directive new in this config if it's not an include
263 15 100       39 unless ($self->isInclude) {
264 9         28 $directive = $self->config;
265 9         12 my $lastPart = pop @parts;
266 9         12 foreach my $part (@parts) {
267 6 100       14 unless (exists $directive->{$part}) {
268 3         12 $directive->{$part} = {};
269             }
270 6         9 $directive = $directive->{$part};
271             }
272 9         19 $directive->{$lastPart} = $value;
273 9         18 $self->write;
274 9         31 return 1;
275             }
276              
277             # didn't find a place to write it
278 6         11 return 0;
279             }
280              
281             #-------------------------------------------------------------------
282             sub splitKeyParts {
283 186     186 1 235 my ($self, $key) = @_;
284 186         629 my @parts = split /(?
285 186         307 map {s{\\\/}{/}} @parts;
  258         473  
286 186         438 return @parts;
287             }
288              
289             #-------------------------------------------------------------------
290             sub write {
291 44     44 1 47 my $self = shift;
292 44         74 my $realfile = $self->pathToFile;
293              
294             # convert data to json
295 44         858 my $json = JSON->new->pretty->utf8->canonical->encode($self->config);
296              
297 44         185 my $to_write = FILE_HEADER . "\n" . $json;
298 44         53 my $needed_bytes = length $to_write;
299              
300             # open as read/write
301 44 50       1345 open my $fh, '+<:raw', $realfile or confess "Unable to open $realfile for write: $!";
302 44         217 my $current_bytes = (stat $fh)[7];
303             # shrink file if needed
304 44 100       127 if ($needed_bytes < $current_bytes) {
    100          
305 22         460 truncate $fh, $needed_bytes;
306             }
307             # make sure we can expand the file to the needed size before we overwrite it
308             elsif ($needed_bytes > $current_bytes) {
309 19         47 my $padding = q{ } x ($needed_bytes - $current_bytes);
310 19         31 sysseek $fh, 0, 2;
311 19 50       292 if (! syswrite $fh, $padding) {
312 0         0 sysseek $fh, 0, 0;
313 0         0 truncate $fh, $current_bytes;
314 0         0 close $fh;
315 0         0 confess "Unable to expand $realfile: $!";
316             }
317 19         37 sysseek $fh, 0, 0;
318 19         49 seek $fh, 0, 0;
319             }
320 44         41 print {$fh} $to_write;
  44         274  
321 44         531 close $fh;
322              
323 44         174 return 1;
324             }
325              
326              
327             =head1 NAME
328              
329             Config::JSON - A JSON based config file system.
330              
331             =head1 VERSION
332              
333             version 1.5202
334              
335             =head1 SYNOPSIS
336              
337             use Config::JSON;
338              
339             my $config = Config::JSON->create($pathToFile);
340             my $config = Config::JSON->new($pathToFile);
341             my $config = Config::JSON->new(pathToFile=>$pathToFile);
342              
343             my $element = $config->get($directive);
344              
345             $config->set($directive,$value);
346              
347             $config->delete($directive);
348             $config->deleteFromHash($directive, $key);
349             $config->deleteFromArray($directive, $value);
350              
351             $config->addToHash($directive, $key, $value);
352             $config->addToArray($directive, $value);
353              
354             my $path = $config->pathToFile;
355             my $filename = $config->getFilename;
356              
357             =head2 Example Config File
358              
359             # config-file-type: JSON 1
360             {
361             "dsn" : "DBI:mysql:test",
362             "user" : "tester",
363             "password" : "xxxxxx",
364              
365             # some colors to choose from
366             "colors" : [ "red", "green", "blue" ],
367              
368             # some statistics
369             "stats" : {
370             "health" : 32,
371             "vitality" : 11
372             },
373              
374             # including another file
375             "includes" : ["macros.conf"]
376             }
377              
378              
379             =head1 DESCRIPTION
380              
381             This package parses the config files written in JSON. It also does some non-JSON stuff, like allowing for comments in the files.
382              
383             If you want to see it in action, it is used as the config file system in WebGUI L.
384              
385              
386             =head2 Why?
387              
388             Why build yet another config file system? Well there are a number
389             of reasons: We used to use other config file parsers, but we kept
390             running into limitations. We already use JSON in our app, so using
391             JSON to store config files means using less memory because we already
392             have the JSON parser in memory. In addition, with JSON we can have
393             any number of hierarchcal data structures represented in the config
394             file, whereas most config files will give you only one level of
395             hierarchy, if any at all. JSON parses faster than XML and YAML.
396             JSON is easier to read and edit than XML. Many other config file
397             systems allow you to read a config file, but they don't provide any
398             mechanism or utilities to write back to it. JSON is taint safe.
399             JSON is easily parsed by languages other than Perl when we need to
400             do that.
401              
402              
403             =head2 Multi-level Directives
404              
405             You may of course access a directive called "foo", but since the config is basically a hash you can traverse
406             multiple elements of the hash when specifying a directive name by simply delimiting each level with a slash, like
407             "foo/bar". For example you may:
408              
409             my $vitality = $config->get("stats/vitality");
410             $config->set("stats/vitality", 15);
411              
412             You may do this wherever you specify a directive name.
413              
414              
415             =head2 Comments
416              
417             You can put comments in the config file as long as # is the first non-space character on the line. However, if you use this API to write to the config file, your comments will be eliminated.
418              
419              
420             =head2 Includes
421              
422             There is a special directive called "includes", which is an array of include files that may be brought in to
423             the config. Even the files you include can have an "includes" directive, so you can do hierarchical includes.
424              
425             Any directive in the main file will take precedence over the directives in the includes. Likewise the files
426             listed first in the "includes" directive will have precedence over the files that come after it. When writing
427             to the files, the same precedence is followed.
428              
429             If you're setting a new directive that doesn't currently exist, it will only be written to the main file.
430              
431             If a directive is deleted, it will be deleted from all files, including the includes.
432              
433             =head1 INTERFACE
434              
435             =head2 addToArray ( directive, value )
436              
437             Adds a value to an array directive in the config file.
438              
439             =head3 directive
440              
441             The name of the array.
442              
443             =head3 value
444              
445             The value to add.
446              
447             =head2 addToArrayBefore ( directive, insertBefore, value )
448              
449             Inserts a value into an array immediately before another item. If
450             that item can't be found, inserts at the beginning on the array.
451              
452             =head3 directive
453              
454             The name of the array.
455              
456             =head3 insertBefore
457              
458             The value to search for and base the positioning on.
459              
460             =head3 value
461              
462             The value to insert.
463              
464              
465             =head2 addToArrayAfter ( directive, insertAfter, value )
466              
467             Inserts a value into an array immediately after another item. If
468             that item can't be found, inserts at the end on the array.
469              
470             =head3 directive
471              
472             The name of the array.
473              
474             =head3 insertAfter
475              
476             The value to search for and base the positioning on.
477              
478             =head3 value
479              
480             The value to insert.
481              
482              
483              
484             =head2 addToHash ( directive, key, value )
485              
486             Adds a value to a hash directive in the config file. B This is really the same as
487             $config->set("directive/key", $value);
488              
489             =head3 directive
490              
491             The name of the hash.
492              
493             =head3 key
494              
495             The key to add.
496              
497             =head3 value
498              
499             The value to add.
500              
501              
502             =head2 create ( pathToFile )
503              
504             Constructor. Creates a new empty config file.
505              
506             =head3 pathToFile
507              
508             The path and filename of the file to create.
509              
510              
511              
512             =head2 delete ( directive )
513              
514             Deletes a key from the config file.
515              
516             =head3 directive
517              
518             The name of the directive to delete.
519              
520              
521             =head2 deleteFromArray ( directive, value )
522              
523             Deletes a value from an array directive in the config file.
524              
525             =head3 directive
526              
527             The name of the array.
528              
529             =head3 value
530              
531             The value to delete.
532              
533              
534              
535             =head2 deleteFromHash ( directive, key )
536              
537             Delete a key from a hash directive in the config file. B This is really just the same as doing
538             $config->delete("directive/key");
539              
540             =head3 directive
541              
542             The name of the hash.
543              
544             =head3 key
545              
546             The key to delete.
547              
548              
549              
550             =head2 get ( directive )
551              
552             Returns the value of a particular directive from the config file.
553              
554             =head3 directive
555              
556             The name of the directive to return.
557              
558              
559              
560             =head2 getFilename ( )
561              
562             Returns the filename for this config.
563              
564              
565              
566             =head2 pathToFile ( )
567              
568             Returns the filename and path for this config. May also be called as C for backward campatibility sake.
569              
570              
571              
572             =head2 includes ( )
573              
574             Returns an array reference of Config::JSON objects that are files included by this config. May also be called as C for backward compatibility sake.
575              
576              
577             =head2 new ( pathToFile )
578              
579             Constructor. Builds an object around a config file.
580              
581             =head3 pathToFile
582              
583             A string representing a path such as "/etc/my-cool-config.conf".
584              
585              
586              
587             =head2 set ( directive, value )
588              
589             Creates a new or updates an existing directive in the config file.
590              
591             =head3 directive
592              
593             A directive name.
594              
595             =head3 value
596              
597             The value to set the paraemter to. Can be a scalar, hash reference, or array reference.
598              
599              
600              
601             =head2 splitKeyParts ( key )
602              
603             Returns an array of key parts.
604              
605             =head3 key
606              
607             A key string. Could be 'foo' (simple key), 'foo/bar' (a multilevel key referring to the bar key as a child of foo), or 'foo\/bar' (a simple key that contains a slash in the key). Don't forget to double escape in your perl code if you have a slash in your key parts like this:
608              
609             $config->get('foo\\/bar');
610              
611             =cut
612              
613              
614              
615             =head2 write ( )
616              
617             Writes the file to the filesystem. Normally you'd never need to call this as it's called automatically by the other methods when a change occurs.
618              
619              
620             =head1 DIAGNOSTICS
621              
622             =over
623              
624             =item C<< Couldn't parse JSON in config file >>
625              
626             This means that the config file does not appear to be formatted properly as a JSON file. Common mistakes are missing commas or trailing commas on the end of a list.
627              
628             =item C<< Cannot read config file >>
629              
630             We couldn't read the config file. This usually means that the path specified in the constructor is incorrect.
631              
632             =item C<< Can't write to config file >>
633              
634             We couldn't write to the config file. This usually means that the file system is full, or the that the file is write protected.
635              
636             =back
637              
638             =head1 PREREQS
639              
640             L L L L L
641              
642             =head1 SUPPORT
643              
644             =over
645              
646             =item Repository
647              
648             L
649              
650             =item Bug Reports
651              
652             L
653              
654             =back
655              
656             =head1 AUTHOR
657              
658             JT Smith
659              
660             =head1 LEGAL
661              
662             Config::JSON is Copyright 2009 Plain Black Corporation (L) and is licensed under the same terms as Perl itself.
663              
664             =cut
665              
666             1;