File Coverage

blib/lib/Mojolicious/Plugin/Config/Structured/Command/config_dump.pm
Criterion Covered Total %
statement 95 107 88.7
branch 22 38 57.8
condition 9 17 52.9
subroutine 15 16 93.7
pod 1 1 100.0
total 142 179 79.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Config::Structured::Command::config_dump 3.01;
2 1     1   1016004 use v5.26;
  1         5  
3 1     1   8 use warnings;
  1         2  
  1         68  
4              
5             =encoding UTF-8
6              
7             =head1 NAME
8              
9             Mojolicious::Plugin::Config::Structured::Command::config_dump - dump a Config::Structured configuration to text
10              
11             =head1 SYNOPSIS
12              
13             Dump a Config::Structured configuration to text
14             Usage: <APP> config-dump [--verbose] [--depth n] [--path str] [--reveal-sensitive]
15              
16             Options:
17             --verbose display all node metadata instead of just the value
18             --depth n truncate/ignore nodes below depth n (relative to real root, not --path)
19             --path str display only config nodes from str down
20             --reveal-sensitive do not obscure sensitive values in output
21              
22             =head1 DESCRIPTION
23              
24             C<config_dump> is a utility for displaying a L<Config::Structured> config that
25             has been loaded into Mojolicious as formatted text
26              
27             In its default mode, it displays each node name and the associated value. With
28             the L<--verbose|/verbose> flag, though, all node metadata is output, including
29             description, examples, notes, and where the configured value came from.
30              
31             =head1 OPTIONS
32              
33             =head2 verbose
34              
35             Display complete node metadata instead of just node names and values. All
36             supported L<Config::Structured> node metadata keys and values are included.
37              
38             =head2 path
39            
40             Begin the output at the specified configuration path, ignoring configuration
41             nodes above/outside that point
42              
43             =head2 depth
44              
45             End the output at the specified configuration depth (relative to the original
46             root node -- does not regard L<--path|/path>), ignoring configuration nodes
47             below that point
48              
49             =head2 reveal-sensitive
50              
51             By default, configured values for any nodes marked C<sensitive> are obscured and
52             replaced by a string of asterisks. Use this option to show the actual values for
53             these nodes
54              
55             =cut
56              
57 1     1   7 use Mojo::Base 'Mojolicious::Command';
  1         2  
  1         23  
58 1     1   360 use Getopt::Long qw(GetOptionsFromArray);
  1         3  
  1         13  
59              
60 1     1   923 use JSON qw(encode_json);
  1         11492  
  1         8  
61 1     1   203 use List::Util qw(max);
  1         3  
  1         114  
62 1     1   8 use Scalar::Util qw(looks_like_number);
  1         3  
  1         58  
63 1     1   585 use Syntax::Keyword::Try;
  1         2835  
  1         13  
64 1     1   106 use Term::ANSIColor;
  1         2  
  1         102  
65              
66 1     1   607 use experimental qw(signatures);
  1         6705  
  1         7  
