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