]> Dogcows Code - chaz/graphql-client/commitdiff
Version 0.602
authorCharles McGarvey <chazmcgarvey@brokenzipper.com>
Wed, 18 Mar 2020 00:29:08 +0000 (18:29 -0600)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Wed, 18 Mar 2020 00:29:08 +0000 (18:29 -0600)
17 files changed:
Changes
MANIFEST
META.json
META.yml
Makefile.PL
README
bin/graphql
lib/GraphQL/Client.pm
lib/GraphQL/Client/CLI.pm [new file with mode: 0644]
lib/GraphQL/Client/http.pm
lib/GraphQL/Client/https.pm
t/00-compile.t
t/00-report-prereqs.dd
t/cli.t [new file with mode: 0755]
t/client.t
xt/author/eol.t
xt/author/no-tabs.t

diff --git a/Changes b/Changes
index 81a7db9b01f1082b67e96eec121f71448667e591..24d318937e93ac6a3fe471edb30cd5571b3a094a 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,8 +1,11 @@
 Revision history for GraphQL-Client.
 
+0.602     2020-03-17 18:27:46-06:00 MST7MDT
+  * Add support for GRAPHQL_CLIENT_OPTIONS environment variable.
+  * Fix a slew of CLI option problems.
+
 0.601     2020-03-15 20:38:38-06:00 MST7MDT
   * Rename "class" attribute to "transport_class".
-  * Minor pod fixups.
 
 0.600     2020-03-15 18:08:57-06:00 MST7MDT
   * Initial public release.
index 8c517a2d6405c4581f8c0e0b7447ed99c5b5c735..b03df1818e9073a1929e708da6586d4cf1209336 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -8,11 +8,13 @@ Makefile.PL
 README
 bin/graphql
 lib/GraphQL/Client.pm
+lib/GraphQL/Client/CLI.pm
 lib/GraphQL/Client/http.pm
 lib/GraphQL/Client/https.pm
 t/00-compile.t
 t/00-report-prereqs.dd
 t/00-report-prereqs.t
+t/cli.t
 t/client.t
 t/http.t
 t/https.t
index 7139b3837deac7298adb7477dc9275763a68c173..c98c070429220aba8a0c8a86ea190016b2363789 100644 (file)
--- a/META.json
+++ b/META.json
@@ -1,5 +1,5 @@
 {
-   "abstract" : "A GraphQL client",
+   "abstract" : "Command-line GraphQL client",
    "author" : [
       "Charles McGarvey <chazmcgarvey@brokenzipper.com>"
    ],
          },
          "requires" : {
             "Carp" : "0",
-            "Getopt::Long" : "0",
+            "Getopt::Long" : "2.39",
             "HTTP::AnyUA" : "0",
             "HTTP::AnyUA::Util" : "0",
             "JSON::MaybeXS" : "0",
             "Module::Load" : "0",
             "Scalar::Util" : "0",
+            "Text::ParseWords" : "0",
             "namespace::clean" : "0",
             "overload" : "0",
             "parent" : "0",
    "provides" : {
       "GraphQL::Client" : {
          "file" : "lib/GraphQL/Client.pm",
-         "version" : "0.601"
+         "version" : "0.602"
+      },
+      "GraphQL::Client::CLI" : {
+         "file" : "lib/GraphQL/Client/CLI.pm",
+         "version" : "0.602"
       },
       "GraphQL::Client::Error" : {
          "file" : "lib/GraphQL/Client.pm",
-         "version" : "0.601"
+         "version" : "0.602"
       },
       "GraphQL::Client::http" : {
          "file" : "lib/GraphQL/Client/http.pm",
-         "version" : "0.601"
+         "version" : "0.602"
       },
       "GraphQL::Client::https" : {
          "file" : "lib/GraphQL/Client/https.pm",
-         "version" : "0.601"
+         "version" : "0.602"
       }
    },
    "release_status" : "stable",
          "web" : "https://github.com/chazmcgarvey/graphql-client"
       }
    },
-   "version" : "0.601",
+   "version" : "0.602",
    "x_authority" : "cpan:CCM",
    "x_generated_by_perl" : "v5.28.0",
    "x_serialization_backend" : "Cpanel::JSON::XS version 4.15",
index 727e00dd61bd016a3a867c4ed4046b9f9e2bc460..95a95186666c8257d6d0c53ae39b7765897bcb24 100644 (file)
--- a/META.yml
+++ b/META.yml
@@ -1,5 +1,5 @@
 ---
-abstract: 'A GraphQL client'
+abstract: 'Command-line GraphQL client'
 author:
   - 'Charles McGarvey <chazmcgarvey@brokenzipper.com>'
 build_requires:
@@ -33,27 +33,31 @@ no_index:
 provides:
   GraphQL::Client:
     file: lib/GraphQL/Client.pm
-    version: '0.601'
+    version: '0.602'
+  GraphQL::Client::CLI:
+    file: lib/GraphQL/Client/CLI.pm
+    version: '0.602'
   GraphQL::Client::Error:
     file: lib/GraphQL/Client.pm
-    version: '0.601'
+    version: '0.602'
   GraphQL::Client::http:
     file: lib/GraphQL/Client/http.pm
-    version: '0.601'
+    version: '0.602'
   GraphQL::Client::https:
     file: lib/GraphQL/Client/https.pm
-    version: '0.601'
+    version: '0.602'
 recommends:
   HTTP::Tiny: '0'
   Pod::Usage: '0'
 requires:
   Carp: '0'
-  Getopt::Long: '0'
+  Getopt::Long: '2.39'
   HTTP::AnyUA: '0'
   HTTP::AnyUA::Util: '0'
   JSON::MaybeXS: '0'
   Module::Load: '0'
   Scalar::Util: '0'
+  Text::ParseWords: '0'
   namespace::clean: '0'
   overload: '0'
   parent: '0'
@@ -64,7 +68,7 @@ resources:
   bugtracker: https://github.com/chazmcgarvey/graphql-client/issues
   homepage: https://github.com/chazmcgarvey/graphql-client
   repository: https://github.com/chazmcgarvey/graphql-client.git