67              
68             has description => 'Display Config::Structured configuration';
69             has usage => sub ($self) {$self->extract_usage};
70              
71 7     7   14 my sub resolve_path($conf, $path) {
  7         12  
  7         17  
  7         9  
72 7         23 my @path = split(q{/}, $path);
73 7         35 shift(@path);
74 7         14 my $node_name = pop(@path);
75 7 100       30 return $conf unless ($node_name);
76             try {
77             $conf = $conf->$_ foreach (@path);
78             return ($conf, $node_name) if ($conf->get_node($node_name));
79 3         7 } catch ($e) {
80             }
81 0         0 die("'$path' is not a valid configuration path\n");
82             }
83              
84             my sub stringify_value($value) {
85             return 'undef' unless (defined($value));
86             return encode_json($value) if (ref($value));
87             return $value if (looks_like_number($value));
88             return qq{"$value"};
89             }
90              
91             my sub is_branch($node) {
92             return eval {$node->isa('Config::Structured::Node')} # pre-5.32 safe "isa" check
93             }
94              
95 7     7   37 my sub dump_node($conf, %params) {
  7         12  
  7         27  
  7         12  
96 7         24 my ($name, $allow_sensitive, $depth, $max_depth) = @params{qw(node sensitive depth max_depth)};
97 7   100     35 $depth //= 0;
98 7   33     19 my $at_limit = defined($max_depth) && $depth >= $max_depth;
99 7         21 my $indent = ' ' x $depth;
100              
101 7 100       17 if (defined($name)) {
102 3 50 50     6 say stringify_value($conf->$name($allow_sensitive)) and return unless (is_branch($conf->$name));
103 0         0 $conf = $conf->$name;
104             }
105              
106 4   50     21 my $m = (max map {length} ($conf->get_node->{leaves}->@*, $conf->get_node->{branches}->@*)) // 0;
  6         4260  
107 4         29 foreach (sort($conf->get_node->{leaves}->@*)) {
108 4         1230 printf("%s%-${m}s%s%s\n", $indent, $_, ' => ', stringify_value($conf->$_($allow_sensitive)));
109             }
110 4         830 foreach (sort($conf->get_node->{branches}->@*)) {
111 2 50       902 printf("%s%-${m}s%s%s\n", $indent, $_, ' =>', $at_limit ? ' {...}' : '');
112 2 50       12 __SUB__->($conf->$_, max_depth => $max_depth, depth => $depth + 1, sensitive => $allow_sensitive) unless ($at_limit);
113             }
114             }
115              
116             my sub fv($label, $value = '') {
117             printf(" %-12s%s\n", $label . ':', $value);
118             }
119              
120 8     8   1592 my sub dump_node_verbose($conf, %params) {
  8         14  
  8         25  
  8         13  
121 8         24 my ($name, $allow_sensitive, $depth, $max_depth) = @params{qw(node sensitive depth max_depth)};
122 8   100     26 $depth //= 0;
123 8   33     21 my $at_limit = defined($max_depth) && $depth >= $max_depth;
124              
125 8 100       28 if (!defined($name)) {
    50          
126 4 50 33     13 return if (defined($max_depth) && $max_depth < $depth);
127             __SUB__->($conf, sensitive => $allow_sensitive, depth => $depth + 1, max_depth => $max_depth, node => $_)
128 4         13 foreach (sort $conf->get_node->{leaves}->@*);
129             __SUB__->($conf->$_, sensitive => $allow_sensitive, depth => $depth + 1, max_depth => $max_depth)
130 4         742 foreach (sort $conf->get_node->{branches}->@*);
131             } elsif (is_branch($conf->$name)) {
132 0         0 __SUB__->($conf->$name, sensitive => $allow_sensitive, depth => $depth + 1, max_depth => $max_depth);
133             } else {
134 4         11 my $node = $conf->get_node($name, $allow_sensitive);
135 4         1688 my @fmt;
136 4 50       14 push(@fmt, qw(red)) if (!defined($node->{value}));
137 4 50       10 push(@fmt, qw(italic)) unless ($node->{overridden});
138 4 50       12 @fmt = qw(reset) unless (@fmt);
139              
140 4         23 say colored([qw(clear)], $node->{path});
141 4 50       57 say " " . colored([qw(faint)], $node->{description}) if ($node->{description});
142 4 50       10 if ($node->{notes}) {
143 0         0 my @lines = split("\n", $node->{notes});
144 0         0 say colored([qw(faint)], ' | ') . colored([qw(faint italic)], $_) foreach (@lines);
145             }
146 4         11 fv('Type', $node->{isa});
147 4 100       13 fv('Sensitive', colored([qw(green)], 'Y')) if ($node->{sensitive});
148 4 50       10 fv('URL', colored([qw(underline)], $node->{url})) if ($node->{url});
149 4 50       8 fv('Default', colored([qw(italic)], stringify_value($node->{default}))) if ($node->{default});
150 4 50       9 fv('Ref<' . ucfirst($node->{reference}->{source}) . '>', $node->{reference}->{ref}) if ($node->{reference});
151 4         10 fv('Value', colored([@fmt], stringify_value($node->{value})));
152 4 50       10 if ($node->{examples}) {
153 0         0 fv('Examples');
154 0 0       0 say " " . stringify_value($_) foreach (ref($node->{examples}) eq 'ARRAY') ? $node->{examples}->@* : ($node->{examples});
155             }
156 4         22 say "";
157             }
158             }
159              
160 7     7 1 16923 sub run ($self, @args) {
  7         16  
  7         18  
  7         14  
161 7         25 my ($dump_node, $depth, $path, $sensitive) = (\&dump_node, undef, '/', 0);
162 2         5 GetOptionsFromArray(
163             \@args,
164 2     2   18 'verbose' => sub($n, $v) {$dump_node = \&dump_node_verbose},
  2         930  
  2         4  
  2         4  
165 0     0   0 'depth=i' => sub($n, $v) {$depth = $v - 1},
  0            
  0            
  0            
  0            
166 7         82 'path=s' => \$path,
167             'reveal-sensitive' => \$sensitive,
168             );
169 7         2986 my ($conf, $node_name) = resolve_path($self->app->conf, $path);
170 7         1189 $dump_node->($conf, node => $node_name, max_depth => $depth, sensitive => $sensitive);
171             }
172              
173             =pod
174              
175             =head1 EXAMPLES
176              
177             Standard:
178              
179             /app$ script/myapp config-dump
180             secrets => "************"
181             db =>
182             dsn => "dbi:mysql:host=mydb;port=3306;database=myapp_dev"
183             pass => "************"
184             user => "myapp_user"
185             migration =>
186             directory => "/schema"
187             pass => "************"
188             registry => "sqitch_myapp_dev"
189             user => "sqitch"
190              
191             At path:
192              
193             /app$ script/myapp config-dump --path /db/migration
194             directory => "/schema"
195             pass => "************"
196             registry => "sqitch_myapp_dev"
197             user => "sqitch"
198              
199             With depth limit:
200              
201             /app$ script/myapp config-dump --path /db --depth 1
202             dsn => "dbi:mysql:host=mydb;port=3306;database=myapp_dev"
203             pass => "************"
204             user => "myapp_user"
205             migration => {...}
206              
207             Without sensitive obscurement:
208              
209             /app$ script/myapp config-dump --path /db --depth 1 --reveal-sensitive
210             dsn => "dbi:mysql:host=mydb;port=3306;database=myapp_dev"
211             pass => "&xyus7#^kP**6Eeo9Yht6fU"
212             user => "devapps"
213             migration => {...}
214              
215             Verbose:
216              
217             /app$ script/myapp config-dump --verbose
218             /secrets
219             private key to encrypt session data with
220             Type: ArrayRef[Str]
221             Sensitive: Y
222             Default: ["not-very-secret"]
223             Value: "************"
224              
225             /db/dsn
226             Data Source Name for the database connection
227             Type: Str
228             URL: https://en.wikipedia.org/wiki/Data_source_name
229             Default: "dbi:mysql:host=localhost;port=3306;database=myapp"
230             Value: "dbi:mysql:host=mydb;port=3306;database=myapp_dev"
231              
232             /db/pass
233             database connection password
234             | Often passed as a file or ENV for security
235             Type: Str
236             Sensitive: Y
237             Ref<File>: /run/secrets/app_db_password
238             Value: "************"
239              
240             /db/user
241             database connection username
242             Type: Str
243             Default: "app_user"
244             Value: "myapp_user"
245              
246             /db/migration/directory
247             location of schema migration files
248             Type: Str
249             Default: "/schema"
250             Value: "/schema"
251              
252             /db/migration/pass
253             database connection password for migrations
254             | Often passed as a file or ENV for security
255             Type: Str
256             Sensitive: Y
257             Ref<File>: /run/secrets/sqitch_password
258             Value: "************"
259             ...
260              
261             =head1 AUTHOR
262              
263             Mark Tyrrell C<< <mark@tyrrminal.dev> >>
264              
265             =head1 LICENSE
266              
267             Copyright (c) 2024 Mark Tyrrell
268              
269             Permission is hereby granted, free of charge, to any person obtaining a copy
270             of this software and associated documentation files (the "Software"), to deal
271             in the Software without restriction, including without limitation the rights
272             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
273             copies of the Software, and to permit persons to whom the Software is
274             furnished to do so, subject to the following conditions:
275              
276             The above copyright notice and this permission notice shall be included in all
277             copies or substantial portions of the Software.
278              
279             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
280             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
281             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
282             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
283             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
284             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
285             SOFTWARE.
286              
287             =cut
288              
289             1;
290              
291             __END__