File Coverage

blib/lib/App/Config/Chronicle.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package App::Config::Chronicle;
2              
3 1     1   46813 use strict;
  1         1  
  1         21  
4 1     1   3 use warnings;
  1         1  
  1         20  
5 1     1   454 use Time::HiRes qw(time);
  1         941  
  1         3  
6              
7             =head1 NAME
8              
9             App::Config::Chronicle - An OO configuration module which can be changed and stored into chronicle database.
10              
11             =head1 VERSION
12              
13             Version 0.04
14              
15             =cut
16              
17             our $VERSION = '0.04';
18              
19             =head1 SYNOPSIS
20              
21             my $app_config = App::Config::Chronicle->new;
22              
23             =head1 DESCRIPTION
24              
25             This module parses configuration files and provides interface to access
26             configuration information.
27              
28             =head1 FILE FORMAT
29              
30             The configuration file is a YAML file. Here is an example:
31              
32             system:
33             description: "Various parameters determining core application functionality"
34             isa: section
35             contains:
36             email:
37             description: "Dummy email address"
38             isa: Str
39             default: "dummy@mail.com"
40             global: 1
41             admins:
42             description: "Are we on Production?"
43             isa: ArrayRef
44             default: []
45              
46             Every attribute is very intuitive. If an item is global, you can change its value and the value will be stored into chronicle database by calling the method C<save_dynamic>.
47              
48             =head1 SUBROUTINES/METHODS
49              
50             =cut
51              
52 1     1   628 use Moose;
  1         281621  
  1         4  
53 1     1   4706 use namespace::autoclean;
  1         5279  
  1         3  
54 1     1   410 use YAML::XS qw(LoadFile);
  1         1809  
  1         45  
55              
56 1     1   370 use App::Config::Chronicle::Attribute::Section;
  1         2  
  1         23  
57 1     1   412 use App::Config::Chronicle::Attribute::Global;
  0            
  0            
