File Coverage

blib/lib/App/OnePif.pm
Criterion Covered Total %
statement 26 179 14.5
branch 0 56 0.0
condition 0 22 0.0
subroutine 9 33 27.2
pod 18 19 94.7
total 53 309 17.1


line stmt bran cond sub pod time code
1             package App::OnePif;
2 1     1   1321 use strict;
  1         1  
  1         25  
3 1     1   3 use warnings;
  1         1  
  1         49  
4             { our $VERSION = '0.001'; }
5 1     1   542 use English qw( -no_match_vars );
  1         1381  
  1         4  
6 1     1   278 use Carp;
  1         1  
  1         48  
7 1     1   20 use 5.010;
  1         2  
8              
9 1     1   375 use Mo qw< is default >;
  1         397  
  1         5  
10 1     1   1159 use Path::Tiny;
  1         2  
  1         35  
11 1     1   454 use YAML::Tiny;
  1         3773  
  1         48  
12              
13             {
14 1     1   5 no warnings 'redefine';
  1         2  
  1         1710  
15             my @booleans = (
16             'JSON::PP::Boolean',
17             'JSON::XS::Boolean',
18             'Types::Serialiser::Boolean', # should not be needed
19             'Mojo::JSON::_Bool', # only up to Mojolicious 6.21
20             # Dancer, Dancer2 use JSON
21             );
22             sub YAML::Tiny::dumper_for_unknown {
23 0     0 0   my ($self, $element, $line, $indent, $seen) = @_;
24 0           my $type = ref $element;
25              
26 0           for my $boolean (@booleans) {
27 0 0         next unless $element->isa($boolean);
28 0 0         $line .= $element ? ' true' : ' false';
29 0           return $line;
30             }
31              
32             # no known boolean... complain!
33 0           die \"YAML::Tiny does not support $type references";
34             }
35             }
36              
37             has file => (
38             is => 'rw',
39             lazy => 1,
40             default => sub { 'data.1pif' },
41             );
42              
43             has attachments_dir => (
44             is => 'rw',
45             lazy => 1,
46             default => sub {
47             my $self = shift;
48             path($self->file())->sibling('attachments')
49             },
50             );
51              
52             has records_byid => (
53             is => 'rw',
54             lazy => 1,
55             default => sub {
56             my $self = shift;
57             my $bt = $self->records_bytype;
58             my %retval;
59             for my $list (values %$bt) {
60             $retval{$_->{_id}} = $_ for @$list;
61             }
62             return \%retval;
63             },
64             );
65              
66             has records_bytype => (
67             is => 'rw',
68             lazy => 1,
69             default => \&DEFAULT_records,
70             );
71              
72             has JSON_decoder => (
73             is => 'rw',
74             lazy => 1,
75             default => sub {
76             for my $module (qw< JSON JSON::PP >) {
77             (my $filename = "$module.pm") =~ s{::}{/}gmxs;
78             my $retval = eval {
79             require $filename;
80             $module->can('decode_json');
81             } or next;
82             return $retval;
83             } ## end for my $module (qw< JSON JSON::PP >)
84             return;
85             },
86             );
87              
88             has YAML_dumper => (
89             is => 'rw',
90             lazy => 1,
91             default => sub {
92             for my $module (qw< YAML YAML::Tiny >) {
93             (my $filename = "$module.pm") =~ s{::}{/}gmxs;
94             my $retval = eval {
95             require $filename;
96             $module->can('Dump');
97             } or next;
98             return $retval;
99             } ## end for my $module (qw< YAML YAML::Tiny >)
100             return;
101             },
102             );
103              
104             has term => (
105             is => 'rw',
106             lazy => 1,
107             default => sub {
108             return Term::ReadLine->new('1password');
109             },
110             );
111              
112             has out => (
113             is => 'rw',
114             lazy => 1,
115             default => sub {
116             my ($self) = @_;
117             my $term = $self->term();
118             my $out = eval { $term->out() } || \*STDOUT;
119             binmode $out, ':encoding(utf8)';
120             return $out;
121             },
122             );
123              
124             has type => (
125             is => 'rw',
126             lazy => 1,
127             default => sub { '*' },
128             );
129              
130             has types => (
131             is => 'rw',
132             lazy => 1,
133             default => \&DEFAULT_types,
134             );
135              
136             sub run {
137 0     0 1   my ($package, @ARGV) = @_;
138 0           $package->new(args => \@ARGV)->run_interactive();
139             }
140              
141             sub run_interactive {
142 0     0 1   my ($self) = @_;
143 0           my %main_for = (
144             '.q' => 'quit',
145             e => 'exit',
146             f => 'file',
147             h => 'help',
148             l => 'list',
149             p => 'print',
150             q => 'quit',
151             s => 'search',
152             t => 'type',
153             ts => 'types',
154             u => 'type',
155             use => 'type',
156             );
157 0           require Term::ReadLine;
158 0           my $term = $self->term();
159 0           my $out = $self->out();
160 0           $self->records_bytype; # just load...
161 0           while (defined(my $line = $term->readline('1password> '))) {
162 0           my ($command, $rest) = $line =~ m{\A \s* (\S+) \s* (.*?) \s*\z}mxs;
163 0 0 0       next unless defined($command) && length($command);
164 0 0         $command = $main_for{$command} if exists $main_for{$command};
165 0 0         if (my $cb = $self->can("do_$command")) {
166 0           $self->$cb($rest);
167             }
168             else {
169 0           print {$out} "ERROR: unknown command [$command]\n",;
  0            
170             }
171             } ## end while (defined(my $line =...
172             } ## end sub run_interactive
173              
174             sub attachments_for {
175 0     0 1   my ($self, $uuid) = @_;
176 0           my $target = $self->attachments_dir()->child($uuid);
177 0 0         return unless $target->exists;
178 0           return [ map { $_->stringify } $target->children ];
  0            
179             }
180              
181             sub clear_records {
182 0     0 1   my ($self) = @_;
183 0           delete $self->{records_bytype};
184 0           delete $self->{records_byid};
185 0           delete $self->{types};
186 0           $self->{type} = '*';
187 0           return $self;
188             }
189              
190             sub do_help {
191 0     0 1   my ($self) = @_;
192 0           $self->print(<<'END_OF_HELP');
193             Available commands:
194             * quit (also: q, .q)
195             exit the program immediately, exit code is 0
196             * exit [code] (also: e)
197             exit the program immediately, can accept optional exit code
198             * file [filename] (also: f)
199             set the filename to use for taking data (default: 'data1.pif')
200             * types (also: ts)
201             show available types and possible aliases
202             * type [wanted] (also: t, use, u)
203             get current default type or set it to wanted. It is possible to
204             reset the default type by setting type "*" (no quotes)
205             * list [type] (also: l)
206             get a list for the current set type. By default no type is set
207             and the list includes all elements, otherwise it is filtered
208             by the wanted type.
209             If type parameter is provided, work on specified type instead
210             of default one.
211             * print [ <id> ] (also: p)
212             show record by provided id (look for ids with the list command).
213             It is also possible to specify the type, in which case the id
214             is interpreted in the context of the specific type.
215             * search <query-string> (also: s)
216             search for the query-string, literally. Looks for a substring in
217             the YAML rendition of each record that is equal to the query-string,
218             case-insensitively. If a type is set, the search is restricted to
219             that type.
220             END_OF_HELP
221             }
222              
223             sub do_quit {
224 0     0 1   exit 0;
225             }
226              
227             sub do_exit {
228 0     0 1   my ($self, $code) = @_;
229 0   0       exit($code || 0);
230             }
231              
232             sub do_file {
233 0     0 1   my ($self, $filename) = @_;
234 0 0 0       if (defined $filename && length $filename) {
235 0 0         if ($filename =~ m{\A(['"])(.*)$1\z}mxs) {
236 0           $filename = $2;
237             }
238 0           $self->file($filename);
239 0           $self->clear_records();
240             } ## end if (defined $filename ...
241             else {
242 0           $self->print($self->file());
243             }
244 0           return $self;
245             } ## end sub do_file
246              
247             sub DEFAULT_types {
248 0     0 1   my $self = shift;
249              
250 0           state $aliases_for = {
251             'passwords.Password' => [qw< p password passwords >],
252             'securenotes.SecureNote' => [qw< note notes >],
253             'wallet.computer.License' => [qw< license licenses >],
254             'webforms.WebForm' => [qw< form forms >],
255             'wallet.financial.CreditCard' => [qw< card cards >],
256             };
257              
258 0           my $rbt = $self->records_bytype;
259 0           my %retval = ('*' => ['*', '*']);
260 0           for my $type (keys %$rbt) {
261             my @alternatives = ($type, sort {
262 0 0         (length($a) <=> length($b)) || ($a cmp $b)
263 0   0       } @{$aliases_for->{$type} // []});
  0            
264 0           push @alternatives, $type;
265 0           $retval{$_} = \@alternatives for @alternatives;
266             }
267              
268             # now first item is always the canonical form, second is the shortest
269             # alias, then the rest including the canonical form at the end
270 0           return \%retval;
271             }
272              
273             sub do_types {
274 0     0 1   my ($self) = @_;
275              
276             # might cache this somewhere...
277 0           my %shorts;
278 0           my $length = 0;
279 0           for my $list (values %{$self->types}) {
  0            
280 0           my ($canon, $shorter, @rest) = @$list;
281 0           $shorts{$shorter} = \@rest;
282 0 0         $length = length($shorter) if length($shorter) > $length;
283             }
284 0           $shorts{'*'} = ' (accept any type)';
285              
286 0           my $current = $self->type;
287 0           my $marker = '<*>';
288 0           my $blanks = ' ' x length $marker;
289 0           for my $type (sort(keys %shorts)) {
290 0           my $rest = $shorts{$type};
291 0 0 0       $rest = " (also: @$rest)" if ref($rest) && @$rest;
292 0 0         $rest = '' if ref $rest;
293 0 0         my $indicator = $type eq $current ? $marker : $blanks;
294 0           $self->print(sprintf "%s %${length}s%s", $indicator, $type, $rest);
295             }
296             }
297              
298             sub real_type {
299 0     0 1   my ($self, $type) = @_;
300 0 0         return '*' unless defined $type;
301 0           my $types = $self->types;
302 0 0         return unless exists $types->{$type};
303 0           return $types->{$type}[0];
304             }
305              
306             sub do_type {
307 0     0 1   my ($self, $type) = @_;
308 0 0 0       if (defined $type && length $type) {
309 0 0         if ($self->real_type($type)) {
310 0           $self->type($type);
311             }
312             else {
313 0           $self->print("unknown type [$type]");
314             }
315             }
316             else {
317 0           $self->print($self->type());
318             }
319             } ## end sub do_type
320              
321             sub print {
322 0     0 1   my $self = shift;
323 0           print {$self->out()} @_, "\n";
  0            
324             }
325              
326             sub DEFAULT_records {
327 0     0 1   my ($self) = @_;
328 0           my $file = $self->file();
329 0 0         open my $fh, '<:raw', $file
330             or croak "open('$file'): $OS_ERROR";
331 0           my $decoder = $self->JSON_decoder;
332 0           my %by_type;
333 0           while (<$fh>) {
334 0           my $record = $decoder->($_);
335 0 0         if (my $attachments = $self->attachments_for($record->{uuid})) {
336 0           $record->{attachments} = $attachments;
337             }
338 0           push @{$by_type{$record->{typeName}}}, $record;
  0            
339 0           scalar <$fh>; # drop a line
340             }
341              
342 0           for my $list (values %by_type) {
343 0           @$list = sort { $a->{title} cmp $b->{title} } @$list;
  0            
344             }
345              
346 0           my $dumper = $self->YAML_dumper;
347             _traverse(\%by_type, undef, sub {
348 0     0     my ($n, $v) = @_;
349 0           $v->{_id} = $n;
350 0           $v->{_yaml} = $dumper->($v);
351 0           });
352              
353 0           return \%by_type;
354             }
355              
356             sub do_list {
357 0     0 1   my ($self, $type) = @_;
358 0   0       $type ||= $self->type();
359 0           $type = $self->real_type($type);
360 0           my $records = $self->clipped_records_bytype($type);
361             _traverse($records, sub {
362 0     0     my ($key) = @_;
363 0 0         $self->print($key) if $type eq '*';
364             }, sub {
365 0     0     my ($n, $record) = @_;
366 0           $self->print(sprintf(' %3d %s', $record->{_id}, $record->{title}));
367 0           });
368             }
369              
370             sub do_print {
371 0     0 1   my ($self, $id) = @_;
372 0           my $by_id = $self->records_byid;
373 0 0 0       if ($id && exists($by_id->{$id})) {
374 0           $self->print($by_id->{$id}{_yaml});
375             }
376             else {
377 0           $self->print('invalid id');
378             }
379             }
380              
381             sub do_search {
382 0     0 1   my ($self, $query) = @_;
383 0 0         $query = '' unless defined $query;
384 0           $query =~ s{\A\s+|\s+\z}{}gmxs;
385 0 0         return $self->do_list unless length $query;
386              
387 0           $query = quotemeta $query; # ready for a regex now
388 0           my $type = $self->real_type($self->type);
389 0           my $records = $self->clipped_records_bytype($type);
390 0           my $last_printed_type = $type;
391             _traverse($records, undef, sub {
392 0     0     my ($n, $record) = @_;
393 0 0         if ($record->{_yaml} =~ m{$query}i) {
394 0           my $rt = $self->real_type($record->{typeName});
395 0 0         if ($last_printed_type ne $rt) {
396 0           $self->print($record->{typeName});
397 0           $last_printed_type = $rt;
398             }
399             $self->print(sprintf(' %3d %s', $record->{_id},
400 0           $record->{title}));
401             }
402 0           });
403             }
404              
405             sub clipped_records_bytype {
406 0     0 1   my ($self, $type) = @_;
407 0           $type = $self->real_type($type);
408 0           my $records = $self->records_bytype();
409 0 0         $records = { $type => $records->{$type} }
410             unless $type eq '*';
411 0           return $records;
412             }
413              
414             sub _traverse {
415 0     0     my ($hash, $key_callback, $values_callback) = @_;
416 0           my $n = 0;
417 0           for my $key (sort keys %$hash) {
418 0 0         $key_callback->($key) if $key_callback;
419 0 0         next unless $values_callback;
420 0           $values_callback->(++$n, $_) for @{$hash->{$key}};
  0            
421             }
422             }
423              
424             1;