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   3502 use 5.010;
  4         22  
4 4     4   25 use strict;
  4         16  
  4         157  
5 4     4   54 use warnings;
  4         9  
  4         230  
6 4     4   31 use utf8;
  4         9  
  4         33  
7 4     4   177 use Moo::Role;
  4         13  
  4         41  
8 4     4   2580 use App::Sqitch::Types qw(HashRef);
  4         12  
  4         82  
9 4     4   13273 use App::Sqitch::X qw(hurl);
  4         10  
  4         41  
10 4     4   1341 use Path::Class;
  4         15  
  4         260  
11 4     4   22 use Try::Tiny;
  4         7  
  4         224  
12 4     4   628 use URI::db;
  4         17630  
  4         147  
13 4     4   35 use Locale::TextDomain qw(App-Sqitch);
  4         7  
  4         55  
14 4     4   837 use List::Util qw(first);
  4         22  
  4         321  
15 4     4   25 use File::Path qw(make_path);
  4         10  
  4         209  
16 4     4   23 use namespace::autoclean;
  4         6  
  4         38  
17 4     4   305 use constant extra_target_keys => ();
  4         13  
  4         10770  
18              
19             our $VERSION = 'v1.6.1'; # 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 69008 my $self = shift;
116 37         220 my $props = $self->properties;
117              
118 37 100       240 if (my $engine = $props->{engine}) {
119             # Validate engine.
120             hurl $self->command => __x(
121             'Unknown engine "{engine}"', engine => $engine
122 4 50   6   134 ) unless first { $engine eq $_ } App::Sqitch::Command::ENGINES;
  6         35  
123             }
124              
125 37 100       1880 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       241 ) unless eval { $uri->isa('URI::db') };
  5         66  
131              
132 3 100       24 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   15   153 ) unless first { $engine eq $_ } App::Sqitch::Command::ENGINES;
  15         58  