58             use Data::Hash::DotNotation;
59              
60             use Data::Chronicle::Reader;
61             use Data::Chronicle::Writer;
62              
63             =head2 definition_yml
64              
65             The YAML file that store the configuration
66              
67             =cut
68              
69             has definition_yml => (
70             is => 'ro',
71             isa => 'Str',
72             required => 1,
73             );
74              
75             =head2 chronicle_reader
76              
77             The chronicle store that configurations can be fetch from it. It should be an instance of L<Data::Chronicle::Reader>.
78             But user is free to implement any storage backend he wants if it is implemented with a 'get' method.
79              
80             =cut
81              
82             has chronicle_reader => (
83             is => 'ro',
84             isa => 'Data::Chronicle::Reader',
85             required => 1,
86             );
87              
88             =head2 chronicle_writer
89              
90             The chronicle store that updated configurations can be stored into it. It should be an instance of L<Data::Chronicle::Writer>.
91             But user is free to implement any storage backend he wants if it is implemented with a 'set' method.
92              
93             =cut
94              
95             has chronicle_writer => (
96             is => 'ro',
97             isa => 'Data::Chronicle::Writer',
98             required => 1,
99             );
100              
101             has setting_namespace => (
102             is => 'ro',
103             isa => 'Str',
104             default => 'app_settings',
105             );
106             has setting_name => (
107             is => 'ro',
108             isa => 'Str',
109             required => 1,
110             default => 'settings1',
111             );
112              
113             =head2 refresh_interval
114              
115             How much time (in seconds) should pass between L<check_for_update> invocations until
116             it actually will do (a bit heavy) lookup for settings in redis.
117              
118             Default value is 10 seconds
119              
120             =cut
121              
122             has refresh_interval => (
123             is => 'ro',
124             isa => 'Num',
125             required => 1,
126             default => 10,
127             );
128              
129             has _updated_at => (
130             is => 'rw',
131             isa => 'Num',
132             required => 1,
133             default => 0,
134             );
135              
136             # definitions database
137             has _defdb => (
138             is => 'rw',
139             lazy => 1,
140             default => sub { LoadFile(shift->definition_yml) },
141             );
142              
143             has 'data_set' => (
144             is => 'ro',
145             lazy_build => 1,
146             );
147              
148             sub _build_class {
149             my $self = shift;
150             $self->_create_attributes($self->_defdb, $self);
151             return;
152             }
153              
154             sub _create_attributes {
155             my $self = shift;
156             my $definitions = shift;
157             my $containing_section = shift;
158              
159             $containing_section->meta->make_mutable;
160             foreach my $definition_key (keys %{$definitions}) {
161             $self->_validate_key($definition_key, $containing_section);
162             my $definition = $definitions->{$definition_key};
163             if ($definition->{isa} eq 'section') {
164             $self->_create_section($containing_section, $definition_key, $definition);
165             $self->_create_attributes($definition->{contains}, $containing_section->$definition_key);
166             } elsif ($definition->{global}) {
167             $self->_create_global_attribute($containing_section, $definition_key, $definition);
168             } else {
169             $self->_create_generic_attribute($containing_section, $definition_key, $definition);
170             }
171             }
172             $containing_section->meta->make_immutable;
173              
174             return;
175             }
176              
177             sub _create_section {
178             my $self = shift;
179             my $section = shift;
180             my $name = shift;
181             my $definition = shift;
182              
183             my $writer = "_$name";
184             my $path_config = {};
185             if ($section->isa('App::Config::Chronicle::Attribute::Section')) {
186             $path_config = {parent_path => $section->path};
187             }
188              
189             my $new_section = Moose::Meta::Class->create_anon_class(superclasses => ['App::Config::Chronicle::Attribute::Section'])->new_object(
190             name => $name,
191             definition => $definition,
192             data_set => {},
193             %$path_config
194             );
195              
196             $section->meta->add_attribute(
197             $name,
198             is => 'ro',
199             isa => 'App::Config::Chronicle::Attribute::Section',
200             writer => $writer,
201             documentation => $definition->{description},
202             );
203             $section->$writer($new_section);
204              
205             #Force Moose Validation
206             $section->$name;
207              
208             return;
209             }
210              
211             sub _create_global_attribute {
212             my $self = shift;
213             my $section = shift;
214             my $name = shift;
215             my $definition = shift;
216              
217             my $attribute = $self->_add_attribute('App::Config::Chronicle::Attribute::Global', $section, $name, $definition);
218             $self->_add_dynamic_setting_info($attribute->path, $definition);
219              
220             return;
221             }
222              
223             sub _create_generic_attribute {
224             my $self = shift;
225             my $section = shift;
226             my $name = shift;
227             my $definition = shift;
228              
229             $self->_add_attribute('App::Config::Chronicle::Attribute', $section, $name, $definition);
230              
231             return;
232             }
233              
234             sub _add_attribute {
235             my $self = shift;
236             my $attr_class = shift;
237             my $section = shift;
238             my $name = shift;
239             my $definition = shift;
240              
241             my $fake_name = "a_$name";
242             my $writer = "_$fake_name";
243              
244             my $attribute = $attr_class->new(
245             name => $name,
246             definition => $definition,
247             parent_path => $section->path,
248             data_set => $self->data_set,
249             )->build;
250              
251             $section->meta->add_attribute(
252             $fake_name,
253             is => 'ro',
254             handles => {
255             $name => 'value',
256             'has_' . $name => 'has_value',
257             },
258             documentation => $definition->{description},
259             writer => $writer,
260             );
261              
262             $section->$writer($attribute);
263              
264             return $attribute;
265             }
266              
267             sub _validate_key {
268             my $self = shift;
269             my $key = shift;
270             my $section = shift;
271              
272             if (grep { $key eq $_ } qw(path parent_path name definition version data_set check_for_update save_dynamic refresh_interval)) {
273             die "Variable with name $key found under "
274             . $section->path
275             . ".\n$key is an internally used variable and cannot be reused, please use a different name";
276             }
277              
278             return;
279             }
280              
281             =head2 check_for_update
282              
283             check and load updated settings from chronicle db
284              
285             =cut
286              
287             sub check_for_update {
288             my $self = shift;
289              
290             # do fast cached check
291             my $now = time;
292             my $prev_update = $self->_updated_at;
293             return if ($now - $prev_update < $self->refresh_interval);
294              
295             $self->_updated_at($now);
296             # do check in Redis
297             my $data_set = $self->data_set;
298             my $app_settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name);
299              
300             my $db_version;
301             if ($app_settings and $data_set) {
302             $db_version = $app_settings->{_rev};
303             unless ($data_set->{version} and $db_version and $db_version eq $data_set->{version}) {
304             # refresh all
305             $self->_add_app_setttings($data_set, $app_settings);
306             }
307             }
308              
309             return $db_version;
310             }
311              
312             =head2 save_dynamic
313              
314             Save synamic settings into chronicle db
315              
316             =cut
317              
318             sub save_dynamic {
319             my $self = shift;
320             my $settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name) || {};
321              
322             #Cleanup globals
323             my $global = Data::Hash::DotNotation->new();
324             foreach my $key (keys %{$self->dynamic_settings_info->{global}}) {
325             if ($self->data_set->{global}->key_exists($key)) {
326             $global->set($key, $self->data_set->{global}->get($key));
327             }
328             }
329              
330             $settings->{global} = $global->data;
331             $settings->{_rev} = time;
332             $self->chronicle_writer->set($self->setting_namespace, $self->setting_name, $settings, Date::Utility->new);
333              
334             return 1;
335             }
336              
337             =head2 current_revision
338              
339             loads setting from chronicle reader and returns the last revision and drops them
340              
341             =cut
342              
343             sub current_revision {
344             my $self = shift;
345             my $settings = $self->chronicle_reader->get($self->setting_namespace, $self->setting_name);
346             return $settings->{_rev};
347             }
348              
349             sub _build_data_set {
350             my $self = shift;
351              
352             # relatively small yaml, so loading it shouldn't be expensive.
353             my $data_set->{app_config} = Data::Hash::DotNotation->new(data => {});
354              
355             $self->_add_app_setttings($data_set, $self->chronicle_reader->get($self->setting_namespace, $self->setting_name) || {});
356              
357             return $data_set;
358             }
359              
360             sub _add_app_setttings {
361             my $self = shift;
362             my $data_set = shift;
363             my $app_settings = shift;
364              
365             if ($app_settings) {
366             $data_set->{global} = Data::Hash::DotNotation->new(data => $app_settings->{global});
367             $data_set->{version} = $app_settings->{_rev};
368             }
369              
370             return;
371             }
372              
373             has dynamic_settings_info => (
374             is => 'ro',
375             isa => 'HashRef',
376             default => sub { {} },
377             );
378              
379             sub _add_dynamic_setting_info {
380             my $self = shift;
381             my $path = shift;
382             my $definition = shift;
383              
384             $self->dynamic_settings_info = {} unless ($self->dynamic_settings_info);
385             $self->dynamic_settings_info->{global} = {} unless ($self->dynamic_settings_info->{global});
386              
387             $self->dynamic_settings_info->{global}->{$path} = {
388             type => $definition->{isa},
389             default => $definition->{default},
390             description => $definition->{description}};
391              
392             return;
393             }
394              
395             =head2 BUILD
396              
397             =cut
398              
399             sub BUILD {
400             my $self = shift;
401              
402             $self->_build_class;
403              
404             return;
405             }
406              
407             __PACKAGE__->meta->make_immutable;
408              
409             =head1 AUTHOR
410              
411             Binary.com, C<< <binary at cpan.org> >>
412              
413             =head1 BUGS
414              
415             Please report any bugs or feature requests to C<bug-app-config at rt.cpan.org>, or through
416             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-Config>. I will be notified, and then you'll
417             automatically be notified of progress on your bug as I make changes.
418              
419              
420              
421              
422             =head1 SUPPORT
423              
424             You can find documentation for this module with the perldoc command.
425              
426             perldoc App::Config::Chronicle
427              
428              
429             You can also look for information at:
430              
431             =over 4
432              
433             =item * RT: CPAN's request tracker (report bugs here)
434              
435             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-Config>
436              
437             =item * AnnoCPAN: Annotated CPAN documentation
438              
439             L<http://annocpan.org/dist/App-Config>
440              
441             =item * CPAN Ratings
442              
443             L<http://cpanratings.perl.org/d/App-Config>
444              
445             =item * Search CPAN
446              
447             L<http://search.cpan.org/dist/App-Config/>
448              
449             =back
450              
451              
452             =head1 ACKNOWLEDGEMENTS
453              
454             =cut
455              
456             1; # End of App::Config::Chronicle