File Coverage

blib/lib/GraphQL/Client/CLI.pm
Criterion Covered Total %
statement 76 227 33.4
branch 30 140 21.4
condition 7 46 15.2
subroutine 13 18 72.2
pod 2 2 100.0
total 128 433 29.5


line stmt bran cond sub pod time code
1             package GraphQL::Client::CLI;
2             # ABSTRACT: Implementation of the graphql CLI program
3              
4 1     1   51803 use warnings;
  1         9  
  1         26  
5 1     1   4 use strict;
  1         2  
  1         18  
6              
7 1     1   435 use Encode qw(decode);
  1         7441  
  1         67  
8 1     1   531 use Getopt::Long 2.39 qw(GetOptionsFromArray);
  1         9382  
  1         19  
9 1     1   462 use GraphQL::Client;
  1         2  
  1         22  
10 1     1   312 use JSON::MaybeXS;
  1         3984  
  1         44  
11 1     1   315 use Text::ParseWords;
  1         939  
  1         46  
12 1     1   5 use namespace::clean;
  1         2  
  1         4  
13              
14             our $VERSION = '0.605'; # VERSION
15              
16             my $JSON = JSON::MaybeXS->new(canonical => 1);
17              
18 2     2   10 sub _croak { require Carp; goto &Carp::croak }
  2         23  
19              
20             sub new {
21 0     0 1 0 my $class = shift;
22 0         0 bless {}, $class;
23             }
24              
25             sub main {
26 0     0 1 0 my $self = shift;
27 0 0       0 $self = $self->new if !ref $self;
28              
29 0         0 my $options = eval { $self->_get_options(@_) };
  0         0  
30 0 0       0 if (my $err = $@) {
31 0         0 print STDERR $err;
32 0         0 _pod2usage(2);
33             }
34              
35 0 0       0 if ($options->{version}) {
36 0         0 print "graphql $VERSION\n";
37 0         0 exit 0;
38             }
39 0 0       0 if ($options->{help}) {
40 0         0 _pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS)]);
41             }
42 0 0       0 if ($options->{manual}) {
43 0         0 _pod2usage(-exitval => 0, -verbose => 2);
44             }
45              
46 0         0 my $url = $options->{url};
47 0 0       0 if (!$url) {
48 0         0 print STDERR "The or --url option argument is required.\n";
49 0         0 _pod2usage(2);
50             }
51              
52 0         0 my $variables = $options->{variables};
53 0         0 my $query = $options->{query};
54 0         0 my $operation_name = $options->{operation_name};
55 0         0 my $unpack = $options->{unpack};
56 0         0 my $outfile = $options->{outfile};
57 0         0 my $format = $options->{format};
58 0         0 my $transport = $options->{transport};
59              
60 0         0 my $client = GraphQL::Client->new(url => $url);
61              
62 0         0 eval { $client->transport };
  0         0  