141              
142             }
143             }
144              
145             sub config_target {
146 36     36 1 26837 my ($self, %p) = @_;
147 36         232 my $sqitch = $self->sqitch;
148 36         236 my $props = $self->properties;
149 36         235 my @params = (sqitch => $sqitch);
150              
151 36 100 100     569 if (my $name = $p{name} || $props->{target}) {
    100 100        
152 12         54 push @params => (name => $name);
153 12 100       68 if (my $uri = $p{uri}) {
154 1         31 push @params => (uri => $uri);
155             } else {
156 11         424 my $config = $sqitch->config;
157 11 100 100     306 if ($name !~ /:/ && !$config->get(key => "target.$name.uri")) {
158             # No URI. Give it one.
159             my $engine = $p{engine} || $props->{engine}
160 5   33     925 || $config->get(key => 'core.engine')
161             || hurl $self->command => __(
162             'No engine specified; specify via target or core.engine'
163             );
164 5         1057 push @params => (uri => URI::db->new("db:$engine:"));
165             }
166             }
167             } elsif (my $engine = $p{engine} || $props->{engine}) {
168 10         388 my $config = $sqitch->config;
169 10   66     177 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         582 my $default = $self->default_target;
177 14         2090 push @params => (
178             name => $default->name,
179             uri => $default->uri,
180             );
181             }
182              
183             # Return the target with all relevant attributes overridden.
184 36         6259 require App::Sqitch::Target;
185             return App::Sqitch::Target->new(
186             @params,
187 36         213 map { $_ => $props->{$_} } grep { $props->{$_} } qw(
  70         1030  
  432         1677  
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 4439 my $self = shift;
206 19         95 my $props = $self->properties;
207 19         52 my (@dirs, %seen);
208              
209 19         126 for my $target (@_) {
210             # Script directories.
211 19 100       159 if (my $top_dir = $props->{top_dir}) {
212 39         1182 push @dirs => grep { !$seen{$_}++ } map {
213 13 50       126 $props->{"$_\_$_"} || $top_dir->subdir($_);
  39         1925  
214             } qw(deploy revert verify);
215             } else {
216 18         1543 push @dirs => grep { !$seen{$_}++ } map {
217 6         19 my $name = "$_\_dir";
  18         3398  
218 18 50       724 $props->{$name} || $target->$name;
219             } qw(deploy revert verify);
220             }
221              
222             # Reworked script directories.
223 19 100 100     424 if (my $reworked_dir = $props->{reworked_dir} || $props->{top_dir}) {
224 39         1077 push @dirs => grep { !$seen{$_}++ } map {
225 13 100       182 $props->{"reworked_$_\_dir"} || $reworked_dir->subdir($_);
  39         1204  
226             } qw(deploy revert verify);
227             } else {
228 18         1530 push @dirs => grep { !$seen{$_}++ } map {
229 6         19 my $name = "reworked_$_\_dir";
  18         3061  
230 18 50       591 $props->{$name} || $target->$name;
231             } qw(deploy revert verify);
232             }
233             }
234              
235 19         492 return @dirs;
236             }
237              
238             sub make_directories_for {
239 17     17 1 14252 my $self = shift;
240 17         115 $self->mkdirs( $self->directories_for(@_) );
241             }
242              
243             sub mkdirs {
244 20     20 1 7939 my $self = shift;
245              
246 20         99 for my $dir (@_) {
247 73 100       958 next if -d $dir;
248 50         2658 my $sep = dir('')->stringify; # OS-specific directory separator.
249 50 100       5968 $self->info(__x(
250             'Created {file}',
251             file => "$dir$sep"
252             )) if make_path $dir, { error => \my $err };
253 50 100       22658 if ( my $diag = shift @{ $err } ) {
  50         282  
254 3         7 my ( $path, $msg ) = %{ $diag };
  3         11  
255 3 100       30 hurl $self->command => __x(
256             'Error creating {path}: {error}',
257             path => $path,
258             error => $msg,
259             ) if $path;
260 1         9 hurl $self->command => $msg;
261             }
262             }
263              
264 17         480 return $self;
265             }
266              
267             sub write_plan {
268 14     14 1 12718 my ( $self, %p ) = @_;
269 14         56 my $project = $p{project};
270 14         46 my $uri = $p{uri};
271 14   66     98 my $target = $p{target} || $self->config_target;
272 14         1248 my $file = $target->plan_file;
273              
274 14 100 100     9015 unless ($project && $uri) {
275             # Find a plan to copy the project name and URI from.
276 12         352 my $conf_plan = $target->plan;
277 12         20713 my $def_plan = $self->default_target->plan;
278 12 100   12   1733 if (try { $def_plan->project }) {
  12 100       1223  
279 10   66     1440 $project ||= $def_plan->project;
280 10   33     415 $uri ||= $def_plan->uri;
281 2     2   6197 } elsif (try { $conf_plan->project }) {
282 1   33     204 $project ||= $conf_plan->project;
283 1   33     49 $uri ||= $conf_plan->uri;
284             } else {
285 1 50       2206 hurl $self->command => __x(
286             'Missing %project pragma in {file}',
287             file => $file,
288             ) unless $project;
289             }
290             }
291              
292 14 100       1816 if (-e $file) {
293 9 50       342 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         527 my $plan = App::Sqitch::Plan->new(
300             sqitch => $self->sqitch,
301             file => $file,
302             target => $target,
303             );
304 9 100   9   1562 my $file_proj = try { $plan->project } or hurl init => __x(
  9         622  
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       1082 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         534 return $self;
316             }
317              
318 5 100       274 $self->mkdirs( $file->dir ) unless -d $file->dir;
319              
320 5 50       488 my $fh = $file->open('>:utf8_strict') or hurl init => __x(
321             'Cannot open {file}: {error}',
322             file => $file,
323             error => $!,
324             );
325 5         3676 require App::Sqitch::Plan;
326 5 100       138 $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       637 $fh->close or hurl add => __x(
332             'Error closing {file}: {error}',
333             file => $file,
334             error => $!
335             );
336              
337 5         583 $self->sqitch->info( __x 'Created {file}', file => $file );
338 5         897 return $self;
339             }
340              
341             sub config_params {
342 13     13 1 328 my ($self, $key) = @_;
343 13         40 my @vars;
344 13         40 while (my ($prop, $val) = each %{ $self->properties } ) {
  69         297  
345 56 100       217 if (ref $val eq 'HASH') {
346             push @vars => map {{
347             key => "$key.$prop.$_",
348 10         78 value => $val->{$_},
349 6         38 }} keys %{ $val };
  6         31  
350             } else {
351 50         225 push @vars => {
352             key => "$key.$prop",
353             value => $val,
354             };
355             }
356             }
357 13         75 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-2026 David E. Wheeler, 2012-2021 iovation Inc.
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