From a7d093be5958539bf2f3d2abfcdb95e3a703e44a Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Wed, 12 Dec 2018 08:50:55 +0200 Subject: [PATCH 01/68] Added DWIM-y processing of param names for {+params} --- lib/WebService/GoogleAPI/Client.pm | 132 +++++++++++++++++++---------- 1 file changed, 88 insertions(+), 44 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 3b2cf7c..1a09366 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -9,8 +9,10 @@ use WebService::GoogleAPI::Client::UserAgent; use WebService::GoogleAPI::Client::Discovery; use Carp; use CHI; -use List::MoreUtils qw(uniq); +use Mojo::Util; +#TODO- batch requests. The only thing necessary is to send a +#multipart request as I wrote in that e-mail. # ABSTRACT: Google API Discovery and SDK @@ -327,58 +329,100 @@ sub _process_params_for_api_endpoint_and_return_errors ################################################## sub _interpolate_path_parameters_append_query_params_and_return_errors { - my ( $self, $params, $method_discovery_struct ) = @_; + my ( $self, $params, $discovery_struct ) = @_; my @teapot_errors = (); my @get_query_params = (); - my %path_params = map { $_ => 1 } ($params->{path} =~ /\{\+*([^\}]+)\}/xg); ## the params embedded in the path as indexes of hash - foreach my $meth_param_spec ( uniq(keys %path_params ,keys %{ $method_discovery_struct->{ parameters } } ) ) - { - if ( (defined $path_params{ $meth_param_spec } ) && (defined $params->{ options }{ $meth_param_spec } ) ) ## if param option is in the path then interpolate and forget - { - $params->{ path } =~ s/\{\+*$meth_param_spec\}/$params->{options}{$meth_param_spec}/xsmg; - delete $params->{ options }{ $meth_param_spec }; ## if a GET param should not also be a BODY param + #create a hash of whatever the expected params may be + my %path_params; my $param_regex = qr/\{ \+? ([^\}]+) \}/x; + if ($params->{path} ne $discovery_struct->{path}) { + #check if the path was given as a custom path. If it is, just + #interpolate things directly, and assume the user is responsible + %path_params = map { $_ => 'custom' } ($params->{path} =~ /$param_regex/xg); + } else { + #label which param names are from the normal path and from the + #flat path + %path_params = map { $_ => 'plain' } ($discovery_struct->{path} =~ /$param_regex/xg); + if ($discovery_struct->{flatPath}) { + %path_params = (%path_params, + map { $_ => 'flat' } ($discovery_struct->{flatPath} =~ /$param_regex/xg) ) } - elsif ( $method_discovery_struct->{ parameters }{ $meth_param_spec }{ 'location' } eq 'path' ) ## this is a path variable - { - ## interpolate variables into URI if available and not filled - if ( $params->{ path } =~ /\{.+\}/xms ) ## there are un-interpolated variables in the path - try to fill them for this param if reqd - { - #carp( "$params->{path} includes un-filled variables " ) if $self->debug > 10; - ## requires interpolations into the URI -- consider && into containing if - if ( $params->{ path } =~ /\{\+*$meth_param_spec\}/xg ) ## eg match {jobId} or {+jobId} in 'v1/jobs/{jobId}/reports/{reportId}' - { - ## if provided as an option - if ( defined $params->{ options }{ $meth_param_spec } ) - { - carp( "DEBUG: $meth_param_spec is defined in param->{options}" ) if $self->{debug} > 10; - $params->{ path } =~ s/\{\+*$meth_param_spec\}/$params->{options}{$meth_param_spec}/xsmg; - delete $params->{ options }{ $meth_param_spec }; ## if a GET param should not also be a BODY param - } - elsif ( defined $method_discovery_struct->{ parameters }{ $meth_param_spec }{ default } ) ## not provided as an option but a default value is provided in the spec - should be assumed by server so could probbaly remove this - { - $params->{ path } =~ s/\{\+*$meth_param_spec\}/$method_discovery_struct->{parameters}{$meth_param_spec}{default}/xsmg; - } - else ## otherwise flag as an error - unable to interpolate - { - push( @teapot_errors, "$params->{path} requires interpolation value for $meth_param_spec but none provided as option and no default value provided by specification" ); - } - } + } + + + #small subs to convert between these_types to theseTypes of params + sub camel { $_[0] =~ s/ _(\w) /\u$1/grx }; + sub snake { $_[0] =~ s/([[:upper:]])/_\l$1/grx }; + + #switch the path we're dealing with to the flat path if any of + #the parameters match the flat path + $params->{path} = $discovery_struct->{flatPath} + if grep { $_ eq 'flat' } map {$path_params{camel $_} || ()} keys %{ $params->{options} }; + + + #loop through params given, placing them in the path or query, + #or leaving them for the request body + for my $param_name ( keys %{ $params->{options} } ) { + + #first check if it needs to be interpolated into the path + if ($path_params{$param_name}) { + #pull out the value from the hash, and remove the key + my $param_value = delete $params->{options}{$param_name}; + + #camelize the param name if not passed in customly, allowing + #the user to pass in camelCase or snake_case param names + $param_name = camel $param_name if $path_params{$param_name} ne 'custom'; + + #first deal with any param that doesn't have a plus, b/c + #those just get interpolated + $params->{path} =~ s/\{$param_name\}/$param_value/; + + #if there's a plus in the path spec, we need more work + if ($params->{path} =~ /\{ \+ $param_name \}/x) { + my $pattern = $discovery_struct->{parameters}{$param_name}{pattern}; + #if the given param matches google's pattern for the + #param, just interpolate it straight + if ($param_value =~ /$pattern/) { + $params->{path} =~ s/\{\+$param_name\}/$param_value/; + } else { + #N.B. perhaps support passing an arrayref or csv for those + #params such as jobs.projects.jobs.delete which have two + #dynamic parts. But for now, unnecessary + #remove the regexy parts of the pattern to interpolate it + #into the path, assuming the user has provided just the + #dynamic portion of the param. + $pattern =~ s/\^ \$//gx; + $params->{path} =~ s/\{\+$param_name\}/$pattern/x; + $params->{path} =~ s/\[ \^ \/ \]/$param_value/x; + } } + #skip to the next run, so I don't need an else clause later + next; #I don't like nested if-elses } - elsif ( ( defined $params->{ options }{ $meth_param_spec } ) && ( $method_discovery_struct->{ parameters }{ $meth_param_spec }{ 'location' } eq 'query' )) ## check post form variables .. assume not get? - { - $params->{ options }{ $meth_param_spec } = $method_discovery_struct->{ parameters }{ $meth_param_spec }{ default } if ( defined $method_discovery_struct->{ parameters }{ $meth_param_spec }{ default } ); - push( @get_query_params, "$meth_param_spec=$params->{options}{$meth_param_spec}" ) if defined $params->{ options }{ $meth_param_spec }; - delete $params->{ options }{ $meth_param_spec }; - } + #if it's not in the list of params, then it goes in the + #request body, and our work here is done + next unless $discovery_struct->{parameters}{$param_name}; + + #it must be a GET type query param, so push the name and value + #on our param stack and take it off of the options list + push @get_query_params, $param_name, delete $params->{options}{$param_name}; } - if ( scalar(@get_query_params)>0 ) - { - #$params->{path} .= '/' unless $params->{path} =~ /\/$/mx; - $params->{path} .= '?' . join('&', @get_query_params ); + + #if there are any query params... + if ( @get_query_params ) { + #interpolate and escape the get query params built up in our + #former for loop + $params->{path} .= '?' . Mojo::Parameters->new(@get_query_params); } + + #interpolate default value for path params if not given. Needed + #for things like the gmail API, where userID is 'me' by default + for my $param_name ( $params->{path} =~ /$param_regex/g ) { + my $param_value = $discovery_struct->{parameters}{$param_name}{default}; + $params->{path} =~ s/\{$param_name\}/$param_value/ if $param_value; + } + #print pp $params; #exit; return @teapot_errors; From e61c23b4a69a80a88186483a77ad8d1425677459 Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Thu, 13 Dec 2018 20:20:43 +0200 Subject: [PATCH 02/68] Added tests and docs for new feature --- lib/WebService/GoogleAPI/Client.pm | 147 +++++++++++++++++++++-------- t/00-load.t | 2 +- t/01-client-discovery.t | 2 +- t/03-pre-warmed-cache-discovery.t | 16 +++- t/04-client-mocked-agent.t | 4 +- t/05-spec-interpolation.t | 90 ++++++++++++++++++ 6 files changed, 215 insertions(+), 46 deletions(-) create mode 100644 t/05-spec-interpolation.t diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 1a09366..de67001 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -147,7 +147,7 @@ handles user auth token inclusion in request headers and refreshes token if requ Required params: method, route -Optional params: api_endpoint_id cb_method_discovery_modify +Optional params: api_endpoint_id cb_method_discovery_modify, options $self->access_token must be valid @@ -166,45 +166,112 @@ $self->access_token must be valid ## if provide the Google API Endpoint to inform pre-query validation say $gapi_agent->api_query( api_endpoint_id => 'gmail.users.messages.send', - options => { raw => encode_base64( - Email::Simple->create( header => [To => $user, From => $user, Subject =>"Test email from $user",], - body => "This is the body of email from $user to $user", )->as_string - ), - }, - )->to_string; ## + options => + { raw => encode_base64( Email::Simple->create( + header => [To => $user, From => $user, + Subject =>"Test email from $user",], + body => "This is the body of email from $user to $user", + )->as_string ), + }, + )->to_string; ## print $gapi_agent->api_query( - api_endpoint_id => 'gmail.users.messages.list', ## auto sets method to GET, path to 'https://www.googleapis.com/calendar' + api_endpoint_id => 'gmail.users.messages.list', + ## auto sets method to GET, and the path to + ## 'https://www.googleapis.com/gmail/v1/users/me/messages' )->to_string; - #print pp $r; +If the pre-query validation fails then a 418 - I'm a Teapot error response is +returned with the body containing the specific description of the +errors ( Tea Leaves ;^) ). - if the pre-query validation fails then a 418 - I'm a Teapot error response is returned with the - body containing the specific description of the errors ( Tea Leaves ;^) ). +=head3 Dealing with inconsistencies -NB: If you pass a 'path' parameter this takes precendence over the API Discovery Spec. Any parameters defined in the path of the format {VARNAME} will be - filled in with values within the options=>{ VARNAME => 'value '} parameter structure. This is the simplest way of addressing issues where the API - discovery spec is inaccurate. ( See dev_sheets_example.pl as at 14/11/18 for illustration ) +NB: If you pass a 'path' parameter this takes precendence over the +API Discovery Spec. Any parameters defined in the path of the +format {VARNAME} will be filled in with values within the +options=>{ VARNAME => 'value '} parameter structure. This is the +simplest way of addressing issues where the API discovery spec is +inaccurate. ( See dev_sheets_example.pl as at 14/11/18 for +illustration ). This particular issue has been since solved, but +you never know where else there are problems with the discovery spec. -To allow the user to fix discrepencies in the Discovery Specification the cb_method_discovery_modify callback can be used which must accept the -method specification as a parameter and must return a (potentially modified) method spec. +Sometimes, Google is slightly inconsistent about how to name the parameters. For example, +error messages sent back to the user tend to have the param names in snake_case, whereas +the discovery document always has them in camelCase. To address this issue, and in +the DWIM spirit of perl, parameters may be passed in camelCase or snake_case. That +means that + + $gapi_agent->api_query( + api_endpoint_id => 'gmail.users.messages.list', + options => { userId => 'foobar' }); + +and + + $gapi_agent->api_query( + api_endpoint_id => 'gmail.users.messages.list', + options => { user_id => 'foobar' }); + +will produce the same result. + +Sometimes a param expects a dynamic part and a static part. The endpoint +jobs.projects.jobs.list, for example, has a param called 'parent' which has a +format '^projects/[^/]+$'. In cases like this, you can just skip out the constant +part, making + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'sner' } ); + +and + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'projects/sner' } ); + +the same. How's that for DWIM? + +In addition, you can use different names to refer to multi-part +parameters. For example, the endpoint jobs.projects.jobs.delete officially +expects one parameter, 'name'. The description for the param tells you +that you it expects it to contain 'projectsId' and 'jobsId'. For cases like this, + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {name => 'projects/sner/jobs/bler'} ); + +and + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projectsId => 'sner', jobsId => 'bler'} ); + +will produce the same result. Note that for now, in this case you can't pass +the official param name without the constant parts. That may change in the +future. + +To further fix discrepencies in the Discovery Specification, the +cb_method_discovery_modify callback can be used which must accept +the method specification as a parameter and must return a +(potentially modified) method spec. eg. - my $r = $gapi_client->api_query( api_endpoint_id => "sheets:v4.spreadsheets.values.update", - options => { - spreadsheetId => '1111111111111111111', - valueInputOption => 'RAW', - range => 'Sheet1!A1:A2', - 'values' => [[99],[98]] - }, - cb_method_discovery_modify => sub { - my $meth_spec = shift; - $meth_spec->{parameters}{valueInputOption}{location} = 'path'; - $meth_spec->{path} = "v4/spreadsheets/{spreadsheetId}/values/{range}?valueInputOption={valueInputOption}"; - return $meth_spec; - } - ); + my $r = $gapi_client->api_query( + api_endpoint_id => "sheets:v4.spreadsheets.values.update", + options => { + spreadsheetId => '1111111111111111111', + valueInputOption => 'RAW', + range => 'Sheet1!A1:A2', + 'values' => [[99],[98]] + }, + cb_method_discovery_modify => sub { + my $meth_spec = shift; + $meth_spec->{parameters}{valueInputOption}{location} = 'path'; + $meth_spec->{path} = join '', + "v4/spreadsheets/{spreadsheetId}/values/{range}', + "?valueInputOption={valueInputOption}"; + return $meth_spec; + } + ); + +Again, this specific issue has been fixed. Returns L object @@ -319,7 +386,7 @@ sub _process_params_for_api_endpoint_and_return_errors $params->{ path } = "$api_discovery_struct->{baseUrl}/$params->{path}" unless $params->{ path } =~ /^$api_discovery_struct->{baseUrl}/ixsmg; ## prepend baseUrl if required ## if errors - add detail available in the discovery struct for the method and service to aid debugging - push (@teapot_errors, qq{ $api_discovery_struct->{title} $api_discovery_struct->{rest} API into $api_discovery_struct->{ownerName} $api_discovery_struct->{canonicalName} $api_discovery_struct->{version} with id $method_discovery_struct->{id} as described by discovery document version $method_discovery_struct->{discoveryVersion} revision $method_discovery_struct->{revision} with documentation at $api_discovery_struct->{documentationLink} \nDescription $api_discovery_struct->{description}\n} ) if ( @teapot_errors ); + push @teapot_errors, qq{ $api_discovery_struct->{title} $api_discovery_struct->{rest} API into $api_discovery_struct->{ownerName} $api_discovery_struct->{canonicalName} $api_discovery_struct->{version} with id $method_discovery_struct->{id} as described by discovery document version $api_discovery_struct->{discoveryVersion} revision $api_discovery_struct->{revision} with documentation at $api_discovery_struct->{documentationLink} \nDescription: $method_discovery_struct->{description}\n} if @teapot_errors; return @teapot_errors; } @@ -366,13 +433,15 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors for my $param_name ( keys %{ $params->{options} } ) { #first check if it needs to be interpolated into the path - if ($path_params{$param_name}) { + if ($path_params{$param_name} || $path_params{camel $param_name}) { #pull out the value from the hash, and remove the key my $param_value = delete $params->{options}{$param_name}; - #camelize the param name if not passed in customly, allowing - #the user to pass in camelCase or snake_case param names - $param_name = camel $param_name if $path_params{$param_name} ne 'custom'; + #Camelize the param name if not passed in customly, allowing + #the user to pass in camelCase or snake_case param names. + #This implictly allows for a non camelCased param to be left + #alone in a custom param. + $param_name = camel $param_name if $path_params{camel $param_name}; #first deal with any param that doesn't have a plus, b/c #those just get interpolated @@ -392,9 +461,11 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors #remove the regexy parts of the pattern to interpolate it #into the path, assuming the user has provided just the #dynamic portion of the param. - $pattern =~ s/\^ \$//gx; + $pattern =~ s/^\^ | \$$//gx; my $placeholder = qr/ \[ \^ \/ \] \+ /x; $params->{path} =~ s/\{\+$param_name\}/$pattern/x; - $params->{path} =~ s/\[ \^ \/ \]/$param_value/x; + $params->{path} =~ s/$placeholder/$param_value/x; + push @teapot_errors, "Not enough parameters given for {+$param_name}." + if $params->{path} =~ /$placeholder/; } } #skip to the next run, so I don't need an else clause later @@ -422,6 +493,8 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors my $param_value = $discovery_struct->{parameters}{$param_name}{default}; $params->{path} =~ s/\{$param_name\}/$param_value/ if $param_value; } + push @teapot_errors, "Missing a parameter for {$_}." + for $params->{path} =~ /$param_regex/g; #print pp $params; #exit; diff --git a/t/00-load.t b/t/00-load.t index 97be0ba..9877489 100755 --- a/t/00-load.t +++ b/t/00-load.t @@ -18,7 +18,7 @@ use Cwd; my $dir = getcwd; # plan tests => 1; ## or at end done_test(); -my $DEBUG = 0; +my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; #BEGIN { diff --git a/t/01-client-discovery.t b/t/01-client-discovery.t index 4241aaf..d2b32ef 100755 --- a/t/01-client-discovery.t +++ b/t/01-client-discovery.t @@ -18,7 +18,7 @@ use Cwd; use CHI; my $dir = getcwd; -my $DEBUG = 0; ## to see noise of class debugging +my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging use_ok( 'WebService::GoogleAPI::Client' ); # || print "Bail out!\n"; diff --git a/t/03-pre-warmed-cache-discovery.t b/t/03-pre-warmed-cache-discovery.t index 1d09315..36b52b2 100644 --- a/t/03-pre-warmed-cache-discovery.t +++ b/t/03-pre-warmed-cache-discovery.t @@ -24,7 +24,7 @@ use JSON; #use CHI::Driver::TEST_MOCKED; my $dir = getcwd; -my $DEBUG = 1; ## to see noise of class debugging +my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging my $warm_hash = {}; #$warm_hash->{'https://www.googleapis.com/discovery/v1/apis'} = 'foo'; #print Dumper $hash; @@ -48,9 +48,14 @@ ok( my $disc = WebService::GoogleAPI::Client::Discovery->new( chi=>$chi, debug=> return $res; }); - ok( my $all = $disc->discover_all(), 'discover_all()' ); + #stop that annoying cluck from trying to get the auth header + #for our fake request + my $cluck = Sub::Override->new('WebService::GoogleAPI::Client::UserAgent::cluck', + sub { } ); - ## INJECT HTTP RESPONSE FOR GAMIL V1 API SPECIFICATION + ok( my $all = $disc->discover_all(), 'discover_all()' ); + + ## INJECT HTTP RESPONSE FOR GMAIL V1 API SPECIFICATION my $override2 = Sub::Override->new('Mojo::Transaction::res', sub { my $res2 = Mojo::Message::Response->new ; @@ -60,8 +65,9 @@ ok( my $disc = WebService::GoogleAPI::Client::Discovery->new( chi=>$chi, debug=> }); - ok( my $gmail_api_spec = $disc->methods_available_for_google_api_id('gmail', 'v1'), 'Get available methods for Gmail V1'); + ok( my $gmail_api_spec = $disc->methods_available_for_google_api_id('gmail', 'v1'), 'Get available methods for Gmail V1'); + $cluck->restore; my $override3 = Sub::Override->new('Mojo::Transaction::res', @@ -7127,4 +7133,4 @@ sub pre_get_gmail_spec_json } } __END -} \ No newline at end of file +} diff --git a/t/04-client-mocked-agent.t b/t/04-client-mocked-agent.t index ea04d25..495270e 100644 --- a/t/04-client-mocked-agent.t +++ b/t/04-client-mocked-agent.t @@ -24,7 +24,7 @@ use Sub::Override; my $dir = getcwd; -my $DEBUG = 1; ## to see noise of class debugging +my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging my $warm_hash = {}; #$warm_hash->{'https://www.googleapis.com/discovery/v1/apis'} = 'foo'; #print Dumper $hash; @@ -7107,4 +7107,4 @@ sub pre_get_gmail_spec_json } } __END -} \ No newline at end of file +} diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t new file mode 100644 index 0000000..497ab47 --- /dev/null +++ b/t/05-spec-interpolation.t @@ -0,0 +1,90 @@ +use strict; +use warnings; +use Test::More; +use Cwd; + +use WebService::GoogleAPI::Client; + +my $dir = getcwd; +my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging +my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil +$default_file = "$dir/../gapi.json" unless -e $default_file; ## if file doesn't exist try one level up ( allows to run directly from t/ if gapi.json in parent dir ) + +plan skip_all => 'No service configuration - set $ENV{GOOGLE_TOKENSFILE} or create gapi.json in dzil source root directory' unless -e $default_file; + +ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); + +my %options; + +subtest 'Testing {+param} type interpolation options' => sub { + plan skip_all => <has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); + + my $interpolated = 'https://jobs.googleapis.com/v3/projects/sner/jobs'; + + %options = ( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {name => 'projects/sner/jobs/bler'} ); + $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + is $options{path}, "$interpolated/bler", + 'Interpolates a {+param} that matches the spec pattern'; + + %options = + ( api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'sner' } ); + $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + is $options{path}, $interpolated, + 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; + + %options = + ( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projectsId => 'sner', jobsId => 'bler'} ); + $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + + is $options{path}, "$interpolated/bler", + 'Interpolates params that match the flatName spec (camelCase)'; + + %options = + ( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projects_id => 'sner', jobs_id => 'bler'} ); + $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + + is $options{path}, "$interpolated/bler", + 'Interpolates params that match the names in the api description (snake_case)'; + + +}; + +my @errors; +subtest 'Checking for proper failure with {+params} in unsupported ways' => sub { + plan skip_all => <has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); + + + %options = + ( api_endpoint_id => 'jobs.projects.jobs.delete', + options => { name => 'sner' } ); + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + is $errors[0], 'Not enough parameters given for {+name}.', + "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; + + %options = + ( api_endpoint_id => 'jobs.projects.jobs.delete', + options => { jobsId => 'sner' }); + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + is $errors[0], 'Missing a parameter for {projectsId}.', + "Fails if you don't supply enough values to fill the flatPath"; + +}; + + + + + + +done_testing; From 480f98c0a29a699a5c46e9f517ed6f2d6cdd0599 Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Thu, 13 Dec 2018 22:54:38 +0200 Subject: [PATCH 03/68] Fixed perlcritic complaint --- lib/WebService/GoogleAPI/Client.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index de67001..b376df5 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -392,6 +392,9 @@ sub _process_params_for_api_endpoint_and_return_errors } ################################################## +#small subs to convert between these_types to theseTypes of params +sub camel { shift if @_ > 1; $_[0] =~ s/ _(\w) /\u$1/grx }; +sub snake { shift if @_ > 1; $_[0] =~ s/([[:upper:]])/_\l$1/grx }; ################################################## sub _interpolate_path_parameters_append_query_params_and_return_errors @@ -418,9 +421,6 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors } - #small subs to convert between these_types to theseTypes of params - sub camel { $_[0] =~ s/ _(\w) /\u$1/grx }; - sub snake { $_[0] =~ s/([[:upper:]])/_\l$1/grx }; #switch the path we're dealing with to the flat path if any of #the parameters match the flat path From 1da26c46f5904a14c8f06485510be7c682515e02 Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sat, 15 Dec 2018 21:36:33 +0200 Subject: [PATCH 04/68] squashed bug introduced and added test --- lib/WebService/GoogleAPI/Client.pm | 30 +++++++----- t/05-spec-interpolation.t | 79 ++++++++++++++++++++---------- t/gapi.json | 2 +- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index b376df5..172c674 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -60,12 +60,12 @@ Package includes I CLI Script to collect initial end-user authorisation my $my_email_address = 'peter@shotgundriver.com' - my $raw_email_payload = encode_base64( Email::Simple->create( header => [To => $my_email_address, - From => $my_email_address, - Subject =>"Test email from '$my_email_address' ",], - body => "This is the body of email to '$my_email_address'", - )->as_string - ); + my $raw_email_payload = encode_base64( Email::Simple->create( + header => [To => $my_email_address, From => $my_email_address, + Subject =>"Test email from '$my_email_address' ",], + body => "This is the body of email to '$my_email_address'", + )->as_string + ); $gapi_client->api_query( api_endpoint_id => 'gmail.users.messages.send', @@ -185,7 +185,9 @@ If the pre-query validation fails then a 418 - I'm a Teapot error response is returned with the body containing the specific description of the errors ( Tea Leaves ;^) ). -=head3 Dealing with inconsistencies +=head3 B + + NB: If you pass a 'path' parameter this takes precendence over the API Discovery Spec. Any parameters defined in the path of the @@ -264,9 +266,7 @@ eg. cb_method_discovery_modify => sub { my $meth_spec = shift; $meth_spec->{parameters}{valueInputOption}{location} = 'path'; - $meth_spec->{path} = join '', - "v4/spreadsheets/{spreadsheetId}/values/{range}', - "?valueInputOption={valueInputOption}"; + $meth_spec->{path} .= "?valueInputOption={valueInputOption}"; return $meth_spec; } ); @@ -366,6 +366,10 @@ sub _process_params_for_api_endpoint_and_return_errors my $method_discovery_struct = $self->extract_method_discovery_detail_from_api_spec( $params->{ api_endpoint_id } ); ## if can get discovery data for google api endpoint then continue to perform detailed checks + #save away original path so we can know if it's fiddled with + #later + $method_discovery_struct->{origPath} = $method_discovery_struct->{path}; + ## allow optional user callback pre-processing of method_discovery_struct $method_discovery_struct = &{$params->{cb_method_discovery_modify}}($method_discovery_struct) if ( defined $params->{cb_method_discovery_modify} && ref( $params->{cb_method_discovery_modify} ) eq 'CODE' ); @@ -406,7 +410,7 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors #create a hash of whatever the expected params may be my %path_params; my $param_regex = qr/\{ \+? ([^\}]+) \}/x; - if ($params->{path} ne $discovery_struct->{path}) { + if ($params->{path} ne $discovery_struct->{origPath}) { #check if the path was given as a custom path. If it is, just #interpolate things directly, and assume the user is responsible %path_params = map { $_ => 'custom' } ($params->{path} =~ /$param_regex/xg); @@ -458,6 +462,7 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors #N.B. perhaps support passing an arrayref or csv for those #params such as jobs.projects.jobs.delete which have two #dynamic parts. But for now, unnecessary + #remove the regexy parts of the pattern to interpolate it #into the path, assuming the user has provided just the #dynamic portion of the param. @@ -484,7 +489,8 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors if ( @get_query_params ) { #interpolate and escape the get query params built up in our #former for loop - $params->{path} .= '?' . Mojo::Parameters->new(@get_query_params); + $params->{path} .= ($params->{path} =~ /\?/) ? '&' : '?'; + $params->{path} .= Mojo::Parameters->new(@get_query_params); } #interpolate default value for path params if not given. Needed diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 497ab47..1ec42c2 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -16,6 +16,31 @@ ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => my %options; +#TODO- make a test for when the user already passes in a query +#param + +my $options = { + api_endpoint_id => "sheets:v4.spreadsheets.values.update", + options => { + spreadsheetId => 'sner', + includeValuesInResponse => 'true', + valueInputOption => 'RAW', + range => 'Sheet1!A1:A2', + 'values' => [[99],[98]] + }, + cb_method_discovery_modify => sub { + my $meth_spec = shift; + $meth_spec->{parameters}{valueInputOption}{location} = 'path'; + $meth_spec->{path} .= "?valueInputOption={valueInputOption}"; + return $meth_spec; + } +}; + +$gapi->_process_params_for_api_endpoint_and_return_errors($options); + +is $options->{path}, 'https://sheets.googleapis.com/v4/spreadsheets/sner/values/Sheet1!A1:A2?valueInputOption=RAW&includeValuesInResponse=true', +'interpolation works with user fiddled path, too'; + subtest 'Testing {+param} type interpolation options' => sub { plan skip_all => < 'jobs.projects.jobs.delete', - options => {name => 'projects/sner/jobs/bler'} ); - $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); - is $options{path}, "$interpolated/bler", + $options = { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {name => 'projects/sner/jobs/bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $options->{path}, "$interpolated/bler", 'Interpolates a {+param} that matches the spec pattern'; - %options = - ( api_endpoint_id => 'jobs.projects.jobs.list', - options => { parent => 'sner' } ); - $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); - is $options{path}, $interpolated, + $options = + { api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'sner' } }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $options->{path}, $interpolated, 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; - %options = - ( api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projectsId => 'sner', jobsId => 'bler'} ); - $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projectsId => 'sner', jobsId => 'bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options{path}, "$interpolated/bler", + is $options->{path}, "$interpolated/bler", 'Interpolates params that match the flatName spec (camelCase)'; - %options = - ( api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projects_id => 'sner', jobs_id => 'bler'} ); - $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projects_id => 'sner', jobs_id => 'bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options{path}, "$interpolated/bler", + is $options->{path}, "$interpolated/bler", 'Interpolates params that match the names in the api description (snake_case)'; @@ -66,17 +91,17 @@ MSG unless $gapi->has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); - %options = - ( api_endpoint_id => 'jobs.projects.jobs.delete', - options => { name => 'sner' } ); - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => { name => 'sner' } }; + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); is $errors[0], 'Not enough parameters given for {+name}.', "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; - %options = - ( api_endpoint_id => 'jobs.projects.jobs.delete', - options => { jobsId => 'sner' }); - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( \%options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => { jobsId => 'sner' } }; + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); is $errors[0], 'Missing a parameter for {projectsId}.', "Fails if you don't supply enough values to fill the flatPath"; diff --git a/t/gapi.json b/t/gapi.json index eb51aa4..72647d2 100644 --- a/t/gapi.json +++ b/t/gapi.json @@ -2,7 +2,7 @@ "gapi" : { "client_id" : "mocked.apps.googleusercontent.com", "client_secret" : "6iQ5pFYLgsssssssssssssssssssss", - "scopes" : "email profile https://www.googleapis.com/auth/plus.profile.emails.read https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly https://mail.google.com", + "scopes" : "email profile https://www.googleapis.com/auth/plus.profile.emails.read https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/contacts.readonly https://mail.google.com https://www.googleapis.com/auth/jobs https://www.googleapis.com/auth/spreadsheets", "tokens" : { "peter@shotgundriver.com" : { "access_token" : "ya29.GltBBsl56Dd-drJHt3MfpuRkp7A3cwVJy--areTiZvSlHdsfdsjkfdsjkfdskfjaii--------mocked--------------------", From 2bb1f8e74a85b9a33cb6317dd42d8e73ee19c2e3 Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sun, 16 Dec 2018 00:19:13 +0200 Subject: [PATCH 05/68] Added global parameters like 'fields' support --- lib/WebService/GoogleAPI/Client.pm | 3 +++ lib/WebService/GoogleAPI/Client/Discovery.pm | 21 +++++++++++--------- t/05-spec-interpolation.t | 6 ++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 172c674..963cb8b 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -406,6 +406,9 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors my ( $self, $params, $discovery_struct ) = @_; my @teapot_errors = (); + #TODO- we need to add globally available parameters, i guess + #from the discovery object + my @get_query_params = (); #create a hash of whatever the expected params may be diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index 553dffb..ffea656 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -533,7 +533,7 @@ sub extract_method_discovery_detail_from_api_spec - ## we use the schames to substitute into '$ref' keyed placeholders + ## we use the schemas to substitute into '$ref' keyed placeholders my $schemas = {}; foreach my $schema_key (sort keys %{$api_spec->{schemas}}) { @@ -551,18 +551,21 @@ sub extract_method_discovery_detail_from_api_spec ## now extract all the methods (recursive ) my $all_api_methods = $self->_extract_resource_methods_from_api_spec( "$api_id:$api_version", $api_spec ); #print dump $all_api_methods;exit; - if ( defined $all_api_methods->{ $tree } ) - { + unless ( defined $all_api_methods->{ $tree } ) + { + $all_api_methods = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec ); + } + if ($all_api_methods->{$tree}){ + #add in the global parameters to the endpoint, + #stored in the top level of the api_spec + $all_api_methods->{$tree}{parameters} = { + %{ $all_api_methods->{$tree}{parameters} }, %{ $api_spec->{parameters} } + }; return $all_api_methods->{ $tree }; } - else - { - #return $all_api_methods->{ "$api_id:$api_version" } if ( defined $all_api_methods->{ "$api_id:$api_version" } ); - return $all_api_methods->{ $tree } if ( $all_api_methods = $self->_extract_resource_methods_from_api_spec( "$api_id", $api_spec ) ); carp( "Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version" ) if $self->debug; return {}; - } } ######################################################## @@ -715,4 +718,4 @@ sub list_of_available_google_api_ids 1; ## TODO - CODE REVIEW -## get_expires_at .. deos this do what is expected ? - what if has expired and so get fails - will this still return a value? \ No newline at end of file +## get_expires_at .. does this do what is expected ? - what if has expired and so get fails - will this still return a value? diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 1ec42c2..dfbcdea 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -14,11 +14,9 @@ plan skip_all => 'No service configuration - set $ENV{GOOGLE_TOKENSFILE} or crea ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); -my %options; - -#TODO- make a test for when the user already passes in a query -#param +#TODO- make a test for a default param that should go into the +#query, like 'fields'. my $options = { api_endpoint_id => "sheets:v4.spreadsheets.values.update", options => { From 602eeab861984d5af5a06f55230ab72d5cd994ef Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sun, 16 Dec 2018 21:35:28 +0200 Subject: [PATCH 06/68] Added test for global param support --- lib/WebService/GoogleAPI/Client.pm | 3 --- t/05-spec-interpolation.t | 19 +++++++++++++++++-- t/gapi.json | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 963cb8b..172c674 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -406,9 +406,6 @@ sub _interpolate_path_parameters_append_query_params_and_return_errors my ( $self, $params, $discovery_struct ) = @_; my @teapot_errors = (); - #TODO- we need to add globally available parameters, i guess - #from the discovery object - my @get_query_params = (); #create a hash of whatever the expected params may be diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index dfbcdea..705a4b1 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -10,14 +10,29 @@ my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debuggi my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil $default_file = "$dir/../gapi.json" unless -e $default_file; ## if file doesn't exist try one level up ( allows to run directly from t/ if gapi.json in parent dir ) +#if running from root of the repo, grab the one from the t/ directory +$default_file = "$dir/t/gapi.json" unless -e $default_file; + plan skip_all => 'No service configuration - set $ENV{GOOGLE_TOKENSFILE} or create gapi.json in dzil source root directory' unless -e $default_file; ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); +my $options = { + api_endpoint_id => 'drive.files.list', + options => { + fields => 'files(id,name,parents)' + } +}; + +$gapi->_process_params_for_api_endpoint_and_return_errors($options); + +is $options->{path}, +'https://www.googleapis.com/drive/v3/files?fields=files%28id%2Cname%2Cparents%29', +'Can interpolate globally available query parameters'; #TODO- make a test for a default param that should go into the #query, like 'fields'. -my $options = { +$options = { api_endpoint_id => "sheets:v4.spreadsheets.values.update", options => { spreadsheetId => 'sner', @@ -40,7 +55,7 @@ is $options->{path}, 'https://sheets.googleapis.com/v4/spreadsheets/sner/values/ 'interpolation works with user fiddled path, too'; subtest 'Testing {+param} type interpolation options' => sub { - plan skip_all => < < Date: Fri, 22 Nov 2019 00:15:29 +0200 Subject: [PATCH 07/68] cleaned up tests more --- t/05-spec-interpolation.t | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 705a4b1..3a98ff2 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -24,8 +24,11 @@ my $options = { } }; -$gapi->_process_params_for_api_endpoint_and_return_errors($options); +sub build_req { + $gapi->_process_params_for_api_endpoint_and_return_errors(shift); +} +build_req($options); is $options->{path}, 'https://www.googleapis.com/drive/v3/files?fields=files%28id%2Cname%2Cparents%29', 'Can interpolate globally available query parameters'; @@ -49,11 +52,23 @@ $options = { } }; -$gapi->_process_params_for_api_endpoint_and_return_errors($options); +build_req($options); is $options->{path}, 'https://sheets.googleapis.com/v4/spreadsheets/sner/values/Sheet1!A1:A2?valueInputOption=RAW&includeValuesInResponse=true', 'interpolation works with user fiddled path, too'; +$options = { + api_endpoint_id => "sheets:v4.spreadsheets.values.batchGet", + options => { + spreadsheetId => 'sner', + ranges => ['Sheet1!A1:A2', 'Sheet1!A3:B5'], + }, +}; +build_req($options); +is $options->{path}, +'https://sheets.googleapis.com/v4/spreadsheets/sner/values:batchGet?ranges=Sheet1%21A1%3AA2&ranges=Sheet1%21A3%3AB5', +'interpolates arrayref correctly' ; + subtest 'Testing {+param} type interpolation options' => sub { plan skip_all => < Date: Fri, 22 Nov 2019 00:29:46 +0200 Subject: [PATCH 08/68] Made service accounts work --- examples/service_account_example.pl | 52 ++----- lib/WebService/GoogleAPI/JWT.pm | 204 ++++++++++++++++++++++++++++ t/JWT/01_basic.t | 114 ++++++++++++++++ 3 files changed, 330 insertions(+), 40 deletions(-) create mode 100644 lib/WebService/GoogleAPI/JWT.pm create mode 100644 t/JWT/01_basic.t diff --git a/examples/service_account_example.pl b/examples/service_account_example.pl index 0306006..b58c232 100644 --- a/examples/service_account_example.pl +++ b/examples/service_account_example.pl @@ -1,61 +1,33 @@ #!/usr/bin/env perl use strictures; -## use Crypt::JWT qw(encode_jwt); -- couldn't get this to work -use Data::Dump qw/pp/; -use feature 'say'; -use LWP::UserAgent; -use JSON; -use Mojo::File; -use Mojo::JWT; ## there is also Mojo::JWT::Google but the cpan version is broken - pull request submitted but is easy enough to use parent Mojo::JWT +use Data::Printer; +use Mojo::UserAgent; +use WebService::GoogleAPI::JWT; my $config = { path => $ARGV[0] // '/Users/peter/Downloads/computerproscomau-b9f59b8ee34a.json', scopes => $ARGV[1] // 'https://www.googleapis.com/auth/plus.business.manage https://www.googleapis.com/auth/compute' }; -my $jwt = mojo_jwt_from_json_or_croak( $config->{path}, $config->{scopes} ); -my $ua = LWP::UserAgent->new(); #WWW::Mechanize->new( autocheck => 1 ); +my $jwt = WebService::GoogleAPI::JWT->new( from_json => $config->{path}, scopes => [ split / /, $config->{scopes} ] ); -my $response = $ua->post('https://www.googleapis.com/oauth2/v4/token', { 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', - 'assertion' => $jwt } +my $ua = Mojo::UserAgent->new(); + +my $response = $ua->post('https://www.googleapis.com/oauth2/v4/token', form => { 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion' => $jwt->encode } ); -if ($response->is_success) +if ($response->res) { - print $response->decoded_content; + p $response->res->json; } else { - print STDERR $response->status_line, "\n"; -} -exit; - -####################################################################### -sub mojo_jwt_from_json_or_croak -{ - my ( $path, $scope ) = @_; - croak("No path provided") if not defined $path; - croak("$path no available") if not -f $path; - my $json = decode_json( Mojo::File->new($path)->slurp ); - croak("No Private key in $path") if not defined $json->{private_key}; - croak("Not a service account") if $json->{type} ne 'service_account'; - my $jwt = Mojo::JWT->new(); - $jwt->algorithm('RS256'); - $jwt->secret($json->{private_key}); - - $jwt->claims( { - iss => $json->{client_email}, - scope => $scope, - aud => 'https://www.googleapis.com/oauth2/v4/token', - iat => time(), - exp => time()+3600 - } ); - $jwt->set_iat( 1 ); - return $jwt->encode; + warn $response->res->status, "\n"; } -####################################################################### +exit; =pod diff --git a/lib/WebService/GoogleAPI/JWT.pm b/lib/WebService/GoogleAPI/JWT.pm new file mode 100644 index 0000000..e408912 --- /dev/null +++ b/lib/WebService/GoogleAPI/JWT.pm @@ -0,0 +1,204 @@ +package WebService::GoogleAPI::JWT; +use utf8; +use Mojo::Base qw(Mojo::JWT); +use vars qw($VERSION); +use Mojo::Collection 'c'; +use Mojo::File (); +use Mojo::JSON qw(decode_json); +use Carp; + +has client_email => undef; +has expires_in => 3600; +has issue_at => undef; +has scopes => sub { c() }; +has target => q(https://www.googleapis.com/oauth2/v4/token); +has user_as => undef; +has audience => undef; + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(%options); + return $self if not defined $self->{from_json}; + + my $result = $self->from_json($self->{from_json}); + + if ( $result == 0 ) { + croak 'Your JSON file import failed.'; + return undef; + } + return $self; +} + + +sub claims { + my ($self, $value) = @_; + if (defined $value) { + $self->{claims} = $value; + return $self; + } + my $claims = $self->_construct_claims; + unless (exists $claims->{exp}) { + $claims->{exp} = $self->now + $self->expires_in ; + } + return $claims; +} + +sub _construct_claims { + my $self = shift; + my $result = {}; + $result->{iss} = $self->client_email; + $result->{aud} = $self->target; + $result->{sub} = $self->user_as if defined $self->user_as; + my @scopes = @{ $self->scopes }; + + croak "Can't use both scopes and audience in the same token" if @scopes && $self->audience; + $result->{scope} = c(@scopes)->join(' ')->to_string if @scopes; + $result->{target_audience} = $self->audience if defined $self->audience; + + if ( not defined $self->issue_at ) { + $self->set_iat(1); + } + else { + $self->set_iat(0); + $result->{iat} = $self->issue_at; + $result->{exp} = $self->issue_at + $self->expires_in; + } + return $result; +} + +sub from_json { + my ($self, $value) = @_; + return 0 if not defined $value; + return 0 if not -f $value; + my $json = decode_json( Mojo::File->new($value)->slurp ); + return 0 if not defined $json->{private_key}; + return 0 if $json->{type} ne 'service_account'; + $self->algorithm('RS256'); + $self->secret($json->{private_key}); + $self->client_email($json->{client_email}); + return 1 +} + +1; + + +=head1 NAME + +Mojo::JWT::Google - Service Account tokens + +=head1 VERSION + +0.05 + +=head1 SYNOPSIS + +my $gjwt = Mojo::JWT::Google->new(secret => 's3cr3t', + scopes => [ '/my/scope/a', '/my/scope/b' ], + client_email => 'riche@cpan.org')->encode; + +=head1 DESCRIPTION + +Like L, you can instantiate this class by using the same syntax, +except that this class constructs the claims for you. + + my $jwt = Mojo::JWT::Google->new(secret => 's3cr3t')->encode; + +And add any attribute defined in this class. The JWT is fairly useless unless +you define your scopes. + + my $gjwt = Mojo::JWT::Google->new(secret => 's3cr3t', + scopes => [ '/my/scope/a', '/my/scope/b' ], + client_email => 'riche@cpan.org')->encode; + +You can also get your information automatically from the .json you received +from Google. Your secret key is in that file, so it's best to keep it safe +somewhere. This will ease some busy work in configuring the object -- with +virtually the only things to do is determine the scopes and the user_as if you +need to impersonate. + + my $gjwt = Mojo::JWT::Google + ->new( from_json => '/my/secret.json', + scopes => [ '/my/scope/a', '/my/scope/b' ])->encode; + +=cut + +=head1 ATTRIBUTES + +L inherits all attributes from L and defines the +following new ones. + +=head2 claims + +Overrides the parent class and constructs a hashref representing Google's +required attribution. + + +=head2 client_email + +Get or set the Client ID email address. + +=head2 expires_in + +Defines the threshold for when the token expires. Defaults to 3600. + +=head2 issue_at + +Defines the time of issuance in epoch seconds. If not defined, the claims issue +at date defaults to the time when it is being encoded. + +=head2 scopes + +Get or set the Google scopes. If impersonating, these scopes must be set up by +your Google Business Administrator. + +=head2 target + +Get or set the target. At the time of writing, there is only one valid target: +https://www.googleapis.com/oauth2/v3/token. This is the default value; if you +have no need to customize this, then just fetch the default. + + +=head2 user_as + +Set the Google user to impersonate. Your Google Business Administrator must +have already set up your Client ID as a trusted app in order to use this +successfully. + +=cut + +=head1 METHODS + +Inherits all methods from L and defines the following new ones. + +=head2 from_json + +Loads the JSON file from Google with the client ID information in it and sets +the respective attributes. + +Returns 0 on failure: file not found or value not defined + + $gjwt->from_json('/my/google/app/project/sa/json/file'); + + +=head1 SEE ALSO + +L + +=head1 SOURCE REPOSITORY + +L + +=head1 AUTHOR + +Richard Elberger, + +=head1 CONTRIBUTORS + +Scott Wiersdorf, + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2015 by Richard Elberger + +This library is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. diff --git a/t/JWT/01_basic.t b/t/JWT/01_basic.t new file mode 100644 index 0000000..848f739 --- /dev/null +++ b/t/JWT/01_basic.t @@ -0,0 +1,114 @@ +use strict; +use warnings; +use Test2::V0; +use WebService::GoogleAPI::JWT; + +use Mojo::Collection 'c'; +use File::Basename 'dirname'; +use Cwd 'abs_path'; +#my $grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + +my $client_email = 'mysa@developer.gserviceaccount.com'; +my $target = 'https://www.googleapis.com/oauth2/v4/token'; + + +isa_ok my $jwt = WebService::GoogleAPI::JWT->new, ['WebService::GoogleAPI::JWT'], 'empty object creation'; + +# accessors +is $jwt->client_email, undef, 'not init'; +is $jwt->client_email($client_email), $jwt, 'service_account set'; +is $jwt->client_email, $client_email, 'service_account get'; + +is $jwt->scopes, [], 'no scopes'; +is push( @{ $jwt->scopes }, '/a/scope'), 1, 'scopes add one scope'; +is push( @{ $jwt->scopes }, '/b/scope'), 2, 'scopes add another'; +is $jwt->scopes, ['/a/scope','/b/scope'], 'scopes get all'; + +is $jwt->target, 'https://www.googleapis.com/oauth2/v4/token', 'target get'; +is $jwt->target('https://a/new/target'), $jwt, 'target set'; +is $jwt->target, 'https://a/new/target', 'target get'; + +is $jwt->expires_in, 3600, 'expires in one hour by default'; +is $jwt->expires_in(300), $jwt, 'set to 5 minutes'; +is $jwt->expires_in, 300, 'right value'; + +is $jwt->issue_at, undef, 'unset by default'; +is $jwt->issue_at('1429812717'), $jwt, 'issue_at set'; +is $jwt->issue_at, '1429812717', 'issue_at get'; + +# basic claim work +for my $scopes ( c('/a/scope', '/b/scope'), [qw+ /a/scope /b/scope +] ){ + ok $jwt = WebService::GoogleAPI::JWT->new( client_email => $client_email, + issue_at => '1429812717', + scopes => $scopes), 'created a token for ' . ref($scopes) . ' as scopes'; + + is $jwt->claims, { iss => $client_email, + scope => '/a/scope /b/scope', + aud => 'https://www.googleapis.com/oauth2/v4/token', + exp => '1429816317', + iat => '1429812717', + }, 'claims based on accessor settings'; +} + +is $jwt->user_as, undef, 'impersonate user undef by default'; +is $jwt->user_as('riche@cpan.org'), $jwt, 'set user'; +is $jwt->user_as, 'riche@cpan.org', 'get user'; + +is $jwt->claims, { iss => $client_email, + scope => '/a/scope /b/scope', + aud => 'https://www.googleapis.com/oauth2/v4/token', + exp => '1429816317', + iat => '1429812717', + sub => 'riche@cpan.org', + }, 'claims based on accessor settings w impersonate'; + +# interop with Mojo::JWT + +$jwt = WebService::GoogleAPI::JWT->new( client_email => $client_email, + scopes => c('/a/scope', '/b/scope')); + + + +# we must set this +is $jwt->scopes(c->new('/a/scope')), $jwt, 'scopes add one scope'; + +my $claims = $jwt->claims; + +# predefine +$jwt = WebService::GoogleAPI::JWT->new( scopes => c('/scope/a/', '/scope/b/')); + +# predefine w json file +my $testdir = dirname ( abs_path( __FILE__ ) ); + + +like dies { + WebService::GoogleAPI::JWT->new( from_json => "$testdir/load0.json" ), +}, qr/Your JSON file import failed.*/, 'dies on file load failure'; + + +$jwt = WebService::GoogleAPI::JWT->new( from_json => "$testdir/load1.json" ); + +is $jwt->secret, <client_email, '9dvse@developer.gserviceaccount.com', + 'client email matches'; + +is $jwt->from_json, 0, 'requires parameter'; +is $jwt->from_json('/foo/bar/baz/me'), 0, 'file must exist'; +is $jwt->from_json( "$testdir/load3.json" ), 0, 'must have key defined'; +is $jwt->from_json( "$testdir/load4.json" ), 0, 'must be for service account'; + + +$jwt = WebService::GoogleAPI::JWT->new; +is $jwt->client_email('mysa@developer.gserviceaccount.com'), $jwt, 'sa set'; +$jwt->expires('9999999999'); + +my $jwte = $jwt->encode; +my $jwtd = $jwt->decode($jwte); + +done_testing; From 55b4d8512e23140d43824bd70f72e55a8d7cc46c Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Fri, 22 Nov 2019 11:38:25 +0200 Subject: [PATCH 09/68] Fixed up JWT for dzil --- README.txt | 136 +++++++++++++++++++++++--------- lib/WebService/GoogleAPI/JWT.pm | 30 ++++--- 2 files changed, 116 insertions(+), 50 deletions(-) diff --git a/README.txt b/README.txt index 81e90ec..1c2d1bd 100644 --- a/README.txt +++ b/README.txt @@ -43,12 +43,12 @@ EXAMPLES my $my_email_address = 'peter@shotgundriver.com' - my $raw_email_payload = encode_base64( Email::Simple->create( header => [To => $my_email_address, - From => $my_email_address, - Subject =>"Test email from '$my_email_address' ",], - body => "This is the body of email to '$my_email_address'", - )->as_string - ); + my $raw_email_payload = encode_base64( Email::Simple->create( + header => [To => $my_email_address, From => $my_email_address, + Subject =>"Test email from '$my_email_address' ",], + body => "This is the body of email to '$my_email_address'", + )->as_string + ); $gapi_client->api_query( api_endpoint_id => 'gmail.users.messages.send', @@ -91,7 +91,7 @@ METHODS Required params: method, route - Optional params: api_endpoint_id cb_method_discovery_modify + Optional params: api_endpoint_id cb_method_discovery_modify, options $self->access_token must be valid @@ -109,50 +109,112 @@ METHODS ## if provide the Google API Endpoint to inform pre-query validation say $gapi_agent->api_query( api_endpoint_id => 'gmail.users.messages.send', - options => { raw => encode_base64( - Email::Simple->create( header => [To => $user, From => $user, Subject =>"Test email from $user",], - body => "This is the body of email from $user to $user", )->as_string - ), - }, - )->to_string; ## + options => + { raw => encode_base64( Email::Simple->create( + header => [To => $user, From => $user, + Subject =>"Test email from $user",], + body => "This is the body of email from $user to $user", + )->as_string ), + }, + )->to_string; ## print $gapi_agent->api_query( - api_endpoint_id => 'gmail.users.messages.list', ## auto sets method to GET, path to 'https://www.googleapis.com/calendar' + api_endpoint_id => 'gmail.users.messages.list', + ## auto sets method to GET, and the path to + ## 'https://www.googleapis.com/gmail/v1/users/me/messages' )->to_string; - #print pp $r; - - - if the pre-query validation fails then a 418 - I'm a Teapot error response is returned with the - body containing the specific description of the errors ( Tea Leaves ;^) ). + + If the pre-query validation fails then a 418 - I'm a Teapot error + response is returned with the body containing the specific description + of the errors ( Tea Leaves ;^) ). + + Dealing with inconsistencies NB: If you pass a 'path' parameter this takes precendence over the API Discovery Spec. Any parameters defined in the path of the format {VARNAME} will be filled in with values within the options=>{ VARNAME => 'value '} parameter structure. This is the simplest way of addressing issues where the API discovery spec is inaccurate. ( See - dev_sheets_example.pl as at 14/11/18 for illustration ) + dev_sheets_example.pl as at 14/11/18 for illustration ). This + particular issue has been since solved, but you never know where else + there are problems with the discovery spec. + + Sometimes, Google is slightly inconsistent about how to name the + parameters. For example, error messages sent back to the user tend to + have the param names in snake_case, whereas the discovery document + always has them in camelCase. To address this issue, and in the DWIM + spirit of perl, parameters may be passed in camelCase or snake_case. + That means that + + $gapi_agent->api_query( + api_endpoint_id => 'gmail.users.messages.list', + options => { userId => 'foobar' }); + + and + + $gapi_agent->api_query( + api_endpoint_id => 'gmail.users.messages.list', + options => { user_id => 'foobar' }); + + will produce the same result. + + Sometimes a param expects a dynamic part and a static part. The + endpoint jobs.projects.jobs.list, for example, has a param called + 'parent' which has a format '^projects/[^/]+$'. In cases like this, you + can just skip out the constant part, making + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'sner' } ); + + and + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'projects/sner' } ); + + the same. How's that for DWIM? + + In addition, you can use different names to refer to multi-part + parameters. For example, the endpoint jobs.projects.jobs.delete + officially expects one parameter, 'name'. The description for the param + tells you that you it expects it to contain 'projectsId' and 'jobsId'. + For cases like this, + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {name => 'projects/sner/jobs/bler'} ); + + and + + $gapi_agent->api_query( api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projectsId => 'sner', jobsId => 'bler'} ); + + will produce the same result. Note that for now, in this case you can't + pass the official param name without the constant parts. That may + change in the future. - To allow the user to fix discrepencies in the Discovery Specification - the cb_method_discovery_modify callback can be used which must accept - the method specification as a parameter and must return a (potentially + To further fix discrepencies in the Discovery Specification, the + cb_method_discovery_modify callback can be used which must accept the + method specification as a parameter and must return a (potentially modified) method spec. eg. - my $r = $gapi_client->api_query( api_endpoint_id => "sheets:v4.spreadsheets.values.update", - options => { - spreadsheetId => '1111111111111111111', - valueInputOption => 'RAW', - range => 'Sheet1!A1:A2', - 'values' => [[99],[98]] - }, - cb_method_discovery_modify => sub { - my $meth_spec = shift; - $meth_spec->{parameters}{valueInputOption}{location} = 'path'; - $meth_spec->{path} = "v4/spreadsheets/{spreadsheetId}/values/{range}?valueInputOption={valueInputOption}"; - return $meth_spec; - } - ); + my $r = $gapi_client->api_query( + api_endpoint_id => "sheets:v4.spreadsheets.values.update", + options => { + spreadsheetId => '1111111111111111111', + valueInputOption => 'RAW', + range => 'Sheet1!A1:A2', + 'values' => [[99],[98]] + }, + cb_method_discovery_modify => sub { + my $meth_spec = shift; + $meth_spec->{parameters}{valueInputOption}{location} = 'path'; + $meth_spec->{path} .= "?valueInputOption={valueInputOption}"; + return $meth_spec; + } + ); + + Again, this specific issue has been fixed. Returns Mojo::Message::Response object diff --git a/lib/WebService/GoogleAPI/JWT.pm b/lib/WebService/GoogleAPI/JWT.pm index e408912..daf1a3b 100644 --- a/lib/WebService/GoogleAPI/JWT.pm +++ b/lib/WebService/GoogleAPI/JWT.pm @@ -1,4 +1,8 @@ +use strictures; package WebService::GoogleAPI::JWT; + +# ABSTRACT: JWT for authorizing service accounts + use utf8; use Mojo::Base qw(Mojo::JWT); use vars qw($VERSION); @@ -24,7 +28,6 @@ sub new { if ( $result == 0 ) { croak 'Your JSON file import failed.'; - return undef; } return $self; } @@ -76,7 +79,7 @@ sub from_json { $self->algorithm('RS256'); $self->secret($json->{private_key}); $self->client_email($json->{client_email}); - return 1 + return $self } 1; @@ -84,15 +87,16 @@ sub from_json { =head1 NAME -Mojo::JWT::Google - Service Account tokens +WebService::GoogleAPI::JWT - Service Account tokens =head1 VERSION 0.05 + =head1 SYNOPSIS -my $gjwt = Mojo::JWT::Google->new(secret => 's3cr3t', + my $gjwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t', scopes => [ '/my/scope/a', '/my/scope/b' ], client_email => 'riche@cpan.org')->encode; @@ -101,12 +105,12 @@ my $gjwt = Mojo::JWT::Google->new(secret => 's3cr3t', Like L, you can instantiate this class by using the same syntax, except that this class constructs the claims for you. - my $jwt = Mojo::JWT::Google->new(secret => 's3cr3t')->encode; + my $jwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t')->encode; And add any attribute defined in this class. The JWT is fairly useless unless you define your scopes. - my $gjwt = Mojo::JWT::Google->new(secret => 's3cr3t', + my $gjwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t', scopes => [ '/my/scope/a', '/my/scope/b' ], client_email => 'riche@cpan.org')->encode; @@ -116,7 +120,7 @@ somewhere. This will ease some busy work in configuring the object -- with virtually the only things to do is determine the scopes and the user_as if you need to impersonate. - my $gjwt = Mojo::JWT::Google + my $gjwt = WebService::GoogleAPI::JWT ->new( from_json => '/my/secret.json', scopes => [ '/my/scope/a', '/my/scope/b' ])->encode; @@ -124,7 +128,7 @@ need to impersonate. =head1 ATTRIBUTES -L inherits all attributes from L and defines the +L inherits all attributes from L and defines the following new ones. =head2 claims @@ -154,7 +158,7 @@ your Google Business Administrator. =head2 target Get or set the target. At the time of writing, there is only one valid target: -https://www.googleapis.com/oauth2/v3/token. This is the default value; if you +https://www.googleapis.com/oauth2/v4/token. This is the default value; if you have no need to customize this, then just fetch the default. @@ -175,7 +179,7 @@ Inherits all methods from L and defines the following new ones. Loads the JSON file from Google with the client ID information in it and sets the respective attributes. -Returns 0 on failure: file not found or value not defined +Dies a horrible death on failure: 'Your JSON file import failed.' $gjwt->from_json('/my/google/app/project/sa/json/file'); @@ -184,9 +188,6 @@ Returns 0 on failure: file not found or value not defined L -=head1 SOURCE REPOSITORY - -L =head1 AUTHOR @@ -195,6 +196,7 @@ Richard Elberger, =head1 CONTRIBUTORS Scott Wiersdorf, +Avishai Goldman, =head1 COPYRIGHT AND LICENSE @@ -202,3 +204,5 @@ Copyright (C) 2015 by Richard Elberger This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. + +=cut From a332dd9978e0cd8b6011a8014686b3303fc3948e Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sat, 23 Nov 2019 20:19:54 +0200 Subject: [PATCH 10/68] ignore vim temp files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8b633e1..d28ae1c 100755 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ examples/*.png **/delme* *.json docs/cover/* + +.*.sw? From 85633e80aadc477012a6f8e6a26116f047997cc3 Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sat, 23 Nov 2019 20:46:28 +0200 Subject: [PATCH 11/68] began making notes for changes and tapping out the service account business --- lib/WebService/GoogleAPI/Client.pm | 8 +++++ .../GoogleAPI/Client/AuthStorage.pm | 2 +- .../Client/AuthStorage/ConfigJSON.pm | 4 +-- .../Client/AuthStorage/ServiceAccount.pm | 33 +++++++++++++++++++ .../GoogleAPI/Client/Credentials.pm | 6 ++-- lib/WebService/GoogleAPI/Client/UserAgent.pm | 12 +++++-- 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 172c674..83ffb26 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -13,6 +13,13 @@ use Mojo::Util; #TODO- batch requests. The only thing necessary is to send a #multipart request as I wrote in that e-mail. +# +#TODO- implement auth for service accounts. +# +#TODO- allow using promises instead of just calling. Also allow +# for full access to the tx instead of assuming it's always +# returning the res. Perhaps mix in something that delegates the +# json method to the res? # ABSTRACT: Google API Discovery and SDK @@ -397,6 +404,7 @@ sub _process_params_for_api_endpoint_and_return_errors ################################################## #small subs to convert between these_types to theseTypes of params +#TODO- should probs move this into a Util module sub camel { shift if @_ > 1; $_[0] =~ s/ _(\w) /\u$1/grx }; sub snake { shift if @_ > 1; $_[0] =~ s/([[:upper:]])/_\l$1/grx }; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 0aa30ff..53ac879 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -34,7 +34,7 @@ sub setup my ( $self, $params ) = @_; if ( $params->{ type } eq 'jsonfile' ) { - $self->storage->pathToTokensFile( $params->{ path } ); + $self->storage->path( $params->{ path } ); $self->storage->setup; $self->is_set( 1 ); } diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 7c647b3..e1afb1a 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -8,7 +8,7 @@ use Moo; use Config::JSON; use Carp; -has 'pathToTokensFile' => ( is => 'rw', default => 'gapi.json' ); # default is gapi.json +has 'path' => ( is => 'rw', default => 'gapi.json' ); # default is gapi.json # has 'tokensfile'; # Config::JSON object pointer my $tokensfile; @@ -19,7 +19,7 @@ has 'debug' => ( is => 'rw', default => 0 ); sub setup { my ( $self ) = @_; - $tokensfile = Config::JSON->new( $self->pathToTokensFile ); + $tokensfile = Config::JSON->new( $self->path ); return $self; } diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm new file mode 100644 index 0000000..7b88942 --- /dev/null +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -0,0 +1,33 @@ +use strictures; + +package WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; + +# ABSTRACT: Specific methods to fetch tokens from a service +# account json file + +use Moo; +use Config::JSON; +use Carp; + +extends 'WebService::GoogleAPI::Client::AuthStorage::ConfigJSON'; + + + + + + + + + + + + + + + + + + + + +9001 diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index 69ff33f..be4e5e0 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -12,6 +12,8 @@ with 'MooX::Singleton'; has 'access_token' => ( is => 'rw' ); +#TODO- user is a little bit imprecise, b/c a service account +#doesn't necessarily have a user has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); # full gmail, like peter@shotgundriver.com has 'auth_storage' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::AuthStorage->new } ); # dont delete to able to configure @@ -30,7 +32,7 @@ sub get_access_token_for_user } else { - croak q/Can get access token for specified user because storage isn't set/; + croak q/Can't get access token for specified user because storage isn't set/; } return $self; ## ?? is self the access token for user? } @@ -44,7 +46,7 @@ sub get_scopes_as_array } else { - croak q/Can get access token for specified user because storage isn't set/; + croak q/Can't get access token for specified user because storage isn't set/; } } diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index f8fe3c0..21b9b53 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -18,8 +18,12 @@ use Carp qw/croak carp cluck/; has 'do_autorefresh' => ( is => 'rw', default => 1 ); # if 1 storage must be configured has 'auto_update_tokens_in_storage' => ( is => 'rw', default => 1 ); has 'debug' => ( is => 'rw', default => 0 ); -has 'credentials' => - ( is => 'rw', default => sub { WebService::GoogleAPI::Client::Credentials->instance }, handles => [qw/access_token auth_storage get_scopes_as_array user /], lazy => 1 ); +has 'credentials' => ( + is => 'rw', + default => sub { WebService::GoogleAPI::Client::Credentials->instance }, + handles => [qw/access_token auth_storage get_scopes_as_array user /], + lazy => 1 +); ## NB - could cache using https://metacpan.org/pod/Mojo::UserAgent::Cached TODO: Review source of this for ideas @@ -44,7 +48,7 @@ sub BUILD =cut -## TODO: this should probably nbe handled ->on('start' => sub {}) as per https://metacpan.org/pod/Mojolicious::Guides::Cookbook#Decorating-follow-up-requests +## TODO: this should probably be handled ->on('start' => sub {}) as per https://metacpan.org/pod/Mojolicious::Guides::Cookbook#Decorating-follow-up-requests sub header_with_bearer_auth_token { @@ -179,6 +183,7 @@ sub validated_api_query my $tx = $self->build_http_transaction( $params ); cluck("$params->{method} $params->{path}") if $self->debug; + #TODO- figure out how we can alter this to use promises my $res = $self->start( $tx )->res; ## TODO: HANDLE TIMEOUTS AND OTHER ERRORS IF THEY WEREN'T HANDLED BY build_http_transaction @@ -189,6 +194,7 @@ sub validated_api_query if ( ( $res->code == 401 ) && $self->do_autorefresh ) { if ( $res->code == 401 ) ## redundant - was there something else in mind ? + #TODO- I'm fairly certain this fires too often { croak "No user specified, so cant find refresh token and update access_token" unless $self->user; cluck "401 response - access_token was expired. Attemptimg to update it automatically ..." if ($self->debug > 11); From fcb1128098306b1396c2e28ae48c40e8c0e47a9a Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Sat, 23 Nov 2019 21:28:22 +0200 Subject: [PATCH 12/68] fixed up missing important test files --- t/JWT/load1.json | 7 +++++++ t/JWT/load2.json | 1 + t/JWT/load3.json | 6 ++++++ t/JWT/load4.json | 7 +++++++ 4 files changed, 21 insertions(+) create mode 100644 t/JWT/load1.json create mode 100644 t/JWT/load2.json create mode 100644 t/JWT/load3.json create mode 100644 t/JWT/load4.json diff --git a/t/JWT/load1.json b/t/JWT/load1.json new file mode 100644 index 0000000..0a22880 --- /dev/null +++ b/t/JWT/load1.json @@ -0,0 +1,7 @@ +{ + "private_key_id": "8", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIC\nk8KLWw6r/ERRBg\u003d\u003d\n-----END PRIVATE KEY-----\n", + "client_email": "9dvse@developer.gserviceaccount.com", + "client_id": "ihkkmv89dvse.apps.googleusercontent.com", + "type": "service_account" +} diff --git a/t/JWT/load2.json b/t/JWT/load2.json new file mode 100644 index 0000000..6b7a9f4 --- /dev/null +++ b/t/JWT/load2.json @@ -0,0 +1 @@ +not json diff --git a/t/JWT/load3.json b/t/JWT/load3.json new file mode 100644 index 0000000..04d2769 --- /dev/null +++ b/t/JWT/load3.json @@ -0,0 +1,6 @@ +{ + "private_key_id": "8", + "client_email": "9dvse@developer.gserviceaccount.com", + "client_id": "ihkkmv89dvse.apps.googleusercontent.com", + "type": "service_account" +} diff --git a/t/JWT/load4.json b/t/JWT/load4.json new file mode 100644 index 0000000..965d0bc --- /dev/null +++ b/t/JWT/load4.json @@ -0,0 +1,7 @@ +{ + "private_key_id": "8", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIC\nk8KLWw6r/ERRBg\u003d\u003d\n-----END PRIVATE KEY-----\n", + "client_email": "9dvse@developer.gserviceaccount.com", + "client_id": "ihkkmv89dvse.apps.googleusercontent.com", + "type": "not_a_service_account" +} From 4b0c6986816eac068d5de9e995ea116b1562f1dc Mon Sep 17 00:00:00 2001 From: LapVeesh Date: Wed, 25 Dec 2019 18:46:52 +0200 Subject: [PATCH 13/68] started TODO file --- TODO | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..0c3846d --- /dev/null +++ b/TODO @@ -0,0 +1,38 @@ +we're trying to take care of a bunch of things. + +In order to implement service accounts, we need to have an +encapsulated interface for retrieving and refreshing tokens. Then, +there can be a shared interface, and both normal files and service +accounts can be called the same way. + +We expect to implement something which parses the json file, and +then determines what type it is. A gapi file has 'gapi' as the top +level key. A service account has the following top level keys: +qw/ type project_id private_key_id private_key client_email +client_id auth_uri token_uri auth_provider_x509_cert_url +client_x509_cert_url /. + + + +#Other Issues + +We'd like to implement a cache for constructing calls, b/c it +works WAAAY too hard. Constructing 100 calls to +sheets.spreadsheets.values.batchGetByDataFilter made my CPU churn +like a tell, and for way too long, too. + + +I'd like to abstract the interpolation and crud into a seperate +module, so that I could use it to make the inner requests for a +batch request. + +I'd like to implement generators for the main ua, and use that as +an interface for adding the headers and crud. The only reason to +not use the 'start' hook is b/c we don't want our auth stuff on a +request for a token. Unless it doesn't matter... + +We need some live tests that we'll put in the xt directory, so +that we can be sure that when it's live things work. + +We must fix all the weird warnings in the test suite, and improve +our coverage and stuff. From ba67806035a2ac9cc907a840b04a33c228fa70d4 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Tue, 26 May 2020 18:29:25 +0300 Subject: [PATCH 14/68] switched perltidy to my tastes tapped out more TODO updated Deps (we can move Mojo::JWT::Google out now) updated repo to point to ours began a major refactor cleaning up cruft from Client::Discovery now and testing it more sanely now, too began cleaning up examples (oh goodness gracious) started sprinkling my own TODOs Client::Discover - added 403 error handling, and don't bother using header otherwise refactored the cache to be its own function removed a few stupid tests made some test tools for NON-stupid testing basically, beginning major league refactor --- .perltidyrc | 33 +- TODO | 20 +- dist.ini | 23 +- examples/usage_basic_examples.pl | 52 +- lib/WebService/GoogleAPI/Client.pm | 3 +- .../GoogleAPI/Client/Credentials.pm | 26 +- lib/WebService/GoogleAPI/Client/Discovery.pm | 783 +- lib/WebService/GoogleAPI/Client/UserAgent.pm | 16 +- t/00-load.t | 57 - t/03-pre-warmed-cache-discovery.t | 7136 ----------------- t/04-client-mocked-agent.t | 7110 ---------------- t/05-spec-interpolation.t | 3 + .../GoogleAPI/Client/Discovery.t} | 226 +- t/lib/TestTools.pm | 39 + t/manifest.t | 2 +- t/pod.t | 2 +- 16 files changed, 707 insertions(+), 14824 deletions(-) delete mode 100755 t/00-load.t delete mode 100644 t/03-pre-warmed-cache-discovery.t delete mode 100644 t/04-client-mocked-agent.t rename t/{01-client-discovery.t => WebService/GoogleAPI/Client/Discovery.t} (74%) create mode 100644 t/lib/TestTools.pm diff --git a/.perltidyrc b/.perltidyrc index d0c1d65..737e532 100644 --- a/.perltidyrc +++ b/.perltidyrc @@ -1,16 +1,17 @@ --pbp -nst # Start with Perl Best Practices - not sure how to drive and cuddles my braces --w # Show all warnings --iob # Ignore old breakpoints --l=180 # 80 characters per line --mbl=2 # No more than 2 blank lines --i=2 # Indentation is 2 columns --ci=2 # Continuation indentation is 2 columns --vt=0 # vertical tightness --pt=0 # parenthesis tightness --bt=0 # brace tightness --sbt=2 # square bracket tightness --wn # Weld nested containers --isbc # Don't indent comments without leading space --sbl # sub-brace on new line --bl # Opening Braces Left --b -bext='/' # operwrite the file and delete backup if no errors +--warning-output +--ignore-old-breakpoints +--maximum-line-length=80 +--maximum-consecutive-blank-lines=2 +--indent-columns=2 +--continuation-indentation=2 +--vertical-tightness=0 +--paren-tightness=2 +--brace-tightness=1 +--square-bracket-tightness=2 +--weld-nested-containers +--indent-spaced-block-comments +--cuddled-else +--trim-qw +--opening-brace-always-on-right +--opening-token-right +-b # overwrite the file diff --git a/TODO b/TODO index 0c3846d..180fe14 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,7 @@ we're trying to take care of a bunch of things. +#better support for service accounts + In order to implement service accounts, we need to have an encapsulated interface for retrieving and refreshing tokens. Then, there can be a shared interface, and both normal files and service @@ -13,19 +15,28 @@ client_id auth_uri token_uri auth_provider_x509_cert_url client_x509_cert_url /. - #Other Issues +## Less logic on repeated calls + We'd like to implement a cache for constructing calls, b/c it works WAAAY too hard. Constructing 100 calls to sheets.spreadsheets.values.batchGetByDataFilter made my CPU churn like a tell, and for way too long, too. +Maybe even go for dynamic class creation, similar to what OpenAPI +producers use. Although I'm thinking of just moving over to using +gRPC, which does actually have a perl client (seemingly). + + +## Encapsulate logic better I'd like to abstract the interpolation and crud into a seperate module, so that I could use it to make the inner requests for a batch request. +## Use generators instead + I'd like to implement generators for the main ua, and use that as an interface for adding the headers and crud. The only reason to not use the 'start' hook is b/c we don't want our auth stuff on a @@ -34,5 +45,12 @@ request for a token. Unless it doesn't matter... We need some live tests that we'll put in the xt directory, so that we can be sure that when it's live things work. +## Test suite is filled with garbage + We must fix all the weird warnings in the test suite, and improve our coverage and stuff. + +Fix the usage of a gapi.json here. We need to clean up its attempt +to find. + +Move everything to Test2. diff --git a/dist.ini b/dist.ini index 3a48936..4075792 100755 --- a/dist.ini +++ b/dist.ini @@ -27,9 +27,13 @@ main_module = lib/WebService/GoogleAPI/Client.pm ; Mojo::Message::Response = 7.12 ## can't do this - no version info available for Mojo::Message::Response ;Moose = 2.2 perl = 5.14.04 -Mojolicious = 7.12 +Mojolicious = 8.30 Mojolicious::Plugin::OAuth2 = 1.5 List::Util = 1.45 +IO::Socket::SSL = 2.06 +Mojo::JWT = 0 +Mojo::JWT::Google = 0.09 +Exporter::Shiny = 0 [AutoPrereqs] @@ -71,20 +75,9 @@ copy = LICENSE [MetaJSON] - ;[GitHub::Meta] - ;issues = 1 - ;user = pscott-au - ;repository = https://github.com/pscott-au/WebService-GoogleAPI-Client - ;require_auth = 1 - ; uncomment when doing a major release - don't want to hit every build - [GitHub::Meta] -repository = https://github.com/pscott-au/WebService-GoogleAPI-Client - -; Configured by setting use_github_issues for the bundle -;bugs = 1 -; Configured by setting use_github_homepage for the bundle -;homepage = 1 +repository = https://github.com/rabbiveesh/WebService-GoogleAPI-Client +issues = 1 [ChangelogFromGit] max_age = 14 @@ -104,4 +97,4 @@ critic_config = .perlcriticrc ; default / relative to project root ; the top of the file, which will violate ; Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict. One ; solution is to use the Dist::Zilla::Plugin::OurPkgVersion which allows -; you to control where the $VERSION declaration appears. \ No newline at end of file +; you to control where the $VERSION declaration appears. diff --git a/examples/usage_basic_examples.pl b/examples/usage_basic_examples.pl index 3609807..ec2baba 100755 --- a/examples/usage_basic_examples.pl +++ b/examples/usage_basic_examples.pl @@ -8,9 +8,17 @@ use File::Which; use feature 'say'; +use Mojo::Util qw/getopt/; + +getopt + 'c|credentials' => \my $creds, + 'u|user' => \my $user; + +$creds //= './gapi.json'; +die "can't do cool things with no username!" unless $user; ## assumes gapi.json configuration in working directory with scoped project and user authorization -my $gapi_client = WebService::GoogleAPI::Client->new( debug => 1, gapi_json => './gapi.json', user => 'peter@shotgundriver.com' ); +my $gapi_client = WebService::GoogleAPI::Client->new(gapi_json => $creds, user => $user); use Email::Simple; ## RFC2822 formatted messages @@ -18,25 +26,20 @@ my $r; ## using to contain result of queries ( Mojo::Message::Response instance ) -my $my_email_address = 'peter@shotgundriver.com'; - - -if ( 0 ) -{ - print "Sending email to self\n"; - $r = $gapi_client->api_query( - api_endpoint_id => 'gmail.users.messages.send', - options => { - raw => encode_base64( - Email::Simple->create( - header => [To => $my_email_address, From => $my_email_address, Subject => "Test email from '$my_email_address' ",], - body => "This is the body of email to '$my_email_address'", - )->as_string - ) - }, - ); -} +print "Sending email to self\n"; +$r = $gapi_client->api_query( + api_endpoint_id => 'gmail.users.messages.send', + options => { + raw => encode_base64( + Email::Simple->create( + header => + [To => $user, From => $user, Subject => "Test email from '$user' ",], + body => "This is the body of email to '$user'", + )->as_string + ) + }, +); if ( 0 ) { @@ -120,16 +123,6 @@ #say my $x = WebService::GoogleAPI::Client::Discovery->new->list_of_available_google_api_ids(); say 'fnarly' if ref( WebService::GoogleAPI::Client::Discovery->new->discover_all() ) eq 'HASH'; -#exit; -if ( WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api() ) -{ - #say Dumper [ WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api() ]; -} -else -{ - #say Dumper WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api(); -} - say length( WebService::GoogleAPI::Client::Discovery->new->supported_as_text ) > 100; #say Dumper $x; @@ -141,6 +134,5 @@ my $dd = $f->get_credentials_for_refresh(); -#say WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api(); #say join(',', WebService::GoogleAPI::Client->new->list_of_available_google_api_ids() ) . ' as list'; #say WebService::GoogleAPI::Client->new->list_of_available_google_api_ids() . ' as scalar'; diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 83ffb26..64c9c1f 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -38,7 +38,7 @@ Access Google API Services Version 1 using an OAUTH2 User Agent. Includes Discovery, validation authentication and API Access. assumes gapi.json configuration in working directory with scoped Google project -redentials and user authorization created by _goauth_ +credentials and user authorization created by _goauth_ use WebService::GoogleAPI::Client; @@ -138,6 +138,7 @@ sub BUILD { my ( $self, $params ) = @_; + #TODO- implement google standard way of finding the credentials $self->auth_storage->setup( { type => 'jsonfile', path => $params->{ gapi_json } } ) if ( defined $params->{ gapi_json } ); $self->user( $params->{ user } ) if ( defined $params->{ user } ); diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index be4e5e0..75ff2aa 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -23,25 +23,21 @@ Automatically get access_token for current user if auth_storage is set =cut -sub get_access_token_for_user -{ - my ( $self ) = @_; - if ( $self->auth_storage->is_set ) - { # chech that auth_storage initialized fine - $self->access_token( $self->auth_storage->get_access_token_from_storage( $self->user ) ); - } - else - { - croak q/Can't get access token for specified user because storage isn't set/; +sub get_access_token_for_user { + my ($self) = @_; + if ($self->auth_storage->is_set) { + # check that auth_storage initialized fine + $self->access_token( + $self->auth_storage->get_access_token_from_storage($self->user)); + } else { + croak q/Can't get access token, Storage isn't set/; } - return $self; ## ?? is self the access token for user? + return $self; } -sub get_scopes_as_array -{ +sub get_scopes_as_array { my ( $self ) = @_; - if ( $self->auth_storage->is_set ) - { # chech that auth_storage initialized fine + if ($self->auth_storage->is_set) { return $self->access_token( $self->auth_storage->get_scopes_from_storage_as_array() ); } else diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index ffea656..1a45434 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -23,7 +23,9 @@ say $client-dicovery->chi->root_dir(); ## provides full file path to temp storag =over 2 -=item * handle 403 ( Daily Limit for Unauthenticated Use Exceeded.) errors when reqeusting a discovrey resource for a service but do we have access to authenticated reqeusts? +=item * handle 403 (Daily Limit for Unauthenticated Use Exceeded) + +errors when reqeusting a discovery resource for a service but do we have access to authenticated reqeusts? =back @@ -32,38 +34,97 @@ say $client-dicovery->chi->root_dir(); ## provides full file path to temp storag use Moo; use Carp; use WebService::GoogleAPI::Client::UserAgent; -use List::Util qw/uniq/; ## are these util dependencies necessary? +use List::Util qw/uniq reduce/; use Hash::Slice qw/slice/; use Data::Dump qw/pp/; -use CHI; # Caching .. NB Consider reviewing https://metacpan.org/pod/Mojo::UserAgent::Role::Cache -# use feature - -## NB - I am not familiar with this moosey approach to OO so there may be obvious errors - keep an eye on this. - -has 'ua' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::UserAgent->new }, lazy => 1 ); ## typically shared with parent instance of Client which sets on new -has 'debug' => ( is => 'rw', default => 0, lazy => 1 ); -has 'chi' => ( is => 'rw', default => sub { CHI->new( driver => 'File', namespace => __PACKAGE__ ) }, lazy => 1 ); ## i believe that this gives priority to param if provided ? - - -my $stats = { - network => { - # last request timestamp - # last request response_code - # last request response_code_count - # total bytes sent - # total bytes received - }, - cache => { - get => 0, - # last request timestamp - # last request response_code - # last request response_code_count - # total bytes sent - # total bytes received - } -}; +use CHI; + +has ua => ( + is => 'rw', + default => sub { WebService::GoogleAPI::Client::UserAgent->new } +); +has debug => (is => 'rw', default => 0); +has 'chi' => ( + is => 'rw', + default => sub { CHI->new(driver => 'File', namespace => __PACKAGE__) }, + lazy => 1 +); ## i believe that this gives priority to param if provided ? +has 'stats' => is => 'rw', + default => sub { { network => { get => 0 }, cache => { get => 0 } } }; + =head1 METHODS +=head2 get_with_cache + + my $hashref = $disco->get_with_cache($url, $force, $authenticate) + +Retrieves the given API URL, retrieving and caching the returned +JSON. If it gets a 403 Unauthenticated error, then it will try +again using the credentials that are save on this instances 'ua'. + +If passed a truthy value for $force, then will not use the cache. +If passed a truthy value for $authenticate, then will make the +request with credentials. + +=cut + +sub get_with_cache { + my ($self, $key, $force, $authorized) = @_; + + my $expiration = $self->chi->get_expires_at($key) // 0; + + my $will_expire = $expiration - time(); + if ($will_expire > 0 && not $force) { + carp "discovery_data cached data expires in $will_expire seconds" + if $self->debug > 2; + my $ret = $self->chi->get($key); + croak 'was expecting a HASHREF!' unless ref $ret eq 'HASH'; + $self->stats->{cache}{get}++; + return $ret; + } else { + my $ret; + if ($authorized) { + $ret = $self->ua->validated_api_query($key); + $self->stats->{network}{authorized}++; + } else { + $ret = $self->ua->get($key)->res; + } + if ($ret->is_success) { + my $all = $ret->json; + $self->stats->{network}{get}++; + $self->chi->set($key, $all, '30d'); + return $all; + } else { + if ($ret->code == 403 && !$authorized) { + return $self->get_with_cache($key, $force, 1); + } + croak $ret->message; + } + } + return {}; +} + +=head2 C + + my $hashref = $disco->discover_all($force, $authenticate) + +Return details about all Available Google APIs as provided by Google or in CHI Cache. +Does the fetching with C, and arguments are as above. + +On Success: Returns HASHREF with keys discoveryVersion,items,kind +On Failure: Warns and returns empty hashref + +SEE ALSO: available_APIs, list_of_available_google_api_ids + +=cut + +sub discover_key {'https://www.googleapis.com/discovery/v1/apis'} + +sub discover_all { + my $self = shift; + $self->get_with_cache($self->discover_key, @_); +} + =head2 get_api_discovery_for_api_id @@ -73,298 +134,248 @@ a Perl data structure. my $hashref = $self->get_api_discovery_for_api_id( 'gmail' ); my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3' ); my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3.users.list' ); - my $hashref = $self->get_api_discovery_for_api_id( { api=> 'gmail', version => 'v3' } ); ## nb enclosing bracer - must be hashref + my $hashref = $self->get_api_discovery_for_api_id( { api=> 'gmail', version => 'v3' } ); NB: if deeper structure than the api_id is provided then only the head is used so get_api_discovery_for_api_id( 'gmail' ) is the same as get_api_discovery_for_api_id( 'gmail.some.child.method' ) -returns the api discovery specification structure ( cached by CHI ) for api id ( eg 'gmail ') +returns the api discovery specification structure ( cached by CHI ) for api id (eg 'gmail') returns the discovery data as a hashref, an empty hashref on certain failing conditions or croaks on critical errors. =cut -sub get_api_discovery_for_api_id -{ - my ( $self, $params ) = @_; +sub get_api_discovery_for_api_id { + my ($self, $params) = @_; ## TODO: warn if user doesn't have the necessary scope .. no should stil be able to examine ## TODO: consolidate the http method calls to a single function - ie - discover_all - simplistic quick fix - assume that if no param then endpoint is as per discover_all - $params = { api => $params } if ref( $params ) eq ''; ## scalar parameter not hashref - so assume is intended to be $params->{api} + $params = { api => $params } + if ref $params eq '' + ; ## scalar parameter not hashref - so assume is intended to be $params->{api} ## trim any resource, method or version details in api id - if ( $params->{ api } =~ /([^:]+):(v[^\.]+)/ixsm ) - { - $params->{ api } = $1; - $params->{ version } = $2; + if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) { + $params->{api} = $1; + $params->{version} = $2; } - if ( $params->{ api } =~ /^(.*?)\./xsm ) ## we only want the api and not the children so trime them out here - { - $params->{ api } = $1; - + if ($params->{api} =~ /^(.*?)\./xsm) { + $params->{api} = $1; } - - croak( "get_api_discovery_for_api_id called with api param undefined" . pp $params) unless defined $params->{ api }; - $params->{ version } = $self->latest_stable_version( $params->{ api } ) unless defined $params->{ version }; - croak( "get_api_discovery_for_api_id called with empty api param defined" . pp $params) if $params->{ api } eq ''; - croak( "get_api_discovery_for_api_id called with empty version param defined" . pp $params) if $params->{ version } eq ''; + croak( + "get_api_discovery_for_api_id called with api param undefined" . pp $params) + unless defined $params->{api}; + $params->{version} = $self->latest_stable_version($params->{api}) + unless defined $params->{version}; + + croak("get_api_discovery_for_api_id called with empty api param defined" + . pp $params) + if $params->{api} eq ''; + croak("get_api_discovery_for_api_id called with empty version param defined" + . pp $params) + if $params->{version} eq ''; my $aapis = $self->available_APIs(); my $api_verson_urls = {}; - for my $api ( @{ $aapis } ) - { - for ( my $i = 0; $i < scalar @{ $api->{ versions } }; $i++ ) - { - $api_verson_urls->{ $api->{ name } }{ $api->{ versions }[$i] } = $api->{ discoveryRestUrl }[$i]; + for my $api (@{$aapis}) { + for (my $i = 0; $i < scalar @{ $api->{versions} }; $i++) { + $api_verson_urls->{ $api->{name} }{ $api->{versions}[$i] } + = $api->{discoveryRestUrl}[$i]; } } - croak( "Unable to determine discovery URI for any version of $params->{api}" ) unless defined $api_verson_urls->{ $params->{ api } }; - croak( "Unable to determine discovery URI for $params->{api} $params->{version}" ) unless defined $api_verson_urls->{ $params->{ api } }{ $params->{ version } }; - my $api_discovery_uri = $api_verson_urls->{ $params->{ api } }{ $params->{ version } }; - - #carp "get_api_discovery_for_api_id requires data from $api_discovery_uri" if $self->debug; - if ( my $dat = $self->chi->get( $api_discovery_uri ) ) ## clobbers some of the attempted thinking further on .. just return it for now if it's there + croak("Unable to determine discovery URI for any version of $params->{api}") + unless defined $api_verson_urls->{ $params->{api} }; + croak( + "Unable to determine discovery URI for $params->{api} $params->{version}") + unless defined $api_verson_urls->{ $params->{api} }{ $params->{version} }; + my $api_discovery_uri + = $api_verson_urls->{ $params->{api} }{ $params->{version} }; + +#carp "get_api_discovery_for_api_id requires data from $api_discovery_uri" if $self->debug; + if (my $dat = $self->chi->get($api_discovery_uri) + ) ## clobbers some of the attempted thinking further on .. just return it for now if it's there { #carp pp $dat; $self->{stats}{cache}{get}++; return $dat; } - if ( my $expires_at = $self->chi->get_expires_at( $api_discovery_uri ) ) ## maybe this isn't th ebest way to check if get available. + if (my $expires_at = $self->chi->get_expires_at($api_discovery_uri) + ) ## maybe this isn't th ebest way to check if get available. { - carp "CHI '$api_discovery_uri' cached data with root = " . $self->chi->root_dir . "expires in ", scalar( $expires_at ) - time(), " seconds\n" if $self->debug; + carp "CHI '$api_discovery_uri' cached data with root = " + . $self->chi->root_dir + . "expires in ", scalar($expires_at) - time(), " seconds\n" + if $self->debug; - #carp "Value = " . pp $self->chi->get( $api_discovery_uri ) if $self->debug ; - return $self->chi->get( $api_discovery_uri ); + #carp "Value = " . pp $self->chi->get( $api_discovery_uri ) if $self->debug ; + return $self->chi->get($api_discovery_uri); - } - else - { + } else { carp "'$api_discovery_uri' not in cache - fetching it" if $self->debug; ## TODO: better handle failed response - if 403 then consider adding in the auth headers and trying again. #croak("Huh $api_discovery_uri"); - my $ret = $self->ua->validated_api_query( $api_discovery_uri ); # || croak("Failed to retrieve $api_discovery_uri");; - if ( $ret->is_success ) - { - my $dat = $ret->json || croak( "failed to convert $api_discovery_uri return data in json" ); + my $ret = $self->ua->validated_api_query($api_discovery_uri) + ; # || croak("Failed to retrieve $api_discovery_uri");; + if ($ret->is_success) { + my $dat = $ret->json + || croak("failed to convert $api_discovery_uri return data in json"); #carp("dat1 = " . pp $dat); - $self->chi->set( $api_discovery_uri, $dat, '30 days' ); + $self->chi->set($api_discovery_uri, $dat, '30 days'); $self->{stats}{network}{get}++; return $dat; - #my $ret_data = $self->chi->get( $api_discovery_uri ); - #carp ("ret_data = " . pp $ret_data) unless ref($ret_data) eq 'HASH'; - #return $ret_data;# if ref($ret_data) eq 'HASH'; - #croak(); - #$self->chi->remove( $api_discovery_uri ) unless eval '${^TAINT}'; ## if not hashref then assume is corrupt so delete it - } - else - { +#my $ret_data = $self->chi->get( $api_discovery_uri ); +#carp ("ret_data = " . pp $ret_data) unless ref($ret_data) eq 'HASH'; +#return $ret_data;# if ref($ret_data) eq 'HASH'; +#croak(); +#$self->chi->remove( $api_discovery_uri ) unless eval '${^TAINT}'; ## if not hashref then assume is corrupt so delete it + } else { ## TODO - why is this failing for certain resources ?? because the urls contain a '$' ? because they are now authenticated? - carp( "Fetching resource failed - $ret->message" ); ## was croaking - carp( pp $ret ); - return {}; #$ret; + carp("Fetching resource failed - $ret->message"); ## was croaking + carp(pp $ret ); + return {}; #$ret; } } - croak( "something went wrong in get_api_discovery_for_api_id key = '$api_discovery_uri' - try again to see if data corruption has been flushed for " . pp $params); + croak( + "something went wrong in get_api_discovery_for_api_id key = '$api_discovery_uri' - try again to see if data corruption has been flushed for " + . pp $params); - #return $self->chi->get( $api_discovery_uri ); - #croak('never gets here'); } -=head2 C - -TODO: Consider rename to return_fetched_google_v1_apis_discovery_structure - -TODO - handle auth required error and resubmit request with OAUTH headers if response indicates - access requires auth ( when exceed free access limits ) - - Return details about all Available Google APIs as provided by Google or in CHI Cache - - On Success: Returns HASHREF with keys discoveryVersion,items,kind - On Failure: Warns and returns empty hashref - my $d = WebService::GoogleAPI::Client::Discovery->new; - print Dumper $d; +#TODO- is used in get_api_discover_for_api_id +# is used in service_exists +# is used in supported_as_text +# is used in available_versions +# is used in api_versions_urls +# is used in list_of_available_google_api_ids +=head2 C - WebService::GoogleAPI::Client::Discovery->discover_all(); +Return arrayref of all available API's (services) - $client->discover_all(); - $client->discover_all(1); ## NB if include a parameter that evaluates to true such as '1' then the cache is flushed with a new version + { + name => 'youtube', + versions => [ 'v3' ] + doclinks => [ ... ] , + discoveryRestUrl => [ ... ] , + }, +Originally for printing list of supported API's in documentation .. + -SEE ALSO: available_APIs, list_of_available_google_api_ids +SEE ALSO: +may be better/more flexible to use client->list_of_available_google_api_ids +client->discover_all which is delegated to Client::Discovery->discover_all =cut -sub discover_all -{ - - my ( $self, $force ) = @_; - - if ( my $expires_at = $self->chi->get_expires_at( 'https://www.googleapis.com/discovery/v1/apis' ) && not $force ) - { - carp "discovery_data cached data expires in ", scalar( $expires_at ) - time(), " seconds\n" if ( $self->debug > 2 ); - my $ret = $self->chi->get( 'https://www.googleapis.com/discovery/v1/apis' ); - croak( 'CHI Discovery should be a hash - got something other' ) unless ref( $ret ) eq 'HASH'; - $self->{stats}{cache}{get}++; - return $ret; - } - else ## - { - #return $self->chi->get('https://www.googleapis.com/discovery/v1/apis') if ($self->chi->get('https://www.googleapis.com/discovery/v1/apis')); - my $ret = $self->ua->validated_api_query( 'https://www.googleapis.com/discovery/v1/apis' ); - - # if ( $ret->is_status_class(200) ) ## older versions of Mojo::Message::Response don't support is_success .. Require V > 7.12 - if ( $ret->is_success ) - { - my $all = $ret->json; - $self->{stats}{network}{get}++; - $self->chi->set( 'https://www.googleapis.com/discovery/v1/apis', $all, '30d' ); - return $self->chi->get( 'https://www.googleapis.com/discovery/v1/apis' ); - } - else - { +#TODO- maybe cache this on disk too? +my $available; +sub _invalidate_available { + $available = undef; +} - carp( "$ret->message" ); ## should probably croak - return {}; +sub available_APIs { + #cache this crunch + return $available if $available; + + my ($self) = @_; + my $d_all = $self->discover_all; + croak 'no items in discovery data' unless defined $d_all->{items}; + + my @keys = qw/name version documentationLink discoveryRestUrl/; + my @relevant; + for my $i (@{ $d_all->{items} }) { + #grab only entries with the four keys we want, and strip other + #keys + next unless @keys == grep { exists $i->{$_} } @keys; + push @relevant, { %{$i}{@keys} }; + }; + + my $reduced = reduce { + for my $key (qw/version documentationLink discoveryRestUrl/) { + $a->{$b->{name}}->{$key} //= []; + push @{$a->{$b->{name}}->{$key}}, $b->{$key}; } - } - return {}; + $a; + } {}, @relevant; + + $available = [ + map { { + name => $_, + versions => $reduced->{$_}{version}, + doclinks => $reduced->{$_}{documentationLink}, + discoveryRestUrl => $reduced->{$_}{discoveryRestUrl} + } } keys %$reduced + ]; } -=head2 C +=head2 C Allows you to augment the cached stored version of the discovery structure - augment_discover_all_with_unlisted_experimental_api( - { - 'version' => 'v4', - 'preferred' => 1, - 'title' => 'Google My Business API', - 'description' => 'The Google My Business API provides an interface for managing business location information on Google.', - 'id' => 'mybusiness:v4', - 'kind' => 'discovery#directoryItem', - 'documentationLink' => "https://developers.google.com/my-business/", - 'icons' => { - "x16"=> "http://www.google.com/images/icons/product/search-16.gif", - "x32"=> "http://www.google.com/images/icons/product/search-32.gif" - }, - 'discoveryRestUrl' => - 'https://developers.google.com/my-business/samples/mybusiness_google_rest_v4p2.json', - 'name' => 'mybusiness' - } ); - -if there is a conflict with the existing then warn and return the existing data without modification - -on success just returns the augmented structure - -NB - does not interpolate schema object '$ref' values. + $augmented_document = $disco->augment_with({ + 'version' => 'v4', + 'preferred' => 1, + 'title' => 'Google My Business API', + 'description' => 'The Google My Business API provides an interface for managing business location information on Google.', + 'id' => 'mybusiness:v4', + 'kind' => 'discovery#directoryItem', + 'documentationLink' => "https://developers.google.com/my-business/", + 'icons' => { + "x16" => "http://www.google.com/images/icons/product/search-16.gif", + "x32" => "http://www.google.com/images/icons/product/search-32.gif" + }, + 'discoveryRestUrl' => 'https://developers.google.com/my-business/samples/mybusiness_google_rest_v4p2.json', + 'name' => 'mybusiness' + }); + +This can also be used to overwrite the cached structure. + +Can also be called as C, which is +being deprecated for being plain old too long. =cut +sub augment_discover_all_with_unlisted_experimental_api { + my ($self, $api_spec) = @_; + carp <augment_with($api_spec); +} -sub augment_discover_all_with_unlisted_experimental_api -{ - my ( $self, $api_spec ) = @_; +sub augment_with { + my ($self, $api_spec) = @_; my $all = $self->discover_all(); - #warn pp $all; ## fail if any of the expected fields are not provided - foreach my $field ( qw/version title description id kind documentationLink discoveryRestUrl name/ ) ## icons preferred - { - if ( not defined $api_spec->{ $field } ) - { - carp( "required $field in provided experimental api spec in not defined - returning without augmentation" ); - return $all; + for my $field ( + qw/version title description id kind documentationLink + discoveryRestUrl name/ + ) { + if (not defined $api_spec->{$field}) { + carp("required $field in provided api spec missing"); } - } - - ## warn and return existing data if entry appears to already exist - foreach my $i ( @{ $all->{ items } } ) - { - $i->{ id } = '' unless defined $i->{ id }; - if ( ( $i->{ name } eq $api_spec->{ name } ) && ( $i->{ version } eq $api_spec->{ version } ) ) - { - carp( "There is already an entry with name = $i->{name} and version = $i->{version} - no modifications saved" ); - return $all; - } - if ( $i->{ id } eq $api_spec->{id} ) - { - carp( "There is already an entry with id = $i->{id} - no modifications saved" ); - return $all; - } - } - push @{ $all->{ items } }, $api_spec; - $self->chi->set( 'https://www.googleapis.com/discovery/v1/apis', $all, '30d' ); - return $self->chi->get( 'https://www.googleapis.com/discovery/v1/apis' ); - + push @{ $all->{items} }, $api_spec; + $self->chi->set($self->discover_key, $all, '30d'); + $self->_invalidate_available; + return $all } -=head2 C - -Return arrayref of all available API's (services) - - { - 'name' => 'youtube', - 'versions' => [ 'v3' ] - documentationLink => , - discoveryRestUrl => , - }, - -Originally for printing list of supported API's in documentation .. - - -SEE ALSO: -may be better/more flexible to use client->list_of_available_google_api_ids -client->discover_all which is delegated to Client::Discovery->discover_all - -=cut - -sub available_APIs -{ - my ( $self ) = @_; - my $d_all = $self->discover_all(); ## - warn( 'no items in discovery data' ) unless defined $d_all->{ items }; - return [] unless defined $d_all->{ items }; - my $all = $d_all->{ items }; - - #print pp $all; - for my $i ( @$all ) - { - $i = { map { $_ => $i->{ $_ } } grep { exists $i->{ $_ } } qw/name version documentationLink discoveryRestUrl/ }; - } - my @subset = uniq map { $_->{ name } } @$all; ## unique names - # carp scalar @$all; - # carp scalar @subset; - # carp dump \@subset; - # my @a = map { $_->{name} } @$all; - - my @arr; - for my $s ( @subset ) - { - #print pp $s; - my @v = map { $_->{ version } } grep { $_->{ name } eq $s } @$all; - my @doclinks = uniq map { $_->{ documentationLink } } grep { $_->{ name } eq $s } @$all; - my @discovery_links = map { $_->{ discoveryRestUrl } } grep { $_->{ name } eq $s } @$all; - - push @arr, { name => $s, versions => \@v, doclinks => \@doclinks, discoveryRestUrl => \@discovery_links }; - } - - return \@arr; -} - =head2 C Return 1 if Google Service API ID is described by Google API discovery. @@ -377,12 +388,13 @@ NB - Is case sensitive - all lower is required so $d->service_exists('Calendar') =cut -sub service_exists -{ - my ( $self, $api ) = @_; +sub service_exists { + my ($self, $api) = @_; return 0 unless $api; my $apis_all = $self->available_APIs(); - return grep { $_->{ name } eq $api } @$apis_all; ## 1 iff an equality is found with keyed name + return + grep { $_->{name} eq $api } + @$apis_all; ## 1 iff an equality is found with keyed name } =head2 C @@ -393,22 +405,29 @@ sub service_exists =cut -sub supported_as_text -{ - my ( $self ) = @_; +sub supported_as_text { + my ($self) = @_; my $ret = ''; - for my $api ( @{ $self->available_APIs() } ) - { - croak( 'doclinks key defined but is not the expected arrayref' ) unless ref $api->{ doclinks } eq 'ARRAY'; - croak( 'array of apis provided by available_APIs includes one without a defined name' ) unless defined $api->{ name }; + for my $api (@{ $self->available_APIs() }) { + croak('doclinks key defined but is not the expected arrayref') + unless ref $api->{doclinks} eq 'ARRAY'; + croak( + 'array of apis provided by available_APIs includes one without a defined name' + ) unless defined $api->{name}; - my @clean_doclinks = grep { defined $_ } @{ $api->{ doclinks } }; ## was seeing undef in doclinks array - eg 'surveys'causing warnings in join + my @clean_doclinks = grep { defined $_ } + @{ $api->{doclinks} } + ; ## was seeing undef in doclinks array - eg 'surveys'causing warnings in join ## unique doclinks using idiom from https://www.oreilly.com/library/view/perl-cookbook/1565922433/ch04s07.html - my %seen = (); - my $doclinks = join( ',', ( grep { !$seen{ $_ }++ } @clean_doclinks ) ) || ''; ## unique doclinks as string - - $ret .= $api->{ name } . ' : ' . join( ',', @{ $api->{ versions } } ) . ' : ' . $doclinks . "\n"; + my %seen = (); + my $doclinks = join(',', (grep { !$seen{$_}++ } @clean_doclinks)) + || ''; ## unique doclinks as string + + $ret + .= $api->{name} . ' : ' + . join(',', @{ $api->{versions} }) . ' : ' + . $doclinks . "\n"; } return $ret; } @@ -424,13 +443,12 @@ sub supported_as_text =cut -sub available_versions -{ - my ( $self, $api ) = @_; +sub available_versions { + my ($self, $api) = @_; return [] unless $api; - my @api_target = grep { $_->{ name } eq $api } @{ $self->available_APIs() }; - return [] if scalar( @api_target ) == 0; - return $api_target[0]->{ versions }; + my @api_target = grep { $_->{name} eq $api } @{ $self->available_APIs() }; + return [] if scalar(@api_target) == 0; + return $api_target[0]->{versions}; } =head2 C @@ -448,36 +466,30 @@ return latest stable verion of API =cut -sub latest_stable_version -{ - my ( $self, $api ) = @_; +sub latest_stable_version { + my ($self, $api) = @_; return '' unless $api; - return '' unless $self->available_versions( $api ); - return '' unless @{ $self->available_versions( $api ) } > 0; - my $versions = $self->available_versions( $api ); # arrayref - if ( $versions->[-1] =~ /beta/ ) - { + return '' unless $self->available_versions($api); + return '' unless @{ $self->available_versions($api) } > 0; + my $versions = $self->available_versions($api); # arrayref + if ($versions->[-1] =~ /beta/) { return $versions->[0]; - } - else - { + } else { return $versions->[-1]; } } ######################################################## -sub api_version_urls -{ - my ( $self ) = @_; +sub api_version_urls { + my ($self) = @_; ## transform structure to be keyed on api->versionRestUrl my $aapis = $self->available_APIs(); my $api_verson_urls = {}; - for my $api ( @{ $aapis } ) - { - for ( my $i = 0; $i < scalar @{ $api->{ versions } }; $i++ ) - { - $api_verson_urls->{ $api->{ name } }{ $api->{ versions }[$i] } = $api->{ discoveryRestUrl }[$i]; + for my $api (@{$aapis}) { + for (my $i = 0; $i < scalar @{ $api->{versions} }; $i++) { + $api_verson_urls->{ $api->{name} }{ $api->{versions}[$i] } + = $api->{discoveryRestUrl}[$i]; } } return $api_verson_urls; @@ -496,48 +508,48 @@ returns an empty hashref if not found ######################################################## -sub extract_method_discovery_detail_from_api_spec -{ - my ( $self, $tree, $api_version ) = @_; +sub extract_method_discovery_detail_from_api_spec { + my ($self, $tree, $api_version) = @_; ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method return {} unless defined $tree; my @nodes = split /\./smx, $tree; - croak( "tree structure '$tree' must contain at least 2 nodes including api id, [list of hierarchical resources ] and method - not " . scalar( @nodes ) ) - unless scalar( @nodes ) > 1; + croak( + "tree structure '$tree' must contain at least 2 nodes including api id, [list of hierarchical resources ] and method - not " + . scalar(@nodes)) + unless scalar(@nodes) > 1; - my $api_id = shift( @nodes ); ## api was head - my $method = pop( @nodes ); ## method was tail + my $api_id = shift(@nodes); ## api was head + my $method = pop(@nodes); ## method was tail ## split out version if is defined as part of $tree ## trim any resource, method or version details in api id - if ( $api_id =~ /([^:]+):([^\.]+)$/ixsm ) ## we have already isolated head from api tree children + if ($api_id =~ /([^:]+):([^\.]+)$/ixsm + ) ## we have already isolated head from api tree children { - $api_id = $1; - $api_version = $2; + $api_id = $1; + $api_version = $2; } ## handle incorrect api_id - if ( $self->service_exists( $api_id ) == 0 ) - { - carp( "unable to confirm that '$api_id' is a valid Google API service id" ); + if ($self->service_exists($api_id) == 0) { + carp("unable to confirm that '$api_id' is a valid Google API service id"); return {}; } - $api_version = $self->latest_stable_version( $api_id ) unless $api_version; + $api_version = $self->latest_stable_version($api_id) unless $api_version; ## TODO: confirm that spec available for api version - my $api_spec = $self->get_api_discovery_for_api_id( { api => $api_id, version => $api_version } ); - + my $api_spec = $self->get_api_discovery_for_api_id( + { api => $api_id, version => $api_version }); - ## we use the schemas to substitute into '$ref' keyed placeholders + ## we use the schemas to substitute into '$ref' keyed placeholders my $schemas = {}; - foreach my $schema_key (sort keys %{$api_spec->{schemas}}) - { - $schemas->{$schema_key} = $api_spec->{'schemas'}{$schema_key}; + foreach my $schema_key (sort keys %{ $api_spec->{schemas} }) { + $schemas->{$schema_key} = $api_spec->{'schemas'}{$schema_key}; } ## recursive walk through the structure in _fix_ref @@ -545,50 +557,56 @@ sub extract_method_discovery_detail_from_api_spec ## '$ref' values within the schema structures themselves ## including within the schema spec structures (NB assumes no cyclic structures ) ## otherwise would could recursive chaos - my $api_spec_fix = $self->_fix_ref( $api_spec, $schemas ); ## first level ( '$ref' in the method params and return values etc ) - $api_spec = $self->_fix_ref($api_spec_fix, $schemas ); ## second level ( '$ref' in the interpolated schemas from first level ) + my $api_spec_fix = $self->_fix_ref($api_spec, $schemas) + ; ## first level ( '$ref' in the method params and return values etc ) + $api_spec = $self->_fix_ref($api_spec_fix, $schemas) + ; ## second level ( '$ref' in the interpolated schemas from first level ) ## now extract all the methods (recursive ) - my $all_api_methods = $self->_extract_resource_methods_from_api_spec( "$api_id:$api_version", $api_spec ); + my $all_api_methods + = $self->_extract_resource_methods_from_api_spec("$api_id:$api_version", + $api_spec); + #print dump $all_api_methods;exit; - unless ( defined $all_api_methods->{ $tree } ) - { - $all_api_methods = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec ); + unless (defined $all_api_methods->{$tree}) { + $all_api_methods + = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec); } - if ($all_api_methods->{$tree}){ - #add in the global parameters to the endpoint, + if ($all_api_methods->{$tree}) { + + #add in the global parameters to the endpoint, #stored in the top level of the api_spec - $all_api_methods->{$tree}{parameters} = { - %{ $all_api_methods->{$tree}{parameters} }, %{ $api_spec->{parameters} } + $all_api_methods->{$tree}{parameters} = { + %{ $all_api_methods->{$tree}{parameters} }, + %{ $api_spec->{parameters} } }; - return $all_api_methods->{ $tree }; + return $all_api_methods->{$tree}; } - carp( "Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version" ) if $self->debug; - return {}; + carp( + "Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version" + ) if $self->debug; + return {}; } ######################################################## ######################################################## -sub _extract_resource_methods_from_api_spec -{ - my ( $self, $tree, $api_spec, $ret ) = @_; +sub _extract_resource_methods_from_api_spec { + my ($self, $tree, $api_spec, $ret) = @_; $ret = {} unless defined $ret; - croak( "ret not a hash - $tree, $api_spec, $ret" ) unless ref( $ret ) eq 'HASH'; + croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH'; - if ( defined $api_spec->{ methods } && ref( $api_spec->{ methods } ) eq 'HASH' ) - { - foreach my $method ( keys %{ $api_spec->{ methods } } ) - { - $ret->{ "$tree.$method" } = $api_spec->{ methods }{ $method } if ref( $api_spec->{ methods }{ $method } ) eq 'HASH'; + if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') { + foreach my $method (keys %{ $api_spec->{methods} }) { + $ret->{"$tree.$method"} = $api_spec->{methods}{$method} + if ref($api_spec->{methods}{$method}) eq 'HASH'; } } - if ( defined $api_spec->{ resources } ) - { - foreach my $resource ( keys %{ $api_spec->{ resources } } ) - { + if (defined $api_spec->{resources}) { + foreach my $resource (keys %{ $api_spec->{resources} }) { ## NB - recursive traversal down tree of api_spec resources - $self->_extract_resource_methods_from_api_spec( "$tree.$resource", $api_spec->{ resources }{ $resource }, $ret ); + $self->_extract_resource_methods_from_api_spec("$tree.$resource", + $api_spec->{resources}{$resource}, $ret); } } return $ret; @@ -600,52 +618,45 @@ sub _extract_resource_methods_from_api_spec #This sub walks through the structure and replaces any hashes keyed with '$ref' with #the value defined in $schemas->{ } # -#eg -# ->{'response'}{'$ref'}{'Buckets'} -# is replaced with +#eg +# ->{'response'}{'$ref'}{'Buckets'} +# is replaced with # ->{response}{ $schemas->{Buckets} } # # It assumes that the schemas have been extracted from the original discover for the API -# and is typically applued to the method ( api endpoint ) to provide a fully descriptive +# and is typically applued to the method ( api endpoint ) to provide a fully descriptive # structure without external references. # #=cut ######################################################## -sub _fix_ref -{ - my ( $self, $node, $schemas ) = @_; - my $ret = undef; - my $r = ref($node); - - - if ( $r eq 'ARRAY' ) { - $ret = []; - foreach my $el ( @$node ) - { - push @$ret, $self->_fix_ref( $el, $schemas ); - } +sub _fix_ref { + my ($self, $node, $schemas) = @_; + my $ret = undef; + my $r = ref($node); + + + if ($r eq 'ARRAY') { + $ret = []; + foreach my $el (@$node) { + push @$ret, $self->_fix_ref($el, $schemas); } - elsif ( $r eq 'HASH') - { - $ret = {}; - foreach my $key ( keys %$node ) - { - if ( $key eq '$ref' ) - { - #say $node->{'$ref'}; - $ret = $schemas->{ $node->{'$ref'} }; - } - else - { - $ret->{$key} = $self->_fix_ref( $node->{$key}, $schemas ); - } - } + } elsif ($r eq 'HASH') { + $ret = {}; + foreach my $key (keys %$node) { + if ($key eq '$ref') { + + #say $node->{'$ref'}; + $ret = $schemas->{ $node->{'$ref'} }; + } else { + $ret->{$key} = $self->_fix_ref($node->{$key}, $schemas); + } } - else - { $ret = $node; } - - return $ret; + } else { + $ret = $node; + } + + return $ret; } ######################################################## @@ -663,27 +674,28 @@ representing the corresponding discovery specification for that method ( API End =cut -#TODO: consider ? refactor to allow parameters either as a single api id such as 'gmail' +#TODO: consider ? refactor to allow parameters either as a single api id such as 'gmail' # as well as the currently accepted hash keyed on the api and version # -#SEE ALSO: +#SEE ALSO: # The following methods are delegated through to Client::Discovery - see perldoc WebService::Client::Discovery for detils # -# get_method_meta -# discover_all -# extract_method_discovery_detail_from_api_spec +# get_method_meta +# discover_all +# extract_method_discovery_detail_from_api_spec # get_api_discovery_for_api_id ######################################################## ## TODO: consider renaming ? -sub methods_available_for_google_api_id -{ - my ( $self, $api_id, $version ) = @_; +sub methods_available_for_google_api_id { + my ($self, $api_id, $version) = @_; - $version = $self->latest_stable_version( $api_id ) unless $version; + $version = $self->latest_stable_version($api_id) unless $version; ## TODO: confirm that spec available for api version - my $api_spec = $self->get_api_discovery_for_api_id( { api => $api_id, version => $version } ); - my $methods = $self->_extract_resource_methods_from_api_spec( $api_id, $api_spec ); + my $api_spec = $self->get_api_discovery_for_api_id( + { api => $api_id, version => $version }); + my $methods + = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec); return $methods; } ######################################################## @@ -703,13 +715,16 @@ that is either fetched or cached in CHI locally for 30 days. ######################################################## ## returns a list of all available API Services -sub list_of_available_google_api_ids -{ - my ( $self ) = @_; - my $aapis = $self->available_APIs(); ## array of hashes - my @api_list = map { $_->{ name } } @$aapis; - return wantarray ? @api_list : join( ',', @api_list ); ## allows to be called in either list or scalar context - #return @api_list; +sub list_of_available_google_api_ids { + my ($self) = @_; + my $aapis = $self->available_APIs(); ## array of hashes + my @api_list = map { $_->{name} } @$aapis; + return + wantarray + ? @api_list + : join(',', @api_list) + ; ## allows to be called in either list or scalar context + #return @api_list; } ######################################################## diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 21b9b53..a22799f 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -50,22 +50,18 @@ sub BUILD ## TODO: this should probably be handled ->on('start' => sub {}) as per https://metacpan.org/pod/Mojolicious::Guides::Cookbook#Decorating-follow-up-requests -sub header_with_bearer_auth_token -{ +sub header_with_bearer_auth_token { my ( $self, $headers ) = @_; $headers = {} unless defined $headers; - $headers->{ 'Accept-Encoding' } = 'gzip'; + $headers->{'Accept-Encoding'} = 'gzip'; - # cluck "header_with_bearer_auth_token: ".$self->access_token; - if ( $self->access_token ) - { - $headers->{ 'Authorization' } = 'Bearer ' . $self->access_token; + if ($self->access_token) { + $headers->{Authorization} = 'Bearer ' . $self->access_token; } - else - { - cluck 'No access_token, can\'t build Auth header'; + else { + carp 'No access_token, can\'t build Auth header'; } return $headers; } diff --git a/t/00-load.t b/t/00-load.t deleted file mode 100755 index 9877489..0000000 --- a/t/00-load.t +++ /dev/null @@ -1,57 +0,0 @@ -#!perl -T - -=head2 USAGE - -To run manually in the local directory assuming gapi.json present in source root and in xt/author/calendar directory - C - -NB: is also run as part of dzil test - -=cut - -use 5.006; -use strict; -use warnings; -use Test::More; - -use Cwd; -my $dir = getcwd; - -# plan tests => 1; ## or at end done_test(); -my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; - -#BEGIN { - -use_ok( 'WebService::GoogleAPI::Client' ) || print "Bail out!\n"; - -my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil -my $user = $ENV{ 'GMAIL_FOR_TESTING' } || ''; ## will be populated by first available if set to '' and default_file exists - - -subtest 'Test with User Configuration' => sub { - plan( skip_all => 'No user configuration - set $ENV{GOOGLE_TOKENSFILE} or create gapi.json in dzil source root directory' ) unless -e $default_file; - - ok( 1 == 1, 'Configured WebService::GoogleAPI::Client User' ); - my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG ); - - $gapi->auth_storage->setup( { type => 'jsonfile', path => $default_file } ) || croak( $! ); - my $aref_token_emails = $gapi->auth_storage->storage->get_token_emails_from_storage; - $user = $aref_token_emails->[0] unless $user; ## default to the first user if none defined yet - - #note("ENV CONFIG SETS $ENV{'GMAIL_FOR_TESTING'} WITHIN $ENV{'GOOGLE_TOKENSFILE'} "); - - if ( -e $default_file && $user ) - { - note( "Running tests with user '$user' using '$default_file' credentials" ); - $gapi->auth_storage->setup( { type => 'jsonfile', path => $default_file } ); - $gapi->user( $user ); - - #p($gapi); - - } -}; ## END 'Test with User Configuration' SUBTEST - - -#note("Testing WebService::GoogleAPI::Client $WebService::GoogleAPI::Client::VERSION, Perl $], $^X"); - -done_testing(); diff --git a/t/03-pre-warmed-cache-discovery.t b/t/03-pre-warmed-cache-discovery.t deleted file mode 100644 index 36b52b2..0000000 --- a/t/03-pre-warmed-cache-discovery.t +++ /dev/null @@ -1,7136 +0,0 @@ -#!perl -T - -=head2 USAGE - -To run manually in the local directory assuming gapi.json present in source root and in xt/author/calendar directory - C - -NB: is also run as part of dzil test - -=cut - -use 5.006; -use strict; -use warnings; -use Test::More; -use Data::Dumper; ## remove this when finish tweaking -use Cwd; -use CHI; -use WebService::GoogleAPI::Client; -use Sub::Override; -#use WebService::GoogleAPI::Client::Discovery; -use JSON; -#use lib './lib'; ## provides pre-populated values -#use CHI::Driver::TEST_MOCKED; - -my $dir = getcwd; -my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging -my $warm_hash = {}; -#$warm_hash->{'https://www.googleapis.com/discovery/v1/apis'} = 'foo'; -#print Dumper $hash; - - -my $chi = CHI->new(driver => 'RawMemory', datastore => $warm_hash ); - -#$chi->set('https://www.googleapis.com/discovery/v1/apis' => from_json( pre_get_all_apis_json()) ); -#$chi->set('https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest' => from_json(pre_get_gmail_spec_json()) ); -use_ok( 'WebService::GoogleAPI::Client::Discovery' ); - -#my $disc = WebService::GoogleAPI::Client::Discovery->new( ); - -ok( my $disc = WebService::GoogleAPI::Client::Discovery->new( chi=>$chi, debug=>$DEBUG ) ,'Create a new discovery instance to use mocked agent responses'); - - ## INJECT HTTP RESPONSE FOR API DISCOVERY REQUEST - my $override = Sub::Override->new('Mojo::Transaction::res', sub { - my $res = Mojo::Message::Response->new ; - $res->code( 200 ); - $res->body( pre_get_all_apis_json() ); - return $res; - }); - - #stop that annoying cluck from trying to get the auth header - #for our fake request - my $cluck = Sub::Override->new('WebService::GoogleAPI::Client::UserAgent::cluck', - sub { } ); - - ok( my $all = $disc->discover_all(), 'discover_all()' ); - - ## INJECT HTTP RESPONSE FOR GMAIL V1 API SPECIFICATION - my $override2 = Sub::Override->new('Mojo::Transaction::res', - sub { - my $res2 = Mojo::Message::Response->new ; - $res2->code( 200 ); - $res2->body( pre_get_gmail_spec_json() ); - return $res2; - }); - - - ok( my $gmail_api_spec = $disc->methods_available_for_google_api_id('gmail', 'v1'), 'Get available methods for Gmail V1'); - - $cluck->restore; - - - my $override3 = Sub::Override->new('Mojo::Transaction::res', - sub { - my $res2 = Mojo::Message::Response->new ; - $res2->code( 200 ); - $res2->body( "TESTED FINE" ); - return $res2; - }); - - - #my $gapi_client = WebService::GoogleAPI::Client->new( debug => 0, gapi_json => 'gapi.json', chi => $chi ); - #ok( my $cl = $gapi_client->api_query( api_endpoint_id => 'gmail.users.messages.list' ), 'api_query - "gmail.users.messages.list"'); - - - - - -ok ( $disc->augment_discover_all_with_unlisted_experimental_api( - { - 'version' => 'v4', - 'preferred' => 1, - 'title' => 'Google My Business API', - 'description' => 'The Google My Business API - provides an interface for managing business location information on - Google.', - 'id' => 'mybusiness:v4', - 'kind' => 'discovery#directoryItem', - 'documentationLink' => - "https://developers.google.com/my-business/", - 'icons' => { - "x16"=> - "http://www.google.com/images/icons/product/search-16.gif", - "x32"=> - "http://www.google.com/images/icons/product/search-32.gif" - }, - 'discoveryRestUrl' => - 'https://developers.google.com/my-business/samples/mybusiness_google_rest_v4p2.json', - 'name' => 'mybusiness' - } ), 'Augment discovery all with experimental specification' ); - - - - -ok ( $disc->available_APIs(), 'Get available APIs'); -ok ( $disc->supported_as_text(), 'Supported as text'); -ok ( $disc->available_versions('gmail'), 'Available versions for Gmail'); -ok ( $disc->latest_stable_version('gmail'), 'Latest stable version for Gmail'); -ok( $disc->api_version_urls(), 'api version urls'); -ok( $disc->methods_available_for_google_api_id('gmail'), 'Get end points available for gmail'); -ok( $disc->list_of_available_google_api_ids(), 'All available Google API IDs'); - -note( ' CHI TYPE + ' . ref( $disc->chi() ) ); -#note( my$y = $x->get('https://www.googleapis.com/discovery/v1/apis')); -#print Dumper $x; - -#use_ok( 'WebService::GoogleAPI::Client' ); # || print "Bail out!\n"; - - -#ok( ref( WebService::GoogleAPI::Client::Discovery->new->( chi => $chi )->discover_all() ) eq 'HASH', 'WebService::GoogleAPI::Client::Discovery->new->discover_all() return HASREF' ); - - - -done_testing(); - - -sub pre_get_all_apis_json -{ - return <<'_END' -{ - "kind": "discovery#directoryList", - "discoveryVersion": "v1", - "items": [ - { - "kind": "discovery#directoryItem", - "id": "abusiveexperiencereport:v1", - "name": "abusiveexperiencereport", - "version": "v1", - "title": "Abusive Experience Report API", - "description": "Views Abusive Experience Report data, and gets a list of sites that have a significant number of abusive experiences.", - "discoveryRestUrl": "https://abusiveexperiencereport.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/abusive-experience-report/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "acceleratedmobilepageurl:v1", - "name": "acceleratedmobilepageurl", - "version": "v1", - "title": "Accelerated Mobile Pages (AMP) URL API", - "description": "This API contains a single method, batchGet. Call this method to retrieve the AMP URL (and equivalent AMP Cache URL) for given public URL(s).", - "discoveryRestUrl": "https://acceleratedmobilepageurl.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/amp/cache/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "accesscontextmanager:v1beta", - "name": "accesscontextmanager", - "version": "v1beta", - "title": "Access Context Manager API", - "description": "An API for setting attribute based access control to requests to GCP services.", - "discoveryRestUrl": "https://accesscontextmanager.googleapis.com/$discovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/access-context-manager/docs/reference/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.2", - "name": "adexchangebuyer", - "version": "v1.2", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.2/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.3", - "name": "adexchangebuyer", - "version": "v1.3", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.3/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.4", - "name": "adexchangebuyer", - "version": "v1.4", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.4/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer2:v2beta1", - "name": "adexchangebuyer2", - "version": "v2beta1", - "title": "Ad Exchange Buyer API II", - "description": "Accesses the latest features for managing Ad Exchange accounts, Real-Time Bidding configurations and auction metrics, and Marketplace programmatic deals.", - "discoveryRestUrl": "https://adexchangebuyer.googleapis.com/$discovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest/reference/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexperiencereport:v1", - "name": "adexperiencereport", - "version": "v1", - "title": "Ad Experience Report API", - "description": "Views Ad Experience Report data, and gets a list of sites that have a significant number of annoying ads.", - "discoveryRestUrl": "https://adexperiencereport.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/ad-experience-report/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "admin:datatransfer_v1", - "name": "admin", - "version": "datatransfer_v1", - "title": "Admin Data Transfer API", - "description": "Transfers user data from one user to another.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/datatransfer_v1/rest", - "discoveryLink": "./apis/admin/datatransfer_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/data-transfer/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "admin:directory_v1", - "name": "admin", - "version": "directory_v1", - "title": "Admin Directory API", - "description": "Manages enterprise resources such as users and groups, administrative notifications, security features, and more.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/directory_v1/rest", - "discoveryLink": "./apis/admin/directory_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/directory/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "admin:reports_v1", - "name": "admin", - "version": "reports_v1", - "title": "Admin Reports API", - "description": "Fetches reports for the administrators of G Suite customers about the usage, collaboration, security, and risk for their users.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/reports_v1/rest", - "discoveryLink": "./apis/admin/reports_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/reports/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adsense:v1.4", - "name": "adsense", - "version": "v1.4", - "title": "AdSense Management API", - "description": "Accesses AdSense publishers' inventory and generates performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adsense/v1.4/rest", - "discoveryLink": "./apis/adsense/v1.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/adsense-16.png", - "x32": "https://www.google.com/images/icons/product/adsense-32.png" - }, - "documentationLink": "https://developers.google.com/adsense/management/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adsensehost:v4.1", - "name": "adsensehost", - "version": "v4.1", - "title": "AdSense Host API", - "description": "Generates performance reports, generates ad codes, and provides publisher management capabilities for AdSense Hosts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adsensehost/v4.1/rest", - "discoveryLink": "./apis/adsensehost/v4.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/adsense-16.png", - "x32": "https://www.google.com/images/icons/product/adsense-32.png" - }, - "documentationLink": "https://developers.google.com/adsense/host/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "alertcenter:v1beta1", - "name": "alertcenter", - "version": "v1beta1", - "title": "G Suite Alert Center API", - "description": "G Suite Alert Center API to view and manage alerts on issues affecting your domain.", - "discoveryRestUrl": "https://alertcenter.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/alertcenter/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "analytics:v2.4", - "name": "analytics", - "version": "v2.4", - "title": "Google Analytics API", - "description": "Views and manages your Google Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/analytics/v2.4/rest", - "discoveryLink": "./apis/analytics/v2.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/analytics-16.png", - "x32": "https://www.google.com/images/icons/product/analytics-32.png" - }, - "documentationLink": "https://developers.google.com/analytics/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "analytics:v3", - "name": "analytics", - "version": "v3", - "title": "Google Analytics API", - "description": "Views and manages your Google Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/analytics/v3/rest", - "discoveryLink": "./apis/analytics/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/analytics-16.png", - "x32": "https://www.google.com/images/icons/product/analytics-32.png" - }, - "documentationLink": "https://developers.google.com/analytics/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "analyticsreporting:v4", - "name": "analyticsreporting", - "version": "v4", - "title": "Google Analytics Reporting API", - "description": "Accesses Analytics report data.", - "discoveryRestUrl": "https://analyticsreporting.googleapis.com/$discovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/analytics/devguides/reporting/core/v4/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androiddeviceprovisioning:v1", - "name": "androiddeviceprovisioning", - "version": "v1", - "title": "Android Device Provisioning Partner API", - "description": "Automates Android zero-touch enrollment for device resellers, customers, and EMMs.", - "discoveryRestUrl": "https://androiddeviceprovisioning.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/zero-touch/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidenterprise:v1", - "name": "androidenterprise", - "version": "v1", - "title": "Google Play EMM API", - "description": "Manages the deployment of apps to Android for Work users.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidenterprise/v1/rest", - "discoveryLink": "./apis/androidenterprise/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android/work/play/emm-api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidmanagement:v1", - "name": "androidmanagement", - "version": "v1", - "title": "Android Management API", - "description": "The Android Management API provides remote enterprise management of Android devices and apps.", - "discoveryRestUrl": "https://androidmanagement.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/android/management", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v1", - "name": "androidpublisher", - "version": "v1", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v1/rest", - "discoveryLink": "./apis/androidpublisher/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v1.1", - "name": "androidpublisher", - "version": "v1.1", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v1.1/rest", - "discoveryLink": "./apis/androidpublisher/v1.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v2", - "name": "androidpublisher", - "version": "v2", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v2/rest", - "discoveryLink": "./apis/androidpublisher/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v3", - "name": "androidpublisher", - "version": "v3", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v3/rest", - "discoveryLink": "./apis/androidpublisher/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1alpha", - "name": "appengine", - "version": "v1alpha", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$discovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta", - "name": "appengine", - "version": "v1beta", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$discovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1", - "name": "appengine", - "version": "v1", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta4", - "name": "appengine", - "version": "v1beta4", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$discovery/rest?version=v1beta4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta5", - "name": "appengine", - "version": "v1beta5", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$discovery/rest?version=v1beta5", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appsactivity:v1", - "name": "appsactivity", - "version": "v1", - "title": "Drive Activity API", - "description": "Provides a historical view of activity.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/appsactivity/v1/rest", - "discoveryLink": "./apis/appsactivity/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/activity/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appstate:v1", - "name": "appstate", - "version": "v1", - "title": "Google App State API", - "description": "The Google App State API.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/appstate/v1/rest", - "discoveryLink": "./apis/appstate/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services/web/api/states", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "bigquery:v2", - "name": "bigquery", - "version": "v2", - "title": "BigQuery API", - "description": "A data platform for customers to create, manage, share and query data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/bigquery/v2/rest", - "discoveryLink": "./apis/bigquery/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/search-16.gif", - "x32": "https://www.google.com/images/icons/product/search-32.gif" - }, - "documentationLink": "https://cloud.google.com/bigquery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "bigquerydatatransfer:v1", - "name": "bigquerydatatransfer", - "version": "v1", - "title": "BigQuery Data Transfer API", - "description": "Transfers data from partner SaaS applications to Google BigQuery on a scheduled, managed basis.", - "discoveryRestUrl": "https://bigquerydatatransfer.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/bigquery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "binaryauthorization:v1beta1", - "name": "binaryauthorization", - "version": "v1beta1", - "title": "Binary Authorization API", - "description": "The management interface for Binary Authorization, a system providing policy control for images deployed to Kubernetes Engine clusters.", - "discoveryRestUrl": "https://binaryauthorization.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/binary-authorization/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "blogger:v2", - "name": "blogger", - "version": "v2", - "title": "Blogger API", - "description": "API for access to the data within Blogger.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/blogger/v2/rest", - "discoveryLink": "./apis/blogger/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/blogger-16.png", - "x32": "https://www.google.com/images/icons/product/blogger-32.png" - }, - "documentationLink": "https://developers.google.com/blogger/docs/2.0/json/getting_started", - "labels": [ - "limited_availability" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "blogger:v3", - "name": "blogger", - "version": "v3", - "title": "Blogger API", - "description": "API for access to the data within Blogger.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/blogger/v3/rest", - "discoveryLink": "./apis/blogger/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/blogger-16.png", - "x32": "https://www.google.com/images/icons/product/blogger-32.png" - }, - "documentationLink": "https://developers.google.com/blogger/docs/3.0/getting_started", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "books:v1", - "name": "books", - "version": "v1", - "title": "Books API", - "description": "Searches for books and manages your Google Books library.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/books/v1/rest", - "discoveryLink": "./apis/books/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/ebooks-16.png", - "x32": "https://www.google.com/images/icons/product/ebooks-32.png" - }, - "documentationLink": "https://developers.google.com/books/docs/v1/getting_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "calendar:v3", - "name": "calendar", - "version": "v3", - "title": "Calendar API", - "description": "Manipulates events and other calendar data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest", - "discoveryLink": "./apis/calendar/v3/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/calendar-16.png", - "x32": "http://www.google.com/images/icons/product/calendar-32.png" - }, - "documentationLink": "https://developers.google.com/google-apps/calendar/firstapp", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "chat:v1", - "name": "chat", - "version": "v1", - "title": "Hangouts Chat API", - "description": "Create bots and extend the new Hangouts Chat.", - "discoveryRestUrl": "https://chat.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/hangouts/chat", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "civicinfo:v2", - "name": "civicinfo", - "version": "v2", - "title": "Google Civic Information API", - "description": "Provides polling places, early vote locations, contest data, election officials, and government representatives for U.S. residential addresses.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/civicinfo/v2/rest", - "discoveryLink": "./apis/civicinfo/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/civic-information", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "classroom:v1", - "name": "classroom", - "version": "v1", - "title": "Google Classroom API", - "description": "Manages classes, rosters, and invitations in Google Classroom.", - "discoveryRestUrl": "https://classroom.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/classroom", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudasset:v1beta1", - "name": "cloudasset", - "version": "v1beta1", - "title": "Cloud Asset API", - "description": "The cloud asset API manages the history and inventory of cloud resources.", - "discoveryRestUrl": "https://cloudasset.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://console.cloud.google.com/apis/api/cloudasset.googleapis.com/overview", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbilling:v1", - "name": "cloudbilling", - "version": "v1", - "title": "Cloud Billing API", - "description": "Allows developers to manage billing for their Google Cloud Platform projects programmatically.", - "discoveryRestUrl": "https://cloudbilling.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/billing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbuild:v1alpha1", - "name": "cloudbuild", - "version": "v1alpha1", - "title": "Cloud Build API", - "description": "Creates and manages builds on Google Cloud Platform.", - "discoveryRestUrl": "https://cloudbuild.googleapis.com/$discovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/cloud-build/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbuild:v1", - "name": "cloudbuild", - "version": "v1", - "title": "Cloud Build API", - "description": "Creates and manages builds on Google Cloud Platform.", - "discoveryRestUrl": "https://cloudbuild.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/cloud-build/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "clouddebugger:v2", - "name": "clouddebugger", - "version": "v2", - "title": "Stackdriver Debugger API", - "description": "Examines the call stack and variables of a running application without stopping or slowing it down.", - "discoveryRestUrl": "https://clouddebugger.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/debugger", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "clouderrorreporting:v1beta1", - "name": "clouderrorreporting", - "version": "v1beta1", - "title": "Stackdriver Error Reporting API", - "description": "Groups and counts similar errors from cloud services and applications, reports new errors, and provides access to error groups and their associated errors.", - "discoveryRestUrl": "https://clouderrorreporting.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/error-reporting/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudfunctions:v1", - "name": "cloudfunctions", - "version": "v1", - "title": "Cloud Functions API", - "description": "Manages lightweight user-provided functions executed in response to events.", - "discoveryRestUrl": "https://cloudfunctions.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/functions", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudfunctions:v1beta2", - "name": "cloudfunctions", - "version": "v1beta2", - "title": "Cloud Functions API", - "description": "Manages lightweight user-provided functions executed in response to events.", - "discoveryRestUrl": "https://cloudfunctions.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/functions", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudiot:v1", - "name": "cloudiot", - "version": "v1", - "title": "Cloud IoT API", - "description": "Registers and manages IoT (Internet of Things) devices that connect to the Google Cloud Platform.", - "discoveryRestUrl": "https://cloudiot.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iot", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudiot:v1beta1", - "name": "cloudiot", - "version": "v1beta1", - "title": "Cloud IoT API", - "description": "Registers and manages IoT (Internet of Things) devices that connect to the Google Cloud Platform.", - "discoveryRestUrl": "https://cloudiot.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iot", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudkms:v1", - "name": "cloudkms", - "version": "v1", - "title": "Cloud Key Management Service (KMS) API", - "description": "Manages keys and performs cryptographic operations in a central cloud service, for direct use by other cloud resources and applications.", - "discoveryRestUrl": "https://cloudkms.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kms/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudprofiler:v2", - "name": "cloudprofiler", - "version": "v2", - "title": "Stackdriver Profiler API", - "description": "Manages continuous profiling information.", - "discoveryRestUrl": "https://cloudprofiler.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/profiler/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v1", - "name": "cloudresourcemanager", - "version": "v1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v1beta1", - "name": "cloudresourcemanager", - "version": "v1beta1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v2", - "name": "cloudresourcemanager", - "version": "v2", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v2beta1", - "name": "cloudresourcemanager", - "version": "v2beta1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$discovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudshell:v1alpha1", - "name": "cloudshell", - "version": "v1alpha1", - "title": "Cloud Shell API", - "description": "Allows users to start, configure, and connect to interactive shell sessions running in the cloud.", - "discoveryRestUrl": "https://cloudshell.googleapis.com/$discovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/shell/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudshell:v1", - "name": "cloudshell", - "version": "v1", - "title": "Cloud Shell API", - "description": "Allows users to start, configure, and connect to interactive shell sessions running in the cloud.", - "discoveryRestUrl": "https://cloudshell.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/shell/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtasks:v2beta2", - "name": "cloudtasks", - "version": "v2beta2", - "title": "Cloud Tasks API", - "description": "Manages the execution of large numbers of distributed requests.", - "discoveryRestUrl": "https://cloudtasks.googleapis.com/$discovery/rest?version=v2beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tasks/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtasks:v2beta3", - "name": "cloudtasks", - "version": "v2beta3", - "title": "Cloud Tasks API", - "description": "Manages the execution of large numbers of distributed requests.", - "discoveryRestUrl": "https://cloudtasks.googleapis.com/$discovery/rest?version=v2beta3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tasks/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v2alpha1", - "name": "cloudtrace", - "version": "v2alpha1", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$discovery/rest?version=v2alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v1", - "name": "cloudtrace", - "version": "v1", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v2", - "name": "cloudtrace", - "version": "v2", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "composer:v1", - "name": "composer", - "version": "v1", - "title": "Cloud Composer API", - "description": "Manages Apache Airflow environments on Google Cloud Platform.", - "discoveryRestUrl": "https://composer.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/composer/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "composer:v1beta1", - "name": "composer", - "version": "v1beta1", - "title": "Cloud Composer API", - "description": "Manages Apache Airflow environments on Google Cloud Platform.", - "discoveryRestUrl": "https://composer.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/composer/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:alpha", - "name": "compute", - "version": "alpha", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/alpha/rest", - "discoveryLink": "./apis/compute/alpha/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:beta", - "name": "compute", - "version": "beta", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/beta/rest", - "discoveryLink": "./apis/compute/beta/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:v1", - "name": "compute", - "version": "v1", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", - "discoveryLink": "./apis/compute/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "container:v1", - "name": "container", - "version": "v1", - "title": "Kubernetes Engine API", - "description": "The Google Kubernetes Engine API is used for building and managing container based applications, powered by the open source Kubernetes technology.", - "discoveryRestUrl": "https://container.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/container-engine/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "container:v1beta1", - "name": "container", - "version": "v1beta1", - "title": "Kubernetes Engine API", - "description": "The Google Kubernetes Engine API is used for building and managing container based applications, powered by the open source Kubernetes technology.", - "discoveryRestUrl": "https://container.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/container-engine/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "content:v2", - "name": "content", - "version": "v2", - "title": "Content API for Shopping", - "description": "Manages product items, inventory, and Merchant Center accounts for Google Shopping.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/content/v2/rest", - "discoveryLink": "./apis/content/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/shopping-content", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "customsearch:v1", - "name": "customsearch", - "version": "v1", - "title": "CustomSearch API", - "description": "Searches over a website or collection of websites", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/customsearch/v1/rest", - "discoveryLink": "./apis/customsearch/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/custom-search/v1/using_rest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataflow:v1b3", - "name": "dataflow", - "version": "v1b3", - "title": "Dataflow API", - "description": "Manages Google Cloud Dataflow projects on Google Cloud Platform.", - "discoveryRestUrl": "https://dataflow.googleapis.com/$discovery/rest?version=v1b3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataflow", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataproc:v1", - "name": "dataproc", - "version": "v1", - "title": "Cloud Dataproc API", - "description": "Manages Hadoop-based clusters and jobs on Google Cloud Platform.", - "discoveryRestUrl": "https://dataproc.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataproc/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataproc:v1beta2", - "name": "dataproc", - "version": "v1beta2", - "title": "Cloud Dataproc API", - "description": "Manages Hadoop-based clusters and jobs on Google Cloud Platform.", - "discoveryRestUrl": "https://dataproc.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataproc/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1", - "name": "datastore", - "version": "v1", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1beta1", - "name": "datastore", - "version": "v1beta1", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1beta3", - "name": "datastore", - "version": "v1beta3", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$discovery/rest?version=v1beta3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:alpha", - "name": "deploymentmanager", - "version": "alpha", - "title": "Google Cloud Deployment Manager Alpha API", - "description": "The Deployment Manager API allows users to declaratively configure, deploy and run complex solutions on the Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/alpha/rest", - "discoveryLink": "./apis/deploymentmanager/alpha/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:v2beta", - "name": "deploymentmanager", - "version": "v2beta", - "title": "Google Cloud Deployment Manager API V2Beta Methods", - "description": "The Deployment Manager API allows users to declaratively configure, deploy and run complex solutions on the Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/v2beta/rest", - "discoveryLink": "./apis/deploymentmanager/v2beta/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/deployment-manager/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:v2", - "name": "deploymentmanager", - "version": "v2", - "title": "Google Cloud Deployment Manager API", - "description": "Declares, configures, and deploys complex solutions on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/v2/rest", - "discoveryLink": "./apis/deploymentmanager/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v2.8", - "name": "dfareporting", - "version": "v2.8", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v2.8/rest", - "discoveryLink": "./apis/dfareporting/v2.8/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.0", - "name": "dfareporting", - "version": "v3.0", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.0/rest", - "discoveryLink": "./apis/dfareporting/v3.0/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.1", - "name": "dfareporting", - "version": "v3.1", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.1/rest", - "discoveryLink": "./apis/dfareporting/v3.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.2", - "name": "dfareporting", - "version": "v3.2", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.2/rest", - "discoveryLink": "./apis/dfareporting/v3.2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dialogflow:v2", - "name": "dialogflow", - "version": "v2", - "title": "Dialogflow API", - "description": "Builds conversational interfaces (for example, chatbots, and voice-powered apps and devices).", - "discoveryRestUrl": "https://dialogflow.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dialogflow-enterprise/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dialogflow:v2beta1", - "name": "dialogflow", - "version": "v2beta1", - "title": "Dialogflow API", - "description": "Builds conversational interfaces (for example, chatbots, and voice-powered apps and devices).", - "discoveryRestUrl": "https://dialogflow.googleapis.com/$discovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dialogflow-enterprise/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "digitalassetlinks:v1", - "name": "digitalassetlinks", - "version": "v1", - "title": "Digital Asset Links API", - "description": "Discovers relationships between online assets such as websites or mobile apps.", - "discoveryRestUrl": "https://digitalassetlinks.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/digital-asset-links/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "discovery:v1", - "name": "discovery", - "version": "v1", - "title": "APIs Discovery Service", - "description": "Provides information about other Google APIs, such as what APIs are available, the resource, and method details for each API.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/discovery/v1/rest", - "discoveryLink": "./apis/discovery/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/feature/filing_cabinet_search-g16.png", - "x32": "http://www.google.com/images/icons/feature/filing_cabinet_search-g32.png" - }, - "documentationLink": "https://developers.google.com/discovery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dlp:v2", - "name": "dlp", - "version": "v2", - "title": "Cloud Data Loss Prevention (DLP) API", - "description": "Provides methods for detection, risk analysis, and de-identification of privacy-sensitive fragments in text, images, and Google Cloud Platform storage repositories.", - "discoveryRestUrl": "https://dlp.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dlp/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v1", - "name": "dns", - "version": "v1", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v1/rest", - "discoveryLink": "./apis/dns/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v1beta2", - "name": "dns", - "version": "v1beta2", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v1beta2/rest", - "discoveryLink": "./apis/dns/v1beta2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v2beta1", - "name": "dns", - "version": "v2beta1", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v2beta1/rest", - "discoveryLink": "./apis/dns/v2beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "doubleclickbidmanager:v1", - "name": "doubleclickbidmanager", - "version": "v1", - "title": "DoubleClick Bid Manager API", - "description": "API for viewing and managing your reports in DoubleClick Bid Manager.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/doubleclickbidmanager/v1/rest", - "discoveryLink": "./apis/doubleclickbidmanager/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/bid-manager/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "doubleclicksearch:v2", - "name": "doubleclicksearch", - "version": "v2", - "title": "DoubleClick Search API", - "description": "Reports and modifies your advertising data in DoubleClick Search (for example, campaigns, ad groups, keywords, and conversions).", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/doubleclicksearch/v2/rest", - "discoveryLink": "./apis/doubleclicksearch/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/doubleclick-search/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "drive:v2", - "name": "drive", - "version": "v2", - "title": "Drive API", - "description": "Manages files in Drive including uploading, downloading, searching, detecting changes, and updating sharing permissions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/drive/v2/rest", - "discoveryLink": "./apis/drive/v2/rest", - "icons": { - "x16": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_16.png", - "x32": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_32.png" - }, - "documentationLink": "https://developers.google.com/drive/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "drive:v3", - "name": "drive", - "version": "v3", - "title": "Drive API", - "description": "Manages files in Drive including uploading, downloading, searching, detecting changes, and updating sharing permissions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", - "discoveryLink": "./apis/drive/v3/rest", - "icons": { - "x16": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_16.png", - "x32": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_32.png" - }, - "documentationLink": "https://developers.google.com/drive/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "file:v1beta1", - "name": "file", - "version": "v1beta1", - "title": "Cloud Filestore API", - "description": "The Cloud Filestore API is used for creating and managing cloud file servers.", - "discoveryRestUrl": "https://file.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/filestore/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebasedynamiclinks:v1", - "name": "firebasedynamiclinks", - "version": "v1", - "title": "Firebase Dynamic Links API", - "description": "Programmatically creates and manages Firebase Dynamic Links.", - "discoveryRestUrl": "https://firebasedynamiclinks.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/dynamic-links/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebasehosting:v1beta1", - "name": "firebasehosting", - "version": "v1beta1", - "title": "Firebase Hosting API", - "description": "", - "discoveryRestUrl": "https://firebasehosting.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/hosting/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebaserules:v1", - "name": "firebaserules", - "version": "v1", - "title": "Firebase Rules API", - "description": "Creates and manages rules that determine when a Firebase Rules-enabled service should permit a request.", - "discoveryRestUrl": "https://firebaserules.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/storage/security", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1", - "name": "firestore", - "version": "v1", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1beta1", - "name": "firestore", - "version": "v1beta1", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1beta2", - "name": "firestore", - "version": "v1beta2", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "fitness:v1", - "name": "fitness", - "version": "v1", - "title": "Fitness", - "description": "Stores and accesses user data in the fitness store from apps on any platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fitness/v1/rest", - "discoveryLink": "./apis/fitness/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fit/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "fusiontables:v1", - "name": "fusiontables", - "version": "v1", - "title": "Fusion Tables API", - "description": "API for working with Fusion Tables data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fusiontables/v1/rest", - "discoveryLink": "./apis/fusiontables/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fusiontables", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "fusiontables:v2", - "name": "fusiontables", - "version": "v2", - "title": "Fusion Tables API", - "description": "API for working with Fusion Tables data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fusiontables/v2/rest", - "discoveryLink": "./apis/fusiontables/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fusiontables", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "games:v1", - "name": "games", - "version": "v1", - "title": "Google Play Game Services API", - "description": "The API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/games/v1/rest", - "discoveryLink": "./apis/games/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gamesConfiguration:v1configuration", - "name": "gamesConfiguration", - "version": "v1configuration", - "title": "Google Play Game Services Publishing API", - "description": "The Publishing API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gamesConfiguration/v1configuration/rest", - "discoveryLink": "./apis/gamesConfiguration/v1configuration/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gamesManagement:v1management", - "name": "gamesManagement", - "version": "v1management", - "title": "Google Play Game Services Management API", - "description": "The Management API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gamesManagement/v1management/rest", - "discoveryLink": "./apis/gamesManagement/v1management/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v1alpha2", - "name": "genomics", - "version": "v1alpha2", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$discovery/rest?version=v1alpha2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v2alpha1", - "name": "genomics", - "version": "v2alpha1", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$discovery/rest?version=v2alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v1", - "name": "genomics", - "version": "v1", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gmail:v1", - "name": "gmail", - "version": "v1", - "title": "Gmail API", - "description": "Access Gmail mailboxes including sending user email.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", - "discoveryLink": "./apis/gmail/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/googlemail-16.png", - "x32": "https://www.google.com/images/icons/product/googlemail-32.png" - }, - "documentationLink": "https://developers.google.com/gmail/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "groupsmigration:v1", - "name": "groupsmigration", - "version": "v1", - "title": "Groups Migration API", - "description": "Groups Migration Api.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/groupsmigration/v1/rest", - "discoveryLink": "./apis/groupsmigration/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/discussions-16.gif", - "x32": "https://www.google.com/images/icons/product/discussions-32.gif" - }, - "documentationLink": "https://developers.google.com/google-apps/groups-migration/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "groupssettings:v1", - "name": "groupssettings", - "version": "v1", - "title": "Groups Settings API", - "description": "Lets you manage permission levels and related settings of a group.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/groupssettings/v1/rest", - "discoveryLink": "./apis/groupssettings/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/groups-settings/get_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iam:v1", - "name": "iam", - "version": "v1", - "title": "Identity and Access Management (IAM) API", - "description": "Manages identity and access control for Google Cloud Platform resources, including the creation of service accounts, which you can use to authenticate to Google and make API calls.", - "discoveryRestUrl": "https://iam.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iam/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iamcredentials:v1", - "name": "iamcredentials", - "version": "v1", - "title": "IAM Service Account Credentials API", - "description": "Creates short-lived, limited-privilege credentials for IAM service accounts.", - "discoveryRestUrl": "https://iamcredentials.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iap:v1beta1", - "name": "iap", - "version": "v1beta1", - "title": "Cloud Identity-Aware Proxy API", - "description": "Controls access to cloud applications running on Google Cloud Platform.", - "discoveryRestUrl": "https://iap.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iap", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "identitytoolkit:v3", - "name": "identitytoolkit", - "version": "v3", - "title": "Google Identity Toolkit API", - "description": "Help the third party sites to implement federated login.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/identitytoolkit/v3/rest", - "discoveryLink": "./apis/identitytoolkit/v3/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/identity-toolkit/v3/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "indexing:v3", - "name": "indexing", - "version": "v3", - "title": "Indexing API", - "description": "Notifies Google when your web pages change.", - "discoveryRestUrl": "https://indexing.googleapis.com/$discovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/search/apis/indexing-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v3p1beta1", - "name": "jobs", - "version": "v3p1beta1", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$discovery/rest?version=v3p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v2", - "name": "jobs", - "version": "v2", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v3", - "name": "jobs", - "version": "v3", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$discovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "kgsearch:v1", - "name": "kgsearch", - "version": "v1", - "title": "Knowledge Graph Search API", - "description": "Searches the Google Knowledge Graph for entities.", - "discoveryRestUrl": "https://kgsearch.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/knowledge-graph/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1", - "name": "language", - "version": "v1", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1beta1", - "name": "language", - "version": "v1beta1", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1beta2", - "name": "language", - "version": "v1beta2", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "licensing:v1", - "name": "licensing", - "version": "v1", - "title": "Enterprise License Manager API", - "description": "Views and manages licenses for your domain.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/licensing/v1/rest", - "discoveryLink": "./apis/licensing/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/licensing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "logging:v2", - "name": "logging", - "version": "v2", - "title": "Stackdriver Logging API", - "description": "Writes log entries and manages your Logging configuration.", - "discoveryRestUrl": "https://logging.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/logging/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "logging:v2beta1", - "name": "logging", - "version": "v2beta1", - "title": "Stackdriver Logging API", - "description": "Writes log entries and manages your Logging configuration.", - "discoveryRestUrl": "https://logging.googleapis.com/$discovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/logging/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "manufacturers:v1", - "name": "manufacturers", - "version": "v1", - "title": "Manufacturer Center API", - "description": "Public API for managing Manufacturer Center related data.", - "discoveryRestUrl": "https://manufacturers.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/manufacturers/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "mirror:v1", - "name": "mirror", - "version": "v1", - "title": "Google Mirror API", - "description": "Interacts with Glass users via the timeline.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/mirror/v1/rest", - "discoveryLink": "./apis/mirror/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/glass", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "ml:v1", - "name": "ml", - "version": "v1", - "title": "Cloud Machine Learning Engine", - "description": "An API to enable creating and using machine learning models.", - "discoveryRestUrl": "https://ml.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/ml/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "monitoring:v3", - "name": "monitoring", - "version": "v3", - "title": "Stackdriver Monitoring API", - "description": "Manages your Stackdriver Monitoring data and configurations. Most projects must be associated with a Stackdriver account, with a few exceptions as noted on the individual method pages.", - "discoveryRestUrl": "https://monitoring.googleapis.com/$discovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/monitoring/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "oauth2:v1", - "name": "oauth2", - "version": "v1", - "title": "Google OAuth2 API", - "description": "Obtains end-user authorization grants for use with other Google APIs.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest", - "discoveryLink": "./apis/oauth2/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/accounts/docs/OAuth2", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oauth2:v2", - "name": "oauth2", - "version": "v2", - "title": "Google OAuth2 API", - "description": "Obtains end-user authorization grants for use with other Google APIs.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest", - "discoveryLink": "./apis/oauth2/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/accounts/docs/OAuth2", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1alpha", - "name": "oslogin", - "version": "v1alpha", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$discovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1beta", - "name": "oslogin", - "version": "v1beta", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$discovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1", - "name": "oslogin", - "version": "v1", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v1", - "name": "pagespeedonline", - "version": "v1", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v1/rest", - "discoveryLink": "./apis/pagespeedonline/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v1/getting_started", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v2", - "name": "pagespeedonline", - "version": "v2", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v2/rest", - "discoveryLink": "./apis/pagespeedonline/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v2/getting-started", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v4", - "name": "pagespeedonline", - "version": "v4", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v4/rest", - "discoveryLink": "./apis/pagespeedonline/v4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v4/getting-started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "partners:v2", - "name": "partners", - "version": "v2", - "title": "Google Partners API", - "description": "Searches certified companies and creates contact leads with them, and also audits the usage of clients.", - "discoveryRestUrl": "https://partners.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/partners/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "people:v1", - "name": "people", - "version": "v1", - "title": "People API", - "description": "Provides access to information about profiles and contacts.", - "discoveryRestUrl": "https://people.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/people/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "playcustomapp:v1", - "name": "playcustomapp", - "version": "v1", - "title": "Google Play Custom App Publishing API", - "description": "An API to publish custom Android apps.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/playcustomapp/v1/rest", - "discoveryLink": "./apis/playcustomapp/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/android/work/play/custom-app-api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "plus:v1", - "name": "plus", - "version": "v1", - "title": "Google+ API", - "description": "Builds on top of the Google+ platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest", - "discoveryLink": "./apis/plus/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/gplus-16.png", - "x32": "http://www.google.com/images/icons/product/gplus-32.png" - }, - "documentationLink": "https://developers.google.com/+/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "plusDomains:v1", - "name": "plusDomains", - "version": "v1", - "title": "Google+ Domains API", - "description": "Builds on top of the Google+ platform for Google Apps Domains.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/plusDomains/v1/rest", - "discoveryLink": "./apis/plusDomains/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/gplus-16.png", - "x32": "http://www.google.com/images/icons/product/gplus-32.png" - }, - "documentationLink": "https://developers.google.com/+/domains/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "poly:v1", - "name": "poly", - "version": "v1", - "title": "Poly API", - "description": "The Poly API provides read access to assets hosted on poly.google.com to all, and upload access to poly.google.com for whitelisted accounts.", - "discoveryRestUrl": "https://poly.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/poly/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "proximitybeacon:v1beta1", - "name": "proximitybeacon", - "version": "v1beta1", - "title": "Proximity Beacon API", - "description": "Registers, manages, indexes, and searches beacons.", - "discoveryRestUrl": "https://proximitybeacon.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/beacons/proximity/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1beta1a", - "name": "pubsub", - "version": "v1beta1a", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$discovery/rest?version=v1beta1a", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1", - "name": "pubsub", - "version": "v1", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1beta2", - "name": "pubsub", - "version": "v1beta2", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "redis:v1", - "name": "redis", - "version": "v1", - "title": "Google Cloud Memorystore for Redis API", - "description": "The Google Cloud Memorystore for Redis API is used for creating and managing Redis instances on the Google Cloud Platform.", - "discoveryRestUrl": "https://redis.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/memorystore/docs/redis/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "redis:v1beta1", - "name": "redis", - "version": "v1beta1", - "title": "Google Cloud Memorystore for Redis API", - "description": "The Google Cloud Memorystore for Redis API is used for creating and managing Redis instances on the Google Cloud Platform.", - "discoveryRestUrl": "https://redis.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/memorystore/docs/redis/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "replicapool:v1beta1", - "name": "replicapool", - "version": "v1beta1", - "title": "Replica Pool API", - "description": "The Replica Pool API allows users to declaratively provision and manage groups of Google Compute Engine instances based on a common template.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/replicapool/v1beta1/rest", - "discoveryLink": "./apis/replicapool/v1beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/replica-pool/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "replicapoolupdater:v1beta1", - "name": "replicapoolupdater", - "version": "v1beta1", - "title": "Google Compute Engine Instance Group Updater API", - "description": "[Deprecated. Please use compute.instanceGroupManagers.update method. replicapoolupdater API will be disabled after December 30th, 2016] Updates groups of Compute Engine instances.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/replicapoolupdater/v1beta1/rest", - "discoveryLink": "./apis/replicapoolupdater/v1beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/instance-groups/manager/#applying_rolling_updates_using_the_updater_service", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "reseller:v1", - "name": "reseller", - "version": "v1", - "title": "Enterprise Apps Reseller API", - "description": "Creates and manages your customers and their subscriptions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/reseller/v1/rest", - "discoveryLink": "./apis/reseller/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/reseller/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "runtimeconfig:v1", - "name": "runtimeconfig", - "version": "v1", - "title": "Cloud Runtime Configuration API", - "description": "The Runtime Configurator allows you to dynamically configure and expose variables through Google Cloud Platform. In addition, you can also set Watchers and Waiters that will watch for changes to your data and return based on certain conditions.", - "discoveryRestUrl": "https://runtimeconfig.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/runtime-configurator/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "runtimeconfig:v1beta1", - "name": "runtimeconfig", - "version": "v1beta1", - "title": "Cloud Runtime Configuration API", - "description": "The Runtime Configurator allows you to dynamically configure and expose variables through Google Cloud Platform. In addition, you can also set Watchers and Waiters that will watch for changes to your data and return based on certain conditions.", - "discoveryRestUrl": "https://runtimeconfig.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/runtime-configurator/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "safebrowsing:v4", - "name": "safebrowsing", - "version": "v4", - "title": "Safe Browsing API", - "description": "Enables client applications to check web resources (most commonly URLs) against Google-generated lists of unsafe web resources.", - "discoveryRestUrl": "https://safebrowsing.googleapis.com/$discovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/safe-browsing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "script:v1", - "name": "script", - "version": "v1", - "title": "Apps Script API", - "description": "Manages and executes Google Apps Script projects.", - "discoveryRestUrl": "https://script.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/apps-script/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "searchconsole:v1", - "name": "searchconsole", - "version": "v1", - "title": "Google Search Console URL Testing Tools API", - "description": "Provides tools for running validation tests against single URLs", - "discoveryRestUrl": "https://searchconsole.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/webmaster-tools/search-console-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1alpha1", - "name": "servicebroker", - "version": "v1alpha1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$discovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1", - "name": "servicebroker", - "version": "v1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1beta1", - "name": "servicebroker", - "version": "v1beta1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "serviceconsumermanagement:v1", - "name": "serviceconsumermanagement", - "version": "v1", - "title": "Service Consumer Management API", - "description": "Manages the service consumers of a Service Infrastructure service.", - "discoveryRestUrl": "https://serviceconsumermanagement.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-consumer-management/docs/overview", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicecontrol:v1", - "name": "servicecontrol", - "version": "v1", - "title": "Service Control API", - "description": "Provides control plane functionality to managed services, such as logging, monitoring, and status checks.", - "discoveryRestUrl": "https://servicecontrol.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-control/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicemanagement:v1", - "name": "servicemanagement", - "version": "v1", - "title": "Service Management API", - "description": "Google Service Management allows service producers to publish their services on Google Cloud Platform so that they can be discovered and used by service consumers.", - "discoveryRestUrl": "https://servicemanagement.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-management/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicenetworking:v1beta", - "name": "servicenetworking", - "version": "v1beta", - "title": "Service Networking API", - "description": "Provides automatic management of network configurations necessary for certain services.", - "discoveryRestUrl": "https://servicenetworking.googleapis.com/$discovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-infrastructure/docs/service-networking/getting-started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "serviceusage:v1", - "name": "serviceusage", - "version": "v1", - "title": "Service Usage API", - "description": "Enables services that service consumers want to use on Google Cloud Platform, lists the available or enabled services, or disables services that service consumers no longer use.", - "discoveryRestUrl": "https://serviceusage.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-usage/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "serviceusage:v1beta1", - "name": "serviceusage", - "version": "v1beta1", - "title": "Service Usage API", - "description": "Enables services that service consumers want to use on Google Cloud Platform, lists the available or enabled services, or disables services that service consumers no longer use.", - "discoveryRestUrl": "https://serviceusage.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-usage/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "sheets:v4", - "name": "sheets", - "version": "v4", - "title": "Google Sheets API", - "description": "Reads and writes Google Sheets.", - "discoveryRestUrl": "https://sheets.googleapis.com/$discovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/sheets/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "siteVerification:v1", - "name": "siteVerification", - "version": "v1", - "title": "Google Site Verification API", - "description": "Verifies ownership of websites or domains with Google.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/siteVerification/v1/rest", - "discoveryLink": "./apis/siteVerification/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/site-verification/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "slides:v1", - "name": "slides", - "version": "v1", - "title": "Google Slides API", - "description": "An API for creating and editing Google Slides presentations.", - "discoveryRestUrl": "https://slides.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/slides/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "sourcerepo:v1", - "name": "sourcerepo", - "version": "v1", - "title": "Cloud Source Repositories API", - "description": "Access source code repositories hosted by Google.", - "discoveryRestUrl": "https://sourcerepo.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/source-repositories/docs/apis", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "spanner:v1", - "name": "spanner", - "version": "v1", - "title": "Cloud Spanner API", - "description": "Cloud Spanner is a managed, mission-critical, globally consistent and scalable relational database service.", - "discoveryRestUrl": "https://spanner.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/spanner/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "speech:v1", - "name": "speech", - "version": "v1", - "title": "Cloud Speech API", - "description": "Converts audio to text by applying powerful neural network models.", - "discoveryRestUrl": "https://speech.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/speech-to-text/docs/quickstart-protocol", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "speech:v1beta1", - "name": "speech", - "version": "v1beta1", - "title": "Cloud Speech API", - "description": "Converts audio to text by applying powerful neural network models.", - "discoveryRestUrl": "https://speech.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/speech-to-text/docs/quickstart-protocol", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "sqladmin:v1beta4", - "name": "sqladmin", - "version": "v1beta4", - "title": "Cloud SQL Admin API", - "description": "Creates and manages Cloud SQL instances, which provide fully managed MySQL or PostgreSQL databases.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/sqladmin/v1beta4/rest", - "discoveryLink": "./apis/sqladmin/v1beta4/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/sql/docs/reference/latest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1", - "name": "storage", - "version": "v1", - "title": "Cloud Storage JSON API", - "description": "Stores and retrieves potentially large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1/rest", - "discoveryLink": "./apis/storage/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1beta1", - "name": "storage", - "version": "v1beta1", - "title": "Cloud Storage JSON API", - "description": "Lets you store and retrieve potentially-large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1beta1/rest", - "discoveryLink": "./apis/storage/v1beta1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1beta2", - "name": "storage", - "version": "v1beta2", - "title": "Cloud Storage JSON API", - "description": "Lets you store and retrieve potentially-large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1beta2/rest", - "discoveryLink": "./apis/storage/v1beta2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "storagetransfer:v1", - "name": "storagetransfer", - "version": "v1", - "title": "Storage Transfer API", - "description": "Transfers data from external data sources to a Google Cloud Storage bucket or between Google Cloud Storage buckets.", - "discoveryRestUrl": "https://storagetransfer.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/storage/transfer", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "streetviewpublish:v1", - "name": "streetviewpublish", - "version": "v1", - "title": "Street View Publish API", - "description": "Publishes 360 photos to Google Maps, along with position, orientation, and connectivity metadata. Apps can offer an interface for positioning, connecting, and uploading user-generated Street View images.", - "discoveryRestUrl": "https://streetviewpublish.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/streetview/publish/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "surveys:v2", - "name": "surveys", - "version": "v2", - "title": "Surveys API", - "description": "Creates and conducts surveys, lists the surveys that an authenticated user owns, and retrieves survey results and information about specified surveys.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/surveys/v2/rest", - "discoveryLink": "./apis/surveys/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tagmanager:v1", - "name": "tagmanager", - "version": "v1", - "title": "Tag Manager API", - "description": "Accesses Tag Manager accounts and containers.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tagmanager/v1/rest", - "discoveryLink": "./apis/tagmanager/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/tag-manager/api/v1/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "tagmanager:v2", - "name": "tagmanager", - "version": "v2", - "title": "Tag Manager API", - "description": "Accesses Tag Manager accounts and containers.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tagmanager/v2/rest", - "discoveryLink": "./apis/tagmanager/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/tag-manager/api/v2/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tasks:v1", - "name": "tasks", - "version": "v1", - "title": "Tasks API", - "description": "Lets you manage your tasks and task lists.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tasks/v1/rest", - "discoveryLink": "./apis/tasks/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/tasks-16.png", - "x32": "https://www.google.com/images/icons/product/tasks-32.png" - }, - "documentationLink": "https://developers.google.com/google-apps/tasks/firstapp", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "testing:v1", - "name": "testing", - "version": "v1", - "title": "Cloud Testing API", - "description": "Allows developers to run automated tests for their mobile applications on Google infrastructure.", - "discoveryRestUrl": "https://testing.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-test-lab/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "texttospeech:v1", - "name": "texttospeech", - "version": "v1", - "title": "Cloud Text-to-Speech API", - "description": "Synthesizes natural-sounding speech by applying powerful neural network models.", - "discoveryRestUrl": "https://texttospeech.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/text-to-speech/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "texttospeech:v1beta1", - "name": "texttospeech", - "version": "v1beta1", - "title": "Cloud Text-to-Speech API", - "description": "Synthesizes natural-sounding speech by applying powerful neural network models.", - "discoveryRestUrl": "https://texttospeech.googleapis.com/$discovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/text-to-speech/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "toolresults:v1beta3", - "name": "toolresults", - "version": "v1beta3", - "title": "Cloud Tool Results API", - "description": "Reads and publishes results from Firebase Test Lab.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/toolresults/v1beta3/rest", - "discoveryLink": "./apis/toolresults/v1beta3/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/test-lab/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tpu:v1alpha1", - "name": "tpu", - "version": "v1alpha1", - "title": "Cloud TPU API", - "description": "TPU API provides customers with access to Google TPU technology.", - "discoveryRestUrl": "https://tpu.googleapis.com/$discovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tpu/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "tpu:v1", - "name": "tpu", - "version": "v1", - "title": "Cloud TPU API", - "description": "TPU API provides customers with access to Google TPU technology.", - "discoveryRestUrl": "https://tpu.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tpu/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "translate:v2", - "name": "translate", - "version": "v2", - "title": "Cloud Translation API", - "description": "Integrates text translation into your website or application.", - "discoveryRestUrl": "https://translation.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://code.google.com/apis/language/translate/v2/getting_started.html", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "urlshortener:v1", - "name": "urlshortener", - "version": "v1", - "title": "URL Shortener API", - "description": "Lets you create, inspect, and manage goo.gl short URLs", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/urlshortener/v1/rest", - "discoveryLink": "./apis/urlshortener/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/url-shortener/v1/getting_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "vault:v1", - "name": "vault", - "version": "v1", - "title": "G Suite Vault API", - "description": "Archiving and eDiscovery for G Suite.", - "discoveryRestUrl": "https://vault.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/vault", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1p1beta1", - "name": "videointelligence", - "version": "v1p1beta1", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$discovery/rest?version=v1p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1", - "name": "videointelligence", - "version": "v1", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1beta2", - "name": "videointelligence", - "version": "v1beta2", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$discovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1p1beta1", - "name": "vision", - "version": "v1p1beta1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$discovery/rest?version=v1p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1p2beta1", - "name": "vision", - "version": "v1p2beta1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$discovery/rest?version=v1p2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1", - "name": "vision", - "version": "v1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "webfonts:v1", - "name": "webfonts", - "version": "v1", - "title": "Google Fonts Developer API", - "description": "Accesses the metadata for all families served by Google Fonts, providing a list of families currently available (including available styles and a list of supported script subsets).", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/webfonts/v1/rest", - "discoveryLink": "./apis/webfonts/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/feature/font_api-16.png", - "x32": "https://www.google.com/images/icons/feature/font_api-32.gif" - }, - "documentationLink": "https://developers.google.com/fonts/docs/developer_api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "webmasters:v3", - "name": "webmasters", - "version": "v3", - "title": "Search Console API", - "description": "View Google Search Console data for your verified sites.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/webmasters/v3/rest", - "discoveryLink": "./apis/webmasters/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/webmaster_tools-16.png", - "x32": "https://www.google.com/images/icons/product/webmaster_tools-32.png" - }, - "documentationLink": "https://developers.google.com/webmaster-tools/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "websecurityscanner:v1alpha", - "name": "websecurityscanner", - "version": "v1alpha", - "title": "Web Security Scanner API", - "description": "Web Security Scanner API (under development).", - "discoveryRestUrl": "https://websecurityscanner.googleapis.com/$discovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/security-scanner/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "websecurityscanner:v1beta", - "name": "websecurityscanner", - "version": "v1beta", - "title": "Web Security Scanner API", - "description": "Web Security Scanner API (under development).", - "discoveryRestUrl": "https://websecurityscanner.googleapis.com/$discovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/security-scanner/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtube:v3", - "name": "youtube", - "version": "v3", - "title": "YouTube Data API", - "description": "Supports core YouTube features, such as uploading videos, creating and managing playlists, searching for content, and much more.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest", - "discoveryLink": "./apis/youtube/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "https://developers.google.com/youtube/v3", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v1", - "name": "youtubeAnalytics", - "version": "v1", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtubeAnalytics/v1/rest", - "discoveryLink": "./apis/youtubeAnalytics/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "http://developers.google.com/youtube/analytics/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v1beta1", - "name": "youtubeAnalytics", - "version": "v1beta1", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtubeAnalytics/v1beta1/rest", - "discoveryLink": "./apis/youtubeAnalytics/v1beta1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "http://developers.google.com/youtube/analytics/", - "labels": [ - "deprecated" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v2", - "name": "youtubeAnalytics", - "version": "v2", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://youtubeanalytics.googleapis.com/$discovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/youtube/analytics", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "youtubereporting:v1", - "name": "youtubereporting", - "version": "v1", - "title": "YouTube Reporting API", - "description": "Schedules reporting jobs containing your YouTube Analytics data and downloads the resulting bulk data reports in the form of CSV files.", - "discoveryRestUrl": "https://youtubereporting.googleapis.com/$discovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/youtube/reporting/v1/reports/", - "preferred": true - } - ] -} -_END -} - -sub pre_get_gmail_spec_json -{ - return <<'__END' -{ - "kind": "discovery#restDescription", - "etag": "\"J3WqvAcMk4eQjJXvfSI4Yr8VouA/zhBQnUVXp-QQ6Y6QUt5UiGZD_sA\"", - "discoveryVersion": "v1", - "id": "gmail:v1", - "name": "gmail", - "version": "v1", - "revision": "20180904", - "title": "Gmail API", - "description": "Access Gmail mailboxes including sending user email.", - "ownerDomain": "google.com", - "ownerName": "Google", - "icons": { - "x16": "https://www.google.com/images/icons/product/googlemail-16.png", - "x32": "https://www.google.com/images/icons/product/googlemail-32.png" - }, - "documentationLink": "https://developers.google.com/gmail/api/", - "protocol": "rest", - "baseUrl": "https://www.googleapis.com/gmail/v1/users/", - "basePath": "/gmail/v1/users/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "gmail/v1/users/", - "batchPath": "batch/gmail/v1", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "quotaUser": { - "type": "string", - "description": "An opaque string that represents a user for quota purposes. Must not exceed 40 characters.", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "Deprecated. Please use quotaUser instead.", - "location": "query" - } - }, - "auth": { - "oauth2": { - "scopes": { - "https://mail.google.com/": { - "description": "Read, send, delete, and manage your email" - }, - "https://www.googleapis.com/auth/gmail.compose": { - "description": "Manage drafts and send emails" - }, - "https://www.googleapis.com/auth/gmail.insert": { - "description": "Insert mail into your mailbox" - }, - "https://www.googleapis.com/auth/gmail.labels": { - "description": "Manage mailbox labels" - }, - "https://www.googleapis.com/auth/gmail.metadata": { - "description": "View your email message metadata such as labels and headers, but not the email body" - }, - "https://www.googleapis.com/auth/gmail.modify": { - "description": "View and modify but not delete your email" - }, - "https://www.googleapis.com/auth/gmail.readonly": { - "description": "View your email messages and settings" - }, - "https://www.googleapis.com/auth/gmail.send": { - "description": "Send email on your behalf" - }, - "https://www.googleapis.com/auth/gmail.settings.basic": { - "description": "Manage your basic mail settings" - }, - "https://www.googleapis.com/auth/gmail.settings.sharing": { - "description": "Manage your sensitive mail settings, including who can manage your mail" - } - } - } - }, - "schemas": { - "AutoForwarding": { - "id": "AutoForwarding", - "type": "object", - "description": "Auto-forwarding settings for an account.", - "properties": { - "disposition": { - "type": "string", - "description": "The state that a message should be left in after it has been forwarded.", - "enum": [ - "archive", - "dispositionUnspecified", - "leaveInInbox", - "markRead", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - }, - "emailAddress": { - "type": "string", - "description": "Email address to which all incoming messages are forwarded. This email address must be a verified member of the forwarding addresses." - }, - "enabled": { - "type": "boolean", - "description": "Whether all incoming mail is automatically forwarded to another address." - } - } - }, - "BatchDeleteMessagesRequest": { - "id": "BatchDeleteMessagesRequest", - "type": "object", - "properties": { - "ids": { - "type": "array", - "description": "The IDs of the messages to delete.", - "items": { - "type": "string" - } - } - } - }, - "BatchModifyMessagesRequest": { - "id": "BatchModifyMessagesRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of label IDs to add to messages.", - "items": { - "type": "string" - } - }, - "ids": { - "type": "array", - "description": "The IDs of the messages to modify. There is a limit of 1000 ids per request.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list of label IDs to remove from messages.", - "items": { - "type": "string" - } - } - } - }, - "Delegate": { - "id": "Delegate", - "type": "object", - "description": "Settings for a delegate. Delegates can read, send, and delete messages, as well as manage contacts, for the delegator's account. See \"Set up mail delegation\" for more information about delegates.", - "properties": { - "delegateEmail": { - "type": "string", - "description": "The email address of the delegate." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified and can act as a delegate for the account. Read-only.", - "enum": [ - "accepted", - "expired", - "pending", - "rejected", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - } - } - }, - "Draft": { - "id": "Draft", - "type": "object", - "description": "A draft email in the user's mailbox.", - "properties": { - "id": { - "type": "string", - "description": "The immutable ID of the draft.", - "annotations": { - "required": [ - "gmail.users.drafts.send" - ] - } - }, - "message": { - "$ref": "Message", - "description": "The message content of the draft." - } - } - }, - "Filter": { - "id": "Filter", - "type": "object", - "description": "Resource definition for Gmail filters. Filters apply to specific messages instead of an entire email thread.", - "properties": { - "action": { - "$ref": "FilterAction", - "description": "Action that the filter performs." - }, - "criteria": { - "$ref": "FilterCriteria", - "description": "Matching criteria for the filter." - }, - "id": { - "type": "string", - "description": "The server assigned ID of the filter." - } - } - }, - "FilterAction": { - "id": "FilterAction", - "type": "object", - "description": "A set of actions to perform on a message.", - "properties": { - "addLabelIds": { - "type": "array", - "description": "List of labels to add to the message.", - "items": { - "type": "string" - } - }, - "forward": { - "type": "string", - "description": "Email address that the message should be forwarded to." - }, - "removeLabelIds": { - "type": "array", - "description": "List of labels to remove from the message.", - "items": { - "type": "string" - } - } - } - }, - "FilterCriteria": { - "id": "FilterCriteria", - "type": "object", - "description": "Message matching criteria.", - "properties": { - "excludeChats": { - "type": "boolean", - "description": "Whether the response should exclude chats." - }, - "from": { - "type": "string", - "description": "The sender's display name or email address." - }, - "hasAttachment": { - "type": "boolean", - "description": "Whether the message has any attachment." - }, - "negatedQuery": { - "type": "string", - "description": "Only return messages not matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\"." - }, - "query": { - "type": "string", - "description": "Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\"." - }, - "size": { - "type": "integer", - "description": "The size of the entire RFC822 message in bytes, including all headers and attachments.", - "format": "int32" - }, - "sizeComparison": { - "type": "string", - "description": "How the message size in bytes should be in relation to the size field.", - "enum": [ - "larger", - "smaller", - "unspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - }, - "subject": { - "type": "string", - "description": "Case-insensitive phrase found in the message's subject. Trailing and leading whitespace are be trimmed and adjacent spaces are collapsed." - }, - "to": { - "type": "string", - "description": "The recipient's display name or email address. Includes recipients in the \"to\", \"cc\", and \"bcc\" header fields. You can use simply the local part of the email address. For example, \"example\" and \"example@\" both match \"example@gmail.com\". This field is case-insensitive." - } - } - }, - "ForwardingAddress": { - "id": "ForwardingAddress", - "type": "object", - "description": "Settings for a forwarding address.", - "properties": { - "forwardingEmail": { - "type": "string", - "description": "An email address to which messages can be forwarded." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified and is usable for forwarding. Read-only.", - "enum": [ - "accepted", - "pending", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - } - } - }, - "History": { - "id": "History", - "type": "object", - "description": "A record of a change to the user's mailbox. Each history change may affect multiple messages in multiple ways.", - "properties": { - "id": { - "type": "string", - "description": "The mailbox sequence ID.", - "format": "uint64" - }, - "labelsAdded": { - "type": "array", - "description": "Labels added to messages in this history record.", - "items": { - "$ref": "HistoryLabelAdded" - } - }, - "labelsRemoved": { - "type": "array", - "description": "Labels removed from messages in this history record.", - "items": { - "$ref": "HistoryLabelRemoved" - } - }, - "messages": { - "type": "array", - "description": "List of messages changed in this history record. The fields for specific change types, such as messagesAdded may duplicate messages in this field. We recommend using the specific change-type fields instead of this.", - "items": { - "$ref": "Message" - } - }, - "messagesAdded": { - "type": "array", - "description": "Messages added to the mailbox in this history record.", - "items": { - "$ref": "HistoryMessageAdded" - } - }, - "messagesDeleted": { - "type": "array", - "description": "Messages deleted (not Trashed) from the mailbox in this history record.", - "items": { - "$ref": "HistoryMessageDeleted" - } - } - } - }, - "HistoryLabelAdded": { - "id": "HistoryLabelAdded", - "type": "object", - "properties": { - "labelIds": { - "type": "array", - "description": "Label IDs added to the message.", - "items": { - "type": "string" - } - }, - "message": { - "$ref": "Message" - } - } - }, - "HistoryLabelRemoved": { - "id": "HistoryLabelRemoved", - "type": "object", - "properties": { - "labelIds": { - "type": "array", - "description": "Label IDs removed from the message.", - "items": { - "type": "string" - } - }, - "message": { - "$ref": "Message" - } - } - }, - "HistoryMessageAdded": { - "id": "HistoryMessageAdded", - "type": "object", - "properties": { - "message": { - "$ref": "Message" - } - } - }, - "HistoryMessageDeleted": { - "id": "HistoryMessageDeleted", - "type": "object", - "properties": { - "message": { - "$ref": "Message" - } - } - }, - "ImapSettings": { - "id": "ImapSettings", - "type": "object", - "description": "IMAP settings for an account.", - "properties": { - "autoExpunge": { - "type": "boolean", - "description": "If this value is true, Gmail will immediately expunge a message when it is marked as deleted in IMAP. Otherwise, Gmail will wait for an update from the client before expunging messages marked as deleted." - }, - "enabled": { - "type": "boolean", - "description": "Whether IMAP is enabled for the account." - }, - "expungeBehavior": { - "type": "string", - "description": "The action that will be executed on a message when it is marked as deleted and expunged from the last visible IMAP folder.", - "enum": [ - "archive", - "deleteForever", - "expungeBehaviorUnspecified", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "maxFolderSize": { - "type": "integer", - "description": "An optional limit on the number of messages that an IMAP folder may contain. Legal values are 0, 1000, 2000, 5000 or 10000. A value of zero is interpreted to mean that there is no limit.", - "format": "int32" - } - } - }, - "Label": { - "id": "Label", - "type": "object", - "description": "Labels are used to categorize messages and threads within the user's mailbox.", - "properties": { - "color": { - "$ref": "LabelColor", - "description": "The color to assign to the label. Color is only available for labels that have their type set to user." - }, - "id": { - "type": "string", - "description": "The immutable ID of the label.", - "annotations": { - "required": [ - "gmail.users.labels.update" - ] - } - }, - "labelListVisibility": { - "type": "string", - "description": "The visibility of the label in the label list in the Gmail web interface.", - "enum": [ - "labelHide", - "labelShow", - "labelShowIfUnread" - ], - "enumDescriptions": [ - "", - "", - "" - ], - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "messageListVisibility": { - "type": "string", - "description": "The visibility of the label in the message list in the Gmail web interface.", - "enum": [ - "hide", - "show" - ], - "enumDescriptions": [ - "", - "" - ], - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "messagesTotal": { - "type": "integer", - "description": "The total number of messages with the label.", - "format": "int32" - }, - "messagesUnread": { - "type": "integer", - "description": "The number of unread messages with the label.", - "format": "int32" - }, - "name": { - "type": "string", - "description": "The display name of the label.", - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "threadsTotal": { - "type": "integer", - "description": "The total number of threads with the label.", - "format": "int32" - }, - "threadsUnread": { - "type": "integer", - "description": "The number of unread threads with the label.", - "format": "int32" - }, - "type": { - "type": "string", - "description": "The owner type for the label. User labels are created by the user and can be modified and deleted by the user and can be applied to any message or thread. System labels are internally created and cannot be added, modified, or deleted. System labels may be able to be applied to or removed from messages and threads under some circumstances but this is not guaranteed. For example, users can apply and remove the INBOX and UNREAD labels from messages and threads, but cannot apply or remove the DRAFTS or SENT labels from messages or threads.", - "enum": [ - "system", - "user" - ], - "enumDescriptions": [ - "", - "" - ] - } - } - }, - "LabelColor": { - "id": "LabelColor", - "type": "object", - "properties": { - "backgroundColor": { - "type": "string", - "description": "The background color represented as hex string #RRGGBB (ex #000000). This field is required in order to set the color of a label. Only the following predefined set of color values are allowed:\n#000000, #434343, #666666, #999999, #cccccc, #efefef, #f3f3f3, #ffffff, #fb4c2f, #ffad47, #fad165, #16a766, #43d692, #4a86e8, #a479e2, #f691b3, #f6c5be, #ffe6c7, #fef1d1, #b9e4d0, #c6f3de, #c9daf8, #e4d7f5, #fcdee8, #efa093, #ffd6a2, #fce8b3, #89d3b2, #a0eac9, #a4c2f4, #d0bcf1, #fbc8d9, #e66550, #ffbc6b, #fcda83, #44b984, #68dfa9, #6d9eeb, #b694e8, #f7a7c0, #cc3a21, #eaa041, #f2c960, #149e60, #3dc789, #3c78d8, #8e63ce, #e07798, #ac2b16, #cf8933, #d5ae49, #0b804b, #2a9c68, #285bac, #653e9b, #b65775, #822111, #a46a21, #aa8831, #076239, #1a764d, #1c4587, #41236d, #83334c" - }, - "textColor": { - "type": "string", - "description": "The text color of the label, represented as hex string. This field is required in order to set the color of a label. Only the following predefined set of color values are allowed:\n#000000, #434343, #666666, #999999, #cccccc, #efefef, #f3f3f3, #ffffff, #fb4c2f, #ffad47, #fad165, #16a766, #43d692, #4a86e8, #a479e2, #f691b3, #f6c5be, #ffe6c7, #fef1d1, #b9e4d0, #c6f3de, #c9daf8, #e4d7f5, #fcdee8, #efa093, #ffd6a2, #fce8b3, #89d3b2, #a0eac9, #a4c2f4, #d0bcf1, #fbc8d9, #e66550, #ffbc6b, #fcda83, #44b984, #68dfa9, #6d9eeb, #b694e8, #f7a7c0, #cc3a21, #eaa041, #f2c960, #149e60, #3dc789, #3c78d8, #8e63ce, #e07798, #ac2b16, #cf8933, #d5ae49, #0b804b, #2a9c68, #285bac, #653e9b, #b65775, #822111, #a46a21, #aa8831, #076239, #1a764d, #1c4587, #41236d, #83334c" - } - } - }, - "ListDelegatesResponse": { - "id": "ListDelegatesResponse", - "type": "object", - "description": "Response for the ListDelegates method.", - "properties": { - "delegates": { - "type": "array", - "description": "List of the user's delegates (with any verification status).", - "items": { - "$ref": "Delegate" - } - } - } - }, - "ListDraftsResponse": { - "id": "ListDraftsResponse", - "type": "object", - "properties": { - "drafts": { - "type": "array", - "description": "List of drafts.", - "items": { - "$ref": "Draft" - } - }, - "nextPageToken": { - "type": "string", - "description": "Token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - } - } - }, - "ListFiltersResponse": { - "id": "ListFiltersResponse", - "type": "object", - "description": "Response for the ListFilters method.", - "properties": { - "filter": { - "type": "array", - "description": "List of a user's filters.", - "items": { - "$ref": "Filter" - } - } - } - }, - "ListForwardingAddressesResponse": { - "id": "ListForwardingAddressesResponse", - "type": "object", - "description": "Response for the ListForwardingAddresses method.", - "properties": { - "forwardingAddresses": { - "type": "array", - "description": "List of addresses that may be used for forwarding.", - "items": { - "$ref": "ForwardingAddress" - } - } - } - }, - "ListHistoryResponse": { - "id": "ListHistoryResponse", - "type": "object", - "properties": { - "history": { - "type": "array", - "description": "List of history records. Any messages contained in the response will typically only have id and threadId fields populated.", - "items": { - "$ref": "History" - } - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - }, - "nextPageToken": { - "type": "string", - "description": "Page token to retrieve the next page of results in the list." - } - } - }, - "ListLabelsResponse": { - "id": "ListLabelsResponse", - "type": "object", - "properties": { - "labels": { - "type": "array", - "description": "List of labels.", - "items": { - "$ref": "Label" - } - } - } - }, - "ListMessagesResponse": { - "id": "ListMessagesResponse", - "type": "object", - "properties": { - "messages": { - "type": "array", - "description": "List of messages.", - "items": { - "$ref": "Message" - } - }, - "nextPageToken": { - "type": "string", - "description": "Token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - } - } - }, - "ListSendAsResponse": { - "id": "ListSendAsResponse", - "type": "object", - "description": "Response for the ListSendAs method.", - "properties": { - "sendAs": { - "type": "array", - "description": "List of send-as aliases.", - "items": { - "$ref": "SendAs" - } - } - } - }, - "ListSmimeInfoResponse": { - "id": "ListSmimeInfoResponse", - "type": "object", - "properties": { - "smimeInfo": { - "type": "array", - "description": "List of SmimeInfo.", - "items": { - "$ref": "SmimeInfo" - } - } - } - }, - "ListThreadsResponse": { - "id": "ListThreadsResponse", - "type": "object", - "properties": { - "nextPageToken": { - "type": "string", - "description": "Page token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - }, - "threads": { - "type": "array", - "description": "List of threads.", - "items": { - "$ref": "Thread" - } - } - } - }, - "Message": { - "id": "Message", - "type": "object", - "description": "An email message.", - "properties": { - "historyId": { - "type": "string", - "description": "The ID of the last history record that modified this message.", - "format": "uint64" - }, - "id": { - "type": "string", - "description": "The immutable ID of the message." - }, - "internalDate": { - "type": "string", - "description": "The internal message creation timestamp (epoch ms), which determines ordering in the inbox. For normal SMTP-received email, this represents the time the message was originally accepted by Google, which is more reliable than the Date header. However, for API-migrated mail, it can be configured by client to be based on the Date header.", - "format": "int64" - }, - "labelIds": { - "type": "array", - "description": "List of IDs of labels applied to this message.", - "items": { - "type": "string" - } - }, - "payload": { - "$ref": "MessagePart", - "description": "The parsed email structure in the message parts." - }, - "raw": { - "type": "string", - "description": "The entire email message in an RFC 2822 formatted and base64url encoded string. Returned in messages.get and drafts.get responses when the format=RAW parameter is supplied.", - "format": "byte", - "annotations": { - "required": [ - "gmail.users.drafts.create", - "gmail.users.drafts.update", - "gmail.users.messages.insert", - "gmail.users.messages.send" - ] - } - }, - "sizeEstimate": { - "type": "integer", - "description": "Estimated size in bytes of the message.", - "format": "int32" - }, - "snippet": { - "type": "string", - "description": "A short part of the message text." - }, - "threadId": { - "type": "string", - "description": "The ID of the thread the message belongs to. To add a message or draft to a thread, the following criteria must be met: \n- The requested threadId must be specified on the Message or Draft.Message you supply with your request. \n- The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard. \n- The Subject headers must match." - } - } - }, - "MessagePart": { - "id": "MessagePart", - "type": "object", - "description": "A single MIME message part.", - "properties": { - "body": { - "$ref": "MessagePartBody", - "description": "The message part body for this part, which may be empty for container MIME message parts." - }, - "filename": { - "type": "string", - "description": "The filename of the attachment. Only present if this message part represents an attachment." - }, - "headers": { - "type": "array", - "description": "List of headers on this message part. For the top-level message part, representing the entire message payload, it will contain the standard RFC 2822 email headers such as To, From, and Subject.", - "items": { - "$ref": "MessagePartHeader" - } - }, - "mimeType": { - "type": "string", - "description": "The MIME type of the message part." - }, - "partId": { - "type": "string", - "description": "The immutable ID of the message part." - }, - "parts": { - "type": "array", - "description": "The child MIME message parts of this part. This only applies to container MIME message parts, for example multipart/*. For non- container MIME message part types, such as text/plain, this field is empty. For more information, see RFC 1521.", - "items": { - "$ref": "MessagePart" - } - } - } - }, - "MessagePartBody": { - "id": "MessagePartBody", - "type": "object", - "description": "The body of a single MIME message part.", - "properties": { - "attachmentId": { - "type": "string", - "description": "When present, contains the ID of an external attachment that can be retrieved in a separate messages.attachments.get request. When not present, the entire content of the message part body is contained in the data field." - }, - "data": { - "type": "string", - "description": "The body data of a MIME message part as a base64url encoded string. May be empty for MIME container types that have no message body or when the body data is sent as a separate attachment. An attachment ID is present if the body data is contained in a separate attachment.", - "format": "byte" - }, - "size": { - "type": "integer", - "description": "Number of bytes for the message part data (encoding notwithstanding).", - "format": "int32" - } - } - }, - "MessagePartHeader": { - "id": "MessagePartHeader", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the header before the : separator. For example, To." - }, - "value": { - "type": "string", - "description": "The value of the header after the : separator. For example, someuser@example.com." - } - } - }, - "ModifyMessageRequest": { - "id": "ModifyMessageRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of IDs of labels to add to this message.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list IDs of labels to remove from this message.", - "items": { - "type": "string" - } - } - } - }, - "ModifyThreadRequest": { - "id": "ModifyThreadRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of IDs of labels to add to this thread.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list of IDs of labels to remove from this thread.", - "items": { - "type": "string" - } - } - } - }, - "PopSettings": { - "id": "PopSettings", - "type": "object", - "description": "POP settings for an account.", - "properties": { - "accessWindow": { - "type": "string", - "description": "The range of messages which are accessible via POP.", - "enum": [ - "accessWindowUnspecified", - "allMail", - "disabled", - "fromNowOn" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "disposition": { - "type": "string", - "description": "The action that will be executed on a message after it has been fetched via POP.", - "enum": [ - "archive", - "dispositionUnspecified", - "leaveInInbox", - "markRead", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - } - } - }, - "Profile": { - "id": "Profile", - "type": "object", - "description": "Profile for a Gmail user.", - "properties": { - "emailAddress": { - "type": "string", - "description": "The user's email address." - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - }, - "messagesTotal": { - "type": "integer", - "description": "The total number of messages in the mailbox.", - "format": "int32" - }, - "threadsTotal": { - "type": "integer", - "description": "The total number of threads in the mailbox.", - "format": "int32" - } - } - }, - "SendAs": { - "id": "SendAs", - "type": "object", - "description": "Settings associated with a send-as alias, which can be either the primary login address associated with the account or a custom \"from\" address. Send-as aliases correspond to the \"Send Mail As\" feature in the web interface.", - "properties": { - "displayName": { - "type": "string", - "description": "A name that appears in the \"From:\" header for mail sent using this alias. For custom \"from\" addresses, when this is empty, Gmail will populate the \"From:\" header with the name that is used for the primary address associated with the account. If the admin has disabled the ability for users to update their name format, requests to update this field for the primary login will silently fail." - }, - "isDefault": { - "type": "boolean", - "description": "Whether this address is selected as the default \"From:\" address in situations such as composing a new message or sending a vacation auto-reply. Every Gmail account has exactly one default send-as address, so the only legal value that clients may write to this field is true. Changing this from false to true for an address will result in this field becoming false for the other previous default address." - }, - "isPrimary": { - "type": "boolean", - "description": "Whether this address is the primary address used to login to the account. Every Gmail account has exactly one primary address, and it cannot be deleted from the collection of send-as aliases. This field is read-only." - }, - "replyToAddress": { - "type": "string", - "description": "An optional email address that is included in a \"Reply-To:\" header for mail sent using this alias. If this is empty, Gmail will not generate a \"Reply-To:\" header." - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias. This is read-only for all operations except create." - }, - "signature": { - "type": "string", - "description": "An optional HTML signature that is included in messages composed with this alias in the Gmail web UI." - }, - "smtpMsa": { - "$ref": "SmtpMsa", - "description": "An optional SMTP service that will be used as an outbound relay for mail sent using this alias. If this is empty, outbound mail will be sent directly from Gmail's servers to the destination SMTP service. This setting only applies to custom \"from\" aliases." - }, - "treatAsAlias": { - "type": "boolean", - "description": "Whether Gmail should treat this address as an alias for the user's primary email address. This setting only applies to custom \"from\" aliases." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified for use as a send-as alias. Read-only. This setting only applies to custom \"from\" aliases.", - "enum": [ - "accepted", - "pending", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - } - } - }, - "SmimeInfo": { - "id": "SmimeInfo", - "type": "object", - "description": "An S/MIME email config.", - "properties": { - "encryptedKeyPassword": { - "type": "string", - "description": "Encrypted key password, when key is encrypted." - }, - "expiration": { - "type": "string", - "description": "When the certificate expires (in milliseconds since epoch).", - "format": "int64" - }, - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo." - }, - "isDefault": { - "type": "boolean", - "description": "Whether this SmimeInfo is the default one for this user's send-as address." - }, - "issuerCn": { - "type": "string", - "description": "The S/MIME certificate issuer's common name." - }, - "pem": { - "type": "string", - "description": "PEM formatted X509 concatenated certificate string (standard base64 encoding). Format used for returning key, which includes public key as well as certificate chain (not private key)." - }, - "pkcs12": { - "type": "string", - "description": "PKCS#12 format containing a single private/public key pair and certificate chain. This format is only accepted from client for creating a new SmimeInfo and is never returned, because the private key is not intended to be exported. PKCS#12 may be encrypted, in which case encryptedKeyPassword should be set appropriately.", - "format": "byte" - } - } - }, - "SmtpMsa": { - "id": "SmtpMsa", - "type": "object", - "description": "Configuration for communication with an SMTP service.", - "properties": { - "host": { - "type": "string", - "description": "The hostname of the SMTP service. Required." - }, - "password": { - "type": "string", - "description": "The password that will be used for authentication with the SMTP service. This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses." - }, - "port": { - "type": "integer", - "description": "The port of the SMTP service. Required.", - "format": "int32" - }, - "securityMode": { - "type": "string", - "description": "The protocol that will be used to secure communication with the SMTP service. Required.", - "enum": [ - "none", - "securityModeUnspecified", - "ssl", - "starttls" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "username": { - "type": "string", - "description": "The username that will be used for authentication with the SMTP service. This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses." - } - } - }, - "Thread": { - "id": "Thread", - "type": "object", - "description": "A collection of messages representing a conversation.", - "properties": { - "historyId": { - "type": "string", - "description": "The ID of the last history record that modified this thread.", - "format": "uint64" - }, - "id": { - "type": "string", - "description": "The unique ID of the thread." - }, - "messages": { - "type": "array", - "description": "The list of messages in the thread.", - "items": { - "$ref": "Message" - } - }, - "snippet": { - "type": "string", - "description": "A short part of the message text." - } - } - }, - "VacationSettings": { - "id": "VacationSettings", - "type": "object", - "description": "Vacation auto-reply settings for an account. These settings correspond to the \"Vacation responder\" feature in the web interface.", - "properties": { - "enableAutoReply": { - "type": "boolean", - "description": "Flag that controls whether Gmail automatically replies to messages." - }, - "endTime": { - "type": "string", - "description": "An optional end time for sending auto-replies (epoch ms). When this is specified, Gmail will automatically reply only to messages that it receives before the end time. If both startTime and endTime are specified, startTime must precede endTime.", - "format": "int64" - }, - "responseBodyHtml": { - "type": "string", - "description": "Response body in HTML format. Gmail will sanitize the HTML before storing it." - }, - "responseBodyPlainText": { - "type": "string", - "description": "Response body in plain text format." - }, - "responseSubject": { - "type": "string", - "description": "Optional text to prepend to the subject line in vacation responses. In order to enable auto-replies, either the response subject or the response body must be nonempty." - }, - "restrictToContacts": { - "type": "boolean", - "description": "Flag that determines whether responses are sent to recipients who are not in the user's list of contacts." - }, - "restrictToDomain": { - "type": "boolean", - "description": "Flag that determines whether responses are sent to recipients who are outside of the user's domain. This feature is only available for G Suite users." - }, - "startTime": { - "type": "string", - "description": "An optional start time for sending auto-replies (epoch ms). When this is specified, Gmail will automatically reply only to messages that it receives after the start time. If both startTime and endTime are specified, startTime must precede endTime.", - "format": "int64" - } - } - }, - "WatchRequest": { - "id": "WatchRequest", - "type": "object", - "description": "Set up or update a new push notification watch on this user's mailbox.", - "properties": { - "labelFilterAction": { - "type": "string", - "description": "Filtering behavior of labelIds list specified.", - "enum": [ - "exclude", - "include" - ], - "enumDescriptions": [ - "", - "" - ] - }, - "labelIds": { - "type": "array", - "description": "List of label_ids to restrict notifications about. By default, if unspecified, all changes are pushed out. If specified then dictates which labels are required for a push notification to be generated.", - "items": { - "type": "string" - } - }, - "topicName": { - "type": "string", - "description": "A fully qualified Google Cloud Pub/Sub API topic name to publish the events to. This topic name **must** already exist in Cloud Pub/Sub and you **must** have already granted gmail \"publish\" permission on it. For example, \"projects/my-project-identifier/topics/my-topic-name\" (using the Cloud Pub/Sub \"v1\" topic naming format).\n\nNote that the \"my-project-identifier\" portion must exactly match your Google developer project id (the one executing this watch request)." - } - } - }, - "WatchResponse": { - "id": "WatchResponse", - "type": "object", - "description": "Push notification watch response.", - "properties": { - "expiration": { - "type": "string", - "description": "When Gmail will stop sending notifications for mailbox updates (epoch millis). Call watch again before this time to renew the watch.", - "format": "int64" - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - } - } - } - }, - "resources": { - "users": { - "methods": { - "getProfile": { - "id": "gmail.users.getProfile", - "path": "{userId}/profile", - "httpMethod": "GET", - "description": "Gets the current user's Gmail profile.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "Profile" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "stop": { - "id": "gmail.users.stop", - "path": "{userId}/stop", - "httpMethod": "POST", - "description": "Stop receiving push notifications for the given user mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "watch": { - "id": "gmail.users.watch", - "path": "{userId}/watch", - "httpMethod": "POST", - "description": "Set up or update a push notification watch on the given user mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "WatchRequest" - }, - "response": { - "$ref": "WatchResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - }, - "resources": { - "drafts": { - "methods": { - "create": { - "id": "gmail.users.drafts.create", - "path": "{userId}/drafts", - "httpMethod": "POST", - "description": "Creates a new draft with the DRAFT label.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts" - } - } - } - }, - "delete": { - "id": "gmail.users.drafts.delete", - "path": "{userId}/drafts/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified draft. Does not simply trash it.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the draft to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "get": { - "id": "gmail.users.drafts.get", - "path": "{userId}/drafts/{id}", - "httpMethod": "GET", - "description": "Gets the specified draft.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the draft in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal", - "raw" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the draft to retrieve.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.drafts.list", - "path": "{userId}/drafts", - "httpMethod": "GET", - "description": "Lists the drafts in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include drafts from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of drafts to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return draft messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\".", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListDraftsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "send": { - "id": "gmail.users.drafts.send", - "path": "{userId}/drafts/send", - "httpMethod": "POST", - "description": "Sends the specified, existing draft to the recipients in the To, Cc, and Bcc headers.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts/send" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts/send" - } - } - } - }, - "update": { - "id": "gmail.users.drafts.update", - "path": "{userId}/drafts/{id}", - "httpMethod": "PUT", - "description": "Replaces a draft's content.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the draft to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts/{id}" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts/{id}" - } - } - } - } - } - }, - "history": { - "methods": { - "list": { - "id": "gmail.users.history.list", - "path": "{userId}/history", - "httpMethod": "GET", - "description": "Lists the history of all changes to the given mailbox. History results are returned in chronological order (increasing historyId).", - "parameters": { - "historyTypes": { - "type": "string", - "description": "History types to be returned by the function", - "enum": [ - "labelAdded", - "labelRemoved", - "messageAdded", - "messageDeleted" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "repeated": true, - "location": "query" - }, - "labelId": { - "type": "string", - "description": "Only return messages with a label matching the ID.", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of history records to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "startHistoryId": { - "type": "string", - "description": "Required. Returns history records after the specified startHistoryId. The supplied startHistoryId should be obtained from the historyId of a message, thread, or previous list response. History IDs increase chronologically but are not contiguous with random gaps in between valid IDs. Supplying an invalid or out of date startHistoryId typically returns an HTTP 404 error code. A historyId is typically valid for at least a week, but in some rare circumstances may be valid for only a few hours. If you receive an HTTP 404 error response, your application should perform a full sync. If you receive no nextPageToken in the response, there are no updates to retrieve and you can store the returned historyId for a future request.", - "format": "uint64", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListHistoryResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - } - }, - "labels": { - "methods": { - "create": { - "id": "gmail.users.labels.create", - "path": "{userId}/labels", - "httpMethod": "POST", - "description": "Creates a new label.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "delete": { - "id": "gmail.users.labels.delete", - "path": "{userId}/labels/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified label and removes it from any messages and threads that it is applied to.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "get": { - "id": "gmail.users.labels.get", - "path": "{userId}/labels/{id}", - "httpMethod": "GET", - "description": "Gets the specified label.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to retrieve.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.labels.list", - "path": "{userId}/labels", - "httpMethod": "GET", - "description": "Lists all labels in the user's mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListLabelsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "patch": { - "id": "gmail.users.labels.patch", - "path": "{userId}/labels/{id}", - "httpMethod": "PATCH", - "description": "Updates the specified label. This method supports patch semantics.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "update": { - "id": "gmail.users.labels.update", - "path": "{userId}/labels/{id}", - "httpMethod": "PUT", - "description": "Updates the specified label.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - } - }, - "messages": { - "methods": { - "batchDelete": { - "id": "gmail.users.messages.batchDelete", - "path": "{userId}/messages/batchDelete", - "httpMethod": "POST", - "description": "Deletes many messages by message ID. Provides no guarantees that messages were not already deleted or even existed at all.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "BatchDeleteMessagesRequest" - }, - "scopes": [ - "https://mail.google.com/" - ] - }, - "batchModify": { - "id": "gmail.users.messages.batchModify", - "path": "{userId}/messages/batchModify", - "httpMethod": "POST", - "description": "Modifies the labels on the specified messages.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "BatchModifyMessagesRequest" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "delete": { - "id": "gmail.users.messages.delete", - "path": "{userId}/messages/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified message. This operation cannot be undone. Prefer messages.trash instead.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/" - ] - }, - "get": { - "id": "gmail.users.messages.get", - "path": "{userId}/messages/{id}", - "httpMethod": "GET", - "description": "Gets the specified message.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the message in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal", - "raw" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the message to retrieve.", - "required": true, - "location": "path" - }, - "metadataHeaders": { - "type": "string", - "description": "When given and format is METADATA, only include headers specified.", - "repeated": true, - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "import": { - "id": "gmail.users.messages.import", - "path": "{userId}/messages/import", - "httpMethod": "POST", - "description": "Imports a message into only this user's mailbox, with standard email delivery scanning and classification similar to receiving via SMTP. Does not send a message.", - "parameters": { - "deleted": { - "type": "boolean", - "description": "Mark the email as permanently deleted (not TRASH) and only visible in Google Vault to a Vault administrator. Only used for G Suite accounts.", - "default": "false", - "location": "query" - }, - "internalDateSource": { - "type": "string", - "description": "Source for Gmail's internal date of the message.", - "default": "dateHeader", - "enum": [ - "dateHeader", - "receivedTime" - ], - "enumDescriptions": [ - "", - "" - ], - "location": "query" - }, - "neverMarkSpam": { - "type": "boolean", - "description": "Ignore the Gmail spam classifier decision and never mark this email as SPAM in the mailbox.", - "default": "false", - "location": "query" - }, - "processForCalendar": { - "type": "boolean", - "description": "Process calendar invites in the email and add any extracted meetings to the Google Calendar for this user.", - "default": "false", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.insert", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "50MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages/import" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages/import" - } - } - } - }, - "insert": { - "id": "gmail.users.messages.insert", - "path": "{userId}/messages", - "httpMethod": "POST", - "description": "Directly inserts a message into only this user's mailbox similar to IMAP APPEND, bypassing most scanning and classification. Does not send a message.", - "parameters": { - "deleted": { - "type": "boolean", - "description": "Mark the email as permanently deleted (not TRASH) and only visible in Google Vault to a Vault administrator. Only used for G Suite accounts.", - "default": "false", - "location": "query" - }, - "internalDateSource": { - "type": "string", - "description": "Source for Gmail's internal date of the message.", - "default": "receivedTime", - "enum": [ - "dateHeader", - "receivedTime" - ], - "enumDescriptions": [ - "", - "" - ], - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.insert", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "50MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages" - } - } - } - }, - "list": { - "id": "gmail.users.messages.list", - "path": "{userId}/messages", - "httpMethod": "GET", - "description": "Lists the messages in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include messages from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "labelIds": { - "type": "string", - "description": "Only return messages with labels that match all of the specified label IDs.", - "repeated": true, - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of messages to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid:\u003csomemsgid@example.com\u003e is:unread\". Parameter cannot be used when accessing the api using the gmail.metadata scope.", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListMessagesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "modify": { - "id": "gmail.users.messages.modify", - "path": "{userId}/messages/{id}/modify", - "httpMethod": "POST", - "description": "Modifies the labels on the specified message.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to modify.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "ModifyMessageRequest" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "send": { - "id": "gmail.users.messages.send", - "path": "{userId}/messages/send", - "httpMethod": "POST", - "description": "Sends the specified message to the recipients in the To, Cc, and Bcc headers.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.send" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages/send" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages/send" - } - } - } - }, - "trash": { - "id": "gmail.users.messages.trash", - "path": "{userId}/messages/{id}/trash", - "httpMethod": "POST", - "description": "Moves the specified message to the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "untrash": { - "id": "gmail.users.messages.untrash", - "path": "{userId}/messages/{id}/untrash", - "httpMethod": "POST", - "description": "Removes the specified message from the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to remove from Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - }, - "resources": { - "attachments": { - "methods": { - "get": { - "id": "gmail.users.messages.attachments.get", - "path": "{userId}/messages/{messageId}/attachments/{id}", - "httpMethod": "GET", - "description": "Gets the specified message attachment.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the attachment.", - "required": true, - "location": "path" - }, - "messageId": { - "type": "string", - "description": "The ID of the message containing the attachment.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "messageId", - "id" - ], - "response": { - "$ref": "MessagePartBody" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - } - } - } - }, - "settings": { - "methods": { - "getAutoForwarding": { - "id": "gmail.users.settings.getAutoForwarding", - "path": "{userId}/settings/autoForwarding", - "httpMethod": "GET", - "description": "Gets the auto-forwarding setting for the specified account.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "AutoForwarding" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getImap": { - "id": "gmail.users.settings.getImap", - "path": "{userId}/settings/imap", - "httpMethod": "GET", - "description": "Gets IMAP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ImapSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getPop": { - "id": "gmail.users.settings.getPop", - "path": "{userId}/settings/pop", - "httpMethod": "GET", - "description": "Gets POP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "PopSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getVacation": { - "id": "gmail.users.settings.getVacation", - "path": "{userId}/settings/vacation", - "httpMethod": "GET", - "description": "Gets vacation responder settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "VacationSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updateAutoForwarding": { - "id": "gmail.users.settings.updateAutoForwarding", - "path": "{userId}/settings/autoForwarding", - "httpMethod": "PUT", - "description": "Updates the auto-forwarding setting for the specified account. A verified forwarding address must be specified when auto-forwarding is enabled.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "AutoForwarding" - }, - "response": { - "$ref": "AutoForwarding" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "updateImap": { - "id": "gmail.users.settings.updateImap", - "path": "{userId}/settings/imap", - "httpMethod": "PUT", - "description": "Updates IMAP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "ImapSettings" - }, - "response": { - "$ref": "ImapSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updatePop": { - "id": "gmail.users.settings.updatePop", - "path": "{userId}/settings/pop", - "httpMethod": "PUT", - "description": "Updates POP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "PopSettings" - }, - "response": { - "$ref": "PopSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updateVacation": { - "id": "gmail.users.settings.updateVacation", - "path": "{userId}/settings/vacation", - "httpMethod": "PUT", - "description": "Updates vacation responder settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "VacationSettings" - }, - "response": { - "$ref": "VacationSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - }, - "resources": { - "delegates": { - "methods": { - "create": { - "id": "gmail.users.settings.delegates.create", - "path": "{userId}/settings/delegates", - "httpMethod": "POST", - "description": "Adds a delegate with its verification status set directly to accepted, without sending any verification email. The delegate user must be a member of the same G Suite organization as the delegator user.\n\nGmail imposes limtations on the number of delegates and delegators each user in a G Suite organization can have. These limits depend on your organization, but in general each user can have up to 25 delegates and up to 10 delegators.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nAlso note that when a new delegate is created, there may be up to a one minute delay before the new delegate is available for use.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Delegate" - }, - "response": { - "$ref": "Delegate" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.delegates.delete", - "path": "{userId}/settings/delegates/{delegateEmail}", - "httpMethod": "DELETE", - "description": "Removes the specified delegate (which can be of any verification status), and revokes any verification that may have been required for using it.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "delegateEmail": { - "type": "string", - "description": "The email address of the user to be removed as a delegate.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "delegateEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.delegates.get", - "path": "{userId}/settings/delegates/{delegateEmail}", - "httpMethod": "GET", - "description": "Gets the specified delegate.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "delegateEmail": { - "type": "string", - "description": "The email address of the user whose delegate relationship is to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "delegateEmail" - ], - "response": { - "$ref": "Delegate" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.delegates.list", - "path": "{userId}/settings/delegates", - "httpMethod": "GET", - "description": "Lists the delegates for the specified account.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListDelegatesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "filters": { - "methods": { - "create": { - "id": "gmail.users.settings.filters.create", - "path": "{userId}/settings/filters", - "httpMethod": "POST", - "description": "Creates a filter.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Filter" - }, - "response": { - "$ref": "Filter" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "delete": { - "id": "gmail.users.settings.filters.delete", - "path": "{userId}/settings/filters/{id}", - "httpMethod": "DELETE", - "description": "Deletes a filter.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the filter to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "get": { - "id": "gmail.users.settings.filters.get", - "path": "{userId}/settings/filters/{id}", - "httpMethod": "GET", - "description": "Gets a filter.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the filter to be fetched.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Filter" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.filters.list", - "path": "{userId}/settings/filters", - "httpMethod": "GET", - "description": "Lists the message filters of a Gmail user.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListFiltersResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "forwardingAddresses": { - "methods": { - "create": { - "id": "gmail.users.settings.forwardingAddresses.create", - "path": "{userId}/settings/forwardingAddresses", - "httpMethod": "POST", - "description": "Creates a forwarding address. If ownership verification is required, a message will be sent to the recipient and the resource's verification status will be set to pending; otherwise, the resource will be created with verification status set to accepted.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "ForwardingAddress" - }, - "response": { - "$ref": "ForwardingAddress" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.forwardingAddresses.delete", - "path": "{userId}/settings/forwardingAddresses/{forwardingEmail}", - "httpMethod": "DELETE", - "description": "Deletes the specified forwarding address and revokes any verification that may have been required.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "forwardingEmail": { - "type": "string", - "description": "The forwarding address to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "forwardingEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.forwardingAddresses.get", - "path": "{userId}/settings/forwardingAddresses/{forwardingEmail}", - "httpMethod": "GET", - "description": "Gets the specified forwarding address.", - "parameters": { - "forwardingEmail": { - "type": "string", - "description": "The forwarding address to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "forwardingEmail" - ], - "response": { - "$ref": "ForwardingAddress" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.forwardingAddresses.list", - "path": "{userId}/settings/forwardingAddresses", - "httpMethod": "GET", - "description": "Lists the forwarding addresses for the specified account.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListForwardingAddressesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "sendAs": { - "methods": { - "create": { - "id": "gmail.users.settings.sendAs.create", - "path": "{userId}/settings/sendAs", - "httpMethod": "POST", - "description": "Creates a custom \"from\" send-as alias. If an SMTP MSA is specified, Gmail will attempt to connect to the SMTP service to validate the configuration before creating the alias. If ownership verification is required for the alias, a message will be sent to the email address and the resource's verification status will be set to pending; otherwise, the resource will be created with verification status set to accepted. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.sendAs.delete", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "DELETE", - "description": "Deletes the specified send-as alias. Revokes any verification that may have been required for using it.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.sendAs.get", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "GET", - "description": "Gets the specified send-as alias. Fails with an HTTP 404 error if the specified address is not a member of the collection.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.sendAs.list", - "path": "{userId}/settings/sendAs", - "httpMethod": "GET", - "description": "Lists the send-as aliases for the specified account. The result includes the primary send-as address associated with the account as well as any custom \"from\" aliases.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListSendAsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "patch": { - "id": "gmail.users.settings.sendAs.patch", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "PATCH", - "description": "Updates a send-as alias. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nAddresses other than the primary address for the account can only be updated by service account clients that have been delegated domain-wide authority. This method supports patch semantics.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be updated.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "update": { - "id": "gmail.users.settings.sendAs.update", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "PUT", - "description": "Updates a send-as alias. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nAddresses other than the primary address for the account can only be updated by service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be updated.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "verify": { - "id": "gmail.users.settings.sendAs.verify", - "path": "{userId}/settings/sendAs/{sendAsEmail}/verify", - "httpMethod": "POST", - "description": "Sends a verification email to the specified send-as alias address. The verification status must be pending.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be verified.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - } - }, - "resources": { - "smimeInfo": { - "methods": { - "delete": { - "id": "gmail.users.settings.sendAs.smimeInfo.delete", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}", - "httpMethod": "DELETE", - "description": "Deletes the specified S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.sendAs.smimeInfo.get", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}", - "httpMethod": "GET", - "description": "Gets the specified S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "response": { - "$ref": "SmimeInfo" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "insert": { - "id": "gmail.users.settings.sendAs.smimeInfo.insert", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo", - "httpMethod": "POST", - "description": "Insert (upload) the given S/MIME config for the specified send-as alias. Note that pkcs12 format is required for the key.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SmimeInfo" - }, - "response": { - "$ref": "SmimeInfo" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "list": { - "id": "gmail.users.settings.sendAs.smimeInfo.list", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo", - "httpMethod": "GET", - "description": "Lists S/MIME configs for the specified send-as alias.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "response": { - "$ref": "ListSmimeInfoResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "setDefault": { - "id": "gmail.users.settings.sendAs.smimeInfo.setDefault", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}/setDefault", - "httpMethod": "POST", - "description": "Sets the default S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - } - } - } - } - } - } - }, - "threads": { - "methods": { - "delete": { - "id": "gmail.users.threads.delete", - "path": "{userId}/threads/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified thread. This operation cannot be undone. Prefer threads.trash instead.", - "parameters": { - "id": { - "type": "string", - "description": "ID of the Thread to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/" - ] - }, - "get": { - "id": "gmail.users.threads.get", - "path": "{userId}/threads/{id}", - "httpMethod": "GET", - "description": "Gets the specified thread.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the messages in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal" - ], - "enumDescriptions": [ - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the thread to retrieve.", - "required": true, - "location": "path" - }, - "metadataHeaders": { - "type": "string", - "description": "When given and format is METADATA, only include headers specified.", - "repeated": true, - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.threads.list", - "path": "{userId}/threads", - "httpMethod": "GET", - "description": "Lists the threads in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include threads from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "labelIds": { - "type": "string", - "description": "Only return threads with labels that match all of the specified label IDs.", - "repeated": true, - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of threads to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return threads matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\". Parameter cannot be used when accessing the api using the gmail.metadata scope.", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListThreadsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "modify": { - "id": "gmail.users.threads.modify", - "path": "{userId}/threads/{id}/modify", - "httpMethod": "POST", - "description": "Modifies the labels applied to the thread. This applies to all messages in the thread.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to modify.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "ModifyThreadRequest" - }, - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "trash": { - "id": "gmail.users.threads.trash", - "path": "{userId}/threads/{id}/trash", - "httpMethod": "POST", - "description": "Moves the specified thread to the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "untrash": { - "id": "gmail.users.threads.untrash", - "path": "{userId}/threads/{id}/untrash", - "httpMethod": "POST", - "description": "Removes the specified thread from the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to remove from Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - } - } - } - } - } -} -__END -} diff --git a/t/04-client-mocked-agent.t b/t/04-client-mocked-agent.t deleted file mode 100644 index 495270e..0000000 --- a/t/04-client-mocked-agent.t +++ /dev/null @@ -1,7110 +0,0 @@ -#!perl -T - -=head2 USAGE - -To run manually in the local directory assuming gapi.json present in source root and in xt/author/calendar directory - C - -NB: is also run as part of dzil test - -=cut - -use 5.006; -use strict; -use warnings; -use Test::More; -use Data::Dumper; ## remove this when finish tweaking -use Cwd; -use CHI; -use WebService::GoogleAPI::Client; -use Sub::Override; -use JSON; -use FindBin qw/$Bin/; -use Sub::Override; - - -my $dir = getcwd; -my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging -my $warm_hash = {}; -#$warm_hash->{'https://www.googleapis.com/discovery/v1/apis'} = 'foo'; -#print Dumper $hash; - - -my $chi = CHI->new(driver => 'RawMemory', datastore => $warm_hash ); - -#$chi->set('https://www.googleapis.com/discovery/v1/apis' => from_json( pre_get_all_apis_json()) ); -#$chi->set('https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest' => from_json(pre_get_gmail_spec_json()) ); -use_ok( 'WebService::GoogleAPI::Client::Discovery' ); - -#my $client = WebService::GoogleAPI::Client::Discovery->new( ); - -ok( my $client = WebService::GoogleAPI::Client->new( gapi_json => "$Bin/gapi.json", chi=>$chi, debug=>$DEBUG ) ,'Create a new Client instance'); - -my $aref_token_emails = $client->auth_storage->storage->get_token_emails_from_storage; -my $user = $aref_token_emails->[0]; ## default to the first user -$client->user( $user ); - - ## INJECT HTTP RESPONSE FOR API DISCOVERY REQUEST - my $override = Sub::Override->new('Mojo::Transaction::res', sub { - my $res = Mojo::Message::Response->new ; - $res->code( 200 ); - $res->body( pre_get_all_apis_json() ); - return $res; - }); - - ok( my $all = $client->discover_all(), 'discover_all()' ); - - ## INJECT HTTP RESPONSE FOR GAMIL V1 API SPECIFICATION - my $override2 = Sub::Override->new('Mojo::Transaction::res', - sub { - my $res2 = Mojo::Message::Response->new ; - $res2->code( 200 ); - $res2->body( pre_get_gmail_spec_json() ); - return $res2; - }); - - - ok( my $gmail_api_spec = $client->methods_available_for_google_api_id('gmail', 'v1'), 'Get available methods for Gmail V1'); - - - - my $override3 = Sub::Override->new('Mojo::Transaction::res', - sub { - my $res2 = Mojo::Message::Response->new ; - $res2->code( 200 ); - $res2->body( '{ "status": "ok" }'); - return $res2; - }); - - - ok( $client->api_query( api_endpoint_id => 'gmail.users.messages.list' ), 'api_query - "gmail.users.messages.list"'); - ok ( $client->extract_method_discovery_detail_from_api_spec( 'gmail.users.messages.list', 'v1' ), 'extract_method_discovery_detail_from_api_spec'); - ok ( $client->ua->header_with_bearer_auth_token(), 'header_with_bearer_auth_token' ); - ok( my $cred = $client->ua->auth_storage->get_credentials_for_refresh( $user ), 'get credentials'); - ok( my $new_token = $client->ua->refresh_access_token( $cred ), 'refresh token' ); - ok( $client->debug(1), 'set debug'); - ok( $client->do_autorefresh(1), 'set debug'); - ok( $client->ua->debug(1), 'set debug'); - -#ok ( $client->available_APIs(), 'Get available APIs'); -#ok ( $client->supported_as_text(), 'Supported as text'); -#ok ( $client->available_versions('gmail'), 'Available versions for Gmail'); -#ok ( $client->latest_stable_version('gmail'), 'Latest stable version for Gmail'); -#ok( $client->api_verson_urls(), 'api version urls'); -ok( $client->methods_available_for_google_api_id('gmail'), 'Get end points available for gmail'); -ok( $client->list_of_available_google_api_ids(), 'All available Google API IDs'); -ok( $client->has_scope_to_access_api_endpoint('gmail.users.settings.sendAs.get'),'has GMail scope'); -note( ' CHI TYPE + ' . ref( $client->chi() ) ); -#note( my$y = $x->get('https://www.googleapis.com/discovery/v1/apis')); -#print Dumper $x; - -#use_ok( 'WebService::GoogleAPI::Client' ); # || print "Bail out!\n"; - - -#ok( ref( WebService::GoogleAPI::Client::Discovery->new->( chi => $chi )->discover_all() ) eq 'HASH', 'WebService::GoogleAPI::Client::Discovery->new->discover_all() return HASREF' ); - - - -done_testing(); - - -sub pre_get_all_apis_json -{ - return <<'_END' -{ - "kind": "discovery#directoryList", - "discoveryVersion": "v1", - "items": [ - { - "kind": "discovery#directoryItem", - "id": "abusiveexperiencereport:v1", - "name": "abusiveexperiencereport", - "version": "v1", - "title": "Abusive Experience Report API", - "description": "Views Abusive Experience Report data, and gets a list of sites that have a significant number of abusive experiences.", - "discoveryRestUrl": "https://abusiveexperiencereport.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/abusive-experience-report/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "acceleratedmobilepageurl:v1", - "name": "acceleratedmobilepageurl", - "version": "v1", - "title": "Accelerated Mobile Pages (AMP) URL API", - "description": "This API contains a single method, batchGet. Call this method to retrieve the AMP URL (and equivalent AMP Cache URL) for given public URL(s).", - "discoveryRestUrl": "https://acceleratedmobilepageurl.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/amp/cache/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "accesscontextmanager:v1beta", - "name": "accesscontextmanager", - "version": "v1beta", - "title": "Access Context Manager API", - "description": "An API for setting attribute based access control to requests to GCP services.", - "discoveryRestUrl": "https://accesscontextmanager.googleapis.com/$clientovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/access-context-manager/docs/reference/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.2", - "name": "adexchangebuyer", - "version": "v1.2", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.2/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.3", - "name": "adexchangebuyer", - "version": "v1.3", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.3/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer:v1.4", - "name": "adexchangebuyer", - "version": "v1.4", - "title": "Ad Exchange Buyer API", - "description": "Accesses your bidding-account information, submits creatives for validation, finds available direct deals, and retrieves performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adexchangebuyer/v1.4/rest", - "discoveryLink": "./apis/adexchangebuyer/v1.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexchangebuyer2:v2beta1", - "name": "adexchangebuyer2", - "version": "v2beta1", - "title": "Ad Exchange Buyer API II", - "description": "Accesses the latest features for managing Ad Exchange accounts, Real-Time Bidding configurations and auction metrics, and Marketplace programmatic deals.", - "discoveryRestUrl": "https://adexchangebuyer.googleapis.com/$clientovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/ad-exchange/buyer-rest/reference/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adexperiencereport:v1", - "name": "adexperiencereport", - "version": "v1", - "title": "Ad Experience Report API", - "description": "Views Ad Experience Report data, and gets a list of sites that have a significant number of annoying ads.", - "discoveryRestUrl": "https://adexperiencereport.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/ad-experience-report/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "admin:datatransfer_v1", - "name": "admin", - "version": "datatransfer_v1", - "title": "Admin Data Transfer API", - "description": "Transfers user data from one user to another.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/datatransfer_v1/rest", - "discoveryLink": "./apis/admin/datatransfer_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/data-transfer/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "admin:directory_v1", - "name": "admin", - "version": "directory_v1", - "title": "Admin Directory API", - "description": "Manages enterprise resources such as users and groups, administrative notifications, security features, and more.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/directory_v1/rest", - "discoveryLink": "./apis/admin/directory_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/directory/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "admin:reports_v1", - "name": "admin", - "version": "reports_v1", - "title": "Admin Reports API", - "description": "Fetches reports for the administrators of G Suite customers about the usage, collaboration, security, and risk for their users.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/admin/reports_v1/rest", - "discoveryLink": "./apis/admin/reports_v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/reports/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adsense:v1.4", - "name": "adsense", - "version": "v1.4", - "title": "AdSense Management API", - "description": "Accesses AdSense publishers' inventory and generates performance reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adsense/v1.4/rest", - "discoveryLink": "./apis/adsense/v1.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/adsense-16.png", - "x32": "https://www.google.com/images/icons/product/adsense-32.png" - }, - "documentationLink": "https://developers.google.com/adsense/management/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "adsensehost:v4.1", - "name": "adsensehost", - "version": "v4.1", - "title": "AdSense Host API", - "description": "Generates performance reports, generates ad codes, and provides publisher management capabilities for AdSense Hosts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/adsensehost/v4.1/rest", - "discoveryLink": "./apis/adsensehost/v4.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/adsense-16.png", - "x32": "https://www.google.com/images/icons/product/adsense-32.png" - }, - "documentationLink": "https://developers.google.com/adsense/host/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "alertcenter:v1beta1", - "name": "alertcenter", - "version": "v1beta1", - "title": "G Suite Alert Center API", - "description": "G Suite Alert Center API to view and manage alerts on issues affecting your domain.", - "discoveryRestUrl": "https://alertcenter.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/admin-sdk/alertcenter/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "analytics:v2.4", - "name": "analytics", - "version": "v2.4", - "title": "Google Analytics API", - "description": "Views and manages your Google Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/analytics/v2.4/rest", - "discoveryLink": "./apis/analytics/v2.4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/analytics-16.png", - "x32": "https://www.google.com/images/icons/product/analytics-32.png" - }, - "documentationLink": "https://developers.google.com/analytics/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "analytics:v3", - "name": "analytics", - "version": "v3", - "title": "Google Analytics API", - "description": "Views and manages your Google Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/analytics/v3/rest", - "discoveryLink": "./apis/analytics/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/analytics-16.png", - "x32": "https://www.google.com/images/icons/product/analytics-32.png" - }, - "documentationLink": "https://developers.google.com/analytics/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "analyticsreporting:v4", - "name": "analyticsreporting", - "version": "v4", - "title": "Google Analytics Reporting API", - "description": "Accesses Analytics report data.", - "discoveryRestUrl": "https://analyticsreporting.googleapis.com/$clientovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/analytics/devguides/reporting/core/v4/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androiddeviceprovisioning:v1", - "name": "androiddeviceprovisioning", - "version": "v1", - "title": "Android Device Provisioning Partner API", - "description": "Automates Android zero-touch enrollment for device resellers, customers, and EMMs.", - "discoveryRestUrl": "https://androiddeviceprovisioning.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/zero-touch/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidenterprise:v1", - "name": "androidenterprise", - "version": "v1", - "title": "Google Play EMM API", - "description": "Manages the deployment of apps to Android for Work users.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidenterprise/v1/rest", - "discoveryLink": "./apis/androidenterprise/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android/work/play/emm-api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidmanagement:v1", - "name": "androidmanagement", - "version": "v1", - "title": "Android Management API", - "description": "The Android Management API provides remote enterprise management of Android devices and apps.", - "discoveryRestUrl": "https://androidmanagement.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/android/management", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v1", - "name": "androidpublisher", - "version": "v1", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v1/rest", - "discoveryLink": "./apis/androidpublisher/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v1.1", - "name": "androidpublisher", - "version": "v1.1", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v1.1/rest", - "discoveryLink": "./apis/androidpublisher/v1.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v2", - "name": "androidpublisher", - "version": "v2", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v2/rest", - "discoveryLink": "./apis/androidpublisher/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "androidpublisher:v3", - "name": "androidpublisher", - "version": "v3", - "title": "Google Play Developer API", - "description": "Accesses Android application developers' Google Play accounts.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/androidpublisher/v3/rest", - "discoveryLink": "./apis/androidpublisher/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/android-16.png", - "x32": "https://www.google.com/images/icons/product/android-32.png" - }, - "documentationLink": "https://developers.google.com/android-publisher", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1alpha", - "name": "appengine", - "version": "v1alpha", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$clientovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta", - "name": "appengine", - "version": "v1beta", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$clientovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1", - "name": "appengine", - "version": "v1", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta4", - "name": "appengine", - "version": "v1beta4", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$clientovery/rest?version=v1beta4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appengine:v1beta5", - "name": "appengine", - "version": "v1beta5", - "title": "App Engine Admin API", - "description": "Provisions and manages developers' App Engine applications.", - "discoveryRestUrl": "https://appengine.googleapis.com/$clientovery/rest?version=v1beta5", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/appengine/docs/admin-api/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "appsactivity:v1", - "name": "appsactivity", - "version": "v1", - "title": "Drive Activity API", - "description": "Provides a historical view of activity.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/appsactivity/v1/rest", - "discoveryLink": "./apis/appsactivity/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/activity/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "appstate:v1", - "name": "appstate", - "version": "v1", - "title": "Google App State API", - "description": "The Google App State API.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/appstate/v1/rest", - "discoveryLink": "./apis/appstate/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services/web/api/states", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "bigquery:v2", - "name": "bigquery", - "version": "v2", - "title": "BigQuery API", - "description": "A data platform for customers to create, manage, share and query data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/bigquery/v2/rest", - "discoveryLink": "./apis/bigquery/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/search-16.gif", - "x32": "https://www.google.com/images/icons/product/search-32.gif" - }, - "documentationLink": "https://cloud.google.com/bigquery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "bigquerydatatransfer:v1", - "name": "bigquerydatatransfer", - "version": "v1", - "title": "BigQuery Data Transfer API", - "description": "Transfers data from partner SaaS applications to Google BigQuery on a scheduled, managed basis.", - "discoveryRestUrl": "https://bigquerydatatransfer.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/bigquery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "binaryauthorization:v1beta1", - "name": "binaryauthorization", - "version": "v1beta1", - "title": "Binary Authorization API", - "description": "The management interface for Binary Authorization, a system providing policy control for images deployed to Kubernetes Engine clusters.", - "discoveryRestUrl": "https://binaryauthorization.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/binary-authorization/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "blogger:v2", - "name": "blogger", - "version": "v2", - "title": "Blogger API", - "description": "API for access to the data within Blogger.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/blogger/v2/rest", - "discoveryLink": "./apis/blogger/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/blogger-16.png", - "x32": "https://www.google.com/images/icons/product/blogger-32.png" - }, - "documentationLink": "https://developers.google.com/blogger/docs/2.0/json/getting_started", - "labels": [ - "limited_availability" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "blogger:v3", - "name": "blogger", - "version": "v3", - "title": "Blogger API", - "description": "API for access to the data within Blogger.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/blogger/v3/rest", - "discoveryLink": "./apis/blogger/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/blogger-16.png", - "x32": "https://www.google.com/images/icons/product/blogger-32.png" - }, - "documentationLink": "https://developers.google.com/blogger/docs/3.0/getting_started", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "books:v1", - "name": "books", - "version": "v1", - "title": "Books API", - "description": "Searches for books and manages your Google Books library.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/books/v1/rest", - "discoveryLink": "./apis/books/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/ebooks-16.png", - "x32": "https://www.google.com/images/icons/product/ebooks-32.png" - }, - "documentationLink": "https://developers.google.com/books/docs/v1/getting_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "calendar:v3", - "name": "calendar", - "version": "v3", - "title": "Calendar API", - "description": "Manipulates events and other calendar data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest", - "discoveryLink": "./apis/calendar/v3/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/calendar-16.png", - "x32": "http://www.google.com/images/icons/product/calendar-32.png" - }, - "documentationLink": "https://developers.google.com/google-apps/calendar/firstapp", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "chat:v1", - "name": "chat", - "version": "v1", - "title": "Hangouts Chat API", - "description": "Create bots and extend the new Hangouts Chat.", - "discoveryRestUrl": "https://chat.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/hangouts/chat", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "civicinfo:v2", - "name": "civicinfo", - "version": "v2", - "title": "Google Civic Information API", - "description": "Provides polling places, early vote locations, contest data, election officials, and government representatives for U.S. residential addresses.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/civicinfo/v2/rest", - "discoveryLink": "./apis/civicinfo/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/civic-information", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "classroom:v1", - "name": "classroom", - "version": "v1", - "title": "Google Classroom API", - "description": "Manages classes, rosters, and invitations in Google Classroom.", - "discoveryRestUrl": "https://classroom.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/classroom", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudasset:v1beta1", - "name": "cloudasset", - "version": "v1beta1", - "title": "Cloud Asset API", - "description": "The cloud asset API manages the history and inventory of cloud resources.", - "discoveryRestUrl": "https://cloudasset.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://console.cloud.google.com/apis/api/cloudasset.googleapis.com/overview", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbilling:v1", - "name": "cloudbilling", - "version": "v1", - "title": "Cloud Billing API", - "description": "Allows developers to manage billing for their Google Cloud Platform projects programmatically.", - "discoveryRestUrl": "https://cloudbilling.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/billing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbuild:v1alpha1", - "name": "cloudbuild", - "version": "v1alpha1", - "title": "Cloud Build API", - "description": "Creates and manages builds on Google Cloud Platform.", - "discoveryRestUrl": "https://cloudbuild.googleapis.com/$clientovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/cloud-build/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudbuild:v1", - "name": "cloudbuild", - "version": "v1", - "title": "Cloud Build API", - "description": "Creates and manages builds on Google Cloud Platform.", - "discoveryRestUrl": "https://cloudbuild.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/cloud-build/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "clouddebugger:v2", - "name": "clouddebugger", - "version": "v2", - "title": "Stackdriver Debugger API", - "description": "Examines the call stack and variables of a running application without stopping or slowing it down.", - "discoveryRestUrl": "https://clouddebugger.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/debugger", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "clouderrorreporting:v1beta1", - "name": "clouderrorreporting", - "version": "v1beta1", - "title": "Stackdriver Error Reporting API", - "description": "Groups and counts similar errors from cloud services and applications, reports new errors, and provides access to error groups and their associated errors.", - "discoveryRestUrl": "https://clouderrorreporting.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/error-reporting/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudfunctions:v1", - "name": "cloudfunctions", - "version": "v1", - "title": "Cloud Functions API", - "description": "Manages lightweight user-provided functions executed in response to events.", - "discoveryRestUrl": "https://cloudfunctions.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/functions", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudfunctions:v1beta2", - "name": "cloudfunctions", - "version": "v1beta2", - "title": "Cloud Functions API", - "description": "Manages lightweight user-provided functions executed in response to events.", - "discoveryRestUrl": "https://cloudfunctions.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/functions", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudiot:v1", - "name": "cloudiot", - "version": "v1", - "title": "Cloud IoT API", - "description": "Registers and manages IoT (Internet of Things) devices that connect to the Google Cloud Platform.", - "discoveryRestUrl": "https://cloudiot.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iot", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudiot:v1beta1", - "name": "cloudiot", - "version": "v1beta1", - "title": "Cloud IoT API", - "description": "Registers and manages IoT (Internet of Things) devices that connect to the Google Cloud Platform.", - "discoveryRestUrl": "https://cloudiot.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iot", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudkms:v1", - "name": "cloudkms", - "version": "v1", - "title": "Cloud Key Management Service (KMS) API", - "description": "Manages keys and performs cryptographic operations in a central cloud service, for direct use by other cloud resources and applications.", - "discoveryRestUrl": "https://cloudkms.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kms/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudprofiler:v2", - "name": "cloudprofiler", - "version": "v2", - "title": "Stackdriver Profiler API", - "description": "Manages continuous profiling information.", - "discoveryRestUrl": "https://cloudprofiler.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/profiler/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v1", - "name": "cloudresourcemanager", - "version": "v1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v1beta1", - "name": "cloudresourcemanager", - "version": "v1beta1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v2", - "name": "cloudresourcemanager", - "version": "v2", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudresourcemanager:v2beta1", - "name": "cloudresourcemanager", - "version": "v2beta1", - "title": "Cloud Resource Manager API", - "description": "The Google Cloud Resource Manager API provides methods for creating, reading, and updating project metadata.", - "discoveryRestUrl": "https://cloudresourcemanager.googleapis.com/$clientovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/resource-manager", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudshell:v1alpha1", - "name": "cloudshell", - "version": "v1alpha1", - "title": "Cloud Shell API", - "description": "Allows users to start, configure, and connect to interactive shell sessions running in the cloud.", - "discoveryRestUrl": "https://cloudshell.googleapis.com/$clientovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/shell/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudshell:v1", - "name": "cloudshell", - "version": "v1", - "title": "Cloud Shell API", - "description": "Allows users to start, configure, and connect to interactive shell sessions running in the cloud.", - "discoveryRestUrl": "https://cloudshell.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/shell/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtasks:v2beta2", - "name": "cloudtasks", - "version": "v2beta2", - "title": "Cloud Tasks API", - "description": "Manages the execution of large numbers of distributed requests.", - "discoveryRestUrl": "https://cloudtasks.googleapis.com/$clientovery/rest?version=v2beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tasks/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtasks:v2beta3", - "name": "cloudtasks", - "version": "v2beta3", - "title": "Cloud Tasks API", - "description": "Manages the execution of large numbers of distributed requests.", - "discoveryRestUrl": "https://cloudtasks.googleapis.com/$clientovery/rest?version=v2beta3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tasks/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v2alpha1", - "name": "cloudtrace", - "version": "v2alpha1", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$clientovery/rest?version=v2alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v1", - "name": "cloudtrace", - "version": "v1", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "cloudtrace:v2", - "name": "cloudtrace", - "version": "v2", - "title": "Stackdriver Trace API", - "description": "Sends application trace data to Stackdriver Trace for viewing. Trace data is collected for all App Engine applications by default. Trace data from other applications can be provided using this API. This library is used to interact with the Trace API directly. If you are looking to instrument your application for Stackdriver Trace, we recommend using OpenCensus.", - "discoveryRestUrl": "https://cloudtrace.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/trace", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "composer:v1", - "name": "composer", - "version": "v1", - "title": "Cloud Composer API", - "description": "Manages Apache Airflow environments on Google Cloud Platform.", - "discoveryRestUrl": "https://composer.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/composer/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "composer:v1beta1", - "name": "composer", - "version": "v1beta1", - "title": "Cloud Composer API", - "description": "Manages Apache Airflow environments on Google Cloud Platform.", - "discoveryRestUrl": "https://composer.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/composer/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:alpha", - "name": "compute", - "version": "alpha", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/alpha/rest", - "discoveryLink": "./apis/compute/alpha/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:beta", - "name": "compute", - "version": "beta", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/beta/rest", - "discoveryLink": "./apis/compute/beta/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "compute:v1", - "name": "compute", - "version": "v1", - "title": "Compute Engine API", - "description": "Creates and runs virtual machines on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/compute/v1/rest", - "discoveryLink": "./apis/compute/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/compute_engine-16.png", - "x32": "https://www.google.com/images/icons/product/compute_engine-32.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/reference/latest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "container:v1", - "name": "container", - "version": "v1", - "title": "Kubernetes Engine API", - "description": "The Google Kubernetes Engine API is used for building and managing container based applications, powered by the open source Kubernetes technology.", - "discoveryRestUrl": "https://container.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/container-engine/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "container:v1beta1", - "name": "container", - "version": "v1beta1", - "title": "Kubernetes Engine API", - "description": "The Google Kubernetes Engine API is used for building and managing container based applications, powered by the open source Kubernetes technology.", - "discoveryRestUrl": "https://container.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/container-engine/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "content:v2", - "name": "content", - "version": "v2", - "title": "Content API for Shopping", - "description": "Manages product items, inventory, and Merchant Center accounts for Google Shopping.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/content/v2/rest", - "discoveryLink": "./apis/content/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/shopping-content", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "customsearch:v1", - "name": "customsearch", - "version": "v1", - "title": "CustomSearch API", - "description": "Searches over a website or collection of websites", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/customsearch/v1/rest", - "discoveryLink": "./apis/customsearch/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/custom-search/v1/using_rest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataflow:v1b3", - "name": "dataflow", - "version": "v1b3", - "title": "Dataflow API", - "description": "Manages Google Cloud Dataflow projects on Google Cloud Platform.", - "discoveryRestUrl": "https://dataflow.googleapis.com/$clientovery/rest?version=v1b3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataflow", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataproc:v1", - "name": "dataproc", - "version": "v1", - "title": "Cloud Dataproc API", - "description": "Manages Hadoop-based clusters and jobs on Google Cloud Platform.", - "discoveryRestUrl": "https://dataproc.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataproc/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dataproc:v1beta2", - "name": "dataproc", - "version": "v1beta2", - "title": "Cloud Dataproc API", - "description": "Manages Hadoop-based clusters and jobs on Google Cloud Platform.", - "discoveryRestUrl": "https://dataproc.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dataproc/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1", - "name": "datastore", - "version": "v1", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1beta1", - "name": "datastore", - "version": "v1beta1", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "datastore:v1beta3", - "name": "datastore", - "version": "v1beta3", - "title": "Cloud Datastore API", - "description": "Accesses the schemaless NoSQL database to provide fully managed, robust, scalable storage for your application.", - "discoveryRestUrl": "https://datastore.googleapis.com/$clientovery/rest?version=v1beta3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/datastore/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:alpha", - "name": "deploymentmanager", - "version": "alpha", - "title": "Google Cloud Deployment Manager Alpha API", - "description": "The Deployment Manager API allows users to declaratively configure, deploy and run complex solutions on the Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/alpha/rest", - "discoveryLink": "./apis/deploymentmanager/alpha/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:v2beta", - "name": "deploymentmanager", - "version": "v2beta", - "title": "Google Cloud Deployment Manager API V2Beta Methods", - "description": "The Deployment Manager API allows users to declaratively configure, deploy and run complex solutions on the Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/v2beta/rest", - "discoveryLink": "./apis/deploymentmanager/v2beta/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/deployment-manager/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "deploymentmanager:v2", - "name": "deploymentmanager", - "version": "v2", - "title": "Google Cloud Deployment Manager API", - "description": "Declares, configures, and deploys complex solutions on Google Cloud Platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/deploymentmanager/v2/rest", - "discoveryLink": "./apis/deploymentmanager/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v2.8", - "name": "dfareporting", - "version": "v2.8", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v2.8/rest", - "discoveryLink": "./apis/dfareporting/v2.8/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.0", - "name": "dfareporting", - "version": "v3.0", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.0/rest", - "discoveryLink": "./apis/dfareporting/v3.0/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.1", - "name": "dfareporting", - "version": "v3.1", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.1/rest", - "discoveryLink": "./apis/dfareporting/v3.1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dfareporting:v3.2", - "name": "dfareporting", - "version": "v3.2", - "title": "DCM/DFA Reporting And Trafficking API", - "description": "Manages your DoubleClick Campaign Manager ad campaigns and reports.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dfareporting/v3.2/rest", - "discoveryLink": "./apis/dfareporting/v3.2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/doubleclick-16.gif", - "x32": "https://www.google.com/images/icons/product/doubleclick-32.gif" - }, - "documentationLink": "https://developers.google.com/doubleclick-advertisers/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dialogflow:v2", - "name": "dialogflow", - "version": "v2", - "title": "Dialogflow API", - "description": "Builds conversational interfaces (for example, chatbots, and voice-powered apps and devices).", - "discoveryRestUrl": "https://dialogflow.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dialogflow-enterprise/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dialogflow:v2beta1", - "name": "dialogflow", - "version": "v2beta1", - "title": "Dialogflow API", - "description": "Builds conversational interfaces (for example, chatbots, and voice-powered apps and devices).", - "discoveryRestUrl": "https://dialogflow.googleapis.com/$clientovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dialogflow-enterprise/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "digitalassetlinks:v1", - "name": "digitalassetlinks", - "version": "v1", - "title": "Digital Asset Links API", - "description": "Discovers relationships between online assets such as websites or mobile apps.", - "discoveryRestUrl": "https://digitalassetlinks.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/digital-asset-links/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "discovery:v1", - "name": "discovery", - "version": "v1", - "title": "APIs Discovery Service", - "description": "Provides information about other Google APIs, such as what APIs are available, the resource, and method details for each API.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/discovery/v1/rest", - "discoveryLink": "./apis/discovery/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/feature/filing_cabinet_search-g16.png", - "x32": "http://www.google.com/images/icons/feature/filing_cabinet_search-g32.png" - }, - "documentationLink": "https://developers.google.com/discovery/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dlp:v2", - "name": "dlp", - "version": "v2", - "title": "Cloud Data Loss Prevention (DLP) API", - "description": "Provides methods for detection, risk analysis, and de-identification of privacy-sensitive fragments in text, images, and Google Cloud Platform storage repositories.", - "discoveryRestUrl": "https://dlp.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/dlp/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v1", - "name": "dns", - "version": "v1", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v1/rest", - "discoveryLink": "./apis/dns/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v1beta2", - "name": "dns", - "version": "v1beta2", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v1beta2/rest", - "discoveryLink": "./apis/dns/v1beta2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "dns:v2beta1", - "name": "dns", - "version": "v2beta1", - "title": "Google Cloud DNS API", - "description": "Configures and serves authoritative DNS records.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/dns/v2beta1/rest", - "discoveryLink": "./apis/dns/v2beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-dns", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "doubleclickbidmanager:v1", - "name": "doubleclickbidmanager", - "version": "v1", - "title": "DoubleClick Bid Manager API", - "description": "API for viewing and managing your reports in DoubleClick Bid Manager.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/doubleclickbidmanager/v1/rest", - "discoveryLink": "./apis/doubleclickbidmanager/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/bid-manager/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "doubleclicksearch:v2", - "name": "doubleclicksearch", - "version": "v2", - "title": "DoubleClick Search API", - "description": "Reports and modifies your advertising data in DoubleClick Search (for example, campaigns, ad groups, keywords, and conversions).", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/doubleclicksearch/v2/rest", - "discoveryLink": "./apis/doubleclicksearch/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/doubleclick-search/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "drive:v2", - "name": "drive", - "version": "v2", - "title": "Drive API", - "description": "Manages files in Drive including uploading, downloading, searching, detecting changes, and updating sharing permissions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/drive/v2/rest", - "discoveryLink": "./apis/drive/v2/rest", - "icons": { - "x16": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_16.png", - "x32": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_32.png" - }, - "documentationLink": "https://developers.google.com/drive/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "drive:v3", - "name": "drive", - "version": "v3", - "title": "Drive API", - "description": "Manages files in Drive including uploading, downloading, searching, detecting changes, and updating sharing permissions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", - "discoveryLink": "./apis/drive/v3/rest", - "icons": { - "x16": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_16.png", - "x32": "https://ssl.gstatic.com/docs/doclist/images/drive_icon_32.png" - }, - "documentationLink": "https://developers.google.com/drive/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "file:v1beta1", - "name": "file", - "version": "v1beta1", - "title": "Cloud Filestore API", - "description": "The Cloud Filestore API is used for creating and managing cloud file servers.", - "discoveryRestUrl": "https://file.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/filestore/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebasedynamiclinks:v1", - "name": "firebasedynamiclinks", - "version": "v1", - "title": "Firebase Dynamic Links API", - "description": "Programmatically creates and manages Firebase Dynamic Links.", - "discoveryRestUrl": "https://firebasedynamiclinks.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/dynamic-links/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebasehosting:v1beta1", - "name": "firebasehosting", - "version": "v1beta1", - "title": "Firebase Hosting API", - "description": "", - "discoveryRestUrl": "https://firebasehosting.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/hosting/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firebaserules:v1", - "name": "firebaserules", - "version": "v1", - "title": "Firebase Rules API", - "description": "Creates and manages rules that determine when a Firebase Rules-enabled service should permit a request.", - "discoveryRestUrl": "https://firebaserules.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/storage/security", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1", - "name": "firestore", - "version": "v1", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1beta1", - "name": "firestore", - "version": "v1beta1", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "firestore:v1beta2", - "name": "firestore", - "version": "v1beta2", - "title": "Cloud Firestore API", - "description": "Accesses the NoSQL document database built for automatic scaling, high performance, and ease of application development.", - "discoveryRestUrl": "https://firestore.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/firestore", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "fitness:v1", - "name": "fitness", - "version": "v1", - "title": "Fitness", - "description": "Stores and accesses user data in the fitness store from apps on any platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fitness/v1/rest", - "discoveryLink": "./apis/fitness/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fit/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "fusiontables:v1", - "name": "fusiontables", - "version": "v1", - "title": "Fusion Tables API", - "description": "API for working with Fusion Tables data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fusiontables/v1/rest", - "discoveryLink": "./apis/fusiontables/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fusiontables", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "fusiontables:v2", - "name": "fusiontables", - "version": "v2", - "title": "Fusion Tables API", - "description": "API for working with Fusion Tables data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/fusiontables/v2/rest", - "discoveryLink": "./apis/fusiontables/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/fusiontables", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "games:v1", - "name": "games", - "version": "v1", - "title": "Google Play Game Services API", - "description": "The API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/games/v1/rest", - "discoveryLink": "./apis/games/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gamesConfiguration:v1configuration", - "name": "gamesConfiguration", - "version": "v1configuration", - "title": "Google Play Game Services Publishing API", - "description": "The Publishing API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gamesConfiguration/v1configuration/rest", - "discoveryLink": "./apis/gamesConfiguration/v1configuration/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gamesManagement:v1management", - "name": "gamesManagement", - "version": "v1management", - "title": "Google Play Game Services Management API", - "description": "The Management API for Google Play Game Services.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gamesManagement/v1management/rest", - "discoveryLink": "./apis/gamesManagement/v1management/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/games/services", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v1alpha2", - "name": "genomics", - "version": "v1alpha2", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$clientovery/rest?version=v1alpha2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v2alpha1", - "name": "genomics", - "version": "v2alpha1", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$clientovery/rest?version=v2alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "genomics:v1", - "name": "genomics", - "version": "v1", - "title": "Genomics API", - "description": "Uploads, processes, queries, and searches Genomics data in the cloud.", - "discoveryRestUrl": "https://genomics.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/genomics", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "gmail:v1", - "name": "gmail", - "version": "v1", - "title": "Gmail API", - "description": "Access Gmail mailboxes including sending user email.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", - "discoveryLink": "./apis/gmail/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/googlemail-16.png", - "x32": "https://www.google.com/images/icons/product/googlemail-32.png" - }, - "documentationLink": "https://developers.google.com/gmail/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "groupsmigration:v1", - "name": "groupsmigration", - "version": "v1", - "title": "Groups Migration API", - "description": "Groups Migration Api.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/groupsmigration/v1/rest", - "discoveryLink": "./apis/groupsmigration/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/discussions-16.gif", - "x32": "https://www.google.com/images/icons/product/discussions-32.gif" - }, - "documentationLink": "https://developers.google.com/google-apps/groups-migration/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "groupssettings:v1", - "name": "groupssettings", - "version": "v1", - "title": "Groups Settings API", - "description": "Lets you manage permission levels and related settings of a group.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/groupssettings/v1/rest", - "discoveryLink": "./apis/groupssettings/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/groups-settings/get_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iam:v1", - "name": "iam", - "version": "v1", - "title": "Identity and Access Management (IAM) API", - "description": "Manages identity and access control for Google Cloud Platform resources, including the creation of service accounts, which you can use to authenticate to Google and make API calls.", - "discoveryRestUrl": "https://iam.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iam/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iamcredentials:v1", - "name": "iamcredentials", - "version": "v1", - "title": "IAM Service Account Credentials API", - "description": "Creates short-lived, limited-privilege credentials for IAM service accounts.", - "discoveryRestUrl": "https://iamcredentials.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "iap:v1beta1", - "name": "iap", - "version": "v1beta1", - "title": "Cloud Identity-Aware Proxy API", - "description": "Controls access to cloud applications running on Google Cloud Platform.", - "discoveryRestUrl": "https://iap.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/iap", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "identitytoolkit:v3", - "name": "identitytoolkit", - "version": "v3", - "title": "Google Identity Toolkit API", - "description": "Help the third party sites to implement federated login.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/identitytoolkit/v3/rest", - "discoveryLink": "./apis/identitytoolkit/v3/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/identity-toolkit/v3/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "indexing:v3", - "name": "indexing", - "version": "v3", - "title": "Indexing API", - "description": "Notifies Google when your web pages change.", - "discoveryRestUrl": "https://indexing.googleapis.com/$clientovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/search/apis/indexing-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v3p1beta1", - "name": "jobs", - "version": "v3p1beta1", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$clientovery/rest?version=v3p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v2", - "name": "jobs", - "version": "v2", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "jobs:v3", - "name": "jobs", - "version": "v3", - "title": "Cloud Talent Solution API", - "description": "Cloud Talent Solution provides the capability to create, read, update, and delete job postings, as well as search jobs based on keywords and filters.", - "discoveryRestUrl": "https://jobs.googleapis.com/$clientovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/talent-solution/job-search/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "kgsearch:v1", - "name": "kgsearch", - "version": "v1", - "title": "Knowledge Graph Search API", - "description": "Searches the Google Knowledge Graph for entities.", - "discoveryRestUrl": "https://kgsearch.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/knowledge-graph/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1", - "name": "language", - "version": "v1", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1beta1", - "name": "language", - "version": "v1beta1", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "language:v1beta2", - "name": "language", - "version": "v1beta2", - "title": "Cloud Natural Language API", - "description": "Provides natural language understanding technologies to developers. Examples include sentiment analysis, entity recognition, entity sentiment analysis, and text annotations.", - "discoveryRestUrl": "https://language.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/natural-language/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "licensing:v1", - "name": "licensing", - "version": "v1", - "title": "Enterprise License Manager API", - "description": "Views and manages licenses for your domain.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/licensing/v1/rest", - "discoveryLink": "./apis/licensing/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/licensing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "logging:v2", - "name": "logging", - "version": "v2", - "title": "Stackdriver Logging API", - "description": "Writes log entries and manages your Logging configuration.", - "discoveryRestUrl": "https://logging.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/logging/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "logging:v2beta1", - "name": "logging", - "version": "v2beta1", - "title": "Stackdriver Logging API", - "description": "Writes log entries and manages your Logging configuration.", - "discoveryRestUrl": "https://logging.googleapis.com/$clientovery/rest?version=v2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/logging/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "manufacturers:v1", - "name": "manufacturers", - "version": "v1", - "title": "Manufacturer Center API", - "description": "Public API for managing Manufacturer Center related data.", - "discoveryRestUrl": "https://manufacturers.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/manufacturers/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "mirror:v1", - "name": "mirror", - "version": "v1", - "title": "Google Mirror API", - "description": "Interacts with Glass users via the timeline.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/mirror/v1/rest", - "discoveryLink": "./apis/mirror/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/glass", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "ml:v1", - "name": "ml", - "version": "v1", - "title": "Cloud Machine Learning Engine", - "description": "An API to enable creating and using machine learning models.", - "discoveryRestUrl": "https://ml.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/ml/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "monitoring:v3", - "name": "monitoring", - "version": "v3", - "title": "Stackdriver Monitoring API", - "description": "Manages your Stackdriver Monitoring data and configurations. Most projects must be associated with a Stackdriver account, with a few exceptions as noted on the individual method pages.", - "discoveryRestUrl": "https://monitoring.googleapis.com/$clientovery/rest?version=v3", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/monitoring/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "oauth2:v1", - "name": "oauth2", - "version": "v1", - "title": "Google OAuth2 API", - "description": "Obtains end-user authorization grants for use with other Google APIs.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest", - "discoveryLink": "./apis/oauth2/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/accounts/docs/OAuth2", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oauth2:v2", - "name": "oauth2", - "version": "v2", - "title": "Google OAuth2 API", - "description": "Obtains end-user authorization grants for use with other Google APIs.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest", - "discoveryLink": "./apis/oauth2/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/accounts/docs/OAuth2", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1alpha", - "name": "oslogin", - "version": "v1alpha", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$clientovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1beta", - "name": "oslogin", - "version": "v1beta", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$clientovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "oslogin:v1", - "name": "oslogin", - "version": "v1", - "title": "Cloud OS Login API", - "description": "Manages OS login configuration for Google account users.", - "discoveryRestUrl": "https://oslogin.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/oslogin/rest/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v1", - "name": "pagespeedonline", - "version": "v1", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v1/rest", - "discoveryLink": "./apis/pagespeedonline/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v1/getting_started", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v2", - "name": "pagespeedonline", - "version": "v2", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v2/rest", - "discoveryLink": "./apis/pagespeedonline/v2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v2/getting-started", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pagespeedonline:v4", - "name": "pagespeedonline", - "version": "v4", - "title": "PageSpeed Insights API", - "description": "Analyzes the performance of a web page and provides tailored suggestions to make that page faster.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/pagespeedonline/v4/rest", - "discoveryLink": "./apis/pagespeedonline/v4/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/pagespeed-16.png", - "x32": "https://www.google.com/images/icons/product/pagespeed-32.png" - }, - "documentationLink": "https://developers.google.com/speed/docs/insights/v4/getting-started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "partners:v2", - "name": "partners", - "version": "v2", - "title": "Google Partners API", - "description": "Searches certified companies and creates contact leads with them, and also audits the usage of clients.", - "discoveryRestUrl": "https://partners.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/partners/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "people:v1", - "name": "people", - "version": "v1", - "title": "People API", - "description": "Provides access to information about profiles and contacts.", - "discoveryRestUrl": "https://people.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/people/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "playcustomapp:v1", - "name": "playcustomapp", - "version": "v1", - "title": "Google Play Custom App Publishing API", - "description": "An API to publish custom Android apps.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/playcustomapp/v1/rest", - "discoveryLink": "./apis/playcustomapp/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/android/work/play/custom-app-api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "plus:v1", - "name": "plus", - "version": "v1", - "title": "Google+ API", - "description": "Builds on top of the Google+ platform.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest", - "discoveryLink": "./apis/plus/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/gplus-16.png", - "x32": "http://www.google.com/images/icons/product/gplus-32.png" - }, - "documentationLink": "https://developers.google.com/+/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "plusDomains:v1", - "name": "plusDomains", - "version": "v1", - "title": "Google+ Domains API", - "description": "Builds on top of the Google+ platform for Google Apps Domains.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/plusDomains/v1/rest", - "discoveryLink": "./apis/plusDomains/v1/rest", - "icons": { - "x16": "http://www.google.com/images/icons/product/gplus-16.png", - "x32": "http://www.google.com/images/icons/product/gplus-32.png" - }, - "documentationLink": "https://developers.google.com/+/domains/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "poly:v1", - "name": "poly", - "version": "v1", - "title": "Poly API", - "description": "The Poly API provides read access to assets hosted on poly.google.com to all, and upload access to poly.google.com for whitelisted accounts.", - "discoveryRestUrl": "https://poly.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/poly/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "proximitybeacon:v1beta1", - "name": "proximitybeacon", - "version": "v1beta1", - "title": "Proximity Beacon API", - "description": "Registers, manages, indexes, and searches beacons.", - "discoveryRestUrl": "https://proximitybeacon.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/beacons/proximity/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1beta1a", - "name": "pubsub", - "version": "v1beta1a", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$clientovery/rest?version=v1beta1a", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1", - "name": "pubsub", - "version": "v1", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "pubsub:v1beta2", - "name": "pubsub", - "version": "v1beta2", - "title": "Cloud Pub/Sub API", - "description": "Provides reliable, many-to-many, asynchronous messaging between applications.", - "discoveryRestUrl": "https://pubsub.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/pubsub/docs", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "redis:v1", - "name": "redis", - "version": "v1", - "title": "Google Cloud Memorystore for Redis API", - "description": "The Google Cloud Memorystore for Redis API is used for creating and managing Redis instances on the Google Cloud Platform.", - "discoveryRestUrl": "https://redis.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/memorystore/docs/redis/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "redis:v1beta1", - "name": "redis", - "version": "v1beta1", - "title": "Google Cloud Memorystore for Redis API", - "description": "The Google Cloud Memorystore for Redis API is used for creating and managing Redis instances on the Google Cloud Platform.", - "discoveryRestUrl": "https://redis.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/memorystore/docs/redis/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "replicapool:v1beta1", - "name": "replicapool", - "version": "v1beta1", - "title": "Replica Pool API", - "description": "The Replica Pool API allows users to declaratively provision and manage groups of Google Compute Engine instances based on a common template.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/replicapool/v1beta1/rest", - "discoveryLink": "./apis/replicapool/v1beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/compute/docs/replica-pool/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "replicapoolupdater:v1beta1", - "name": "replicapoolupdater", - "version": "v1beta1", - "title": "Google Compute Engine Instance Group Updater API", - "description": "[Deprecated. Please use compute.instanceGroupManagers.update method. replicapoolupdater API will be disabled after December 30th, 2016] Updates groups of Compute Engine instances.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/replicapoolupdater/v1beta1/rest", - "discoveryLink": "./apis/replicapoolupdater/v1beta1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/compute/docs/instance-groups/manager/#applying_rolling_updates_using_the_updater_service", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "reseller:v1", - "name": "reseller", - "version": "v1", - "title": "Enterprise Apps Reseller API", - "description": "Creates and manages your customers and their subscriptions.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/reseller/v1/rest", - "discoveryLink": "./apis/reseller/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/google-apps/reseller/", - "labels": [ - "limited_availability" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "runtimeconfig:v1", - "name": "runtimeconfig", - "version": "v1", - "title": "Cloud Runtime Configuration API", - "description": "The Runtime Configurator allows you to dynamically configure and expose variables through Google Cloud Platform. In addition, you can also set Watchers and Waiters that will watch for changes to your data and return based on certain conditions.", - "discoveryRestUrl": "https://runtimeconfig.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/runtime-configurator/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "runtimeconfig:v1beta1", - "name": "runtimeconfig", - "version": "v1beta1", - "title": "Cloud Runtime Configuration API", - "description": "The Runtime Configurator allows you to dynamically configure and expose variables through Google Cloud Platform. In addition, you can also set Watchers and Waiters that will watch for changes to your data and return based on certain conditions.", - "discoveryRestUrl": "https://runtimeconfig.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/deployment-manager/runtime-configurator/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "safebrowsing:v4", - "name": "safebrowsing", - "version": "v4", - "title": "Safe Browsing API", - "description": "Enables client applications to check web resources (most commonly URLs) against Google-generated lists of unsafe web resources.", - "discoveryRestUrl": "https://safebrowsing.googleapis.com/$clientovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/safe-browsing/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "script:v1", - "name": "script", - "version": "v1", - "title": "Apps Script API", - "description": "Manages and executes Google Apps Script projects.", - "discoveryRestUrl": "https://script.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/apps-script/api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "searchconsole:v1", - "name": "searchconsole", - "version": "v1", - "title": "Google Search Console URL Testing Tools API", - "description": "Provides tools for running validation tests against single URLs", - "discoveryRestUrl": "https://searchconsole.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/webmaster-tools/search-console-api/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1alpha1", - "name": "servicebroker", - "version": "v1alpha1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$clientovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1", - "name": "servicebroker", - "version": "v1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicebroker:v1beta1", - "name": "servicebroker", - "version": "v1beta1", - "title": "Service Broker API", - "description": "The Google Cloud Platform Service Broker API provides Google hosted implementation of the Open Service Broker API (https://www.openservicebrokerapi.org/).", - "discoveryRestUrl": "https://servicebroker.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/service-broker", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "serviceconsumermanagement:v1", - "name": "serviceconsumermanagement", - "version": "v1", - "title": "Service Consumer Management API", - "description": "Manages the service consumers of a Service Infrastructure service.", - "discoveryRestUrl": "https://serviceconsumermanagement.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-consumer-management/docs/overview", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicecontrol:v1", - "name": "servicecontrol", - "version": "v1", - "title": "Service Control API", - "description": "Provides control plane functionality to managed services, such as logging, monitoring, and status checks.", - "discoveryRestUrl": "https://servicecontrol.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-control/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicemanagement:v1", - "name": "servicemanagement", - "version": "v1", - "title": "Service Management API", - "description": "Google Service Management allows service producers to publish their services on Google Cloud Platform so that they can be discovered and used by service consumers.", - "discoveryRestUrl": "https://servicemanagement.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-management/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "servicenetworking:v1beta", - "name": "servicenetworking", - "version": "v1beta", - "title": "Service Networking API", - "description": "Provides automatic management of network configurations necessary for certain services.", - "discoveryRestUrl": "https://servicenetworking.googleapis.com/$clientovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-infrastructure/docs/service-networking/getting-started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "serviceusage:v1", - "name": "serviceusage", - "version": "v1", - "title": "Service Usage API", - "description": "Enables services that service consumers want to use on Google Cloud Platform, lists the available or enabled services, or disables services that service consumers no longer use.", - "discoveryRestUrl": "https://serviceusage.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-usage/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "serviceusage:v1beta1", - "name": "serviceusage", - "version": "v1beta1", - "title": "Service Usage API", - "description": "Enables services that service consumers want to use on Google Cloud Platform, lists the available or enabled services, or disables services that service consumers no longer use.", - "discoveryRestUrl": "https://serviceusage.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/service-usage/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "sheets:v4", - "name": "sheets", - "version": "v4", - "title": "Google Sheets API", - "description": "Reads and writes Google Sheets.", - "discoveryRestUrl": "https://sheets.googleapis.com/$clientovery/rest?version=v4", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/sheets/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "siteVerification:v1", - "name": "siteVerification", - "version": "v1", - "title": "Google Site Verification API", - "description": "Verifies ownership of websites or domains with Google.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/siteVerification/v1/rest", - "discoveryLink": "./apis/siteVerification/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/site-verification/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "slides:v1", - "name": "slides", - "version": "v1", - "title": "Google Slides API", - "description": "An API for creating and editing Google Slides presentations.", - "discoveryRestUrl": "https://slides.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/slides/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "sourcerepo:v1", - "name": "sourcerepo", - "version": "v1", - "title": "Cloud Source Repositories API", - "description": "Access source code repositories hosted by Google.", - "discoveryRestUrl": "https://sourcerepo.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/source-repositories/docs/apis", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "spanner:v1", - "name": "spanner", - "version": "v1", - "title": "Cloud Spanner API", - "description": "Cloud Spanner is a managed, mission-critical, globally consistent and scalable relational database service.", - "discoveryRestUrl": "https://spanner.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/spanner/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "speech:v1", - "name": "speech", - "version": "v1", - "title": "Cloud Speech API", - "description": "Converts audio to text by applying powerful neural network models.", - "discoveryRestUrl": "https://speech.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/speech-to-text/docs/quickstart-protocol", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "speech:v1beta1", - "name": "speech", - "version": "v1beta1", - "title": "Cloud Speech API", - "description": "Converts audio to text by applying powerful neural network models.", - "discoveryRestUrl": "https://speech.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/speech-to-text/docs/quickstart-protocol", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "sqladmin:v1beta4", - "name": "sqladmin", - "version": "v1beta4", - "title": "Cloud SQL Admin API", - "description": "Creates and manages Cloud SQL instances, which provide fully managed MySQL or PostgreSQL databases.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/sqladmin/v1beta4/rest", - "discoveryLink": "./apis/sqladmin/v1beta4/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/sql/docs/reference/latest", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1", - "name": "storage", - "version": "v1", - "title": "Cloud Storage JSON API", - "description": "Stores and retrieves potentially large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1/rest", - "discoveryLink": "./apis/storage/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1beta1", - "name": "storage", - "version": "v1beta1", - "title": "Cloud Storage JSON API", - "description": "Lets you store and retrieve potentially-large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1beta1/rest", - "discoveryLink": "./apis/storage/v1beta1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "storage:v1beta2", - "name": "storage", - "version": "v1beta2", - "title": "Cloud Storage JSON API", - "description": "Lets you store and retrieve potentially-large, immutable data objects.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/storage/v1beta2/rest", - "discoveryLink": "./apis/storage/v1beta2/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", - "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" - }, - "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "storagetransfer:v1", - "name": "storagetransfer", - "version": "v1", - "title": "Storage Transfer API", - "description": "Transfers data from external data sources to a Google Cloud Storage bucket or between Google Cloud Storage buckets.", - "discoveryRestUrl": "https://storagetransfer.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/storage/transfer", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "streetviewpublish:v1", - "name": "streetviewpublish", - "version": "v1", - "title": "Street View Publish API", - "description": "Publishes 360 photos to Google Maps, along with position, orientation, and connectivity metadata. Apps can offer an interface for positioning, connecting, and uploading user-generated Street View images.", - "discoveryRestUrl": "https://streetviewpublish.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/streetview/publish/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "surveys:v2", - "name": "surveys", - "version": "v2", - "title": "Surveys API", - "description": "Creates and conducts surveys, lists the surveys that an authenticated user owns, and retrieves survey results and information about specified surveys.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/surveys/v2/rest", - "discoveryLink": "./apis/surveys/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tagmanager:v1", - "name": "tagmanager", - "version": "v1", - "title": "Tag Manager API", - "description": "Accesses Tag Manager accounts and containers.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tagmanager/v1/rest", - "discoveryLink": "./apis/tagmanager/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/tag-manager/api/v1/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "tagmanager:v2", - "name": "tagmanager", - "version": "v2", - "title": "Tag Manager API", - "description": "Accesses Tag Manager accounts and containers.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tagmanager/v2/rest", - "discoveryLink": "./apis/tagmanager/v2/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/tag-manager/api/v2/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tasks:v1", - "name": "tasks", - "version": "v1", - "title": "Tasks API", - "description": "Lets you manage your tasks and task lists.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/tasks/v1/rest", - "discoveryLink": "./apis/tasks/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/tasks-16.png", - "x32": "https://www.google.com/images/icons/product/tasks-32.png" - }, - "documentationLink": "https://developers.google.com/google-apps/tasks/firstapp", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "testing:v1", - "name": "testing", - "version": "v1", - "title": "Cloud Testing API", - "description": "Allows developers to run automated tests for their mobile applications on Google infrastructure.", - "discoveryRestUrl": "https://testing.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/cloud-test-lab/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "texttospeech:v1", - "name": "texttospeech", - "version": "v1", - "title": "Cloud Text-to-Speech API", - "description": "Synthesizes natural-sounding speech by applying powerful neural network models.", - "discoveryRestUrl": "https://texttospeech.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/text-to-speech/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "texttospeech:v1beta1", - "name": "texttospeech", - "version": "v1beta1", - "title": "Cloud Text-to-Speech API", - "description": "Synthesizes natural-sounding speech by applying powerful neural network models.", - "discoveryRestUrl": "https://texttospeech.googleapis.com/$clientovery/rest?version=v1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/text-to-speech/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "toolresults:v1beta3", - "name": "toolresults", - "version": "v1beta3", - "title": "Cloud Tool Results API", - "description": "Reads and publishes results from Firebase Test Lab.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/toolresults/v1beta3/rest", - "discoveryLink": "./apis/toolresults/v1beta3/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://firebase.google.com/docs/test-lab/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "tpu:v1alpha1", - "name": "tpu", - "version": "v1alpha1", - "title": "Cloud TPU API", - "description": "TPU API provides customers with access to Google TPU technology.", - "discoveryRestUrl": "https://tpu.googleapis.com/$clientovery/rest?version=v1alpha1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tpu/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "tpu:v1", - "name": "tpu", - "version": "v1", - "title": "Cloud TPU API", - "description": "TPU API provides customers with access to Google TPU technology.", - "discoveryRestUrl": "https://tpu.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/tpu/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "translate:v2", - "name": "translate", - "version": "v2", - "title": "Cloud Translation API", - "description": "Integrates text translation into your website or application.", - "discoveryRestUrl": "https://translation.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://code.google.com/apis/language/translate/v2/getting_started.html", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "urlshortener:v1", - "name": "urlshortener", - "version": "v1", - "title": "URL Shortener API", - "description": "Lets you create, inspect, and manage goo.gl short URLs", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/urlshortener/v1/rest", - "discoveryLink": "./apis/urlshortener/v1/rest", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/url-shortener/v1/getting_started", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "vault:v1", - "name": "vault", - "version": "v1", - "title": "G Suite Vault API", - "description": "Archiving and eDiscovery for G Suite.", - "discoveryRestUrl": "https://vault.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/vault", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1p1beta1", - "name": "videointelligence", - "version": "v1p1beta1", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$clientovery/rest?version=v1p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1", - "name": "videointelligence", - "version": "v1", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "videointelligence:v1beta2", - "name": "videointelligence", - "version": "v1beta2", - "title": "Cloud Video Intelligence API", - "description": "Cloud Video Intelligence API.", - "discoveryRestUrl": "https://videointelligence.googleapis.com/$clientovery/rest?version=v1beta2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/video-intelligence/docs/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1p1beta1", - "name": "vision", - "version": "v1p1beta1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$clientovery/rest?version=v1p1beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1p2beta1", - "name": "vision", - "version": "v1p2beta1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$clientovery/rest?version=v1p2beta1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "vision:v1", - "name": "vision", - "version": "v1", - "title": "Cloud Vision API", - "description": "Integrates Google Vision features, including image labeling, face, logo, and landmark detection, optical character recognition (OCR), and detection of explicit content, into applications.", - "discoveryRestUrl": "https://vision.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/vision/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "webfonts:v1", - "name": "webfonts", - "version": "v1", - "title": "Google Fonts Developer API", - "description": "Accesses the metadata for all families served by Google Fonts, providing a list of families currently available (including available styles and a list of supported script subsets).", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/webfonts/v1/rest", - "discoveryLink": "./apis/webfonts/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/feature/font_api-16.png", - "x32": "https://www.google.com/images/icons/feature/font_api-32.gif" - }, - "documentationLink": "https://developers.google.com/fonts/docs/developer_api", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "webmasters:v3", - "name": "webmasters", - "version": "v3", - "title": "Search Console API", - "description": "View Google Search Console data for your verified sites.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/webmasters/v3/rest", - "discoveryLink": "./apis/webmasters/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/webmaster_tools-16.png", - "x32": "https://www.google.com/images/icons/product/webmaster_tools-32.png" - }, - "documentationLink": "https://developers.google.com/webmaster-tools/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "websecurityscanner:v1alpha", - "name": "websecurityscanner", - "version": "v1alpha", - "title": "Web Security Scanner API", - "description": "Web Security Scanner API (under development).", - "discoveryRestUrl": "https://websecurityscanner.googleapis.com/$clientovery/rest?version=v1alpha", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/security-scanner/", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "websecurityscanner:v1beta", - "name": "websecurityscanner", - "version": "v1beta", - "title": "Web Security Scanner API", - "description": "Web Security Scanner API (under development).", - "discoveryRestUrl": "https://websecurityscanner.googleapis.com/$clientovery/rest?version=v1beta", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://cloud.google.com/security-scanner/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtube:v3", - "name": "youtube", - "version": "v3", - "title": "YouTube Data API", - "description": "Supports core YouTube features, such as uploading videos, creating and managing playlists, searching for content, and much more.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest", - "discoveryLink": "./apis/youtube/v3/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "https://developers.google.com/youtube/v3", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v1", - "name": "youtubeAnalytics", - "version": "v1", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtubeAnalytics/v1/rest", - "discoveryLink": "./apis/youtubeAnalytics/v1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "http://developers.google.com/youtube/analytics/", - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v1beta1", - "name": "youtubeAnalytics", - "version": "v1beta1", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://www.googleapis.com/discovery/v1/apis/youtubeAnalytics/v1beta1/rest", - "discoveryLink": "./apis/youtubeAnalytics/v1beta1/rest", - "icons": { - "x16": "https://www.google.com/images/icons/product/youtube-16.png", - "x32": "https://www.google.com/images/icons/product/youtube-32.png" - }, - "documentationLink": "http://developers.google.com/youtube/analytics/", - "labels": [ - "deprecated" - ], - "preferred": false - }, - { - "kind": "discovery#directoryItem", - "id": "youtubeAnalytics:v2", - "name": "youtubeAnalytics", - "version": "v2", - "title": "YouTube Analytics API", - "description": "Retrieves your YouTube Analytics data.", - "discoveryRestUrl": "https://youtubeanalytics.googleapis.com/$clientovery/rest?version=v2", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/youtube/analytics", - "preferred": true - }, - { - "kind": "discovery#directoryItem", - "id": "youtubereporting:v1", - "name": "youtubereporting", - "version": "v1", - "title": "YouTube Reporting API", - "description": "Schedules reporting jobs containing your YouTube Analytics data and downloads the resulting bulk data reports in the form of CSV files.", - "discoveryRestUrl": "https://youtubereporting.googleapis.com/$clientovery/rest?version=v1", - "icons": { - "x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png", - "x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png" - }, - "documentationLink": "https://developers.google.com/youtube/reporting/v1/reports/", - "preferred": true - } - ] -} -_END -} - -sub pre_get_gmail_spec_json -{ - return <<'__END' -{ - "kind": "discovery#restDescription", - "etag": "\"J3WqvAcMk4eQjJXvfSI4Yr8VouA/zhBQnUVXp-QQ6Y6QUt5UiGZD_sA\"", - "discoveryVersion": "v1", - "id": "gmail:v1", - "name": "gmail", - "version": "v1", - "revision": "20180904", - "title": "Gmail API", - "description": "Access Gmail mailboxes including sending user email.", - "ownerDomain": "google.com", - "ownerName": "Google", - "icons": { - "x16": "https://www.google.com/images/icons/product/googlemail-16.png", - "x32": "https://www.google.com/images/icons/product/googlemail-32.png" - }, - "documentationLink": "https://developers.google.com/gmail/api/", - "protocol": "rest", - "baseUrl": "https://www.googleapis.com/gmail/v1/users/", - "basePath": "/gmail/v1/users/", - "rootUrl": "https://www.googleapis.com/", - "servicePath": "gmail/v1/users/", - "batchPath": "batch/gmail/v1", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "quotaUser": { - "type": "string", - "description": "An opaque string that represents a user for quota purposes. Must not exceed 40 characters.", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "Deprecated. Please use quotaUser instead.", - "location": "query" - } - }, - "auth": { - "oauth2": { - "scopes": { - "https://mail.google.com/": { - "description": "Read, send, delete, and manage your email" - }, - "https://www.googleapis.com/auth/gmail.compose": { - "description": "Manage drafts and send emails" - }, - "https://www.googleapis.com/auth/gmail.insert": { - "description": "Insert mail into your mailbox" - }, - "https://www.googleapis.com/auth/gmail.labels": { - "description": "Manage mailbox labels" - }, - "https://www.googleapis.com/auth/gmail.metadata": { - "description": "View your email message metadata such as labels and headers, but not the email body" - }, - "https://www.googleapis.com/auth/gmail.modify": { - "description": "View and modify but not delete your email" - }, - "https://www.googleapis.com/auth/gmail.readonly": { - "description": "View your email messages and settings" - }, - "https://www.googleapis.com/auth/gmail.send": { - "description": "Send email on your behalf" - }, - "https://www.googleapis.com/auth/gmail.settings.basic": { - "description": "Manage your basic mail settings" - }, - "https://www.googleapis.com/auth/gmail.settings.sharing": { - "description": "Manage your sensitive mail settings, including who can manage your mail" - } - } - } - }, - "schemas": { - "AutoForwarding": { - "id": "AutoForwarding", - "type": "object", - "description": "Auto-forwarding settings for an account.", - "properties": { - "disposition": { - "type": "string", - "description": "The state that a message should be left in after it has been forwarded.", - "enum": [ - "archive", - "dispositionUnspecified", - "leaveInInbox", - "markRead", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - }, - "emailAddress": { - "type": "string", - "description": "Email address to which all incoming messages are forwarded. This email address must be a verified member of the forwarding addresses." - }, - "enabled": { - "type": "boolean", - "description": "Whether all incoming mail is automatically forwarded to another address." - } - } - }, - "BatchDeleteMessagesRequest": { - "id": "BatchDeleteMessagesRequest", - "type": "object", - "properties": { - "ids": { - "type": "array", - "description": "The IDs of the messages to delete.", - "items": { - "type": "string" - } - } - } - }, - "BatchModifyMessagesRequest": { - "id": "BatchModifyMessagesRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of label IDs to add to messages.", - "items": { - "type": "string" - } - }, - "ids": { - "type": "array", - "description": "The IDs of the messages to modify. There is a limit of 1000 ids per request.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list of label IDs to remove from messages.", - "items": { - "type": "string" - } - } - } - }, - "Delegate": { - "id": "Delegate", - "type": "object", - "description": "Settings for a delegate. Delegates can read, send, and delete messages, as well as manage contacts, for the delegator's account. See \"Set up mail delegation\" for more information about delegates.", - "properties": { - "delegateEmail": { - "type": "string", - "description": "The email address of the delegate." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified and can act as a delegate for the account. Read-only.", - "enum": [ - "accepted", - "expired", - "pending", - "rejected", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - } - } - }, - "Draft": { - "id": "Draft", - "type": "object", - "description": "A draft email in the user's mailbox.", - "properties": { - "id": { - "type": "string", - "description": "The immutable ID of the draft.", - "annotations": { - "required": [ - "gmail.users.drafts.send" - ] - } - }, - "message": { - "$ref": "Message", - "description": "The message content of the draft." - } - } - }, - "Filter": { - "id": "Filter", - "type": "object", - "description": "Resource definition for Gmail filters. Filters apply to specific messages instead of an entire email thread.", - "properties": { - "action": { - "$ref": "FilterAction", - "description": "Action that the filter performs." - }, - "criteria": { - "$ref": "FilterCriteria", - "description": "Matching criteria for the filter." - }, - "id": { - "type": "string", - "description": "The server assigned ID of the filter." - } - } - }, - "FilterAction": { - "id": "FilterAction", - "type": "object", - "description": "A set of actions to perform on a message.", - "properties": { - "addLabelIds": { - "type": "array", - "description": "List of labels to add to the message.", - "items": { - "type": "string" - } - }, - "forward": { - "type": "string", - "description": "Email address that the message should be forwarded to." - }, - "removeLabelIds": { - "type": "array", - "description": "List of labels to remove from the message.", - "items": { - "type": "string" - } - } - } - }, - "FilterCriteria": { - "id": "FilterCriteria", - "type": "object", - "description": "Message matching criteria.", - "properties": { - "excludeChats": { - "type": "boolean", - "description": "Whether the response should exclude chats." - }, - "from": { - "type": "string", - "description": "The sender's display name or email address." - }, - "hasAttachment": { - "type": "boolean", - "description": "Whether the message has any attachment." - }, - "negatedQuery": { - "type": "string", - "description": "Only return messages not matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\"." - }, - "query": { - "type": "string", - "description": "Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\"." - }, - "size": { - "type": "integer", - "description": "The size of the entire RFC822 message in bytes, including all headers and attachments.", - "format": "int32" - }, - "sizeComparison": { - "type": "string", - "description": "How the message size in bytes should be in relation to the size field.", - "enum": [ - "larger", - "smaller", - "unspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - }, - "subject": { - "type": "string", - "description": "Case-insensitive phrase found in the message's subject. Trailing and leading whitespace are be trimmed and adjacent spaces are collapsed." - }, - "to": { - "type": "string", - "description": "The recipient's display name or email address. Includes recipients in the \"to\", \"cc\", and \"bcc\" header fields. You can use simply the local part of the email address. For example, \"example\" and \"example@\" both match \"example@gmail.com\". This field is case-insensitive." - } - } - }, - "ForwardingAddress": { - "id": "ForwardingAddress", - "type": "object", - "description": "Settings for a forwarding address.", - "properties": { - "forwardingEmail": { - "type": "string", - "description": "An email address to which messages can be forwarded." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified and is usable for forwarding. Read-only.", - "enum": [ - "accepted", - "pending", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - } - } - }, - "History": { - "id": "History", - "type": "object", - "description": "A record of a change to the user's mailbox. Each history change may affect multiple messages in multiple ways.", - "properties": { - "id": { - "type": "string", - "description": "The mailbox sequence ID.", - "format": "uint64" - }, - "labelsAdded": { - "type": "array", - "description": "Labels added to messages in this history record.", - "items": { - "$ref": "HistoryLabelAdded" - } - }, - "labelsRemoved": { - "type": "array", - "description": "Labels removed from messages in this history record.", - "items": { - "$ref": "HistoryLabelRemoved" - } - }, - "messages": { - "type": "array", - "description": "List of messages changed in this history record. The fields for specific change types, such as messagesAdded may duplicate messages in this field. We recommend using the specific change-type fields instead of this.", - "items": { - "$ref": "Message" - } - }, - "messagesAdded": { - "type": "array", - "description": "Messages added to the mailbox in this history record.", - "items": { - "$ref": "HistoryMessageAdded" - } - }, - "messagesDeleted": { - "type": "array", - "description": "Messages deleted (not Trashed) from the mailbox in this history record.", - "items": { - "$ref": "HistoryMessageDeleted" - } - } - } - }, - "HistoryLabelAdded": { - "id": "HistoryLabelAdded", - "type": "object", - "properties": { - "labelIds": { - "type": "array", - "description": "Label IDs added to the message.", - "items": { - "type": "string" - } - }, - "message": { - "$ref": "Message" - } - } - }, - "HistoryLabelRemoved": { - "id": "HistoryLabelRemoved", - "type": "object", - "properties": { - "labelIds": { - "type": "array", - "description": "Label IDs removed from the message.", - "items": { - "type": "string" - } - }, - "message": { - "$ref": "Message" - } - } - }, - "HistoryMessageAdded": { - "id": "HistoryMessageAdded", - "type": "object", - "properties": { - "message": { - "$ref": "Message" - } - } - }, - "HistoryMessageDeleted": { - "id": "HistoryMessageDeleted", - "type": "object", - "properties": { - "message": { - "$ref": "Message" - } - } - }, - "ImapSettings": { - "id": "ImapSettings", - "type": "object", - "description": "IMAP settings for an account.", - "properties": { - "autoExpunge": { - "type": "boolean", - "description": "If this value is true, Gmail will immediately expunge a message when it is marked as deleted in IMAP. Otherwise, Gmail will wait for an update from the client before expunging messages marked as deleted." - }, - "enabled": { - "type": "boolean", - "description": "Whether IMAP is enabled for the account." - }, - "expungeBehavior": { - "type": "string", - "description": "The action that will be executed on a message when it is marked as deleted and expunged from the last visible IMAP folder.", - "enum": [ - "archive", - "deleteForever", - "expungeBehaviorUnspecified", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "maxFolderSize": { - "type": "integer", - "description": "An optional limit on the number of messages that an IMAP folder may contain. Legal values are 0, 1000, 2000, 5000 or 10000. A value of zero is interpreted to mean that there is no limit.", - "format": "int32" - } - } - }, - "Label": { - "id": "Label", - "type": "object", - "description": "Labels are used to categorize messages and threads within the user's mailbox.", - "properties": { - "color": { - "$ref": "LabelColor", - "description": "The color to assign to the label. Color is only available for labels that have their type set to user." - }, - "id": { - "type": "string", - "description": "The immutable ID of the label.", - "annotations": { - "required": [ - "gmail.users.labels.update" - ] - } - }, - "labelListVisibility": { - "type": "string", - "description": "The visibility of the label in the label list in the Gmail web interface.", - "enum": [ - "labelHide", - "labelShow", - "labelShowIfUnread" - ], - "enumDescriptions": [ - "", - "", - "" - ], - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "messageListVisibility": { - "type": "string", - "description": "The visibility of the label in the message list in the Gmail web interface.", - "enum": [ - "hide", - "show" - ], - "enumDescriptions": [ - "", - "" - ], - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "messagesTotal": { - "type": "integer", - "description": "The total number of messages with the label.", - "format": "int32" - }, - "messagesUnread": { - "type": "integer", - "description": "The number of unread messages with the label.", - "format": "int32" - }, - "name": { - "type": "string", - "description": "The display name of the label.", - "annotations": { - "required": [ - "gmail.users.labels.create", - "gmail.users.labels.update" - ] - } - }, - "threadsTotal": { - "type": "integer", - "description": "The total number of threads with the label.", - "format": "int32" - }, - "threadsUnread": { - "type": "integer", - "description": "The number of unread threads with the label.", - "format": "int32" - }, - "type": { - "type": "string", - "description": "The owner type for the label. User labels are created by the user and can be modified and deleted by the user and can be applied to any message or thread. System labels are internally created and cannot be added, modified, or deleted. System labels may be able to be applied to or removed from messages and threads under some circumstances but this is not guaranteed. For example, users can apply and remove the INBOX and UNREAD labels from messages and threads, but cannot apply or remove the DRAFTS or SENT labels from messages or threads.", - "enum": [ - "system", - "user" - ], - "enumDescriptions": [ - "", - "" - ] - } - } - }, - "LabelColor": { - "id": "LabelColor", - "type": "object", - "properties": { - "backgroundColor": { - "type": "string", - "description": "The background color represented as hex string #RRGGBB (ex #000000). This field is required in order to set the color of a label. Only the following predefined set of color values are allowed:\n#000000, #434343, #666666, #999999, #cccccc, #efefef, #f3f3f3, #ffffff, #fb4c2f, #ffad47, #fad165, #16a766, #43d692, #4a86e8, #a479e2, #f691b3, #f6c5be, #ffe6c7, #fef1d1, #b9e4d0, #c6f3de, #c9daf8, #e4d7f5, #fcdee8, #efa093, #ffd6a2, #fce8b3, #89d3b2, #a0eac9, #a4c2f4, #d0bcf1, #fbc8d9, #e66550, #ffbc6b, #fcda83, #44b984, #68dfa9, #6d9eeb, #b694e8, #f7a7c0, #cc3a21, #eaa041, #f2c960, #149e60, #3dc789, #3c78d8, #8e63ce, #e07798, #ac2b16, #cf8933, #d5ae49, #0b804b, #2a9c68, #285bac, #653e9b, #b65775, #822111, #a46a21, #aa8831, #076239, #1a764d, #1c4587, #41236d, #83334c" - }, - "textColor": { - "type": "string", - "description": "The text color of the label, represented as hex string. This field is required in order to set the color of a label. Only the following predefined set of color values are allowed:\n#000000, #434343, #666666, #999999, #cccccc, #efefef, #f3f3f3, #ffffff, #fb4c2f, #ffad47, #fad165, #16a766, #43d692, #4a86e8, #a479e2, #f691b3, #f6c5be, #ffe6c7, #fef1d1, #b9e4d0, #c6f3de, #c9daf8, #e4d7f5, #fcdee8, #efa093, #ffd6a2, #fce8b3, #89d3b2, #a0eac9, #a4c2f4, #d0bcf1, #fbc8d9, #e66550, #ffbc6b, #fcda83, #44b984, #68dfa9, #6d9eeb, #b694e8, #f7a7c0, #cc3a21, #eaa041, #f2c960, #149e60, #3dc789, #3c78d8, #8e63ce, #e07798, #ac2b16, #cf8933, #d5ae49, #0b804b, #2a9c68, #285bac, #653e9b, #b65775, #822111, #a46a21, #aa8831, #076239, #1a764d, #1c4587, #41236d, #83334c" - } - } - }, - "ListDelegatesResponse": { - "id": "ListDelegatesResponse", - "type": "object", - "description": "Response for the ListDelegates method.", - "properties": { - "delegates": { - "type": "array", - "description": "List of the user's delegates (with any verification status).", - "items": { - "$ref": "Delegate" - } - } - } - }, - "ListDraftsResponse": { - "id": "ListDraftsResponse", - "type": "object", - "properties": { - "drafts": { - "type": "array", - "description": "List of drafts.", - "items": { - "$ref": "Draft" - } - }, - "nextPageToken": { - "type": "string", - "description": "Token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - } - } - }, - "ListFiltersResponse": { - "id": "ListFiltersResponse", - "type": "object", - "description": "Response for the ListFilters method.", - "properties": { - "filter": { - "type": "array", - "description": "List of a user's filters.", - "items": { - "$ref": "Filter" - } - } - } - }, - "ListForwardingAddressesResponse": { - "id": "ListForwardingAddressesResponse", - "type": "object", - "description": "Response for the ListForwardingAddresses method.", - "properties": { - "forwardingAddresses": { - "type": "array", - "description": "List of addresses that may be used for forwarding.", - "items": { - "$ref": "ForwardingAddress" - } - } - } - }, - "ListHistoryResponse": { - "id": "ListHistoryResponse", - "type": "object", - "properties": { - "history": { - "type": "array", - "description": "List of history records. Any messages contained in the response will typically only have id and threadId fields populated.", - "items": { - "$ref": "History" - } - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - }, - "nextPageToken": { - "type": "string", - "description": "Page token to retrieve the next page of results in the list." - } - } - }, - "ListLabelsResponse": { - "id": "ListLabelsResponse", - "type": "object", - "properties": { - "labels": { - "type": "array", - "description": "List of labels.", - "items": { - "$ref": "Label" - } - } - } - }, - "ListMessagesResponse": { - "id": "ListMessagesResponse", - "type": "object", - "properties": { - "messages": { - "type": "array", - "description": "List of messages.", - "items": { - "$ref": "Message" - } - }, - "nextPageToken": { - "type": "string", - "description": "Token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - } - } - }, - "ListSendAsResponse": { - "id": "ListSendAsResponse", - "type": "object", - "description": "Response for the ListSendAs method.", - "properties": { - "sendAs": { - "type": "array", - "description": "List of send-as aliases.", - "items": { - "$ref": "SendAs" - } - } - } - }, - "ListSmimeInfoResponse": { - "id": "ListSmimeInfoResponse", - "type": "object", - "properties": { - "smimeInfo": { - "type": "array", - "description": "List of SmimeInfo.", - "items": { - "$ref": "SmimeInfo" - } - } - } - }, - "ListThreadsResponse": { - "id": "ListThreadsResponse", - "type": "object", - "properties": { - "nextPageToken": { - "type": "string", - "description": "Page token to retrieve the next page of results in the list." - }, - "resultSizeEstimate": { - "type": "integer", - "description": "Estimated total number of results.", - "format": "uint32" - }, - "threads": { - "type": "array", - "description": "List of threads.", - "items": { - "$ref": "Thread" - } - } - } - }, - "Message": { - "id": "Message", - "type": "object", - "description": "An email message.", - "properties": { - "historyId": { - "type": "string", - "description": "The ID of the last history record that modified this message.", - "format": "uint64" - }, - "id": { - "type": "string", - "description": "The immutable ID of the message." - }, - "internalDate": { - "type": "string", - "description": "The internal message creation timestamp (epoch ms), which determines ordering in the inbox. For normal SMTP-received email, this represents the time the message was originally accepted by Google, which is more reliable than the Date header. However, for API-migrated mail, it can be configured by client to be based on the Date header.", - "format": "int64" - }, - "labelIds": { - "type": "array", - "description": "List of IDs of labels applied to this message.", - "items": { - "type": "string" - } - }, - "payload": { - "$ref": "MessagePart", - "description": "The parsed email structure in the message parts." - }, - "raw": { - "type": "string", - "description": "The entire email message in an RFC 2822 formatted and base64url encoded string. Returned in messages.get and drafts.get responses when the format=RAW parameter is supplied.", - "format": "byte", - "annotations": { - "required": [ - "gmail.users.drafts.create", - "gmail.users.drafts.update", - "gmail.users.messages.insert", - "gmail.users.messages.send" - ] - } - }, - "sizeEstimate": { - "type": "integer", - "description": "Estimated size in bytes of the message.", - "format": "int32" - }, - "snippet": { - "type": "string", - "description": "A short part of the message text." - }, - "threadId": { - "type": "string", - "description": "The ID of the thread the message belongs to. To add a message or draft to a thread, the following criteria must be met: \n- The requested threadId must be specified on the Message or Draft.Message you supply with your request. \n- The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard. \n- The Subject headers must match." - } - } - }, - "MessagePart": { - "id": "MessagePart", - "type": "object", - "description": "A single MIME message part.", - "properties": { - "body": { - "$ref": "MessagePartBody", - "description": "The message part body for this part, which may be empty for container MIME message parts." - }, - "filename": { - "type": "string", - "description": "The filename of the attachment. Only present if this message part represents an attachment." - }, - "headers": { - "type": "array", - "description": "List of headers on this message part. For the top-level message part, representing the entire message payload, it will contain the standard RFC 2822 email headers such as To, From, and Subject.", - "items": { - "$ref": "MessagePartHeader" - } - }, - "mimeType": { - "type": "string", - "description": "The MIME type of the message part." - }, - "partId": { - "type": "string", - "description": "The immutable ID of the message part." - }, - "parts": { - "type": "array", - "description": "The child MIME message parts of this part. This only applies to container MIME message parts, for example multipart/*. For non- container MIME message part types, such as text/plain, this field is empty. For more information, see RFC 1521.", - "items": { - "$ref": "MessagePart" - } - } - } - }, - "MessagePartBody": { - "id": "MessagePartBody", - "type": "object", - "description": "The body of a single MIME message part.", - "properties": { - "attachmentId": { - "type": "string", - "description": "When present, contains the ID of an external attachment that can be retrieved in a separate messages.attachments.get request. When not present, the entire content of the message part body is contained in the data field." - }, - "data": { - "type": "string", - "description": "The body data of a MIME message part as a base64url encoded string. May be empty for MIME container types that have no message body or when the body data is sent as a separate attachment. An attachment ID is present if the body data is contained in a separate attachment.", - "format": "byte" - }, - "size": { - "type": "integer", - "description": "Number of bytes for the message part data (encoding notwithstanding).", - "format": "int32" - } - } - }, - "MessagePartHeader": { - "id": "MessagePartHeader", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The name of the header before the : separator. For example, To." - }, - "value": { - "type": "string", - "description": "The value of the header after the : separator. For example, someuser@example.com." - } - } - }, - "ModifyMessageRequest": { - "id": "ModifyMessageRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of IDs of labels to add to this message.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list IDs of labels to remove from this message.", - "items": { - "type": "string" - } - } - } - }, - "ModifyThreadRequest": { - "id": "ModifyThreadRequest", - "type": "object", - "properties": { - "addLabelIds": { - "type": "array", - "description": "A list of IDs of labels to add to this thread.", - "items": { - "type": "string" - } - }, - "removeLabelIds": { - "type": "array", - "description": "A list of IDs of labels to remove from this thread.", - "items": { - "type": "string" - } - } - } - }, - "PopSettings": { - "id": "PopSettings", - "type": "object", - "description": "POP settings for an account.", - "properties": { - "accessWindow": { - "type": "string", - "description": "The range of messages which are accessible via POP.", - "enum": [ - "accessWindowUnspecified", - "allMail", - "disabled", - "fromNowOn" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "disposition": { - "type": "string", - "description": "The action that will be executed on a message after it has been fetched via POP.", - "enum": [ - "archive", - "dispositionUnspecified", - "leaveInInbox", - "markRead", - "trash" - ], - "enumDescriptions": [ - "", - "", - "", - "", - "" - ] - } - } - }, - "Profile": { - "id": "Profile", - "type": "object", - "description": "Profile for a Gmail user.", - "properties": { - "emailAddress": { - "type": "string", - "description": "The user's email address." - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - }, - "messagesTotal": { - "type": "integer", - "description": "The total number of messages in the mailbox.", - "format": "int32" - }, - "threadsTotal": { - "type": "integer", - "description": "The total number of threads in the mailbox.", - "format": "int32" - } - } - }, - "SendAs": { - "id": "SendAs", - "type": "object", - "description": "Settings associated with a send-as alias, which can be either the primary login address associated with the account or a custom \"from\" address. Send-as aliases correspond to the \"Send Mail As\" feature in the web interface.", - "properties": { - "displayName": { - "type": "string", - "description": "A name that appears in the \"From:\" header for mail sent using this alias. For custom \"from\" addresses, when this is empty, Gmail will populate the \"From:\" header with the name that is used for the primary address associated with the account. If the admin has disabled the ability for users to update their name format, requests to update this field for the primary login will silently fail." - }, - "isDefault": { - "type": "boolean", - "description": "Whether this address is selected as the default \"From:\" address in situations such as composing a new message or sending a vacation auto-reply. Every Gmail account has exactly one default send-as address, so the only legal value that clients may write to this field is true. Changing this from false to true for an address will result in this field becoming false for the other previous default address." - }, - "isPrimary": { - "type": "boolean", - "description": "Whether this address is the primary address used to login to the account. Every Gmail account has exactly one primary address, and it cannot be deleted from the collection of send-as aliases. This field is read-only." - }, - "replyToAddress": { - "type": "string", - "description": "An optional email address that is included in a \"Reply-To:\" header for mail sent using this alias. If this is empty, Gmail will not generate a \"Reply-To:\" header." - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias. This is read-only for all operations except create." - }, - "signature": { - "type": "string", - "description": "An optional HTML signature that is included in messages composed with this alias in the Gmail web UI." - }, - "smtpMsa": { - "$ref": "SmtpMsa", - "description": "An optional SMTP service that will be used as an outbound relay for mail sent using this alias. If this is empty, outbound mail will be sent directly from Gmail's servers to the destination SMTP service. This setting only applies to custom \"from\" aliases." - }, - "treatAsAlias": { - "type": "boolean", - "description": "Whether Gmail should treat this address as an alias for the user's primary email address. This setting only applies to custom \"from\" aliases." - }, - "verificationStatus": { - "type": "string", - "description": "Indicates whether this address has been verified for use as a send-as alias. Read-only. This setting only applies to custom \"from\" aliases.", - "enum": [ - "accepted", - "pending", - "verificationStatusUnspecified" - ], - "enumDescriptions": [ - "", - "", - "" - ] - } - } - }, - "SmimeInfo": { - "id": "SmimeInfo", - "type": "object", - "description": "An S/MIME email config.", - "properties": { - "encryptedKeyPassword": { - "type": "string", - "description": "Encrypted key password, when key is encrypted." - }, - "expiration": { - "type": "string", - "description": "When the certificate expires (in milliseconds since epoch).", - "format": "int64" - }, - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo." - }, - "isDefault": { - "type": "boolean", - "description": "Whether this SmimeInfo is the default one for this user's send-as address." - }, - "issuerCn": { - "type": "string", - "description": "The S/MIME certificate issuer's common name." - }, - "pem": { - "type": "string", - "description": "PEM formatted X509 concatenated certificate string (standard base64 encoding). Format used for returning key, which includes public key as well as certificate chain (not private key)." - }, - "pkcs12": { - "type": "string", - "description": "PKCS#12 format containing a single private/public key pair and certificate chain. This format is only accepted from client for creating a new SmimeInfo and is never returned, because the private key is not intended to be exported. PKCS#12 may be encrypted, in which case encryptedKeyPassword should be set appropriately.", - "format": "byte" - } - } - }, - "SmtpMsa": { - "id": "SmtpMsa", - "type": "object", - "description": "Configuration for communication with an SMTP service.", - "properties": { - "host": { - "type": "string", - "description": "The hostname of the SMTP service. Required." - }, - "password": { - "type": "string", - "description": "The password that will be used for authentication with the SMTP service. This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses." - }, - "port": { - "type": "integer", - "description": "The port of the SMTP service. Required.", - "format": "int32" - }, - "securityMode": { - "type": "string", - "description": "The protocol that will be used to secure communication with the SMTP service. Required.", - "enum": [ - "none", - "securityModeUnspecified", - "ssl", - "starttls" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ] - }, - "username": { - "type": "string", - "description": "The username that will be used for authentication with the SMTP service. This is a write-only field that can be specified in requests to create or update SendAs settings; it is never populated in responses." - } - } - }, - "Thread": { - "id": "Thread", - "type": "object", - "description": "A collection of messages representing a conversation.", - "properties": { - "historyId": { - "type": "string", - "description": "The ID of the last history record that modified this thread.", - "format": "uint64" - }, - "id": { - "type": "string", - "description": "The unique ID of the thread." - }, - "messages": { - "type": "array", - "description": "The list of messages in the thread.", - "items": { - "$ref": "Message" - } - }, - "snippet": { - "type": "string", - "description": "A short part of the message text." - } - } - }, - "VacationSettings": { - "id": "VacationSettings", - "type": "object", - "description": "Vacation auto-reply settings for an account. These settings correspond to the \"Vacation responder\" feature in the web interface.", - "properties": { - "enableAutoReply": { - "type": "boolean", - "description": "Flag that controls whether Gmail automatically replies to messages." - }, - "endTime": { - "type": "string", - "description": "An optional end time for sending auto-replies (epoch ms). When this is specified, Gmail will automatically reply only to messages that it receives before the end time. If both startTime and endTime are specified, startTime must precede endTime.", - "format": "int64" - }, - "responseBodyHtml": { - "type": "string", - "description": "Response body in HTML format. Gmail will sanitize the HTML before storing it." - }, - "responseBodyPlainText": { - "type": "string", - "description": "Response body in plain text format." - }, - "responseSubject": { - "type": "string", - "description": "Optional text to prepend to the subject line in vacation responses. In order to enable auto-replies, either the response subject or the response body must be nonempty." - }, - "restrictToContacts": { - "type": "boolean", - "description": "Flag that determines whether responses are sent to recipients who are not in the user's list of contacts." - }, - "restrictToDomain": { - "type": "boolean", - "description": "Flag that determines whether responses are sent to recipients who are outside of the user's domain. This feature is only available for G Suite users." - }, - "startTime": { - "type": "string", - "description": "An optional start time for sending auto-replies (epoch ms). When this is specified, Gmail will automatically reply only to messages that it receives after the start time. If both startTime and endTime are specified, startTime must precede endTime.", - "format": "int64" - } - } - }, - "WatchRequest": { - "id": "WatchRequest", - "type": "object", - "description": "Set up or update a new push notification watch on this user's mailbox.", - "properties": { - "labelFilterAction": { - "type": "string", - "description": "Filtering behavior of labelIds list specified.", - "enum": [ - "exclude", - "include" - ], - "enumDescriptions": [ - "", - "" - ] - }, - "labelIds": { - "type": "array", - "description": "List of label_ids to restrict notifications about. By default, if unspecified, all changes are pushed out. If specified then dictates which labels are required for a push notification to be generated.", - "items": { - "type": "string" - } - }, - "topicName": { - "type": "string", - "description": "A fully qualified Google Cloud Pub/Sub API topic name to publish the events to. This topic name **must** already exist in Cloud Pub/Sub and you **must** have already granted gmail \"publish\" permission on it. For example, \"projects/my-project-identifier/topics/my-topic-name\" (using the Cloud Pub/Sub \"v1\" topic naming format).\n\nNote that the \"my-project-identifier\" portion must exactly match your Google developer project id (the one executing this watch request)." - } - } - }, - "WatchResponse": { - "id": "WatchResponse", - "type": "object", - "description": "Push notification watch response.", - "properties": { - "expiration": { - "type": "string", - "description": "When Gmail will stop sending notifications for mailbox updates (epoch millis). Call watch again before this time to renew the watch.", - "format": "int64" - }, - "historyId": { - "type": "string", - "description": "The ID of the mailbox's current history record.", - "format": "uint64" - } - } - } - }, - "resources": { - "users": { - "methods": { - "getProfile": { - "id": "gmail.users.getProfile", - "path": "{userId}/profile", - "httpMethod": "GET", - "description": "Gets the current user's Gmail profile.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "Profile" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "stop": { - "id": "gmail.users.stop", - "path": "{userId}/stop", - "httpMethod": "POST", - "description": "Stop receiving push notifications for the given user mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "watch": { - "id": "gmail.users.watch", - "path": "{userId}/watch", - "httpMethod": "POST", - "description": "Set up or update a push notification watch on the given user mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "WatchRequest" - }, - "response": { - "$ref": "WatchResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - }, - "resources": { - "drafts": { - "methods": { - "create": { - "id": "gmail.users.drafts.create", - "path": "{userId}/drafts", - "httpMethod": "POST", - "description": "Creates a new draft with the DRAFT label.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts" - } - } - } - }, - "delete": { - "id": "gmail.users.drafts.delete", - "path": "{userId}/drafts/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified draft. Does not simply trash it.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the draft to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "get": { - "id": "gmail.users.drafts.get", - "path": "{userId}/drafts/{id}", - "httpMethod": "GET", - "description": "Gets the specified draft.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the draft in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal", - "raw" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the draft to retrieve.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.drafts.list", - "path": "{userId}/drafts", - "httpMethod": "GET", - "description": "Lists the drafts in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include drafts from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of drafts to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return draft messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\".", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListDraftsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "send": { - "id": "gmail.users.drafts.send", - "path": "{userId}/drafts/send", - "httpMethod": "POST", - "description": "Sends the specified, existing draft to the recipients in the To, Cc, and Bcc headers.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts/send" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts/send" - } - } - } - }, - "update": { - "id": "gmail.users.drafts.update", - "path": "{userId}/drafts/{id}", - "httpMethod": "PUT", - "description": "Replaces a draft's content.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the draft to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Draft" - }, - "response": { - "$ref": "Draft" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/drafts/{id}" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/drafts/{id}" - } - } - } - } - } - }, - "history": { - "methods": { - "list": { - "id": "gmail.users.history.list", - "path": "{userId}/history", - "httpMethod": "GET", - "description": "Lists the history of all changes to the given mailbox. History results are returned in chronological order (increasing historyId).", - "parameters": { - "historyTypes": { - "type": "string", - "description": "History types to be returned by the function", - "enum": [ - "labelAdded", - "labelRemoved", - "messageAdded", - "messageDeleted" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "repeated": true, - "location": "query" - }, - "labelId": { - "type": "string", - "description": "Only return messages with a label matching the ID.", - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "The maximum number of history records to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "startHistoryId": { - "type": "string", - "description": "Required. Returns history records after the specified startHistoryId. The supplied startHistoryId should be obtained from the historyId of a message, thread, or previous list response. History IDs increase chronologically but are not contiguous with random gaps in between valid IDs. Supplying an invalid or out of date startHistoryId typically returns an HTTP 404 error code. A historyId is typically valid for at least a week, but in some rare circumstances may be valid for only a few hours. If you receive an HTTP 404 error response, your application should perform a full sync. If you receive no nextPageToken in the response, there are no updates to retrieve and you can store the returned historyId for a future request.", - "format": "uint64", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListHistoryResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - } - }, - "labels": { - "methods": { - "create": { - "id": "gmail.users.labels.create", - "path": "{userId}/labels", - "httpMethod": "POST", - "description": "Creates a new label.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "delete": { - "id": "gmail.users.labels.delete", - "path": "{userId}/labels/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified label and removes it from any messages and threads that it is applied to.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "get": { - "id": "gmail.users.labels.get", - "path": "{userId}/labels/{id}", - "httpMethod": "GET", - "description": "Gets the specified label.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to retrieve.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.labels.list", - "path": "{userId}/labels", - "httpMethod": "GET", - "description": "Lists all labels in the user's mailbox.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListLabelsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "patch": { - "id": "gmail.users.labels.patch", - "path": "{userId}/labels/{id}", - "httpMethod": "PATCH", - "description": "Updates the specified label. This method supports patch semantics.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "update": { - "id": "gmail.users.labels.update", - "path": "{userId}/labels/{id}", - "httpMethod": "PUT", - "description": "Updates the specified label.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the label to update.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "Label" - }, - "response": { - "$ref": "Label" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - } - }, - "messages": { - "methods": { - "batchDelete": { - "id": "gmail.users.messages.batchDelete", - "path": "{userId}/messages/batchDelete", - "httpMethod": "POST", - "description": "Deletes many messages by message ID. Provides no guarantees that messages were not already deleted or even existed at all.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "BatchDeleteMessagesRequest" - }, - "scopes": [ - "https://mail.google.com/" - ] - }, - "batchModify": { - "id": "gmail.users.messages.batchModify", - "path": "{userId}/messages/batchModify", - "httpMethod": "POST", - "description": "Modifies the labels on the specified messages.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "BatchModifyMessagesRequest" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "delete": { - "id": "gmail.users.messages.delete", - "path": "{userId}/messages/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified message. This operation cannot be undone. Prefer messages.trash instead.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/" - ] - }, - "get": { - "id": "gmail.users.messages.get", - "path": "{userId}/messages/{id}", - "httpMethod": "GET", - "description": "Gets the specified message.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the message in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal", - "raw" - ], - "enumDescriptions": [ - "", - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the message to retrieve.", - "required": true, - "location": "path" - }, - "metadataHeaders": { - "type": "string", - "description": "When given and format is METADATA, only include headers specified.", - "repeated": true, - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "import": { - "id": "gmail.users.messages.import", - "path": "{userId}/messages/import", - "httpMethod": "POST", - "description": "Imports a message into only this user's mailbox, with standard email delivery scanning and classification similar to receiving via SMTP. Does not send a message.", - "parameters": { - "deleted": { - "type": "boolean", - "description": "Mark the email as permanently deleted (not TRASH) and only visible in Google Vault to a Vault administrator. Only used for G Suite accounts.", - "default": "false", - "location": "query" - }, - "internalDateSource": { - "type": "string", - "description": "Source for Gmail's internal date of the message.", - "default": "dateHeader", - "enum": [ - "dateHeader", - "receivedTime" - ], - "enumDescriptions": [ - "", - "" - ], - "location": "query" - }, - "neverMarkSpam": { - "type": "boolean", - "description": "Ignore the Gmail spam classifier decision and never mark this email as SPAM in the mailbox.", - "default": "false", - "location": "query" - }, - "processForCalendar": { - "type": "boolean", - "description": "Process calendar invites in the email and add any extracted meetings to the Google Calendar for this user.", - "default": "false", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.insert", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "50MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages/import" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages/import" - } - } - } - }, - "insert": { - "id": "gmail.users.messages.insert", - "path": "{userId}/messages", - "httpMethod": "POST", - "description": "Directly inserts a message into only this user's mailbox similar to IMAP APPEND, bypassing most scanning and classification. Does not send a message.", - "parameters": { - "deleted": { - "type": "boolean", - "description": "Mark the email as permanently deleted (not TRASH) and only visible in Google Vault to a Vault administrator. Only used for G Suite accounts.", - "default": "false", - "location": "query" - }, - "internalDateSource": { - "type": "string", - "description": "Source for Gmail's internal date of the message.", - "default": "receivedTime", - "enum": [ - "dateHeader", - "receivedTime" - ], - "enumDescriptions": [ - "", - "" - ], - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.insert", - "https://www.googleapis.com/auth/gmail.modify" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "50MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages" - } - } - } - }, - "list": { - "id": "gmail.users.messages.list", - "path": "{userId}/messages", - "httpMethod": "GET", - "description": "Lists the messages in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include messages from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "labelIds": { - "type": "string", - "description": "Only return messages with labels that match all of the specified label IDs.", - "repeated": true, - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of messages to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return messages matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid:\u003csomemsgid@example.com\u003e is:unread\". Parameter cannot be used when accessing the api using the gmail.metadata scope.", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListMessagesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "modify": { - "id": "gmail.users.messages.modify", - "path": "{userId}/messages/{id}/modify", - "httpMethod": "POST", - "description": "Modifies the labels on the specified message.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to modify.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "ModifyMessageRequest" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "send": { - "id": "gmail.users.messages.send", - "path": "{userId}/messages/send", - "httpMethod": "POST", - "description": "Sends the specified message to the recipients in the To, Cc, and Bcc headers.", - "parameters": { - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Message" - }, - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.compose", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.send" - ], - "supportsMediaUpload": true, - "mediaUpload": { - "accept": [ - "message/rfc822" - ], - "maxSize": "35MB", - "protocols": { - "simple": { - "multipart": true, - "path": "/upload/gmail/v1/users/{userId}/messages/send" - }, - "resumable": { - "multipart": true, - "path": "/resumable/upload/gmail/v1/users/{userId}/messages/send" - } - } - } - }, - "trash": { - "id": "gmail.users.messages.trash", - "path": "{userId}/messages/{id}/trash", - "httpMethod": "POST", - "description": "Moves the specified message to the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "untrash": { - "id": "gmail.users.messages.untrash", - "path": "{userId}/messages/{id}/untrash", - "httpMethod": "POST", - "description": "Removes the specified message from the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the message to remove from Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Message" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - }, - "resources": { - "attachments": { - "methods": { - "get": { - "id": "gmail.users.messages.attachments.get", - "path": "{userId}/messages/{messageId}/attachments/{id}", - "httpMethod": "GET", - "description": "Gets the specified message attachment.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the attachment.", - "required": true, - "location": "path" - }, - "messageId": { - "type": "string", - "description": "The ID of the message containing the attachment.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "messageId", - "id" - ], - "response": { - "$ref": "MessagePartBody" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - } - } - } - } - }, - "settings": { - "methods": { - "getAutoForwarding": { - "id": "gmail.users.settings.getAutoForwarding", - "path": "{userId}/settings/autoForwarding", - "httpMethod": "GET", - "description": "Gets the auto-forwarding setting for the specified account.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "AutoForwarding" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getImap": { - "id": "gmail.users.settings.getImap", - "path": "{userId}/settings/imap", - "httpMethod": "GET", - "description": "Gets IMAP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ImapSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getPop": { - "id": "gmail.users.settings.getPop", - "path": "{userId}/settings/pop", - "httpMethod": "GET", - "description": "Gets POP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "PopSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "getVacation": { - "id": "gmail.users.settings.getVacation", - "path": "{userId}/settings/vacation", - "httpMethod": "GET", - "description": "Gets vacation responder settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "VacationSettings" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updateAutoForwarding": { - "id": "gmail.users.settings.updateAutoForwarding", - "path": "{userId}/settings/autoForwarding", - "httpMethod": "PUT", - "description": "Updates the auto-forwarding setting for the specified account. A verified forwarding address must be specified when auto-forwarding is enabled.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "AutoForwarding" - }, - "response": { - "$ref": "AutoForwarding" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "updateImap": { - "id": "gmail.users.settings.updateImap", - "path": "{userId}/settings/imap", - "httpMethod": "PUT", - "description": "Updates IMAP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "ImapSettings" - }, - "response": { - "$ref": "ImapSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updatePop": { - "id": "gmail.users.settings.updatePop", - "path": "{userId}/settings/pop", - "httpMethod": "PUT", - "description": "Updates POP settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "PopSettings" - }, - "response": { - "$ref": "PopSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "updateVacation": { - "id": "gmail.users.settings.updateVacation", - "path": "{userId}/settings/vacation", - "httpMethod": "PUT", - "description": "Updates vacation responder settings.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "VacationSettings" - }, - "response": { - "$ref": "VacationSettings" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - }, - "resources": { - "delegates": { - "methods": { - "create": { - "id": "gmail.users.settings.delegates.create", - "path": "{userId}/settings/delegates", - "httpMethod": "POST", - "description": "Adds a delegate with its verification status set directly to accepted, without sending any verification email. The delegate user must be a member of the same G Suite organization as the delegator user.\n\nGmail imposes limtations on the number of delegates and delegators each user in a G Suite organization can have. These limits depend on your organization, but in general each user can have up to 25 delegates and up to 10 delegators.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nAlso note that when a new delegate is created, there may be up to a one minute delay before the new delegate is available for use.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Delegate" - }, - "response": { - "$ref": "Delegate" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.delegates.delete", - "path": "{userId}/settings/delegates/{delegateEmail}", - "httpMethod": "DELETE", - "description": "Removes the specified delegate (which can be of any verification status), and revokes any verification that may have been required for using it.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "delegateEmail": { - "type": "string", - "description": "The email address of the user to be removed as a delegate.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "delegateEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.delegates.get", - "path": "{userId}/settings/delegates/{delegateEmail}", - "httpMethod": "GET", - "description": "Gets the specified delegate.\n\nNote that a delegate user must be referred to by their primary email address, and not an email alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "delegateEmail": { - "type": "string", - "description": "The email address of the user whose delegate relationship is to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "delegateEmail" - ], - "response": { - "$ref": "Delegate" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.delegates.list", - "path": "{userId}/settings/delegates", - "httpMethod": "GET", - "description": "Lists the delegates for the specified account.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListDelegatesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "filters": { - "methods": { - "create": { - "id": "gmail.users.settings.filters.create", - "path": "{userId}/settings/filters", - "httpMethod": "POST", - "description": "Creates a filter.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "Filter" - }, - "response": { - "$ref": "Filter" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "delete": { - "id": "gmail.users.settings.filters.delete", - "path": "{userId}/settings/filters/{id}", - "httpMethod": "DELETE", - "description": "Deletes a filter.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the filter to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "get": { - "id": "gmail.users.settings.filters.get", - "path": "{userId}/settings/filters/{id}", - "httpMethod": "GET", - "description": "Gets a filter.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the filter to be fetched.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Filter" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.filters.list", - "path": "{userId}/settings/filters", - "httpMethod": "GET", - "description": "Lists the message filters of a Gmail user.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListFiltersResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "forwardingAddresses": { - "methods": { - "create": { - "id": "gmail.users.settings.forwardingAddresses.create", - "path": "{userId}/settings/forwardingAddresses", - "httpMethod": "POST", - "description": "Creates a forwarding address. If ownership verification is required, a message will be sent to the recipient and the resource's verification status will be set to pending; otherwise, the resource will be created with verification status set to accepted.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "ForwardingAddress" - }, - "response": { - "$ref": "ForwardingAddress" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.forwardingAddresses.delete", - "path": "{userId}/settings/forwardingAddresses/{forwardingEmail}", - "httpMethod": "DELETE", - "description": "Deletes the specified forwarding address and revokes any verification that may have been required.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "forwardingEmail": { - "type": "string", - "description": "The forwarding address to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "forwardingEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.forwardingAddresses.get", - "path": "{userId}/settings/forwardingAddresses/{forwardingEmail}", - "httpMethod": "GET", - "description": "Gets the specified forwarding address.", - "parameters": { - "forwardingEmail": { - "type": "string", - "description": "The forwarding address to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "forwardingEmail" - ], - "response": { - "$ref": "ForwardingAddress" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.forwardingAddresses.list", - "path": "{userId}/settings/forwardingAddresses", - "httpMethod": "GET", - "description": "Lists the forwarding addresses for the specified account.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListForwardingAddressesResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - } - } - }, - "sendAs": { - "methods": { - "create": { - "id": "gmail.users.settings.sendAs.create", - "path": "{userId}/settings/sendAs", - "httpMethod": "POST", - "description": "Creates a custom \"from\" send-as alias. If an SMTP MSA is specified, Gmail will attempt to connect to the SMTP service to validate the configuration before creating the alias. If ownership verification is required for the alias, a message will be sent to the email address and the resource's verification status will be set to pending; otherwise, the resource will be created with verification status set to accepted. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "delete": { - "id": "gmail.users.settings.sendAs.delete", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "DELETE", - "description": "Deletes the specified send-as alias. Revokes any verification that may have been required for using it.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be deleted.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.sendAs.get", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "GET", - "description": "Gets the specified send-as alias. Fails with an HTTP 404 error if the specified address is not a member of the collection.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be retrieved.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "list": { - "id": "gmail.users.settings.sendAs.list", - "path": "{userId}/settings/sendAs", - "httpMethod": "GET", - "description": "Lists the send-as aliases for the specified account. The result includes the primary send-as address associated with the account as well as any custom \"from\" aliases.", - "parameters": { - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListSendAsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic" - ] - }, - "patch": { - "id": "gmail.users.settings.sendAs.patch", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "PATCH", - "description": "Updates a send-as alias. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nAddresses other than the primary address for the account can only be updated by service account clients that have been delegated domain-wide authority. This method supports patch semantics.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be updated.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "update": { - "id": "gmail.users.settings.sendAs.update", - "path": "{userId}/settings/sendAs/{sendAsEmail}", - "httpMethod": "PUT", - "description": "Updates a send-as alias. If a signature is provided, Gmail will sanitize the HTML before saving it with the alias.\n\nAddresses other than the primary address for the account can only be updated by service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be updated.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SendAs" - }, - "response": { - "$ref": "SendAs" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "verify": { - "id": "gmail.users.settings.sendAs.verify", - "path": "{userId}/settings/sendAs/{sendAsEmail}/verify", - "httpMethod": "POST", - "description": "Sends a verification email to the specified send-as alias address. The verification status must be pending.\n\nThis method is only available to service account clients that have been delegated domain-wide authority.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The send-as alias to be verified.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "User's email address. The special value \"me\" can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - } - }, - "resources": { - "smimeInfo": { - "methods": { - "delete": { - "id": "gmail.users.settings.sendAs.smimeInfo.delete", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}", - "httpMethod": "DELETE", - "description": "Deletes the specified S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "get": { - "id": "gmail.users.settings.sendAs.smimeInfo.get", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}", - "httpMethod": "GET", - "description": "Gets the specified S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "response": { - "$ref": "SmimeInfo" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "insert": { - "id": "gmail.users.settings.sendAs.smimeInfo.insert", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo", - "httpMethod": "POST", - "description": "Insert (upload) the given S/MIME config for the specified send-as alias. Note that pkcs12 format is required for the key.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "request": { - "$ref": "SmimeInfo" - }, - "response": { - "$ref": "SmimeInfo" - }, - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "list": { - "id": "gmail.users.settings.sendAs.smimeInfo.list", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo", - "httpMethod": "GET", - "description": "Lists S/MIME configs for the specified send-as alias.", - "parameters": { - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail" - ], - "response": { - "$ref": "ListSmimeInfoResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly", - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - }, - "setDefault": { - "id": "gmail.users.settings.sendAs.smimeInfo.setDefault", - "path": "{userId}/settings/sendAs/{sendAsEmail}/smimeInfo/{id}/setDefault", - "httpMethod": "POST", - "description": "Sets the default S/MIME config for the specified send-as alias.", - "parameters": { - "id": { - "type": "string", - "description": "The immutable ID for the SmimeInfo.", - "required": true, - "location": "path" - }, - "sendAsEmail": { - "type": "string", - "description": "The email address that appears in the \"From:\" header for mail sent using this alias.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "sendAsEmail", - "id" - ], - "scopes": [ - "https://www.googleapis.com/auth/gmail.settings.basic", - "https://www.googleapis.com/auth/gmail.settings.sharing" - ] - } - } - } - } - } - } - }, - "threads": { - "methods": { - "delete": { - "id": "gmail.users.threads.delete", - "path": "{userId}/threads/{id}", - "httpMethod": "DELETE", - "description": "Immediately and permanently deletes the specified thread. This operation cannot be undone. Prefer threads.trash instead.", - "parameters": { - "id": { - "type": "string", - "description": "ID of the Thread to delete.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "scopes": [ - "https://mail.google.com/" - ] - }, - "get": { - "id": "gmail.users.threads.get", - "path": "{userId}/threads/{id}", - "httpMethod": "GET", - "description": "Gets the specified thread.", - "parameters": { - "format": { - "type": "string", - "description": "The format to return the messages in.", - "default": "full", - "enum": [ - "full", - "metadata", - "minimal" - ], - "enumDescriptions": [ - "", - "", - "" - ], - "location": "query" - }, - "id": { - "type": "string", - "description": "The ID of the thread to retrieve.", - "required": true, - "location": "path" - }, - "metadataHeaders": { - "type": "string", - "description": "When given and format is METADATA, only include headers specified.", - "repeated": true, - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "list": { - "id": "gmail.users.threads.list", - "path": "{userId}/threads", - "httpMethod": "GET", - "description": "Lists the threads in the user's mailbox.", - "parameters": { - "includeSpamTrash": { - "type": "boolean", - "description": "Include threads from SPAM and TRASH in the results.", - "default": "false", - "location": "query" - }, - "labelIds": { - "type": "string", - "description": "Only return threads with labels that match all of the specified label IDs.", - "repeated": true, - "location": "query" - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of threads to return.", - "default": "100", - "format": "uint32", - "location": "query" - }, - "pageToken": { - "type": "string", - "description": "Page token to retrieve a specific page of results in the list.", - "location": "query" - }, - "q": { - "type": "string", - "description": "Only return threads matching the specified query. Supports the same query format as the Gmail search box. For example, \"from:someuser@example.com rfc822msgid: is:unread\". Parameter cannot be used when accessing the api using the gmail.metadata scope.", - "location": "query" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId" - ], - "response": { - "$ref": "ListThreadsResponse" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.metadata", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/gmail.readonly" - ] - }, - "modify": { - "id": "gmail.users.threads.modify", - "path": "{userId}/threads/{id}/modify", - "httpMethod": "POST", - "description": "Modifies the labels applied to the thread. This applies to all messages in the thread.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to modify.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "request": { - "$ref": "ModifyThreadRequest" - }, - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "trash": { - "id": "gmail.users.threads.trash", - "path": "{userId}/threads/{id}/trash", - "httpMethod": "POST", - "description": "Moves the specified thread to the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - }, - "untrash": { - "id": "gmail.users.threads.untrash", - "path": "{userId}/threads/{id}/untrash", - "httpMethod": "POST", - "description": "Removes the specified thread from the trash.", - "parameters": { - "id": { - "type": "string", - "description": "The ID of the thread to remove from Trash.", - "required": true, - "location": "path" - }, - "userId": { - "type": "string", - "description": "The user's email address. The special value me can be used to indicate the authenticated user.", - "default": "me", - "required": true, - "location": "path" - } - }, - "parameterOrder": [ - "userId", - "id" - ], - "response": { - "$ref": "Thread" - }, - "scopes": [ - "https://mail.google.com/", - "https://www.googleapis.com/auth/gmail.modify" - ] - } - } - } - } - } - } -} -__END -} diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 3a98ff2..8c457e3 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -7,6 +7,7 @@ use WebService::GoogleAPI::Client; my $dir = getcwd; my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging + my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil $default_file = "$dir/../gapi.json" unless -e $default_file; ## if file doesn't exist try one level up ( allows to run directly from t/ if gapi.json in parent dir ) @@ -17,6 +18,8 @@ plan skip_all => 'No service configuration - set $ENV{GOOGLE_TOKENSFILE} or crea ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); +$gapi->user('peter@shotgundriver.com'); + my $options = { api_endpoint_id => 'drive.files.list', options => { diff --git a/t/01-client-discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t similarity index 74% rename from t/01-client-discovery.t rename to t/WebService/GoogleAPI/Client/Discovery.t index d2b32ef..b82a077 100755 --- a/t/01-client-discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -1,64 +1,198 @@ -#!perl -T +#!/usr/bin/env perl -=head2 USAGE +use strict; +use warnings; +use Test2::V0; +use CHI; +use Mojo::File qw/tempdir/; -To run manually in the local directory assuming gapi.json present in source root and in xt/author/calendar directory - C +use lib 't/lib'; +use TestTools qw/has_credentials set_credentials DEBUG/; -NB: is also run as part of dzil test +use WebService::GoogleAPI::Client; +use WebService::GoogleAPI::Client::Discovery; -=cut +my $package = 'WebService::GoogleAPI::Client::Discovery'; +my $tmp = tempdir; +my $cache = CHI->new(driver => 'File', root_dir => $tmp->to_string); -use 5.006; -use strict; -use warnings; -use Test::More; -use Data::Dumper; ## remove this when finish tweaking -use Cwd; -use CHI; +ok my $disco = WebService::GoogleAPI::Client::Discovery->new(chi => $cache), 'instanciation works'; -my $dir = getcwd; -my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging +subtest 'WebService::GoogleAPI::Client::Discovery class properties' => sub { + #TODO- these tests are stupid, ne? bakajanaikana? + is ref($disco->ua), 'WebService::GoogleAPI::Client::UserAgent', + 'ua property is of type WebService::GoogleAPI::Client::UserAgent'; + like ref($disco->chi), qr/^CHI::Driver/xm, + 'chi property (WebService::GoogleAPI::Client::Discovery->new->chi) is of sub-type CHI::Driver::'; -use_ok( 'WebService::GoogleAPI::Client' ); # || print "Bail out!\n"; -use_ok( 'WebService::GoogleAPI::Client::Discovery' ); + note( "SESSION DEFAULT CHI Root Directory = " . $disco->chi->root_dir ); + is $disco->debug, '0', 'debug property defaults to 0'; +}; +$disco->debug(DEBUG); + +#TODO- mock away the actual get request... +subtest 'discover_all' => sub { + subtest 'hits the wire with no cache' => sub { + like $disco->discover_all->{items}, bag { + item hash { + field kind => "discovery#directoryItem"; + field id => "gmail:v1"; + field name => "gmail"; + field version => "v1"; + field title => "Gmail API"; + etc; + }; + etc; + }, 'got a reasonable discovery document'; + like $disco->stats, { + cache => { get => 0 }, + network => { get => 1 } + }, 'used the network'; + }; + + subtest 'uses cache if available' => sub { + like $disco->discover_all->{items}, bag { + item hash { + field kind => "discovery#directoryItem"; + field id => "gmail:v1"; + field name => "gmail"; + field version => "v1"; + field title => "Gmail API"; + etc; + }; + etc; + }, 'got a reasonable discovery document'; + like $disco->stats, { + cache => { get => 1 }, + network => { get => 1 } + }, 'used the cache'; + }; + + subtest 'Ignores cache if passed $force flag' => sub { + like $disco->discover_all(1)->{items}, bag { + item hash { + field kind => "discovery#directoryItem"; + field id => "gmail:v1"; + field name => "gmail"; + field version => "v1"; + field title => "Gmail API"; + etc; + }; + etc; + }, 'got a reasonable discovery document'; + like $disco->stats, { + cache => { get => 1 }, + network => { get => 2 } + }, 'used the network'; + }; + + subtest 'Ignores cache if expired' => sub { + $disco->chi->set($disco->discover_key, {}, 'now'); + like $disco->discover_all->{items}, bag { + item hash { + field kind => "discovery#directoryItem"; + field id => "gmail:v1"; + field name => "gmail"; + field version => "v1"; + field title => "Gmail API"; + etc; + }; + etc; + }, 'got a reasonable discovery document'; + + like $disco->stats, { + cache => { get => 1 }, + network => { get => 3 } + }, 'used the network'; + }; + + subtest 'Uses authenticated request when asked' => sub { + skip_all 'needs real credentials' unless has_credentials; + my $obj = $disco->new; + set_credentials($obj); + like $obj->discover_all(1,1)->{items}, bag { + item hash { + field kind => "discovery#directoryItem"; + field id => "gmail:v1"; + field name => "gmail"; + field version => "v1"; + field title => "Gmail API"; + etc; + }; + etc; + }, 'got a reasonable discovery document'; + like $obj->stats, { + network => { authorized => 1 } + }, 'used the credentials'; + }; +}; #end of discover_all subtest + +subtest 'Augmenting the api' => sub { + my $to_augment = { + 'version' => 'v4', + 'preferred' => 1, + 'title' => 'Google My Business API', + 'description' => 'The Google My Business API provides an interface for managing business location information on Google.', + 'id' => 'mybusiness:v4', + 'kind' => 'discovery#directoryItem', + 'documentationLink' => "https://developers.google.com/my-business/", + 'icons' => { + "x16"=> "http://www.google.com/images/icons/product/search-16.gif", + "x32"=> "http://www.google.com/images/icons/product/search-32.gif" + }, + 'discoveryRestUrl' => + 'https://developers.google.com/my-business/samples/mybusiness_google_rest_v4p2.json', + 'name' => 'mybusiness' + }; + + $disco->augment_with($to_augment); + like $disco->discover_all->{items}, bag { + item hash { + field $_ => $to_augment->{$_} for qw/kind id name version title/; + etc; + }; + etc; + }, 'got back what we just added'; -my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil -$default_file = "$dir/../gapi.json" unless -e $default_file; ## if file doesn't exist try one level up ( allows to run directly from t/ if gapi.json in parent dir ) -my $user = $ENV{ 'GMAIL_FOR_TESTING' } || ''; ## will be populated by first available if set to '' and default_file exists +}; +subtest 'available_APIs' => sub { + #NOTE- this is used a lot internally, so let's just make sure it + # does what we want + my $list = $disco->available_APIs; + + like $list, bag { + item hash { + field name => 'gmail'; + field versions => bag { item 'v1'; etc; }; + field doclinks => bag { item 'https://developers.google.com/gmail/api/'; etc; }; + field discoveryRestUrl => bag { item 'https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest'; etc; }; + end; + }; + etc; + }, 'collapsed structure as expected'; -subtest 'WebService::GoogleAPI::Client::Discovery class properties' => sub { - ok( - ref WebService::GoogleAPI::Client::Discovery->new->ua eq 'WebService::GoogleAPI::Client::UserAgent', - 'ua property (WebService::GoogleAPI::Client::Discovery->new->ua) is of type WebService::GoogleAPI::Client::UserAgent' - ); - ok( - ref( WebService::GoogleAPI::Client::Discovery->new->chi ) =~ /^CHI::Driver/xm, - 'chi property (WebService::GoogleAPI::Client::Discovery->new->chi) is of sub-type CHI::Driver::' - ); - note( "SESSION DEFAULT CHI Root Directory = " . WebService::GoogleAPI::Client::Discovery->new()->chi->root_dir ); - ok( WebService::GoogleAPI::Client::Discovery->new->debug eq '0', 'debug property defaults to 0' ); - ok( WebService::GoogleAPI::Client::Discovery->new( debug => 1 )->debug eq '1', 'debug property when set to 1 on new returns 1' ); }; +#TODO- get available_APIs and list_of_available_api_ids +# compare and contrast which one is extraneous. Make sure outside +# code doesn't peek into our innards... +done_testing; -## NB - should probably skip tests that will fail when a dependent test fails -subtest 'Naked instance method tests (without Client parent container)' => sub { +__DATA__ +#we'll get back to these eventually - ok( ref( WebService::GoogleAPI::Client->new->api_query() ) eq 'Mojo::Message::Response', "WebService::GoogleAPI::Client->new->api_query() is a 'Mojo::Message::Response'" ); - ok( - WebService::GoogleAPI::Client::Discovery->new->list_of_available_google_api_ids() =~ /gmail/xm, - 'WebService::GoogleAPI::Client::Discovery->new->list_of_available_google_api_ids()' - ); +subtest 'Naked instance method tests (without Client parent container)' => sub { + subtest 'Getting api id for gmail' => sub { + like scalar($disco->list_of_available_google_api_ids), qr/gmail/xm, 'gets id from a string in scalar context'; + like [ $disco->list_of_available_google_api_ids ], bag { + item qr/gmail/; + etc; + }, 'gets it in list context too'; + }; - ok( ref( WebService::GoogleAPI::Client::Discovery->new->discover_all() ) eq 'HASH', 'WebService::GoogleAPI::Client::Discovery->new->discover_all() return HASREF' ); - ok( - ref( WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api() ) eq 'HASH', - ' WebService::GoogleAPI::Client::Discovery->new->augment_discover_all_with_unlisted_experimental_api() returns HASHREF' - ); ok( length( WebService::GoogleAPI::Client::Discovery->new->supported_as_text ) > 100, 'WebService::GoogleAPI::Client::Discovery->new->supported_as_text() returns string > 100 chars' @@ -204,10 +338,8 @@ subtest 'Discovery methods with User Configuration' => sub { ok( $gapi->user( $user ) eq $user, "\$gapi->user('$user') eq '$user'" ); -#$ENV{CHI_FILE_PATH} = $ENV{TMPDIR}; plan( skip_all => 'Skipping network impacting tests unless ENV VAR CHI_FILE_PATH is set' ) unless defined $ENV{ CHI_FILE_PATH }; - ok( my $chi = CHI->new( driver => 'File', @@ -234,7 +366,7 @@ subtest 'Discovery methods with User Configuration' => sub { ## ensure that tests don't fail when Google blocks discovery requests for unauthenticated users ## due to exceeding access limits. - ## DISCOVERY ALL - RETURNS THE DESCIVOERY STRUCTURE DESCRIBING ALL GOOGLE SERVICE API ID's (gmail,calendar etc) + ## DISCOVERY ALL - RETURNS THE DISCOVERY STRUCTURE DESCRIBING ALL GOOGLE SERVICE API ID's (gmail,calendar etc) #ok( my $ret = WebService::GoogleAPI::Client->new( chi => $chi )->discovery->new->discover_all, 'WebService::GoogleAPI::Client::Discovery->new->discover_all returns something'); ok( ref( $gapi->discover_all ) eq 'HASH', 'Client->discover_all returns HASHREF' ); ok( join( ',', sort keys( %{ WebService::GoogleAPI::Client::Discovery->new->discover_all() } ) ) eq 'discoveryVersion,items,kind', diff --git a/t/lib/TestTools.pm b/t/lib/TestTools.pm new file mode 100644 index 0000000..cf471c6 --- /dev/null +++ b/t/lib/TestTools.pm @@ -0,0 +1,39 @@ +package TestTools; +use strict; +use warnings; + +use Exporter::Shiny qw/ gapi_json DEBUG user has_credentials set_credentials/; +use Mojo::File qw/ curfile path/; + +my $gapi; +#try and find a good gapi.json to use here. Check as follows: +# 1) first try whatever was set in the ENV variable +# 2) the current directory +# 3) the directory BELOW the main dir for the project, so dzil's +# author tests can find the one in our main folder +# 4) the main dir of this project +# 5) the fake one in the t/ directory +$gapi = path($ENV{GOOGLE_TOKENSFILE} || 'gapi.json'); +$gapi = curfile->dirname->dirname->dirname->sibling('gapi.json') + unless $gapi->stat; +$gapi = curfile->dirname->dirname->sibling('gapi.json') + unless $gapi->stat; +$gapi = curfile->dirname->sibling('gapi.json') + unless $gapi->stat; + +sub gapi_json { + return "$gapi"; +} +sub user { $ENV{GMAIL_FOR_TESTING} } + +sub has_credentials { $gapi->stat && user } +sub set_credentials { + my ($obj) = @_; + $obj->ua->auth_storage->setup({ type => 'jsonfile', path => "$gapi" }); + $obj->ua->user(user) +} + + +sub DEBUG { $ENV{GAPI_DEBUG_LEVEL} // 0 } + +9033 diff --git a/t/manifest.t b/t/manifest.t index 10e775b..701c00d 100755 --- a/t/manifest.t +++ b/t/manifest.t @@ -1,4 +1,4 @@ -#!perl -T +#!perl use 5.006; use strict; use warnings; diff --git a/t/pod.t b/t/pod.t index 5950ec1..3237e0b 100755 --- a/t/pod.t +++ b/t/pod.t @@ -1,4 +1,4 @@ -#!perl -T +#!perl use 5.006; use strict; use warnings; From 080f4e17ac63d500cdbc1c442b927ac768d5e6c5 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 27 May 2020 15:54:34 +0300 Subject: [PATCH 15/68] cleaned up code (A LOT!) removed (as far as I know) our currently deprecated methods --- dist.ini | 9 +- examples/discovery_example.pl | 6 - examples/usage_basic_examples.pl | 2 - lib/WebService/GoogleAPI/Client.pm | 68 ++-- lib/WebService/GoogleAPI/Client/Discovery.pm | 327 +++++++------------ t/WebService/GoogleAPI/Client/Discovery.t | 65 +++- 6 files changed, 223 insertions(+), 254 deletions(-) diff --git a/dist.ini b/dist.ini index 4075792..3f89b16 100755 --- a/dist.ini +++ b/dist.ini @@ -24,17 +24,20 @@ main_module = lib/WebService/GoogleAPI/Client.pm [PkgVersion] [Prereqs] -; Mojo::Message::Response = 7.12 ## can't do this - no version info available for Mojo::Message::Response -;Moose = 2.2 perl = 5.14.04 +Moo = 2.00 Mojolicious = 8.30 Mojolicious::Plugin::OAuth2 = 1.5 List::Util = 1.45 +List::SomeUtils = 0 IO::Socket::SSL = 2.06 Mojo::JWT = 0 -Mojo::JWT::Google = 0.09 +Mojo::JWT::Google = 0.11 Exporter::Shiny = 0 +[Prereqs / TestRequires ] +Test2::V0 = 0 + [AutoPrereqs] [CPANFile] diff --git a/examples/discovery_example.pl b/examples/discovery_example.pl index f02b912..63e5140 100755 --- a/examples/discovery_example.pl +++ b/examples/discovery_example.pl @@ -365,11 +365,6 @@ =head2 ABSTRACT ###################################### -# my $api_verson_urls = api_version_urls(); ## hash of api discovery urls api->version->url .. bugger deleted this function -#print Dumper $api_verson_urls ; -#my $api = 'appengine'; my $version = 'v1beta5'; -#my $api = 'sheets'; my $version = 'v4'; -#my $api = 'sheets'; my $version = ''; my $api = 'gmail'; my $version = 'v1'; @@ -465,7 +460,6 @@ =head2 ABSTRACT exit; -print WebService::GoogleAPI::Client::Discovery->new->supported_as_text(); exit; diff --git a/examples/usage_basic_examples.pl b/examples/usage_basic_examples.pl index ec2baba..2c7b90f 100755 --- a/examples/usage_basic_examples.pl +++ b/examples/usage_basic_examples.pl @@ -123,8 +123,6 @@ #say my $x = WebService::GoogleAPI::Client::Discovery->new->list_of_available_google_api_ids(); say 'fnarly' if ref( WebService::GoogleAPI::Client::Discovery->new->discover_all() ) eq 'HASH'; -say length( WebService::GoogleAPI::Client::Discovery->new->supported_as_text ) > 100; - #say Dumper $x; say WebService::GoogleAPI::Client::Discovery->new->api_verson_urls; diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 64c9c1f..1e580a5 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -91,23 +91,48 @@ Package includes I CLI Script to collect initial end-user authorisation =cut -has 'debug' => ( is => 'rw', default => 0, lazy => 1 ); ## NB - when udpated change doesn't propogate ! -has 'ua' => ( - handles => [qw/access_token auth_storage do_autorefresh get_scopes_as_array user /], - is => 'ro', - default => sub { WebService::GoogleAPI::Client::UserAgent->new( debug => shift->debug ) }, - lazy => 1, +has 'debug' => ( + is => 'rw', + default => 0, + lazy => 1 +); +has 'ua' => ( + handles => + [qw/access_token auth_storage do_autorefresh get_scopes_as_array user /], + is => 'ro', + default => + sub { WebService::GoogleAPI::Client::UserAgent->new(debug => shift->debug) } + , + lazy => 1, +); + +has 'chi' => ( + is => 'rw', + default => sub { + CHI->new( + driver => 'File', + max_key_length => 512, + namespace => __PACKAGE__ + ); + }, + lazy => 1 ); -has 'chi' => ( is => 'rw', default => sub { CHI->new( driver => 'File', max_key_length => 512, namespace => __PACKAGE__ ) }, lazy => 1 ); + has 'discovery' => ( handles => [ - qw/ discover_all extract_method_discovery_detail_from_api_spec get_api_discovery_for_api_id - methods_available_for_google_api_id list_of_available_google_api_ids / ## get_method_meta + qw/ discover_all + get_method_details + get_api_document service_exists + methods_available_for_google_api_id list_api_ids / ], is => 'ro', default => sub { my $self = shift; - return WebService::GoogleAPI::Client::Discovery->new( debug => $self->debug, ua => $self->ua, chi => $self->chi ); + return WebService::GoogleAPI::Client::Discovery->new( + debug => $self->debug, + ua => $self->ua, + chi => $self->chi + ); }, lazy => 1, ); @@ -369,10 +394,10 @@ sub _process_params_for_api_endpoint_and_return_errors croak('this should never happen - this method is internal only!') unless defined $params->{ api_endpoint_id }; - my $api_discovery_struct = $self->_ensure_api_spec_has_defined_fields( $self->discovery->get_api_discovery_for_api_id( $params->{ api_endpoint_id } ) ); ## $api_discovery_struct requried for service base URL + my $api_discovery_struct = $self->_ensure_api_spec_has_defined_fields( $self->discovery->get_api_document( $params->{ api_endpoint_id } ) ); ## $api_discovery_struct requried for service base URL $api_discovery_struct->{ baseUrl } =~ s/\/$//sxmg; ## remove trailing '/' from baseUrl - my $method_discovery_struct = $self->extract_method_discovery_detail_from_api_spec( $params->{ api_endpoint_id } ); ## if can get discovery data for google api endpoint then continue to perform detailed checks + my $method_discovery_struct = $self->get_method_details( $params->{ api_endpoint_id } ); ## if can get discovery data for google api endpoint then continue to perform detailed checks #save away original path so we can know if it's fiddled with #later @@ -536,7 +561,7 @@ sub has_scope_to_access_api_endpoint my ( $self, $api_ep ) = @_; return 0 unless defined $api_ep; return 0 if $api_ep eq ''; - my $method_spec = $self->extract_method_discovery_detail_from_api_spec( $api_ep ); + my $method_spec = $self->get_method_details( $api_ep ); if ( keys( %$method_spec ) > 0 ) ## empty hash indicates failure { @@ -627,13 +652,13 @@ sub has_scope_to_access_api_endpoint -=head2 C +=head2 C returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as a Perl data structure. - my $hashref = $self->get_api_discovery_for_api_id( 'gmail' ); - my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3' ); + my $hashref = $self->get_api_document( 'gmail' ); + my $hashref = $self->get_api_document( 'gmail:v3' ); returns the api discovery specification structure ( cached by CHI ) for api id ( eg 'gmail ') @@ -649,25 +674,26 @@ discovery specification for that method ( API Endpoint ). methods_available_for_google_api_id('gmail') -=head2 C +=head2 C - $my $api_detail = $gapi->discovery->extract_method_discovery_detail_from_api_spec( 'gmail.users.settings' ); + $my $api_detail = $gapi->discovery->get_method_details( 'gmail.users.settings' ); returns a hashref representing the discovery specification for the method identified by $tree in dotted API format such as texttospeech.text.synthesize returns an empty hashref if not found -=head2 C +=head2 C Returns an array list of all the available API's described in the API Discovery Resource that is either fetched or cached in CHI locally for 30 days. - my $r = $agent->list_of_available_google_api_ids(); + my $r = $agent->list_api_ids(); print "List of API Services ( comma separated): $r\n"; - my @list = $agent->list_of_available_google_api_ids(); + my @list = $agent->list_api_ids(); +To check for just one service id, use C instead. =head1 FEATURES diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index 1a45434..79ee93c 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -35,6 +35,7 @@ use Moo; use Carp; use WebService::GoogleAPI::Client::UserAgent; use List::Util qw/uniq reduce/; +use List::SomeUtils qw/pairwise/; use Hash::Slice qw/slice/; use Data::Dump qw/pp/; use CHI; @@ -112,7 +113,7 @@ Return details about all Available Google APIs as provided by Google or in CHI C Does the fetching with C, and arguments are as above. On Success: Returns HASHREF with keys discoveryVersion,items,kind -On Failure: Warns and returns empty hashref +On Failure: dies a horrible death. You probably don't want to continue in that case. SEE ALSO: available_APIs, list_of_available_google_api_ids @@ -125,35 +126,27 @@ sub discover_all { $self->get_with_cache($self->discover_key, @_); } +=head2 C -=head2 get_api_discovery_for_api_id + my $hashref = $disco->process_api_version('gmail') + # { api => 'gmail', version => 'v1' } + my $hashref = $disco->process_api_version({ api => 'gmail' }) + # { api => 'gmail', version => 'v1' } + my $hashref = $disco->process_api_version('gmail:v2') + # { api => 'gmail', version 'v2' } -returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as -a Perl data structure. - - my $hashref = $self->get_api_discovery_for_api_id( 'gmail' ); - my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3' ); - my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3.users.list' ); - my $hashref = $self->get_api_discovery_for_api_id( { api=> 'gmail', version => 'v3' } ); - -NB: if deeper structure than the api_id is provided then only the head is used +Takes a version string and breaks it into a hashref. If no version is +given, then default to the latest stable version in the discover document. -so get_api_discovery_for_api_id( 'gmail' ) is the same as get_api_discovery_for_api_id( 'gmail.some.child.method' ) - -returns the api discovery specification structure ( cached by CHI ) for api id (eg 'gmail') - -returns the discovery data as a hashref, an empty hashref on certain failing conditions or croaks on critical errors. =cut -sub get_api_discovery_for_api_id { +sub process_api_version { my ($self, $params) = @_; - ## TODO: warn if user doesn't have the necessary scope .. no should stil be able to examine - ## TODO: consolidate the http method calls to a single function - ie - discover_all - simplistic quick fix - assume that if no param then endpoint is as per discover_all + # scalar parameter not hashref - so assume is intended to be $params->{api} + $params = { api => $params } if ref $params eq ''; - $params = { api => $params } - if ref $params eq '' - ; ## scalar parameter not hashref - so assume is intended to be $params->{api} + croak "'api' must be defined" unless $params->{api}; ## trim any resource, method or version details in api id if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) { @@ -164,99 +157,61 @@ sub get_api_discovery_for_api_id { $params->{api} = $1; } + $params->{version} //= $self->latest_stable_version($params->{api}); + return $params +} - croak( - "get_api_discovery_for_api_id called with api param undefined" . pp $params) - unless defined $params->{api}; - $params->{version} = $self->latest_stable_version($params->{api}) - unless defined $params->{version}; - - croak("get_api_discovery_for_api_id called with empty api param defined" - . pp $params) - if $params->{api} eq ''; - croak("get_api_discovery_for_api_id called with empty version param defined" - . pp $params) - if $params->{version} eq ''; - - my $aapis = $self->available_APIs(); - - - my $api_verson_urls = {}; - for my $api (@{$aapis}) { - for (my $i = 0; $i < scalar @{ $api->{versions} }; $i++) { - $api_verson_urls->{ $api->{name} }{ $api->{versions}[$i] } - = $api->{discoveryRestUrl}[$i]; - } - } - croak("Unable to determine discovery URI for any version of $params->{api}") - unless defined $api_verson_urls->{ $params->{api} }; - croak( - "Unable to determine discovery URI for $params->{api} $params->{version}") - unless defined $api_verson_urls->{ $params->{api} }{ $params->{version} }; - my $api_discovery_uri - = $api_verson_urls->{ $params->{api} }{ $params->{version} }; - -#carp "get_api_discovery_for_api_id requires data from $api_discovery_uri" if $self->debug; - if (my $dat = $self->chi->get($api_discovery_uri) - ) ## clobbers some of the attempted thinking further on .. just return it for now if it's there - { - #carp pp $dat; - $self->{stats}{cache}{get}++; - return $dat; - } - if (my $expires_at = $self->chi->get_expires_at($api_discovery_uri) - ) ## maybe this isn't th ebest way to check if get available. - { - carp "CHI '$api_discovery_uri' cached data with root = " - . $self->chi->root_dir - . "expires in ", scalar($expires_at) - time(), " seconds\n" - if $self->debug; +=head2 get_api_document + +returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as +a Perl data structure. - #carp "Value = " . pp $self->chi->get( $api_discovery_uri ) if $self->debug ; - return $self->chi->get($api_discovery_uri); + my $hashref = $self->get_api_document( 'gmail' ); + my $hashref = $self->get_api_document( 'gmail:v3' ); + my $hashref = $self->get_api_document( 'gmail:v3.users.list' ); + my $hashref = $self->get_api_document( { api=> 'gmail', version => 'v3' } ); - } else { - carp "'$api_discovery_uri' not in cache - fetching it" if $self->debug; - ## TODO: better handle failed response - if 403 then consider adding in the auth headers and trying again. - #croak("Huh $api_discovery_uri"); - my $ret = $self->ua->validated_api_query($api_discovery_uri) - ; # || croak("Failed to retrieve $api_discovery_uri");; - if ($ret->is_success) { - my $dat = $ret->json - || croak("failed to convert $api_discovery_uri return data in json"); - - #carp("dat1 = " . pp $dat); - $self->chi->set($api_discovery_uri, $dat, '30 days'); - $self->{stats}{network}{get}++; - return $dat; - -#my $ret_data = $self->chi->get( $api_discovery_uri ); -#carp ("ret_data = " . pp $ret_data) unless ref($ret_data) eq 'HASH'; -#return $ret_data;# if ref($ret_data) eq 'HASH'; -#croak(); -#$self->chi->remove( $api_discovery_uri ) unless eval '${^TAINT}'; ## if not hashref then assume is corrupt so delete it - } else { - ## TODO - why is this failing for certain resources ?? because the urls contain a '$' ? because they are now authenticated? - carp("Fetching resource failed - $ret->message"); ## was croaking - carp(pp $ret ); - return {}; #$ret; - } - } - croak( - "something went wrong in get_api_discovery_for_api_id key = '$api_discovery_uri' - try again to see if data corruption has been flushed for " - . pp $params); +NB: if deeper structure than the api_id is provided then only the head is used +so get_api_document( 'gmail' ) is the same as get_api_document( 'gmail.some.child.method' ) +returns the api discovery specification structure ( cached by CHI ) for api id (eg 'gmail') +returns the discovery data as a hashref, an empty hashref on certain failing conditions or croaks on critical errors. + +Also available as get_api_discovery_for_api_id, which is being deprecated. + +=cut + +sub get_api_discovery_for_api_id { + carp <get_api_document(@_) +} + +sub get_api_document { + my ($self, $arg) = @_; + + my $params = $self->process_api_version($arg); + my $apis = $self->available_APIs(); + my $api = $apis->{$params->{api}}; + croak "No versions found for $params->{api}" unless $api->{version}; + my @versions = @{$api->{version}}; + my @urls = @{$api->{discoveryRestUrl}}; + + my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls; + + croak "Couldn't find correct url for $params->{api} $params->{version}" + unless $url; + + $self->get_with_cache($url); } -#TODO- is used in get_api_discover_for_api_id -# is used in service_exists -# is used in supported_as_text -# is used in available_versions -# is used in api_versions_urls -# is used in list_of_available_google_api_ids +#TODO- double triple check that we didn't break anything with the +#hashref change =head2 C Return arrayref of all available API's (services) @@ -308,14 +263,8 @@ sub available_APIs { $a; } {}, @relevant; - $available = [ - map { { - name => $_, - versions => $reduced->{$_}{version}, - doclinks => $reduced->{$_}{documentationLink}, - discoveryRestUrl => $reduced->{$_}{discoveryRestUrl} - } } keys %$reduced - ]; + $available = $reduced; + } =head2 C @@ -378,66 +327,29 @@ sub augment_with { =head2 C - Return 1 if Google Service API ID is described by Google API discovery. - Otherwise return 0 +Return 1 if Google Service API ID is described by Google API discovery. +Otherwise return 0 - print $d->service_exists('calendar'); # 1 - print $d->service_exists('someapi'); # 0 + print $disco->service_exists('calendar'); # 1 + print $disco->service_exists('someapi'); # 0 -NB - Is case sensitive - all lower is required so $d->service_exists('Calendar') returns 0 +Note that most Google APIs are fully lowercase, but some are camelCase. Please +check the documentation from Google for reference. =cut sub service_exists { my ($self, $api) = @_; - return 0 unless $api; - my $apis_all = $self->available_APIs(); - return - grep { $_->{name} eq $api } - @$apis_all; ## 1 iff an equality is found with keyed name -} - -=head2 C - - No params. - Returns list of supported APIs as string in human-readible format ( name, versions and doclinks ) - - -=cut - -sub supported_as_text { - my ($self) = @_; - my $ret = ''; - for my $api (@{ $self->available_APIs() }) { - croak('doclinks key defined but is not the expected arrayref') - unless ref $api->{doclinks} eq 'ARRAY'; - croak( - 'array of apis provided by available_APIs includes one without a defined name' - ) unless defined $api->{name}; - - my @clean_doclinks = grep { defined $_ } - @{ $api->{doclinks} } - ; ## was seeing undef in doclinks array - eg 'surveys'causing warnings in join - - ## unique doclinks using idiom from https://www.oreilly.com/library/view/perl-cookbook/1565922433/ch04s07.html - my %seen = (); - my $doclinks = join(',', (grep { !$seen{$_}++ } @clean_doclinks)) - || ''; ## unique doclinks as string - - $ret - .= $api->{name} . ' : ' - . join(',', @{ $api->{versions} }) . ' : ' - . $doclinks . "\n"; - } - return $ret; + return unless $api; + return $self->available_APIs->{$api} } =head2 C Show available versions of particular API described by api id passed as parameter such as 'gmail' - $d->available_versions('calendar'); # ['v3'] - $d->available_versions('youtubeAnalytics'); # ['v1','v1beta1'] + $disco->available_versions('calendar'); # ['v3'] + $disco->available_versions('youtubeAnalytics'); # ['v1','v1beta1'] Returns arrayref @@ -446,9 +358,7 @@ sub supported_as_text { sub available_versions { my ($self, $api) = @_; return [] unless $api; - my @api_target = grep { $_->{name} eq $api } @{ $self->available_APIs() }; - return [] if scalar(@api_target) == 0; - return $api_target[0]->{versions}; + return $self->available_APIs->{$api}->{version} // [] } =head2 C @@ -469,46 +379,38 @@ return latest stable verion of API sub latest_stable_version { my ($self, $api) = @_; return '' unless $api; - return '' unless $self->available_versions($api); - return '' unless @{ $self->available_versions($api) } > 0; - my $versions = $self->available_versions($api); # arrayref - if ($versions->[-1] =~ /beta/) { - return $versions->[0]; - } else { - return $versions->[-1]; - } -} - - -######################################################## -sub api_version_urls { - my ($self) = @_; - ## transform structure to be keyed on api->versionRestUrl - my $aapis = $self->available_APIs(); - my $api_verson_urls = {}; - for my $api (@{$aapis}) { - for (my $i = 0; $i < scalar @{ $api->{versions} }; $i++) { - $api_verson_urls->{ $api->{name} }{ $api->{versions}[$i] } - = $api->{discoveryRestUrl}[$i]; - } - } - return $api_verson_urls; + my $versions = $self->available_versions($api); + return '' unless $versions; + return '' unless @{ $versions } > 0; + #remove alpha or beta versions + my @stable = grep { !/beta|alpha/ } @$versions; + return $stable[-1] || ''; } -######################################################## -=head2 C +=head2 C - $agent->extract_method_discovery_detail_from_api_spec( $tree, $api_version ) + $disco->get_method_details($tree, $api_version) returns a hashref representing the discovery specification for the method identified by $tree in dotted API format such as texttospeech.text.synthesize -returns an empty hashref if not found +returns an empty hashref if not found. +Also available as C, but the long name is being +deprecated in favor of the more compact one. =cut ######################################################## sub extract_method_discovery_detail_from_api_spec { + carp <get_method_details(@_) +} + +sub get_method_details { my ($self, $tree, $api_version) = @_; ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method @@ -542,7 +444,7 @@ sub extract_method_discovery_detail_from_api_spec { ## TODO: confirm that spec available for api version - my $api_spec = $self->get_api_discovery_for_api_id( + my $api_spec = $self->get_api_document( { api => $api_id, version => $api_version }); @@ -624,7 +526,7 @@ sub _extract_resource_methods_from_api_spec { # ->{response}{ $schemas->{Buckets} } # # It assumes that the schemas have been extracted from the original discover for the API -# and is typically applued to the method ( api endpoint ) to provide a fully descriptive +# and is typically applied to the method ( api endpoint ) to provide a fully descriptive # structure without external references. # #=cut @@ -686,7 +588,6 @@ representing the corresponding discovery specification for that method ( API End # get_api_discovery_for_api_id ######################################################## -## TODO: consider renaming ? sub methods_available_for_google_api_id { my ($self, $api_id, $version) = @_; @@ -701,36 +602,36 @@ sub methods_available_for_google_api_id { ######################################################## -=head2 C +=head2 C Returns an array list of all the available API's described in the API Discovery Resource that is either fetched or cached in CHI locally for 30 days. - my $r = $agent->list_of_available_google_api_ids(); + my $r = $agent->list_api_ids(); print "List of API Services ( comma separated): $r\n"; - my @list = $agent->list_of_available_google_api_ids(); + my @list = $agent->list_api_ids(); + +Formerly was list_of_available_google_api_ids, which will now give a deprecation warning +to switch to list_api_ids. =cut -######################################################## -## returns a list of all available API Services sub list_of_available_google_api_ids { + carp <list_api_ids +} +## returns a list of all available API Services +sub list_api_ids { my ($self) = @_; - my $aapis = $self->available_APIs(); ## array of hashes - my @api_list = map { $_->{name} } @$aapis; - return - wantarray - ? @api_list - : join(',', @api_list) - ; ## allows to be called in either list or scalar context - #return @api_list; - + my @api_list = keys %{$self->available_APIs}; + return wantarray ? @api_list + : join(',', @api_list); } -######################################################## 1; - -## TODO - CODE REVIEW -## get_expires_at .. does this do what is expected ? - what if has expired and so get fails - will this still return a value? diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index b82a077..b7ac0ff 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -158,27 +158,74 @@ subtest 'Augmenting the api' => sub { }; -subtest 'available_APIs' => sub { +subtest 'checking for API availablity' => sub { #NOTE- this is used a lot internally, so let's just make sure it # does what we want my $list = $disco->available_APIs; - like $list, bag { - item hash { - field name => 'gmail'; - field versions => bag { item 'v1'; etc; }; - field doclinks => bag { item 'https://developers.google.com/gmail/api/'; etc; }; + like $list, hash { + field gmail => hash { + field version => bag { item 'v1'; etc; }; + field documentationLink => bag { item 'https://developers.google.com/gmail/api/'; etc; }; field discoveryRestUrl => bag { item 'https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest'; etc; }; end; }; etc; }, 'collapsed structure as expected'; + subtest 'service_exists' => sub { + ok !$disco->service_exists('yourfez'), 'non-extant tells us such'; + ok $disco->service_exists('gmail'), 'verified that gmail exists'; + ok !$disco->service_exists('Gmail'), 'is case sensitive'; + ok $disco->service_exists('youtubeAnalytics'), 'gets youtube analytics'; + }; + + subtest 'available_version' => sub { + is $disco->available_versions('gmail'), [ 'v1' ], 'got gmail'; + is $disco->available_versions('GMAIL'), [], 'case sensitive gmail'; + is $disco->available_versions('firestore'), [ qw/v1 v1beta1 v1beta2/ ], + 'got many versions for firestore'; + is $disco->available_versions('youtubeAnalytics'), [ qw/v1 v2/ ], + 'got many for youtube analytics'; + is $disco->available_versions('yourfez'), [], 'empty for non-existant'; + }; + + subtest 'latest_stable_version' => sub { + is $disco->latest_stable_version('gmail'), 'v1', 'got for gmail'; + is $disco->latest_stable_version('compute'), 'v1', 'got for compute'; + is $disco->latest_stable_version('bigqueryreservation'), + 'v1', 'ignores alphas AND betas'; + }; + + subtest 'list_api_ids' => sub { + for my $id (qw/gmail compute bigqueryreservation/) { + like scalar($disco->list_api_ids), qr/$id/, + "has an entry for $id in scalar mode"; + like [ $disco->list_api_ids ], bag { + item $id; + etc; + }, "has an entry for $id in list mode"; + } + }; + + subtest 'process_api_version' => sub { + is $disco->process_api_version('gmail'), + { api => 'gmail', version => 'v1' }, + 'got default'; + + is $disco->process_api_version({ api => 'gmail' }), + { api => 'gmail', version => 'v1' }, + 'got default from hashref'; + + is $disco->process_api_version('gmail:v2'), + { api => 'gmail', version => 'v2' }, + 'take a version if given'; + + #TODO- check for proper behavior on errors + }; + }; -#TODO- get available_APIs and list_of_available_api_ids -# compare and contrast which one is extraneous. Make sure outside -# code doesn't peek into our innards... done_testing; __DATA__ From 6e6ab1776cae4633da38dc5ffcac4eb8e5f255c1 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 27 May 2020 18:10:48 +0300 Subject: [PATCH 16/68] continuing this sweet refactor almost done with Client::Discovery! --- lib/WebService/GoogleAPI/Client/Discovery.pm | 257 ++++++++++--------- t/WebService/GoogleAPI/Client/Discovery.t | 7 +- 2 files changed, 137 insertions(+), 127 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index 79ee93c..ff9a4a4 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -126,109 +126,25 @@ sub discover_all { $self->get_with_cache($self->discover_key, @_); } -=head2 C - - my $hashref = $disco->process_api_version('gmail') - # { api => 'gmail', version => 'v1' } - my $hashref = $disco->process_api_version({ api => 'gmail' }) - # { api => 'gmail', version => 'v1' } - my $hashref = $disco->process_api_version('gmail:v2') - # { api => 'gmail', version 'v2' } - -Takes a version string and breaks it into a hashref. If no version is -given, then default to the latest stable version in the discover document. - - -=cut - -sub process_api_version { - my ($self, $params) = @_; - # scalar parameter not hashref - so assume is intended to be $params->{api} - $params = { api => $params } if ref $params eq ''; - - croak "'api' must be defined" unless $params->{api}; - - ## trim any resource, method or version details in api id - if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) { - $params->{api} = $1; - $params->{version} = $2; - } - if ($params->{api} =~ /^(.*?)\./xsm) { - $params->{api} = $1; - } - - $params->{version} //= $self->latest_stable_version($params->{api}); - return $params -} - - -=head2 get_api_document - -returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as -a Perl data structure. - - my $hashref = $self->get_api_document( 'gmail' ); - my $hashref = $self->get_api_document( 'gmail:v3' ); - my $hashref = $self->get_api_document( 'gmail:v3.users.list' ); - my $hashref = $self->get_api_document( { api=> 'gmail', version => 'v3' } ); - -NB: if deeper structure than the api_id is provided then only the head is used -so get_api_document( 'gmail' ) is the same as get_api_document( 'gmail.some.child.method' ) -returns the api discovery specification structure ( cached by CHI ) for api id (eg 'gmail') -returns the discovery data as a hashref, an empty hashref on certain failing conditions or croaks on critical errors. - -Also available as get_api_discovery_for_api_id, which is being deprecated. - -=cut - -sub get_api_discovery_for_api_id { - carp <get_api_document(@_) -} - -sub get_api_document { - my ($self, $arg) = @_; - - my $params = $self->process_api_version($arg); - my $apis = $self->available_APIs(); - - my $api = $apis->{$params->{api}}; - croak "No versions found for $params->{api}" unless $api->{version}; - my @versions = @{$api->{version}}; - my @urls = @{$api->{discoveryRestUrl}}; - - my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls; - - croak "Couldn't find correct url for $params->{api} $params->{version}" - unless $url; - - $self->get_with_cache($url); -} - - - #TODO- double triple check that we didn't break anything with the #hashref change =head2 C -Return arrayref of all available API's (services) +Return hashref keyed on api name, with arrays of versions, links to +documentation, and links to the url for that version's API document. { - name => 'youtube', - versions => [ 'v3' ] - doclinks => [ ... ] , - discoveryRestUrl => [ ... ] , - }, - -Originally for printing list of supported API's in documentation .. - + youtube => { + version => [ 'v3', ... ] + documentationLink => [ ..., ... ] , + discoveryRestUrl => [ ..., ... ] , + }, + gmail => { + ... + } + } -SEE ALSO: -may be better/more flexible to use client->list_of_available_google_api_ids -client->discover_all which is delegated to Client::Discovery->discover_all +Used internally to pull relevant discovery documents. =cut @@ -246,11 +162,11 @@ sub available_APIs { my $d_all = $self->discover_all; croak 'no items in discovery data' unless defined $d_all->{items}; + #grab only entries with the four keys we want + #and strip other keys my @keys = qw/name version documentationLink discoveryRestUrl/; my @relevant; for my $i (@{ $d_all->{items} }) { - #grab only entries with the four keys we want, and strip other - #keys next unless @keys == grep { exists $i->{$_} } @keys; push @relevant, { %{$i}{@keys} }; }; @@ -263,8 +179,8 @@ sub available_APIs { $a; } {}, @relevant; + #store it away globally $available = $reduced; - } =head2 C @@ -297,8 +213,8 @@ being deprecated for being plain old too long. sub augment_discover_all_with_unlisted_experimental_api { my ($self, $api_spec) = @_; carp <augment_with($api_spec); } @@ -387,11 +303,120 @@ sub latest_stable_version { return $stable[-1] || ''; } +=head2 C + + my $hashref = $disco->process_api_version('gmail') + # { api => 'gmail', version => 'v1' } + my $hashref = $disco->process_api_version({ api => 'gmail' }) + # { api => 'gmail', version => 'v1' } + my $hashref = $disco->process_api_version('gmail:v2') + # { api => 'gmail', version 'v2' } + +Takes a version string and breaks it into a hashref. If no version is +given, then default to the latest stable version in the discover document. + +=cut + +sub process_api_version { + my ($self, $params) = @_; + # scalar parameter not hashref - so assume is intended to be $params->{api} + $params = { api => $params } if ref $params eq ''; + + croak "'api' must be defined" unless $params->{api}; + + ## trim any resource, method or version details in api id + if ($params->{api} =~ /([^:]+):(v[^\.]+)/ixsm) { + $params->{api} = $1; + $params->{version} = $2; + } + if ($params->{api} =~ /^(.*?)\./xsm) { + $params->{api} = $1; + } + + unless ($self->service_exists($params->{api})) { + croak "$params->{api} does not seem to be a valid Google API"; + } + + $params->{version} //= $self->latest_stable_version($params->{api}); + return $params +} + + +=head2 get_api_document + +returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as +a Perl data structure. + + my $hashref = $self->get_api_document( 'gmail' ); + my $hashref = $self->get_api_document( 'gmail:v3' ); + my $hashref = $self->get_api_document( 'gmail:v3.users.list' ); + my $hashref = $self->get_api_document( { api=> 'gmail', version => 'v3' } ); + +NB: if deeper structure than the api_id is provided then only the head is used +so get_api_document( 'gmail' ) is the same as get_api_document( 'gmail.some.child.method' ) +returns the api discovery specification structure ( cached by CHI ) for api id (eg 'gmail') +returns the discovery data as a hashref, an empty hashref on certain failing conditions or croaks on critical errors. + +Also available as get_api_discovery_for_api_id, which is being deprecated. + +=cut + +sub get_api_discovery_for_api_id { + carp <get_api_document(@_) +} + +sub get_api_document { + my ($self, $arg) = @_; + + my $params = $self->process_api_version($arg); + my $apis = $self->available_APIs(); + + my $api = $apis->{$params->{api}}; + croak "No versions found for $params->{api}" unless $api->{version}; + + my @versions = @{$api->{version}}; + my @urls = @{$api->{discoveryRestUrl}}; + my ($url) = pairwise { $a eq $params->{version} ? $b : () } @versions, @urls; + + croak "Couldn't find correct url for $params->{api} $params->{version}" + unless $url; + + $self->get_with_cache($url); +} + +#TODO- HERE - we are here in refactoring +sub _extract_resource_methods_from_api_spec { + my ($self, $tree, $api_spec, $ret) = @_; + $ret = {} unless defined $ret; + croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH'; + + if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') { + foreach my $method (keys %{ $api_spec->{methods} }) { + $ret->{"$tree.$method"} = $api_spec->{methods}{$method} + if ref($api_spec->{methods}{$method}) eq 'HASH'; + } + } + if (defined $api_spec->{resources}) { + foreach my $resource (keys %{ $api_spec->{resources} }) { + ## NB - recursive traversal down tree of api_spec resources + $self->_extract_resource_methods_from_api_spec("$tree.$resource", + $api_spec->{resources}{$resource}, $ret); + } + } + return $ret; +} + =head2 C $disco->get_method_details($tree, $api_version) -returns a hashref representing the discovery specification for the method identified by $tree in dotted API format such as texttospeech.text.synthesize +returns a hashref representing the discovery specification for the +method identified by $tree in dotted API format such as +texttospeech.text.synthesize returns an empty hashref if not found. @@ -400,12 +425,10 @@ deprecated in favor of the more compact one. =cut -######################################################## sub extract_method_discovery_detail_from_api_spec { carp <get_method_details(@_) } @@ -414,6 +437,7 @@ sub get_method_details { my ($self, $tree, $api_version) = @_; ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method + #TODO- should die a horrible death if method not found return {} unless defined $tree; my @nodes = split /\./smx, $tree; @@ -493,26 +517,6 @@ sub get_method_details { ######################################################## ######################################################## -sub _extract_resource_methods_from_api_spec { - my ($self, $tree, $api_spec, $ret) = @_; - $ret = {} unless defined $ret; - croak("ret not a hash - $tree, $api_spec, $ret") unless ref($ret) eq 'HASH'; - - if (defined $api_spec->{methods} && ref($api_spec->{methods}) eq 'HASH') { - foreach my $method (keys %{ $api_spec->{methods} }) { - $ret->{"$tree.$method"} = $api_spec->{methods}{$method} - if ref($api_spec->{methods}{$method}) eq 'HASH'; - } - } - if (defined $api_spec->{resources}) { - foreach my $resource (keys %{ $api_spec->{resources} }) { - ## NB - recursive traversal down tree of api_spec resources - $self->_extract_resource_methods_from_api_spec("$tree.$resource", - $api_spec->{resources}{$resource}, $ret); - } - } - return $ret; -} ######################################################## #=head2 C @@ -588,6 +592,7 @@ representing the corresponding discovery specification for that method ( API End # get_api_discovery_for_api_id ######################################################## +#TODO- give short name and deprecate sub methods_available_for_google_api_id { my ($self, $api_id, $version) = @_; diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index b7ac0ff..b761c41 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -218,10 +218,15 @@ subtest 'checking for API availablity' => sub { 'got default from hashref'; is $disco->process_api_version('gmail:v2'), + { api => 'gmail', version => 'v2' }, + 'take a version if given (even if imaginary)'; + + is $disco->process_api_version({ api => 'gmail', version => 'v2' }), { api => 'gmail', version => 'v2' }, 'take a version if given'; - #TODO- check for proper behavior on errors + like dies { $disco->process_api_version('fleurbop') }, + qr/fleurbop .* not .* valid .* API/, 'errors on non-found api'; }; }; From 486083192b121708fe6e098dddb43a8578f961b2 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 5 Aug 2020 19:46:50 +0300 Subject: [PATCH 17/68] updated gitignore to allow .yath.user.rc for testing w/ real creds started fixing brace style in client-userAgent --- .gitignore | 1 + lib/WebService/GoogleAPI/Client/UserAgent.pm | 17 +++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index d28ae1c..9c1a4f5 100755 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ examples/*.png *.json docs/cover/* +.yath.user.rc .*.sw? diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index a22799f..4e04492 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -184,26 +184,20 @@ sub validated_api_query ## TODO: HANDLE TIMEOUTS AND OTHER ERRORS IF THEY WEREN'T HANDLED BY build_http_transaction - ## TODO: -# return Mojo::Message::Response->new unless ref( $res ) eq 'Mojo::Message::Response'; + ## TODO: return Mojo::Message::Response->new unless ref( $res ) eq 'Mojo::Message::Response'; - if ( ( $res->code == 401 ) && $self->do_autorefresh ) - { - if ( $res->code == 401 ) ## redundant - was there something else in mind ? + if ( ( $res->code == 401 ) && $self->do_autorefresh ) { + if ( $res->code == 401 ) { ## redundant - was there something else in mind ? #TODO- I'm fairly certain this fires too often - { croak "No user specified, so cant find refresh token and update access_token" unless $self->user; cluck "401 response - access_token was expired. Attemptimg to update it automatically ..." if ($self->debug > 11); - # cluck "Seems like access_token was expired. Attemptimg update it automatically ..." if $self->debug; - my $cred = $self->auth_storage->get_credentials_for_refresh( $self->user ); # get client_id, client_secret and refresh_token my $new_token = $self->refresh_access_token( $cred )->{ access_token }; # here also {id_token} etc cluck "validated_api_query() Got a new token: " . $new_token if ($self->debug > 11); $self->access_token( $new_token ); - if ( $self->auto_update_tokens_in_storage ) - { + if ( $self->auto_update_tokens_in_storage ) { $self->auth_storage->set_access_token_to_storage( $self->user, $self->access_token ); } @@ -212,8 +206,7 @@ sub validated_api_query $res = $self->start( $self->build_http_transaction( $params ) )->res; # Mojo::Message::Response } } - elsif ( $res->code == 403 ) - { + elsif ( $res->code == 403 ) { cluck( 'Unexpected permission denied 403 error ' ); return $res; } From 31a27c22aea764c8959e0aedac6acc1c065c1533 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 4 Nov 2020 17:04:19 +0200 Subject: [PATCH 18/68] fix up tests which depended on outside sources --- README.txt | 20 +++++++++++--------- t/WebService/GoogleAPI/Client/Discovery.t | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.txt b/README.txt index 1c2d1bd..d8ead4d 100644 --- a/README.txt +++ b/README.txt @@ -13,7 +13,7 @@ SYNOPSIS Includes Discovery, validation authentication and API Access. assumes gapi.json configuration in working directory with scoped Google - project redentials and user authorization created by _goauth_ + project credentials and user authorization created by _goauth_ use WebService::GoogleAPI::Client; @@ -281,14 +281,14 @@ METHODS DELEGATED TO WebService::GoogleAPI::Client::Discovery } print dump $new_hash->{gmail}; - get_api_discovery_for_api_id + get_api_document returns the cached version if avaiable in CHI otherwise retrieves discovery data via HTTP, stores in CHI cache and returns as a Perl data structure. - my $hashref = $self->get_api_discovery_for_api_id( 'gmail' ); - my $hashref = $self->get_api_discovery_for_api_id( 'gmail:v3' ); + my $hashref = $self->get_api_document( 'gmail' ); + my $hashref = $self->get_api_document( 'gmail:v3' ); returns the api discovery specification structure ( cached by CHI ) for api id ( eg 'gmail ') @@ -304,9 +304,9 @@ METHODS DELEGATED TO WebService::GoogleAPI::Client::Discovery methods_available_for_google_api_id('gmail') - extract_method_discovery_detail_from_api_spec + get_method_details - $my $api_detail = $gapi->discovery->extract_method_discovery_detail_from_api_spec( 'gmail.users.settings' ); + $my $api_detail = $gapi->discovery->get_method_details( 'gmail.users.settings' ); returns a hashref representing the discovery specification for the method identified by $tree in dotted API format such as @@ -314,16 +314,18 @@ METHODS DELEGATED TO WebService::GoogleAPI::Client::Discovery returns an empty hashref if not found - list_of_available_google_api_ids + list_api_ids Returns an array list of all the available API's described in the API Discovery Resource that is either fetched or cached in CHI locally for 30 days. - my $r = $agent->list_of_available_google_api_ids(); + my $r = $agent->list_api_ids(); print "List of API Services ( comma separated): $r\n"; - my @list = $agent->list_of_available_google_api_ids(); + my @list = $agent->list_api_ids(); + + To check for just one service id, use service_exists instead. FEATURES diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index b761c41..b7f9a01 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -167,7 +167,7 @@ subtest 'checking for API availablity' => sub { field gmail => hash { field version => bag { item 'v1'; etc; }; field documentationLink => bag { item 'https://developers.google.com/gmail/api/'; etc; }; - field discoveryRestUrl => bag { item 'https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest'; etc; }; + field discoveryRestUrl => bag { item 'https://gmail.googleapis.com/$discovery/rest?version=v1'; etc; }; end; }; etc; @@ -183,7 +183,7 @@ subtest 'checking for API availablity' => sub { subtest 'available_version' => sub { is $disco->available_versions('gmail'), [ 'v1' ], 'got gmail'; is $disco->available_versions('GMAIL'), [], 'case sensitive gmail'; - is $disco->available_versions('firestore'), [ qw/v1 v1beta1 v1beta2/ ], + is $disco->available_versions('firestore'), bag { item $_ for qw/v1 v1beta1 v1beta2/; end }, 'got many versions for firestore'; is $disco->available_versions('youtubeAnalytics'), [ qw/v1 v2/ ], 'got many for youtube analytics'; From 8599d96aedf2431cf4f20d410d5949ad3b6e299b Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 4 Nov 2020 17:13:12 +0200 Subject: [PATCH 19/68] bump version --- dist.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist.ini b/dist.ini index 3f89b16..1e50886 100755 --- a/dist.ini +++ b/dist.ini @@ -3,7 +3,7 @@ author = Peter Scott license = Apache_2_0 copyright_holder = Peter Scott and others copyright_year = 2017-2018 -version = 0.21 +version = 0.22 main_module = lib/WebService/GoogleAPI/Client.pm ;[MinimumPerl] From d5e6f4498a8de8f10e3c5a031520d301d6479849 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Wed, 4 Nov 2020 17:19:17 +0200 Subject: [PATCH 20/68] bumped version --- README.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.txt b/README.txt index d8ead4d..9cd80ed 100644 --- a/README.txt +++ b/README.txt @@ -4,7 +4,7 @@ NAME VERSION - version 0.21 + version 0.22 SYNOPSIS From f12630f729bdb2f6c1b3beea656a7feff010822e Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 5 Nov 2020 13:55:10 +0200 Subject: [PATCH 21/68] add Git::CommitBuild plugin --- dist.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/dist.ini b/dist.ini index 1e50886..a2bf3d0 100755 --- a/dist.ini +++ b/dist.ini @@ -89,6 +89,7 @@ file_name = CHANGES wrap_column = 74 debug = 0 +[Git::CommitBuild] [Test::Perl::Critic] critic_config = .perlcriticrc ; default / relative to project root ; i am unable to get dzil test to use my .perlcriticrc or use modern to avoid structure warnings From 11a1b35f0b41ecfb26ebf0d340ab254c62e2207a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Mon, 18 Jan 2021 22:22:25 +0200 Subject: [PATCH 22/68] fix tests to work with google updates and newer JWT uses --- .gitignore | 1 + t/JWT/01_basic.t | 1 + t/WebService/GoogleAPI/Client/Discovery.t | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9c1a4f5..e8d2c10 100755 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ docs/cover/* .yath.user.rc .*.sw? +.perl-version diff --git a/t/JWT/01_basic.t b/t/JWT/01_basic.t index 848f739..50d0e1f 100644 --- a/t/JWT/01_basic.t +++ b/t/JWT/01_basic.t @@ -107,6 +107,7 @@ is $jwt->from_json( "$testdir/load4.json" ), 0, 'must be for service account'; $jwt = WebService::GoogleAPI::JWT->new; is $jwt->client_email('mysa@developer.gserviceaccount.com'), $jwt, 'sa set'; $jwt->expires('9999999999'); +$jwt->secret('hi'); my $jwte = $jwt->encode; my $jwtd = $jwt->decode($jwte); diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index b7f9a01..4e7474a 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -185,8 +185,8 @@ subtest 'checking for API availablity' => sub { is $disco->available_versions('GMAIL'), [], 'case sensitive gmail'; is $disco->available_versions('firestore'), bag { item $_ for qw/v1 v1beta1 v1beta2/; end }, 'got many versions for firestore'; - is $disco->available_versions('youtubeAnalytics'), [ qw/v1 v2/ ], - 'got many for youtube analytics'; + is $disco->available_versions('youtubeAnalytics'), [ 'v2' ], + 'got correct only one version for youtube analytics'; is $disco->available_versions('yourfez'), [], 'empty for non-existant'; }; From 5bc8e0ce9fdb95c1056fa468eb396f0174455345 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Mon, 18 Jan 2021 22:30:49 +0200 Subject: [PATCH 23/68] fix up random warning from test --- t/WebService/GoogleAPI/Client/Discovery.t | 1 + t/lib/TestTools.pm | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index 4e7474a..e4d9405 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -33,6 +33,7 @@ subtest 'WebService::GoogleAPI::Client::Discovery class properties' => sub { $disco->debug(DEBUG); #TODO- mock away the actual get request... +# will prevent the occasional flake on this in the future subtest 'discover_all' => sub { subtest 'hits the wire with no cache' => sub { like $disco->discover_all->{items}, bag { diff --git a/t/lib/TestTools.pm b/t/lib/TestTools.pm index cf471c6..6fd614c 100644 --- a/t/lib/TestTools.pm +++ b/t/lib/TestTools.pm @@ -3,7 +3,7 @@ use strict; use warnings; use Exporter::Shiny qw/ gapi_json DEBUG user has_credentials set_credentials/; -use Mojo::File qw/ curfile path/; +use Mojo::File qw/curfile path/; my $gapi; #try and find a good gapi.json to use here. Check as follows: @@ -13,7 +13,7 @@ my $gapi; # author tests can find the one in our main folder # 4) the main dir of this project # 5) the fake one in the t/ directory -$gapi = path($ENV{GOOGLE_TOKENSFILE} || 'gapi.json'); +$gapi = path($ENV{GOOGLE_TOKENSFILE} || './gapi.json'); $gapi = curfile->dirname->dirname->dirname->sibling('gapi.json') unless $gapi->stat; $gapi = curfile->dirname->dirname->sibling('gapi.json') From 82d81b5b2b2c70a1c7c380aa4488d5a1e1de1f0d Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 15:15:38 +0200 Subject: [PATCH 24/68] Remove this implementation of JWT::Google in favor of our CPAN one --- dist.ini | 2 +- examples/service_account_example.pl | 4 +- lib/WebService/GoogleAPI/JWT.pm | 208 ---------------------------- t/JWT/01_basic.t | 115 --------------- t/JWT/load1.json | 7 - t/JWT/load2.json | 1 - t/JWT/load3.json | 6 - t/JWT/load4.json | 7 - 8 files changed, 3 insertions(+), 347 deletions(-) delete mode 100644 lib/WebService/GoogleAPI/JWT.pm delete mode 100644 t/JWT/01_basic.t delete mode 100644 t/JWT/load1.json delete mode 100644 t/JWT/load2.json delete mode 100644 t/JWT/load3.json delete mode 100644 t/JWT/load4.json diff --git a/dist.ini b/dist.ini index a2bf3d0..389b58f 100755 --- a/dist.ini +++ b/dist.ini @@ -32,7 +32,7 @@ List::Util = 1.45 List::SomeUtils = 0 IO::Socket::SSL = 2.06 Mojo::JWT = 0 -Mojo::JWT::Google = 0.11 +Mojo::JWT::Google = 0.12 Exporter::Shiny = 0 [Prereqs / TestRequires ] diff --git a/examples/service_account_example.pl b/examples/service_account_example.pl index b58c232..2f6cde6 100644 --- a/examples/service_account_example.pl +++ b/examples/service_account_example.pl @@ -3,14 +3,14 @@ use strictures; use Data::Printer; use Mojo::UserAgent; -use WebService::GoogleAPI::JWT; +use Mojo::JWT::Google; my $config = { path => $ARGV[0] // '/Users/peter/Downloads/computerproscomau-b9f59b8ee34a.json', scopes => $ARGV[1] // 'https://www.googleapis.com/auth/plus.business.manage https://www.googleapis.com/auth/compute' }; -my $jwt = WebService::GoogleAPI::JWT->new( from_json => $config->{path}, scopes => [ split / /, $config->{scopes} ] ); +my $jwt = Mojo::JWT::Google->new( from_json => $config->{path}, scopes => [ split / /, $config->{scopes} ] ); my $ua = Mojo::UserAgent->new(); diff --git a/lib/WebService/GoogleAPI/JWT.pm b/lib/WebService/GoogleAPI/JWT.pm deleted file mode 100644 index daf1a3b..0000000 --- a/lib/WebService/GoogleAPI/JWT.pm +++ /dev/null @@ -1,208 +0,0 @@ -use strictures; -package WebService::GoogleAPI::JWT; - -# ABSTRACT: JWT for authorizing service accounts - -use utf8; -use Mojo::Base qw(Mojo::JWT); -use vars qw($VERSION); -use Mojo::Collection 'c'; -use Mojo::File (); -use Mojo::JSON qw(decode_json); -use Carp; - -has client_email => undef; -has expires_in => 3600; -has issue_at => undef; -has scopes => sub { c() }; -has target => q(https://www.googleapis.com/oauth2/v4/token); -has user_as => undef; -has audience => undef; - -sub new { - my ($class, %options) = @_; - my $self = $class->SUPER::new(%options); - return $self if not defined $self->{from_json}; - - my $result = $self->from_json($self->{from_json}); - - if ( $result == 0 ) { - croak 'Your JSON file import failed.'; - } - return $self; -} - - -sub claims { - my ($self, $value) = @_; - if (defined $value) { - $self->{claims} = $value; - return $self; - } - my $claims = $self->_construct_claims; - unless (exists $claims->{exp}) { - $claims->{exp} = $self->now + $self->expires_in ; - } - return $claims; -} - -sub _construct_claims { - my $self = shift; - my $result = {}; - $result->{iss} = $self->client_email; - $result->{aud} = $self->target; - $result->{sub} = $self->user_as if defined $self->user_as; - my @scopes = @{ $self->scopes }; - - croak "Can't use both scopes and audience in the same token" if @scopes && $self->audience; - $result->{scope} = c(@scopes)->join(' ')->to_string if @scopes; - $result->{target_audience} = $self->audience if defined $self->audience; - - if ( not defined $self->issue_at ) { - $self->set_iat(1); - } - else { - $self->set_iat(0); - $result->{iat} = $self->issue_at; - $result->{exp} = $self->issue_at + $self->expires_in; - } - return $result; -} - -sub from_json { - my ($self, $value) = @_; - return 0 if not defined $value; - return 0 if not -f $value; - my $json = decode_json( Mojo::File->new($value)->slurp ); - return 0 if not defined $json->{private_key}; - return 0 if $json->{type} ne 'service_account'; - $self->algorithm('RS256'); - $self->secret($json->{private_key}); - $self->client_email($json->{client_email}); - return $self -} - -1; - - -=head1 NAME - -WebService::GoogleAPI::JWT - Service Account tokens - -=head1 VERSION - -0.05 - - -=head1 SYNOPSIS - - my $gjwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t', - scopes => [ '/my/scope/a', '/my/scope/b' ], - client_email => 'riche@cpan.org')->encode; - -=head1 DESCRIPTION - -Like L, you can instantiate this class by using the same syntax, -except that this class constructs the claims for you. - - my $jwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t')->encode; - -And add any attribute defined in this class. The JWT is fairly useless unless -you define your scopes. - - my $gjwt = WebService::GoogleAPI::JWT->new(secret => 's3cr3t', - scopes => [ '/my/scope/a', '/my/scope/b' ], - client_email => 'riche@cpan.org')->encode; - -You can also get your information automatically from the .json you received -from Google. Your secret key is in that file, so it's best to keep it safe -somewhere. This will ease some busy work in configuring the object -- with -virtually the only things to do is determine the scopes and the user_as if you -need to impersonate. - - my $gjwt = WebService::GoogleAPI::JWT - ->new( from_json => '/my/secret.json', - scopes => [ '/my/scope/a', '/my/scope/b' ])->encode; - -=cut - -=head1 ATTRIBUTES - -L inherits all attributes from L and defines the -following new ones. - -=head2 claims - -Overrides the parent class and constructs a hashref representing Google's -required attribution. - - -=head2 client_email - -Get or set the Client ID email address. - -=head2 expires_in - -Defines the threshold for when the token expires. Defaults to 3600. - -=head2 issue_at - -Defines the time of issuance in epoch seconds. If not defined, the claims issue -at date defaults to the time when it is being encoded. - -=head2 scopes - -Get or set the Google scopes. If impersonating, these scopes must be set up by -your Google Business Administrator. - -=head2 target - -Get or set the target. At the time of writing, there is only one valid target: -https://www.googleapis.com/oauth2/v4/token. This is the default value; if you -have no need to customize this, then just fetch the default. - - -=head2 user_as - -Set the Google user to impersonate. Your Google Business Administrator must -have already set up your Client ID as a trusted app in order to use this -successfully. - -=cut - -=head1 METHODS - -Inherits all methods from L and defines the following new ones. - -=head2 from_json - -Loads the JSON file from Google with the client ID information in it and sets -the respective attributes. - -Dies a horrible death on failure: 'Your JSON file import failed.' - - $gjwt->from_json('/my/google/app/project/sa/json/file'); - - -=head1 SEE ALSO - -L - - -=head1 AUTHOR - -Richard Elberger, - -=head1 CONTRIBUTORS - -Scott Wiersdorf, -Avishai Goldman, - -=head1 COPYRIGHT AND LICENSE - -Copyright (C) 2015 by Richard Elberger - -This library is free software; you can redistribute it and/or modify it -under the same terms as Perl itself. - -=cut diff --git a/t/JWT/01_basic.t b/t/JWT/01_basic.t deleted file mode 100644 index 50d0e1f..0000000 --- a/t/JWT/01_basic.t +++ /dev/null @@ -1,115 +0,0 @@ -use strict; -use warnings; -use Test2::V0; -use WebService::GoogleAPI::JWT; - -use Mojo::Collection 'c'; -use File::Basename 'dirname'; -use Cwd 'abs_path'; -#my $grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"; - -my $client_email = 'mysa@developer.gserviceaccount.com'; -my $target = 'https://www.googleapis.com/oauth2/v4/token'; - - -isa_ok my $jwt = WebService::GoogleAPI::JWT->new, ['WebService::GoogleAPI::JWT'], 'empty object creation'; - -# accessors -is $jwt->client_email, undef, 'not init'; -is $jwt->client_email($client_email), $jwt, 'service_account set'; -is $jwt->client_email, $client_email, 'service_account get'; - -is $jwt->scopes, [], 'no scopes'; -is push( @{ $jwt->scopes }, '/a/scope'), 1, 'scopes add one scope'; -is push( @{ $jwt->scopes }, '/b/scope'), 2, 'scopes add another'; -is $jwt->scopes, ['/a/scope','/b/scope'], 'scopes get all'; - -is $jwt->target, 'https://www.googleapis.com/oauth2/v4/token', 'target get'; -is $jwt->target('https://a/new/target'), $jwt, 'target set'; -is $jwt->target, 'https://a/new/target', 'target get'; - -is $jwt->expires_in, 3600, 'expires in one hour by default'; -is $jwt->expires_in(300), $jwt, 'set to 5 minutes'; -is $jwt->expires_in, 300, 'right value'; - -is $jwt->issue_at, undef, 'unset by default'; -is $jwt->issue_at('1429812717'), $jwt, 'issue_at set'; -is $jwt->issue_at, '1429812717', 'issue_at get'; - -# basic claim work -for my $scopes ( c('/a/scope', '/b/scope'), [qw+ /a/scope /b/scope +] ){ - ok $jwt = WebService::GoogleAPI::JWT->new( client_email => $client_email, - issue_at => '1429812717', - scopes => $scopes), 'created a token for ' . ref($scopes) . ' as scopes'; - - is $jwt->claims, { iss => $client_email, - scope => '/a/scope /b/scope', - aud => 'https://www.googleapis.com/oauth2/v4/token', - exp => '1429816317', - iat => '1429812717', - }, 'claims based on accessor settings'; -} - -is $jwt->user_as, undef, 'impersonate user undef by default'; -is $jwt->user_as('riche@cpan.org'), $jwt, 'set user'; -is $jwt->user_as, 'riche@cpan.org', 'get user'; - -is $jwt->claims, { iss => $client_email, - scope => '/a/scope /b/scope', - aud => 'https://www.googleapis.com/oauth2/v4/token', - exp => '1429816317', - iat => '1429812717', - sub => 'riche@cpan.org', - }, 'claims based on accessor settings w impersonate'; - -# interop with Mojo::JWT - -$jwt = WebService::GoogleAPI::JWT->new( client_email => $client_email, - scopes => c('/a/scope', '/b/scope')); - - - -# we must set this -is $jwt->scopes(c->new('/a/scope')), $jwt, 'scopes add one scope'; - -my $claims = $jwt->claims; - -# predefine -$jwt = WebService::GoogleAPI::JWT->new( scopes => c('/scope/a/', '/scope/b/')); - -# predefine w json file -my $testdir = dirname ( abs_path( __FILE__ ) ); - - -like dies { - WebService::GoogleAPI::JWT->new( from_json => "$testdir/load0.json" ), -}, qr/Your JSON file import failed.*/, 'dies on file load failure'; - - -$jwt = WebService::GoogleAPI::JWT->new( from_json => "$testdir/load1.json" ); - -is $jwt->secret, <client_email, '9dvse@developer.gserviceaccount.com', - 'client email matches'; - -is $jwt->from_json, 0, 'requires parameter'; -is $jwt->from_json('/foo/bar/baz/me'), 0, 'file must exist'; -is $jwt->from_json( "$testdir/load3.json" ), 0, 'must have key defined'; -is $jwt->from_json( "$testdir/load4.json" ), 0, 'must be for service account'; - - -$jwt = WebService::GoogleAPI::JWT->new; -is $jwt->client_email('mysa@developer.gserviceaccount.com'), $jwt, 'sa set'; -$jwt->expires('9999999999'); -$jwt->secret('hi'); - -my $jwte = $jwt->encode; -my $jwtd = $jwt->decode($jwte); - -done_testing; diff --git a/t/JWT/load1.json b/t/JWT/load1.json deleted file mode 100644 index 0a22880..0000000 --- a/t/JWT/load1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "private_key_id": "8", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIC\nk8KLWw6r/ERRBg\u003d\u003d\n-----END PRIVATE KEY-----\n", - "client_email": "9dvse@developer.gserviceaccount.com", - "client_id": "ihkkmv89dvse.apps.googleusercontent.com", - "type": "service_account" -} diff --git a/t/JWT/load2.json b/t/JWT/load2.json deleted file mode 100644 index 6b7a9f4..0000000 --- a/t/JWT/load2.json +++ /dev/null @@ -1 +0,0 @@ -not json diff --git a/t/JWT/load3.json b/t/JWT/load3.json deleted file mode 100644 index 04d2769..0000000 --- a/t/JWT/load3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "private_key_id": "8", - "client_email": "9dvse@developer.gserviceaccount.com", - "client_id": "ihkkmv89dvse.apps.googleusercontent.com", - "type": "service_account" -} diff --git a/t/JWT/load4.json b/t/JWT/load4.json deleted file mode 100644 index 965d0bc..0000000 --- a/t/JWT/load4.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "private_key_id": "8", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIC\nk8KLWw6r/ERRBg\u003d\u003d\n-----END PRIVATE KEY-----\n", - "client_email": "9dvse@developer.gserviceaccount.com", - "client_id": "ihkkmv89dvse.apps.googleusercontent.com", - "type": "not_a_service_account" -} From 305b0828e9e5de2fa15e92cdc6c14c3b0c4a0f2d Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 21:14:39 +0200 Subject: [PATCH 25/68] fix up some typos, take notes --- TODO | 71 +++++++++++++++++-- dist.ini | 2 +- .../GoogleAPI/Client/Credentials.pm | 12 ++-- lib/WebService/GoogleAPI/Client/UserAgent.pm | 3 +- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/TODO b/TODO index 180fe14..f9381ec 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,7 @@ +# vim: ft=markdown we're trying to take care of a bunch of things. -#better support for service accounts +# better support for service accounts In order to implement service accounts, we need to have an encapsulated interface for retrieving and refreshing tokens. Then, @@ -14,6 +15,71 @@ qw/ type project_id private_key_id private_key client_email client_id auth_uri token_uri auth_provider_x509_cert_url client_x509_cert_url /. +## targets for fixing + +### in Client::UserAgent + +`refresh_access_token` needs to be controlled by the auth storage, b/c the +process of getting a token differs between normal oauth and service accounts. + +see line 196 of the main module, for example. + yeah, gut that whole method and move it to the storage module in the code + +we must move refresh_access_token from the main module to the auth storage + +subclassing Mojo::UserAgent seems silly to me. This should be a role that gets +composed in. + +### in ::Client::AuthStorage + +the class ::Client::AuthStorage should be able to auto-route to any storage +class. Then it would be more sane to have the values passed to it match a class +name. This makes this part of the system more plugable. It's not like this is +unique to google, right. + this includes making the setup method take more sane parameters also, or at + least be somewhat documented + +ew, the way it's implemented the AuthStorage object holds the actual storage +object. definitiely, definitely should be a role + +we've got some icky long function names to shorten + +one thing to ponder is that AuthStorage should deal only with the storage +(meaning flat-file, cached in memory, in redis, in a database). The TYPE of cred +that we want l'chora should be in ::Credentials + +#### for service accounts in general + +when implementing service accounts, note that they don't need to have a user set + +we would want to cache keys if we can, combined with an expiration time. the +things that would need to be cached are 'user' and 'scopes', b/c i assume you +get a different token depending on what scopes you ask for. + + i guess that means that we'd need to alphabetize the scopes and trim spaces, + so that we can just string compare and skip an extra ask for permissions + + +### in ::Client::Credentials + +we need a way that the main module can pass config information. Make it clear, +at least + +why does this class even exist? + + okay, so i decided that ::Credentials should manage the retrieval and renewal + of credentials, depending on the type. AuthStorage should only store (read + only or read-write). + +why is this a singleton? + + it's a singleton b/c you may want to share credentials between all instances + i guess that's a reasonable default for the module to provide, and let you + override if you need to + +i don't see a reason that this should be implemented seperately from the +storage. + #Other Issues @@ -50,7 +116,4 @@ that we can be sure that when it's live things work. We must fix all the weird warnings in the test suite, and improve our coverage and stuff. -Fix the usage of a gapi.json here. We need to clean up its attempt -to find. - Move everything to Test2. diff --git a/dist.ini b/dist.ini index 389b58f..0d9848e 100755 --- a/dist.ini +++ b/dist.ini @@ -32,7 +32,7 @@ List::Util = 1.45 List::SomeUtils = 0 IO::Socket::SSL = 2.06 Mojo::JWT = 0 -Mojo::JWT::Google = 0.12 +Mojo::JWT::Google = 0.14 Exporter::Shiny = 0 [Prereqs / TestRequires ] diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index 75ff2aa..c85d6ee 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -12,8 +12,7 @@ with 'MooX::Singleton'; has 'access_token' => ( is => 'rw' ); -#TODO- user is a little bit imprecise, b/c a service account -#doesn't necessarily have a user +# TODO - service accounts can impersonate a user, but don't need to has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); # full gmail, like peter@shotgundriver.com has 'auth_storage' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::AuthStorage->new } ); # dont delete to able to configure @@ -38,13 +37,10 @@ sub get_access_token_for_user { sub get_scopes_as_array { my ( $self ) = @_; if ($self->auth_storage->is_set) { - return $self->access_token( $self->auth_storage->get_scopes_from_storage_as_array() ); - } - else - { - croak q/Can't get access token for specified user because storage isn't set/; + $self->auth_storage->get_scopes_from_storage_as_array; + } else { + croak q/Can't get scopes, Storage isn't set/; } - } 1; diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 4e04492..03e1bfd 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -9,7 +9,6 @@ use Moo; extends 'Mojo::UserAgent'; #extends 'Mojo::UserAgent::Mockable'; use WebService::GoogleAPI::Client::Credentials; -use WebService::GoogleAPI::Client::AuthStorage; use Mojo::UserAgent; use Data::Dump qw/pp/; # for dev debug @@ -241,7 +240,7 @@ sub refresh_access_token || ( !defined $credentials->{ refresh_token } ) ) { - croak 'If you credentials are missing the refresh_token - consider removing the auth at ' + croak 'If your credentials are missing the refresh_token - consider removing the auth at ' . 'https://myaccount.google.com/permissions as The oauth2 server will only ever mint one refresh ' . 'token at a time, and if you request another access token via the flow it will operate as if ' . 'you only asked for an access token.' From ab8b88c3bcaa0865bc0cdea10818986e7b6e0bb8 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 22:16:17 +0200 Subject: [PATCH 26/68] begin sketching out service accounts from user's perspective - including POD --- lib/WebService/GoogleAPI/Client.pm | 70 ++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 1e580a5..a227348 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -144,31 +144,73 @@ has 'discovery' => ( =head2 C - WebService::GoogleAPI::Client->new( user => 'peter@pscott.com.au', gapi_json => '/fullpath/gapi.json' ); + WebService::GoogleAPI::Client->new( + user => 'peter@pscott.com.au', gapi_json => '/fullpath/gapi.json' ); -=head3 PARAMETERS +=head3 General parameters -=head4 user :: the email address that identifies key of credentials in the config file +=over -=head4 gapi_json :: Location of the configuration credentials - default gapi.json +debug - if truthy then diagnostics are send to STDERR - default false. Crank it +up to 11 for maximal debug output -=head4 debug :: if '1' then diagnostics are send to STDERR - default false +chi - an instance to a CHI persistent storage case object - if none provided FILE is used -=head4 chi :: an instance to a CHI persistent storage case object - if none provided FILE is used +=back + +=head3 Login Parameters + +You can use either gapi_json, which is the file you get from using the bundled +goauth tool, or service_account which is the json file you can download from +https://console.cloud.google.com/iam-admin/serviceaccounts. + +If nothing is passed, then we check the GOOGLE_APPLICATION_CREDENTIALS env +variable for the location of a service account file. This matches the +functionality of the Google Cloud libraries from other languages (well, +somewhat. I haven't fully implemented ADC yet - see +https://cloud.google.com/docs/authentication/production for some details. PRs +are welcome!) + +If that doesn't exist, then we default to gapi.json in the current directory. + +service_account and gapi_json are mutually exclusive, and gapi_json takes precedence. + +=over +user - the email address that requests will be made for + +gapi_json - Location of end user credentials + +service_account - Location of service account credentials + +=back + +If you're using a service account, user represents the user that you're +impersonating. Make sure you have domain-wide delegation set up, or else this +won't work. =cut -sub BUILD -{ - my ( $self, $params ) = @_; +sub BUILD { + my ($self, $params) = @_; + + if (defined $params->{gapi_json}) { + $self->auth_storage->setup( + { type => 'jsonfile', path => $params->{gapi_json} }); + } elsif (defined $params->{service_account}) { + $self->auth_storage->setup( + { type => 'jsonfile', path => $params->{service_account} }); + } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { + $self->auth_storage->setup({ type => 'jsonfile', path => $file }); + } - #TODO- implement google standard way of finding the credentials - $self->auth_storage->setup( { type => 'jsonfile', path => $params->{ gapi_json } } ) if ( defined $params->{ gapi_json } ); - $self->user( $params->{ user } ) if ( defined $params->{ user } ); +#NOTE- in terms of implementing google's ADC +# (see https://cloud.google.com/docs/authentication/production and also +# https://github.com/googleapis/python-cloud-core/blob/master/google/cloud/client.py) +# I looked into it and based on that, it seems that every environment has different reqs, +# so it's a maintenance liability + $self->user($params->{user}) if (defined $params->{user}); - ## how to handle chi as a parameter - $self->discovery->chi( $self->chi ); ## is this redundant? set in default? ## TODO - think about consequences of user not providing auth storage or user on instantiaton } From a4957a7aab2184b49c224c6e7fd842ace3b5ec7c Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 22:49:11 +0200 Subject: [PATCH 27/68] continue setting up the chain to load service account storage --- lib/WebService/GoogleAPI/Client.pm | 42 ++++++---- .../GoogleAPI/Client/AuthStorage.pm | 52 ++++++------- .../Client/AuthStorage/ConfigJSON.pm | 76 +++++++++---------- .../GoogleAPI/Client/Credentials.pm | 5 +- 4 files changed, 93 insertions(+), 82 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index a227348..345eed4 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -147,41 +147,55 @@ has 'discovery' => ( WebService::GoogleAPI::Client->new( user => 'peter@pscott.com.au', gapi_json => '/fullpath/gapi.json' ); -=head3 General parameters +=head3 B -=over +=over 4 -debug - if truthy then diagnostics are send to STDERR - default false. Crank it -up to 11 for maximal debug output +=item debug -chi - an instance to a CHI persistent storage case object - if none provided FILE is used +if truthy then diagnostics are send to STDERR - default false. Crank it up to 11 +for maximal debug output + +=item chi + +an instance to a CHI persistent storage case object - if none provided FILE is used =back -=head3 Login Parameters +=head3 B You can use either gapi_json, which is the file you get from using the bundled goauth tool, or service_account which is the json file you can download from https://console.cloud.google.com/iam-admin/serviceaccounts. +service_account and gapi_json are mutually exclusive, and gapi_json takes precedence. + If nothing is passed, then we check the GOOGLE_APPLICATION_CREDENTIALS env variable for the location of a service account file. This matches the functionality of the Google Cloud libraries from other languages (well, somewhat. I haven't fully implemented ADC yet - see -https://cloud.google.com/docs/authentication/production for some details. PRs +L for some details. PRs are welcome!) + If that doesn't exist, then we default to gapi.json in the current directory. -service_account and gapi_json are mutually exclusive, and gapi_json takes precedence. +B This default is subject to change as more storage backends are implemented. +A deprecation warning will be emmitted when this is likely to start happening. + +=over 4 + +=item user + +the email address that requests will be made for -=over +=item gapi_json -user - the email address that requests will be made for +Location of end user credentials -gapi_json - Location of end user credentials +=item service_account -service_account - Location of service account credentials +Location of service account credentials =back @@ -199,9 +213,9 @@ sub BUILD { { type => 'jsonfile', path => $params->{gapi_json} }); } elsif (defined $params->{service_account}) { $self->auth_storage->setup( - { type => 'jsonfile', path => $params->{service_account} }); + { type => 'servicefile', path => $params->{service_account} }); } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { - $self->auth_storage->setup({ type => 'jsonfile', path => $file }); + $self->auth_storage->setup({ type => 'servicefile', path => $file }); } #NOTE- in terms of implementing google's ADC diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 53ac879..0a5ed9d 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -14,7 +14,9 @@ use Carp; use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; -has 'storage' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new } ); # by default +has 'storage' => + is => 'rw', + default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new } ); # by default has 'is_set' => ( is => 'rw', default => 0 ); @@ -29,17 +31,18 @@ Set appropriate storage =cut -sub setup -{ - my ( $self, $params ) = @_; - if ( $params->{ type } eq 'jsonfile' ) - { - $self->storage->path( $params->{ path } ); +sub setup { + my ($self, $params) = @_; + if ($params->{type} eq 'jsonfile') { + $self->storage->path($params->{path}); $self->storage->setup; - $self->is_set( 1 ); - } - else - { + $self->is_set(1); + } elsif ($params->{type} eq 'servicefile') { + $self->storage( + WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(path => $path); + ); + $self->is_set(1); + } else { croak "Unknown storage type."; } return $self; @@ -57,28 +60,23 @@ This method must have all subclasses of WebService::GoogleAPI::Client::AuthStora =cut -sub get_credentials_for_refresh -{ - my ( $self, $user ) = @_; - return $self->storage->get_credentials_for_refresh( $user ); +sub get_credentials_for_refresh { + my ($self, $user) = @_; + return $self->storage->get_credentials_for_refresh($user); } -sub get_access_token_from_storage -{ - my ( $self, $user ) = @_; - return $self->storage->get_access_token_from_storage( $user ); +sub get_access_token_from_storage { + my ($self, $user) = @_; + return $self->storage->get_access_token_from_storage($user); } -sub set_access_token_to_storage -{ - my ( $self, $user, $access_token ) = @_; - return $self->storage->set_access_token_to_storage( $user, $access_token ); +sub set_access_token_to_storage { + my ($self, $user, $access_token) = @_; + return $self->storage->set_access_token_to_storage($user, $access_token); } - -sub get_scopes_from_storage_as_array -{ - my ( $self ) = @_; +sub get_scopes_from_storage_as_array { + my ($self) = @_; return $self->storage->get_scopes_from_storage_as_array(); } diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index e1afb1a..847a4f0 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -10,75 +10,71 @@ use Carp; has 'path' => ( is => 'rw', default => 'gapi.json' ); # default is gapi.json -# has 'tokensfile'; # Config::JSON object pointer -my $tokensfile; +has 'tokensfile' => ( is => 'rw' ); # Config::JSON object pointer has 'debug' => ( is => 'rw', default => 0 ); -## cringe .. getters and setters, tokenfile?, global $tokensfile? .. *sigh* +# NOTE- this type of class has getters and setters b/c the implementation of +# getting and setting depends on what's storing -sub setup -{ - my ( $self ) = @_; - $tokensfile = Config::JSON->new( $self->path ); +sub setup { + my ($self) = @_; + $self->tokensfile(Config::JSON->new($self->path)); return $self; } -sub get_credentials_for_refresh -{ - my ( $self, $user ) = @_; +sub get_credentials_for_refresh { + my ($self, $user) = @_; return { client_id => $self->get_client_id_from_storage(), client_secret => $self->get_client_secret_from_storage(), - refresh_token => $self->get_refresh_token_from_storage( $user ) + refresh_token => $self->get_refresh_token_from_storage($user) }; } -sub get_token_emails_from_storage -{ - my $tokens = $tokensfile->get( 'gapi/tokens' ); +sub get_token_emails_from_storage { + my ($self) = @_; + my $tokens = $self->tokensfile->get('gapi/tokens'); return [keys %$tokens]; } -sub get_client_id_from_storage -{ - return $tokensfile->get( 'gapi/client_id' ); +sub get_client_id_from_storage { + my ($self) = @_; + return $self->tokensfile->get('gapi/client_id'); } -sub get_client_secret_from_storage -{ - return $tokensfile->get( 'gapi/client_secret' ); +sub get_client_secret_from_storage { + my ($self) = @_; + return $self->tokensfile->get('gapi/client_secret'); } -sub get_refresh_token_from_storage -{ - my ( $self, $user ) = @_; +sub get_refresh_token_from_storage { + my ($self, $user) = @_; carp "get_refresh_token_from_storage(" . $user . ")" if $self->debug; - return $tokensfile->get( 'gapi/tokens/' . $user . '/refresh_token' ); + return $self->tokensfile->get('gapi/tokens/' . $user . '/refresh_token'); } -sub get_access_token_from_storage -{ - my ( $self, $user ) = @_; - return $tokensfile->get( 'gapi/tokens/' . $user . '/access_token' ); +sub get_access_token_from_storage { + my ($self, $user) = @_; + return $self->tokensfile->get('gapi/tokens/' . $user . '/access_token'); } -sub set_access_token_to_storage -{ - my ( $self, $user, $token ) = @_; - return $tokensfile->set( 'gapi/tokens/' . $user . '/access_token', $token ); +sub set_access_token_to_storage { + my ($self, $user, $token) = @_; + return $self->tokensfile->set('gapi/tokens/' . $user . '/access_token', + $token); } -sub get_scopes_from_storage -{ - my ( $self ) = @_; - return $tokensfile->get( 'gapi/scopes' ); ## NB - returns an array - is stored as space sep list +sub get_scopes_from_storage { + my ($self) = @_; + return $self->tokensfile->get('gapi/scopes') + ; ## NB - returns an array - is stored as space sep list } -sub get_scopes_from_storage_as_array -{ - my ( $self ) = @_; - return [split( ' ', $tokensfile->get( 'gapi/scopes' ) )]; ## NB - returns an array - is stored as space sep list +sub get_scopes_from_storage_as_array { + my ($self) = @_; + return [split / /, $self->tokensfile->get('gapi/scopes')] + ; ## NB - returns an array - is stored as space sep list } 1; diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index c85d6ee..47236c2 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -8,13 +8,16 @@ package WebService::GoogleAPI::Client::Credentials; use Carp; use Moo; +use WebService::GoogleAPI::Client::AuthStorage; with 'MooX::Singleton'; has 'access_token' => ( is => 'rw' ); # TODO - service accounts can impersonate a user, but don't need to has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); # full gmail, like peter@shotgundriver.com -has 'auth_storage' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::AuthStorage->new } ); # dont delete to able to configure +has 'auth_storage' => + is => 'rw', + default => sub { WebService::GoogleAPI::Client::AuthStorage->new }; # dont delete to able to configure =method get_access_token_for_user From ad7e854df936befa6d0a5fb33dc8c30a26b2452d Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 22:51:49 +0200 Subject: [PATCH 28/68] cosmetics --- lib/WebService/GoogleAPI/Client.pm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 345eed4..5ae9b83 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -177,8 +177,7 @@ somewhat. I haven't fully implemented ADC yet - see L for some details. PRs are welcome!) - -If that doesn't exist, then we default to gapi.json in the current directory. +If that doesn't exist, then we default to F in the current directory. B This default is subject to change as more storage backends are implemented. A deprecation warning will be emmitted when this is likely to start happening. From f5e8b2ab9bd8dd32c92871f8e205f5a31aac7dc6 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 23:34:31 +0200 Subject: [PATCH 29/68] spellcheck goauth, not go_auth --- README.md | 2 +- README.txt | 2 +- bin/goauth | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df612c0..4b90567 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ See the examples folder for specific access examples. - API Discovery with local caching using [CHI](https://metacpan.org/pod/CHI) File - OAUTH app credentials (client\_id, client\_secret, users access\_token && refresh\_token) storage stored in local gapi.json file - Automatic access\_token refresh (if user has refresh\_token) and saving refreshed token to storage -- CLI tool ([go_auth](https://metacpan.org/pod/distribution/WebService-GoogleAPI-Client/bin/goauth)) with lightweight HTTP server to simplify config OAuth2 configuration, sccoping, authorization and obtaining access\_ and refresh\_ tokens +- CLI tool ([goauth](https://metacpan.org/pod/distribution/WebService-GoogleAPI-Client/bin/goauth)) with lightweight HTTP server to simplify config OAuth2 configuration, sccoping, authorization and obtaining access\_ and refresh\_ tokens # SEE ALSO diff --git a/README.txt b/README.txt index 9cd80ed..a631c5e 100644 --- a/README.txt +++ b/README.txt @@ -30,7 +30,7 @@ SYNOPSIS Internal User Agent provided be property WebService::GoogleAPI::Client::UserAgent dervied from Mojo::UserAgent - Package includes go_auth CLI Script to collect initial end-user + Package includes goauth CLI Script to collect initial end-user authorisation to scoped services EXAMPLES diff --git a/bin/goauth b/bin/goauth index abab602..1c90958 100755 --- a/bin/goauth +++ b/bin/goauth @@ -42,14 +42,14 @@ Once installed as part of the WebService::GoogleAPI::Client bundle, this tool ca configuration that allows authenticated and authorised users to grant permission to access Google API services on their data (Email, Files etc) to the extent provided by the Access Scopes for the project and the auth tokens. -In order to successfully run _go_auth_ for the first time requires the following Google Project configuration variables: +In order to successfully run _goauth_ for the first time requires the following Google Project configuration variables: Client ID Client Secret List of Scopes to request access to on behalf of users ( must be a subset of those enabled for the project. ) If not already available in the gapi.json file, you will be prompted to supply these as the file is created. -Once this configuration is complete, the go_auth tool will launch a mini HTTP server to provide an interface to request users to authorise your project application. +Once this configuration is complete, the goauth tool will launch a mini HTTP server to provide an interface to request users to authorise your project application. NB: To run successfully your Google Project must be configured to accept requests from the domain. For initial testing ensure that you include localhost in your allowed domains. From 720071f60f0fdf076e636bedd1689fbdcc58eece Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 23:34:51 +0200 Subject: [PATCH 30/68] start making ::Client::AuthStorage a role --- lib/WebService/GoogleAPI/Client.pm | 57 +++++++++++-------- .../GoogleAPI/Client/AuthStorage.pm | 35 +++--------- .../Client/AuthStorage/ConfigJSON.pm | 17 ++++-- .../GoogleAPI/Client/Credentials.pm | 13 +++-- 4 files changed, 58 insertions(+), 64 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 5ae9b83..1a51d22 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -7,6 +7,8 @@ use Data::Dump qw/pp/; use Moo; use WebService::GoogleAPI::Client::UserAgent; use WebService::GoogleAPI::Client::Discovery; +use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; +use WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; use Carp; use CHI; use Mojo::Util; @@ -55,7 +57,7 @@ credentials and user authorization created by _goauth_ Internal User Agent provided be property WebService::GoogleAPI::Client::UserAgent dervied from Mojo::UserAgent -Package includes I CLI Script to collect initial end-user authorisation to scoped services +Package includes I CLI Script to collect initial end-user authorisation to scoped services =head1 EXAMPLES @@ -66,18 +68,21 @@ Package includes I CLI Script to collect initial end-user authorisation use MIME::Base64; my $my_email_address = 'peter@shotgundriver.com' + my $raw_email_payload = encode_base64( + Email::Simple->create( + header => [ + To => $my_email_address, + From => $my_email_address, + Subject => "Test email from '$my_email_address' ", + ], + body => "This is the body of email to '$my_email_address'", + )->as_string + ); - my $raw_email_payload = encode_base64( Email::Simple->create( - header => [To => $my_email_address, From => $my_email_address, - Subject =>"Test email from '$my_email_address' ",], - body => "This is the body of email to '$my_email_address'", - )->as_string - ); - - $gapi_client->api_query( - api_endpoint_id => 'gmail.users.messages.send', - options => { raw => $raw_email_payload }, - ); + $gapi_client->api_query( + api_endpoint_id => 'gmail.users.messages.send', + options => { raw => $raw_email_payload }, + ); =head2 MANUAL API REQUEST CONSTRUCTION - GET CALENDAR LIST @@ -204,27 +209,31 @@ won't work. =cut +#NOTE- in terms of implementing google's ADC +# (see https://cloud.google.com/docs/authentication/production and also +# https://github.com/googleapis/python-cloud-core/blob/master/google/cloud/client.py) +# I looked into it and based on that, it seems that every environment has different reqs, +# so it's a maintenance liability sub BUILD { my ($self, $params) = @_; if (defined $params->{gapi_json}) { - $self->auth_storage->setup( - { type => 'jsonfile', path => $params->{gapi_json} }); + $self->auth_storage( + WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new(path => $params->{gapi_json}) + ); } elsif (defined $params->{service_account}) { - $self->auth_storage->setup( - { type => 'servicefile', path => $params->{service_account} }); + $self->auth_storage( + WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + path => $params->{service_account}) + ); } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { - $self->auth_storage->setup({ type => 'servicefile', path => $file }); + $self->auth_storage( + WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + path => $file) + ); } -#NOTE- in terms of implementing google's ADC -# (see https://cloud.google.com/docs/authentication/production and also -# https://github.com/googleapis/python-cloud-core/blob/master/google/cloud/client.py) -# I looked into it and based on that, it seems that every environment has different reqs, -# so it's a maintenance liability $self->user($params->{user}) if (defined $params->{user}); - - ## TODO - think about consequences of user not providing auth storage or user on instantiaton } =head2 C diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 0a5ed9d..9bb6943 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -9,16 +9,15 @@ package WebService::GoogleAPI::Client::AuthStorage; ## or is UserAgent->credentials -use Moo; +use Moo::Role; use Carp; -use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; - - -has 'storage' => - is => 'rw', - default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new } ); # by default -has 'is_set' => ( is => 'rw', default => 0 ); +requires qw/ + refresh_token + get_access_token_from_storage + set_access_token_to_storage + scopes +/; =method setup @@ -60,25 +59,5 @@ This method must have all subclasses of WebService::GoogleAPI::Client::AuthStora =cut -sub get_credentials_for_refresh { - my ($self, $user) = @_; - return $self->storage->get_credentials_for_refresh($user); -} - -sub get_access_token_from_storage { - my ($self, $user) = @_; - return $self->storage->get_access_token_from_storage($user); -} - -sub set_access_token_to_storage { - my ($self, $user, $access_token) = @_; - return $self->storage->set_access_token_to_storage($user, $access_token); -} - -sub get_scopes_from_storage_as_array { - my ($self) = @_; - return $self->storage->get_scopes_from_storage_as_array(); -} - 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 847a4f0..23598c4 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -8,6 +8,8 @@ use Moo; use Config::JSON; use Carp; +with 'WebService::GoogleAPI::Client::AuthStorage'; + has 'path' => ( is => 'rw', default => 'gapi.json' ); # default is gapi.json has 'tokensfile' => ( is => 'rw' ); # Config::JSON object pointer @@ -16,7 +18,7 @@ has 'debug' => ( is => 'rw', default => 0 ); # NOTE- this type of class has getters and setters b/c the implementation of # getting and setting depends on what's storing -sub setup { +sub BUILD { my ($self) = @_; $self->tokensfile(Config::JSON->new($self->path)); return $self; @@ -67,14 +69,17 @@ sub set_access_token_to_storage { sub get_scopes_from_storage { my ($self) = @_; - return $self->tokensfile->get('gapi/scopes') - ; ## NB - returns an array - is stored as space sep list + return $self->tokensfile->get('gapi/scopes'); } sub get_scopes_from_storage_as_array { - my ($self) = @_; - return [split / /, $self->tokensfile->get('gapi/scopes')] - ; ## NB - returns an array - is stored as space sep list + carp 'get_scopes_from_storage_as_array is being deprecated, please use the more succint scopes accessor'; + return $_[0]->scopes } +# NOTE - the scopes are stored as a space seperated list +sub scopes { + my ($self) = @_; + return [split / /, $self->tokensfile->get('gapi/scopes')]; +} 1; diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index 47236c2..ec4f84f 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -2,22 +2,23 @@ use strictures; package WebService::GoogleAPI::Client::Credentials; -# ABSTRACT: Credentials for particular Client instance. You can use this module as singleton also if you need to share -# credentials between two or more modules +# ABSTRACT: Credentials for particular Client instance. You can use this module +# as singleton also if you need to share credentials between two or more +# instances use Carp; use Moo; -use WebService::GoogleAPI::Client::AuthStorage; +use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; with 'MooX::Singleton'; has 'access_token' => ( is => 'rw' ); -# TODO - service accounts can impersonate a user, but don't need to -has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); # full gmail, like peter@shotgundriver.com +has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); has 'auth_storage' => is => 'rw', - default => sub { WebService::GoogleAPI::Client::AuthStorage->new }; # dont delete to able to configure + default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new }, + lazy => 1; =method get_access_token_for_user From 95fb41c95f372d198fbc12ed6dba1a170335727a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 23:36:38 +0200 Subject: [PATCH 31/68] make all uses of auth_storage->storage skip the indirection --- README.md | 2 +- docs/myapp.pl | 2 +- examples/cloud_dns.pl | 2 +- examples/cloudstorage_bucket_example.pl | 2 +- examples/dev_calendar_example.pl | 2 +- examples/dev_generate_full_json_method_tree_struct.pl | 2 +- examples/dev_mybusiness_example.pl | 2 +- examples/dev_sheets_example.pl | 2 +- examples/drive_example.pl | 2 +- examples/geocoding_api.pl | 2 +- examples/gmail_example.pl | 2 +- examples/people_api.pl | 2 +- examples/text_to_speech.pl | 2 +- examples/translation_example.pl | 2 +- t/WebService/GoogleAPI/Client/Discovery.t | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4b90567..5526a79 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ I was unable to get the [Dist::Zilla](https://metacpan.org/pod/Dist::Zilla) pack my $gapi = WebService::GoogleAPI::Client->new(debug => 0); ## This idiom selects the first authorised user from gapi.json - my $aref_token_emails = $gapi->auth_storage->storage->get_token_emails_from_storage; + my $aref_token_emails = $gapi->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; print "Running tests with default user email = $user\n"; $gapi->user($user); diff --git a/docs/myapp.pl b/docs/myapp.pl index a9305d3..03341cd 100755 --- a/docs/myapp.pl +++ b/docs/myapp.pl @@ -24,7 +24,7 @@ my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); ## , -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; #my $user = $aref_token_emails->[0]; ## default to the first user #$gapi_client->user( $user ); diff --git a/examples/cloud_dns.pl b/examples/cloud_dns.pl index ece62c8..b5c09c0 100644 --- a/examples/cloud_dns.pl +++ b/examples/cloud_dns.pl @@ -107,7 +107,7 @@ =head2 GOOGLE API LINKS ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/examples/cloudstorage_bucket_example.pl b/examples/cloudstorage_bucket_example.pl index 1d39eb3..b1a632d 100755 --- a/examples/cloudstorage_bucket_example.pl +++ b/examples/cloudstorage_bucket_example.pl @@ -110,7 +110,7 @@ =head2 GOOGLE API LINKS ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/examples/dev_calendar_example.pl b/examples/dev_calendar_example.pl index 844c173..9ecf707 100644 --- a/examples/dev_calendar_example.pl +++ b/examples/dev_calendar_example.pl @@ -85,7 +85,7 @@ =head2 GOALS else { croak( 'I only work if gapi.json is here' ); } ; ## prolly better to fail on setup ? my $gapi_agent = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => './gapi.json' ); -my $aref_token_emails = $gapi_agent->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_agent->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_agent->user( $user ); diff --git a/examples/dev_generate_full_json_method_tree_struct.pl b/examples/dev_generate_full_json_method_tree_struct.pl index 08776e5..29f31c5 100644 --- a/examples/dev_generate_full_json_method_tree_struct.pl +++ b/examples/dev_generate_full_json_method_tree_struct.pl @@ -25,7 +25,7 @@ ; ## prolly better to fail on setup ? my $gapi_agent = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => './gapi.json' ); -my $aref_token_emails = $gapi_agent->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_agent->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_agent->user( $user ); diff --git a/examples/dev_mybusiness_example.pl b/examples/dev_mybusiness_example.pl index 25d0d89..3cd40f3 100644 --- a/examples/dev_mybusiness_example.pl +++ b/examples/dev_mybusiness_example.pl @@ -102,7 +102,7 @@ =head2 GOALS else { croak( 'I only work if gapi.json is here' ); } ; ## prolly better to fail on setup ? my $gapi_agent = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => './gapi.json' ); -my $aref_token_emails = $gapi_agent->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_agent->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_agent->user( $user ); diff --git a/examples/dev_sheets_example.pl b/examples/dev_sheets_example.pl index 6871643..8b78cf8 100644 --- a/examples/dev_sheets_example.pl +++ b/examples/dev_sheets_example.pl @@ -132,7 +132,7 @@ =head2 LIST ALL SHEETS else { croak( 'I only work if gapi.json is here' ); } ; ## prolly better to fail on setup ? my $gapi_agent = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_agent->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_agent->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_agent->user( $user ); diff --git a/examples/drive_example.pl b/examples/drive_example.pl index fe0aee0..fd4f2c8 100644 --- a/examples/drive_example.pl +++ b/examples/drive_example.pl @@ -93,7 +93,7 @@ =head2 GOOGLE API LINKS ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/examples/geocoding_api.pl b/examples/geocoding_api.pl index 9fd6ab2..a10c63d 100755 --- a/examples/geocoding_api.pl +++ b/examples/geocoding_api.pl @@ -74,7 +74,7 @@ =head2 RELEVANT GOOGLE API LINKS ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); say 'x' x 180; diff --git a/examples/gmail_example.pl b/examples/gmail_example.pl index 776df7c..26f8b7b 100755 --- a/examples/gmail_example.pl +++ b/examples/gmail_example.pl @@ -103,7 +103,7 @@ =head2 send else { croak( 'I only work if gapi.json is here' ); } ; ## prolly better to fail on setup ? my $gapi_agent = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => './gapi.json' ); -my $aref_token_emails = $gapi_agent->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_agent->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_agent->user( $user ); diff --git a/examples/people_api.pl b/examples/people_api.pl index 87b381d..3d1e0d1 100755 --- a/examples/people_api.pl +++ b/examples/people_api.pl @@ -81,7 +81,7 @@ =head2 GOOGLE API LINKS ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manually sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => $config->{debug}, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/examples/text_to_speech.pl b/examples/text_to_speech.pl index ca70c7f..9708cf2 100755 --- a/examples/text_to_speech.pl +++ b/examples/text_to_speech.pl @@ -51,7 +51,7 @@ =head2 TODO ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => 0, gapi_json => 'gapi.json' ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/examples/translation_example.pl b/examples/translation_example.pl index 3d4ba38..8448f95 100755 --- a/examples/translation_example.pl +++ b/examples/translation_example.pl @@ -98,7 +98,7 @@ =head2 TODO ## assumes gapi.json configuration in working directory with scoped project and user authorization ## manunally sets the client user email to be the first in the gapi.json file my $gapi_client = WebService::GoogleAPI::Client->new( debug => 0, gapi_json => 'gapi.json', debug=>0 ); -my $aref_token_emails = $gapi_client->auth_storage->storage->get_token_emails_from_storage; +my $aref_token_emails = $gapi_client->auth_storage->get_token_emails_from_storage; my $user = $aref_token_emails->[0]; ## default to the first user $gapi_client->user( $user ); diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index e4d9405..3ddf34e 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -377,7 +377,7 @@ subtest 'Discovery methods with User Configuration' => sub { #ok( ref $gapi->auth_storage->setup( { type => 'jsonfile', path => $default_file } ) eq 'WebService::GoogleAPI::Client::AuthStorage', 'auth_storage returns WebService::GoogleAPI::Client::AuthStorage'); ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); - ok( my $aref_token_emails = $gapi->auth_storage->storage->get_token_emails_from_storage, 'Load token emails from config' ); + ok( my $aref_token_emails = $gapi->auth_storage->get_token_emails_from_storage, 'Load token emails from config' ); if ( !$user ) ## default to the first user in config file if none defined yet { ok( $user = $aref_token_emails->[0], "setting test user to first configured entry in config - '$user'" ); From 73ad26608753c37a0d6addf50c0d6f1293fc37c8 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 21 Jan 2021 23:39:10 +0200 Subject: [PATCH 32/68] remove unnecessary 'is_set' attr from AuthStorage classes --- lib/WebService/GoogleAPI/Client/AuthStorage.pm | 2 +- .../GoogleAPI/Client/AuthStorage/ConfigJSON.pm | 1 + lib/WebService/GoogleAPI/Client/Credentials.pm | 14 ++------------ 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 9bb6943..60617c2 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -38,7 +38,7 @@ sub setup { $self->is_set(1); } elsif ($params->{type} eq 'servicefile') { $self->storage( - WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(path => $path); + WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(path => $params->{path}) ); $self->is_set(1); } else { diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 23598c4..13320e3 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -24,6 +24,7 @@ sub BUILD { return $self; } +sub refresh_token { ... } # TODO sub get_credentials_for_refresh { my ($self, $user) = @_; return { diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index ec4f84f..fe52053 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -28,23 +28,13 @@ Automatically get access_token for current user if auth_storage is set sub get_access_token_for_user { my ($self) = @_; - if ($self->auth_storage->is_set) { - # check that auth_storage initialized fine - $self->access_token( - $self->auth_storage->get_access_token_from_storage($self->user)); - } else { - croak q/Can't get access token, Storage isn't set/; - } + $self->access_token($self->auth_storage->get_access_token_from_storage($self->user)); return $self; } sub get_scopes_as_array { my ( $self ) = @_; - if ($self->auth_storage->is_set) { - $self->auth_storage->get_scopes_from_storage_as_array; - } else { - croak q/Can't get scopes, Storage isn't set/; - } + $self->auth_storage->get_scopes_from_storage_as_array; } 1; From 5b783f3f053af7a564dee653fc233166b12e9386 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Fri, 22 Jan 2021 10:48:55 +0200 Subject: [PATCH 33/68] rename scopes throughout the code --- examples/discovery_example.pl | 2 +- lib/WebService/GoogleAPI/Client.pm | 4 +- .../GoogleAPI/Client/AuthStorage.pm | 47 +------------------ .../GoogleAPI/Client/Credentials.pm | 4 +- lib/WebService/GoogleAPI/Client/UserAgent.pm | 2 +- t/WebService/GoogleAPI/Client/Discovery.t | 2 +- 6 files changed, 8 insertions(+), 53 deletions(-) diff --git a/examples/discovery_example.pl b/examples/discovery_example.pl index 63e5140..efcf94c 100755 --- a/examples/discovery_example.pl +++ b/examples/discovery_example.pl @@ -182,7 +182,7 @@ =head2 ABSTRACT #print Dumper $foo;exit; ## list all user scopes configured - my $configured_scopes = $gapi_agent->get_scopes_as_array(); ## TODO rename to array_ref + my $configured_scopes = $gapi_agent->scopes; say "Scopes currently configured are: \n * " . join( "\n * ", @$configured_scopes ); ## transform list into a hash so can do instant lookups diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 1a51d22..e4d950b 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -103,7 +103,7 @@ has 'debug' => ( ); has 'ua' => ( handles => - [qw/access_token auth_storage do_autorefresh get_scopes_as_array user /], + [qw/access_token auth_storage do_autorefresh scopes user /], is => 'ro', default => sub { WebService::GoogleAPI::Client::UserAgent->new(debug => shift->debug) } @@ -629,7 +629,7 @@ sub has_scope_to_access_api_endpoint if ( keys( %$method_spec ) > 0 ) ## empty hash indicates failure { - my $configured_scopes = $self->ua->get_scopes_as_array(); ## get user scopes arrayref + my $configured_scopes = $self->scopes; ## get user scopes arrayref ## create a hashindex to facilitate quick lookups my %configured_scopes_hash = map { s/\/$//xr, 1 } @$configured_scopes; ## NB r switch as per https://www.perlmonks.org/?node_id=613280 to filter out any trailing '/' my $granted = 0; ## assume permission not granted until we find a matching scope diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 60617c2..e4afed9 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -10,54 +10,9 @@ package WebService::GoogleAPI::Client::AuthStorage; ## or is UserAgent->credentials use Moo::Role; -use Carp; -requires qw/ - refresh_token - get_access_token_from_storage - set_access_token_to_storage - scopes -/; +requires qw/refresh_token get_access_token_from_storage set_access_token_to_storage scopes/; -=method setup - -Set appropriate storage - - my $auth_storage = WebService::GoogleAPI::Client::AuthStorage->new; - $auth_storage->setup; # by default will be config.json - $auth_storage->setup({type => 'jsonfile', path => '/abs_path' }); - - -=cut - -sub setup { - my ($self, $params) = @_; - if ($params->{type} eq 'jsonfile') { - $self->storage->path($params->{path}); - $self->storage->setup; - $self->is_set(1); - } elsif ($params->{type} eq 'servicefile') { - $self->storage( - WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new(path => $params->{path}) - ); - $self->is_set(1); - } else { - croak "Unknown storage type."; - } - return $self; -} - -### Below are list of methods that each Storage subclass must provide - -=method get_credentials_for_refresh - -Return all parameters that is needed for Mojo::Google::AutoTokenRefresh::refresh_access_token() function: client_id, client_secret and refresh_token - -$c->get_credentials_for_refresh('examplemail@gmail.com') - -This method must have all subclasses of WebService::GoogleAPI::Client::AuthStorage - -=cut 1; diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm index fe52053..873a51f 100644 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ b/lib/WebService/GoogleAPI/Client/Credentials.pm @@ -32,9 +32,9 @@ sub get_access_token_for_user { return $self; } -sub get_scopes_as_array { +sub scopes { my ( $self ) = @_; - $self->auth_storage->get_scopes_from_storage_as_array; + $self->auth_storage->scopes; } 1; diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 03e1bfd..ade1cac 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -20,7 +20,7 @@ has 'debug' => ( is => 'rw', default => 0 ); has 'credentials' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::Credentials->instance }, - handles => [qw/access_token auth_storage get_scopes_as_array user /], + handles => [qw/access_token auth_storage scopes user/], lazy => 1 ); diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index 3ddf34e..b69ae03 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -445,7 +445,7 @@ subtest 'Discovery methods with User Configuration' => sub { ok( keys %{ $gapi->extract_method_discovery_detail_from_api_spec( 'not-a-google-service.list' ) } == 0, "WebService::GoogleAPI::Client->extract_method_discovery_detail_from_api_spec('not-a-google-service.list') returns empty hashref" ); - ok( scalar( [$gapi->get_scopes_as_array()] ) > 0, 'more than 0 scopes returned from config' ); + ok @$gapi->scopes + 0, 'more than 0 scopes returned from config'; ok( $gapi->has_scope_to_access_api_endpoint( "gmail.users.messages.send" ) =~ /^0|1$/xmg, 'has_scope_to_access_api_endpoint("gmail.users.messages.send") returns either 0 or 1' From 11811d22f76e8a036d5786e8ae905edcdd29076a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Fri, 22 Jan 2021 10:50:20 +0200 Subject: [PATCH 34/68] put in filler deprecation code --- lib/WebService/GoogleAPI/Client.pm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index e4d950b..12a7fcd 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -111,6 +111,11 @@ has 'ua' => ( lazy => 1, ); +sub get_scopes_as_array { + carp 'get_scopes_as_array has deprecated in favor of the shorter "scopes"'; + return $_[0]->scopes +} + has 'chi' => ( is => 'rw', default => sub { From b9dffecdcaf2f1a12aaa86de743b03a4a59af32c Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Fri, 22 Jan 2021 13:50:16 +0200 Subject: [PATCH 35/68] move functionality of ::Credentials to the AuthStorage role - adjust for its absence in the rest of the code --- .../GoogleAPI/Client/AuthStorage.pm | 29 +++++++++- .../Client/AuthStorage/AccessToken.pm | 12 ++++ .../Client/AuthStorage/ConfigJSON.pm | 56 +++++++------------ .../GoogleAPI/Client/Credentials.pm | 40 ------------- lib/WebService/GoogleAPI/Client/UserAgent.pm | 8 +-- 5 files changed, 62 insertions(+), 83 deletions(-) create mode 100644 lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm delete mode 100644 lib/WebService/GoogleAPI/Client/Credentials.pm diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index e4afed9..1f8fd5a 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -10,9 +10,32 @@ package WebService::GoogleAPI::Client::AuthStorage; ## or is UserAgent->credentials use Moo::Role; - -requires qw/refresh_token get_access_token_from_storage set_access_token_to_storage scopes/; - +with 'MooX::Singleton'; + +use WebService::GoogleAPI::Client::AuthStorage::AccessToken; + +requires qw/ + refresh_access_token + get_access_token + scopes +/; + +around get_access_token => sub { + my ($orig, $self) = @_; + my $user = $self->user; + my $token = $self->$orig; + my $wrapped = WebService::GoogleAPI::Client::AuthStorage::AccessToken->new( + user => $user, token => $token ); + $self->access_token($wrapped); + return $wrapped; +}; + +has access_token => + is => 'rw'; + +has user => + is => 'rw', + trigger => sub { shift->get_access_token }; 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm new file mode 100644 index 0000000..71c5050 --- /dev/null +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm @@ -0,0 +1,12 @@ +package WebService::GoogleAPI::Client::AuthStorage::AccessToken; + +use Moo; + +use overload '""' => sub { shift->token }; + +has [ qw/token user/ ] => + is => 'ro', + required => 1; + + +9008 diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 13320e3..948021a 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -10,10 +10,9 @@ use Carp; with 'WebService::GoogleAPI::Client::AuthStorage'; -has 'path' => ( is => 'rw', default => 'gapi.json' ); # default is gapi.json +has 'path' => ( is => 'rw', default => './gapi.json' ); # default is gapi.json has 'tokensfile' => ( is => 'rw' ); # Config::JSON object pointer -has 'debug' => ( is => 'rw', default => 0 ); # NOTE- this type of class has getters and setters b/c the implementation of # getting and setting depends on what's storing @@ -24,53 +23,37 @@ sub BUILD { return $self; } -sub refresh_token { ... } # TODO +sub refresh_access_token { + ... #TODO +} + sub get_credentials_for_refresh { my ($self, $user) = @_; return { - client_id => $self->get_client_id_from_storage(), - client_secret => $self->get_client_secret_from_storage(), - refresh_token => $self->get_refresh_token_from_storage($user) + map { ( $_ => $self->get_from_storage($_) ) } + qw/client_id client_secret refresh_token/ }; } sub get_token_emails_from_storage { my ($self) = @_; - my $tokens = $self->tokensfile->get('gapi/tokens'); + my $tokens = $self->get_from_storage('tokens'); return [keys %$tokens]; } - -sub get_client_id_from_storage { - my ($self) = @_; - return $self->tokensfile->get('gapi/client_id'); -} - -sub get_client_secret_from_storage { - my ($self) = @_; - return $self->tokensfile->get('gapi/client_secret'); -} - -sub get_refresh_token_from_storage { - my ($self, $user) = @_; - carp "get_refresh_token_from_storage(" . $user . ")" if $self->debug; - return $self->tokensfile->get('gapi/tokens/' . $user . '/refresh_token'); -} - -sub get_access_token_from_storage { - my ($self, $user) = @_; - return $self->tokensfile->get('gapi/tokens/' . $user . '/access_token'); -} - -sub set_access_token_to_storage { - my ($self, $user, $token) = @_; - return $self->tokensfile->set('gapi/tokens/' . $user . '/access_token', - $token); +sub get_from_storage { + my ($self, $key) = @_; + if ($key =~ /_token/) { + return $self->tokensfile->get("gapi/tokens/${\$self->user}/$key") + } else { + return $self->tokensfile->get("gapi/$key") + } } -sub get_scopes_from_storage { +sub get_access_token { my ($self) = @_; - return $self->tokensfile->get('gapi/scopes'); + my $value = $self->get_from_storage('access_token'); + return $value } sub get_scopes_from_storage_as_array { @@ -78,7 +61,8 @@ sub get_scopes_from_storage_as_array { return $_[0]->scopes } -# NOTE - the scopes are stored as a space seperated list +# NOTE - the scopes are stored as a space seperated list, and this method +# returns an arrayref sub scopes { my ($self) = @_; return [split / /, $self->tokensfile->get('gapi/scopes')]; diff --git a/lib/WebService/GoogleAPI/Client/Credentials.pm b/lib/WebService/GoogleAPI/Client/Credentials.pm deleted file mode 100644 index 873a51f..0000000 --- a/lib/WebService/GoogleAPI/Client/Credentials.pm +++ /dev/null @@ -1,40 +0,0 @@ -use strictures; - -package WebService::GoogleAPI::Client::Credentials; - -# ABSTRACT: Credentials for particular Client instance. You can use this module -# as singleton also if you need to share credentials between two or more -# instances - - -use Carp; -use Moo; -use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; -with 'MooX::Singleton'; - - -has 'access_token' => ( is => 'rw' ); -has 'user' => ( is => 'rw', trigger => \&get_access_token_for_user ); -has 'auth_storage' => - is => 'rw', - default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new }, - lazy => 1; - -=method get_access_token_for_user - -Automatically get access_token for current user if auth_storage is set - -=cut - -sub get_access_token_for_user { - my ($self) = @_; - $self->access_token($self->auth_storage->get_access_token_from_storage($self->user)); - return $self; -} - -sub scopes { - my ( $self ) = @_; - $self->auth_storage->scopes; -} - -1; diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index ade1cac..3699a0a 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -8,7 +8,7 @@ use Moo; extends 'Mojo::UserAgent'; #extends 'Mojo::UserAgent::Mockable'; -use WebService::GoogleAPI::Client::Credentials; +use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; use Mojo::UserAgent; use Data::Dump qw/pp/; # for dev debug @@ -17,10 +17,10 @@ use Carp qw/croak carp cluck/; has 'do_autorefresh' => ( is => 'rw', default => 1 ); # if 1 storage must be configured has 'auto_update_tokens_in_storage' => ( is => 'rw', default => 1 ); has 'debug' => ( is => 'rw', default => 0 ); -has 'credentials' => ( +has 'auth_storage' => ( is => 'rw', - default => sub { WebService::GoogleAPI::Client::Credentials->instance }, - handles => [qw/access_token auth_storage scopes user/], + default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->instance }, + handles => [qw/access_token scopes user/], lazy => 1 ); From 1bc7316ff237339e24b7f63bdb330f4973f2737a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 20:16:03 +0200 Subject: [PATCH 36/68] rename AccessToken class --- .../Client/{AuthStorage => }/AccessToken.pm | 4 ++-- .../GoogleAPI/Client/AuthStorage.pm | 19 +++++++++++++------ lib/WebService/GoogleAPI/Client/UserAgent.pm | 5 +---- 3 files changed, 16 insertions(+), 12 deletions(-) rename lib/WebService/GoogleAPI/Client/{AuthStorage => }/AccessToken.pm (50%) diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm similarity index 50% rename from lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm rename to lib/WebService/GoogleAPI/Client/AccessToken.pm index 71c5050..e4ac466 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -1,10 +1,10 @@ -package WebService::GoogleAPI::Client::AuthStorage::AccessToken; +package WebService::GoogleAPI::Client::AccessToken; use Moo; use overload '""' => sub { shift->token }; -has [ qw/token user/ ] => +has [ qw/token user scopes/ ] => is => 'ro', required => 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 1f8fd5a..a67aec6 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -10,9 +10,10 @@ package WebService::GoogleAPI::Client::AuthStorage; ## or is UserAgent->credentials use Moo::Role; +use Carp; with 'MooX::Singleton'; -use WebService::GoogleAPI::Client::AuthStorage::AccessToken; +use WebService::GoogleAPI::Client::AccessToken; requires qw/ refresh_access_token @@ -23,15 +24,21 @@ requires qw/ around get_access_token => sub { my ($orig, $self) = @_; my $user = $self->user; + my $scopes = $self->scopes; + my $token = $self->$orig; - my $wrapped = WebService::GoogleAPI::Client::AuthStorage::AccessToken->new( - user => $user, token => $token ); - $self->access_token($wrapped); - return $wrapped; + my $class = 'WebService::GoogleAPI::Client::AccessToken'; + return $token if ref $token eq $class; + return WebService::GoogleAPI::Client::AccessToken->new( + user => $user, token => $token, scopes => $scopes ); }; has access_token => - is => 'rw'; + is => 'rw', + isa => sub { + my $class = 'WebService::GoogleAPI::Client::AccessToken'; + croak "access_token must be an instance of $class" unless ref $_[0] eq $class + }; has user => is => 'rw', diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 3699a0a..1f50416 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -191,10 +191,7 @@ sub validated_api_query croak "No user specified, so cant find refresh token and update access_token" unless $self->user; cluck "401 response - access_token was expired. Attemptimg to update it automatically ..." if ($self->debug > 11); - my $cred = $self->auth_storage->get_credentials_for_refresh( $self->user ); # get client_id, client_secret and refresh_token - my $new_token = $self->refresh_access_token( $cred )->{ access_token }; # here also {id_token} etc - cluck "validated_api_query() Got a new token: " . $new_token if ($self->debug > 11); - $self->access_token( $new_token ); + $self->auth_storage->refresh_access_token; if ( $self->auto_update_tokens_in_storage ) { $self->auth_storage->set_access_token_to_storage( $self->user, $self->access_token ); From 50e311fcc5f3fdfa7d6ea69e8fb03689b78102f2 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 21:20:04 +0200 Subject: [PATCH 37/68] always call get_access_token, don't have a access_token attr --- lib/WebService/GoogleAPI/Client.pm | 4 +- .../GoogleAPI/Client/AuthStorage.pm | 19 +-- lib/WebService/GoogleAPI/Client/UserAgent.pm | 127 ++++++------------ 3 files changed, 46 insertions(+), 104 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 12a7fcd..82d164d 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -103,7 +103,7 @@ has 'debug' => ( ); has 'ua' => ( handles => - [qw/access_token auth_storage do_autorefresh scopes user /], + [qw/get_access_token auth_storage do_autorefresh scopes user/], is => 'ro', default => sub { WebService::GoogleAPI::Client::UserAgent->new(debug => shift->debug) } @@ -251,7 +251,7 @@ Required params: method, route Optional params: api_endpoint_id cb_method_discovery_modify, options -$self->access_token must be valid +$self->get_access_token must return a valid token $gapi->api_query({ diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index a67aec6..a6a1a5e 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -4,21 +4,17 @@ package WebService::GoogleAPI::Client::AuthStorage; # ABSTRACT: JSON File Persistence for Google OAUTH Project and User Access Tokens -## is client->auth_storage -## or is Client->ua->auth_storage delegated as auth_storage to client - -## or is UserAgent->credentials - use Moo::Role; use Carp; with 'MooX::Singleton'; use WebService::GoogleAPI::Client::AccessToken; +# some backends may have scopes as read only, and others as read write requires qw/ + scopes refresh_access_token get_access_token - scopes /; around get_access_token => sub { @@ -33,16 +29,7 @@ around get_access_token => sub { user => $user, token => $token, scopes => $scopes ); }; -has access_token => - is => 'rw', - isa => sub { - my $class = 'WebService::GoogleAPI::Client::AccessToken'; - croak "access_token must be an instance of $class" unless ref $_[0] eq $class - }; - -has user => - is => 'rw', - trigger => sub { shift->get_access_token }; +has user => is => 'rw'; 1; diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 1f50416..9581035 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -20,7 +20,7 @@ has 'debug' => ( is => 'rw', default => 0 ); has 'auth_storage' => ( is => 'rw', default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->instance }, - handles => [qw/access_token scopes user/], + handles => [qw/get_access_token scopes user/], lazy => 1 ); @@ -31,13 +31,12 @@ has 'auth_storage' => ( # Keep access_token in headers always actual -sub BUILD -{ - my ( $self ) = @_; - ## performance tip as per https://developers.google.com/calendar/performance and similar links - ## NB - to work with Google APIs also assumes that Accept-Encoding: gzip is set in HTTP headers - $self->transactor->name( __PACKAGE__ . ' (gzip enabled)' ); - ## MAX SIZE ETC _ WHAT OTHER CONFIGURABLE PARAMS ARE AVAILABLE +## performance tip as per https://developers.google.com/calendar/performance and similar links +## NB - to work with Google APIs also assumes that Accept-Encoding: gzip is set in HTTP headers +sub BUILD { + my ($self) = @_; + $self->transactor->name(__PACKAGE__ . ' (gzip enabled)'); + ## MAX SIZE ETC _ WHAT OTHER CONFIGURABLE PARAMS ARE AVAILABLE } @@ -47,8 +46,6 @@ sub BUILD =cut -## TODO: this should probably be handled ->on('start' => sub {}) as per https://metacpan.org/pod/Mojolicious::Guides::Cookbook#Decorating-follow-up-requests - sub header_with_bearer_auth_token { my ( $self, $headers ) = @_; @@ -56,11 +53,12 @@ sub header_with_bearer_auth_token { $headers->{'Accept-Encoding'} = 'gzip'; - if ($self->access_token) { - $headers->{Authorization} = 'Bearer ' . $self->access_token; + if (my $token = $self->get_access_token) { + $headers->{Authorization} = "Bearer $token"; } else { - carp 'No access_token, can\'t build Auth header'; + # TODO - why is this not fatal? + carp "Can't build Auth header, couldn't get an access token. Is you AuthStorage set up correctly?"; } return $headers; } @@ -141,7 +139,7 @@ NB - handles auth headers injection and token refresh if required and possible Required params: method, route -$self->access_token must be valid +$self->get_access_token must return a valid token Examples of usage: @@ -163,94 +161,51 @@ Returns L object =cut -sub validated_api_query -{ - my ( $self, $params ) = @_; - ## NB validated means that assumes checking against discovery specs has already been done. - - if ( ref( $params ) eq '' ) ## assume is a GET for the URI at $params +# NOTE validated means that we assume checking against discovery specs has already been done. +sub validated_api_query { + my ($self, $params) = @_; + + ## assume is a GET for the URI at $params + if (ref($params) eq '') { - cluck( "transcribing $params to a hashref for validated_api_query" ) if $self->debug; + cluck("transcribing $params to a hashref for validated_api_query") + if $self->debug; my $val = $params; $params = { path => $val, method => 'get', options => {}, }; } - my $tx = $self->build_http_transaction( $params ); - - cluck("$params->{method} $params->{path}") if $self->debug; + my $tx = $self->build_http_transaction($params); + + cluck("$params->{method} $params->{path}") if $self->debug; + #TODO- figure out how we can alter this to use promises - my $res = $self->start( $tx )->res; - + # at this point, i think we'd have to make a different method entirely to + # do this promise-wise + my $res = $self->start($tx)->res; + ## TODO: HANDLE TIMEOUTS AND OTHER ERRORS IF THEY WEREN'T HANDLED BY build_http_transaction - - ## TODO: return Mojo::Message::Response->new unless ref( $res ) eq 'Mojo::Message::Response'; - if ( ( $res->code == 401 ) && $self->do_autorefresh ) { - if ( $res->code == 401 ) { ## redundant - was there something else in mind ? - #TODO- I'm fairly certain this fires too often - croak "No user specified, so cant find refresh token and update access_token" unless $self->user; - cluck "401 response - access_token was expired. Attemptimg to update it automatically ..." if ($self->debug > 11); + if (($res->code == 401) && $self->do_autorefresh) { + cluck "Your access token was expired. Attemptimg to update it automatically..." + if ($self->debug > 11); - $self->auth_storage->refresh_access_token; + $self->auth_storage->refresh_access_token($self); - if ( $self->auto_update_tokens_in_storage ) { - $self->auth_storage->set_access_token_to_storage( $self->user, $self->access_token ); - } - - #$tx = $self->build_http_transaction( $params ); - - $res = $self->start( $self->build_http_transaction( $params ) )->res; # Mojo::Message::Response - } - } - elsif ( $res->code == 403 ) { - cluck( 'Unexpected permission denied 403 error ' ); + return $self->validated_api_query($params); + } elsif ($res->code == 403) { + cluck('Unexpected permission denied 403 error'); return $res; + } elsif ($res->code == 429) { + cluck('HTTP 429 - you hit a rate limit. Try again later'); + return $res } return $res if $res->code == 200; - return $res if $res->code == 204; ## NO CONTENT - INDICATES OK FOR DELETE ETC - return $res if $res->code == 400; ## general failure - cluck( "unhandled validated_api_query response code " . $res->code ); + return $res if $res->code == 204; ## NO CONTENT - INDICATES OK FOR DELETE ETC + return $res if $res->code == 400; ## general failure + cluck("unhandled validated_api_query response code " . $res->code); return $res; } -=head2 C - -Get new access token for user from Google API server - - $self->refresh_access_token({ - client_id => '', - client_secret => '', - refresh_token => '' - }) - - Q: under what conditions could we not have a refresh token? - what scopes are required? ensure that included in defaults if they are req'd - -=cut - -sub refresh_access_token -{ - my ( $self, $credentials ) = @_; - - if ( - ( !defined $credentials->{ client_id } ) ## could this be caught somewhere earlier than here? - || ( !defined $credentials->{ client_secret } ) ## unless credentials include an access token only ? - || ( !defined $credentials->{ refresh_token } ) - ) - { - croak 'If your credentials are missing the refresh_token - consider removing the auth at ' - . 'https://myaccount.google.com/permissions as The oauth2 server will only ever mint one refresh ' - . 'token at a time, and if you request another access token via the flow it will operate as if ' - . 'you only asked for an access token.' - if !defined $credentials->{ refresh_token }; - - croak "Not enough credentials to refresh access_token. Check that you provided client_id, client_secret and refresh_token"; - } - - cluck "refresh_access_token:: Attempt to refresh access_token " if ($self->debug > 11); - $credentials->{ grant_type } = 'refresh_token'; - return $self->post( 'https://www.googleapis.com/oauth2/v4/token' => form => $credentials )->res->json || croak( 'refresh_access_token failed' ); # tokens -} - 1; From 0d7e145a36d3997f7218fbe2a6b539fea66742f2 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 21:20:29 +0200 Subject: [PATCH 38/68] implement refresh_access_token for a user --- .../Client/AuthStorage/ConfigJSON.pm | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 948021a..cc3e6a0 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -20,19 +20,35 @@ has 'tokensfile' => ( is => 'rw' ); # Config::JSON object pointer sub BUILD { my ($self) = @_; $self->tokensfile(Config::JSON->new($self->path)); + my $missing = grep !$_, map $self->get_from_storage($_), qw/client_id client_secret/; + croak < $self->get_from_storage($_) ) } + qw/client_id client_secret refresh_token/; + + croak <user; + + my $tx = $ua->post('https://www.googleapis.com/oauth2/v4/token' => form => \%p); + my $new_token = $tx->res->json('/access_token'); + croak('refresh_access_token failed') unless $new_token; -sub get_credentials_for_refresh { - my ($self, $user) = @_; - return { - map { ( $_ => $self->get_from_storage($_) ) } - qw/client_id client_secret refresh_token/ - }; + $self->tokensfile->set("gapi/tokens/$user/access_token", $new_token); + return $new_token; } sub get_token_emails_from_storage { From 8ec687fdf18987a174c60e45e7ed498aa801b5b4 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 21:24:38 +0200 Subject: [PATCH 39/68] remove auto_update_tokens_in_storage option - it wasn't documented anyways - that could be an option in an AuthStorage class, but i don't want it configurable by default --- lib/WebService/GoogleAPI/Client.pm | 2 +- lib/WebService/GoogleAPI/Client/UserAgent.pm | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 82d164d..45beb3d 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -103,7 +103,7 @@ has 'debug' => ( ); has 'ua' => ( handles => - [qw/get_access_token auth_storage do_autorefresh scopes user/], + [qw/do_autorefresh auth_storage get_access_token scopes user/], is => 'ro', default => sub { WebService::GoogleAPI::Client::UserAgent->new(debug => shift->debug) } diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 9581035..f3b996b 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -14,8 +14,7 @@ use Data::Dump qw/pp/; # for dev debug use Carp qw/croak carp cluck/; -has 'do_autorefresh' => ( is => 'rw', default => 1 ); # if 1 storage must be configured -has 'auto_update_tokens_in_storage' => ( is => 'rw', default => 1 ); +has 'do_autorefresh' => ( is => 'rw', default => 1 ); has 'debug' => ( is => 'rw', default => 0 ); has 'auth_storage' => ( is => 'rw', From a3624c2c362c288f9096e48b4ddafb10f176ec15 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 22:43:06 +0200 Subject: [PATCH 40/68] almost done implementing service accoutns --- lib/WebService/GoogleAPI/Client.pm | 20 ++-- .../GoogleAPI/Client/AuthStorage.pm | 6 +- .../Client/AuthStorage/ConfigJSON.pm | 4 +- .../Client/AuthStorage/ServiceAccount.pm | 93 ++++++++++++++----- lib/WebService/GoogleAPI/Client/UserAgent.pm | 18 +++- 5 files changed, 99 insertions(+), 42 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 45beb3d..54d3ee7 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -223,19 +223,17 @@ sub BUILD { my ($self, $params) = @_; if (defined $params->{gapi_json}) { - $self->auth_storage( - WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new(path => $params->{gapi_json}) - ); + my $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new( + path => $params->{gapi_json}); + $self->auth_storage($storage); } elsif (defined $params->{service_account}) { - $self->auth_storage( - WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( - path => $params->{service_account}) - ); + my $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + path => $params->{service_account}, scopes => $params->{scopes}); + $self->auth_storage($storage); } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { - $self->auth_storage( - WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( - path => $file) - ); + my $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + path => $file, scopes => $params->{scopes}); + $self->auth_storage($storage); } $self->user($params->{user}) if (defined $params->{user}); diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index a6a1a5e..eae8b62 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -1,13 +1,10 @@ use strictures; - package WebService::GoogleAPI::Client::AuthStorage; # ABSTRACT: JSON File Persistence for Google OAUTH Project and User Access Tokens use Moo::Role; use Carp; -with 'MooX::Singleton'; - use WebService::GoogleAPI::Client::AccessToken; # some backends may have scopes as read only, and others as read write @@ -31,5 +28,8 @@ around get_access_token => sub { has user => is => 'rw'; +# this is managed by the BUILD in ::Client::UserAgent, +# and by the BUILD in ::Client +has ua => is => 'rw', weak_ref => 1; 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index cc3e6a0..27c1896 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -30,7 +30,7 @@ NOCLIENT } sub refresh_access_token { - my ($self, $ua) = @_; + my ($self) = @_; my %p = map { ( $_ => $self->get_from_storage($_) ) } qw/client_id client_secret refresh_token/; @@ -43,7 +43,7 @@ MISSINGCREDS $p{grant_type} = 'refresh_token'; my $user = $self->user; - my $tx = $ua->post('https://www.googleapis.com/oauth2/v4/token' => form => \%p); + my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => \%p); my $new_token = $tx->res->json('/access_token'); croak('refresh_access_token failed') unless $new_token; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 7b88942..af9c0f9 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -1,33 +1,80 @@ use strictures; - package WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; # ABSTRACT: Specific methods to fetch tokens from a service # account json file use Moo; -use Config::JSON; use Carp; - -extends 'WebService::GoogleAPI::Client::AuthStorage::ConfigJSON'; - - - - - - - - - - - - - - - - - - - +use Mojo::JWT::Google; + +has scopes => + is => 'rw', + coerce => sub { + my $arg = shift; + return [ split / /, $arg ] unless ref $arg eq 'ARRAY'; + return $arg + }, + default => sub { [] }; + +has user => + is => 'rw', + coerce => sub { $_[0] || '' }, + deafault => ''; + +with 'WebService::GoogleAPI::Client::AuthStorage'; + +has path => + is => 'rw', + required => 1, + trigger => 1; + +has jwt => + is => 'rw'; + +# we keep record of the tokens we've gotten so far +has tokens => + is => 'ro', + default => sub { {} }; + + +sub _trigger_path { + my ($self) = @_; + $self->jwt( + Mojo::JWT::Google->new(from_json => $self->path) + ); +} + +sub scopes_string { + my ($self) = @_; + return join ' ', @{$self->scopes} +} + +sub get_access_token { + my ($self) = @_; + use Data::Printer; p $self; + my $token = $self->tokens->{$self->scopes_string}{$self->user}; + return $self->refresh_access_token unless $token; + return $token +} + +sub refresh_access_token { + my ($self) = @_; + croak "Can't get a token without a set of scopes" unless @{$self->scopes}; + + $self->jwt->scopes($self->scopes); + if ($self->user) { + $self->jwt->user_as($self->user) + } else { + $self->jwt->user_as(undef) + } + + my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => $self->jwt->as_form_data); + my $new_token = $tx->res->json('/access_token'); + croak('refresh_access_token failed') unless $new_token; + + $self->tokens->{$self->scopes_string}{$self->user} = $new_token; + return $new_token +} 9001 diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index f3b996b..f169539 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -18,12 +18,24 @@ has 'do_autorefresh' => ( is => 'rw', default => 1 ); has 'debug' => ( is => 'rw', default => 0 ); has 'auth_storage' => ( is => 'rw', - default => sub { WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->instance }, + default => sub { + WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new + }, handles => [qw/get_access_token scopes user/], - lazy => 1 + trigger => 1, + lazy => 1 ); -## NB - could cache using https://metacpan.org/pod/Mojo::UserAgent::Cached TODO: Review source of this for ideas +sub _trigger_auth_storage { + my ($self) = @_; + # give the auth_storage a ua + # TODO - this seems like code smell to me. Should these storage things be + # roles that get applied to this ua? + $self->auth_storage->ua($self) +} + +## NB - could cache using https://metacpan.org/pod/Mojo::UserAgent::Cached +# TODO: Review source of this for ideas ## NB - used by both Client and Discovery From c4989088b098de08385b55c8c4316fcdad678f65 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sat, 23 Jan 2021 22:55:04 +0200 Subject: [PATCH 41/68] service account is working! --- dist.ini | 2 +- lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dist.ini b/dist.ini index 0d9848e..bd0193e 100755 --- a/dist.ini +++ b/dist.ini @@ -32,7 +32,7 @@ List::Util = 1.45 List::SomeUtils = 0 IO::Socket::SSL = 2.06 Mojo::JWT = 0 -Mojo::JWT::Google = 0.14 +Mojo::JWT::Google = 0.15 Exporter::Shiny = 0 [Prereqs / TestRequires ] diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index af9c0f9..1e536e3 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -20,7 +20,7 @@ has scopes => has user => is => 'rw', coerce => sub { $_[0] || '' }, - deafault => ''; + default => ''; with 'WebService::GoogleAPI::Client::AuthStorage'; @@ -52,7 +52,6 @@ sub scopes_string { sub get_access_token { my ($self) = @_; - use Data::Printer; p $self; my $token = $self->tokens->{$self->scopes_string}{$self->user}; return $self->refresh_access_token unless $token; return $token From 149a5b8c71e75b9775ed7137e133f0de98f272a5 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 00:19:36 +0200 Subject: [PATCH 42/68] update dist.ini and TODO --- README.txt | 95 ++++++++++++++++++++++++++++++++++++++++-------------- TODO | 80 ++++----------------------------------------- dist.ini | 15 ++++----- 3 files changed, 84 insertions(+), 106 deletions(-) diff --git a/README.txt b/README.txt index a631c5e..6f218b3 100644 --- a/README.txt +++ b/README.txt @@ -4,7 +4,7 @@ NAME VERSION - version 0.22 + version 0.23 SYNOPSIS @@ -42,18 +42,21 @@ EXAMPLES use MIME::Base64; my $my_email_address = 'peter@shotgundriver.com' + my $raw_email_payload = encode_base64( + Email::Simple->create( + header => [ + To => $my_email_address, + From => $my_email_address, + Subject => "Test email from '$my_email_address' ", + ], + body => "This is the body of email to '$my_email_address'", + )->as_string + ); - my $raw_email_payload = encode_base64( Email::Simple->create( - header => [To => $my_email_address, From => $my_email_address, - Subject =>"Test email from '$my_email_address' ",], - body => "This is the body of email to '$my_email_address'", - )->as_string - ); - - $gapi_client->api_query( - api_endpoint_id => 'gmail.users.messages.send', - options => { raw => $raw_email_payload }, - ); + $gapi_client->api_query( + api_endpoint_id => 'gmail.users.messages.send', + options => { raw => $raw_email_payload }, + ); MANUAL API REQUEST CONSTRUCTION - GET CALENDAR LIST @@ -67,20 +70,60 @@ METHODS new - WebService::GoogleAPI::Client->new( user => 'peter@pscott.com.au', gapi_json => '/fullpath/gapi.json' ); + WebService::GoogleAPI::Client->new( + user => 'peter@pscott.com.au', gapi_json => '/fullpath/gapi.json' ); + + General parameters + + debug + + if truthy then diagnostics are send to STDERR - default false. Crank + it up to 11 for maximal debug output + + chi + + an instance to a CHI persistent storage case object - if none + provided FILE is used + + Login Parameters - PARAMETERS + You can use either gapi_json, which is the file you get from using the + bundled goauth tool, or service_account which is the json file you can + download from + https://console.cloud.google.com/iam-admin/serviceaccounts. - user :: the email address that identifies key of credentials in the - config file + service_account and gapi_json are mutually exclusive, and gapi_json + takes precedence. - gapi_json :: Location of the configuration credentials - default - gapi.json + If nothing is passed, then we check the GOOGLE_APPLICATION_CREDENTIALS + env variable for the location of a service account file. This matches + the functionality of the Google Cloud libraries from other languages + (well, somewhat. I haven't fully implemented ADC yet - see Google's + Docs for some + details. PRs are welcome!) - debug :: if '1' then diagnostics are send to STDERR - default false + If that doesn't exist, then we default to gapi.json in the current + directory. - chi :: an instance to a CHI persistent storage case object - if none - provided FILE is used + Be wary! This default is subject to change as more storage backends are + implemented. A deprecation warning will be emmitted when this is likely + to start happening. + + user + + the email address that requests will be made for + + gapi_json + + Location of end user credentials + + service_account + + Location of service account credentials + + If you're using a service account, user represents the user that you're + impersonating. Make sure you have domain-wide delegation set up, or + else this won't work. api_query @@ -93,7 +136,7 @@ METHODS Optional params: api_endpoint_id cb_method_discovery_modify, options - $self->access_token must be valid + $self->get_access_token must return a valid token $gapi->api_query({ method => 'get', @@ -345,13 +388,15 @@ FEATURES OAuth2 configuration, sccoping, authorization and obtaining access_ and refresh_ tokens from users -AUTHOR +AUTHORS + + * Peter Scott - Peter Scott + * Veesh Goldman COPYRIGHT AND LICENSE - This software is Copyright (c) 2017-2018 by Peter Scott and others. + This software is Copyright (c) 2017-2020 by Peter Scott and others. This is free software, licensed under: diff --git a/TODO b/TODO index f9381ec..b31c802 100644 --- a/TODO +++ b/TODO @@ -3,83 +3,17 @@ we're trying to take care of a bunch of things. # better support for service accounts -In order to implement service accounts, we need to have an -encapsulated interface for retrieving and refreshing tokens. Then, -there can be a shared interface, and both normal files and service -accounts can be called the same way. +We did a number on the architecture to allow for pluggable storage backends. +I think some more improvement could be had, but hey, it's a start. -We expect to implement something which parses the json file, and -then determines what type it is. A gapi file has 'gapi' as the top -level key. A service account has the following top level keys: -qw/ type project_id private_key_id private_key client_email -client_id auth_uri token_uri auth_provider_x509_cert_url -client_x509_cert_url /. +We need to document ::Client::AccessToken (and why it's there). -## targets for fixing +We also need to dress the res with the token used to create it (for introspection) -### in Client::UserAgent - -`refresh_access_token` needs to be controlled by the auth storage, b/c the -process of getting a token differs between normal oauth and service accounts. - -see line 196 of the main module, for example. - yeah, gut that whole method and move it to the storage module in the code - -we must move refresh_access_token from the main module to the auth storage - -subclassing Mojo::UserAgent seems silly to me. This should be a role that gets -composed in. - -### in ::Client::AuthStorage - -the class ::Client::AuthStorage should be able to auto-route to any storage -class. Then it would be more sane to have the values passed to it match a class -name. This makes this part of the system more plugable. It's not like this is -unique to google, right. - this includes making the setup method take more sane parameters also, or at - least be somewhat documented - -ew, the way it's implemented the AuthStorage object holds the actual storage -object. definitiely, definitely should be a role - -we've got some icky long function names to shorten - -one thing to ponder is that AuthStorage should deal only with the storage -(meaning flat-file, cached in memory, in redis, in a database). The TYPE of cred -that we want l'chora should be in ::Credentials - -#### for service accounts in general - -when implementing service accounts, note that they don't need to have a user set - -we would want to cache keys if we can, combined with an expiration time. the -things that would need to be cached are 'user' and 'scopes', b/c i assume you -get a different token depending on what scopes you ask for. - - i guess that means that we'd need to alphabetize the scopes and trim spaces, - so that we can just string compare and skip an extra ask for permissions - - -### in ::Client::Credentials - -we need a way that the main module can pass config information. Make it clear, -at least - -why does this class even exist? - - okay, so i decided that ::Credentials should manage the retrieval and renewal - of credentials, depending on the type. AuthStorage should only store (read - only or read-write). - -why is this a singleton? - - it's a singleton b/c you may want to share credentials between all instances - i guess that's a reasonable default for the module to provide, and let you - override if you need to - -i don't see a reason that this should be implemented seperately from the -storage. +We need to make auth_storage in the ua make sure the attached object is a +consumer of our role. +We must document our role, and the two consumers that we have. #Other Issues diff --git a/dist.ini b/dist.ini index bd0193e..fc24f87 100755 --- a/dist.ini +++ b/dist.ini @@ -1,9 +1,10 @@ name = WebService-GoogleAPI-Client author = Peter Scott +author = Veesh Goldman license = Apache_2_0 copyright_holder = Peter Scott and others -copyright_year = 2017-2018 -version = 0.22 +copyright_year = 2017-2020 +version = 0.23 main_module = lib/WebService/GoogleAPI/Client.pm ;[MinimumPerl] @@ -24,7 +25,7 @@ main_module = lib/WebService/GoogleAPI/Client.pm [PkgVersion] [Prereqs] -perl = 5.14.04 +perl = 5.16 Moo = 2.00 Mojolicious = 8.30 Mojolicious::Plugin::OAuth2 = 1.5 @@ -48,7 +49,10 @@ perltidyrc = .perltidyrc [GatherDir] ; exclude test scripts from build exclude_filename = gapi.json +exclude_filename = service.json exclude_filename = DEV.MD +exclude_filename = sner +exclude_filename = TODO exclude_match = docs/* exclude_match = examples/dev_* exclude_filename = examples/gapi.json @@ -60,11 +64,6 @@ exclude_match = examples/openapi/* [PodWeaver] -;[ReadmeAnyFromPod] -;type = markdown -;filename = README.md -;location = build - [ReadmeAnyFromPod] type = text filename = README.txt From f415e9dd1bbf96ecaaddef8260abcddd34f37b85 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 00:56:33 +0200 Subject: [PATCH 43/68] start working on the POD --- .../GoogleAPI/Client/AuthStorage.pm | 78 +++++++++++++++++-- .../Client/AuthStorage/ServiceAccount.pm | 3 +- weaver.ini | 4 + 3 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 weaver.ini diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index eae8b62..a65109a 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -1,12 +1,82 @@ use strictures; package WebService::GoogleAPI::Client::AuthStorage; -# ABSTRACT: JSON File Persistence for Google OAUTH Project and User Access Tokens +# ABSTRACT: Role for classes which store your auth credentials use Moo::Role; use Carp; use WebService::GoogleAPI::Client::AccessToken; +=head1 SYNOPSIS + + package My::Cool::AuthStorage::Class; + use Moo; + with 'WebService::GoogleAPI::Client::AuthStorage'; + + ... # implement the necessary functions + + package main; + use WebService::GoogleAPI::Client; + use My::Cool::AuthStorage::Class; + my $gapi = WebService::GoogleAPI::Client->new( + auth_storage => My::Cool::AuthStorage::Class->new + ); + ... # and now your class manages the access_tokens + +WebService::GoogleAPI::Client::AuthStorage is a Moo::Role for auth storage backends. +This dist comes with two consumers, L +and L. See those for more info +on how you can use them with L. + +This is a role which defines the interface that L +will use when making requests. + +=cut + +has user => is => 'rw'; +=attr user + +The user that an access token should be returned for. Is read/write. May be +falsy, depending on the backend. +=cut + +# this is managed by the BUILD in ::Client::UserAgent, +# and by the BUILD in ::Client +has ua => is => 'rw', weak_ref => 1; +=attr ua + +An weak reference to the WebService::GoogleAPI::Client::UserAgent that this is +attached to, so that access tokens can be refreshed. The UserAgent object manages this. + +=cut + + +=head1 REQUIRES + +It requires the consuming class to implement functions with the following names: + +=begin :list + += scopes + +A list of scopes that you expect the access token to be valid for. This could be +read/write, but it's not necessary. Some backends may have different credentials +for different sets of scopes (though as an author, you probably want to just +have the whole set you need upfront). + += refresh_access_token + +A method which will refresh the access token if it has been determined to have expired. + += get_access_token + +A method which will return the access token for the current user and scopes. +This method is wrapped to augment whatever has been returned with user and +scopes data for introspection by making a +WebService::GoogleAPI::Client::AccessToken instance. If you choose to return such an instance yourself, then it will be left alone. + +=end :list +=cut # some backends may have scopes as read only, and others as read write requires qw/ scopes @@ -26,10 +96,4 @@ around get_access_token => sub { user => $user, token => $token, scopes => $scopes ); }; -has user => is => 'rw'; - -# this is managed by the BUILD in ::Client::UserAgent, -# and by the BUILD in ::Client -has ua => is => 'rw', weak_ref => 1; - 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 1e536e3..6bba4b0 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -1,8 +1,7 @@ use strictures; package WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; -# ABSTRACT: Specific methods to fetch tokens from a service -# account json file +# ABSTRACT: Manage access tokens from a service account file use Moo; use Carp; diff --git a/weaver.ini b/weaver.ini new file mode 100644 index 0000000..7223daf --- /dev/null +++ b/weaver.ini @@ -0,0 +1,4 @@ +[@Default] + +[-Transformer] +transformer = List From 26cf64606f64b7adaca4204a9a30a627b7210778 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 01:07:02 +0200 Subject: [PATCH 44/68] allow auth_storage argument to main object --- TODO | 1 + dist.ini | 2 +- lib/WebService/GoogleAPI/Client.pm | 39 ++++++++++++------- .../GoogleAPI/Client/AuthStorage.pm | 5 ++- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/TODO b/TODO index b31c802..30d7cf3 100644 --- a/TODO +++ b/TODO @@ -14,6 +14,7 @@ We need to make auth_storage in the ua make sure the attached object is a consumer of our role. We must document our role, and the two consumers that we have. + Started doc-ing the role, at least #Other Issues diff --git a/dist.ini b/dist.ini index fc24f87..17a0b53 100755 --- a/dist.ini +++ b/dist.ini @@ -1,6 +1,6 @@ name = WebService-GoogleAPI-Client -author = Peter Scott author = Veesh Goldman +author = Peter Scott license = Apache_2_0 copyright_holder = Peter Scott and others copyright_year = 2017-2020 diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 54d3ee7..465d065 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -192,21 +192,30 @@ If that doesn't exist, then we default to F in the current directory. B This default is subject to change as more storage backends are implemented. A deprecation warning will be emmitted when this is likely to start happening. -=over 4 +For more advanced usage, you can supply your own auth storage instance, which is +a consumer of the L role. See the +POD for that module for more information. + +=begin :list -=item user += user the email address that requests will be made for -=item gapi_json += gapi_json Location of end user credentials -=item service_account += service_account Location of service account credentials -=back += auth_storage + +An instance of a class consuming L, already +set up for returning access tokens (barring the ua). + +=end :list If you're using a service account, user represents the user that you're impersonating. Make sure you have domain-wide delegation set up, or else this @@ -222,19 +231,19 @@ won't work. sub BUILD { my ($self, $params) = @_; - if (defined $params->{gapi_json}) { - my $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new( - path => $params->{gapi_json}); - $self->auth_storage($storage); - } elsif (defined $params->{service_account}) { - my $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( - path => $params->{service_account}, scopes => $params->{scopes}); - $self->auth_storage($storage); + my $storage; + if ($params->{auth_storage}) { + $storage = $params->{auth_storage}; + } elsif (my $file = $params->{gapi_json}) { + $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new(path => $file); + } elsif (my $file = $params->{service_account}) { + $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + path => $file, scopes => $params->{scopes}); } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { - my $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( + $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( path => $file, scopes => $params->{scopes}); - $self->auth_storage($storage); } + $self->auth_storage($storage) if $storage; $self->user($params->{user}) if (defined $params->{user}); } diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index a65109a..c7c63d3 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -66,7 +66,9 @@ have the whole set you need upfront). = refresh_access_token -A method which will refresh the access token if it has been determined to have expired. +A method which will refresh the access token if it has been determined to have +expired. Take a look at the two consumers which come with this dist for +examples of how to renew user credentials and service account credentials. = get_access_token @@ -77,7 +79,6 @@ WebService::GoogleAPI::Client::AccessToken instance. If you choose to return suc =end :list =cut -# some backends may have scopes as read only, and others as read write requires qw/ scopes refresh_access_token From aefa66cc473f88d2a7c129c5fb9eff0fecee99b6 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 01:14:50 +0200 Subject: [PATCH 45/68] document AccessToken --- README.txt | 14 +++++++++++-- TODO | 5 +---- dist.ini | 2 +- .../GoogleAPI/Client/AccessToken.pm | 20 +++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/README.txt b/README.txt index 6f218b3..c7a496a 100644 --- a/README.txt +++ b/README.txt @@ -109,6 +109,10 @@ METHODS implemented. A deprecation warning will be emmitted when this is likely to start happening. + For more advanced usage, you can supply your own auth storage instance, + which is a consumer of the WebService::GoogleAPI::Client::AuthStorage + role. See the POD for that module for more information. + user the email address that requests will be made for @@ -121,6 +125,12 @@ METHODS Location of service account credentials + auth_storage + + An instance of a class consuming + WebService::GoogleAPI::Client::AuthStorage, already set up for + returning access tokens (barring the ua). + If you're using a service account, user represents the user that you're impersonating. Make sure you have domain-wide delegation set up, or else this won't work. @@ -390,10 +400,10 @@ FEATURES AUTHORS - * Peter Scott - * Veesh Goldman + * Peter Scott + COPYRIGHT AND LICENSE This software is Copyright (c) 2017-2020 by Peter Scott and others. diff --git a/TODO b/TODO index 30d7cf3..463bd1b 100644 --- a/TODO +++ b/TODO @@ -6,15 +6,12 @@ we're trying to take care of a bunch of things. We did a number on the architecture to allow for pluggable storage backends. I think some more improvement could be had, but hey, it's a start. -We need to document ::Client::AccessToken (and why it's there). - We also need to dress the res with the token used to create it (for introspection) We need to make auth_storage in the ua make sure the attached object is a consumer of our role. -We must document our role, and the two consumers that we have. - Started doc-ing the role, at least +We the two storage backends that we have. #Other Issues diff --git a/dist.ini b/dist.ini index 17a0b53..ad50f72 100755 --- a/dist.ini +++ b/dist.ini @@ -3,7 +3,7 @@ author = Veesh Goldman author = Peter Scott license = Apache_2_0 copyright_holder = Peter Scott and others -copyright_year = 2017-2020 +copyright_year = 2017-2021 version = 0.23 main_module = lib/WebService/GoogleAPI/Client.pm diff --git a/lib/WebService/GoogleAPI/Client/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm index e4ac466..8bc0731 100644 --- a/lib/WebService/GoogleAPI/Client/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -1,5 +1,6 @@ package WebService::GoogleAPI::Client::AccessToken; +# ABSTRACT - A small class for bundling user and scopes with a token use Moo; use overload '""' => sub { shift->token }; @@ -8,5 +9,24 @@ has [ qw/token user scopes/ ] => is => 'ro', required => 1; +=head1 SYNOPSIS + + my $token = $gapi->get_access_token # returns this class + # { + # token => '...', + # user => 'the-user-that-it's-for', + # scopes => [ 'the', 'scopes', 'that', 'its', 'for' ] + # } + +This is a simple class which contains the data related to a Google Cloud access token +that bundles the related user and scopes. + +It overloads stringification so that interpolating it in, say an auth header, +will return just the token. + +This is for introspection purposes, so if something goes wrong, you can check +the response from your request and see the token that was used. + +=cut 9008 From 3b884a77100fdf5ae1b07b5e604f435c2fa2f8f2 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 01:23:23 +0200 Subject: [PATCH 46/68] attach the token to the res --- .../GoogleAPI/Client/AccessToken.pm | 7 +- lib/WebService/GoogleAPI/Client/UserAgent.pm | 85 ++++++++++--------- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm index 8bc0731..ad1ee04 100644 --- a/lib/WebService/GoogleAPI/Client/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -17,6 +17,9 @@ has [ qw/token user scopes/ ] => # user => 'the-user-that-it's-for', # scopes => [ 'the', 'scopes', 'that', 'its', 'for' ] # } + # + my $res = ... # any api call here + $res->{_token} # the token the call was made with This is a simple class which contains the data related to a Google Cloud access token that bundles the related user and scopes. @@ -25,7 +28,9 @@ It overloads stringification so that interpolating it in, say an auth header, will return just the token. This is for introspection purposes, so if something goes wrong, you can check -the response from your request and see the token that was used. +the response from your request and check the C<_token> hash key on that object. +Note that this is subject to change in future versions (there's probably a saner +way to do this). =cut diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index f169539..19ca512 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -58,7 +58,7 @@ sub BUILD { =cut sub header_with_bearer_auth_token { - my ( $self, $headers ) = @_; + my ($self, $headers) = @_; $headers = {} unless defined $headers; @@ -66,10 +66,10 @@ sub header_with_bearer_auth_token { if (my $token = $self->get_access_token) { $headers->{Authorization} = "Bearer $token"; - } - else { + } else { # TODO - why is this not fatal? - carp "Can't build Auth header, couldn't get an access token. Is you AuthStorage set up correctly?"; + carp +"Can't build Auth header, couldn't get an access token. Is you AuthStorage set up correctly?"; } return $headers; } @@ -86,51 +86,55 @@ sub header_with_bearer_auth_token { =cut -sub build_http_transaction -{ - my ( $self, $params ) = @_; +sub build_http_transaction { + my ($self, $params) = @_; ## hack to allow method option as alias for httpMethod - $params->{ httpMethod } = $params->{ method } if defined $params->{ method }; - $params->{ httpMethod } = '' unless defined $params->{ httpMethod }; + $params->{httpMethod} = $params->{method} if defined $params->{method}; + $params->{httpMethod} = '' unless defined $params->{httpMethod}; - my $http_method = uc( $params->{ httpMethod } ) || 'GET'; # uppercase ? - my $optional_data = $params->{ options } || ''; - my $path = $params->{ path } || cluck( 'path parameter required for build_http_transaction' ); - my $no_auth = $params->{ no_auth } || 0; ## default to including auth header - ie not setting no_auth - my $headers = $params->{ headers} || {}; + my $http_method = uc($params->{httpMethod}) || 'GET'; # uppercase ? + my $optional_data = $params->{options} || ''; + my $path = $params->{path} + || cluck('path parameter required for build_http_transaction'); + my $no_auth = $params->{no_auth} + || 0; ## default to including auth header - ie not setting no_auth + my $headers = $params->{headers} || {}; - cluck 'Attention! You are using POST, but no payload specified' if ( ( $http_method eq 'POST' ) && !defined $optional_data ); + cluck 'Attention! You are using POST, but no payload specified' + if (($http_method eq 'POST') && !defined $optional_data); cluck "build_http_transaction:: $http_method $path " if ($self->debug > 11); - cluck "$http_method Not a SUPPORTED HTTP method parameter specified to build_http_transaction" . pp $params unless $http_method =~ /^GET|PATH|PUT|POST|PATCH|DELETE$/ixm; - - ## NB - headers not passed if no_auth - $headers = $self->header_with_bearer_auth_token( $headers ) unless $no_auth; - if ( $http_method =~ /^POST|PATH|PUT|PATCH$/ixg ) - { + cluck +"$http_method Not a SUPPORTED HTTP method parameter specified to build_http_transaction" + . pp $params + unless $http_method =~ /^GET|PATH|PUT|POST|PATCH|DELETE$/ixm; + + ## NB - headers not passed if no_auth + $headers = $self->header_with_bearer_auth_token($headers) unless $no_auth; + if ($http_method =~ /^POST|PATH|PUT|PATCH$/ixg) { ## ternary conditional on whether optional_data is set ## return $optional_data eq '' ? $self->build_tx( $http_method => $path => $headers ) : $self->build_tx( $http_method => $path => $headers => json => $optional_data ); - if ( $optional_data eq '' ) - { - return $self->build_tx( $http_method => $path => $headers ); - } - else - { - if ( ref($optional_data) eq 'HASH' ) + if ($optional_data eq '') { + return $self->build_tx($http_method => $path => $headers); + } else { + if (ref($optional_data) eq 'HASH') { + return $self->build_tx( + $http_method => $path => $headers => json => $optional_data); + } elsif ( + ref($optional_data) eq + '') ## am assuming is a post with options containing a binary payload { - return $self->build_tx( $http_method => $path => $headers => json => $optional_data ); + return $self->build_tx( + $http_method => $path => $headers => $optional_data); } - elsif ( ref($optional_data) eq '') ## am assuming is a post with options containing a binary payload - { - return $self->build_tx( $http_method => $path => $headers => $optional_data ); - } - + } - } - else ## DELETE or GET - { - return $self->build_tx( $http_method => $path => $headers => form => $optional_data ) if ( $http_method eq 'GET' ); - return $self->build_tx( $http_method => $path => $headers ) if ( $http_method eq 'DELETE' ); + } else { ## DELETE or GET + return $self->build_tx( + $http_method => $path => $headers => form => $optional_data) + if ($http_method eq 'GET'); + return $self->build_tx($http_method => $path => $headers) + if ($http_method eq 'DELETE'); } #return undef; ## assert: should never get here @@ -193,8 +197,7 @@ sub validated_api_query { # at this point, i think we'd have to make a different method entirely to # do this promise-wise my $res = $self->start($tx)->res; - - ## TODO: HANDLE TIMEOUTS AND OTHER ERRORS IF THEY WEREN'T HANDLED BY build_http_transaction + $res->{_token} = $self->get_access_token; if (($res->code == 401) && $self->do_autorefresh) { cluck "Your access token was expired. Attemptimg to update it automatically..." From 733609b98c9a3fb509d98da17e948db845aab24b Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 01:27:28 +0200 Subject: [PATCH 47/68] sanity check for auth_storage --- TODO | 3 +-- lib/WebService/GoogleAPI/Client.pm | 8 ++++---- lib/WebService/GoogleAPI/Client/UserAgent.pm | 4 ++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TODO b/TODO index 463bd1b..b57d027 100644 --- a/TODO +++ b/TODO @@ -6,10 +6,9 @@ we're trying to take care of a bunch of things. We did a number on the architecture to allow for pluggable storage backends. I think some more improvement could be had, but hey, it's a start. -We also need to dress the res with the token used to create it (for introspection) - We need to make auth_storage in the ua make sure the attached object is a consumer of our role. + - test this, and a lot of other stuff We the two storage backends that we have. diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 465d065..b418401 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -231,15 +231,15 @@ won't work. sub BUILD { my ($self, $params) = @_; - my $storage; + my ($storage, $file); if ($params->{auth_storage}) { $storage = $params->{auth_storage}; - } elsif (my $file = $params->{gapi_json}) { + } elsif ($file = $params->{gapi_json}) { $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new(path => $file); - } elsif (my $file = $params->{service_account}) { + } elsif ($file = $params->{service_account}) { $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( path => $file, scopes => $params->{scopes}); - } elsif (my $file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { + } elsif ($file = $ENV{GOOGLE_APPLICATION_CREDENTIALS}) { $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( path => $file, scopes => $params->{scopes}); } diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 19ca512..1cb5dad 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -23,6 +23,10 @@ has 'auth_storage' => ( }, handles => [qw/get_access_token scopes user/], trigger => 1, + isa => sub { + my $role = 'WebService::GoogleAPI::Client::AuthStorage'; + die "auth_storage must implement the $role role to work!" unless $_[0]->does($role) + }, lazy => 1 ); From 25359a7b2ff15a8fd1a22a7748e6c82c31fe0ff7 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 14:29:21 +0200 Subject: [PATCH 48/68] add better error messages for failure to refresh creds --- TODO | 5 ++++- .../GoogleAPI/Client/AuthStorage/ConfigJSON.pm | 8 +++++++- .../GoogleAPI/Client/AuthStorage/ServiceAccount.pm | 11 +++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index b57d027..4453581 100644 --- a/TODO +++ b/TODO @@ -10,7 +10,10 @@ We need to make auth_storage in the ua make sure the attached object is a consumer of our role. - test this, and a lot of other stuff -We the two storage backends that we have. +We must document the two storage backends that we have. + +In the auth backends, make failure to get a token return the error attached to +it. #Other Issues diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm index 27c1896..051b24e 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm @@ -45,7 +45,13 @@ MISSINGCREDS my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => \%p); my $new_token = $tx->res->json('/access_token'); - croak('refresh_access_token failed') unless $new_token; + unless ($new_token) { + croak "Failed to refresh access token: ", + join ' - ', map $tx->res->json("/$_"), qw/error error_description/ + if $tx->res->json; + # if the error doesn't come from google + croak "Unknown error refreshing access token"; + } $self->tokensfile->set("gapi/tokens/$user/access_token", $new_token); return $new_token; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 6bba4b0..5df25d9 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -67,9 +67,16 @@ sub refresh_access_token { $self->jwt->user_as(undef) } - my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => $self->jwt->as_form_data); + my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => + $self->jwt->as_form_data); my $new_token = $tx->res->json('/access_token'); - croak('refresh_access_token failed') unless $new_token; + unless ($new_token) { + croak "Failed to get access token: ", + join ' - ', map $tx->res->json("/$_"), qw/error error_description/ + if $tx->res->json; + # if the error doesn't come from google + croak "Unknown error getting access token"; + } $self->tokens->{$self->scopes_string}{$self->user} = $new_token; return $new_token From 505095a5d1af9a12e3f118226d063acb5c2639ad Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 19:39:33 +0200 Subject: [PATCH 49/68] update test suite to Test2 and to use our test helpers --- t/04-no-method.t | 27 +++++ t/05-spec-interpolation.t | 120 ++++++++++------------ t/WebService/GoogleAPI/Client/Discovery.t | 4 + t/lib/TestTools.pm | 24 ++--- 4 files changed, 92 insertions(+), 83 deletions(-) create mode 100644 t/04-no-method.t diff --git a/t/04-no-method.t b/t/04-no-method.t new file mode 100644 index 0000000..2b9a74f --- /dev/null +++ b/t/04-no-method.t @@ -0,0 +1,27 @@ +use Test2::V0; +use lib 't/lib'; +use TestTools qw/gapi_json DEBUG user/; +use WebService::GoogleAPI::Client; + +my $gapi = WebService::GoogleAPI::Client->new( + debug => DEBUG, gapi_json => gapi_json, user => user); + +ok dies { + $gapi->_process_params_for_api_endpoint_and_return_errors({ + api_endpoint_id => 'jobs.non.existant', + options => { + fields => 'your(fez)' + } + }) +}, 'blows up if an endpoint does not exist'; + +ok dies { + $gapi->_process_params_for_api_endpoint_and_return_errors({ + api_endpoint_id => 'i.am.non.existant', + options => { + fields => 'your(fez)' + } + }) +}, 'blows up if an API does not exist'; + +done_testing; diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 8c457e3..920ea98 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -1,24 +1,13 @@ use strict; use warnings; -use Test::More; -use Cwd; +use Test2::V0; +use lib 't/lib'; +use TestTools qw/DEBUG gapi_json user/; use WebService::GoogleAPI::Client; -my $dir = getcwd; -my $DEBUG = $ENV{GAPI_DEBUG_LEVEL} || 0; ## to see noise of class debugging - -my $default_file = $ENV{ 'GOOGLE_TOKENSFILE' } || "$dir/../../gapi.json"; ## assumes running in a sub of the build dir by dzil -$default_file = "$dir/../gapi.json" unless -e $default_file; ## if file doesn't exist try one level up ( allows to run directly from t/ if gapi.json in parent dir ) - -#if running from root of the repo, grab the one from the t/ directory -$default_file = "$dir/t/gapi.json" unless -e $default_file; - -plan skip_all => 'No service configuration - set $ENV{GOOGLE_TOKENSFILE} or create gapi.json in dzil source root directory' unless -e $default_file; - -ok( my $gapi = WebService::GoogleAPI::Client->new( debug => $DEBUG, gapi_json => $default_file ), 'Creating test session instance of WebService::GoogleAPI::Client' ); - -$gapi->user('peter@shotgundriver.com'); +my $gapi = WebService::GoogleAPI::Client->new( + debug => DEBUG, gapi_json => gapi_json, user => user); my $options = { api_endpoint_id => 'drive.files.list', @@ -36,8 +25,6 @@ is $options->{path}, 'https://www.googleapis.com/drive/v3/files?fields=files%28id%2Cname%2Cparents%29', 'Can interpolate globally available query parameters'; -#TODO- make a test for a default param that should go into the -#query, like 'fields'. $options = { api_endpoint_id => "sheets:v4.spreadsheets.values.update", options => { @@ -72,75 +59,74 @@ is $options->{path}, 'https://sheets.googleapis.com/v4/spreadsheets/sner/values:batchGet?ranges=Sheet1%21A1%3AA2&ranges=Sheet1%21A3%3AB5', 'interpolates arrayref correctly' ; -subtest 'Testing {+param} type interpolation options' => sub { - plan skip_all => < sub { + # TODO - let's change this to make sure this check doesn't needa happen + return fail 'has the scopes', <has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); - my $interpolated = 'https://jobs.googleapis.com/v3/projects/sner/jobs'; + subtest 'Testing {+param} type interpolation options' => sub { + my $interpolated = 'https://jobs.googleapis.com/v3/projects/sner/jobs'; - $options = { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {name => 'projects/sner/jobs/bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options->{path}, "$interpolated/bler", - 'Interpolates a {+param} that matches the spec pattern'; + $options = { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {name => 'projects/sner/jobs/bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $options->{path}, "$interpolated/bler", + 'Interpolates a {+param} that matches the spec pattern'; - $options = - { api_endpoint_id => 'jobs.projects.jobs.list', - options => { parent => 'sner' } }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options->{path}, $interpolated, - 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; + $options = + { api_endpoint_id => 'jobs.projects.jobs.list', + options => { parent => 'sner' } }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $options->{path}, $interpolated, + 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projectsId => 'sner', jobsId => 'bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projectsId => 'sner', jobsId => 'bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options->{path}, "$interpolated/bler", - 'Interpolates params that match the flatName spec (camelCase)'; + is $options->{path}, "$interpolated/bler", + 'Interpolates params that match the flatName spec (camelCase)'; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projects_id => 'sner', jobs_id => 'bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => {projects_id => 'sner', jobs_id => 'bler'} }; + $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $options->{path}, "$interpolated/bler", - 'Interpolates params that match the names in the api description (snake_case)'; + is $options->{path}, "$interpolated/bler", + 'Interpolates params that match the names in the api description (snake_case)'; -}; + }; -my @errors; -subtest 'Checking for proper failure with {+params} in unsupported ways' => sub { - plan skip_all => <has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); + my @errors; + subtest 'Checking for proper failure with {+params} in unsupported ways' => sub { + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => { name => 'sner' } }; + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $errors[0], 'Not enough parameters given for {+name}.', + "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => { name => 'sner' } }; - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $errors[0], 'Not enough parameters given for {+name}.', - "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; + $options = + { api_endpoint_id => 'jobs.projects.jobs.delete', + options => { jobsId => 'sner' } }; + @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + is $errors[0], 'Missing a parameter for {projectsId}.', + "Fails if you don't supply enough values to fill the flatPath"; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => { jobsId => 'sner' } }; - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); - is $errors[0], 'Missing a parameter for {projectsId}.', - "Fails if you don't supply enough values to fill the flatPath"; + }; }; - - done_testing; diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index b69ae03..3d62572 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -232,6 +232,10 @@ subtest 'checking for API availablity' => sub { }; +# TODO - we need to deal with issues when you can't find a method. +# We had a bug once where google updated, but our cached discovery document +# didn't and we kept crashing b/c it couldn't find the old version that we were +# using (b/c we were relying on the default version, i think) done_testing; __DATA__ diff --git a/t/lib/TestTools.pm b/t/lib/TestTools.pm index 6fd614c..8fea609 100644 --- a/t/lib/TestTools.pm +++ b/t/lib/TestTools.pm @@ -4,33 +4,25 @@ use warnings; use Exporter::Shiny qw/ gapi_json DEBUG user has_credentials set_credentials/; use Mojo::File qw/curfile path/; +use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; my $gapi; #try and find a good gapi.json to use here. Check as follows: -# 1) first try whatever was set in the ENV variable -# 2) the current directory -# 3) the directory BELOW the main dir for the project, so dzil's -# author tests can find the one in our main folder -# 4) the main dir of this project -# 5) the fake one in the t/ directory -$gapi = path($ENV{GOOGLE_TOKENSFILE} || './gapi.json'); -$gapi = curfile->dirname->dirname->dirname->sibling('gapi.json') - unless $gapi->stat; -$gapi = curfile->dirname->dirname->sibling('gapi.json') - unless $gapi->stat; -$gapi = curfile->dirname->sibling('gapi.json') - unless $gapi->stat; +# for sanity, we only use the fake gapi.json in the t/ directory unless the user +# explicitly gives a GOOGLE_TOKENSFILE +$gapi = path($ENV{GOOGLE_TOKENSFILE} || curfile->dirname->sibling('gapi.json')); sub gapi_json { return "$gapi"; } -sub user { $ENV{GMAIL_FOR_TESTING} } +sub user { $ENV{GMAIL_FOR_TESTING} // 'peter@shotgundriver.com' } sub has_credentials { $gapi->stat && user } sub set_credentials { my ($obj) = @_; - $obj->ua->auth_storage->setup({ type => 'jsonfile', path => "$gapi" }); - $obj->ua->user(user) + my $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new( + path => "$gapi", user => user); + $obj->ua->auth_storage($storage); } From 10bedf56a7fbf38604d32e25baa67d87f8a10171 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Sun, 24 Jan 2021 19:40:03 +0200 Subject: [PATCH 50/68] some perltidy-ing and spelling errors in error messages --- TODO | 17 ++-- lib/WebService/GoogleAPI/Client.pm | 95 +++++++++++++------- lib/WebService/GoogleAPI/Client/UserAgent.pm | 2 +- 3 files changed, 73 insertions(+), 41 deletions(-) diff --git a/TODO b/TODO index 4453581..03f8763 100644 --- a/TODO +++ b/TODO @@ -1,19 +1,19 @@ # vim: ft=markdown -we're trying to take care of a bunch of things. # better support for service accounts We did a number on the architecture to allow for pluggable storage backends. I think some more improvement could be had, but hey, it's a start. -We need to make auth_storage in the ua make sure the attached object is a -consumer of our role. - - test this, and a lot of other stuff -We must document the two storage backends that we have. +- test that auth_storage only takes our subclasses + +- get a real test down which uses normal creds and uses service account creds + i guess to read rows from a spreadsheet (like we have in some $work code) -In the auth backends, make failure to get a token return the error attached to -it. +- test that there are sensible explosions when an endpoint does not exist + +We must document the two storage backends that we have. #Other Issues @@ -28,6 +28,9 @@ Maybe even go for dynamic class creation, similar to what OpenAPI producers use. Although I'm thinking of just moving over to using gRPC, which does actually have a perl client (seemingly). +It just hit me that it could be some of the slowness comes from checking that +you're authenticated for the request. + ## Encapsulate logic better diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index b418401..9217e60 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -450,56 +450,85 @@ sub api_query ################################################## ## _ensure_api_spec_has_defined_fields is really only used to allow carping without undef warnings if needed -sub _ensure_api_spec_has_defined_fields -{ - my ( $self, $api_discovery_struct ) = @_; +sub _ensure_api_spec_has_defined_fields { + my ($self, $api_discovery_struct) = @_; ## Ensure API Discovery has expected fields defined - foreach my $expected_key ( qw/path title ownerName version id discoveryVersion revision description documentationLink rest/ ) - { - $api_discovery_struct->{ $expected_key } = '' unless defined $api_discovery_struct->{ $expected_key }; + foreach my $expected_key (qw/path title ownerName version id discoveryVersion + revision description documentationLink rest/) { + $api_discovery_struct->{$expected_key} = '' + unless defined $api_discovery_struct->{$expected_key}; } - $api_discovery_struct->{ canonicalName } = $api_discovery_struct->{ title } unless defined $api_discovery_struct->{ canonicalName }; + $api_discovery_struct->{canonicalName} = $api_discovery_struct->{title} + unless defined $api_discovery_struct->{canonicalName}; return $api_discovery_struct; } ################################################## ################################################## -sub _process_params_for_api_endpoint_and_return_errors -{ - my ( $self, $params) = @_; ## nb - api_endpoint is a param - param key values are modified through this sub +sub _process_params_for_api_endpoint_and_return_errors { + ## nb - api_endpoint is a param - param key values are modified through this sub + my ($self, $params) = @_; - croak('this should never happen - this method is internal only!') unless defined $params->{ api_endpoint_id }; - - my $api_discovery_struct = $self->_ensure_api_spec_has_defined_fields( $self->discovery->get_api_document( $params->{ api_endpoint_id } ) ); ## $api_discovery_struct requried for service base URL - $api_discovery_struct->{ baseUrl } =~ s/\/$//sxmg; ## remove trailing '/' from baseUrl - - my $method_discovery_struct = $self->get_method_details( $params->{ api_endpoint_id } ); ## if can get discovery data for google api endpoint then continue to perform detailed checks + croak('this should never happen - this method is internal only!') + unless defined $params->{api_endpoint_id}; + + ## $api_discovery_struct requried for service base URL + my $api_discovery_struct = $self->_ensure_api_spec_has_defined_fields( + $self->discovery->get_api_document($params->{api_endpoint_id})); + ## remove trailing '/' from baseUrl + $api_discovery_struct->{baseUrl} =~ s/\/$//sxmg; + + my $method_discovery_struct = + $self->get_method_details($params->{api_endpoint_id}) + ; ## if can get discovery data for google api endpoint then continue to perform detailed checks #save away original path so we can know if it's fiddled with #later $method_discovery_struct->{origPath} = $method_discovery_struct->{path}; ## allow optional user callback pre-processing of method_discovery_struct - $method_discovery_struct = &{$params->{cb_method_discovery_modify}}($method_discovery_struct) if ( defined $params->{cb_method_discovery_modify} && ref( $params->{cb_method_discovery_modify} ) eq 'CODE' ); - - return ("Checking discovery of $params->{api_endpoint_id} method data failed - is this a valid end point") unless ( keys %{ $method_discovery_struct } > 0 ); + $method_discovery_struct = + &{ $params->{cb_method_discovery_modify} }($method_discovery_struct) + if (defined $params->{cb_method_discovery_modify} + && ref($params->{cb_method_discovery_modify}) eq 'CODE'); + + return ( +"Checking discovery of $params->{api_endpoint_id} method data failed - is this a valid end point" + ) unless (keys %{$method_discovery_struct} > 0); ## assertion: method discovery struct ok - or at least has keys - carp( "API Endpoint $params->{api_endpoint_id} discovered specification didn't include expected 'parameters' keyed HASH structure" ) unless ref( $method_discovery_struct->{ parameters } ) eq 'HASH'; - - my @teapot_errors = (); ## errors are pushed into this as encountered - $params->{ method } = $method_discovery_struct->{ httpMethod } || 'GET' if ( not defined $params->{ method } ); - push( @teapot_errors, "method mismatch - you requested a $params->{method} which conflicts with discovery spec requirement for $method_discovery_struct->{httpMethod}" ) if ( $params->{ method } !~ /^$method_discovery_struct->{httpMethod}$/sxim ); - push( @teapot_errors, "Client Credentials do not include required scope to access $params->{api_endpoint_id}" ) unless $self->has_scope_to_access_api_endpoint( $params->{ api_endpoint_id } ); ## ensure user has required scope access - $params->{ path } = $method_discovery_struct->{ path } unless $params->{ path }; ## Set default path iff not set by user - NB - will prepend baseUrl later - push @teapot_errors, 'path is a required parameter' unless $params->{ path }; - - push @teapot_errors, $self->_interpolate_path_parameters_append_query_params_and_return_errors( $params, $method_discovery_struct ); - - $params->{ path } =~ s/^\///sxmg; ## remove leading '/' from path - $params->{ path } = "$api_discovery_struct->{baseUrl}/$params->{path}" unless $params->{ path } =~ /^$api_discovery_struct->{baseUrl}/ixsmg; ## prepend baseUrl if required + carp( +"API Endpoint $params->{api_endpoint_id} discovered specification didn't include expected 'parameters' keyed HASH structure" + ) unless ref($method_discovery_struct->{parameters}) eq 'HASH'; + + my @teapot_errors = (); ## errors are pushed into this as encountered + $params->{method} = $method_discovery_struct->{httpMethod} || 'GET' + if (not defined $params->{method}); + push(@teapot_errors, +"method mismatch - you requested a $params->{method} which conflicts with discovery spec requirement for $method_discovery_struct->{httpMethod}" + ) if ($params->{method} !~ /^$method_discovery_struct->{httpMethod}$/sxim); + push(@teapot_errors, +"Client Credentials do not include required scope to access $params->{api_endpoint_id}" + ) + unless $self->has_scope_to_access_api_endpoint($params->{api_endpoint_id}) + ; ## ensure user has required scope access + $params->{path} = $method_discovery_struct->{path} + unless $params->{path} + ; ## Set default path iff not set by user - NB - will prepend baseUrl later + push @teapot_errors, 'path is a required parameter' unless $params->{path}; + + push @teapot_errors, + $self->_interpolate_path_parameters_append_query_params_and_return_errors( + $params, $method_discovery_struct); + + $params->{path} =~ s/^\///sxmg; ## remove leading '/' from path + $params->{path} = "$api_discovery_struct->{baseUrl}/$params->{path}" + unless $params->{path} =~ + /^$api_discovery_struct->{baseUrl}/ixsmg; ## prepend baseUrl if required ## if errors - add detail available in the discovery struct for the method and service to aid debugging - push @teapot_errors, qq{ $api_discovery_struct->{title} $api_discovery_struct->{rest} API into $api_discovery_struct->{ownerName} $api_discovery_struct->{canonicalName} $api_discovery_struct->{version} with id $method_discovery_struct->{id} as described by discovery document version $api_discovery_struct->{discoveryVersion} revision $api_discovery_struct->{revision} with documentation at $api_discovery_struct->{documentationLink} \nDescription: $method_discovery_struct->{description}\n} if @teapot_errors; + push @teapot_errors, +qq{ $api_discovery_struct->{title} $api_discovery_struct->{rest} API into $api_discovery_struct->{ownerName} $api_discovery_struct->{canonicalName} $api_discovery_struct->{version} with id $method_discovery_struct->{id} as described by discovery document version $api_discovery_struct->{discoveryVersion} revision $api_discovery_struct->{revision} with documentation at $api_discovery_struct->{documentationLink} \nDescription: $method_discovery_struct->{description}\n} + if @teapot_errors; return @teapot_errors; } diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 1cb5dad..44b4028 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -73,7 +73,7 @@ sub header_with_bearer_auth_token { } else { # TODO - why is this not fatal? carp -"Can't build Auth header, couldn't get an access token. Is you AuthStorage set up correctly?"; +"Can't build Auth header, couldn't get an access token. Is your AuthStorage set up correctly?"; } return $headers; } From 0d6c1dac071f65cfe17e4bf019a5c9d18436667a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Mon, 25 Jan 2021 16:37:41 +0200 Subject: [PATCH 51/68] shorten long name to _process_params --- TODO | 2 + lib/WebService/GoogleAPI/Client.pm | 71 ++++++++++++----------- t/04-no-method.t | 4 +- t/05-spec-interpolation.t | 14 ++--- t/WebService/GoogleAPI/Client/Discovery.t | 4 +- 5 files changed, 49 insertions(+), 46 deletions(-) diff --git a/TODO b/TODO index 03f8763..f7d7dce 100644 --- a/TODO +++ b/TODO @@ -15,6 +15,8 @@ I think some more improvement could be had, but hey, it's a start. We must document the two storage backends that we have. +# add support for idiomatic boolean values + #Other Issues ## Less logic on repeated calls diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 9217e60..ef2d65d 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -402,48 +402,45 @@ Returns L object ## NB - uses the ua api_query to execute the server request ################################################## -sub api_query -{ - my ( $self, @params_array ) = @_; +sub api_query { + my ($self, @params_array) = @_; ## TODO - find a more elgant idiom to do this - pulled this off top of head for quick imeplementation my $params = {}; - if ( scalar( @params_array ) == 1 && ref( $params_array[0] ) eq 'HASH' ) - { + if (scalar(@params_array) == 1 && ref($params_array[0]) eq 'HASH') { $params = $params_array[0]; + } else { + $params = {@params_array}; ## what happens if not even count } - else - { - $params = { @params_array }; ## what happens if not even count - } - carp( pp $params) if $self->debug > 10; - + carp(pp $params) if $self->debug > 10; - my @teapot_errors = (); ## used to collect pre-query validation errors - if set we return a response with 418 I'm a teapot - @teapot_errors = $self->_process_params_for_api_endpoint_and_return_errors( $params ) if ( defined $params->{ api_endpoint_id } ); ## ## pre-query validation if api_id parameter is included + ## used to collect pre-query validation errors - if set we return a response + # with 418 I'm a teapot + my @teapot_errors = (); + ## pre-query validation if api_id parameter is included + @teapot_errors = $self->_process_params($params) + if (defined $params->{api_endpoint_id}); - - if ( not defined $params->{ path } ) ## either as param or from discovery - { + ## either as param or from discovery + if (not defined $params->{path}) { push @teapot_errors, 'path is a required parameter'; $params->{path} = ''; } - push @teapot_errors, "Path '$params->{path}' includes unfilled variable after processing" if ( $params->{ path } =~ /\{.+\}/xms ) ; - - if ( @teapot_errors > 0 ) ## carp and include in 418 TEAPOT ERROR - response body with @teapot errors - { - carp( join( "\n", @teapot_errors ) ) if $self->debug; + push @teapot_errors, "Path '$params->{path}' includes unfilled variable after processing" + if ($params->{path} =~ /\{.+\}/xms); + ## carp and include in 418 TEAPOT ERROR - response body with @teapot errors + if (@teapot_errors > 0) { + carp(join("\n", @teapot_errors)) if $self->debug; return Mojo::Message::Response->new( content_type => 'text/plain', code => 418, - message => 'Teapot Error - Reqeust blocked before submitting to server with pre-query validation errors', - body => join( "\n", @teapot_errors ) + message => +'Teapot Error - Reqeust blocked before submitting to server with pre-query validation errors', + body => join("\n", @teapot_errors) ); - } - else ## query looks good - send to user agent to execute - { - #print pp $params; - return $self->ua->validated_api_query( $params ); + } else { + ## query looks good - send to user agent to execute + return $self->ua->validated_api_query($params); } } ################################################## @@ -466,6 +463,11 @@ sub _ensure_api_spec_has_defined_fields { ################################################## sub _process_params_for_api_endpoint_and_return_errors { + warn '_process_params_for_api_endpoint_and_return_errors has been deprecated. Please use _process_params'; + _process_params(@_); +} + +sub _process_params { ## nb - api_endpoint is a param - param key values are modified through this sub my ($self, $params) = @_; @@ -477,10 +479,10 @@ sub _process_params_for_api_endpoint_and_return_errors { $self->discovery->get_api_document($params->{api_endpoint_id})); ## remove trailing '/' from baseUrl $api_discovery_struct->{baseUrl} =~ s/\/$//sxmg; - - my $method_discovery_struct = - $self->get_method_details($params->{api_endpoint_id}) - ; ## if can get discovery data for google api endpoint then continue to perform detailed checks + + ## if can get discovery data for google api endpoint then continue to perform + # detailed checks + my $method_discovery_struct = $self->get_method_details($params->{api_endpoint_id}); #save away original path so we can know if it's fiddled with #later @@ -492,9 +494,8 @@ sub _process_params_for_api_endpoint_and_return_errors { if (defined $params->{cb_method_discovery_modify} && ref($params->{cb_method_discovery_modify}) eq 'CODE'); - return ( -"Checking discovery of $params->{api_endpoint_id} method data failed - is this a valid end point" - ) unless (keys %{$method_discovery_struct} > 0); + croak "Can't get data for $params->{api_endpoint_id}- is this a valid end point?" + unless (keys %{$method_discovery_struct} > 0); ## assertion: method discovery struct ok - or at least has keys carp( "API Endpoint $params->{api_endpoint_id} discovered specification didn't include expected 'parameters' keyed HASH structure" diff --git a/t/04-no-method.t b/t/04-no-method.t index 2b9a74f..51508ef 100644 --- a/t/04-no-method.t +++ b/t/04-no-method.t @@ -7,7 +7,7 @@ my $gapi = WebService::GoogleAPI::Client->new( debug => DEBUG, gapi_json => gapi_json, user => user); ok dies { - $gapi->_process_params_for_api_endpoint_and_return_errors({ + $gapi->_process_params({ api_endpoint_id => 'jobs.non.existant', options => { fields => 'your(fez)' @@ -16,7 +16,7 @@ ok dies { }, 'blows up if an endpoint does not exist'; ok dies { - $gapi->_process_params_for_api_endpoint_and_return_errors({ + $gapi->_process_params({ api_endpoint_id => 'i.am.non.existant', options => { fields => 'your(fez)' diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index 920ea98..b8a92fb 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -17,7 +17,7 @@ my $options = { }; sub build_req { - $gapi->_process_params_for_api_endpoint_and_return_errors(shift); + $gapi->_process_params(shift); } build_req($options); @@ -75,21 +75,21 @@ MSG $options = { api_endpoint_id => 'jobs.projects.jobs.delete', options => {name => 'projects/sner/jobs/bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", 'Interpolates a {+param} that matches the spec pattern'; $options = { api_endpoint_id => 'jobs.projects.jobs.list', options => { parent => 'sner' } }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $gapi->_process_params( $options ); is $options->{path}, $interpolated, 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; $options = { api_endpoint_id => 'jobs.projects.jobs.delete', options => {projectsId => 'sner', jobsId => 'bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", 'Interpolates params that match the flatName spec (camelCase)'; @@ -97,7 +97,7 @@ MSG $options = { api_endpoint_id => 'jobs.projects.jobs.delete', options => {projects_id => 'sner', jobs_id => 'bler'} }; - $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", 'Interpolates params that match the names in the api description (snake_case)'; @@ -111,14 +111,14 @@ MSG $options = { api_endpoint_id => 'jobs.projects.jobs.delete', options => { name => 'sner' } }; - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + @errors = $gapi->_process_params( $options ); is $errors[0], 'Not enough parameters given for {+name}.', "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; $options = { api_endpoint_id => 'jobs.projects.jobs.delete', options => { jobsId => 'sner' } }; - @errors = $gapi->_process_params_for_api_endpoint_and_return_errors( $options ); + @errors = $gapi->_process_params( $options ); is $errors[0], 'Missing a parameter for {projectsId}.', "Fails if you don't supply enough values to fill the flatPath"; diff --git a/t/WebService/GoogleAPI/Client/Discovery.t b/t/WebService/GoogleAPI/Client/Discovery.t index 3d62572..bc799b2 100755 --- a/t/WebService/GoogleAPI/Client/Discovery.t +++ b/t/WebService/GoogleAPI/Client/Discovery.t @@ -218,8 +218,8 @@ subtest 'checking for API availablity' => sub { { api => 'gmail', version => 'v1' }, 'got default from hashref'; - is $disco->process_api_version('gmail:v2'), - { api => 'gmail', version => 'v2' }, + is $disco->process_api_version('gmail:v9000'), + { api => 'gmail', version => 'v9000' }, 'take a version if given (even if imaginary)'; is $disco->process_api_version({ api => 'gmail', version => 'v2' }), From ff1b252bdd302ef4a68be5e65d353861113d6fe8 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Mon, 25 Jan 2021 16:44:50 +0200 Subject: [PATCH 52/68] some notes on idiomaticy --- TODO | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index f7d7dce..aadd885 100644 --- a/TODO +++ b/TODO @@ -15,7 +15,13 @@ I think some more improvement could be had, but hey, it's a start. We must document the two storage backends that we have. -# add support for idiomatic boolean values +# Make this more idiomatic + +Allow users to make requests like this + $gapi->api_query('drive.files.list', $options) + +Allow boolean values to be coerced from truthy or falsy values (except for +explicit false) #Other Issues From a10aa4989071091d137f3ea0576cbee9ada0b3e0 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 11:40:07 +0200 Subject: [PATCH 53/68] rename ::AuthStorage::ConfigJSON to ::AuthStorage::GapiJSON - better reflects that its a reader for a particular file type --- lib/WebService/GoogleAPI/Client.pm | 4 ++-- lib/WebService/GoogleAPI/Client/AuthStorage.pm | 2 +- .../Client/AuthStorage/{ConfigJSON.pm => GapiJSON.pm} | 2 +- lib/WebService/GoogleAPI/Client/UserAgent.pm | 4 ++-- t/lib/TestTools.pm | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename lib/WebService/GoogleAPI/Client/AuthStorage/{ConfigJSON.pm => GapiJSON.pm} (97%) mode change 100755 => 100644 diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index ef2d65d..05e33e9 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -7,7 +7,7 @@ use Data::Dump qw/pp/; use Moo; use WebService::GoogleAPI::Client::UserAgent; use WebService::GoogleAPI::Client::Discovery; -use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; +use WebService::GoogleAPI::Client::AuthStorage::GapiJSON; use WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; use Carp; use CHI; @@ -235,7 +235,7 @@ sub BUILD { if ($params->{auth_storage}) { $storage = $params->{auth_storage}; } elsif ($file = $params->{gapi_json}) { - $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new(path => $file); + $storage = WebService::GoogleAPI::Client::AuthStorage::GapiJSON->new(path => $file); } elsif ($file = $params->{service_account}) { $storage = WebService::GoogleAPI::Client::AuthStorage::ServiceAccount->new( path => $file, scopes => $params->{scopes}); diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index c7c63d3..48e98af 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -24,7 +24,7 @@ use WebService::GoogleAPI::Client::AccessToken; ... # and now your class manages the access_tokens WebService::GoogleAPI::Client::AuthStorage is a Moo::Role for auth storage backends. -This dist comes with two consumers, L +This dist comes with two consumers, L and L. See those for more info on how you can use them with L. diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm old mode 100755 new mode 100644 similarity index 97% rename from lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm rename to lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm index 051b24e..e7b1aa2 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ConfigJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm @@ -1,6 +1,6 @@ use strictures; -package WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; +package WebService::GoogleAPI::Client::AuthStorage::GapiJSON; # ABSTRACT: Specific methods to fetch tokens from JSON data sources diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 44b4028..4dc0f4e 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -8,7 +8,7 @@ use Moo; extends 'Mojo::UserAgent'; #extends 'Mojo::UserAgent::Mockable'; -use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; +use WebService::GoogleAPI::Client::AuthStorage::GapiJSON; use Mojo::UserAgent; use Data::Dump qw/pp/; # for dev debug @@ -19,7 +19,7 @@ has 'debug' => ( is => 'rw', default => 0 ); has 'auth_storage' => ( is => 'rw', default => sub { - WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new + WebService::GoogleAPI::Client::AuthStorage::GapiJSON->new }, handles => [qw/get_access_token scopes user/], trigger => 1, diff --git a/t/lib/TestTools.pm b/t/lib/TestTools.pm index 8fea609..8258ee7 100644 --- a/t/lib/TestTools.pm +++ b/t/lib/TestTools.pm @@ -4,7 +4,7 @@ use warnings; use Exporter::Shiny qw/ gapi_json DEBUG user has_credentials set_credentials/; use Mojo::File qw/curfile path/; -use WebService::GoogleAPI::Client::AuthStorage::ConfigJSON; +use WebService::GoogleAPI::Client::AuthStorage::GapiJSON; my $gapi; #try and find a good gapi.json to use here. Check as follows: @@ -20,7 +20,7 @@ sub user { $ENV{GMAIL_FOR_TESTING} // 'peter@shotgundriver.com' } sub has_credentials { $gapi->stat && user } sub set_credentials { my ($obj) = @_; - my $storage = WebService::GoogleAPI::Client::AuthStorage::ConfigJSON->new( + my $storage = WebService::GoogleAPI::Client::AuthStorage::GapiJSON->new( path => "$gapi", user => user); $obj->ua->auth_storage($storage); } From b76a12647c4a7a4c05c2ccdc6b2c2c5496ac582d Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 11:54:33 +0200 Subject: [PATCH 54/68] document ::AuthStorage::GapiJSON.pm --- README.txt | 2 +- .../GoogleAPI/Client/AccessToken.pm | 2 +- .../GoogleAPI/Client/AuthStorage/GapiJSON.pm | 69 ++++++++++++++++--- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/README.txt b/README.txt index c7a496a..7007727 100644 --- a/README.txt +++ b/README.txt @@ -406,7 +406,7 @@ AUTHORS COPYRIGHT AND LICENSE - This software is Copyright (c) 2017-2020 by Peter Scott and others. + This software is Copyright (c) 2017-2021 by Peter Scott and others. This is free software, licensed under: diff --git a/lib/WebService/GoogleAPI/Client/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm index ad1ee04..75f83bf 100644 --- a/lib/WebService/GoogleAPI/Client/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -1,6 +1,6 @@ package WebService::GoogleAPI::Client::AccessToken; -# ABSTRACT - A small class for bundling user and scopes with a token +# ABSTRACT: A small class for bundling user and scopes with a token use Moo; use overload '""' => sub { shift->token }; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm index e7b1aa2..8e423bd 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm @@ -2,7 +2,7 @@ use strictures; package WebService::GoogleAPI::Client::AuthStorage::GapiJSON; -# ABSTRACT: Specific methods to fetch tokens from JSON data sources +# ABSTRACT: Auth Storage Backend based on gapi.json use Moo; use Config::JSON; @@ -10,8 +10,31 @@ use Carp; with 'WebService::GoogleAPI::Client::AuthStorage'; +=head1 SYNOPSIS + +This class provides an auth backend for gapi.json files produced with the provided L +script. This is used for user credentials. For service accounts, please see L. + +In future versions, I hope to provide the functionality of L as a +L, so you can provide this flow in your app rather than having to run it offline. + +This class mixes in L, and provides +all attributes and methods from that role. As noted there, the C is usually managed by +the L object this is set on. + +=attr path + +The location of the gapi.json file. Default to gapi.json in the current directory. + +=cut has 'path' => ( is => 'rw', default => './gapi.json' ); # default is gapi.json +=attr tokensfile + +A Config::JSON object that contains the parsed gapi.json file. Authomatically set +at object instantiation. + +=cut has 'tokensfile' => ( is => 'rw' ); # Config::JSON object pointer # NOTE- this type of class has getters and setters b/c the implementation of @@ -29,6 +52,29 @@ NOCLIENT return $self; } +=method get_access_token + +Returns the access token for the current user. + +=cut + +sub get_access_token { + my ($self) = @_; + my $value = $self->get_from_storage('access_token'); + return $value +} + +=method refresh_access_token + +This will refresh the access token for the currently set C. If you don't +have a refresh token for that user, it will die with the following message. + +If your credentials are missing the refresh_token - consider removing the auth at +https://myaccount.google.com/permissions as The oauth2 server will only ever mint one refresh +token at a time, and if you request another access token via the flow it will operate as if +you only asked for an access token. + +=cut sub refresh_access_token { my ($self) = @_; my %p = map { ( $_ => $self->get_from_storage($_) ) } @@ -63,6 +109,12 @@ sub get_token_emails_from_storage { return [keys %$tokens]; } +=method get_from_storage + +A method to get stored fields from the gapi.json file. Will retrieve tokens for +the current user, and other fields from the global config. + +=cut sub get_from_storage { my ($self, $key) = @_; if ($key =~ /_token/) { @@ -72,12 +124,6 @@ sub get_from_storage { } } -sub get_access_token { - my ($self) = @_; - my $value = $self->get_from_storage('access_token'); - return $value -} - sub get_scopes_from_storage_as_array { carp 'get_scopes_from_storage_as_array is being deprecated, please use the more succint scopes accessor'; return $_[0]->scopes @@ -85,8 +131,15 @@ sub get_scopes_from_storage_as_array { # NOTE - the scopes are stored as a space seperated list, and this method # returns an arrayref +# +=method scopes + +Read-only accessor returning the list of scopes configured in the gapi.json file. +=cut + sub scopes { my ($self) = @_; return [split / /, $self->tokensfile->get('gapi/scopes')]; } -1; + +9011; From f01e8c921b65c1be49c1304fc8b90359c622e64e Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 11:57:00 +0200 Subject: [PATCH 55/68] finish doc-ing GapiJSON.pm --- lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm index 8e423bd..a7c1f0f 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm @@ -66,8 +66,10 @@ sub get_access_token { =method refresh_access_token -This will refresh the access token for the currently set C. If you don't -have a refresh token for that user, it will die with the following message. +This will refresh the access token for the currently set C. Will write the +new token back into the gapi.json file. + +If you don't have a refresh token for that user, it will die with the following message: If your credentials are missing the refresh_token - consider removing the auth at https://myaccount.google.com/permissions as The oauth2 server will only ever mint one refresh From 3928c836f1695576b678d53dc9ddab2f83d37863 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 14:39:26 +0200 Subject: [PATCH 56/68] start keeping Changes manually with ::Plugin::NextRelease --- Changes | 5 +++++ dist.ini | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 Changes diff --git a/Changes b/Changes new file mode 100644 index 0000000..510de96 --- /dev/null +++ b/Changes @@ -0,0 +1,5 @@ +{{$NEXT}} + +ENHANCEMENTS +- add support for service accounts +- rework storage backend to allow custom implementations (here's looking at you, redis) diff --git a/dist.ini b/dist.ini index ad50f72..6909586 100755 --- a/dist.ini +++ b/dist.ini @@ -81,12 +81,7 @@ copy = LICENSE repository = https://github.com/rabbiveesh/WebService-GoogleAPI-Client issues = 1 -[ChangelogFromGit] -max_age = 14 -tag_regexp = ^v(\d+\.\d+)$ -file_name = CHANGES -wrap_column = 74 -debug = 0 +[NextRelease] [Git::CommitBuild] [Test::Perl::Critic] From 20d6b4f71c0fba3878a1b7ed14ccca078677698c Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 14:39:43 +0200 Subject: [PATCH 57/68] document ::AuthStorage::ServiceAccount.pm --- TODO | 7 +- lib/WebService/GoogleAPI/Client.pm | 15 +-- .../Client/AuthStorage/ServiceAccount.pm | 92 ++++++++++++++++--- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/TODO b/TODO index aadd885..43af93e 100644 --- a/TODO +++ b/TODO @@ -1,11 +1,14 @@ # vim: ft=markdown +# change Changes file + +I think i'll use whatever is used in [@Starter]'s managed_versions + # better support for service accounts We did a number on the architecture to allow for pluggable storage backends. I think some more improvement could be had, but hey, it's a start. - - test that auth_storage only takes our subclasses - get a real test down which uses normal creds and uses service account creds @@ -13,7 +16,7 @@ I think some more improvement could be had, but hey, it's a start. - test that there are sensible explosions when an endpoint does not exist -We must document the two storage backends that we have. +fix docs for goauth # Make this more idiomatic diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 05e33e9..bc2dbcc 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -35,12 +35,12 @@ use Mojo::Util; =head1 SYNOPSIS -Access Google API Services Version 1 using an OAUTH2 User Agent. +Access Google API Services using an OAUTH2 User Agent. -Includes Discovery, validation authentication and API Access. +Includes Discovery, validation, authentication and API Access. -assumes gapi.json configuration in working directory with scoped Google project -credentials and user authorization created by _goauth_ +By default assumes gapi.json configuration in working directory with scoped Google project +credentials and user authorization created by L. use WebService::GoogleAPI::Client; @@ -55,9 +55,12 @@ credentials and user authorization created by _goauth_ } -Internal User Agent provided be property WebService::GoogleAPI::Client::UserAgent dervied from Mojo::UserAgent +Package includes L CLI Script to collect initial end-user authorisation +to scoped services. -Package includes I CLI Script to collect initial end-user authorisation to scoped services +Note to intrepid hackers: Any method that isn't documented is considered +private, and subject to change in breaking ways without notice. (Although I'm a +pretty nice guy, and probably will leave a warning or something). =head1 EXAMPLES diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 5df25d9..6221773 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -3,10 +3,35 @@ package WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; # ABSTRACT: Manage access tokens from a service account file +=head1 SYNOPSIS + +This class provides an auth backend for service account files downloaded from +your google cloud console. For user accounts, please see +L. + +This backend is only for explicitly passing a service account JSON file. It will +not attempt to find one by itself, or to do the Application Default Credentials, +at least yet. + +This backend will cache tokens in memory for any set of scopes requested, and +for any user you ask to impersonate (more on that later). + +This class mixes in L, and provides +all attributes and methods from that role. As noted there, the C is usually managed by +the L object this is set on. + +=cut + use Moo; use Carp; use Mojo::JWT::Google; +=attr scopes + +A read/write attribute containing the scopes the service account is asking access to. +Will coerce a space seperated list of scopes into the required arrayref of scopes. + +=cut has scopes => is => 'rw', coerce => sub { @@ -16,6 +41,13 @@ has scopes => }, default => sub { [] }; +=attr user + +The user you want to impersonate. Defaults to the empty string, which signifies +no user. In order to impersonate a user, you need to have domain-wide delegation +set up for the service account. + +=cut has user => is => 'rw', coerce => sub { $_[0] || '' }, @@ -23,32 +55,50 @@ has user => with 'WebService::GoogleAPI::Client::AuthStorage'; +=attr path + +The location of the file containing the service account credentials. This is +downloaded from your google cloud console's service account page. + +=cut has path => is => 'rw', required => 1, trigger => 1; +sub _trigger_path { + my ($self) = @_; + $self->jwt( + Mojo::JWT::Google->new(from_json => $self->path) + ); +} + + +=attr jwt + +An instance of Mojo::JWT::Google used for retrieving the access tokens. This is +built whenever the C attribute is set. + +=cut has jwt => is => 'rw'; -# we keep record of the tokens we've gotten so far +=attr tokens + +A hash for caching the tokens that have been retrieved by this object. It caches on +scopes (via the C method) and then user. + +=cut has tokens => is => 'ro', default => sub { {} }; +=method get_access_token -sub _trigger_path { - my ($self) = @_; - $self->jwt( - Mojo::JWT::Google->new(from_json => $self->path) - ); -} - -sub scopes_string { - my ($self) = @_; - return join ' ', @{$self->scopes} -} +Return an access token for the current user (if any) and scopes from the cache if exists, +otherwise retrieve a new token with C. +=cut sub get_access_token { my ($self) = @_; my $token = $self->tokens->{$self->scopes_string}{$self->user}; @@ -56,6 +106,12 @@ sub get_access_token { return $token } +=method refresh_access_token + +Retrieve an access token for the current user (if any) and scopes from Google's auth API, +using a JWT. + +=cut sub refresh_access_token { my ($self) = @_; croak "Can't get a token without a set of scopes" unless @{$self->scopes}; @@ -82,4 +138,16 @@ sub refresh_access_token { return $new_token } +=method scopes_string + +Return the list of scopes as a space seperated string. + +=cut + +sub scopes_string { + my ($self) = @_; + return join ' ', @{$self->scopes} +} + + 9001 From 9199fda528890177079709928611469a812673f1 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 15:11:16 +0200 Subject: [PATCH 58/68] fix up docs for goauth --- Changes | 2 ++ README.txt | 19 +++++----- TODO | 4 --- bin/goauth | 102 ++++++++++++++++++++++++++++++----------------------- 4 files changed, 71 insertions(+), 56 deletions(-) diff --git a/Changes b/Changes index 510de96..b8927d9 100644 --- a/Changes +++ b/Changes @@ -3,3 +3,5 @@ ENHANCEMENTS - add support for service accounts - rework storage backend to allow custom implementations (here's looking at you, redis) + +BREAKING CHANGES diff --git a/README.txt b/README.txt index 7007727..177a6cd 100644 --- a/README.txt +++ b/README.txt @@ -8,12 +8,13 @@ VERSION SYNOPSIS - Access Google API Services Version 1 using an OAUTH2 User Agent. + Access Google API Services using an OAUTH2 User Agent. - Includes Discovery, validation authentication and API Access. + Includes Discovery, validation, authentication and API Access. - assumes gapi.json configuration in working directory with scoped Google - project credentials and user authorization created by _goauth_ + By default assumes gapi.json configuration in working directory with + scoped Google project credentials and user authorization created by + goauth. use WebService::GoogleAPI::Client; @@ -27,11 +28,13 @@ SYNOPSIS say 'User has Access to GMail Method End-Point gmail.users.settings.sendAs.get'; } - Internal User Agent provided be property - WebService::GoogleAPI::Client::UserAgent dervied from Mojo::UserAgent - Package includes goauth CLI Script to collect initial end-user - authorisation to scoped services + authorisation to scoped services. + + Note to intrepid hackers: Any method that isn't documented is + considered private, and subject to change in breaking ways without + notice. (Although I'm a pretty nice guy, and probably will leave a + warning or something). EXAMPLES diff --git a/TODO b/TODO index 43af93e..de7406e 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,5 @@ # vim: ft=markdown -# change Changes file - -I think i'll use whatever is used in [@Starter]'s managed_versions - # better support for service accounts We did a number on the architecture to allow for pluggable storage backends. diff --git a/bin/goauth b/bin/goauth index 1c90958..815ad92 100755 --- a/bin/goauth +++ b/bin/goauth @@ -22,8 +22,9 @@ use Crypt::JWT qw(decode_jwt); =head2 SUMMARY -Supports multiple users -OAuth2 for Google. You can find the key (CLIENT ID) and secret (CLIENT SECRET) from the app console here under "APIs & Auth" and "Credentials" in the menu at https://console.developers.google.com/project . +Supports multiple users OAuth2 for Google. You can find the key (CLIENT ID) and +secret (CLIENT SECRET) from the app console here under "APIs & Auth" and +"Credentials" in the menu at https://console.developers.google.com/project. Included as a utility within the CPAN L Bundle. @@ -38,39 +39,68 @@ Optionally you can provide an alternate filename to the default gapi.json as a p goauth my_differently_named_gapi.json -Once installed as part of the WebService::GoogleAPI::Client bundle, this tool can be run from the command line to configure a Google Project to access a Project -configuration that allows authenticated and authorised users to grant permission to access Google API services on their data (Email, Files etc) to the extent -provided by the Access Scopes for the project and the auth tokens. +Once installed as part of the WebService::GoogleAPI::Client bundle, this tool +can be run from the command line to configure a Google Project to access a +Project configuration that allows authenticated and authorised users to grant +permission to access Google API services on their data (Email, Files etc) to the +extent provided by the Access Scopes for the project and the auth tokens. -In order to successfully run _goauth_ for the first time requires the following Google Project configuration variables: -Client ID -Client Secret -List of Scopes to request access to on behalf of users ( must be a subset of those enabled for the project. ) +In order to successfully run C for the first time requires the following +Google Project configuration variables: -If not already available in the gapi.json file, you will be prompted to supply these as the file is created. +=begin :list -Once this configuration is complete, the goauth tool will launch a mini HTTP server to provide an interface to request users to authorise your project application. += * Client ID -NB: To run successfully your Google Project must be configured to accept requests from the domain. For initial testing ensure that you include localhost in your allowed domains. += * Client Secret -Once you have succesfully created a gapi.json file and ahve authenticated a user that is represented in this file then use WebServices::Google::Client -to start making Google API Requests. += * List of Scopes to request access to on behalf of users ( must be a subset of those enabled for the project. ) - perldoc WebServices::Google::Client for more detail. +=end :list +You must also add whatever URL you end up accessing C from as a valid +Redirect URI in your Google Cloud Console. You can get there from +L, and then clicking edit on +the OAuth Client ID that you're using for this project. -=head2 gapi.json +You need to add something like + + http://localhost:3001/ + +with the port number set to whichever C ends up picking. (This is +assuming that you typed C in your browser when you opened +it, it won't work if you typed in C<127.0.0.1:3001>). + +If not already available in the gapi.json file, you will be prompted to supply +these as the file is created. + +Once this configuration is complete, the C tool will launch a mini HTTP +server to provide an interface to request users to authorise your project +application. -The ultimate output of this is the gapi.json file that contains both the Google Project Specification as well as the authorised user access tokens. -The file describes a set of scopes that must all be configured as available to the Project through the Google Admin Console. -You may have multiple gapi.json files for the same project containing a different subset of scopes. -The gapi.json file also contains the authorisation tokens granted by users. Multiple users can be described within a single gapi.json file. -If users exist across multiple gapi.json files for the same project then (I believe) only the most recently granted set of scopes will be usable. +Once you have succesfully created a gapi.json file and have authenticated a user +that is represented in this file then you can start making Google API Requests. -The user can revoke permissions granted to a project ( Application ) by visiting https://myaccount.google.com/permissions +See L for more detail. -This file can be used to access Google API Services using the WebService::Google:API:Client Google API Client Library. +=head2 gapi.json + +The ultimate output of this is the gapi.json file that contains both the Google +Project Specification as well as the authorised user access tokens. The file +describes a set of scopes that must all be configured as available to the +Project through the Google Admin Console. You may have multiple gapi.json files +for the same project containing a different subset of scopes. The gapi.json +file also contains the authorisation tokens granted by users. Multiple users can +be described within a single gapi.json file. If users exist across multiple +gapi.json files for the same project then (I believe) only the most recently +granted set of scopes will be usable. + +The user can revoke permissions granted to a project ( Application ) by visiting +L. + +This file can be used to access Google API Services using the +WebService::Google:API:Client Google API Client Library. =head2 References @@ -86,17 +116,15 @@ This file can be used to access Google API Services using the WebService::Google my $filename = $ARGV[0] || 'gapi.json'; my $file = file( $filename ); -if ( $file->stat() ) ## file exists +if ($file->stat()) ## file exists { - say 'File ' , $file->absolute() . ' exists'; - input_if_not_exists( ['gapi/client_id', 'gapi/client_secret', 'gapi/scopes'] ); ## this potentially allows mreging with a json file with data external + say 'File ', $file->absolute() . ' exists'; + input_if_not_exists(['gapi/client_id', 'gapi/client_secret', 'gapi/scopes']); + ## this potentially allows mreging with a json file with data external ## to the app or to augment missing scope from file generated from ## earlier versions of goauth from other libs runserver(); -} -else -{ - +} else { setup(); runserver(); } @@ -243,7 +271,6 @@ sub runserver } }; #delete $static->extra->{'favicon.ico'}; ## look into https://mojolicious.org/perldoc/Mojolicious/Static with view to repalcing default favicon - app->secrets( ['putyourownsecretcookieseedhereforsecurity' . time] ); ## NB persistence cookies not required beyond server run app->start( 'daemon', '-l', "http://*:$port" ); return 1; } @@ -265,19 +292,6 @@ sub _stdin } -=head2 TODO: Improve user interface of the HTML templates beneath DATA section - -=over 1 - -=item More Testing and documentation - explore cases where refresh token is not returned because previous token was issued - -=item Describe alternative User Agent approaches to WebServices::Google::Client - -=item Explore service accounts and possible inclusion into gapi.json - -=back - -=cut # data:image/vnd.microsoft.icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAAAAAAAAAAAACMhSAAAAGUBS04uKWNqJXxyeiK+d4Af2XeAINhweCK4YWcmckdJMCEAAP8AGRdQAAAAAAAAAAAAAAAAAERFMwA1NToKY2kmbH6HH9uOmR39lKAc/5WhHP+VoRz/k58c/42YHfx7hCDSX2QoXSgmQQY8PDgAAAAAAERGNwAzMj4KaXAkjIqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/laEc/4eRHvRlayV4HRhMBT8/PQD//wAAZGonboqVHfmWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/h5Ee819kKlmLmAUATlEzLH+IINuWohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP96gyHLRkc5G2VrJX+Omh3+lqId/ZaiHviVoRz/laEc/5WhHP+Woh/wlqIf8JWhHfyWoh74laEc/5aiHvmWohz9i5Yd+V5kJ2NzeyHClKAc/5aiH+SbpimjlaEc/5WhHP+YoyLSnagumpynK6+ZpCTVm6YoqJWhHP+ZpSaqlqIf5JKeHP9udSOmeYIf4JWhHP+Woh7enqkxeZikI9SWoh7ynKcrk5umKa6ZpCTOmaQkzZumKZGVoRz+maUllZaiH92UoBz/dHwgxnqDHuGVoRz/lqIe3qGrNW6cpyurnagtjZ2oLpSeqTCen6kxl5umKq6gqjN4mKQj2JqlJ5OWoh/dlKAc/3R9H8h1fSHJlKAc/5aiH92cpiuLlqIe+p2nLYeYpCPJm6Yov5qlJsOXoh/tm6YowpumKb+bpiiPlqIf3ZOfHP9vdyOtZ24kjJCbHP+Woh7nnagufpynK5WbpiihlaEd95WhHP+VoRz/laEc/5WhHP+VoRz/maQkpJejHuKNmB39YWcmb1NWLzeCjB/llqIc/5aiHvWWoh7zlaEd/ZWhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHfWWohz+focg10xONSUAAIgCaG8mg42ZHf6Wohz/laEc/5WhHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/ipUd+mNpKGz//wAAS04xAD9BNhJvdyKmjpkd/paiHP+VoRz/laEc/5WhHP+VoRz/laEc/5WhHP+Wohz/i5Yd+2tyI5IzMz0LRUY2AAAAqQBRVCsAQUMzFGpxJYuEjh7skp0c/5WhHP+Wohz/lqIc/5WhHP+RnBz/gowf5mZtJns5OjYNR0kwAAAAAAAAAAAAAAAAADc3OgAnJUMFVVkqRGtzI6N5giDgfoge936IHvZ4gSDcaXAjmVFVLDkdGVIDMzJEAAAAAAAAAAAA4AcAAMADAACAAQAAgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIABAADAAwAA4AcAAA== From a3ee6b13c023abbeb90c7f6283ff9ce429990c1a Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 15:30:26 +0200 Subject: [PATCH 59/68] move network access methods to ::AuthStorage - allows for easier custom backends --- Changes | 6 ++ .../GoogleAPI/Client/AuthStorage.pm | 67 +++++++++++++++++++ .../GoogleAPI/Client/AuthStorage/GapiJSON.pm | 11 +-- .../Client/AuthStorage/ServiceAccount.pm | 11 +-- 4 files changed, 75 insertions(+), 20 deletions(-) diff --git a/Changes b/Changes index b8927d9..79e15d9 100644 --- a/Changes +++ b/Changes @@ -3,5 +3,11 @@ ENHANCEMENTS - add support for service accounts - rework storage backend to allow custom implementations (here's looking at you, redis) +- allow introspection for credentials on $res returned from an api query BREAKING CHANGES +- rework AuthStorage classes: anything that depended on + ::AuthStorage or ::Credentials in an undocumented way will likely explode. + Please open an issue on github and I'll try and help you migrate +- remove some undocumented options +- the examples/ dir MAY be broken, I didn't check (they need cleaning up anyways) diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 48e98af..839b49f 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -97,4 +97,71 @@ around get_access_token => sub { user => $user, token => $token, scopes => $scopes ); }; +=method refresh_user_token + +Makes the call to Google's OAuth API to refresh a token for a user. +Requires one parameter, a hashref with the keys: + +=begin :list + += client_id Your OAuth Client ID + += client_secret Your OAuth Client Secret + += refresh_token The refresh token for the user + +=end :list + +Will return the token from the API, for the backend to store (wherever it pleases). + +Will die with the error message from Google if the refresh fails. + +=cut + +sub refresh_user_token { + my ($self, $params) = @_; + my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => + form => { %$params, grant_type => 'refresh_token' } + ); + my $new_token = $tx->res->json('/access_token'); + unless ($new_token) { + croak "Failed to refresh access token: ", + join ' - ', map $tx->res->json("/$_"), qw/error error_description/ + if $tx->res->json; + # if the error doesn't come from google + croak "Unknown error refreshing access token"; + } + + return $new_token +} + + +=method refresh_service_account_token + +Makes the call to Google's OAuth API to refresh a token for a service account. +Requires one parameter, a L object already set with the user +and scopes requested. + +Will return the token from the API, for the backend to store (wherever it pleases). + +Will die with the error message from Google if the refresh fails. + +=cut + +sub refresh_service_account_token { + my ($self, $jwt) = @_; + my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => + $jwt->as_form_data + ); + my $new_token = $tx->res->json('/access_token'); + unless ($new_token) { + croak "Failed to get access token: ", + join ' - ', map $tx->res->json("/$_"), qw/error error_description/ + if $tx->res->json; + # if the error doesn't come from google + croak "Unknown error getting access token"; + } + return $new_token +} + 1; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm index a7c1f0f..3757c9a 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm @@ -88,18 +88,9 @@ https://myaccount.google.com/permissions as The oauth2 server will only ever min token at a time, and if you request another access token via the flow it will operate as if you only asked for an access token. MISSINGCREDS - $p{grant_type} = 'refresh_token'; my $user = $self->user; - my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => \%p); - my $new_token = $tx->res->json('/access_token'); - unless ($new_token) { - croak "Failed to refresh access token: ", - join ' - ', map $tx->res->json("/$_"), qw/error error_description/ - if $tx->res->json; - # if the error doesn't come from google - croak "Unknown error refreshing access token"; - } + my $new_token = $self->refresh_user_token(\%p); $self->tokensfile->set("gapi/tokens/$user/access_token", $new_token); return $new_token; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 6221773..547a379 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -123,16 +123,7 @@ sub refresh_access_token { $self->jwt->user_as(undef) } - my $tx = $self->ua->post('https://www.googleapis.com/oauth2/v4/token' => form => - $self->jwt->as_form_data); - my $new_token = $tx->res->json('/access_token'); - unless ($new_token) { - croak "Failed to get access token: ", - join ' - ', map $tx->res->json("/$_"), qw/error error_description/ - if $tx->res->json; - # if the error doesn't come from google - croak "Unknown error getting access token"; - } + my $new_token = $self->refresh_service_account_token($self->jwt); $self->tokens->{$self->scopes_string}{$self->user} = $new_token; return $new_token From 9b09ee587843a2ec63fae695901de745bcbabef0 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 15:50:08 +0200 Subject: [PATCH 60/68] rip out unused deps --- CONTRIBUTING.md | 13 +++++--- bin/goauth | 13 -------- dist.ini | 9 ++++-- lib/WebService/GoogleAPI/Client/Discovery.pm | 13 +------- xt/live_test.t | 34 ++++++++++++++++++++ 5 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 xt/live_test.t diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9de084..f5e43fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,12 @@ +# When hacking on this repo + +There are some live tests in `xt/` which require real user credentials. All they +do is create a file in Google Drive, and then delete it, in order to make sure that +things are ACTUALLY running. + # Bug Reporting and General Help Requests -- Use the [Github Issues Page](https://github.com/pscott-au/WebService-GoogleAPI-Client/issues) +- Use the [Github Issues Page](https://github.com/rabbiveesh/WebService-GoogleAPI-Client/issues) ## Github Repo Management @@ -10,8 +16,6 @@ # CONTRIBUTING CODE -- Use perlcritic and perltidy if my bracer style is offensive -- This is my first module using dzilla to package a module - I'm not completely sold on it and may be using it incorrectly - advice on improving usage welcome - There remain a few architectural bad smells from the original source code this was based on - don't assume that the class structure is sane - Pull reqeusts preferred but whatever works for you I will try to work with @@ -21,7 +25,8 @@ - refactor to improve test coverage - clean up the test structure - survey other Google Perl modules -- explore handling of batch requests +- implement batch requests - really an interface question more than the + technicalities - API worked examples with help functions - ability to examine CHI cache and introspect on Client instance metrics ( number of HTTP calls, cache size, TTL data sent/received etc ) - comparison with other language Client libraries diff --git a/bin/goauth b/bin/goauth index 815ad92..2164bc1 100755 --- a/bin/goauth +++ b/bin/goauth @@ -6,16 +6,10 @@ package goauth; use Carp; use Mojolicious::Lite; -use Path::Class; use Config::JSON; -use Tie::File; use feature 'say'; use Net::EmptyPort qw(empty_port); -use Crypt::JWT qw(decode_jwt); - - - # ABSTRACT: CLI tool with mini http server for negotiating Google OAuth2 Authorisation access tokens that allow offline access to Google API Services on behalf of the user. =pod @@ -108,9 +102,6 @@ WebService::Google:API:Client Google API Client Library. =cut -#use Path::Class; # ::File; ## cros-platform file paths handling # Exports file() by default - - my $filename = $ARGV[0] || 'gapi.json'; @@ -164,10 +155,6 @@ sub setup $tokensfile->set( 'gapi/scopes', $oauth->{ scopes } ); say 'OAuth details updated!'; - # Remove comment for Mojolicious::Plugin::JSONConfig compatibility - #tie my @array, 'Tie::File', $file->absolute() or croak $!; - #shift @array; - #untie @array; return 1; } diff --git a/dist.ini b/dist.ini index 6909586..9b3b9fd 100755 --- a/dist.ini +++ b/dist.ini @@ -35,12 +35,17 @@ IO::Socket::SSL = 2.06 Mojo::JWT = 0 Mojo::JWT::Google = 0.15 Exporter::Shiny = 0 +Net::EmptyPort = 0 +Config::JSON = 0 +Data::Dumper = 0 +Data::Dump = 0 +strictures = 0 +Carp = 0 +CHI = 0 [Prereqs / TestRequires ] Test2::V0 = 0 -[AutoPrereqs] - [CPANFile] [PerlTidy] diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index ff9a4a4..3937926 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -13,22 +13,12 @@ L Not using Swagger but it is interesting - L for Swagger Specs. -L - contains code for parsing discovery structures +L - contains code for parsing discovery structures includes a chi property that is an instance of CHI using File Driver to cache discovery resources for 30 days say $client-dicovery->chi->root_dir(); ## provides full file path to temp storage location used for caching -=head2 TODO - -=over 2 - -=item * handle 403 (Daily Limit for Unauthenticated Use Exceeded) - -errors when reqeusting a discovery resource for a service but do we have access to authenticated reqeusts? - -=back - =cut use Moo; @@ -36,7 +26,6 @@ use Carp; use WebService::GoogleAPI::Client::UserAgent; use List::Util qw/uniq reduce/; use List::SomeUtils qw/pairwise/; -use Hash::Slice qw/slice/; use Data::Dump qw/pp/; use CHI; diff --git a/xt/live_test.t b/xt/live_test.t new file mode 100644 index 0000000..fbc5d11 --- /dev/null +++ b/xt/live_test.t @@ -0,0 +1,34 @@ +use Test2::V0; +use lib 't/lib'; + +# TODO - make a simple test which creates a file, lists it, then deletes it, for +# both a service account and a regular account. + +use WebService::GoogleAPI::Client; +use Mojo::File qw/path/; + +bail_out <new( + gapi_json => path($root)->child('gapi.json')->to_string +); + +my $s = WebService::GoogleAPI::Client->new( + service_account => path($root)->child('service.json')->to_string +); + + + +done_testing; From b5debb2b22e03fa28a18d14c135cc6cb9126053f Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 15:59:57 +0200 Subject: [PATCH 61/68] change version plugin to allow perl critic --- bin/goauth | 2 ++ dist.ini | 5 ++--- lib/WebService/GoogleAPI/Client.pm | 2 ++ lib/WebService/GoogleAPI/Client/AccessToken.pm | 2 ++ lib/WebService/GoogleAPI/Client/AuthStorage.pm | 2 ++ lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm | 1 + .../GoogleAPI/Client/AuthStorage/ServiceAccount.pm | 2 ++ lib/WebService/GoogleAPI/Client/Discovery.pm | 2 ++ lib/WebService/GoogleAPI/Client/UserAgent.pm | 1 + 9 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bin/goauth b/bin/goauth index 2164bc1..24101b7 100755 --- a/bin/goauth +++ b/bin/goauth @@ -4,6 +4,8 @@ use strictures; package goauth; +# VERSION + use Carp; use Mojolicious::Lite; use Config::JSON; diff --git a/dist.ini b/dist.ini index 9b3b9fd..98129b3 100755 --- a/dist.ini +++ b/dist.ini @@ -52,7 +52,7 @@ Test2::V0 = 0 perltidyrc = .perltidyrc [GatherDir] -; exclude test scripts from build +; exclude dev scripts from build exclude_filename = gapi.json exclude_filename = service.json exclude_filename = DEV.MD @@ -65,7 +65,6 @@ exclude_match = examples/delme/* exclude_match = examples/*.avi exclude_match = examples/*.png exclude_match = examples/openapi/* -;exclude_filename = t/gapi.json [PodWeaver] @@ -90,7 +89,7 @@ issues = 1 [Git::CommitBuild] [Test::Perl::Critic] -critic_config = .perlcriticrc ; default / relative to project root +critic_config = .perlcriticrc ; i am unable to get dzil test to use my .perlcriticrc or use modern to avoid structure warnings ; FOR Dist::Zilla USERS (FROM 'perldoc Test::Perl::Critic' ) ; If you use Test::Perl::Critic with Dist::Zilla, beware that some DZ diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index bc2dbcc..29e902c 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -3,6 +3,8 @@ use 5.14.0; package WebService::GoogleAPI::Client; +# VERSION + use Data::Dump qw/pp/; use Moo; use WebService::GoogleAPI::Client::UserAgent; diff --git a/lib/WebService/GoogleAPI/Client/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm index 75f83bf..de296fd 100644 --- a/lib/WebService/GoogleAPI/Client/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -1,5 +1,7 @@ package WebService::GoogleAPI::Client::AccessToken; +# VERSION + # ABSTRACT: A small class for bundling user and scopes with a token use Moo; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage.pm b/lib/WebService/GoogleAPI/Client/AuthStorage.pm index 839b49f..f09d20d 100755 --- a/lib/WebService/GoogleAPI/Client/AuthStorage.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage.pm @@ -3,6 +3,8 @@ package WebService::GoogleAPI::Client::AuthStorage; # ABSTRACT: Role for classes which store your auth credentials +# VERSION + use Moo::Role; use Carp; use WebService::GoogleAPI::Client::AccessToken; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm index 3757c9a..3e9c9b6 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/GapiJSON.pm @@ -2,6 +2,7 @@ use strictures; package WebService::GoogleAPI::Client::AuthStorage::GapiJSON; +# VERSION # ABSTRACT: Auth Storage Backend based on gapi.json use Moo; diff --git a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm index 547a379..0e1b35b 100644 --- a/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm +++ b/lib/WebService/GoogleAPI/Client/AuthStorage/ServiceAccount.pm @@ -1,6 +1,8 @@ use strictures; package WebService::GoogleAPI::Client::AuthStorage::ServiceAccount; +# VERSION + # ABSTRACT: Manage access tokens from a service account file =head1 SYNOPSIS diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index 3937926..e7f3461 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -2,6 +2,8 @@ use strictures; package WebService::GoogleAPI::Client::Discovery; +# VERSION + # ABSTRACT: Google API discovery service =head2 MORE INFORMATION diff --git a/lib/WebService/GoogleAPI/Client/UserAgent.pm b/lib/WebService/GoogleAPI/Client/UserAgent.pm index 4dc0f4e..e73be26 100644 --- a/lib/WebService/GoogleAPI/Client/UserAgent.pm +++ b/lib/WebService/GoogleAPI/Client/UserAgent.pm @@ -2,6 +2,7 @@ use strictures; package WebService::GoogleAPI::Client::UserAgent; +# VERSION # ABSTRACT: User Agent wrapper for working with Google APIs use Moo; From 438601c42bf8840a4034b6fcb1493ea7a4fc4d6f Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 16:02:26 +0200 Subject: [PATCH 62/68] now we're passing perl critic --- dist.ini | 2 +- lib/WebService/GoogleAPI/Client/AccessToken.pm | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dist.ini b/dist.ini index 98129b3..3bb1ec7 100755 --- a/dist.ini +++ b/dist.ini @@ -23,7 +23,7 @@ main_module = lib/WebService/GoogleAPI/Client.pm [ConfirmRelease] ;[UploadToCPAN] -[PkgVersion] +[OurPkgVersion] [Prereqs] perl = 5.16 Moo = 2.00 diff --git a/lib/WebService/GoogleAPI/Client/AccessToken.pm b/lib/WebService/GoogleAPI/Client/AccessToken.pm index de296fd..40522aa 100644 --- a/lib/WebService/GoogleAPI/Client/AccessToken.pm +++ b/lib/WebService/GoogleAPI/Client/AccessToken.pm @@ -1,4 +1,5 @@ package WebService::GoogleAPI::Client::AccessToken; +use strictures; # VERSION From b38dbf570422924182e28631705c4e51a4ede814 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 16:12:01 +0200 Subject: [PATCH 63/68] finish tapping out details on the xt live test --- CONTRIBUTING.md | 13 ++++++++++++- xt/live_test.t | 26 ++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5e43fb..9ee7946 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,18 @@ There are some live tests in `xt/` which require real user credentials. All they do is create a file in Google Drive, and then delete it, in order to make sure that things are ACTUALLY running. +If you don't trust me, then please don't run those tests. They're part of the +release process, so I'll just run them before releasing. + +If you DO trust me (and I'm flattered), then you need to set the +`GAPI_XT_USER_CREDS` environment variable to a string containing the gapi.json +file, followed by `:` followed by the user to user. Like this: + + /absolute/path/to/gapi.json:your_email@gmail.com + +And also set the `GAPI_XT_SERVICE_CREDS` to the service account file downloaded +from Google. + # Bug Reporting and General Help Requests - Use the [Github Issues Page](https://github.com/rabbiveesh/WebService-GoogleAPI-Client/issues) @@ -30,7 +42,6 @@ things are ACTUALLY running. - API worked examples with help functions - ability to examine CHI cache and introspect on Client instance metrics ( number of HTTP calls, cache size, TTL data sent/received etc ) - comparison with other language Client libraries -- The structure under the AuthStorage is ugly and needs some love Github Repo: [https://github.com/pscott-au/WebService-GoogleAPI-Client] diff --git a/xt/live_test.t b/xt/live_test.t index fbc5d11..36ce2d6 100644 --- a/xt/live_test.t +++ b/xt/live_test.t @@ -1,32 +1,34 @@ use Test2::V0; -use lib 't/lib'; # TODO - make a simple test which creates a file, lists it, then deletes it, for # both a service account and a regular account. use WebService::GoogleAPI::Client; -use Mojo::File qw/path/; -bail_out <new( - gapi_json => path($root)->child('gapi.json')->to_string + gapi_json => $path, user => $email ); my $s = WebService::GoogleAPI::Client->new( - service_account => path($root)->child('service.json')->to_string + service_account => $service_creds ); From d9625411e4174066bdccc85d8940dbd7a7075dde Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 16:22:55 +0200 Subject: [PATCH 64/68] finish writing simple live test --- xt/live_test.t | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/xt/live_test.t b/xt/live_test.t index 36ce2d6..341fd03 100644 --- a/xt/live_test.t +++ b/xt/live_test.t @@ -1,4 +1,5 @@ use Test2::V0; +use Test2::Tools::Spec; # TODO - make a simple test which creates a file, lists it, then deletes it, for # both a service account and a regular account. @@ -28,9 +29,34 @@ my $u = WebService::GoogleAPI::Client->new( ); my $s = WebService::GoogleAPI::Client->new( - service_account => $service_creds + service_account => $service_creds, + scopes => [ 'https://www.googleapis.com/auth/drive' ] ); +my $filename = 'a-rather-unlikely-named-file-for-xt-testing'; + +describe 'file creation and deletion' => sub { + my $ua; + case 'user account' => sub { $ua = $u }; + case 'service account' => sub { $ua = $s }; + + tests 'doing it' => sub { + my $res = $ua->api_query({ + api_endpoint_id => 'drive.files.create', + options => { name => $filename } + }); + + is $res->json('/name'), $filename, 'request worked'; + my $id = $res->json('/id'); + + $res = $ua->api_query({ + api_endpoint_id => 'drive.files.delete', + options => { fileId => $id } + }); + + is $res->code, 204, 'delete went as planned'; + }; +}; done_testing; From a06593e4ddb14f4c017a9aeef49f3bcf32d4c2c7 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 18:35:58 +0200 Subject: [PATCH 65/68] make get_method_details die, like it should --- Changes | 1 + lib/WebService/GoogleAPI/Client.pm | 7 ------- lib/WebService/GoogleAPI/Client/Discovery.pm | 22 +++++++------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Changes b/Changes index 79e15d9..387f7d3 100644 --- a/Changes +++ b/Changes @@ -6,6 +6,7 @@ ENHANCEMENTS - allow introspection for credentials on $res returned from an api query BREAKING CHANGES +- will immediately on non-existent API endpoints - rework AuthStorage classes: anything that depended on ::AuthStorage or ::Credentials in an undocumented way will likely explode. Please open an issue on github and I'll try and help you migrate diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index 29e902c..de3a388 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -499,13 +499,6 @@ sub _process_params { if (defined $params->{cb_method_discovery_modify} && ref($params->{cb_method_discovery_modify}) eq 'CODE'); - croak "Can't get data for $params->{api_endpoint_id}- is this a valid end point?" - unless (keys %{$method_discovery_struct} > 0); - ## assertion: method discovery struct ok - or at least has keys - carp( -"API Endpoint $params->{api_endpoint_id} discovered specification didn't include expected 'parameters' keyed HASH structure" - ) unless ref($method_discovery_struct->{parameters}) eq 'HASH'; - my @teapot_errors = (); ## errors are pushed into this as encountered $params->{method} = $method_discovery_struct->{httpMethod} || 'GET' if (not defined $params->{method}); diff --git a/lib/WebService/GoogleAPI/Client/Discovery.pm b/lib/WebService/GoogleAPI/Client/Discovery.pm index e7f3461..350f215 100644 --- a/lib/WebService/GoogleAPI/Client/Discovery.pm +++ b/lib/WebService/GoogleAPI/Client/Discovery.pm @@ -409,7 +409,7 @@ returns a hashref representing the discovery specification for the method identified by $tree in dotted API format such as texttospeech.text.synthesize -returns an empty hashref if not found. +Dies a horrible death if not found. Also available as C, but the long name is being deprecated in favor of the more compact one. @@ -428,31 +428,28 @@ sub get_method_details { my ($self, $tree, $api_version) = @_; ## where tree is the method in format from _extract_resource_methods_from_api_spec() like projects.models.versions.get ## the root is the api id - further '.' sep levels represent resources until the tailing label that represents the method - #TODO- should die a horrible death if method not found - return {} unless defined $tree; + croak 'You must ask for a method!' unless defined $tree; my @nodes = split /\./smx, $tree; croak( "tree structure '$tree' must contain at least 2 nodes including api id, [list of hierarchical resources ] and method - not " . scalar(@nodes)) - unless scalar(@nodes) > 1; + unless @nodes > 1; my $api_id = shift(@nodes); ## api was head my $method = pop(@nodes); ## method was tail ## split out version if is defined as part of $tree ## trim any resource, method or version details in api id - if ($api_id =~ /([^:]+):([^\.]+)$/ixsm - ) ## we have already isolated head from api tree children - { + ## we have already isolated head from api tree children + if ($api_id =~ /([^:]+):([^\.]+)$/ixsm) { $api_id = $1; $api_version = $2; } ## handle incorrect api_id if ($self->service_exists($api_id) == 0) { - carp("unable to confirm that '$api_id' is a valid Google API service id"); - return {}; + croak("unable to confirm that '$api_id' is a valid Google API service id"); } $api_version = $self->latest_stable_version($api_id) unless $api_version; @@ -484,7 +481,6 @@ sub get_method_details { = $self->_extract_resource_methods_from_api_spec("$api_id:$api_version", $api_spec); - #print dump $all_api_methods;exit; unless (defined $all_api_methods->{$tree}) { $all_api_methods = $self->_extract_resource_methods_from_api_spec($api_id, $api_spec); @@ -493,6 +489,7 @@ sub get_method_details { #add in the global parameters to the endpoint, #stored in the top level of the api_spec + # TODO - why are we mutating the main hash? $all_api_methods->{$tree}{parameters} = { %{ $all_api_methods->{$tree}{parameters} }, %{ $api_spec->{parameters} } @@ -500,10 +497,7 @@ sub get_method_details { return $all_api_methods->{$tree}; } - carp( - "Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version" - ) if $self->debug; - return {}; + croak("Unable to find method detail for '$tree' within Google Discovery Spec for $api_id version $api_version") } ######################################################## From 3c82d69de244b7d99546ea442d636791a4be9cfc Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 19:01:35 +0200 Subject: [PATCH 66/68] fix t/05 (at least for now!) --- lib/WebService/GoogleAPI/Client.pm | 55 ++++++++----------- t/05-spec-interpolation.t | 88 ++++++++++++++---------------- 2 files changed, 65 insertions(+), 78 deletions(-) diff --git a/lib/WebService/GoogleAPI/Client.pm b/lib/WebService/GoogleAPI/Client.pm index de3a388..9103eac 100755 --- a/lib/WebService/GoogleAPI/Client.pm +++ b/lib/WebService/GoogleAPI/Client.pm @@ -419,6 +419,9 @@ sub api_query { } carp(pp $params) if $self->debug > 10; + croak "Missing neccessary scopes to access $params->{api_endpoint_id}" + unless $self->has_scope_to_access_api_endpoint($params->{api_endpoint_id}); + ## used to collect pre-query validation errors - if set we return a response # with 418 I'm a teapot my @teapot_errors = (); @@ -505,14 +508,9 @@ sub _process_params { push(@teapot_errors, "method mismatch - you requested a $params->{method} which conflicts with discovery spec requirement for $method_discovery_struct->{httpMethod}" ) if ($params->{method} !~ /^$method_discovery_struct->{httpMethod}$/sxim); - push(@teapot_errors, -"Client Credentials do not include required scope to access $params->{api_endpoint_id}" - ) - unless $self->has_scope_to_access_api_endpoint($params->{api_endpoint_id}) - ; ## ensure user has required scope access - $params->{path} = $method_discovery_struct->{path} - unless $params->{path} - ; ## Set default path iff not set by user - NB - will prepend baseUrl later + + ## Set default path iff not set by user - NB - will prepend baseUrl later + $params->{path} = $method_discovery_struct->{path} unless $params->{path}; push @teapot_errors, 'path is a required parameter' unless $params->{path}; push @teapot_errors, @@ -659,39 +657,32 @@ warns and returns 0 on error ( eg user or config not specified etc ) =cut -######################################################## -sub has_scope_to_access_api_endpoint -{ - my ( $self, $api_ep ) = @_; - return 0 unless defined $api_ep; - return 0 if $api_ep eq ''; - my $method_spec = $self->get_method_details( $api_ep ); +sub has_scope_to_access_api_endpoint { + my ($self, $api_ep) = @_; + my $method_spec = $self->get_method_details($api_ep); - if ( keys( %$method_spec ) > 0 ) ## empty hash indicates failure - { + if (keys %$method_spec) { my $configured_scopes = $self->scopes; ## get user scopes arrayref ## create a hashindex to facilitate quick lookups - my %configured_scopes_hash = map { s/\/$//xr, 1 } @$configured_scopes; ## NB r switch as per https://www.perlmonks.org/?node_id=613280 to filter out any trailing '/' - my $granted = 0; ## assume permission not granted until we find a matching scope - my $required_scope_count = 0 - ; ## if the final count of scope constraints = 0 then we will assume permission is granted - this has proven necessary for the experimental Google My Business because scopes are not defined in the current discovery data as at 14/10/18 - foreach my $method_scope ( map {s/\/$//xr} @{ $method_spec->{ scopes } } ) - { + my %configured_scopes_hash = map { (s/\/$//xr, 1) } @$configured_scopes; + ## NB r switch as per https://www.perlmonks.org/?node_id=613280 to filter + #out any trailing '/' + my $granted = 0; + ## assume permission not granted until we find a matching scope + my $required_scope_count = 0; + ## if the final count of scope constraints = 0 then we will assume permission is granted - this has proven necessary for the experimental Google My Business because scopes are not defined in the current discovery data as at 14/10/18 + for my $method_scope (map { s/\/$//xr } @{ $method_spec->{scopes} }) { $required_scope_count++; - $granted = 1 if defined $configured_scopes_hash{ $method_scope }; - last if $granted; + $granted = 1 if defined $configured_scopes_hash{$method_scope}; + last if $granted; } - $granted = 1 if ( $required_scope_count == 0 ); + $granted = 1 if $required_scope_count == 0; return $granted; - } - else - { - return 0; ## cannot get method spec - warnings should have already been issued - returning - to indicate access denied + } else { + return 0; } } -######################################################## - #TODO: Consider rename to return_fetched_google_v1_apis_discovery_structure diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index b8a92fb..e5e6428 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -60,68 +60,64 @@ is $options->{path}, 'interpolates arrayref correctly' ; subtest 'funky stuff in the jobs api' => sub { - # TODO - let's change this to make sure this check doesn't needa happen - return fail 'has the scopes', <has_scope_to_access_api_endpoint('jobs.projects.jobs.delete'); + my $endpoint = 'jobs.projects.tenants.jobs.delete'; subtest 'Testing {+param} type interpolation options' => sub { - my $interpolated = 'https://jobs.googleapis.com/v3/projects/sner/jobs'; - - $options = { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {name => 'projects/sner/jobs/bler'} }; - $gapi->_process_params( $options ); + my @errors; + my $interpolated = 'https://jobs.googleapis.com/v4/projects/sner/tenants/your-fez/jobs'; + + $options = { + api_endpoint_id => $endpoint, + options => { + name => 'projects/sner/tenants/your-fez/jobs/bler' + } + }; + @errors = $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", - 'Interpolates a {+param} that matches the spec pattern'; - - $options = - { api_endpoint_id => 'jobs.projects.jobs.list', - options => { parent => 'sner' } }; - $gapi->_process_params( $options ); - is $options->{path}, $interpolated, - 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern'; + 'Interpolates a {+param} that matches the spec pattern', @errors; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projectsId => 'sner', jobsId => 'bler'} }; - $gapi->_process_params( $options ); + $options->{options} = {projectsId => 'sner', jobsId => 'bler'}; + @errors = $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", - 'Interpolates params that match the flatName spec (camelCase)'; + 'Interpolates params that match the flatName spec (camelCase)', @errors; - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => {projects_id => 'sner', jobs_id => 'bler'} }; - $gapi->_process_params( $options ); + $options->{options} = {projects_id => 'sner', jobs_id => 'bler'}; + @errors = $gapi->_process_params( $options ); is $options->{path}, "$interpolated/bler", - 'Interpolates params that match the names in the api description (snake_case)'; + 'Interpolates params that match the names in the api description (snake_case)', + @errors; + + $options = { + api_endpoint_id => 'jobs.projects.tenants.list', + options => { parent => 'sner' } + }; + @errors = $gapi->_process_params( $options ); + is $options->{path}, $interpolated =~ s+/your-fez.*$++r, + 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern', + @errors; }; - my @errors; subtest 'Checking for proper failure with {+params} in unsupported ways' => sub { - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => { name => 'sner' } }; - @errors = $gapi->_process_params( $options ); - is $errors[0], 'Not enough parameters given for {+name}.', - "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; - - $options = - { api_endpoint_id => 'jobs.projects.jobs.delete', - options => { jobsId => 'sner' } }; - @errors = $gapi->_process_params( $options ); - is $errors[0], 'Missing a parameter for {projectsId}.', - "Fails if you don't supply enough values to fill the flatPath"; + my @errors; + $options = { + api_endpoint_id => $endpoint, + options => { name => 'sner' } + }; + @errors = $gapi->_process_params( $options ); + is $errors[0], 'Not enough parameters given for {+name}.', + "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; + $options = + { api_endpoint_id => $endpoint, + options => { jobsId => 'sner' } }; + @errors = $gapi->_process_params( $options ); + is $errors[0], 'Missing a parameter for {projectsId}.', + "Fails if you don't supply enough values to fill the flatPath"; }; }; From a03f79c1336f5568aff4107c47f9344d7295eee4 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 19:10:57 +0200 Subject: [PATCH 67/68] finish the tests! ready to ship! --- TODO | 14 -------------- t/05-spec-interpolation.t | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index de7406e..2c0e1f5 100644 --- a/TODO +++ b/TODO @@ -1,19 +1,5 @@ # vim: ft=markdown -# better support for service accounts - -We did a number on the architecture to allow for pluggable storage backends. -I think some more improvement could be had, but hey, it's a start. - -- test that auth_storage only takes our subclasses - -- get a real test down which uses normal creds and uses service account creds - i guess to read rows from a spreadsheet (like we have in some $work code) - -- test that there are sensible explosions when an endpoint does not exist - -fix docs for goauth - # Make this more idiomatic Allow users to make requests like this diff --git a/t/05-spec-interpolation.t b/t/05-spec-interpolation.t index e5e6428..a40ea95 100644 --- a/t/05-spec-interpolation.t +++ b/t/05-spec-interpolation.t @@ -97,8 +97,6 @@ subtest 'funky stuff in the jobs api' => sub { is $options->{path}, $interpolated =~ s+/your-fez.*$++r, 'Interpolates just the dynamic part of the {+param}, when not matching the spec pattern', @errors; - - }; @@ -112,9 +110,18 @@ subtest 'funky stuff in the jobs api' => sub { is $errors[0], 'Not enough parameters given for {+name}.', "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; - $options = - { api_endpoint_id => $endpoint, - options => { jobsId => 'sner' } }; + $options = { + api_endpoint_id => 'jobs.projects.tenants.jobs.list', + options => { parent => 'sner' } + }; + @errors = $gapi->_process_params( $options ); + is $errors[0], 'Not enough parameters given for {+parent}.', + "Fails if you don't supply enough values to fill the dynamic parts of {+param}"; + + $options = { + api_endpoint_id => $endpoint, + options => { jobsId => 'sner' } + }; @errors = $gapi->_process_params( $options ); is $errors[0], 'Missing a parameter for {projectsId}.', "Fails if you don't supply enough values to fill the flatPath"; From c8849738f08d629709792421965230668eedb1e0 Mon Sep 17 00:00:00 2001 From: Veesh Goldman Date: Thu, 28 Jan 2021 19:15:57 +0200 Subject: [PATCH 68/68] last little fix for dist.ini --- dist.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dist.ini b/dist.ini index 3bb1ec7..58b5651 100755 --- a/dist.ini +++ b/dist.ini @@ -7,8 +7,6 @@ copyright_year = 2017-2021 version = 0.23 main_module = lib/WebService/GoogleAPI/Client.pm -;[MinimumPerl] -;Perl 5.14.4 [PruneCruft] [ManifestSkip] @@ -25,7 +23,7 @@ main_module = lib/WebService/GoogleAPI/Client.pm [OurPkgVersion] [Prereqs] -perl = 5.16 +perl = 5.16.0 Moo = 2.00 Mojolicious = 8.30 Mojolicious::Plugin::OAuth2 = 1.5