63 0 0       0 if (my $err = $@) {
64 0 0       0 warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
65 0         0 print STDERR "Could not construct a transport for URL: $url\n";
66 0         0 print STDERR "Is this URL correct?\n";
67 0         0 _pod2usage(2);
68             }
69              
70 0 0       0 if ($query eq '-') {
71 0 0       0 print STDERR "Interactive mode engaged! Waiting for a query on ...\n"
72             if -t STDIN; ## no critic (InputOutput::ProhibitInteractiveTest)
73 0         0 binmode(STDIN, 'encoding(UTF-8)');
74 0         0 $query = do { local $/; };
  0         0  
  0         0  
75             }
76              
77 0         0 my $resp = $client->execute($query, $variables, $operation_name, $transport);
78 0         0 my $err = $resp->{errors};
79 0 0       0 $unpack = 0 if $err;
80 0 0       0 my $data = $unpack ? $resp->{data} : $resp;
81              
82 0 0       0 if ($outfile) {
83 0 0       0 open(my $out, '>', $outfile) or die "Open $outfile failed: $!";
84 0         0 *STDOUT = $out;
85             }
86              
87 0 0       0 if (my $filter = $options->{filter}) {
88 0 0       0 eval { require JSON::Path::Evaluator } or die "Missing dependency: JSON::Path\n";
  0         0  
89 0         0 my @values = JSON::Path::Evaluator::evaluate_jsonpath($data, $filter);
90 0 0       0 if (@values == 1) {
91 0         0 $data = $values[0];
92             }
93             else {
94 0         0 $data = \@values;
95             }
96             }
97              
98 0         0 binmode(STDOUT, 'encoding(UTF-8)');
99 0         0 _print_data($data, $format);
100              
101 0 0 0     0 exit($unpack && $err ? 1 : 0);
102             }
103              
104             sub _get_options {
105 5     5   4426 my $self = shift;
106 5         12 my @args = @_;
107              
108 5   100     21 unshift @args, shellwords($ENV{GRAPHQL_CLIENT_OPTIONS} || '');
109              
110             # assume UTF-8 args if non-ASCII
111 1 50   1   1175 @args = map { decode('UTF-8', $_) } @args if grep { /\P{ASCII}/ } @args;
  1         10  
  1         11  
  5         244  
  0         0  
  24         45  
112              
113 5         12 my %options = (
114             format => 'json:pretty',
115             unpack => 0,
116             );
117              
118             GetOptionsFromArray(\@args,
119             'version' => \$options{version},
120             'help|h|?' => \$options{help},
121             'manual|man' => \$options{manual},
122             'url|u=s' => \$options{url},
123             'query|mutation=s' => \$options{query},
124             'variables|vars|V=s' => \$options{variables},
125             'variable|var|d=s%' => \$options{variables},
126             'operation-name|n=s' => \$options{operation_name},
127             'transport|t=s%' => \$options{transport},
128             'format|f=s' => \$options{format},
129             'filter|p=s' => \$options{filter},
130             'unpack!' => \$options{unpack},
131             'output|o=s' => \$options{outfile},
132 5 50       35 ) or _pod2usage(2);
133              
134 5 100       4307 $options{url} = shift @args if !$options{url};
135 5 100       11 $options{query} = shift @args if !$options{query};
136              
137 5   50     11 $options{query} ||= '-';
138              
139 5         5 my $transport = eval { _expand_vars($options{transport}) };
  5         12  
140 5 50       6 die "Two or more --transport keys are incompatible.\n" if $@;
141 5 100 66     19 $options{transport} = $transport if ref $transport eq 'HASH' && %$transport;
142              
143 5 50       14 if (ref $options{variables}) {
    50          
144 0         0 $options{variables} = eval { _expand_vars($options{variables}) };
  0         0  
145 0 0       0 die "Two or more --variable keys are incompatible.\n" if $@;
146             }
147             elsif ($options{variables}) {
148 0         0 $options{variables} = eval { $JSON->decode($options{variables}) };
  0         0  
149 0 0       0 die "The --variables JSON does not parse.\n" if $@;
150             }
151              
152 5         15 return \%options;
153             }
154              
155             sub _stringify {
156 0     0   0 my ($item) = @_;
157 0 0       0 if (ref($item) eq 'ARRAY') {
158 0   0     0 my $first = @$item && $item->[0];
159 0 0       0 return join(',', @$item) if !ref($first);
160 0         0 return join(',', map { $JSON->encode($_) } @$item);
  0         0  
161             }
162 0 0       0 return $JSON->encode($item) if ref($item) eq 'HASH';
163 0         0 return $item;
164             }
165              
166             sub _print_data {
167 0     0   0 my ($data, $format) = @_;
168 0   0     0 $format = lc($format || 'json:pretty');
169 0 0 0     0 if ($format eq 'json' || $format eq 'json:pretty') {
    0 0        
    0 0        
    0          
    0          
170 0         0 my %opts = (allow_nonref => 1, canonical => 1);
171 0 0       0 $opts{pretty} = 1 if $format eq 'json:pretty';
172 0         0 print JSON::MaybeXS->new(%opts)->encode($data);
173             }
174             elsif ($format eq 'yaml') {
175 0 0       0 eval { require YAML } or die "Missing dependency: YAML\n";
  0         0  
176 0         0 print YAML::Dump($data);
177             }
178             elsif ($format eq 'csv' || $format eq 'tsv' || $format eq 'table') {
179 0 0       0 my $sep = $format eq 'tsv' ? "\t" : ',';
180              
181 0         0 my $unpacked = $data;
182             # $unpacked = $data->{data} if !$unpack && !$err;
183 0 0 0     0 $unpacked = $data->{data} if ref $data eq 'HASH' && $data->{data};
184              
185             # check the response to see if it can be formatted
186 0         0 my @columns;
187 0         0 my $rows = [];
188 0 0       0 if (ref $unpacked eq 'HASH') {
    0          
189 0 0       0 if (keys %$unpacked == 1) {
190 0         0 my ($val) = values %$unpacked;
191 0 0       0 if (ref $val eq 'ARRAY') {
192 0         0 my $first = $val->[0];
193 0 0 0     0 if ($first && ref $first eq 'HASH') {
    0          
194 0         0 @columns = sort keys %$first;
195             $rows = [
196 0         0 map { [map { _stringify($_) } @{$_}{@columns}] } @$val
  0         0  
  0         0  
  0         0  
197             ];
198             }
199             elsif ($first) {
200 0         0 @columns = keys %$unpacked;
201 0         0 $rows = [map { [map { _stringify($_) } $_] } @$val];
  0         0  
  0         0  
202             }
203             }
204             }
205             }
206             elsif (ref $unpacked eq 'ARRAY') {
207 0         0 my $first = $unpacked->[0];
208 0 0 0     0 if ($first && ref $first eq 'HASH') {
    0          
209 0         0 @columns = sort keys %$first;
210             $rows = [
211 0         0 map { [map { _stringify($_) } @{$_}{@columns}] } @$unpacked
  0         0  
  0         0  
  0         0  
212             ];
213             }
214             elsif ($first) {
215 0         0 @columns = qw(column);
216 0         0 $rows = [map { [map { _stringify($_) } $_] } @$unpacked];
  0         0  
  0         0  
217             }
218             }
219              
220 0 0       0 if (@columns) {
221 0 0       0 if ($format eq 'table') {
222 0 0       0 eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n";
  0         0  
223             my $table = Text::Table::Any::table(
224             header_row => 1,
225             rows => [[@columns], @$rows],
226             backend => $ENV{PERL_TEXT_TABLE},
227 0         0 );
228 0         0 print $table;
229             }
230             else {
231 0 0       0 eval { require Text::CSV } or die "Missing dependency: Text::CSV\n";
  0         0  
232 0         0 my $csv = Text::CSV->new({binary => 1, sep => $sep, eol => $/});
233 0         0 $csv->print(*STDOUT, [@columns]);
234 0         0 for my $row (@$rows) {
235 0         0 $csv->print(*STDOUT, $row);
236             }
237             }
238             }
239             else {
240 0         0 _print_data($data);
241 0         0 print STDERR sprintf("Error: Response could not be formatted as %s.\n", uc($format));
242 0         0 exit 3;
243             }
244             }
245             elsif ($format eq 'string') {
246 0 0       0 if (!ref $data) {
    0          
247 0         0 print $data, "\n";
248             }
249             elsif (ref $data eq 'ARRAY') {
250 0         0 print join("\n", @$data);
251             }
252             else {
253 0         0 _print_data($data);
254 0         0 print STDERR sprintf("Error: Response could not be formatted as %s.\n", $format);
255 0         0 exit 3;
256             }
257             }
258             elsif ($format eq 'perl') {
259 0 0       0 eval { require Data::Dumper } or die "Missing dependency: Data::Dumper\n";
  0         0  
260 0         0 print Data::Dumper::Dumper($data);
261             }
262             else {
263 0         0 _print_data($data);
264 0         0 print STDERR "Error: Format not supported: $format\n";
265 0         0 exit 3;
266             }
267             }
268              
269             sub _parse_path {
270 10     10   11 my $path = shift;
271              
272 10         11 my @path;
273              
274 10         25 my @segments = map { split(/\./, $_) } split(/(\[[^\.\]]+\])\.?/, $path);
  17         33  