-version: '0.601'
+version: '0.602'
 x_authority: cpan:CCM
 x_generated_by_perl: v5.28.0
 x_serialization_backend: 'YAML::Tiny version 1.73'
index 7d75ca6fdc173d25cd287a2fe7ea81f3b8f68476..2c7c27a4b517365427fc50e943ede579ae5508a6 100644 (file)
@@ -7,7 +7,7 @@ use 5.010;
 use ExtUtils::MakeMaker;
 
 my %WriteMakefileArgs = (
-  "ABSTRACT" => "A GraphQL client",
+  "ABSTRACT" => "Command-line GraphQL client",
   "AUTHOR" => "Charles McGarvey <chazmcgarvey\@brokenzipper.com>",
   "CONFIGURE_REQUIRES" => {
     "ExtUtils::MakeMaker" => 0
@@ -21,12 +21,13 @@ my %WriteMakefileArgs = (
   "NAME" => "GraphQL::Client",
   "PREREQ_PM" => {
     "Carp" => 0,
-    "Getopt::Long" => 0,
+    "Getopt::Long" => "2.39",
     "HTTP::AnyUA" => 0,
     "HTTP::AnyUA::Util" => 0,
     "JSON::MaybeXS" => 0,
     "Module::Load" => 0,
     "Scalar::Util" => 0,
+    "Text::ParseWords" => 0,
     "namespace::clean" => 0,
     "overload" => 0,
     "parent" => 0,
@@ -46,7 +47,7 @@ my %WriteMakefileArgs = (
     "Test::More" => 0,
     "lib" => 0
   },
-  "VERSION" => "0.601",
+  "VERSION" => "0.602",
   "test" => {
     "TESTS" => "t/*.t"
   }
@@ -59,7 +60,7 @@ my %FallbackPrereqs = (
   "File::Spec" => 0,
   "FindBin" => 0,
   "Future" => 0,
-  "Getopt::Long" => 0,
+  "Getopt::Long" => "2.39",
   "HTTP::AnyUA" => 0,
   "HTTP::AnyUA::Backend" => 0,
   "HTTP::AnyUA::Util" => 0,
@@ -71,6 +72,7 @@ my %FallbackPrereqs = (
   "Test::Deep" => 0,
   "Test::Exception" => 0,
   "Test::More" => 0,
+  "Text::ParseWords" => 0,
   "lib" => 0,
   "namespace::clean" => 0,
   "overload" => 0,
diff --git a/README b/README
index ae64d68d1a89769935251ca5f56aa35fbc23feca..8d892553238c915cc9eda0523e0199495cbb514b 100644 (file)
--- a/README
+++ b/README
 NAME
 
-    GraphQL::Client - A GraphQL client
+    graphql - Command-line GraphQL client
 
 VERSION
 
-    version 0.601
+    version 0.602
 
 SYNOPSIS
 
-        my $graphql = GraphQL::Client->new(url => 'http://localhost:4000/graphql');
+        graphql <URL> <QUERY> [ [--variables JSON] | [--variable KEY=VALUE]... ]
+                [--operation-name NAME] [--transport KEY=VALUE]...
+                [--[no-]unpack] [--format json|json:pretty|yaml|perl|csv|tsv|table]
+                [--output FILE]
     
-        # Example: Hello world!
-    
-        my $response = $graphql->execute('{hello}');
-    
-        # Example: Kitchen sink
-    
-        my $query = q[
-            query GetHuman {
-                human(id: $human_id) {
-                    name
-                    height
-                }
-            }
-        ];
-        my $variables = {
-            human_id => 1000,
-        };
-        my $operation_name = 'GetHuman';
-        my $transport_options = {
-            headers => {
-                authorization => 'Bearer s3cr3t',
-            },
-        };
-        my $response = $graphql->execute($query, $variables, $operation_name, $transport_options);
-    
-        # Example: Asynchronous with Mojo::UserAgent (promisify requires Future::Mojo)
-    
-        my $ua = Mojo::UserAgent->new;
-        my $graphql = GraphQL::Client->new(ua => $ua, url => 'http://localhost:4000/graphql');
-    
-        my $future = $graphql->execute('{hello}');
-    
-        $future->promisify->then(sub {
-            my $response = shift;
-            ...
-        });
+        graphql --version|--help|--manual
 
 DESCRIPTION
 
-    GraphQL::Client provides a simple way to execute GraphQL
-    <https://graphql.org/> queries and mutations on a server.
+    graphql is a command-line program for executing queries and mutations
+    on a GraphQL <https://graphql.org/> server.
 
-    This module is the programmatic interface. There is also a "CLI
-    program".
+INSTALL
 
-    GraphQL servers are usually served over HTTP. The provided transport,
-    GraphQL::Client::http, lets you plug in your own user agent, so this
-    client works naturally with HTTP::Tiny, Mojo::UserAgent, and more. You
-    can also use HTTP::AnyUA middleware.
+    There are several ways to install graphql to your system.
 
-ATTRIBUTES
+ from CPAN
 
- url
+    You can install graphql using cpanm:
 
-    The URL of a GraphQL endpoint, e.g. "http://myapiserver/graphql".
+        cpanm GraphQL::Client
 
- unpack
+ from GitHub
 
-    Whether or not to "unpack" the response, which enables a different
-    style for error-handling.
+    You can also choose to download graphql as a self-contained executable:
 
-    Default is 0.
+        curl -OL https://raw.githubusercontent.com/chazmcgarvey/graphql-client/solo/graphql
+        chmod +x graphql
 
-    See "ERROR HANDLING".
+    To hack on the code, clone the repo instead:
 
- transport_class
+        git clone https://github.com/chazmcgarvey/graphql-client.git
+        cd graphql-client
+        make bootstrap      # installs dependencies; requires cpanm
 
-    The package name of a transport.
+OPTIONS
 
-    This is optional if the correct transport can be correctly determined
-    from the "url".
+ --url URL
 
- transport
+    The URL of the GraphQL server endpoint.
 
-    The transport object.
+    If no --url option is given, the first argument is assumed to be the
+    URL.
 
-    By default this is automatically constructed based on "transport_class"
-    or "url".
+    This option is required.
 
-METHODS
+    Alias: -u
 
- new
+ --query STR
 
-        $graphql = GraphQL::Client->new(%attributes);
+    The query or mutation to execute.
 
-    Construct a new client.
+    If no --query option is given, the next argument (after URL) is assumed
+    to be the query.
 
- execute
+    If the value is "-" (which is the default), the query will be read from
+    STDIN.
 
-        $response = $graphql->execute($query);
-        $response = $graphql->execute($query, \%variables);
-        $response = $graphql->execute($query, \%variables, $operation_name);
-        $response = $graphql->execute($query, \%variables, $operation_name, \%transport_options);
-        $response = $graphql->execute($query, \%variables, \%transport_options);
+    See: https://graphql.org/learn/queries/
 
-    Execute a request on a GraphQL server, and get a response.
+    Alias: --mutation
 
-    By default, the response will either be a hashref with the following
-    structure or a Future that resolves to such a hashref, depending on the
-    transport and how it is configured.
+ --variables JSON
 
-        {
-            data   => {
-                field1  => {...}, # or [...]
-                ...
-            },
-            errors => [
-                { message => 'some error message blah blah blah' },
-                ...
-            ],
-        }
+    Provide the variables as a JSON object.
 
-    Note: Setting the "unpack" attribute affects the response shape.
+    Aliases: --vars, -V
 
-ERROR HANDLING
+ --variable KEY=VALUE
 
-    There are two different styles for handling errors.
+    An alternative way to provide variables one at a time. This option can
+    be repeated to provide multiple variables.
 
-    If "unpack" is 0 (off, the default), every response -- whether success
-    or failure -- is enveloped like this:
+    If used in combination with "--variables JSON", this option is silently
+    ignored.
 
-        {
-            data   => {...},
-            errors => [...],
-        }
+    See: https://graphql.org/learn/queries/#variables
+
+    Aliases: --var, -d
+
+ --operation-name NAME
+
+    Inform the server which query/mutation to execute.
+
+    Alias: -n
+
+ --output FILE
+
+    Write the response to a file instead of STDOUT.
+
+    Alias: -o
+
+ --transport KEY=VALUE
+
+    Key-value pairs for configuring the transport (usually HTTP).
+
+    Alias: -t
+
+ --format STR
+
+    Specify the output format to use. See "FORMAT".
+
+    Alias: -f
+
+ --unpack
+
+    Enables unpack mode.
+
+    By default, the response structure is printed as-is from the server,
+    and the program exits 0.
+
+    When unpack mode is enabled, if the response completes with no errors,
+    only the data section of the response is printed and the program exits
+    0. If the response has errors, the whole response structure is printed
+    as-is and the program exits 1.
+
+    See "EXAMPLES".
+
+FORMAT
+
+    The argument for "--format STR" can be one of:
+
+      * csv - Comma-separated values (requires Text::CSV)
+
+      * json:pretty - Human-readable JSON (default)
+
+      * json - JSON
+
+      * perl - Perl code (requires Data::Dumper)
 
-    where data might be missing or undef if errors occurred (though not
-    necessarily) and errors will be missing if the response completed
-    without error.
+      * table - Table (requires Text::Table::Any)
 
-    It is up to you to check for errors in the response, so your code might
-    look like this:
+      * tsv - Tab-separated values (requires Text::CSV)
 
-        my $response = $graphql->execute(...);
-        if (my $errors = $response->{errors}) {
-            # handle $errors
+      * yaml - YAML (requires YAML)
+
+    The csv, tsv, and table formats will only work if the response has a
+    particular shape:
+
+        {
+            "data" : {
+                "onefield" : [
+                    {
+                        "key" : "value",
+                        ...
+                    },
+                    ...
+                ]
+            }
         }
-        else {
-            my $data = $response->{data};
-            # do something with $data
+
+    or
+
+        {
+            "data" : {
+                "onefield" : [
+                    "value",
+                    ...
+                ]
+            }
         }
 
-    If unpack is 1 (on), then "execute" will return just the data if there
-    were no errors, otherwise it will throw an exception. So your code
-    would instead look like this:
+    If the response cannot be formatted, the default format will be used
+    instead, an error message will be printed to STDERR, and the program
+    will exit 3.
+
+    Table formatting can be done by one of several different modules, each
+    with its own features and bugs. The default module is
+    Text::Table::Tiny, but this can be overridden using the PERL_TEXT_TABLE
+    environment variable if desired, like this:
+
+        PERL_TEXT_TABLE=Text::Table::HTML graphql ... -f table
+
+    The list of supported modules is at "@BACKENDS" in Text::Table::Any.
+
+EXAMPLES
+
+    Different ways to provide the query/mutation to execute:
+
+        graphql http://myserver/graphql {hello}
+    
+        echo {hello} | graphql http://myserver/graphql
+    
+        graphql http://myserver/graphql <<END
+        > {hello}
+        > END
+    
+        graphql http://myserver/graphql
+        Interactive mode engaged! Waiting for a query on <STDIN>...
+        {hello}
+        ^D
+
+    Execute a query with variables:
+
+        graphql http://myserver/graphql <<END --var episode=JEDI
+        > query HeroNameAndFriends($episode: Episode) {
+        >   hero(episode: $episode) {
+        >     name
+        >     friends {
+        >       name
+        >     }
+        >   }
+        > }
+        > END
+    
+        graphql http://myserver/graphql --vars '{"episode":"JEDI"}'
+
+    Configure the transport:
+
+        graphql http://myserver/graphql {hello} -t headers.authorization='Basic s3cr3t'
+
+    This example shows the effect of "--unpack":
 
-        my $data = eval { $graphql->execute(...) };
-        if (my $error = $@) {
-            my $resp = $error->{response};
-            # handle errors
+        graphql http://myserver/graphql {hello}
+    
+        # Output:
+        {
+            "data" : {
+                "hello" : "Hello world!"
+            }
         }
-        else {
-            # do something with $data
+    
+        graphql http://myserver/graphql {hello} --unpack
+    
+        # Output:
+        {
+            "hello" : "Hello world!"
         }
 
-    Or if you want to handle errors in a different stack frame, your code
-    is simply this:
+ENVIRONMENT
 
-        my $data = $graphql->execute(...);
-        # do something with $data
+    Some environment variables affect the way graphql behaves:
 
-    Both styles map to Future responses intuitively. If unpack is 0, the
-    response always resolves to the envelope structure. If unpack is 1,
-    successful responses will resolve to just the data and errors will
-    fail/reject.
+      * GRAPHQL_CLIENT_DEBUG - Set to 1 to print diagnostic messages to
+      STDERR.
 
-SEE ALSO
+      * GRAPHQL_CLIENT_HTTP_USER_AGENT - Set the HTTP user agent string.
+
+      * GRAPHQL_CLIENT_OPTIONS - Set the default set of options.
+
+      * PERL_TEXT_TABLE - Set table format backend; see "FORMAT".
+
+EXIT STATUS
+
+    Here is a consolidated summary of what exit statuses mean:
 
-      * graphql - CLI program
+      * 0 - Success
 
-      * GraphQL - Perl implementation of a GraphQL server
+      * 1 - Client or server errors
+
+      * 2 - Option usage is wrong
+
+      * 3 - Could not format the response as requested
+
+SEE ALSO
 
-      * https://graphql.org/ - GraphQL project website
+      * GraphQL::Client - Programmatic interface
 
 BUGS
 
index 6825edc71261b3c44d64b343f0b8fab5a916c9c9..e5c6784dceb77ce70eb4ee848413233eec19a968 100755 (executable)
 #! perl
-# PODNAME: graphql
 # ABSTRACT: Command-line GraphQL client
+# PODNAME: graphql
+
 
 # FATPACK - Do not remove this line.
 
 use warnings;
 use strict;
 
-use Getopt::Long;
-use GraphQL::Client;
-use JSON::MaybeXS;
-
-our $VERSION = '0.601'; # VERSION
-
-my $version;
-my $help;
-my $manual;
-my $url;
-my $transport       = {};
-my $query           = '-';
-my $variables       = {};
-my $operation_name;
-my $format          = 'json:pretty';
-my $unpack          = 0;
-my $outfile;
-GetOptions(
-    'version'               => \$version,
-    'help|h|?'              => \$help,
-    'manual|man'            => \$manual,
-    'url|u=s'               => \$url,
-    'query|mutation=s'      => \$query,
-    'variables|vars|V=s'    => \$variables,
-    'variable|var|d=s%'     => \$variables,
-    'operation-name|n=s'    => \$operation_name,
-    'transport|t=s%'        => \$transport,
-    'format|f=s'            => \$format,
-    'unpack!'               => \$unpack,
-    'output|o=s'            => \$outfile,
-) or pod2usage(2);
-
-if ($version) {
-    print "graphql $VERSION\n";
-    exit 0;
-}
-if ($help) {
-    pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS)]);
-}
-if ($manual) {
-    pod2usage(-exitval => 0, -verbose => 2);
-}
-
-$url    = shift if !$url;
-$query  = shift if !$query || $query eq '-';
-
-if (!$url) {
-    print STDERR "The <URL> or --url option argument is required.\n";
-    pod2usage(2);
-}
-
-$transport = expand_vars($transport);
-
-if (ref $variables) {
-    $variables = expand_vars($variables);
-}
-else {
-    $variables = JSON::MaybeXS->new->decode($variables);
-}
-
-my $client = GraphQL::Client->new(url => $url);
-
-eval { $client->transport };
-if (my $err = $@) {
-    warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
-    print STDERR "Could not construct a transport for URL: $url\n";
-    print STDERR "Is this URL correct?\n";
-    pod2usage(2);
-}
-
-if (!$query || $query eq '-') {
-    print STDERR "Interactive mode engaged! Waiting for a query on <STDIN>...\n"
-        if -t STDIN; ## no critic (InputOutput::ProhibitInteractiveTest)
-    $query = do { local $/; <> };
-}
-
-my $resp = $client->execute($query, $variables, $operation_name, $transport);
-my $err  = $resp->{errors};
-$unpack = 0 if $err;
-my $data = $unpack ? $resp->{data} : $resp;
-
-if ($outfile) {
-    open(my $out, '>', $outfile) or die "Open $outfile failed: $!";
-    *STDOUT = $out;
-}
-
-print_data($data, $format);
-
-exit($unpack && $err ? 1 : 0);
-
-sub print_data {
-    my ($data, $format) = @_;
-    $format = lc($format || 'json:pretty');
-    if ($format eq 'json' || $format eq 'json:pretty') {
-        my %opts = (canonical => 1, utf8 => 1);
-        $opts{pretty} = 1 if $format eq 'json:pretty';
-        print JSON::MaybeXS->new(%opts)->encode($data);
-    }
-    elsif ($format eq 'yaml') {
-        eval { require YAML } or die "Missing dependency: YAML\n";
-        print YAML::Dump($data);
-    }
-    elsif ($format eq 'csv' || $format eq 'tsv' || $format eq 'table') {
-        my $sep = $format eq 'tsv' ? "\t" : ',';
-
-        my $unpacked = $data;
-        $unpacked = $data->{data} if !$unpack && !$err;
-
-        # check the response to see if it can be formatted
-        my @columns;
-        my $rows = [];
-        if (keys %$unpacked == 1) {
-            my ($val) = values %$unpacked;
-            if (ref $val eq 'ARRAY') {
-                my $first = $val->[0];
-                if ($first && ref $first eq 'HASH') {
-                    @columns = sort keys %$first;
-                    $rows = [
-                        map { [@{$_}{@columns}] } @$val
-                    ];
-                }
-                elsif ($first) {
-                    @columns = keys %$unpacked;
-                    $rows = [map { [$_] } @$val];
-                }
-            }
-        }
-
-        if (@columns) {
-            if ($format eq 'table') {
-                eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n";
-                my $table = Text::Table::Any::table(
-                    header_row  => 1,
-                    rows        => [[@columns], @$rows],
-                    backend     => $ENV{PERL_TEXT_TABLE},
-                );
-                print $table;
-            }
-            else {
-                eval { require Text::CSV } or die "Missing dependency: Text::CSV\n";
-                my $csv = Text::CSV->new({binary => 1, sep => $sep, eol => $/});
-                $csv->print(*STDOUT, [@columns]);
-                for my $row (@$rows) {
-                    $csv->print(*STDOUT, $row);
-                }
-            }
-        }
-        else {
-            print_data($data);
-            print STDERR sprintf("Error: Response could not be formatted as %s.\n", uc($format));
-            exit 3;
-        }
-    }
-    elsif ($format eq 'perl') {
-        eval { require Data::Dumper } or die "Missing dependency: Data::Dumper\n";
-        print Data::Dumper::Dumper($data);
-    }
-    else {
-        print STDERR "Error: Format not supported: $format\n";
-        print_data($data);
-        exit 3;
-    }
-}
-
-sub expand_vars {
-    my $vars = shift;
-
-    my %out;
-    while (my ($key, $value) = each %$vars) {
-        my $var = $value;
-        my @segments = split(/\./, $key);
-        for my $segment (reverse @segments) {
-            my $saved = $var;
-            if ($segment =~ /^(\d+)$/) {
-                $var = [];
-                $var->[$segment] = $saved;
-            }
-            else {
-                $var = {};
-                $var->{$segment} = $saved;
-            }
-        }
-        %out = (%out, %$var);
-    }
-
-    return \%out;
-}
+use GraphQL::Client::CLI;
 
-sub pod2usage {
-    eval { require Pod::Usage };
-    if ($@) {
-        my $ref  = $VERSION eq '999.999' ? 'master' : "v$VERSION";
-        my $exit = (@_ == 1 && $_[0] =~ /^\d+$/ && $_[0]) //
-                   (@_ % 2 == 0 && {@_}->{'-exitval'})    // 2;
-        print STDERR <<END;
-Online documentation is available at:
+our $VERSION = '0.602'; # VERSION
 
-  https://github.com/chazmcgarvey/graphql-client/blob/$ref/README.md
-
-Tip: To enable inline documentation, install the Pod::Usage module.
-
-END
-        exit $exit;
-    }
-    else {
-        goto &Pod::Usage::pod2usage;
-    }
-}
+GraphQL::Client::CLI->main(@ARGV);
 
 __END__
 
@@ -228,7 +26,7 @@ graphql - Command-line GraphQL client
 
 =head1 VERSION
 
-version 0.601
+version 0.602
 
 =head1 SYNOPSIS
 
@@ -299,7 +97,8 @@ Aliases: C<--vars>, C<-V>
 
 =head2 C<--variable KEY=VALUE>
 
-An alternative way to provide variables. Repeat this option to provide multiple variables.
+An alternative way to provide variables one at a time. This option can be repeated to provide
+multiple variables.
 
 If used in combination with L</"--variables JSON">, this option is silently ignored.
 
@@ -485,6 +284,10 @@ C<GRAPHQL_CLIENT_HTTP_USER_AGENT> - Set the HTTP user agent string.
 
 =item *
 
+C<GRAPHQL_CLIENT_OPTIONS> - Set the default set of options.
+
+=item *
+
 C<PERL_TEXT_TABLE> - Set table format backend; see L</FORMAT>.
 
 =back
@@ -513,6 +316,16 @@ C<3> - Could not format the response as requested
 
 =back
 
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<GraphQL::Client> - Programmatic interface
+
+=back
+
 =head1 BUGS
 
 Please report any bugs or feature requests on the bugtracker website
index 8b93e4b0f0def250a6a8ccfd6638240b723a8969..c4a61192950bfa4d5c9d9f077def3ec232cf07e2 100644 (file)
@@ -8,7 +8,7 @@ use Module::Load qw(load);
 use Scalar::Util qw(reftype);
 use namespace::clean;
 
-our $VERSION = '0.601'; # VERSION
+our $VERSION = '0.602'; # VERSION
 
 sub _croak { require Carp; goto &Carp::croak }
 sub _throw { GraphQL::Client::Error->throw(@_) }
@@ -171,7 +171,7 @@ GraphQL::Client - A GraphQL client
 
 =head1 VERSION
 
-version 0.601
+version 0.602
 
 =head1 SYNOPSIS
 
diff --git a/lib/GraphQL/Client/CLI.pm b/lib/GraphQL/Client/CLI.pm
new file mode 100644 (file)
index 0000000..bb2ab03
--- /dev/null
@@ -0,0 +1,350 @@
+package GraphQL::Client::CLI;
+# ABSTRACT: Implementation of the graphql CLI program
+
+use warnings;
+use strict;
+
+use Text::ParseWords;
+use Getopt::Long 2.39 qw(GetOptionsFromArray);
+use GraphQL::Client;
+use JSON::MaybeXS;
+use namespace::clean;
+
+our $VERSION = '0.602'; # VERSION
+
+sub _croak { require Carp; goto &Carp::croak }
+
+sub new {
+    my $class = shift;
+    bless {}, $class;
+}
+
+sub main {
+    my $self = shift;
+    $self = $self->new if !ref $self;
+
+    my $options = eval { $self->_get_options(@_) };
+    if (my $err = $@) {
+        print STDERR $err;
+        _pod2usage(2);
+    }
+
+    if ($options->{version}) {
+        print "graphql $VERSION\n";
+        exit 0;
+    }
+    if ($options->{help}) {
+        _pod2usage(-exitval => 0, -verbose => 99, -sections => [qw(NAME SYNOPSIS OPTIONS)]);
+    }
+    if ($options->{manual}) {
+        _pod2usage(-exitval => 0, -verbose => 2);
+    }
+
+    my $url = $options->{url};
+    if (!$url) {
+        print STDERR "The <URL> or --url option argument is required.\n";
+        _pod2usage(2);
+    }
+
+    my $variables = $options->{variables};
+    my $query = $options->{query};
+    my $operation_name = $options->{operation_name};
+    my $unpack = $options->{unpack};
+    my $outfile = $options->{outfile};
+    my $format = $options->{format};
+    my $transport = $options->{transport};
+
+    my $client = GraphQL::Client->new(url => $url);
+
+    eval { $client->transport };
+    if (my $err = $@) {
+        warn $err if $ENV{GRAPHQL_CLIENT_DEBUG};
+        print STDERR "Could not construct a transport for URL: $url\n";
+        print STDERR "Is this URL correct?\n";
+        _pod2usage(2);
+    }
+
+    if ($query eq '-') {
+        print STDERR "Interactive mode engaged! Waiting for a query on <STDIN>...\n"
+            if -t STDIN; ## no critic (InputOutput::ProhibitInteractiveTest)
+        $query = do { local $/; <STDIN> };
+    }
+
+    my $resp = $client->execute($query, $variables, $operation_name, $transport);
+    my $err  = $resp->{errors};
+    $unpack = 0 if $err;
+    my $data = $unpack ? $resp->{data} : $resp;
+
+    if ($outfile) {
+        open(my $out, '>', $outfile) or die "Open $outfile failed: $!";
+        *STDOUT = $out;
+    }
+
+    _print_data($data, $format);
+
+    exit($unpack && $err ? 1 : 0);
+}
+
+sub _get_options {
+    my $self = shift;
+    my @args = @_;
+
+    unshift @args, shellwords($ENV{GRAPHQL_CLIENT_OPTIONS} || '');
+
+    my %options = (
+        format  => 'json:pretty',
+        unpack  => 0,
+    );
+
+    GetOptionsFromArray(\@args,
+        'version'               => \$options{version},
+        'help|h|?'              => \$options{help},
+        'manual|man'            => \$options{manual},
+        'url|u=s'               => \$options{url},
+        'query|mutation=s'      => \$options{query},
+        'variables|vars|V=s'    => \$options{variables},
+        'variable|var|d=s%'     => \$options{variables},
+        'operation-name|n=s'    => \$options{operation_name},
+        'transport|t=s%'        => \$options{transport},
+        'format|f=s'            => \$options{format},
+        'unpack!'               => \$options{unpack},
+        'output|o=s'            => \$options{outfile},
+    ) or _pod2usage(2);
+
+    $options{url}   = shift @args if !$options{url};
+    $options{query} = shift @args if !$options{query};
+
+    $options{query} ||= '-';
+
+    my $transport = eval { _expand_vars($options{transport}) };
+    die "Two or more --transport keys are incompatible.\n" if $@;
+
+    if (ref $options{variables}) {
+        $options{variables} = eval { _expand_vars($options{variables}) };
+        die "Two or more --variable keys are incompatible.\n" if $@;
+    }
+    elsif ($options{variables}) {
+        $options{variables} = eval { JSON::MaybeXS->new->decode($options{variables}) };
+        die "The --variables JSON does not parse.\n" if $@;
+    }
+
+    return \%options;
+}
+
+sub _print_data {
+    my ($data, $format) = @_;
+    $format = lc($format || 'json:pretty');
+    if ($format eq 'json' || $format eq 'json:pretty') {
+        my %opts = (allow_nonref => 1, canonical => 1, utf8 => 1);
+        $opts{pretty} = 1 if $format eq 'json:pretty';
+        print JSON::MaybeXS->new(%opts)->encode($data);
+    }
+    elsif ($format eq 'yaml') {
+        eval { require YAML } or die "Missing dependency: YAML\n";
+        print YAML::Dump($data);
+    }
+    elsif ($format eq 'csv' || $format eq 'tsv' || $format eq 'table') {
+        my $sep = $format eq 'tsv' ? "\t" : ',';
+
+        my $unpacked = $data;
+        # $unpacked = $data->{data} if !$unpack && !$err;
+        $unpacked = $data->{data} if $data && $data->{data};
+
+        # check the response to see if it can be formatted
+        my @columns;
+        my $rows = [];
+        if (keys %$unpacked == 1) {
+            my ($val) = values %$unpacked;
+            if (ref $val eq 'ARRAY') {
+                my $first = $val->[0];
+                if ($first && ref $first eq 'HASH') {
+                    @columns = sort keys %$first;
+                    $rows = [
+                        map { [@{$_}{@columns}] } @$val
+                    ];
+                }
+                elsif ($first) {
+                    @columns = keys %$unpacked;
+                    $rows = [map { [$_] } @$val];
+                }
+            }
+        }
+
+        if (@columns) {
+            if ($format eq 'table') {
+                eval { require Text::Table::Any } or die "Missing dependency: Text::Table::Any\n";
+                my $table = Text::Table::Any::table(
+                    header_row  => 1,
+                    rows        => [[@columns], @$rows],
+                    backend     => $ENV{PERL_TEXT_TABLE},
+                );
+                print $table;
+            }
+            else {
+                eval { require Text::CSV } or die "Missing dependency: Text::CSV\n";
+                my $csv = Text::CSV->new({binary => 1, sep => $sep, eol => $/});
+                $csv->print(*STDOUT, [@columns]);
+                for my $row (@$rows) {
+                    $csv->print(*STDOUT, $row);
+                }
+            }
+        }
+        else {
+            _print_data($data);
+            print STDERR sprintf("Error: Response could not be formatted as %s.\n", uc($format));
+            exit 3;
+        }
+    }
+    elsif ($format eq 'perl') {
+        eval { require Data::Dumper } or die "Missing dependency: Data::Dumper\n";
+        print Data::Dumper::Dumper($data);
+    }
+    else {
+        print STDERR "Error: Format not supported: $format\n";
+        _print_data($data);
+        exit 3;
+    }
+}
+
+sub _parse_path {
+    my $path = shift;
+
+    my @path;
+
+    my @segments = map { split(/\./, $_) } split(/(\[[^\.\]]+\])\.?/, $path);
+    for my $segment (@segments) {
+        if ($segment =~ /\[([^\.\]]+)\]/) {
+            $path[-1]{type} = 'ARRAY' if @path;
+            push @path, {
+                name  => $1,
+                index => 1,
+            };
+        }
+        else {
+            $path[-1]{type} = 'HASH' if @path;
+            push @path, {
+                name => $segment,
+            };
+        }
+    }
+
+    return \@path;
+}
+
+sub _expand_vars {
+    my $vars = shift;
+
+    my $root = {};
+
+    while (my ($key, $value) = each %$vars) {
+        my $parsed_path = _parse_path($key);
+
+        my $curr = $root;
+        for my $segment (@$parsed_path) {
+            my $name = $segment->{name};
+            my $type = $segment->{type} || '';
+            my $next = $type eq 'HASH' ? {} : $type eq 'ARRAY' ? [] : $value;
+            if (ref $curr eq 'HASH') {
+                _croak 'Conflicting keys' if $segment->{index};
+                if (defined $curr->{$name}) {
+                    _croak 'Conflicting keys' if $type ne ref $curr->{$name};
+                    $next = $curr->{$name};
+                }
+                else {
+                    $curr->{$name} = $next;
+                }
+            }
+            elsif (ref $curr eq 'ARRAY') {
+                _croak 'Conflicting keys' if !$segment->{index};
+                if (defined $curr->[$name]) {
+                    _croak 'Conflicting keys' if $type ne ref $curr->[$name];
+                    $next = $curr->[$name];
+                }
+                else {
+                    $curr->[$name] = $next;
+                }
+            }
+            else {
+                _croak 'Conflicting keys';
+            }
+            $curr = $next;
+        }
+    }
+
+    return $root;
+}
+
+sub _pod2usage {
+    eval { require Pod::Usage };
+    if ($@) {
+        my $ref  = $VERSION eq '999.999' ? 'master' : "v$VERSION";
+        my $exit = (@_ == 1 && $_[0] =~ /^\d+$/ && $_[0]) //
+                   (@_ % 2 == 0 && {@_}->{'-exitval'})    // 2;
+        print STDERR <<END;
+Online documentation is available at:
+
+  https://github.com/chazmcgarvey/graphql-client/blob/$ref/README.md
+
+Tip: To enable inline documentation, install the Pod::Usage module.
+
+END
+        exit $exit;
+    }
+    else {
+        goto &Pod::Usage::pod2usage;
+    }
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+GraphQL::Client::CLI - Implementation of the graphql CLI program
+
+=head1 VERSION
+
+version 0.602
+
+=head1 DESCRIPTION
+
+This is the actual implementation of L<graphql>.
+
+The interface is B<EXPERIMENTAL>. Don't rely on it.
+
+=head1 METHODS
+
+=head2 new
+
+Construct a new CLI.
+
+=head2 main
+
+Run the script.
+
+=head1 BUGS
+
+Please report any bugs or feature requests on the bugtracker website
+L<https://github.com/chazmcgarvey/graphql-client/issues>
+
+When submitting a bug or request, please include a test-file or a
+patch to an existing test-file that illustrates the bug or desired
+feature.
+
+=head1 AUTHOR
+
+Charles McGarvey <chazmcgarvey@brokenzipper.com>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2020 by Charles McGarvey.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
index a53228af1566b08312e0541fc4fa0a7d986d7daa..667e44ebc5ac2f8e0389078b1003c9d0159ff0d3 100644 (file)
@@ -9,7 +9,7 @@ use HTTP::AnyUA::Util qw(www_form_urlencode);
 use HTTP::AnyUA;
 use namespace::clean;
 
-our $VERSION = '0.601'; # VERSION
+our $VERSION = '0.602'; # VERSION
 
 sub _croak { require Carp; goto &Carp::croak }
 
@@ -90,6 +90,8 @@ sub _handle_error {
     my $reason  = $resp->{reason}  // '';
     my $message = "HTTP transport returned $resp->{status} ($reason): $content";
 
+    chomp $message;
+
     return {
         error       => $message,
         response    => $data,
@@ -169,7 +171,7 @@ GraphQL::Client::http - GraphQL over HTTP
 
 =head1 VERSION
 
-version 0.601
+version 0.602
 
 =head1 SYNOPSIS
 
@@ -279,8 +281,8 @@ See L</ATTRIBUTES>.
 
 Get a response from the GraphQL server.
 
-The C<%data> structure must have a C<query> key whose value is the query or mutation string. It may
-optionally have a C<variables> hashref and an C<operationName> string.
+The C<%request> structure must have a C<query> key whose value is the query or mutation string. It
+may optionally have a C<variables> hashref and an C<operationName> string.
 
 The C<%options> structure is optional and may contain options passed through to the user agent. The
 only useful options are C<headers> (which should have a hashref value) and C<method> and C<url> to
@@ -294,7 +296,7 @@ such a hashref:
             data   => {...},
             errors => [...],
         },
-        error       => 'Something happened',    # may be ommitted if no error occurred
+        error       => 'Something happened',    # omitted if no error occurred
         details     => {    # optional information which may aide troubleshooting
         },
     }
index f73b48f4f282b7e5dcd0a1e566482177a019c24f..6df0df98fd1cec4999fdf8c9b2c3aee53d142848 100644 (file)
@@ -6,7 +6,7 @@ use strict;
 
 use parent 'GraphQL::Client::http';
 
-our $VERSION = '0.601'; # VERSION
+our $VERSION = '0.602'; # VERSION
 
 sub new {
     my $class = shift;
@@ -27,7 +27,7 @@ GraphQL::Client::https - GraphQL over HTTPS
 
 =head1 VERSION
 
-version 0.601
+version 0.602
 
 =head1 DESCRIPTION
 
index 676ed70453919689ad4c038ef1158fbe4f0df79e..9c2c70052ffa4efac9eba63ec237fa14dfb16d27 100644 (file)
@@ -6,10 +6,11 @@ use warnings;
 
 use Test::More;
 
-plan tests => 4 + ($ENV{AUTHOR_TESTING} ? 1 : 0);
+plan tests => 5 + ($ENV{AUTHOR_TESTING} ? 1 : 0);
 
 my @module_files = (
     'GraphQL/Client.pm',
+    'GraphQL/Client/CLI.pm',
     'GraphQL/Client/http.pm',
     'GraphQL/Client/https.pm'
 );
index 8ec21f8eb462a46b4cbb2a3f725c0e21c5b57086..fa4b2196e38dcdf7aae13a3e9d358c08f0fff783 100644 (file)
@@ -48,12 +48,13 @@ do { my $x = {
                                       },
                       'requires' => {
                                       'Carp' => '0',
-                                      'Getopt::Long' => '0',
+                                      'Getopt::Long' => '2.39',
                                       'HTTP::AnyUA' => '0',
                                       'HTTP::AnyUA::Util' => '0',
                                       'JSON::MaybeXS' => '0',
                                       'Module::Load' => '0',
                                       'Scalar::Util' => '0',
+                                      'Text::ParseWords' => '0',
                                       'namespace::clean' => '0',
                                       'overload' => '0',
                                       'parent' => '0',
diff --git a/t/cli.t b/t/cli.t
new file mode 100755 (executable)
index 0000000..2ae980e
--- /dev/null
+++ b/t/cli.t
@@ -0,0 +1,81 @@
+#!/usr/bin/env perl
+
+use warnings;
+use strict;
+
+use Test::Exception;
+use Test::More;
+
+use GraphQL::Client::CLI;
+
+subtest 'get_options' => sub {
+    my $expected = {
+        format          => 'json:pretty',
+        help            => undef,
+        manual          => undef,
+        operation_name  => undef,
+        outfile         => undef,
+        query           => 'bar',
+        transport       => undef,
+        unpack          => 0,
+        url             => 'foo',
+        variables       => undef,
+        version         => undef,
+    };
+
+    my $r = GraphQL::Client::CLI->_get_options(qw{--url foo --query bar});
+    is_deeply($r, $expected, '--url and --query set options') or diag explain $r;
+
+    $r = GraphQL::Client::CLI->_get_options(qw{foo --query bar});
+    is_deeply($r, $expected, '--url is optional') or diag explain $r;
+
+    $r = GraphQL::Client::CLI->_get_options(qw{foo bar});
+    is_deeply($r, $expected, '--query is also optional') or diag explain $r;
+};
+
+subtest 'expand_vars' => sub {
+    my $r = GraphQL::Client::CLI::_expand_vars({
+        'foo.bar'       => 'baz',
+        'foo.qux.muf'   => 42,
+        'arr1[1].tut'   => 'whatever',
+        'arr2[1][0].meh'=> 3.1415,
+    });
+    is_deeply($r, {
+        foo => {
+            bar => 'baz',
+            qux => {
+                muf => 42,
+            },
+        },
+        arr1 => [
+            undef,
+            {
+                tut => 'whatever',
+            }
+        ],
+        arr2 => [
+            undef,
+            [
+                {
+                    meh => 3.1415,
+                },
+            ],
+        ],
+    }, 'expand all the vars') or diag explain $r;
+
+    throws_ok {
+        GraphQL::Client::CLI::_expand_vars({
+            'foo[0]'    => 'baz',
+            'foo.bar'   => 'muf',
+        });
+    } qr/^Conflicting keys/, 'throw if conflict between hash and array';
+
+    throws_ok {
+        GraphQL::Client::CLI::_expand_vars({
+            'foo'       => 'baz',
+            'foo.bar'   => 'muf',
+        });
+    } qr/^Conflicting keys/, 'throw if conflict between hash and scalar';
+};
+
+done_testing;
index 1dd0cd425ab19c5f519bee7ab5a63e7a791d173d..841bd8c5f68d5bbb3660a619536370b4d6c082d7 100755 (executable)
@@ -60,7 +60,7 @@ subtest 'request to transport' => sub {
     $req = ($mock->requests)[-1];
     is_deeply($req->[1], {
         baz => 'qux',
-    }, 'operation name can be ommitted with transport options');
+    }, 'operation name can be omitted with transport options');
 };
 
 subtest 'success response' => sub {
index aae18ba62d2b3a5c9f91e0df78503ad327bb1395..afb6494cdee2433aa0ccb04b5b5f2c1dfbd859b5 100644 (file)
@@ -9,11 +9,13 @@ use Test::EOL;
 my @files = (
     'bin/graphql',
     'lib/GraphQL/Client.pm',
+    'lib/GraphQL/Client/CLI.pm',
     'lib/GraphQL/Client/http.pm',
     'lib/GraphQL/Client/https.pm',
     't/00-compile.t',
     't/00-report-prereqs.dd',
     't/00-report-prereqs.t',
+    't/cli.t',
     't/client.t',
     't/http.t',
     't/https.t',
index f70f55c231a4dc2806528379889cf734dc8fc45a..68ac37ba75ed0ce99828a4fe01abc094f5ea2083 100644 (file)
@@ -9,11 +9,13 @@ use Test::NoTabs;
 my @files = (
     'bin/graphql',
     'lib/GraphQL/Client.pm',
+    'lib/GraphQL/Client/CLI.pm',
     'lib/GraphQL/Client/http.pm',
     'lib/GraphQL/Client/https.pm',
     't/00-compile.t',
     't/00-report-prereqs.dd',
     't/00-report-prereqs.t',
+    't/cli.t',
     't/client.t',
     't/http.t',
     't/https.t',
This page took 0.058412 seconds and 4 git commands to generate.