]> Dogcows Code - chaz/graphql-client/blob - bin/graphql
add tests and many fixes
[chaz/graphql-client] / bin / graphql
1 #! perl
2 # PODNAME: graphql
3 # ABSTRACT: Command-line GraphQL client
4
5 # FATPACK - Do not remove this line.
6
7 use warnings;
8 use strict;
9
10 use Getopt::Long;
11 use GraphQL::Client;
12 use JSON::MaybeXS;
13
14 our $VERSION = '999.999'; # VERSION
15
16 my $version;
17 my $help;
18 my $manual;
19 my $url;
20 my $transport = {};
21 my $query = '-';
22 my $variables = {};
23 my $operation_name;
24 my $format = 'json:pretty';
25 my $unpack = 0;
26 my $outfile;
27 GetOptions(
28 'version' => \$version,
29 'help|h|?' => \$help,
30 'manual|man' => \$manual,
31 'url|u=s' => \$url,
32 'query|mutation=s' => \$query,
33 'variables|vars|V=s' => \$variables,
34 'variable|var|d=s%' => \$variables,
35 'operation-name|n=s' => \$operation_name,
36 'transport|t=s%' => \$transport,
37 'format|f=s' => \$format,
38 'unpack!' => \$unpack,
39 'output|o=s' => \$outfile,
40 ) or pod2usage(2);
41
42 if ($version) {
43 print "graphql $VERSION\n";
44 exit 0;
45 }
46 if ($help) {
47 pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS)]);
48 }
49 if ($manual) {
50 pod2usage(-exitval => 0, -verbose => 2);
51 }
52
53 $url = shift if !$url;
54 $query = shift if !$query || $query eq '-';
55
56 if (!$url) {
57 print STDERR "The <URL> or --url option argument is required.\n";
58 pod2usage(2);
59 }
60
61 $transport = expand_vars($transport);
62
63 if (ref $variables) {
64 $variables = expand_vars($variables);
65 }
66 else {
67 $variables = JSON::MaybeXS->new->decode($variables);
68 }
69
70 my $client = GraphQL::Client->new(url => $url);
71
72 eval { $client->transport };
73 if (my $err = $@) {
74 warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
75 print STDERR "Could not construct a transport for URL: $url\n";
76 print STDERR "Is this URL correct?\n";
77 pod2usage(2);
78 }
79
80 if (!$query || $query eq '-') {
81 print STDERR "Interactive mode engaged! Waiting for a query on <STDIN>...\n"
82 if -t STDIN; ## no critic (InputOutput::ProhibitInteractiveTest)
83 $query = do { local $/; <> };
84 }
85
86 my $resp = $client->execute($query, $variables, $operation_name, $transport);
87 my $err = $resp->{errors};
88 $unpack = 0 if $err;
89 my $data = $unpack ? $resp->{data} : $resp;
90
91 if ($outfile) {
92 open(my $out, '>', $outfile) or die "Open $outfile failed: $!";
93 *STDOUT = $out;
94 }
95
96 print_data($data, $format);
97
98 exit($unpack && $err ? 1 : 0);
99
100 sub print_data {
101 my ($data, $format) = @_;
102 $format = lc($format || 'json:pretty');
103 if ($format eq 'json' || $format eq 'json:pretty') {
104 my %opts = (canonical => 1, utf8 => 1);
105 $opts{pretty} = 1 if $format eq 'json:pretty';
106 print JSON::MaybeXS->new(%opts)->encode($data);
107 }
108 elsif ($format eq 'yaml') {
109 eval { require YAML } or die "Missing dependency: YAML\n";
110 print YAML::Dump($data);
111 }
112 elsif ($format eq 'csv' || $format eq 'tsv' || $format eq 'table') {
113 my $sep = $format eq 'tsv' ? "\t" : ',';
114
115 my $unpacked = $data;
116 $unpacked = $data->{data} if !$unpack && !$err;
117
118 # check the response to see if it can be formatted
119 my @columns;
120 my $rows = [];
121 if (keys %$unpacked == 1) {
122 my ($val) = values %$unpacked;
123 if (ref $val eq 'ARRAY') {
124 my $first = $val->[0];
125 if ($first && ref $first eq 'HASH') {
126 @columns = sort keys %$first;
127 $rows = [
128 map { [@{$_}{@columns}] } @$val
129 ];
130 }
131 elsif ($first) {
132 @columns = keys %$unpacked;
133 $rows = [map { [$_] } @$val];
134 }
135 }
136 }
137
138 if (@columns) {
139 if ($format eq 'table') {
140 eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n";
141 my $table = Text::Table::Any::table(
142 header_row => 1,
143 rows => [[@columns], @$rows],
144 backend => $ENV{PERL_TEXT_TABLE},
145 );
146 print $table;
147 }
148 else {
149 eval { require Text::CSV } or die "Missing dependency: Text::CSV\n";
150 my $csv = Text::CSV->new({binary => 1, sep => $sep, eol => $/});
151 $csv->print(*STDOUT, [@columns]);
152 for my $row (@$rows) {
153 $csv->print(*STDOUT, $row);
154 }
155 }
156 }
157 else {
158 print_data($data);
159 print STDERR sprintf("Error: Response could not be formatted as %s.\n", uc($format));
160 exit 3;
161 }
162 }
163 elsif ($format eq 'perl') {
164 eval { require Data::Dumper } or die "Missing dependency: Data::Dumper\n";
165 print Data::Dumper::Dumper($data);
166 }
167 else {
168 print STDERR "Error: Format not supported: $format\n";
169 print_data($data);
170 exit 3;
171 }
172 }
173
174 sub expand_vars {
175 my $vars = shift;
176
177 my %out;
178 while (my ($key, $value) = each %$vars) {
179 my $var = $value;
180 my @segments = split(/\./, $key);
181 for my $segment (reverse @segments) {
182 my $saved = $var;
183 if ($segment =~ /^(\d+)$/) {
184 $var = [];
185 $var->[$segment] = $saved;
186 }
187 else {
188 $var = {};
189 $var->{$segment} = $saved;
190 }
191 }
192 %out = (%out, %$var);
193 }
194
195 return \%out;
196 }
197
198 sub pod2usage {
199 eval { require Pod::Usage };
200 if ($@) {
201 my $ref = $VERSION eq '999.999' ? 'master' : "v$VERSION";
202 my $exit = (@_ == 1 && $_[0] =~ /^\d+$/ && $_[0]) //
203 (@_ % 2 == 0 && {@_}->{'-exitval'}) // 2;
204 print STDERR <<END;
205 Online documentation is available at:
206
207 https://github.com/chazmcgarvey/graphql-client/blob/$ref/README.md
208
209 Tip: To enable inline documentation, install the Pod::Usage module.
210
211 END
212 exit $exit;
213 }
214 else {
215 goto &Pod::Usage::pod2usage;
216 }
217 }
218
219 =head1 SYNOPSIS
220
221 graphql <URL> <QUERY> [ [--variables JSON] | [--variable KEY=VALUE]... ]
222 [--operation-name NAME] [--transport KEY=VALUE]...
223 [--[no-]unpack] [--format json|json:pretty|yaml|perl|csv|tsv|table]
224 [--output FILE]
225
226 graphql --version|--help|--manual
227
228 =head1 DESCRIPTION
229
230 C<graphql> is a command-line program for executing queries and mutations on
231 a L<GraphQL|https://graphql.org/> server.
232
233 =head1 INSTALL
234
235 There are several ways to install F<graphql> to your system.
236
237 =head2 from CPAN
238
239 You can install F<graphql> using L<cpanm>:
240
241 cpanm GraphQL::Client
242
243 =head2 from GitHub
244
245 You can also choose to download F<graphql> as a self-contained executable:
246
247 curl -OL https://raw.githubusercontent.com/chazmcgarvey/graphql-client/solo/graphql
248 chmod +x graphql
249
250 To hack on the code, clone the repo instead:
251
252 git clone https://github.com/chazmcgarvey/graphql-client.git
253 cd graphql-client
254 make bootstrap # installs dependencies; requires cpanm
255
256 =head1 OPTIONS
257
258 =head2 C<--url URL>
259
260 The URL of the GraphQL server endpoint.
261
262 If no C<--url> option is given, the first argument is assumed to be the URL.
263
264 This option is required.
265
266 Alias: C<-u>
267
268 =head2 C<--query STR>
269
270 The query or mutation to execute.
271
272 If no C<--query> option is given, the next argument (after URL) is assumed to be the query.
273
274 If the value is "-" (which is the default), the query will be read from C<STDIN>.
275
276 See: L<https://graphql.org/learn/queries/>
277
278 Alias: C<--mutation>
279
280 =head2 C<--variables JSON>
281
282 Provide the variables as a JSON object.
283
284 Aliases: C<--vars>, C<-V>
285
286 =head2 C<--variable KEY=VALUE>
287
288 An alternative way to provide variables. Repeat this option to provide multiple variables.
289
290 If used in combination with L</"--variables JSON">, this option is silently ignored.
291
292 See: L<https://graphql.org/learn/queries/#variables>
293
294 Aliases: C<--var>, C<-d>
295
296 =head2 C<--operation-name NAME>
297
298 Inform the server which query/mutation to execute.
299
300 Alias: C<-n>
301
302 =head2 C<--output FILE>
303
304 Write the response to a file instead of STDOUT.
305
306 Alias: C<-o>
307
308 =head2 C<--transport KEY=VALUE>
309
310 Key-value pairs for configuring the transport (usually HTTP).
311
312 Alias: C<-t>
313
314 =head2 C<--format STR>
315
316 Specify the output format to use. See L</FORMAT>.
317
318 Alias: C<-f>
319
320 =head2 C<--unpack>
321
322 Enables unpack mode.
323
324 By default, the response structure is printed as-is from the server, and the program exits 0.
325
326 When unpack mode is enabled, if the response completes with no errors, only the data section of
327 the response is printed and the program exits 0. If the response has errors, the whole response
328 structure is printed as-is and the program exits 1.
329
330 See L</EXAMPLES>.
331
332 =head1 FORMAT
333
334 The C<--format> argument can be one of:
335
336 =for :list
337 * C<csv> - Comma-separated values (requires L<Text::CSV>)
338 * C<json:pretty> - Human-readable JSON (default)
339 * C<json> - JSON
340 * C<perl> - Perl code (requires L<Data::Dumper>)
341 * C<table> - Table (requires L<Text::Table::Any>)
342 * C<tsv> - Tab-separated values (requires L<Text::CSV>)
343 * C<yaml> - YAML (requires L<YAML>)
344
345 The C<csv>, C<tsv>, and C<table> formats will only work if the response has a particular shape:
346
347 {
348 "data" : {
349 "onefield" : [
350 {
351 "key" : "value",
352 ...
353 },
354 ...
355 ]
356 }
357 }
358
359 or
360
361 {
362 "data" : {
363 "onefield" : [
364 "value",
365 ...
366 ]
367 }
368 }
369
370 If the response cannot be formatted, the default format will be used instead, an error message will
371 be printed to STDERR, and the program will exit 3.
372
373 Table formatting can be done by one of several different modules, each with its own features and
374 bugs. The default module is L<Text::Table::Tiny>, but this can be overridden using the
375 C<PERL_TEXT_TABLE> environment variable if desired, like this:
376
377 PERL_TEXT_TABLE=Text::Table::HTML graphql ... -f table
378
379 The list of supported modules is at L<Text::Table::Any/@BACKENDS>.
380
381 =head1 EXAMPLES
382
383 Different ways to provide the query/mutation to execute:
384
385 graphql http://myserver/graphql {hello}
386
387 echo {hello} | graphql http://myserver/graphql
388
389 graphql http://myserver/graphql <<END
390 > {hello}
391 > END
392
393 graphql http://myserver/graphql
394 Interactive mode engaged! Waiting for a query on <STDIN>...
395 {hello}
396 ^D
397
398 Execute a query with variables:
399
400 graphql http://myserver/graphql <<END --var episode=JEDI
401 > query HeroNameAndFriends($episode: Episode) {
402 > hero(episode: $episode) {
403 > name
404 > friends {
405 > name
406 > }
407 > }
408 > }
409 > END
410
411 graphql http://myserver/graphql --vars '{"episode":"JEDI"}'
412
413 Configure the transport:
414
415 graphql http://myserver/graphql {hello} -t headers.authorization='Basic s3cr3t'
416
417 This example shows the effect of L</--unpack>:
418
419 graphql http://myserver/graphql {hello}
420
421 # Output:
422 {
423 "data" : {
424 "hello" : "Hello world!"
425 }
426 }
427
428 graphql http://myserver/graphql {hello} --unpack
429
430 # Output:
431 {
432 "hello" : "Hello world!"
433 }
434
435 =head1 ENVIRONMENT
436
437 Some environment variables affect the way C<graphql> behaves:
438
439 =for :list
440 * C<GRAPHQL_CLIENT_DEBUG> - Set to 1 to print diagnostic messages to STDERR.
441 * C<GRAPHQL_CLIENT_HTTP_USER_AGENT> - Set the HTTP user agent string.
442 * C<PERL_TEXT_TABLE> - Set table format backend; see L</FORMAT>.
443
444 =head1 EXIT STATUS
445
446 Here is a consolidated summary of what exit statuses mean:
447
448 =for :list
449 * C<0> - Success
450 * C<1> - Client or server errors
451 * C<2> - Option usage is wrong
452 * C<3> - Could not format the response as requested
453
This page took 0.049668 seconds and 4 git commands to generate.