275 10         17 for my $segment (@segments) {
276 23 100       39 if ($segment =~ /\[([^\.\]]+)\]/) {
277 4 50       10 $path[-1]{type} = 'ARRAY' if @path;
278 4         11 push @path, {
279             name => $1,
280             index => 1,
281             };
282             }
283             else {
284 19 100       28 $path[-1]{type} = 'HASH' if @path;
285 19         31 push @path, {
286             name => $segment,
287             };
288             }
289             }
290              
291 10         17 return \@path;
292             }
293              
294             sub _expand_vars {
295 8     8   3934 my $vars = shift;
296              
297 8         10 my $root = {};
298              
299 8         28 while (my ($key, $value) = each %$vars) {
300 10         17 my $parsed_path = _parse_path($key);
301              
302 10         10 my $curr = $root;
303 10         14 for my $segment (@$parsed_path) {
304 21         24 my $name = $segment->{name};
305 21   100     37 my $type = $segment->{type} || '';
306 21 100       33 my $next = $type eq 'HASH' ? {} : $type eq 'ARRAY' ? [] : $value;
    100          
307 21 100       30 if (ref $curr eq 'HASH') {
    50          
308 17 50       25 _croak 'Conflicting keys' if $segment->{index};
309 17 100       22 if (defined $curr->{$name}) {
310 4 100       11 _croak 'Conflicting keys' if $type ne ref $curr->{$name};
311 2         3 $next = $curr->{$name};
312             }
313             else {
314 13         19 $curr->{$name} = $next;
315             }
316             }
317             elsif (ref $curr eq 'ARRAY') {
318 4 50       7 _croak 'Conflicting keys' if !$segment->{index};
319 4 50       9 if (defined $curr->[$name]) {
320 0 0       0 _croak 'Conflicting keys' if $type ne ref $curr->[$name];
321 0         0 $next = $curr->[$name];
322             }
323             else {
324 4         6 $curr->[$name] = $next;
325             }
326             }
327             else {
328 0         0 _croak 'Conflicting keys';
329             }
330 19         42 $curr = $next;
331             }
332             }
333              
334 6         11 return $root;
335             }
336              
337             sub _pod2usage {
338 0     0     eval { require Pod::Usage };
  0            
339 0 0         if ($@) {
340 0 0         my $ref = $VERSION eq '999.999' ? 'master' : "v$VERSION";
341             my $exit = (@_ == 1 && $_[0] =~ /^\d+$/ && $_[0]) //
342 0   0       (@_ % 2 == 0 && {@_}->{'-exitval'}) // 2;
      0        
      0        
      0        
343 0           print STDERR <
344             Online documentation is available at:
345              
346             https://github.com/chazmcgarvey/graphql-client/blob/$ref/README.md
347              
348             Tip: To enable inline documentation, install the Pod::Usage module.
349              
350             END
351 0           exit $exit;
352             }
353             else {
354 0           goto &Pod::Usage::pod2usage;
355             }
356             }
357              
358             1;
359              
360             __END__