File Coverage

blib/lib/App/Sqitch/Role/TargetConfigCommand.pm
Criterion Covered Total %
statement 150 150 100.0
branch 58 66 87.8
condition 25 36 69.4
subroutine 27 27 100.0
pod 6 7 85.7
total 266 286 93.0


line stmt bran cond sub pod time code
1             package App::Sqitch::Role::TargetConfigCommand;
2              
3 4     4   2921 use 5.010;
  4         15  
4 4     4   25 use strict;
  4         10  
  4         101  
5 4     4   22 use warnings;
  4         8  
  4         118  
6 4     4   20 use utf8;
  4         8  
  4         22  
7 4     4   118 use Moo::Role;
  4         13  
  4         50  
8 4     4   2168 use App::Sqitch::Types qw(HashRef);
  4         9  
  4         62  
9 4     4   3777 use App::Sqitch::X qw(hurl);
  4         10  
  4         28  
10 4     4   995 use Path::Class;
  4         14  
  4         199  
11 4     4   25 use Try::Tiny;
  4         16  
  4         209  
12 4     4   550 use URI::db;
  4         12005  
  4         111  
13 4     4   23 use Locale::TextDomain qw(App-Sqitch);
  4         14  
  4         27  
14 4     4   668 use List::Util qw(first);
  4         16  
  4         241  
15 4     4   33 use File::Path qw(make_path);
  4         10  
  4         235  
16 4     4   39 use namespace::autoclean;
  4         15  
  4         30  
17 4     4   238 use constant extra_target_keys => ();
  4         29  
  4         12092  
18              
19             our $VERSION = 'v1.4.0'; # VERSION
20              
21             requires 'command';
22             requires 'options';
23             requires 'configure';
24             requires 'sqitch';
25             requires 'extra_target_keys';
26             requires 'default_target';
27              
28             has properties => (
29             is => 'ro',
30             isa => HashRef,
31             default => sub { {} },
32             );
33              
34             around options => sub {
35             my ($orig, $class) = @_;
36             return ($class->$orig), (map { "$_=s" } $class->extra_target_keys), qw(
37             plan-file|f=s
38             registry=s
39             client=s
40             extension=s
41             top-dir=s
42             dir|d=s%
43             set|s=s%
44             );
45             };
46              
47             around configure => sub {
48             my ( $orig, $class, $config, $opt ) = @_;
49              
50             # Grab the options we're responsible for.
51             my $props = {};
52             for my $key (
53             $class->extra_target_keys,
54             qw(plan_file registry client extension top_dir dir)
55             ) {
56             $props->{$key} = delete $opt->{$key} if exists $opt->{$key};
57             }
58              
59             # Let the command take care of its options.
60             my $params = $class->$orig($config, $opt);
61              
62             # Convert file option to Class::Path::File object.
63             if ( my $file = $props->{plan_file} ) {
64             $props->{plan_file} = file($file)->cleanup;
65             }
66              
67             # Convert directory option to Class::Path::Dir object.
68             if ( my $file = $props->{top_dir} ) {
69             $props->{top_dir} = dir($file)->cleanup;
70             }
71              
72             # Convert URI.
73             if ( my $uri = $props->{uri} ) {
74             require URI;
75             $props->{uri} = URI->new($uri);
76             }
77              
78             # Convert directory properties to Class::Path::Dir objects.
79             if (my $dirs = delete $props->{dir}) {
80             my %ok_keys = map {; $_ => undef } (
81             qw(reworked),
82             map { ($_, "reworked_$_") } qw(deploy revert verify)
83             );
84              
85             my @unknown;
86             for my $key (keys %{ $dirs }) {
87             unless (exists $ok_keys{$key}) {
88             push @unknown => $key;
89             next;
90             }
91             $props->{"$key\_dir"} = dir(delete $dirs->{$key})->cleanup
92             }
93              
94             if (@unknown) {
95             hurl $class->command => __nx(
96             'Unknown directory name: {dirs}',
97             'Unknown directory names: {dirs}',
98             @unknown,
99             dirs => join(__ ', ', sort @unknown),
100             );
101             }
102             }
103              
104             # Copy variables.
105             if ( my $vars = $opt->{set} ) {
106             $props->{variables} = $vars;
107             }
108              
109             # All done.
110             $params->{properties} = $props;
111             return $params;
112             };
113              
114             sub BUILD {
115 37     37 0 72316 my $self = shift;
116 37         252 my $props = $self->properties;
117              
118 37 100       291 if (my $engine = $props->{engine}) {
119             # Validate engine.
120             hurl $self->command => __x(
121             'Unknown engine "{engine}"', engine => $engine
122 4 50   6   172 ) unless first { $engine eq $_ } App::Sqitch::Command::ENGINES;
  6         78  
123             }
124              
125 37 100       3503 if (my $uri = $props->{uri}) {
126             # Validate URI.
127             hurl $self->command => __x(
128             'URI "{uri}" is not a database URI',
129             uri => $uri,
130 5 100       261 ) unless eval { $uri->isa('URI::db') };
  5         79  
131              
132 3 100       29 my $engine = $uri->canonical_engine or hurl $self->command => __x(
133             'No database engine in URI "{uri}"',
134             uri => $uri,
135             );
136             hurl $self->command => __x(
137             'Unknown engine "{engine}" in URI "{uri}"',
138             engine => $engine,
139             uri => $uri,
140 2 100   14   209 ) unless first { $engine eq $_ } App::Sqitch::Command::ENGINES;
  14         64  
141              
142             }
143             }
144              
145             sub config_target {
146 36     36 1 18276 my ($self, %p) = @_;
147 36         231 my $sqitch = $self->sqitch;
148 36         168 my $props = $self->properties;
149 36         284 my @params = (sqitch => $sqitch);
150              
151 36 100 100     676 if (my $name = $p{name} || $props->{target}) {
    100 100        
152 12         104 push @params => (name => $name);
153 12 100       105 if (my $uri = $p{uri}) {
154 1         27 push @params => (uri => $uri);
155             } else {
156 11         373 my $config = $sqitch->config;
157 11 100 100     359 if ($name !~ /:/ && !$config->get(key => "target.$name.uri")) {
158             # No URI. Give it one.
159             my $engine = $p{engine} || $props->{engine}
160 5   33     825 || $config->get(key => 'core.engine')
161             || hurl $self->command => __(
162             'No engine specified; specify via target or core.engine'
163             );
164 5         887 push @params => (uri => URI::db->new("db:$engine:"));
165             }
166             }
167             } elsif (my $engine = $p{engine} || $props->{engine}) {
168 10         386 my $config = $sqitch->config;
169 10   66     240 push @params => (
170             name => $config->get(key => "engine.$engine.target")
171             || $config->get(key => 'core.target')
172             || "db:$engine:"
173             );
174             } else {
175             # Get the name and URI from the default target.
176 14         476 my $default = $self->default_target;
177 14         1984 push @params => (
178             name => $default->name,
179             uri => $default->uri,
180             );
181             }
182              
183             # Return the target with all relevant attributes overridden.
184 36         6250 require App::Sqitch::Target;
185             return App::Sqitch::Target->new(
186             @params,
187 36         265 map { $_ => $props->{$_} } grep { $props->{$_} } qw(
  70         949  
  432         1417  
188             top_dir
189             plan_file
190             registry
191             client
192             deploy_dir
193             revert_dir
194             verify_dir
195             reworked_dir
196             reworked_deploy_dir
197             reworked_revert_dir
198             reworked_verify_dir
199             extension
200             )
201             );
202             }
203              
204             sub directories_for {
205 19     19 1 2924 my $self = shift;
206 19         139 my $props = $self->properties;
207 19         104 my (@dirs, %seen);
208              
209 19         118 for my $target (@_) {
210             # Script directories.
211 19 100       157 if (my $top_dir = $props->{top_dir}) {
212 39         1144 push @dirs => grep { !$seen{$_}++ } map {
213 13 50       126 $props->{"$_\_$_"} || $top_dir->subdir($_);
  39         1726  
214             } qw(deploy revert verify);
215             } else {
216 18         1354 push @dirs => grep { !$seen{$_}++ } map {
217 6         27 my $name = "$_\_dir";
  18         2737  
218 18 50       628 $props->{$name} || $target->$name;
219             } qw(deploy revert verify);
220             }
221              
222             # Reworked script directories.
223 19 100 100     529 if (my $reworked_dir = $props->{reworked_dir} || $props->{top_dir}) {
224 39         1002 push @dirs => grep { !$seen{$_}++ } map {
225 13 100       198 $props->{"reworked_$_\_dir"} || $reworked_dir->subdir($_);
  39         1171  
226             } qw(deploy revert verify);
227             } else {
228 18         1209 push @dirs => grep { !$seen{$_}++ } map {
229 6         32 my $name = "reworked_$_\_dir";
  18         2552  
230 18 50       896 $props->{$name} || $target->$name;
231             } qw(deploy revert verify);
232             }
233             }
234              
235 19         411 return @dirs;
236             }
237              
238             sub make_directories_for {
239 17     17 1 11481 my $self = shift;
240 17         120 $self->mkdirs( $self->directories_for(@_) );
241             }
242              
243             sub mkdirs {
244 20     20 1 5739 my $self = shift;
245              
246 20         101 for my $dir (@_) {
247 73 100       890 next if -d $dir;
248 50         2040 my $sep = dir('')->stringify; # OS-specific directory separator.
249 50 100       5165 $self->info(__x(
250             'Created {file}',
251             file => "$dir$sep"
252             )) if make_path $dir, { error => \my $err };
253 50 100       19433 if ( my $diag = shift @{ $err } ) {
  50         208  
254 3         7 my ( $path, $msg ) = %{ $diag };
  3         13  
255 3 100       37 hurl $self->command => __x(
256             'Error creating {path}: {error}',
257             path => $path,
258             error => $msg,
259             ) if $path;
260 1         6 hurl $self->command => $msg;
261             }
262             }
263              
264 17         445 return $self;
265             }
266              
267             sub write_plan {
268 14     14 1 12050 my ( $self, %p ) = @_;
269 14         73 my $project = $p{project};
270 14         43 my $uri = $p{uri};
271 14   66     116 my $target = $p{target} || $self->config_target;
272 14         1007 my $file = $target->plan_file;
273              
274 14 100 100     7726 unless ($project && $uri) {
275             # Find a plan to copy the project name and URI from.
276 12         338 my $conf_plan = $target->plan;
277 12         21654 my $def_plan = $self->default_target->plan;
278 12 100   12   1472 if (try { $def_plan->project }) {
  12 100       1349  
279 10   66     1377 $project ||= $def_plan->project;
280 10   33     401 $uri ||= $def_plan->uri;
281 2     2   5523 } elsif (try { $conf_plan->project }) {
282 1   33     175 $project ||= $conf_plan->project;
283 1   33     51 $uri ||= $conf_plan->uri;
284             } else {
285 1 50       1614 hurl $self->command => __x(
286             'Missing %project pragma in {file}',
287             file => $file,
288             ) unless $project;
289             }
290             }
291              
292 14 100       1461 if (-e $file) {
293 9 50       315 hurl init => __x(
294             'Cannot initialize because {file} already exists and is not a file',
295             file => $file,
296             ) unless -f $file;
297              
298             # Try to load the plan file.
299 9         449 my $plan = App::Sqitch::Plan->new(
300             sqitch => $self->sqitch,
301             file => $file,
302             target => $target,
303             );
304 9 100   9   1283 my $file_proj = try { $plan->project } or hurl init => __x(
  9         514  
305             'Cannot initialize because {file} already exists and is not a valid plan file',
306             file => $file,
307             );
308              
309             # Bail if this plan file looks like it's for a different project.
310 8 100       800 hurl init => __x(
311             'Cannot initialize because project "{project}" already initialized in {file}',
312             project => $plan->project,
313             file => $file,
314             ) if $plan->project ne $project;
315 7         353 return $self;
316             }
317              
318 5 100       206 $self->mkdirs( $file->dir ) unless -d $file->dir;
319              
320 5 50       416 my $fh = $file->open('>:utf8_strict') or hurl init => __x(
321             'Cannot open {file}: {error}',
322             file => $file,
323             error => $!,
324             );
325 5         2848 require App::Sqitch::Plan;
326 5 100       142 $fh->print(
327             '%syntax-version=', App::Sqitch::Plan::SYNTAX_VERSION(), "\n",
328             '%project=', "$project\n",
329             ( $uri ? ('%uri=', $uri->canonical, "\n") : () ), "\n",
330             );
331 5 50       580 $fh->close or hurl add => __x(
332             'Error closing {file}: {error}',
333             file => $file,
334             error => $!
335             );
336              
337 5         424 $self->sqitch->info( __x 'Created {file}', file => $file );
338 5         767 return $self;
339             }
340              
341             sub config_params {
342 13     13 1 391 my ($self, $key) = @_;
343 13         43 my @vars;
344 13         45 while (my ($prop, $val) = each %{ $self->properties } ) {
  69         355  
345 56 100       195 if (ref $val eq 'HASH') {
346             push @vars => map {{
347             key => "$key.$prop.$_",
348 10         99 value => $val->{$_},
349 6         31 }} keys %{ $val };
  6         43  
350             } else {
351 50         287 push @vars => {
352             key => "$key.$prop",
353             value => $val,
354             };
355             }
356             }
357 13         122 return \@vars;
358             }
359              
360             1;
361              
362             __END__
363              
364             =head1 Name
365              
366             App::Sqitch::Role::TargetConfigCommand - A command that handles target-related configuration
367              
368             =head1 Synopsis
369              
370             package App::Sqitch::Command::init;
371             extends 'App::Sqitch::Command';
372             with 'App::Sqitch::Role::TargetConfigCommand';
373              
374             =head1 Description
375              
376             This role encapsulates the common attributes and methods required by commands
377             that deal with change script configuration, including script directories and
378             extensions.
379              
380             =head1 Interface
381              
382             =head2 Class Methods
383              
384             =head3 C<options>
385              
386             my @opts = App::Sqitch::Command::checkout->options;
387              
388             Adds options common to the commands that manage script configuration.
389              
390             =head3 C<configure>
391              
392             Configures the options common to commands manage script configuration.
393              
394             =head2 Attributes
395              
396             =head3 C<properties>
397              
398             A hash reference of target configurations. The keys may be as follows:
399              
400             =over
401              
402             =item C<deploy>
403              
404             =item C<revert>
405              
406             =item C<verify>
407              
408             =item C<reworked>
409              
410             =item C<reworked_deploy>
411              
412             =item C<reworked_revert>
413              
414             =item C<reworked_verify>
415              
416             =item C<extension>
417              
418             =back
419              
420             =head2 Instance Methods
421              
422             =head3 C<config_target>
423              
424             my $target = $cmd->config_target;
425             my $target = $cmd->config_target(%params);
426              
427             Constructs a target based on the contents of C<properties>. The supported
428             parameters are:
429              
430             =over
431              
432             =item C<name>
433              
434             A target name.
435              
436             =item C<uri>
437              
438             A target URI.
439              
440             =item C<engine>
441              
442             An engine name.
443              
444             =back
445              
446             The passed target and engine names take highest precedence, falling back on
447             the properties and the C<default_target>. All other properties are applied to
448             the target before returning it.
449              
450             =head3 C<write_plan>
451              
452             $cmd->write_plan(%params);
453              
454             Writes out the plan file. Supported parameters are:
455              
456             =over
457              
458             =item C<target>
459              
460             The target for which the plan will be written. Defaults to the target returned
461             by C<config_target()>.
462              
463             =item C<project>
464              
465             The project name. If not passed, the project name will be read from the
466             default target's plan, if it exists. Otherwise an error will be thrown.
467              
468             =item C<uri>
469              
470             The project URI. Optional. If not passed, the URI will be read from the
471             default target's plan, if it exists. Optional.
472              
473             =back
474              
475             =head3 C<directories_for>
476              
477             my @dirs = $cmd->directories_for(@targets);
478              
479             Returns a set of script directories for a list of targets. Options passed to
480             the command are preferred. Paths are pulled from the command only when they
481             have not been passed as options.
482              
483             =head3 C<make_directories_for>
484              
485             $cmd->directories_for(@targets);
486              
487             Creates script directories for one or more targets. Options passed to the
488             command are preferred. Paths are pulled from the command only when they have
489             not been passed as options.
490              
491             =head3 C<mkdirs>
492              
493             $cmd->directories_for(@dirs);
494              
495             Creates the list of directories on the file system. Directories that already
496             exist are skipped. Messages are sent to C<info()> for each directory, and an
497             error is thrown on the first to fail.
498              
499             =head3 C<config_params>
500              
501             my @params = $cmd->config_params($key);
502              
503             Returns a list of parameters to pass to the L<App::Sqitch::Config> C<set>
504             method, built up from the C<properties>.
505              
506             =head1 See Also
507              
508             =over
509              
510             =item L<App::Sqitch::Command::init>
511              
512             The C<init> command initializes a Sqitch project, setting up the change script
513             configuration and directories.
514              
515             =item L<App::Sqitch::Command::engine>
516              
517             The C<engine> command manages engine configuration, including engine-specific
518             change script configuration.
519              
520             =item L<App::Sqitch::Command::target>
521              
522             The C<engine> command manages target configuration, including target-specific
523             change script configuration.
524              
525             =back
526              
527             =head1 Author
528              
529             David E. Wheeler <david@justatheory.com>
530              
531             =head1 License
532              
533             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
534              
535             Permission is hereby granted, free of charge, to any person obtaining a copy
536             of this software and associated documentation files (the "Software"), to deal
537             in the Software without restriction, including without limitation the rights
538             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
539             copies of the Software, and to permit persons to whom the Software is
540             furnished to do so, subject to the following conditions:
541              
542             The above copyright notice and this permission notice shall be included in all
543             copies or substantial portions of the Software.
544              
545             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
546             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
547             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
548             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
549             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
550             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
551             SOFTWARE.
552              
553             =cut