upgrading from 6.0 to 6.1
This commit is contained in:
parent
6a6fb98351
commit
45f6ce3568
|
|
@ -0,0 +1,136 @@
|
|||
# 0.7.0
|
||||
* Remove CLI
|
||||
* SUpport for automatic retires & backoff. Off by default, enable by setting `retries` on `APIClient`
|
||||
* Experimental new interface (see `Google::APIClient::Service`)
|
||||
* Fix warnings when using Faraday separately
|
||||
* Support Google Compute Engine service accounts
|
||||
* Enable gzip compression for responses
|
||||
* Upgrade to Faraday 0.9.0. Resolves multiple issues with query parameter encodings.
|
||||
* Use bundled root certificates for verifying SSL certificates
|
||||
* Rewind media when retrying uploads
|
||||
|
||||
# 0.6.4
|
||||
* Pin signet version to 0.4.x
|
||||
|
||||
# 0.6.3
|
||||
|
||||
* Update autoparse to 0.3.3 to fix cases where results aren't correctly parsed.
|
||||
* Fix railtie loading for compatibility with rails < 3.0
|
||||
* Fix refresh of access token when passing credentials as parameter to execute
|
||||
* Fix URI processing in batch requests to allow query parameters
|
||||
|
||||
# 0.6.2
|
||||
|
||||
* Update signet to 0.4.6 to support server side continuation of postmessage
|
||||
auth flows.
|
||||
|
||||
# 0.6.1
|
||||
|
||||
* Fix impersonation with service accounts
|
||||
|
||||
# 0.6
|
||||
|
||||
* Apps strongly encouraged to set :application_name & :application_version when
|
||||
initializing a client
|
||||
* JWT/service accounts moved to signet
|
||||
* Added helper class for installed app OAuth flows, updated samples & CLI
|
||||
* Initial logging support for client
|
||||
* Fix PKCS12 loading on windows
|
||||
* Allow disabling auto-refresh of OAuth 2 access tokens
|
||||
* Compatibility with MultiJson >= 1.0.0 & Rails 3.2.8
|
||||
* Fix for body serialization when body doesn't respond to to_json
|
||||
* Remove OAuth 1.0 logins from CLI
|
||||
|
||||
|
||||
# 0.5.0
|
||||
|
||||
* Beta candidate, potential incompatible changes with how requests are processed.
|
||||
* All requests should be made using execute() or execute!()
|
||||
* :api_method in request can no longer be a string
|
||||
* Deprecated ResumableUpload.send_* methods.
|
||||
* Reduce memory utilization when uploading large files
|
||||
* Automatic refresh of OAuth 2 credentials & retry of request when 401 errors
|
||||
are returned
|
||||
* Simplify internal request processing.
|
||||
|
||||
# 0.4.7
|
||||
|
||||
* Added the ability to convert client secrets to an authorization object
|
||||
|
||||
# 0.4.6
|
||||
|
||||
* Backwards compatibility for MultiJson
|
||||
|
||||
# 0.4.5
|
||||
|
||||
* Updated Launchy dependency
|
||||
* Updated Faraday dependency
|
||||
* Updated Addressable dependency
|
||||
* Updated Autoparse dependency
|
||||
* Removed Sinatra development dependency
|
||||
|
||||
# 0.4.4
|
||||
|
||||
* Added batch execution
|
||||
* Added service accounts
|
||||
* Can now supply authorization on a per-request basis.
|
||||
|
||||
# 0.4.3
|
||||
|
||||
* Added media upload capabilities
|
||||
* Support serializing OAuth credentials to client_secrets.json
|
||||
* Fixed OS name/version string on JRuby
|
||||
|
||||
# 0.4.2
|
||||
|
||||
* Fixed incompatibility with Ruby 1.8.7
|
||||
|
||||
# 0.4.1
|
||||
|
||||
* Fixed ancestor checking issue when assigning Autoparse identifiers
|
||||
* Renamed discovery methods to avoid collisions with some APIs
|
||||
* Updated autoparse dependency to avoid JSON bug
|
||||
|
||||
# 0.4.0
|
||||
|
||||
* Replaced httpadapter gem dependency with faraday
|
||||
* Replaced json gem dependency with multi_json
|
||||
* Fixed /dev/null issues on Windows
|
||||
* Repeated parameters now work
|
||||
|
||||
# 0.3.0
|
||||
|
||||
* Updated to use v1 of the discovery API
|
||||
* Updated to use httpadapter 1.0.0
|
||||
* Added OAuth 2 support to the command line tool
|
||||
* Renamed some switches in the command line tool
|
||||
* Added additional configuration capabilities
|
||||
* Fixed a few deprecation warnings from dependencies
|
||||
* Added gemspec to source control
|
||||
|
||||
# 0.2.0
|
||||
|
||||
* Updated to use v1 of the discovery API
|
||||
* Updated to use httpadapter 1.0.0
|
||||
* Added OAuth 2 support to the command line tool
|
||||
* Renamed some switches in the command line tool
|
||||
* Added additional configuration capabilities
|
||||
|
||||
# 0.1.3
|
||||
|
||||
* Added support for manual overrides of the discovery URI
|
||||
* Added support for manual overrides of the API base
|
||||
* Added support for xoauth_requestor_id
|
||||
|
||||
# 0.1.2
|
||||
|
||||
* Added support for two-legged OAuth
|
||||
* Moved some development dependencies into runtime
|
||||
|
||||
# 0.1.1
|
||||
|
||||
* Substantial improvements to the command line interface
|
||||
|
||||
# 0.1.0
|
||||
|
||||
* Initial release
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# How to become a contributor and submit your own code
|
||||
|
||||
## Contributor License Agreements
|
||||
|
||||
We'd love to accept your sample apps and patches! Before we can take them, we
|
||||
have to jump a couple of legal hurdles.
|
||||
|
||||
Please fill out either the individual or corporate Contributor License Agreement
|
||||
(CLA).
|
||||
|
||||
* If you are an individual writing original source code and you're sure you
|
||||
own the intellectual property, then you'll need to sign an [individual CLA]
|
||||
(http://code.google.com/legal/individual-cla-v1.0.html).
|
||||
* If you work for a company that wants to allow you to contribute your work,
|
||||
then you'll need to sign a [corporate CLA]
|
||||
(http://code.google.com/legal/corporate-cla-v1.0.html).
|
||||
|
||||
Follow either of the two links above to access the appropriate CLA and
|
||||
instructions for how to sign and return it. Once we receive it, we'll be able to
|
||||
accept your pull requests.
|
||||
|
||||
## Contributing A Patch
|
||||
|
||||
1. Submit an issue describing your proposed change to the repo in question.
|
||||
1. The repo owner will respond to your issue promptly.
|
||||
1. If your proposed change is accepted, and you haven't already done so, sign a
|
||||
Contributor License Agreement (see details above).
|
||||
1. Fork the desired repo, develop and test your code changes.
|
||||
1. Ensure that your code is clear and comprehensible.
|
||||
1. Ensure that your code has an appropriate set of unit tests which all pass.
|
||||
1. Submit a pull request.
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
gem 'signet', '>= 0.5.0'
|
||||
gem 'addressable', '>= 2.3.2'
|
||||
gem 'uuidtools', '>= 2.1.0'
|
||||
gem 'autoparse', '>= 0.3.3'
|
||||
gem 'faraday', '>= 0.9.0'
|
||||
gem 'multi_json', '>= 1.0.0'
|
||||
gem 'extlib', '>= 0.9.15'
|
||||
gem 'jwt', '~> 0.1.5'
|
||||
gem 'retriable', '>= 1.4'
|
||||
gem 'jruby-openssl', :platforms => :jruby
|
||||
|
||||
group :development do
|
||||
gem 'launchy', '>= 2.1.1'
|
||||
gem 'yard'
|
||||
gem 'kramdown'
|
||||
end
|
||||
|
||||
|
||||
platforms :rbx do
|
||||
gem 'rubysl', '~> 2.0'
|
||||
gem 'psych'
|
||||
end
|
||||
|
||||
|
||||
group :examples do
|
||||
gem 'sinatra'
|
||||
end
|
||||
|
||||
group :test, :development do
|
||||
gem 'json', '~> 1.7.7'
|
||||
gem 'rake', '>= 0.9.0'
|
||||
gem 'rspec', '>= 2.11.0'
|
||||
gem 'rcov', '>= 0.9.9', :platform => :mri_18
|
||||
end
|
||||
|
||||
|
||||
gem 'idn', :platform => :mri_18
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
# Google API Client
|
||||
|
||||
<dl>
|
||||
<dt>Homepage</dt><dd><a href="http://www.github.com/google/google-api-ruby-client">http://www.github.com/google/google-api-ruby-client</a></dd>
|
||||
<dt>Authors</dt><dd>Bob Aman, <a href="mailto:sbazyl@google.com">Steven Bazyl</a></dd>
|
||||
<dt>Copyright</dt><dd>Copyright © 2011 Google, Inc.</dd>
|
||||
<dt>License</dt><dd>Apache 2.0</dd>
|
||||
</dl>
|
||||
|
||||
[](http://travis-ci.org/google/google-api-ruby-client)
|
||||
[](https://gemnasium.com/google/google-api-ruby-client)
|
||||
|
||||
## Description
|
||||
|
||||
The Google API Ruby Client makes it trivial to discover and access supported
|
||||
APIs.
|
||||
|
||||
## Install
|
||||
|
||||
Be sure `https://rubygems.org/` is in your gem sources.
|
||||
|
||||
For normal client usage, this is sufficient:
|
||||
|
||||
```bash
|
||||
$ gem install google-api-client
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
```ruby
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/client_secrets'
|
||||
require 'google/api_client/auth/installed_app'
|
||||
|
||||
# Initialize the client.
|
||||
client = Google::APIClient.new(
|
||||
:application_name => 'Example Ruby application',
|
||||
:application_version => '1.0.0'
|
||||
)
|
||||
|
||||
# Initialize Google+ API. Note this will make a request to the
|
||||
# discovery service every time, so be sure to use serialization
|
||||
# in your production code. Check the samples for more details.
|
||||
plus = client.discovered_api('plus')
|
||||
|
||||
# Load client secrets from your client_secrets.json.
|
||||
client_secrets = Google::APIClient::ClientSecrets.load
|
||||
|
||||
# Run installed application flow. Check the samples for a more
|
||||
# complete example that saves the credentials between runs.
|
||||
flow = Google::APIClient::InstalledAppFlow.new(
|
||||
:client_id => client_secrets.client_id,
|
||||
:client_secret => client_secrets.client_secret,
|
||||
:scope => ['https://www.googleapis.com/auth/plus.me']
|
||||
)
|
||||
client.authorization = flow.authorize
|
||||
|
||||
# Make an API call.
|
||||
result = client.execute(
|
||||
:api_method => plus.activities.list,
|
||||
:parameters => {'collection' => 'public', 'userId' => 'me'}
|
||||
)
|
||||
|
||||
puts result.data
|
||||
```
|
||||
|
||||
## API Features
|
||||
|
||||
### API Discovery
|
||||
|
||||
To take full advantage of the client, load API definitions prior to use. To load an API:
|
||||
|
||||
```ruby
|
||||
urlshortener = client.discovered_api('urlshortener')
|
||||
```
|
||||
|
||||
Specific versions of the API can be loaded as well:
|
||||
|
||||
```ruby
|
||||
drive = client.discovered_api('drive', 'v2')
|
||||
```
|
||||
|
||||
Locally cached discovery documents may be used as well. To load an API from a local file:
|
||||
|
||||
```ruby
|
||||
doc = File.read('my-api.json')
|
||||
my_api = client.register_discovery_document('myapi', 'v1', doc)
|
||||
```
|
||||
|
||||
### Authorization
|
||||
|
||||
Most interactions with Google APIs require users to authorize applications via OAuth 2.0. The client library uses [Signet](https://github.com/google/signet) to handle most aspects of authorization. For additional details about Google's OAuth support, see [Google Developers](https://developers.google.com/accounts/docs/OAuth2).
|
||||
|
||||
Credentials can be managed at the connection level, as shown, or supplied on a per-request basis when calling `execute`.
|
||||
|
||||
For server-to-server interactions, like those between a web application and Google Cloud Storage, Prediction, or BigQuery APIs, use service accounts.
|
||||
|
||||
```ruby
|
||||
key = Google::APIClient::KeyUtils.load_from_pkcs12('client.p12', 'notasecret')
|
||||
client.authorization = Signet::OAuth2::Client.new(
|
||||
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
||||
:audience => 'https://accounts.google.com/o/oauth2/token',
|
||||
:scope => 'https://www.googleapis.com/auth/prediction',
|
||||
:issuer => '123456-abcdef@developer.gserviceaccount.com',
|
||||
:signing_key => key)
|
||||
client.authorization.fetch_access_token!
|
||||
client.execute(...)
|
||||
```
|
||||
|
||||
Service accounts are also used for delegation in Google Apps domains. The target user for impersonation is specified by setting the `:person` parameter to the user's email address
|
||||
in the credentials. Detailed instructions on how to enable delegation for your domain can be found at [developers.google.com](https://developers.google.com/drive/delegation).
|
||||
|
||||
### Automatic Retries & Backoff
|
||||
|
||||
The API client can automatically retry requests for recoverable errors. To enable retries, set the `client.retries` property to
|
||||
the number of additional attempts. To avoid flooding servers, retries invovle a 1 second delay that increases on each subsequent retry.
|
||||
|
||||
The default value for retries is 0, but will be enabled by default in future releases.
|
||||
|
||||
### Batching Requests
|
||||
|
||||
Some Google APIs support batching requests into a single HTTP request. Use `Google::APIClient::BatchRequest`
|
||||
to bundle multiple requests together.
|
||||
|
||||
Example:
|
||||
|
||||
```ruby
|
||||
client = Google::APIClient.new
|
||||
urlshortener = client.discovered_api('urlshortner')
|
||||
|
||||
batch = Google::APIClient::BatchRequest.new do |result|
|
||||
puts result.data
|
||||
end
|
||||
|
||||
batch.add(:api_method => urlshortener.url.insert,
|
||||
:body_object => { 'longUrl' => 'http://example.com/foo' })
|
||||
batch.add(:api_method => urlshortener.url.insert,
|
||||
:body_object => { 'longUrl' => 'http://example.com/bar' })
|
||||
client.execute(batch)
|
||||
```
|
||||
|
||||
Blocks for handling responses can be specified either at the batch level or when adding an individual API call. For example:
|
||||
|
||||
```ruby
|
||||
batch.add(:api_method=>urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/bar' }) do |result|
|
||||
puts result.data
|
||||
end
|
||||
```
|
||||
|
||||
### Media Upload
|
||||
|
||||
For APIs that support file uploads, use `Google::APIClient::UploadIO` to load the stream. Both multipart and resumable
|
||||
uploads can be used. For example, to upload a file to Google Drive using multipart
|
||||
|
||||
```ruby
|
||||
drive = client.discovered_api('drive', 'v2')
|
||||
|
||||
media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
|
||||
metadata = {
|
||||
'title' => 'My movie',
|
||||
'description' => 'The best home movie ever made'
|
||||
}
|
||||
client.execute(:api_method => drive.files.insert,
|
||||
:parameters => { 'uploadType' => 'multipart' },
|
||||
:body_object => metadata,
|
||||
:media => media )
|
||||
```
|
||||
|
||||
To use resumable uploads, change the `uploadType` parameter to `resumable`. To check the status of the upload
|
||||
and continue if necessary, check `result.resumable_upload`.
|
||||
|
||||
```ruby
|
||||
client.execute(:api_method => drive.files.insert,
|
||||
:parameters => { 'uploadType' => 'resumable' },
|
||||
:body_object => metadata,
|
||||
:media => media )
|
||||
upload = result.resumable_upload
|
||||
|
||||
# Resume if needed
|
||||
if upload.resumable?
|
||||
client.execute(upload)
|
||||
end
|
||||
```
|
||||
|
||||
## Samples
|
||||
|
||||
See the full list of [samples on Github](https://github.com/google/google-api-ruby-client-samples).
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
Please [report bugs at the project on Github](https://github.com/google/google-api-ruby-client/issues). Don't hesitate to [ask questions](http://stackoverflow.com/questions/tagged/google-api-ruby-client) about the client or APIs on [StackOverflow](http://stackoverflow.com).
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
lib_dir = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib_dir)
|
||||
$LOAD_PATH.uniq!
|
||||
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
|
||||
require File.join(File.dirname(__FILE__), 'lib/google/api_client', 'version')
|
||||
|
||||
PKG_DISPLAY_NAME = 'Google API Client'
|
||||
PKG_NAME = PKG_DISPLAY_NAME.downcase.gsub(/\s/, '-')
|
||||
PKG_VERSION = Google::APIClient::VERSION::STRING
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
PKG_HOMEPAGE = 'https://github.com/google/google-api-ruby-client'
|
||||
|
||||
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||
|
||||
PKG_AUTHOR = ["Bob Aman", "Steve Bazyl"]
|
||||
PKG_AUTHOR_EMAIL = "sbazyl@google.com"
|
||||
PKG_SUMMARY = 'Package Summary'
|
||||
PKG_DESCRIPTION = <<-TEXT
|
||||
The Google API Ruby Client makes it trivial to discover and access supported
|
||||
APIs.
|
||||
TEXT
|
||||
|
||||
list = FileList[
|
||||
'lib/**/*', 'spec/**/*', 'vendor/**/*',
|
||||
'tasks/**/*', 'website/**/*',
|
||||
'[A-Z]*', 'Rakefile'
|
||||
].exclude(/[_\.]git$/)
|
||||
(open(".gitignore") { |file| file.read }).split("\n").each do |pattern|
|
||||
list.exclude(pattern)
|
||||
end
|
||||
PKG_FILES = list
|
||||
|
||||
RCOV_ENABLED = !!(RUBY_PLATFORM != 'java' && RUBY_VERSION =~ /^1\.8/)
|
||||
if RCOV_ENABLED
|
||||
task :default => 'spec:rcov'
|
||||
else
|
||||
task :default => 'spec'
|
||||
end
|
||||
|
||||
WINDOWS = (RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/) rescue false
|
||||
SUDO = WINDOWS ? '' : ('sudo' unless ENV['SUDOLESS'])
|
||||
|
||||
Dir['tasks/**/*.rake'].each { |rake| load rake }
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
require File.expand_path('../lib/google/api_client/version', __FILE__)
|
||||
|
||||
Gem::Specification.new do |gem|
|
||||
gem.authors = ["Seth Call"]
|
||||
gem.email = ["seth@jamkazam.com"]
|
||||
gem.description = %q{Common library for JamKazam Ruby code}
|
||||
gem.summary = %q{Common library for JamKazam Ruby code}
|
||||
gem.homepage = ""
|
||||
|
||||
gem.files = `git ls-files`.split($\)
|
||||
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
||||
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
||||
gem.name = "google-api-client"
|
||||
gem.require_paths = ["lib"]
|
||||
gem.version = ::Google::APIClient::VERSION::TINY
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
|||
require 'multi_json'
|
||||
|
||||
if !MultiJson.respond_to?(:load) || [
|
||||
Kernel,
|
||||
defined?(ActiveSupport::Dependencies::Loadable) && ActiveSupport::Dependencies::Loadable
|
||||
].compact.include?(MultiJson.method(:load).owner)
|
||||
module MultiJson
|
||||
class <<self
|
||||
alias :load :decode
|
||||
end
|
||||
end
|
||||
end
|
||||
if !MultiJson.respond_to?(:dump)
|
||||
module MultiJson
|
||||
class <<self
|
||||
alias :dump :encode
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,672 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'faraday'
|
||||
require 'multi_json'
|
||||
require 'compat/multi_json'
|
||||
require 'stringio'
|
||||
require 'retriable'
|
||||
|
||||
require 'google/api_client/version'
|
||||
require 'google/api_client/logging'
|
||||
require 'google/api_client/errors'
|
||||
require 'google/api_client/environment'
|
||||
require 'google/api_client/discovery'
|
||||
require 'google/api_client/request'
|
||||
require 'google/api_client/reference'
|
||||
require 'google/api_client/result'
|
||||
require 'google/api_client/media'
|
||||
require 'google/api_client/service_account'
|
||||
require 'google/api_client/batch'
|
||||
require 'google/api_client/gzip'
|
||||
require 'google/api_client/client_secrets'
|
||||
require 'google/api_client/railtie' if defined?(Rails::Railtie)
|
||||
|
||||
module Google
|
||||
|
||||
##
|
||||
# This class manages APIs communication.
|
||||
class APIClient
|
||||
include Google::APIClient::Logging
|
||||
|
||||
##
|
||||
# Creates a new Google API client.
|
||||
#
|
||||
# @param [Hash] options The configuration parameters for the client.
|
||||
# @option options [Symbol, #generate_authenticated_request] :authorization
|
||||
# (:oauth_1)
|
||||
# The authorization mechanism used by the client. The following
|
||||
# mechanisms are supported out-of-the-box:
|
||||
# <ul>
|
||||
# <li><code>:two_legged_oauth_1</code></li>
|
||||
# <li><code>:oauth_1</code></li>
|
||||
# <li><code>:oauth_2</code></li>
|
||||
# </ul>
|
||||
# @option options [Boolean] :auto_refresh_token (true)
|
||||
# The setting that controls whether or not the api client attempts to
|
||||
# refresh authorization when a 401 is hit in #execute. If the token does
|
||||
# not support it, this option is ignored.
|
||||
# @option options [String] :application_name
|
||||
# The name of the application using the client.
|
||||
# @option options [String] :application_version
|
||||
# The version number of the application using the client.
|
||||
# @option options [String] :user_agent
|
||||
# ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
|
||||
# The user agent used by the client. Most developers will want to
|
||||
# leave this value alone and use the `:application_name` option instead.
|
||||
# @option options [String] :host ("www.googleapis.com")
|
||||
# The API hostname used by the client. This rarely needs to be changed.
|
||||
# @option options [String] :port (443)
|
||||
# The port number used by the client. This rarely needs to be changed.
|
||||
# @option options [String] :discovery_path ("/discovery/v1")
|
||||
# The discovery base path. This rarely needs to be changed.
|
||||
# @option options [String] :ca_file
|
||||
# Optional set of root certificates to use when validating SSL connections.
|
||||
# By default, a bundled set of trusted roots will be used.
|
||||
def initialize(options={})
|
||||
logger.debug { "#{self.class} - Initializing client with options #{options}" }
|
||||
|
||||
# Normalize key to String to allow indifferent access.
|
||||
options = options.inject({}) do |accu, (key, value)|
|
||||
accu[key.to_sym] = value
|
||||
accu
|
||||
end
|
||||
# Almost all API usage will have a host of 'www.googleapis.com'.
|
||||
self.host = options[:host] || 'www.googleapis.com'
|
||||
self.port = options[:port] || 443
|
||||
self.discovery_path = options[:discovery_path] || '/discovery/v1'
|
||||
|
||||
# Most developers will want to leave this value alone and use the
|
||||
# application_name option.
|
||||
if options[:application_name]
|
||||
app_name = options[:application_name]
|
||||
app_version = options[:application_version]
|
||||
application_string = "#{app_name}/#{app_version || '0.0.0'}"
|
||||
else
|
||||
logger.warn { "#{self.class} - Please provide :application_name and :application_version when initializing the client" }
|
||||
end
|
||||
self.user_agent = options[:user_agent] || (
|
||||
"#{application_string} " +
|
||||
"google-api-ruby-client/#{Google::APIClient::VERSION::STRING} #{ENV::OS_VERSION} (gzip)"
|
||||
).strip
|
||||
# The writer method understands a few Symbols and will generate useful
|
||||
# default authentication mechanisms.
|
||||
self.authorization =
|
||||
options.key?(:authorization) ? options[:authorization] : :oauth_2
|
||||
self.auto_refresh_token = options.fetch(:auto_refresh_token) { true }
|
||||
self.key = options[:key]
|
||||
self.user_ip = options[:user_ip]
|
||||
self.retries = options.fetch(:retries) { 0 }
|
||||
@discovery_uris = {}
|
||||
@discovery_documents = {}
|
||||
@discovered_apis = {}
|
||||
ca_file = options[:ca_file] || File.expand_path('../../cacerts.pem', __FILE__)
|
||||
self.connection = Faraday.new do |faraday|
|
||||
faraday.response :gzip
|
||||
faraday.options.params_encoder = Faraday::FlatParamsEncoder
|
||||
faraday.ssl.ca_file = ca_file
|
||||
faraday.ssl.verify = true
|
||||
faraday.adapter Faraday.default_adapter
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the authorization mechanism used by the client.
|
||||
#
|
||||
# @return [#generate_authenticated_request] The authorization mechanism.
|
||||
attr_reader :authorization
|
||||
|
||||
##
|
||||
# Sets the authorization mechanism used by the client.
|
||||
#
|
||||
# @param [#generate_authenticated_request] new_authorization
|
||||
# The new authorization mechanism.
|
||||
def authorization=(new_authorization)
|
||||
case new_authorization
|
||||
when :oauth_1, :oauth
|
||||
require 'signet/oauth_1/client'
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
new_authorization = Signet::OAuth1::Client.new(
|
||||
:temporary_credential_uri =>
|
||||
'https://www.google.com/accounts/OAuthGetRequestToken',
|
||||
:authorization_uri =>
|
||||
'https://www.google.com/accounts/OAuthAuthorizeToken',
|
||||
:token_credential_uri =>
|
||||
'https://www.google.com/accounts/OAuthGetAccessToken',
|
||||
:client_credential_key => 'anonymous',
|
||||
:client_credential_secret => 'anonymous'
|
||||
)
|
||||
when :two_legged_oauth_1, :two_legged_oauth
|
||||
require 'signet/oauth_1/client'
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
new_authorization = Signet::OAuth1::Client.new(
|
||||
:client_credential_key => nil,
|
||||
:client_credential_secret => nil,
|
||||
:two_legged => true
|
||||
)
|
||||
when :oauth_2
|
||||
require 'signet/oauth_2/client'
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
new_authorization = Signet::OAuth2::Client.new(
|
||||
:authorization_uri =>
|
||||
'https://accounts.google.com/o/oauth2/auth',
|
||||
:token_credential_uri =>
|
||||
'https://accounts.google.com/o/oauth2/token'
|
||||
)
|
||||
when nil
|
||||
# No authorization mechanism
|
||||
else
|
||||
if !new_authorization.respond_to?(:generate_authenticated_request)
|
||||
raise TypeError,
|
||||
'Expected authorization mechanism to respond to ' +
|
||||
'#generate_authenticated_request.'
|
||||
end
|
||||
end
|
||||
@authorization = new_authorization
|
||||
return @authorization
|
||||
end
|
||||
|
||||
##
|
||||
# Default Faraday/HTTP connection.
|
||||
#
|
||||
# @return [Faraday::Connection]
|
||||
attr_accessor :connection
|
||||
|
||||
##
|
||||
# The setting that controls whether or not the api client attempts to
|
||||
# refresh authorization when a 401 is hit in #execute.
|
||||
#
|
||||
# @return [Boolean]
|
||||
attr_accessor :auto_refresh_token
|
||||
|
||||
##
|
||||
# The application's API key issued by the API console.
|
||||
#
|
||||
# @return [String] The API key.
|
||||
attr_accessor :key
|
||||
|
||||
##
|
||||
# The IP address of the user this request is being performed on behalf of.
|
||||
#
|
||||
# @return [String] The user's IP address.
|
||||
attr_accessor :user_ip
|
||||
|
||||
##
|
||||
# The user agent used by the client.
|
||||
#
|
||||
# @return [String]
|
||||
# The user agent string used in the User-Agent header.
|
||||
attr_accessor :user_agent
|
||||
|
||||
##
|
||||
# The API hostname used by the client.
|
||||
#
|
||||
# @return [String]
|
||||
# The API hostname. Should almost always be 'www.googleapis.com'.
|
||||
attr_accessor :host
|
||||
|
||||
##
|
||||
# The port number used by the client.
|
||||
#
|
||||
# @return [String]
|
||||
# The port number. Should almost always be 443.
|
||||
attr_accessor :port
|
||||
|
||||
##
|
||||
# The base path used by the client for discovery.
|
||||
#
|
||||
# @return [String]
|
||||
# The base path. Should almost always be '/discovery/v1'.
|
||||
attr_accessor :discovery_path
|
||||
|
||||
##
|
||||
# Number of times to retry on recoverable errors
|
||||
#
|
||||
# @return [FixNum]
|
||||
# Number of retries
|
||||
attr_accessor :retries
|
||||
|
||||
##
|
||||
# Returns the URI for the directory document.
|
||||
#
|
||||
# @return [Addressable::URI] The URI of the directory document.
|
||||
def directory_uri
|
||||
return resolve_uri(self.discovery_path + '/apis')
|
||||
end
|
||||
|
||||
##
|
||||
# Manually registers a URI as a discovery document for a specific version
|
||||
# of an API.
|
||||
#
|
||||
# @param [String, Symbol] api The API name.
|
||||
# @param [String] version The desired version of the API.
|
||||
# @param [Addressable::URI] uri The URI of the discovery document.
|
||||
def register_discovery_uri(api, version, uri)
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
@discovery_uris["#{api}:#{version}"] = uri
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the URI for the discovery document.
|
||||
#
|
||||
# @param [String, Symbol] api The API name.
|
||||
# @param [String] version The desired version of the API.
|
||||
# @return [Addressable::URI] The URI of the discovery document.
|
||||
def discovery_uri(api, version=nil)
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
return @discovery_uris["#{api}:#{version}"] ||= (
|
||||
resolve_uri(
|
||||
self.discovery_path + '/apis/{api}/{version}/rest',
|
||||
'api' => api,
|
||||
'version' => version
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Manually registers a pre-loaded discovery document for a specific version
|
||||
# of an API.
|
||||
#
|
||||
# @param [String, Symbol] api The API name.
|
||||
# @param [String] version The desired version of the API.
|
||||
# @param [String, StringIO] discovery_document
|
||||
# The contents of the discovery document.
|
||||
def register_discovery_document(api, version, discovery_document)
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
if discovery_document.kind_of?(StringIO)
|
||||
discovery_document.rewind
|
||||
discovery_document = discovery_document.string
|
||||
elsif discovery_document.respond_to?(:to_str)
|
||||
discovery_document = discovery_document.to_str
|
||||
else
|
||||
raise TypeError,
|
||||
"Expected String or StringIO, got #{discovery_document.class}."
|
||||
end
|
||||
@discovery_documents["#{api}:#{version}"] =
|
||||
MultiJson.load(discovery_document)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the parsed directory document.
|
||||
#
|
||||
# @return [Hash] The parsed JSON from the directory document.
|
||||
def directory_document
|
||||
return @directory_document ||= (begin
|
||||
response = self.execute!(
|
||||
:http_method => :get,
|
||||
:uri => self.directory_uri,
|
||||
:authenticated => false
|
||||
)
|
||||
response.data
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the parsed discovery document.
|
||||
#
|
||||
# @param [String, Symbol] api The API name.
|
||||
# @param [String] version The desired version of the API.
|
||||
# @return [Hash] The parsed JSON from the discovery document.
|
||||
def discovery_document(api, version=nil)
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
return @discovery_documents["#{api}:#{version}"] ||= (begin
|
||||
response = self.execute!(
|
||||
:http_method => :get,
|
||||
:uri => self.discovery_uri(api, version),
|
||||
:authenticated => false
|
||||
)
|
||||
response.data
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns all APIs published in the directory document.
|
||||
#
|
||||
# @return [Array] The list of available APIs.
|
||||
def discovered_apis
|
||||
@directory_apis ||= (begin
|
||||
document_base = self.directory_uri
|
||||
if self.directory_document && self.directory_document['items']
|
||||
self.directory_document['items'].map do |discovery_document|
|
||||
Google::APIClient::API.new(
|
||||
document_base,
|
||||
discovery_document
|
||||
)
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the service object for a given service name and service version.
|
||||
#
|
||||
# @param [String, Symbol] api The API name.
|
||||
# @param [String] version The desired version of the API.
|
||||
#
|
||||
# @return [Google::APIClient::API] The service object.
|
||||
def discovered_api(api, version=nil)
|
||||
if !api.kind_of?(String) && !api.kind_of?(Symbol)
|
||||
raise TypeError,
|
||||
"Expected String or Symbol, got #{api.class}."
|
||||
end
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
return @discovered_apis["#{api}:#{version}"] ||= begin
|
||||
document_base = self.discovery_uri(api, version)
|
||||
discovery_document = self.discovery_document(api, version)
|
||||
if document_base && discovery_document
|
||||
Google::APIClient::API.new(
|
||||
document_base,
|
||||
discovery_document
|
||||
)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the method object for a given RPC name and service version.
|
||||
#
|
||||
# @param [String, Symbol] rpc_name The RPC name of the desired method.
|
||||
# @param [String, Symbol] api The API the method is within.
|
||||
# @param [String] version The desired version of the API.
|
||||
#
|
||||
# @return [Google::APIClient::Method] The method object.
|
||||
def discovered_method(rpc_name, api, version=nil)
|
||||
if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
|
||||
raise TypeError,
|
||||
"Expected String or Symbol, got #{rpc_name.class}."
|
||||
end
|
||||
rpc_name = rpc_name.to_s
|
||||
api = api.to_s
|
||||
version = version || 'v1'
|
||||
service = self.discovered_api(api, version)
|
||||
if service.to_h[rpc_name]
|
||||
return service.to_h[rpc_name]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the service object with the highest version number.
|
||||
#
|
||||
# @note <em>Warning</em>: This method should be used with great care.
|
||||
# As APIs are updated, minor differences between versions may cause
|
||||
# incompatibilities. Requesting a specific version will avoid this issue.
|
||||
#
|
||||
# @param [String, Symbol] api The name of the service.
|
||||
#
|
||||
# @return [Google::APIClient::API] The service object.
|
||||
def preferred_version(api)
|
||||
if !api.kind_of?(String) && !api.kind_of?(Symbol)
|
||||
raise TypeError,
|
||||
"Expected String or Symbol, got #{api.class}."
|
||||
end
|
||||
api = api.to_s
|
||||
return self.discovered_apis.detect do |a|
|
||||
a.name == api && a.preferred == true
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Verifies an ID token against a server certificate. Used to ensure that
|
||||
# an ID token supplied by an untrusted client-side mechanism is valid.
|
||||
# Raises an error if the token is invalid or missing.
|
||||
#
|
||||
# @deprecated Use the google-id-token gem for verifying JWTs
|
||||
def verify_id_token!
|
||||
require 'jwt'
|
||||
require 'openssl'
|
||||
@certificates ||= {}
|
||||
if !self.authorization.respond_to?(:id_token)
|
||||
raise ArgumentError, (
|
||||
"Current authorization mechanism does not support ID tokens: " +
|
||||
"#{self.authorization.class.to_s}"
|
||||
)
|
||||
elsif !self.authorization.id_token
|
||||
raise ArgumentError, (
|
||||
"Could not verify ID token, ID token missing. " +
|
||||
"Scopes were: #{self.authorization.scope.inspect}"
|
||||
)
|
||||
else
|
||||
check_cached_certs = lambda do
|
||||
valid = false
|
||||
for key, cert in @certificates
|
||||
begin
|
||||
self.authorization.decoded_id_token(cert.public_key)
|
||||
valid = true
|
||||
rescue JWT::DecodeError, Signet::UnsafeOperationError
|
||||
# Expected exception. Ignore, ID token has not been validated.
|
||||
end
|
||||
end
|
||||
valid
|
||||
end
|
||||
if check_cached_certs.call()
|
||||
return true
|
||||
end
|
||||
response = self.execute!(
|
||||
:http_method => :get,
|
||||
:uri => 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
:authenticated => false
|
||||
)
|
||||
@certificates.merge!(
|
||||
Hash[MultiJson.load(response.body).map do |key, cert|
|
||||
[key, OpenSSL::X509::Certificate.new(cert)]
|
||||
end]
|
||||
)
|
||||
if check_cached_certs.call()
|
||||
return true
|
||||
else
|
||||
raise InvalidIDTokenError,
|
||||
"Could not verify ID token against any available certificate."
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
##
|
||||
# Generates a request.
|
||||
#
|
||||
# @option options [Google::APIClient::Method] :api_method
|
||||
# The method object or the RPC name of the method being executed.
|
||||
# @option options [Hash, Array] :parameters
|
||||
# The parameters to send to the method.
|
||||
# @option options [Hash, Array] :headers The HTTP headers for the request.
|
||||
# @option options [String] :body The body of the request.
|
||||
# @option options [String] :version ("v1")
|
||||
# The service version. Only used if `api_method` is a `String`.
|
||||
# @option options [#generate_authenticated_request] :authorization
|
||||
# The authorization mechanism for the response. Used only if
|
||||
# `:authenticated` is `true`.
|
||||
# @option options [TrueClass, FalseClass] :authenticated (true)
|
||||
# `true` if the request must be signed or somehow
|
||||
# authenticated, `false` otherwise.
|
||||
#
|
||||
# @return [Google::APIClient::Reference] The generated request.
|
||||
#
|
||||
# @example
|
||||
# request = client.generate_request(
|
||||
# :api_method => 'plus.activities.list',
|
||||
# :parameters =>
|
||||
# {'collection' => 'public', 'userId' => 'me'}
|
||||
# )
|
||||
def generate_request(options={})
|
||||
options = {
|
||||
:api_client => self
|
||||
}.merge(options)
|
||||
return Google::APIClient::Request.new(options)
|
||||
end
|
||||
|
||||
##
|
||||
# Executes a request, wrapping it in a Result object.
|
||||
#
|
||||
# @param [Google::APIClient::Request, Hash, Array] params
|
||||
# Either a Google::APIClient::Request, a Hash, or an Array.
|
||||
#
|
||||
# If a Google::APIClient::Request, no other parameters are expected.
|
||||
#
|
||||
# If a Hash, the below parameters are handled. If an Array, the
|
||||
# parameters are assumed to be in the below order:
|
||||
#
|
||||
# - (Google::APIClient::Method) api_method:
|
||||
# The method object or the RPC name of the method being executed.
|
||||
# - (Hash, Array) parameters:
|
||||
# The parameters to send to the method.
|
||||
# - (String) body: The body of the request.
|
||||
# - (Hash, Array) headers: The HTTP headers for the request.
|
||||
# - (Hash) options: A set of options for the request, of which:
|
||||
# - (#generate_authenticated_request) :authorization (default: true) -
|
||||
# The authorization mechanism for the response. Used only if
|
||||
# `:authenticated` is `true`.
|
||||
# - (TrueClass, FalseClass) :authenticated (default: true) -
|
||||
# `true` if the request must be signed or somehow
|
||||
# authenticated, `false` otherwise.
|
||||
# - (TrueClass, FalseClass) :gzip (default: true) -
|
||||
# `true` if gzip enabled, `false` otherwise.
|
||||
# - (FixNum) :retries -
|
||||
# # of times to retry on recoverable errors
|
||||
#
|
||||
# @return [Google::APIClient::Result] The result from the API, nil if batch.
|
||||
#
|
||||
# @example
|
||||
# result = client.execute(batch_request)
|
||||
#
|
||||
# @example
|
||||
# plus = client.discovered_api('plus')
|
||||
# result = client.execute(
|
||||
# :api_method => plus.activities.list,
|
||||
# :parameters => {'collection' => 'public', 'userId' => 'me'}
|
||||
# )
|
||||
#
|
||||
# @see Google::APIClient#generate_request
|
||||
def execute!(*params)
|
||||
if params.first.kind_of?(Google::APIClient::Request)
|
||||
request = params.shift
|
||||
options = params.shift || {}
|
||||
else
|
||||
# This block of code allows us to accept multiple parameter passing
|
||||
# styles, and maintaining some backwards compatibility.
|
||||
#
|
||||
# Note: I'm extremely tempted to deprecate this style of execute call.
|
||||
if params.last.respond_to?(:to_hash) && params.size == 1
|
||||
options = params.pop
|
||||
else
|
||||
options = {}
|
||||
end
|
||||
|
||||
options[:api_method] = params.shift if params.size > 0
|
||||
options[:parameters] = params.shift if params.size > 0
|
||||
options[:body] = params.shift if params.size > 0
|
||||
options[:headers] = params.shift if params.size > 0
|
||||
options.update(params.shift) if params.size > 0
|
||||
request = self.generate_request(options)
|
||||
end
|
||||
|
||||
request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
|
||||
request.headers['Accept-Encoding'] ||= 'gzip' unless options[:gzip] == false
|
||||
request.headers['Content-Type'] ||= ''
|
||||
request.parameters['key'] ||= self.key unless self.key.nil?
|
||||
request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
|
||||
|
||||
connection = options[:connection] || self.connection
|
||||
request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
|
||||
tries = 1 + (options[:retries] || self.retries)
|
||||
Retriable.retriable :tries => tries,
|
||||
:on => [TransmissionError],
|
||||
:interval => lambda {|attempts| (2 ** attempts) + rand} do
|
||||
result = request.send(connection, true)
|
||||
|
||||
case result.status
|
||||
when 200...300
|
||||
result
|
||||
when 301, 302, 303, 307
|
||||
request = generate_request(request.to_hash.merge({
|
||||
:uri => result.headers['location'],
|
||||
:api_method => nil
|
||||
}))
|
||||
raise RedirectError.new(result.headers['location'], result)
|
||||
when 400...500
|
||||
if result.status == 401 && request.authorization.respond_to?(:refresh_token) && auto_refresh_token
|
||||
begin
|
||||
logger.debug("Attempting refresh of access token & retry of request")
|
||||
request.authorization.fetch_access_token!
|
||||
rescue Signet::AuthorizationError
|
||||
# Ignore since we want the original error
|
||||
end
|
||||
end
|
||||
raise ClientError.new(result.error_message || "A client error has occurred", result)
|
||||
when 500...600
|
||||
raise ServerError.new(result.error_message || "A server error has occurred", result)
|
||||
else
|
||||
raise TransmissionError.new(result.error_message || "A transmission error has occurred", result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Same as Google::APIClient#execute!, but does not raise an exception for
|
||||
# normal API errros.
|
||||
#
|
||||
# @see Google::APIClient#execute
|
||||
def execute(*params)
|
||||
begin
|
||||
return self.execute!(*params)
|
||||
rescue TransmissionError => e
|
||||
return e.result
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
##
|
||||
# Resolves a URI template against the client's configured base.
|
||||
#
|
||||
# @api private
|
||||
# @param [String, Addressable::URI, Addressable::Template] template
|
||||
# The template to resolve.
|
||||
# @param [Hash] mapping The mapping that corresponds to the template.
|
||||
# @return [Addressable::URI] The expanded URI.
|
||||
def resolve_uri(template, mapping={})
|
||||
@base_uri ||= Addressable::URI.new(
|
||||
:scheme => 'https',
|
||||
:host => self.host,
|
||||
:port => self.port
|
||||
).normalize
|
||||
template = if template.kind_of?(Addressable::Template)
|
||||
template.pattern
|
||||
elsif template.respond_to?(:to_str)
|
||||
template.to_str
|
||||
else
|
||||
raise TypeError,
|
||||
"Expected String, Addressable::URI, or Addressable::Template, " +
|
||||
"got #{template.class}."
|
||||
end
|
||||
return Addressable::Template.new(@base_uri + template).expand(mapping)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
require 'google/api_client/version'
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'faraday'
|
||||
require 'signet/oauth_2/client'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class ComputeServiceAccount < Signet::OAuth2::Client
|
||||
def fetch_access_token(options={})
|
||||
connection = options[:connection] || Faraday.default_connection
|
||||
response = connection.get 'http://metadata/computeMetadata/v1beta1/instance/service-accounts/default/token'
|
||||
Signet::OAuth2.parse_json_credentials(response.body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'json'
|
||||
require 'signet/oauth_2/client'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Represents cached OAuth 2 tokens stored on local disk in a
|
||||
# JSON serialized file. Meant to resemble the serialized format
|
||||
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
|
||||
#
|
||||
class FileStorage
|
||||
# @return [String] Path to the credentials file.
|
||||
attr_accessor :path
|
||||
|
||||
# @return [Signet::OAuth2::Client] Path to the credentials file.
|
||||
attr_reader :authorization
|
||||
|
||||
##
|
||||
# Initializes the FileStorage object.
|
||||
#
|
||||
# @param [String] path
|
||||
# Path to the credentials file.
|
||||
def initialize(path)
|
||||
@path = path
|
||||
self.load_credentials
|
||||
end
|
||||
|
||||
##
|
||||
# Attempt to read in credentials from the specified file.
|
||||
def load_credentials
|
||||
if File.exist? self.path
|
||||
File.open(self.path, 'r') do |file|
|
||||
cached_credentials = JSON.load(file)
|
||||
@authorization = Signet::OAuth2::Client.new(cached_credentials)
|
||||
@authorization.issued_at = Time.at(cached_credentials['issued_at'])
|
||||
if @authorization.expired?
|
||||
@authorization.fetch_access_token!
|
||||
self.write_credentials
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Write the credentials to the specified file.
|
||||
#
|
||||
# @param [Signet::OAuth2::Client] authorization
|
||||
# Optional authorization instance. If not provided, the authorization
|
||||
# already associated with this instance will be written.
|
||||
def write_credentials(authorization=nil)
|
||||
@authorization = authorization unless authorization.nil?
|
||||
|
||||
unless @authorization.refresh_token.nil?
|
||||
hash = {}
|
||||
%w'access_token
|
||||
authorization_uri
|
||||
client_id
|
||||
client_secret
|
||||
expires_in
|
||||
refresh_token
|
||||
token_credential_uri'.each do |var|
|
||||
hash[var] = @authorization.instance_variable_get("@#{var}")
|
||||
end
|
||||
hash['issued_at'] = @authorization.issued_at.to_i
|
||||
|
||||
File.open(self.path, 'w', 0600) do |file|
|
||||
file.write(hash.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'webrick'
|
||||
require 'launchy'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
# Small helper for the sample apps for performing OAuth 2.0 flows from the command
|
||||
# line or in any other installed app environment.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# client = Google::APIClient.new
|
||||
# flow = Google::APIClient::InstalledAppFlow.new(
|
||||
# :client_id => '691380668085.apps.googleusercontent.com',
|
||||
# :client_secret => '...,
|
||||
# :scope => 'https://www.googleapis.com/auth/drive'
|
||||
# )
|
||||
# client.authorization = flow.authorize
|
||||
#
|
||||
class InstalledAppFlow
|
||||
|
||||
RESPONSE_BODY = <<-HTML
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.open('', '_self', '');
|
||||
window.close();
|
||||
}
|
||||
setTimeout(closeWindow, 10);
|
||||
</script>
|
||||
</head>
|
||||
<body>You may close this window.</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
##
|
||||
# Configure the flow
|
||||
#
|
||||
# @param [Hash] options The configuration parameters for the client.
|
||||
# @option options [Fixnum] :port
|
||||
# Port to run the embedded server on. Defaults to 9292
|
||||
# @option options [String] :client_id
|
||||
# A unique identifier issued to the client to identify itself to the
|
||||
# authorization server.
|
||||
# @option options [String] :client_secret
|
||||
# A shared symmetric secret issued by the authorization server,
|
||||
# which is used to authenticate the client.
|
||||
# @option options [String] :scope
|
||||
# The scope of the access request, expressed either as an Array
|
||||
# or as a space-delimited String.
|
||||
#
|
||||
# @see Signet::OAuth2::Client
|
||||
def initialize(options)
|
||||
@port = options[:port] || 9292
|
||||
@authorization = Signet::OAuth2::Client.new({
|
||||
:authorization_uri => 'https://accounts.google.com/o/oauth2/auth',
|
||||
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
||||
:redirect_uri => "http://localhost:#{@port}/"}.update(options)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Request authorization. Opens a browser and waits for response.
|
||||
#
|
||||
# @param [Google::APIClient::FileStorage] storage
|
||||
# Optional object that responds to :write_credentials, used to serialize
|
||||
# the OAuth 2 credentials after completing the flow.
|
||||
#
|
||||
# @return [Signet::OAuth2::Client]
|
||||
# Authorization instance, nil if user cancelled.
|
||||
def authorize(storage=nil)
|
||||
auth = @authorization
|
||||
|
||||
server = WEBrick::HTTPServer.new(
|
||||
:Port => @port,
|
||||
:BindAddress =>"localhost",
|
||||
:Logger => WEBrick::Log.new(STDOUT, 0),
|
||||
:AccessLog => []
|
||||
)
|
||||
trap("INT") { server.shutdown }
|
||||
|
||||
server.mount_proc '/' do |req, res|
|
||||
auth.code = req.query['code']
|
||||
if auth.code
|
||||
auth.fetch_access_token!
|
||||
end
|
||||
res.status = WEBrick::HTTPStatus::RC_ACCEPTED
|
||||
res.body = RESPONSE_BODY
|
||||
server.stop
|
||||
end
|
||||
|
||||
Launchy.open(auth.authorization_uri.to_s)
|
||||
server.start
|
||||
if @authorization.access_token
|
||||
if storage.respond_to?(:write_credentials)
|
||||
storage.write_credentials(@authorization)
|
||||
end
|
||||
return @authorization
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'jwt'
|
||||
require 'signet/oauth_2/client'
|
||||
require 'delegate'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Generates access tokens using the JWT assertion profile. Requires a
|
||||
# service account & access to the private key.
|
||||
#
|
||||
# @example Using Signet
|
||||
#
|
||||
# key = Google::APIClient::KeyUtils.load_from_pkcs12('client.p12', 'notasecret')
|
||||
# client.authorization = Signet::OAuth2::Client.new(
|
||||
# :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
||||
# :audience => 'https://accounts.google.com/o/oauth2/token',
|
||||
# :scope => 'https://www.googleapis.com/auth/prediction',
|
||||
# :issuer => '123456-abcdef@developer.gserviceaccount.com',
|
||||
# :signing_key => key)
|
||||
# client.authorization.fetch_access_token!
|
||||
# client.execute(...)
|
||||
#
|
||||
# @deprecated
|
||||
# Service accounts are now supported directly in Signet
|
||||
# @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount
|
||||
class JWTAsserter
|
||||
# @return [String] ID/email of the issuing party
|
||||
attr_accessor :issuer
|
||||
# @return [Fixnum] How long, in seconds, the assertion is valid for
|
||||
attr_accessor :expiry
|
||||
# @return [Fixnum] Seconds to expand the issued at/expiry window to account for clock skew
|
||||
attr_accessor :skew
|
||||
# @return [String] Scopes to authorize
|
||||
attr_reader :scope
|
||||
# @return [String,OpenSSL::PKey] key for signing assertions
|
||||
attr_writer :key
|
||||
# @return [String] Algorithm used for signing
|
||||
attr_accessor :algorithm
|
||||
|
||||
##
|
||||
# Initializes the asserter for a service account.
|
||||
#
|
||||
# @param [String] issuer
|
||||
# Name/ID of the client issuing the assertion
|
||||
# @param [String, Array] scope
|
||||
# Scopes to authorize. May be a space delimited string or array of strings
|
||||
# @param [String,OpenSSL::PKey] key
|
||||
# Key for signing assertions
|
||||
# @param [String] algorithm
|
||||
# Algorithm to use, either 'RS256' for RSA with SHA-256
|
||||
# or 'HS256' for HMAC with SHA-256
|
||||
def initialize(issuer, scope, key, algorithm = "RS256")
|
||||
self.issuer = issuer
|
||||
self.scope = scope
|
||||
self.expiry = 60 # 1 min default
|
||||
self.skew = 60
|
||||
self.key = key
|
||||
self.algorithm = algorithm
|
||||
end
|
||||
|
||||
##
|
||||
# Set the scopes to authorize
|
||||
#
|
||||
# @param [String, Array] new_scope
|
||||
# Scopes to authorize. May be a space delimited string or array of strings
|
||||
def scope=(new_scope)
|
||||
case new_scope
|
||||
when Array
|
||||
@scope = new_scope.join(' ')
|
||||
when String
|
||||
@scope = new_scope
|
||||
when nil
|
||||
@scope = ''
|
||||
else
|
||||
raise TypeError, "Expected Array or String, got #{new_scope.class}"
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Request a new access token.
|
||||
#
|
||||
# @param [String] person
|
||||
# Email address of a user, if requesting a token to act on their behalf
|
||||
# @param [Hash] options
|
||||
# Pass through to Signet::OAuth2::Client.fetch_access_token
|
||||
# @return [Signet::OAuth2::Client] Access token
|
||||
#
|
||||
# @see Signet::OAuth2::Client.fetch_access_token!
|
||||
def authorize(person = nil, options={})
|
||||
authorization = self.to_authorization(person)
|
||||
authorization.fetch_access_token!(options)
|
||||
return authorization
|
||||
end
|
||||
|
||||
##
|
||||
# Builds a Signet OAuth2 client
|
||||
#
|
||||
# @return [Signet::OAuth2::Client] Access token
|
||||
def to_authorization(person = nil)
|
||||
return Signet::OAuth2::Client.new(
|
||||
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
||||
:audience => 'https://accounts.google.com/o/oauth2/token',
|
||||
:scope => self.scope,
|
||||
:issuer => @issuer,
|
||||
:signing_key => @key,
|
||||
:signing_algorithm => @algorithm,
|
||||
:person => person
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Helper for loading keys from the PKCS12 files downloaded when
|
||||
# setting up service accounts at the APIs Console.
|
||||
#
|
||||
module KeyUtils
|
||||
##
|
||||
# Loads a key from PKCS12 file, assuming a single private key
|
||||
# is present.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of the PKCS12 file to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
def self.load_from_pkcs12(keyfile, passphrase)
|
||||
load_key(keyfile, passphrase) do |content, passphrase|
|
||||
OpenSSL::PKCS12.new(content, passphrase).key
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Loads a key from a PEM file.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of the PEM file to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
#
|
||||
def self.load_from_pem(keyfile, passphrase)
|
||||
load_key(keyfile, passphrase) do | content, passphrase|
|
||||
OpenSSL::PKey::RSA.new(content, passphrase)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Helper for loading keys from file or memory. Accepts a block
|
||||
# to handle the specific file format.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of thefile to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @yield [String, String]
|
||||
# Key file & passphrase to extract key from
|
||||
# @yieldparam [String] keyfile
|
||||
# Contents of the file
|
||||
# @yieldparam [String] passphrase
|
||||
# Passphrase to unlock key
|
||||
# @yieldreturn [OpenSSL::PKey]
|
||||
# Private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
def self.load_key(keyfile, passphrase, &block)
|
||||
begin
|
||||
begin
|
||||
content = File.open(keyfile, 'rb') { |io| io.read }
|
||||
rescue
|
||||
content = keyfile
|
||||
end
|
||||
block.call(content, passphrase)
|
||||
rescue OpenSSL::OpenSSLError
|
||||
raise ArgumentError.new("Invalid keyfile or passphrase")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/auth/key_utils'
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Helper for loading keys from the PKCS12 files downloaded when
|
||||
# setting up service accounts at the APIs Console.
|
||||
#
|
||||
module PKCS12
|
||||
##
|
||||
# Loads a key from PKCS12 file, assuming a single private key
|
||||
# is present.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of the PKCS12 file to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
# @deprecated
|
||||
# Use {Google::APIClient::KeyUtils} instead
|
||||
def self.load_key(keyfile, passphrase)
|
||||
KeyUtils.load_from_pkcs12(keyfile, passphrase)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'addressable/uri'
|
||||
require 'google/api_client/reference'
|
||||
require 'uuidtools'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
##
|
||||
# Helper class to contain a response to an individual batched call.
|
||||
#
|
||||
# @api private
|
||||
class BatchedCallResponse
|
||||
# @return [String] UUID of the call
|
||||
attr_reader :call_id
|
||||
# @return [Fixnum] HTTP status code
|
||||
attr_accessor :status
|
||||
# @return [Hash] HTTP response headers
|
||||
attr_accessor :headers
|
||||
# @return [String] HTTP response body
|
||||
attr_accessor :body
|
||||
|
||||
##
|
||||
# Initialize the call response
|
||||
#
|
||||
# @param [String] call_id
|
||||
# UUID of the original call
|
||||
# @param [Fixnum] status
|
||||
# HTTP status
|
||||
# @param [Hash] headers
|
||||
# HTTP response headers
|
||||
# @param [#read, #to_str] body
|
||||
# Response body
|
||||
def initialize(call_id, status = nil, headers = nil, body = nil)
|
||||
@call_id, @status, @headers, @body = call_id, status, headers, body
|
||||
end
|
||||
end
|
||||
|
||||
# Wraps multiple API calls into a single over-the-wire HTTP request.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# client = Google::APIClient.new
|
||||
# urlshortener = client.discovered_api('urlshortener')
|
||||
# batch = Google::APIClient::BatchRequest.new do |result|
|
||||
# puts result.data
|
||||
# end
|
||||
#
|
||||
# batch.add(:api_method => urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/foo' })
|
||||
# batch.add(:api_method => urlshortener.url.insert, :body_object => { 'longUrl' => 'http://example.com/bar' })
|
||||
#
|
||||
# client.execute(batch)
|
||||
#
|
||||
class BatchRequest < Request
|
||||
BATCH_BOUNDARY = "-----------RubyApiBatchRequest".freeze
|
||||
|
||||
# @api private
|
||||
# @return [Array<(String,Google::APIClient::Request,Proc)] List of API calls in the batch
|
||||
attr_reader :calls
|
||||
|
||||
##
|
||||
# Creates a new batch request.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# Set of options for this request.
|
||||
# @param [Proc] block
|
||||
# Callback for every call's response. Won't be called if a call defined
|
||||
# a callback of its own.
|
||||
#
|
||||
# @return [Google::APIClient::BatchRequest]
|
||||
# The constructed object.
|
||||
#
|
||||
# @yield [Google::APIClient::Result]
|
||||
# block to be called when result ready
|
||||
def initialize(options = {}, &block)
|
||||
@calls = []
|
||||
@global_callback = block if block_given?
|
||||
@last_auto_id = 0
|
||||
|
||||
# TODO(sgomes): Use SecureRandom.uuid, drop UUIDTools when we drop 1.8
|
||||
@base_id = UUIDTools::UUID.random_create.to_s
|
||||
|
||||
options[:uri] ||= 'https://www.googleapis.com/batch'
|
||||
options[:http_method] ||= 'POST'
|
||||
|
||||
super options
|
||||
end
|
||||
|
||||
##
|
||||
# Add a new call to the batch request.
|
||||
# Each call must have its own call ID; if not provided, one will
|
||||
# automatically be generated, avoiding collisions. If duplicate call IDs
|
||||
# are provided, an error will be thrown.
|
||||
#
|
||||
# @param [Hash, Google::APIClient::Request] call
|
||||
# the call to be added.
|
||||
# @param [String] call_id
|
||||
# the ID to be used for this call. Must be unique
|
||||
# @param [Proc] block
|
||||
# callback for this call's response.
|
||||
#
|
||||
# @return [Google::APIClient::BatchRequest]
|
||||
# the BatchRequest, for chaining
|
||||
#
|
||||
# @yield [Google::APIClient::Result]
|
||||
# block to be called when result ready
|
||||
def add(call, call_id = nil, &block)
|
||||
unless call.kind_of?(Google::APIClient::Reference)
|
||||
call = Google::APIClient::Reference.new(call)
|
||||
end
|
||||
call_id ||= new_id
|
||||
if @calls.assoc(call_id)
|
||||
raise BatchError,
|
||||
'A call with this ID already exists: %s' % call_id
|
||||
end
|
||||
callback = block_given? ? block : @global_callback
|
||||
@calls << [call_id, call, callback]
|
||||
return self
|
||||
end
|
||||
|
||||
##
|
||||
# Processes the HTTP response to the batch request, issuing callbacks.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Faraday::Response] response
|
||||
# the HTTP response.
|
||||
def process_http_response(response)
|
||||
content_type = find_header('Content-Type', response.headers)
|
||||
boundary = /.*boundary=(.+)/.match(content_type)[1]
|
||||
parts = response.body.split(/--#{Regexp.escape(boundary)}/)
|
||||
parts = parts[1...-1]
|
||||
parts.each do |part|
|
||||
call_response = deserialize_call_response(part)
|
||||
_, call, callback = @calls.assoc(call_response.call_id)
|
||||
result = Google::APIClient::Result.new(call, call_response)
|
||||
callback.call(result) if callback
|
||||
end
|
||||
Google::APIClient::Result.new(self, response)
|
||||
end
|
||||
|
||||
##
|
||||
# Return the request body for the BatchRequest's HTTP request.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String]
|
||||
# the request body.
|
||||
def to_http_request
|
||||
if @calls.nil? || @calls.empty?
|
||||
raise BatchError, 'Cannot make an empty batch request'
|
||||
end
|
||||
parts = @calls.map {|(call_id, call, callback)| serialize_call(call_id, call)}
|
||||
build_multipart(parts, 'multipart/mixed', BATCH_BOUNDARY)
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
##
|
||||
# Helper method to find a header from its name, regardless of case.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] name
|
||||
# the name of the header to find.
|
||||
# @param [Hash] headers
|
||||
# the hash of headers and their values.
|
||||
#
|
||||
# @return [String]
|
||||
# the value of the desired header.
|
||||
def find_header(name, headers)
|
||||
_, header = headers.detect do |h, v|
|
||||
h.downcase == name.downcase
|
||||
end
|
||||
return header
|
||||
end
|
||||
|
||||
##
|
||||
# Create a new call ID. Uses an auto-incrementing, conflict-avoiding ID.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [String]
|
||||
# the new, unique ID.
|
||||
def new_id
|
||||
@last_auto_id += 1
|
||||
while @calls.assoc(@last_auto_id)
|
||||
@last_auto_id += 1
|
||||
end
|
||||
return @last_auto_id.to_s
|
||||
end
|
||||
|
||||
##
|
||||
# Convert a Content-ID header value to an id. Presumes the Content-ID
|
||||
# header conforms to the format that id_to_header() returns.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] header
|
||||
# Content-ID header value.
|
||||
#
|
||||
# @return [String]
|
||||
# The extracted ID value.
|
||||
def header_to_id(header)
|
||||
if !header.start_with?('<') || !header.end_with?('>') ||
|
||||
!header.include?('+')
|
||||
raise BatchError, 'Invalid value for Content-ID: "%s"' % header
|
||||
end
|
||||
|
||||
base, call_id = header[1...-1].split('+')
|
||||
return Addressable::URI.unencode(call_id)
|
||||
end
|
||||
|
||||
##
|
||||
# Auxiliary method to split the headers from the body in an HTTP response.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] response
|
||||
# the response to parse.
|
||||
#
|
||||
# @return [Array<Hash>, String]
|
||||
# the headers and the body, separately.
|
||||
def split_headers_and_body(response)
|
||||
headers = {}
|
||||
payload = response.lstrip
|
||||
while payload
|
||||
line, payload = payload.split("\n", 2)
|
||||
line.sub!(/\s+\z/, '')
|
||||
break if line.empty?
|
||||
match = /\A([^:]+):\s*/.match(line)
|
||||
if match
|
||||
headers[match[1]] = match.post_match
|
||||
else
|
||||
raise BatchError, 'Invalid header line in response: %s' % line
|
||||
end
|
||||
end
|
||||
return headers, payload
|
||||
end
|
||||
|
||||
##
|
||||
# Convert a single batched response into a BatchedCallResponse object.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] call_response
|
||||
# the request to deserialize.
|
||||
#
|
||||
# @return [Google::APIClient::BatchedCallResponse]
|
||||
# the parsed and converted response.
|
||||
def deserialize_call_response(call_response)
|
||||
outer_headers, outer_body = split_headers_and_body(call_response)
|
||||
status_line, payload = outer_body.split("\n", 2)
|
||||
protocol, status, reason = status_line.split(' ', 3)
|
||||
|
||||
headers, body = split_headers_and_body(payload)
|
||||
content_id = find_header('Content-ID', outer_headers)
|
||||
call_id = header_to_id(content_id)
|
||||
return BatchedCallResponse.new(call_id, status.to_i, headers, body)
|
||||
end
|
||||
|
||||
##
|
||||
# Serialize a single batched call for assembling the multipart message
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Google::APIClient::Request] call
|
||||
# the call to serialize.
|
||||
#
|
||||
# @return [Faraday::UploadIO]
|
||||
# the serialized request
|
||||
def serialize_call(call_id, call)
|
||||
method, uri, headers, body = call.to_http_request
|
||||
request = "#{method.to_s.upcase} #{Addressable::URI.parse(uri).request_uri} HTTP/1.1"
|
||||
headers.each do |header, value|
|
||||
request << "\r\n%s: %s" % [header, value]
|
||||
end
|
||||
if body
|
||||
# TODO - CompositeIO if body is a stream
|
||||
request << "\r\n\r\n"
|
||||
if body.respond_to?(:read)
|
||||
request << body.read
|
||||
else
|
||||
request << body.to_s
|
||||
end
|
||||
end
|
||||
Faraday::UploadIO.new(StringIO.new(request), 'application/http', 'ruby-api-request', 'Content-ID' => id_to_header(call_id))
|
||||
end
|
||||
|
||||
##
|
||||
# Convert an id to a Content-ID header value.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [String] call_id
|
||||
# identifier of individual call.
|
||||
#
|
||||
# @return [String]
|
||||
# A Content-ID header with the call_id encoded into it. A UUID is
|
||||
# prepended to the value because Content-ID headers are supposed to be
|
||||
# universally unique.
|
||||
def id_to_header(call_id)
|
||||
return '<%s+%s>' % [@base_id, Addressable::URI.encode(call_id)]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'multi_json'
|
||||
require 'compat/multi_json'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Manages the persistence of client configuration data and secrets. Format
|
||||
# inspired by the Google API Python client.
|
||||
#
|
||||
# @see https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
||||
#
|
||||
# @example
|
||||
# {
|
||||
# "web": {
|
||||
# "client_id": "asdfjasdljfasdkjf",
|
||||
# "client_secret": "1912308409123890",
|
||||
# "redirect_uris": ["https://www.example.com/oauth2callback"],
|
||||
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
# "token_uri": "https://accounts.google.com/o/oauth2/token"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# @example
|
||||
# {
|
||||
# "installed": {
|
||||
# "client_id": "837647042410-75ifg...usercontent.com",
|
||||
# "client_secret":"asdlkfjaskd",
|
||||
# "redirect_uris": ["http://localhost", "urn:ietf:oauth:2.0:oob"],
|
||||
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
# "token_uri": "https://accounts.google.com/o/oauth2/token"
|
||||
# }
|
||||
# }
|
||||
class ClientSecrets
|
||||
|
||||
##
|
||||
# Reads client configuration from a file
|
||||
#
|
||||
# @param [String] filename
|
||||
# Path to file to load
|
||||
#
|
||||
# @return [Google::APIClient::ClientSecrets]
|
||||
# OAuth client settings
|
||||
def self.load(filename=nil)
|
||||
if filename && File.directory?(filename)
|
||||
search_path = File.expand_path(filename)
|
||||
filename = nil
|
||||
end
|
||||
while filename == nil
|
||||
search_path ||= File.expand_path('.')
|
||||
if File.exist?(File.join(search_path, 'client_secrets.json'))
|
||||
filename = File.join(search_path, 'client_secrets.json')
|
||||
elsif search_path == '/' || search_path =~ /[a-zA-Z]:[\/\\]/
|
||||
raise ArgumentError,
|
||||
'No client_secrets.json filename supplied ' +
|
||||
'and/or could not be found in search path.'
|
||||
else
|
||||
search_path = File.expand_path(File.join(search_path, '..'))
|
||||
end
|
||||
end
|
||||
data = File.open(filename, 'r') { |file| MultiJson.load(file.read) }
|
||||
return self.new(data)
|
||||
end
|
||||
|
||||
##
|
||||
# Intialize OAuth client settings.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# Parsed client secrets files
|
||||
def initialize(options={})
|
||||
# Client auth configuration
|
||||
@flow = options[:flow] || options.keys.first.to_s || 'web'
|
||||
fdata = options[@flow]
|
||||
@client_id = fdata[:client_id] || fdata["client_id"]
|
||||
@client_secret = fdata[:client_secret] || fdata["client_secret"]
|
||||
@redirect_uris = fdata[:redirect_uris] || fdata["redirect_uris"]
|
||||
@redirect_uris ||= [fdata[:redirect_uri]]
|
||||
@javascript_origins = (
|
||||
fdata[:javascript_origins] ||
|
||||
fdata["javascript_origins"]
|
||||
)
|
||||
@javascript_origins ||= [fdata[:javascript_origin]]
|
||||
@authorization_uri = fdata[:auth_uri] || fdata["auth_uri"]
|
||||
@authorization_uri ||= fdata[:authorization_uri]
|
||||
@token_credential_uri = fdata[:token_uri] || fdata["token_uri"]
|
||||
@token_credential_uri ||= fdata[:token_credential_uri]
|
||||
|
||||
# Associated token info
|
||||
@access_token = fdata[:access_token] || fdata["access_token"]
|
||||
@refresh_token = fdata[:refresh_token] || fdata["refresh_token"]
|
||||
@id_token = fdata[:id_token] || fdata["id_token"]
|
||||
@expires_in = fdata[:expires_in] || fdata["expires_in"]
|
||||
@expires_at = fdata[:expires_at] || fdata["expires_at"]
|
||||
@issued_at = fdata[:issued_at] || fdata["issued_at"]
|
||||
end
|
||||
|
||||
attr_reader(
|
||||
:flow, :client_id, :client_secret, :redirect_uris, :javascript_origins,
|
||||
:authorization_uri, :token_credential_uri, :access_token,
|
||||
:refresh_token, :id_token, :expires_in, :expires_at, :issued_at
|
||||
)
|
||||
|
||||
##
|
||||
# Serialize back to the original JSON form
|
||||
#
|
||||
# @return [String]
|
||||
# JSON
|
||||
def to_json
|
||||
return MultiJson.dump({
|
||||
self.flow => ({
|
||||
'client_id' => self.client_id,
|
||||
'client_secret' => self.client_secret,
|
||||
'redirect_uris' => self.redirect_uris,
|
||||
'javascript_origins' => self.javascript_origins,
|
||||
'auth_uri' => self.authorization_uri,
|
||||
'token_uri' => self.token_credential_uri,
|
||||
'access_token' => self.access_token,
|
||||
'refresh_token' => self.refresh_token,
|
||||
'id_token' => self.id_token,
|
||||
'expires_in' => self.expires_in,
|
||||
'expires_at' => self.expires_at,
|
||||
'issued_at' => self.issued_at
|
||||
}).inject({}) do |accu, (k, v)|
|
||||
# Prunes empty values from JSON output.
|
||||
unless v == nil || (v.respond_to?(:empty?) && v.empty?)
|
||||
accu[k] = v
|
||||
end
|
||||
accu
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def to_authorization
|
||||
gem 'signet', '>= 0.4.0'
|
||||
require 'signet/oauth_2/client'
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
new_authorization = Signet::OAuth2::Client.new
|
||||
new_authorization.client_id = self.client_id
|
||||
new_authorization.client_secret = self.client_secret
|
||||
new_authorization.authorization_uri = (
|
||||
self.authorization_uri ||
|
||||
'https://accounts.google.com/o/oauth2/auth'
|
||||
)
|
||||
new_authorization.token_credential_uri = (
|
||||
self.token_credential_uri ||
|
||||
'https://accounts.google.com/o/oauth2/token'
|
||||
)
|
||||
new_authorization.redirect_uri = self.redirect_uris.first
|
||||
|
||||
# These are supported, but unlikely.
|
||||
new_authorization.access_token = self.access_token
|
||||
new_authorization.refresh_token = self.refresh_token
|
||||
new_authorization.id_token = self.id_token
|
||||
new_authorization.expires_in = self.expires_in
|
||||
new_authorization.issued_at = self.issued_at if self.issued_at
|
||||
new_authorization.expires_at = self.expires_at if self.expires_at
|
||||
return new_authorization
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'google/api_client/discovery/api'
|
||||
require 'google/api_client/discovery/resource'
|
||||
require 'google/api_client/discovery/method'
|
||||
require 'google/api_client/discovery/schema'
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'addressable/uri'
|
||||
require 'multi_json'
|
||||
require 'google/inflection'
|
||||
require 'google/api_client/discovery/resource'
|
||||
require 'google/api_client/discovery/method'
|
||||
require 'google/api_client/discovery/media'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# A service that has been described by a discovery document.
|
||||
class API
|
||||
|
||||
##
|
||||
# Creates a description of a particular version of a service.
|
||||
#
|
||||
# @param [String] document_base
|
||||
# Base URI for the service
|
||||
# @param [Hash] discovery_document
|
||||
# The section of the discovery document that applies to this service
|
||||
# version.
|
||||
#
|
||||
# @return [Google::APIClient::API] The constructed service object.
|
||||
def initialize(document_base, discovery_document)
|
||||
@document_base = Addressable::URI.parse(document_base)
|
||||
@discovery_document = discovery_document
|
||||
metaclass = (class << self; self; end)
|
||||
self.discovered_resources.each do |resource|
|
||||
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) { resource }
|
||||
end
|
||||
end
|
||||
self.discovered_methods.each do |method|
|
||||
method_name = Google::INFLECTOR.underscore(method.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) { method }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] unparsed discovery document for the API
|
||||
attr_reader :discovery_document
|
||||
|
||||
##
|
||||
# Returns the id of the service.
|
||||
#
|
||||
# @return [String] The service id.
|
||||
def id
|
||||
return (
|
||||
@discovery_document['id'] ||
|
||||
"#{self.name}:#{self.version}"
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the identifier for the service.
|
||||
#
|
||||
# @return [String] The service identifier.
|
||||
def name
|
||||
return @discovery_document['name']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the version of the service.
|
||||
#
|
||||
# @return [String] The service version.
|
||||
def version
|
||||
return @discovery_document['version']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a human-readable title for the API.
|
||||
#
|
||||
# @return [Hash] The API title.
|
||||
def title
|
||||
return @discovery_document['title']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a human-readable description of the API.
|
||||
#
|
||||
# @return [Hash] The API description.
|
||||
def description
|
||||
return @discovery_document['description']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a URI for the API documentation.
|
||||
#
|
||||
# @return [Hash] The API documentation.
|
||||
def documentation
|
||||
return Addressable::URI.parse(@discovery_document['documentationLink'])
|
||||
end
|
||||
|
||||
##
|
||||
# Returns true if this is the preferred version of this API.
|
||||
#
|
||||
# @return [TrueClass, FalseClass]
|
||||
# Whether or not this is the preferred version of this API.
|
||||
def preferred
|
||||
return !!@discovery_document['preferred']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the list of API features.
|
||||
#
|
||||
# @return [Array]
|
||||
# The features supported by this API.
|
||||
def features
|
||||
return @discovery_document['features'] || []
|
||||
end
|
||||
|
||||
##
|
||||
# Returns true if this API uses a data wrapper.
|
||||
#
|
||||
# @return [TrueClass, FalseClass]
|
||||
# Whether or not this API uses a data wrapper.
|
||||
def data_wrapper?
|
||||
return self.features.include?('dataWrapper')
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the base URI for the discovery document.
|
||||
#
|
||||
# @return [Addressable::URI] The base URI.
|
||||
attr_reader :document_base
|
||||
|
||||
##
|
||||
# Returns the base URI for this version of the service.
|
||||
#
|
||||
# @return [Addressable::URI] The base URI that methods are joined to.
|
||||
def method_base
|
||||
if @discovery_document['basePath']
|
||||
return @method_base ||= (
|
||||
self.document_base.join(Addressable::URI.parse(@discovery_document['basePath']))
|
||||
).normalize
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Updates the hierarchy of resources and methods with the new base.
|
||||
#
|
||||
# @param [Addressable::URI, #to_str, String] new_method_base
|
||||
# The new base URI to use for the service.
|
||||
def method_base=(new_method_base)
|
||||
@method_base = Addressable::URI.parse(new_method_base)
|
||||
self.discovered_resources.each do |resource|
|
||||
resource.method_base = @method_base
|
||||
end
|
||||
self.discovered_methods.each do |method|
|
||||
method.method_base = @method_base
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the base URI for batch calls to this service.
|
||||
#
|
||||
# @return [Addressable::URI] The base URI that methods are joined to.
|
||||
def batch_path
|
||||
if @discovery_document['batchPath']
|
||||
return @batch_path ||= (
|
||||
self.document_base.join(Addressable::URI.parse('/' +
|
||||
@discovery_document['batchPath']))
|
||||
).normalize
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# A list of schemas available for this version of the API.
|
||||
#
|
||||
# @return [Hash] A list of {Google::APIClient::Schema} objects.
|
||||
def schemas
|
||||
return @schemas ||= (
|
||||
(@discovery_document['schemas'] || []).inject({}) do |accu, (k, v)|
|
||||
accu[k] = Google::APIClient::Schema.parse(self, v)
|
||||
accu
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a schema for a kind value.
|
||||
#
|
||||
# @return [Google::APIClient::Schema] The associated Schema object.
|
||||
def schema_for_kind(kind)
|
||||
api_name, schema_name = kind.split('#', 2)
|
||||
if api_name != self.name
|
||||
raise ArgumentError,
|
||||
"The kind does not match this API. " +
|
||||
"Expected '#{self.name}', got '#{api_name}'."
|
||||
end
|
||||
for k, v in self.schemas
|
||||
return v if k.downcase == schema_name.downcase
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
##
|
||||
# A list of resources available at the root level of this version of the
|
||||
# API.
|
||||
#
|
||||
# @return [Array] A list of {Google::APIClient::Resource} objects.
|
||||
def discovered_resources
|
||||
return @discovered_resources ||= (
|
||||
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
|
||||
accu << Google::APIClient::Resource.new(
|
||||
self, self.method_base, k, v
|
||||
)
|
||||
accu
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# A list of methods available at the root level of this version of the
|
||||
# API.
|
||||
#
|
||||
# @return [Array] A list of {Google::APIClient::Method} objects.
|
||||
def discovered_methods
|
||||
return @discovered_methods ||= (
|
||||
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
|
||||
accu << Google::APIClient::Method.new(self, self.method_base, k, v)
|
||||
accu
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Allows deep inspection of the discovery document.
|
||||
def [](key)
|
||||
return @discovery_document[key]
|
||||
end
|
||||
|
||||
##
|
||||
# Converts the service to a flat mapping of RPC names and method objects.
|
||||
#
|
||||
# @return [Hash] All methods available on the service.
|
||||
#
|
||||
# @example
|
||||
# # Discover available methods
|
||||
# method_names = client.discovered_api('buzz').to_h.keys
|
||||
def to_h
|
||||
return @hash ||= (begin
|
||||
methods_hash = {}
|
||||
self.discovered_methods.each do |method|
|
||||
methods_hash[method.id] = method
|
||||
end
|
||||
self.discovered_resources.each do |resource|
|
||||
methods_hash.merge!(resource.to_h)
|
||||
end
|
||||
methods_hash
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a <code>String</code> representation of the service's state.
|
||||
#
|
||||
# @return [String] The service's state, as a <code>String</code>.
|
||||
def inspect
|
||||
sprintf(
|
||||
"#<%s:%#0x ID:%s>", self.class.to_s, self.object_id, self.id
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Marshalling support - serialize the API to a string (doc base + original
|
||||
# discovery document).
|
||||
def _dump(level)
|
||||
MultiJson.dump([@document_base.to_s, @discovery_document])
|
||||
end
|
||||
|
||||
##
|
||||
# Marshalling support - Restore an API instance from serialized form
|
||||
def self._load(obj)
|
||||
new(*MultiJson.load(obj))
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'addressable/uri'
|
||||
require 'addressable/template'
|
||||
|
||||
require 'google/api_client/errors'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Media upload elements for discovered methods
|
||||
class MediaUpload
|
||||
|
||||
##
|
||||
# Creates a description of a particular method.
|
||||
#
|
||||
# @param [Google::APIClient::API] api
|
||||
# Base discovery document for the API
|
||||
# @param [Addressable::URI] method_base
|
||||
# The base URI for the service.
|
||||
# @param [Hash] discovery_document
|
||||
# The media upload section of the discovery document.
|
||||
#
|
||||
# @return [Google::APIClient::Method] The constructed method object.
|
||||
def initialize(api, method_base, discovery_document)
|
||||
@api = api
|
||||
@method_base = method_base
|
||||
@discovery_document = discovery_document
|
||||
end
|
||||
|
||||
##
|
||||
# List of acceptable mime types
|
||||
#
|
||||
# @return [Array]
|
||||
# List of acceptable mime types for uploaded content
|
||||
def accepted_types
|
||||
@discovery_document['accept']
|
||||
end
|
||||
|
||||
##
|
||||
# Maximum size of an uplad
|
||||
# TODO: Parse & convert to numeric value
|
||||
#
|
||||
# @return [String]
|
||||
def max_size
|
||||
@discovery_document['maxSize']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the URI template for the method. A parameter list can be
|
||||
# used to expand this into a URI.
|
||||
#
|
||||
# @return [Addressable::Template] The URI template.
|
||||
def uri_template
|
||||
return @uri_template ||= Addressable::Template.new(
|
||||
@api.method_base.join(Addressable::URI.parse(@discovery_document['protocols']['simple']['path']))
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'addressable/uri'
|
||||
require 'addressable/template'
|
||||
|
||||
require 'google/api_client/errors'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# A method that has been described by a discovery document.
|
||||
class Method
|
||||
|
||||
##
|
||||
# Creates a description of a particular method.
|
||||
#
|
||||
# @param [Google::APIClient::API] api
|
||||
# The API this method belongs to.
|
||||
# @param [Addressable::URI] method_base
|
||||
# The base URI for the service.
|
||||
# @param [String] method_name
|
||||
# The identifier for the method.
|
||||
# @param [Hash] discovery_document
|
||||
# The section of the discovery document that applies to this method.
|
||||
#
|
||||
# @return [Google::APIClient::Method] The constructed method object.
|
||||
def initialize(api, method_base, method_name, discovery_document)
|
||||
@api = api
|
||||
@method_base = method_base
|
||||
@name = method_name
|
||||
@discovery_document = discovery_document
|
||||
end
|
||||
|
||||
# @return [String] unparsed discovery document for the method
|
||||
attr_reader :discovery_document
|
||||
|
||||
##
|
||||
# Returns the API this method belongs to.
|
||||
#
|
||||
# @return [Google::APIClient::API] The API this method belongs to.
|
||||
attr_reader :api
|
||||
|
||||
##
|
||||
# Returns the identifier for the method.
|
||||
#
|
||||
# @return [String] The method identifier.
|
||||
attr_reader :name
|
||||
|
||||
##
|
||||
# Returns the base URI for the method.
|
||||
#
|
||||
# @return [Addressable::URI]
|
||||
# The base URI that this method will be joined to.
|
||||
attr_reader :method_base
|
||||
|
||||
##
|
||||
# Updates the method with the new base.
|
||||
#
|
||||
# @param [Addressable::URI, #to_str, String] new_method_base
|
||||
# The new base URI to use for the method.
|
||||
def method_base=(new_method_base)
|
||||
@method_base = Addressable::URI.parse(new_method_base)
|
||||
@uri_template = nil
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a human-readable description of the method.
|
||||
#
|
||||
# @return [Hash] The API description.
|
||||
def description
|
||||
return @discovery_document['description']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the method ID.
|
||||
#
|
||||
# @return [String] The method identifier.
|
||||
def id
|
||||
return @discovery_document['id']
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the HTTP method or 'GET' if none is specified.
|
||||
#
|
||||
# @return [String] The HTTP method that will be used in the request.
|
||||
def http_method
|
||||
return @discovery_document['httpMethod'] || 'GET'
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the URI template for the method. A parameter list can be
|
||||
# used to expand this into a URI.
|
||||
#
|
||||
# @return [Addressable::Template] The URI template.
|
||||
def uri_template
|
||||
return @uri_template ||= Addressable::Template.new(
|
||||
self.method_base.join(Addressable::URI.parse(@discovery_document['path']))
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns media upload information for this method, if supported
|
||||
#
|
||||
# @return [Google::APIClient::MediaUpload] Description of upload endpoints
|
||||
def media_upload
|
||||
if @discovery_document['mediaUpload']
|
||||
return @media_upload ||= Google::APIClient::MediaUpload.new(self, self.method_base, @discovery_document['mediaUpload'])
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the Schema object for the method's request, if any.
|
||||
#
|
||||
# @return [Google::APIClient::Schema] The request schema.
|
||||
def request_schema
|
||||
if @discovery_document['request']
|
||||
schema_name = @discovery_document['request']['$ref']
|
||||
return @api.schemas[schema_name]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the Schema object for the method's response, if any.
|
||||
#
|
||||
# @return [Google::APIClient::Schema] The response schema.
|
||||
def response_schema
|
||||
if @discovery_document['response']
|
||||
schema_name = @discovery_document['response']['$ref']
|
||||
return @api.schemas[schema_name]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Normalizes parameters, converting to the appropriate types.
|
||||
#
|
||||
# @param [Hash, Array] parameters
|
||||
# The parameters to normalize.
|
||||
#
|
||||
# @return [Hash] The normalized parameters.
|
||||
def normalize_parameters(parameters={})
|
||||
# Convert keys to Strings when appropriate
|
||||
if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
|
||||
# Returning an array since parameters can be repeated (ie, Adsense Management API)
|
||||
parameters = parameters.inject([]) do |accu, (k, v)|
|
||||
k = k.to_s if k.kind_of?(Symbol)
|
||||
k = k.to_str if k.respond_to?(:to_str)
|
||||
unless k.kind_of?(String)
|
||||
raise TypeError, "Expected String, got #{k.class}."
|
||||
end
|
||||
accu << [k, v]
|
||||
accu
|
||||
end
|
||||
else
|
||||
raise TypeError,
|
||||
"Expected Hash or Array, got #{parameters.class}."
|
||||
end
|
||||
return parameters
|
||||
end
|
||||
|
||||
##
|
||||
# Expands the method's URI template using a parameter list.
|
||||
#
|
||||
# @api private
|
||||
# @param [Hash, Array] parameters
|
||||
# The parameter list to use.
|
||||
#
|
||||
# @return [Addressable::URI] The URI after expansion.
|
||||
def generate_uri(parameters={})
|
||||
parameters = self.normalize_parameters(parameters)
|
||||
|
||||
self.validate_parameters(parameters)
|
||||
template_variables = self.uri_template.variables
|
||||
upload_type = parameters.assoc('uploadType') || parameters.assoc('upload_type')
|
||||
if upload_type
|
||||
unless self.media_upload
|
||||
raise ArgumentException, "Media upload not supported for this method"
|
||||
end
|
||||
case upload_type.last
|
||||
when 'media', 'multipart', 'resumable'
|
||||
uri = self.media_upload.uri_template.expand(parameters)
|
||||
else
|
||||
raise ArgumentException, "Invalid uploadType '#{upload_type}'"
|
||||
end
|
||||
else
|
||||
uri = self.uri_template.expand(parameters)
|
||||
end
|
||||
query_parameters = parameters.reject do |k, v|
|
||||
template_variables.include?(k)
|
||||
end
|
||||
# encode all non-template parameters
|
||||
params = ""
|
||||
unless query_parameters.empty?
|
||||
params = "?" + Addressable::URI.form_encode(query_parameters.sort)
|
||||
end
|
||||
# Normalization is necessary because of undesirable percent-escaping
|
||||
# during URI template expansion
|
||||
return uri.normalize + params
|
||||
end
|
||||
|
||||
##
|
||||
# Generates an HTTP request for this method.
|
||||
#
|
||||
# @api private
|
||||
# @param [Hash, Array] parameters
|
||||
# The parameters to send.
|
||||
# @param [String, StringIO] body The body for the HTTP request.
|
||||
# @param [Hash, Array] headers The HTTP headers for the request.
|
||||
# @option options [Faraday::Connection] :connection
|
||||
# The HTTP connection to use.
|
||||
#
|
||||
# @return [Array] The generated HTTP request.
|
||||
def generate_request(parameters={}, body='', headers={}, options={})
|
||||
if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
|
||||
raise TypeError, "Expected Hash or Array, got #{headers.class}."
|
||||
end
|
||||
method = self.http_method.to_s.downcase.to_sym
|
||||
uri = self.generate_uri(parameters)
|
||||
headers = Faraday::Utils::Headers.new(headers)
|
||||
return [method, uri, headers, body]
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Returns a <code>Hash</code> of the parameter descriptions for
|
||||
# this method.
|
||||
#
|
||||
# @return [Hash] The parameter descriptions.
|
||||
def parameter_descriptions
|
||||
@parameter_descriptions ||= (
|
||||
@discovery_document['parameters'] || {}
|
||||
).inject({}) { |h,(k,v)| h[k]=v; h }
|
||||
end
|
||||
|
||||
##
|
||||
# Returns an <code>Array</code> of the parameters for this method.
|
||||
#
|
||||
# @return [Array] The parameters.
|
||||
def parameters
|
||||
@parameters ||= ((
|
||||
@discovery_document['parameters'] || {}
|
||||
).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
||||
end
|
||||
|
||||
##
|
||||
# Returns an <code>Array</code> of the required parameters for this
|
||||
# method.
|
||||
#
|
||||
# @return [Array] The required parameters.
|
||||
#
|
||||
# @example
|
||||
# # A list of all required parameters.
|
||||
# method.required_parameters
|
||||
def required_parameters
|
||||
@required_parameters ||= ((self.parameter_descriptions.select do |k, v|
|
||||
v['required']
|
||||
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
||||
end
|
||||
|
||||
##
|
||||
# Returns an <code>Array</code> of the optional parameters for this
|
||||
# method.
|
||||
#
|
||||
# @return [Array] The optional parameters.
|
||||
#
|
||||
# @example
|
||||
# # A list of all optional parameters.
|
||||
# method.optional_parameters
|
||||
def optional_parameters
|
||||
@optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
|
||||
v['required']
|
||||
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
||||
end
|
||||
|
||||
##
|
||||
# Verifies that the parameters are valid for this method. Raises an
|
||||
# exception if validation fails.
|
||||
#
|
||||
# @api private
|
||||
# @param [Hash, Array] parameters
|
||||
# The parameters to verify.
|
||||
#
|
||||
# @return [NilClass] <code>nil</code> if validation passes.
|
||||
def validate_parameters(parameters={})
|
||||
parameters = self.normalize_parameters(parameters)
|
||||
required_variables = ((self.parameter_descriptions.select do |k, v|
|
||||
v['required']
|
||||
end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
|
||||
missing_variables = required_variables - parameters.map { |(k, _)| k }
|
||||
if missing_variables.size > 0
|
||||
raise ArgumentError,
|
||||
"Missing required parameters: #{missing_variables.join(', ')}."
|
||||
end
|
||||
parameters.each do |k, v|
|
||||
# Handle repeated parameters.
|
||||
if self.parameter_descriptions[k] &&
|
||||
self.parameter_descriptions[k]['repeated'] &&
|
||||
v.kind_of?(Array)
|
||||
# If this is a repeated parameter and we've got an array as a
|
||||
# value, just provide the whole array to the loop below.
|
||||
items = v
|
||||
else
|
||||
# If this is not a repeated parameter, or if it is but we're
|
||||
# being given a single value, wrap the value in an array, so that
|
||||
# the loop below still works for the single element.
|
||||
items = [v]
|
||||
end
|
||||
|
||||
items.each do |item|
|
||||
if self.parameter_descriptions[k]
|
||||
enum = self.parameter_descriptions[k]['enum']
|
||||
if enum && !enum.include?(item)
|
||||
raise ArgumentError,
|
||||
"Parameter '#{k}' has an invalid value: #{item}. " +
|
||||
"Must be one of #{enum.inspect}."
|
||||
end
|
||||
pattern = self.parameter_descriptions[k]['pattern']
|
||||
if pattern
|
||||
regexp = Regexp.new("^#{pattern}$")
|
||||
if item !~ regexp
|
||||
raise ArgumentError,
|
||||
"Parameter '#{k}' has an invalid value: #{item}. " +
|
||||
"Must match: /^#{pattern}$/."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a <code>String</code> representation of the method's state.
|
||||
#
|
||||
# @return [String] The method's state, as a <code>String</code>.
|
||||
def inspect
|
||||
sprintf(
|
||||
"#<%s:%#0x ID:%s>",
|
||||
self.class.to_s, self.object_id, self.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'addressable/uri'
|
||||
|
||||
require 'google/inflection'
|
||||
require 'google/api_client/discovery/method'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# A resource that has been described by a discovery document.
|
||||
class Resource
|
||||
|
||||
##
|
||||
# Creates a description of a particular version of a resource.
|
||||
#
|
||||
# @param [Google::APIClient::API] api
|
||||
# The API this resource belongs to.
|
||||
# @param [Addressable::URI] method_base
|
||||
# The base URI for the service.
|
||||
# @param [String] resource_name
|
||||
# The identifier for the resource.
|
||||
# @param [Hash] discovery_document
|
||||
# The section of the discovery document that applies to this resource.
|
||||
#
|
||||
# @return [Google::APIClient::Resource] The constructed resource object.
|
||||
def initialize(api, method_base, resource_name, discovery_document)
|
||||
@api = api
|
||||
@method_base = method_base
|
||||
@name = resource_name
|
||||
@discovery_document = discovery_document
|
||||
metaclass = (class <<self; self; end)
|
||||
self.discovered_resources.each do |resource|
|
||||
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) { resource }
|
||||
end
|
||||
end
|
||||
self.discovered_methods.each do |method|
|
||||
method_name = Google::INFLECTOR.underscore(method.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) { method }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] unparsed discovery document for the resource
|
||||
attr_reader :discovery_document
|
||||
|
||||
##
|
||||
# Returns the identifier for the resource.
|
||||
#
|
||||
# @return [String] The resource identifier.
|
||||
attr_reader :name
|
||||
|
||||
##
|
||||
# Returns the base URI for this resource.
|
||||
#
|
||||
# @return [Addressable::URI] The base URI that methods are joined to.
|
||||
attr_reader :method_base
|
||||
|
||||
##
|
||||
# Returns a human-readable description of the resource.
|
||||
#
|
||||
# @return [Hash] The API description.
|
||||
def description
|
||||
return @discovery_document['description']
|
||||
end
|
||||
|
||||
##
|
||||
# Updates the hierarchy of resources and methods with the new base.
|
||||
#
|
||||
# @param [Addressable::URI, #to_str, String] new_method_base
|
||||
# The new base URI to use for the resource.
|
||||
def method_base=(new_method_base)
|
||||
@method_base = Addressable::URI.parse(new_method_base)
|
||||
self.discovered_resources.each do |resource|
|
||||
resource.method_base = @method_base
|
||||
end
|
||||
self.discovered_methods.each do |method|
|
||||
method.method_base = @method_base
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# A list of sub-resources available on this resource.
|
||||
#
|
||||
# @return [Array] A list of {Google::APIClient::Resource} objects.
|
||||
def discovered_resources
|
||||
return @discovered_resources ||= (
|
||||
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
|
||||
accu << Google::APIClient::Resource.new(
|
||||
@api, self.method_base, k, v
|
||||
)
|
||||
accu
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# A list of methods available on this resource.
|
||||
#
|
||||
# @return [Array] A list of {Google::APIClient::Method} objects.
|
||||
def discovered_methods
|
||||
return @discovered_methods ||= (
|
||||
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
|
||||
accu << Google::APIClient::Method.new(@api, self.method_base, k, v)
|
||||
accu
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Converts the resource to a flat mapping of RPC names and method
|
||||
# objects.
|
||||
#
|
||||
# @return [Hash] All methods available on the resource.
|
||||
def to_h
|
||||
return @hash ||= (begin
|
||||
methods_hash = {}
|
||||
self.discovered_methods.each do |method|
|
||||
methods_hash[method.id] = method
|
||||
end
|
||||
self.discovered_resources.each do |resource|
|
||||
methods_hash.merge!(resource.to_h)
|
||||
end
|
||||
methods_hash
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a <code>String</code> representation of the resource's state.
|
||||
#
|
||||
# @return [String] The resource's state, as a <code>String</code>.
|
||||
def inspect
|
||||
sprintf(
|
||||
"#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'time'
|
||||
require 'multi_json'
|
||||
require 'compat/multi_json'
|
||||
require 'base64'
|
||||
require 'autoparse'
|
||||
require 'addressable/uri'
|
||||
require 'addressable/template'
|
||||
|
||||
require 'google/inflection'
|
||||
require 'google/api_client/errors'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# @api private
|
||||
module Schema
|
||||
def self.parse(api, schema_data)
|
||||
# This method is super-long, but hard to break up due to the
|
||||
# unavoidable dependence on closures and execution context.
|
||||
schema_name = schema_data['id']
|
||||
|
||||
# Due to an oversight, schema IDs may not be URI references.
|
||||
# TODO(bobaman): Remove this code once this has been resolved.
|
||||
schema_uri = (
|
||||
api.document_base +
|
||||
(schema_name[0..0] != '#' ? '#' + schema_name : schema_name)
|
||||
)
|
||||
# puts schema_uri
|
||||
|
||||
# Due to an oversight, schema IDs may not be URI references.
|
||||
# TODO(bobaman): Remove this whole lambda once this has been resolved.
|
||||
reformat_references = lambda do |data|
|
||||
# This code is not particularly efficient due to recursive traversal
|
||||
# and excess object creation, but this hopefully shouldn't be an
|
||||
# issue since it should only be called only once per schema per
|
||||
# process.
|
||||
if data.kind_of?(Hash) &&
|
||||
data['$ref'] && !data['$ref'].kind_of?(Hash)
|
||||
if data['$ref'].respond_to?(:to_str)
|
||||
reference = data['$ref'].to_str
|
||||
else
|
||||
raise TypeError, "Expected String, got #{data['$ref'].class}"
|
||||
end
|
||||
reference = '#' + reference if reference[0..0] != '#'
|
||||
data.merge({
|
||||
'$ref' => reference
|
||||
})
|
||||
elsif data.kind_of?(Hash)
|
||||
data.inject({}) do |accu, (key, value)|
|
||||
if value.kind_of?(Hash)
|
||||
accu[key] = reformat_references.call(value)
|
||||
else
|
||||
accu[key] = value
|
||||
end
|
||||
accu
|
||||
end
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
schema_data = reformat_references.call(schema_data)
|
||||
# puts schema_data.inspect
|
||||
|
||||
if schema_name
|
||||
api_name_string =
|
||||
Google::INFLECTOR.camelize(api.name)
|
||||
api_version_string =
|
||||
Google::INFLECTOR.camelize(api.version).gsub('.', '_')
|
||||
# This is for compatibility with Ruby 1.8.7.
|
||||
# TODO(bobaman) Remove this when we eventually stop supporting 1.8.7.
|
||||
args = []
|
||||
args << false if Class.method(:const_defined?).arity != 1
|
||||
if Google::APIClient::Schema.const_defined?(api_name_string, *args)
|
||||
api_name = Google::APIClient::Schema.const_get(
|
||||
api_name_string, *args
|
||||
)
|
||||
else
|
||||
api_name = Google::APIClient::Schema.const_set(
|
||||
api_name_string, Module.new
|
||||
)
|
||||
end
|
||||
if api_name.const_defined?(api_version_string, *args)
|
||||
api_version = api_name.const_get(api_version_string, *args)
|
||||
else
|
||||
api_version = api_name.const_set(api_version_string, Module.new)
|
||||
end
|
||||
if api_version.const_defined?(schema_name, *args)
|
||||
schema_class = api_version.const_get(schema_name, *args)
|
||||
end
|
||||
end
|
||||
|
||||
# It's possible the schema has already been defined. If so, don't
|
||||
# redefine it. This means that reloading a schema which has already
|
||||
# been loaded into memory is not possible.
|
||||
unless schema_class
|
||||
schema_class = AutoParse.generate(schema_data, :uri => schema_uri)
|
||||
if schema_name
|
||||
api_version.const_set(schema_name, schema_class)
|
||||
end
|
||||
end
|
||||
return schema_class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
module ENV
|
||||
OS_VERSION = begin
|
||||
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
|
||||
# TODO(bobaman)
|
||||
# Confirm that all of these Windows environments actually have access
|
||||
# to the `ver` command.
|
||||
`ver`.sub(/\s*\[Version\s*/, '/').sub(']', '').strip
|
||||
elsif RUBY_PLATFORM =~ /darwin/i
|
||||
"Mac OS X/#{`sw_vers -productVersion`}"
|
||||
elsif RUBY_PLATFORM == 'java'
|
||||
# Get the information from java system properties to avoid spawning a
|
||||
# sub-process, which is not friendly in some contexts (web servers).
|
||||
require 'java'
|
||||
name = java.lang.System.getProperty('os.name')
|
||||
version = java.lang.System.getProperty('os.version')
|
||||
"#{name}/#{version}"
|
||||
else
|
||||
`uname -sr`.sub(' ', '/')
|
||||
end
|
||||
rescue Exception
|
||||
RUBY_PLATFORM
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# An error which is raised when there is an unexpected response or other
|
||||
# transport error that prevents an operation from succeeding.
|
||||
class TransmissionError < StandardError
|
||||
attr_reader :result
|
||||
def initialize(message = nil, result = nil)
|
||||
super(message)
|
||||
@result = result
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# An exception that is raised if a redirect is required
|
||||
#
|
||||
class RedirectError < TransmissionError
|
||||
end
|
||||
|
||||
##
|
||||
# An exception that is raised if a method is called with missing or
|
||||
# invalid parameter values.
|
||||
class ValidationError < StandardError
|
||||
end
|
||||
|
||||
##
|
||||
# A 4xx class HTTP error occurred.
|
||||
class ClientError < TransmissionError
|
||||
end
|
||||
|
||||
##
|
||||
# A 5xx class HTTP error occurred.
|
||||
class ServerError < TransmissionError
|
||||
end
|
||||
|
||||
##
|
||||
# An exception that is raised if an ID token could not be validated.
|
||||
class InvalidIDTokenError < StandardError
|
||||
end
|
||||
|
||||
# Error class for problems in batch requests.
|
||||
class BatchError < StandardError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
require 'faraday'
|
||||
require 'zlib'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Gzip < Faraday::Response::Middleware
|
||||
include Google::APIClient::Logging
|
||||
|
||||
def on_complete(env)
|
||||
encoding = env[:response_headers]['content-encoding'].to_s.downcase
|
||||
case encoding
|
||||
when 'gzip'
|
||||
logger.debug { "Decompressing gzip encoded response (#{env[:body].length} bytes)" }
|
||||
env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body])).read
|
||||
env[:response_headers].delete('content-encoding')
|
||||
logger.debug { "Decompressed (#{env[:body].length} bytes)" }
|
||||
when 'deflate'
|
||||
logger.debug{ "Decompressing deflate encoded response (#{env[:body].length} bytes)" }
|
||||
env[:body] = Zlib::Inflate.inflate(env[:body])
|
||||
env[:response_headers].delete('content-encoding')
|
||||
logger.debug { "Decompressed (#{env[:body].length} bytes)" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Response.register_middleware :gzip => Google::APIClient::Gzip
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
require 'logger'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
class << self
|
||||
##
|
||||
# Logger for the API client
|
||||
#
|
||||
# @return [Logger] logger instance.
|
||||
attr_accessor :logger
|
||||
end
|
||||
|
||||
self.logger = Logger.new(STDOUT)
|
||||
self.logger.level = Logger::WARN
|
||||
|
||||
##
|
||||
# Module to make accessing the logger simpler
|
||||
module Logging
|
||||
##
|
||||
# Logger for the API client
|
||||
#
|
||||
# @return [Logger] logger instance.
|
||||
def logger
|
||||
Google::APIClient.logger
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
require 'google/api_client/reference'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Uploadable media support. Holds an IO stream & content type.
|
||||
#
|
||||
# @see Faraday::UploadIO
|
||||
# @example
|
||||
# media = Google::APIClient::UploadIO.new('mymovie.m4v', 'video/mp4')
|
||||
class UploadIO < Faraday::UploadIO
|
||||
|
||||
# @return [Fixnum] Size of chunks to upload. Default is nil, meaning upload the entire file in a single request
|
||||
attr_accessor :chunk_size
|
||||
|
||||
##
|
||||
# Get the length of the stream
|
||||
#
|
||||
# @return [Fixnum]
|
||||
# Length of stream, in bytes
|
||||
def length
|
||||
io.respond_to?(:length) ? io.length : File.size(local_path)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Wraps an input stream and limits data to a given range
|
||||
#
|
||||
# @example
|
||||
# chunk = Google::APIClient::RangedIO.new(io, 0, 1000)
|
||||
class RangedIO
|
||||
##
|
||||
# Bind an input stream to a specific range.
|
||||
#
|
||||
# @param [IO] io
|
||||
# Source input stream
|
||||
# @param [Fixnum] offset
|
||||
# Starting offset of the range
|
||||
# @param [Fixnum] length
|
||||
# Length of range
|
||||
def initialize(io, offset, length)
|
||||
@io = io
|
||||
@offset = offset
|
||||
@length = length
|
||||
self.rewind
|
||||
end
|
||||
|
||||
##
|
||||
# @see IO#read
|
||||
def read(amount = nil, buf = nil)
|
||||
buffer = buf || ''
|
||||
if amount.nil?
|
||||
size = @length - @pos
|
||||
done = ''
|
||||
elsif amount == 0
|
||||
size = 0
|
||||
done = ''
|
||||
else
|
||||
size = [@length - @pos, amount].min
|
||||
done = nil
|
||||
end
|
||||
|
||||
if size > 0
|
||||
result = @io.read(size)
|
||||
result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
|
||||
buffer << result if result
|
||||
@pos = @pos + size
|
||||
end
|
||||
|
||||
if buffer.length > 0
|
||||
buffer
|
||||
else
|
||||
done
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# @see IO#rewind
|
||||
def rewind
|
||||
self.pos = 0
|
||||
end
|
||||
|
||||
##
|
||||
# @see IO#pos
|
||||
def pos
|
||||
@pos
|
||||
end
|
||||
|
||||
##
|
||||
# @see IO#pos=
|
||||
def pos=(pos)
|
||||
@pos = pos
|
||||
@io.pos = @offset + pos
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Resumable uploader.
|
||||
#
|
||||
class ResumableUpload < Request
|
||||
# @return [Fixnum] Max bytes to send in a single request
|
||||
attr_accessor :chunk_size
|
||||
|
||||
##
|
||||
# Creates a new uploader.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# Request options
|
||||
def initialize(options={})
|
||||
super options
|
||||
self.uri = options[:uri]
|
||||
self.http_method = :put
|
||||
@offset = options[:offset] || 0
|
||||
@complete = false
|
||||
@expired = false
|
||||
end
|
||||
|
||||
##
|
||||
# Sends all remaining chunks to the server
|
||||
#
|
||||
# @deprecated Pass the instance to {Google::APIClient#execute} instead
|
||||
#
|
||||
# @param [Google::APIClient] api_client
|
||||
# API Client instance to use for sending
|
||||
def send_all(api_client)
|
||||
result = nil
|
||||
until complete?
|
||||
result = send_chunk(api_client)
|
||||
break unless result.status == 308
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Sends the next chunk to the server
|
||||
#
|
||||
# @deprecated Pass the instance to {Google::APIClient#execute} instead
|
||||
#
|
||||
# @param [Google::APIClient] api_client
|
||||
# API Client instance to use for sending
|
||||
def send_chunk(api_client)
|
||||
return api_client.execute(self)
|
||||
end
|
||||
|
||||
##
|
||||
# Check if upload is complete
|
||||
#
|
||||
# @return [TrueClass, FalseClass]
|
||||
# Whether or not the upload complete successfully
|
||||
def complete?
|
||||
return @complete
|
||||
end
|
||||
|
||||
##
|
||||
# Check if the upload URL expired (upload not completed in alotted time.)
|
||||
# Expired uploads must be restarted from the beginning
|
||||
#
|
||||
# @return [TrueClass, FalseClass]
|
||||
# Whether or not the upload has expired and can not be resumed
|
||||
def expired?
|
||||
return @expired
|
||||
end
|
||||
|
||||
##
|
||||
# Check if upload is resumable. That is, neither complete nor expired
|
||||
#
|
||||
# @return [TrueClass, FalseClass] True if upload can be resumed
|
||||
def resumable?
|
||||
return !(self.complete? or self.expired?)
|
||||
end
|
||||
|
||||
##
|
||||
# Convert to an HTTP request. Returns components in order of method, URI,
|
||||
# request headers, and body
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
|
||||
def to_http_request
|
||||
if @complete
|
||||
raise Google::APIClient::ClientError, "Upload already complete"
|
||||
elsif @offset.nil?
|
||||
self.headers.update({
|
||||
'Content-Length' => "0",
|
||||
'Content-Range' => "bytes */#{media.length}" })
|
||||
else
|
||||
start_offset = @offset
|
||||
remaining = self.media.length - start_offset
|
||||
chunk_size = self.media.chunk_size || self.chunk_size || self.media.length
|
||||
content_length = [remaining, chunk_size].min
|
||||
chunk = RangedIO.new(self.media.io, start_offset, content_length)
|
||||
end_offset = start_offset + content_length - 1
|
||||
self.headers.update({
|
||||
'Content-Length' => "#{content_length}",
|
||||
'Content-Type' => self.media.content_type,
|
||||
'Content-Range' => "bytes #{start_offset}-#{end_offset}/#{media.length}" })
|
||||
self.body = chunk
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
##
|
||||
# Check the result from the server, updating the offset and/or location
|
||||
# if available.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Faraday::Response] response
|
||||
# HTTP response
|
||||
#
|
||||
# @return [Google::APIClient::Result]
|
||||
# Processed API response
|
||||
def process_http_response(response)
|
||||
case response.status
|
||||
when 200...299
|
||||
@complete = true
|
||||
when 308
|
||||
range = response.headers['range']
|
||||
if range
|
||||
@offset = range.scan(/\d+/).collect{|x| Integer(x)}.last + 1
|
||||
end
|
||||
if response.headers['location']
|
||||
self.uri = response.headers['location']
|
||||
end
|
||||
when 400...499
|
||||
@expired = true
|
||||
when 500...599
|
||||
# Invalidate the offset to mark it needs to be queried on the
|
||||
# next request
|
||||
@offset = nil
|
||||
end
|
||||
return Google::APIClient::Result.new(self, response)
|
||||
end
|
||||
|
||||
##
|
||||
# Hashified verison of the API request
|
||||
#
|
||||
# @return [Hash]
|
||||
def to_hash
|
||||
super.merge(:offset => @offset)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
require 'google/api_client/logging'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
##
|
||||
# Optional support class for Rails. Currently replaces the built-in logger
|
||||
# with Rails' application log.
|
||||
#
|
||||
class Railtie < Rails::Railtie
|
||||
initializer 'google-api-client' do |app|
|
||||
Google::APIClient.logger = Rails.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/request'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Subclass of Request for backwards compatibility with pre-0.5.0 versions of the library
|
||||
#
|
||||
# @deprecated
|
||||
# use {Google::APIClient::Request} instead
|
||||
class Reference < Request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'faraday'
|
||||
require 'faraday/request/multipart'
|
||||
require 'multi_json'
|
||||
require 'compat/multi_json'
|
||||
require 'addressable/uri'
|
||||
require 'stringio'
|
||||
require 'google/api_client/discovery'
|
||||
require 'google/api_client/logging'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
##
|
||||
# Represents an API request.
|
||||
class Request
|
||||
include Google::APIClient::Logging
|
||||
|
||||
MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
|
||||
|
||||
# @return [Hash] Request parameters
|
||||
attr_reader :parameters
|
||||
# @return [Hash] Additional HTTP headers
|
||||
attr_reader :headers
|
||||
# @return [Google::APIClient::Method] API method to invoke
|
||||
attr_reader :api_method
|
||||
# @return [Google::APIClient::UploadIO] File to upload
|
||||
attr_accessor :media
|
||||
# @return [#generated_authenticated_request] User credentials
|
||||
attr_accessor :authorization
|
||||
# @return [TrueClass,FalseClass] True if request should include credentials
|
||||
attr_accessor :authenticated
|
||||
# @return [#read, #to_str] Request body
|
||||
attr_accessor :body
|
||||
|
||||
##
|
||||
# Build a request
|
||||
#
|
||||
# @param [Hash] options
|
||||
# @option options [Hash, Array] :parameters
|
||||
# Request parameters for the API method.
|
||||
# @option options [Google::APIClient::Method] :api_method
|
||||
# API method to invoke. Either :api_method or :uri must be specified
|
||||
# @option options [TrueClass, FalseClass] :authenticated
|
||||
# True if request should include credentials. Implicitly true if
|
||||
# unspecified and :authorization present
|
||||
# @option options [#generate_signed_request] :authorization
|
||||
# OAuth credentials
|
||||
# @option options [Google::APIClient::UploadIO] :media
|
||||
# File to upload, if media upload request
|
||||
# @option options [#to_json, #to_hash] :body_object
|
||||
# Main body of the API request. Typically hash or object that can
|
||||
# be serialized to JSON
|
||||
# @option options [#read, #to_str] :body
|
||||
# Raw body to send in POST/PUT requests
|
||||
# @option options [String, Addressable::URI] :uri
|
||||
# URI to request. Either :api_method or :uri must be specified
|
||||
# @option options [String, Symbol] :http_method
|
||||
# HTTP method when requesting a URI
|
||||
def initialize(options={})
|
||||
@parameters = Faraday::Utils::ParamsHash.new
|
||||
@headers = Faraday::Utils::Headers.new
|
||||
|
||||
self.parameters.merge!(options[:parameters]) unless options[:parameters].nil?
|
||||
self.headers.merge!(options[:headers]) unless options[:headers].nil?
|
||||
self.api_method = options[:api_method]
|
||||
self.authenticated = options[:authenticated]
|
||||
self.authorization = options[:authorization]
|
||||
|
||||
# These parameters are handled differently because they're not
|
||||
# parameters to the API method, but rather to the API system.
|
||||
self.parameters['key'] ||= options[:key] if options[:key]
|
||||
self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
|
||||
|
||||
if options[:media]
|
||||
self.initialize_media_upload(options)
|
||||
elsif options[:body]
|
||||
self.body = options[:body]
|
||||
elsif options[:body_object]
|
||||
self.headers['Content-Type'] ||= 'application/json'
|
||||
self.body = serialize_body(options[:body_object])
|
||||
else
|
||||
self.body = ''
|
||||
end
|
||||
|
||||
unless self.api_method
|
||||
self.http_method = options[:http_method] || 'GET'
|
||||
self.uri = options[:uri]
|
||||
end
|
||||
end
|
||||
|
||||
# @!attribute [r] upload_type
|
||||
# @return [String] protocol used for upload
|
||||
def upload_type
|
||||
return self.parameters['uploadType'] || self.parameters['upload_type']
|
||||
end
|
||||
|
||||
# @!attribute http_method
|
||||
# @return [Symbol] HTTP method if invoking a URI
|
||||
def http_method
|
||||
return @http_method ||= self.api_method.http_method.to_s.downcase.to_sym
|
||||
end
|
||||
|
||||
def http_method=(new_http_method)
|
||||
if new_http_method.kind_of?(Symbol)
|
||||
@http_method = new_http_method.to_s.downcase.to_sym
|
||||
elsif new_http_method.respond_to?(:to_str)
|
||||
@http_method = new_http_method.to_s.downcase.to_sym
|
||||
else
|
||||
raise TypeError,
|
||||
"Expected String or Symbol, got #{new_http_method.class}."
|
||||
end
|
||||
end
|
||||
|
||||
def api_method=(new_api_method)
|
||||
if new_api_method.nil? || new_api_method.kind_of?(Google::APIClient::Method)
|
||||
@api_method = new_api_method
|
||||
else
|
||||
raise TypeError,
|
||||
"Expected Google::APIClient::Method, got #{new_api_method.class}."
|
||||
end
|
||||
end
|
||||
|
||||
# @!attribute uri
|
||||
# @return [Addressable::URI] URI to send request
|
||||
def uri
|
||||
return @uri ||= self.api_method.generate_uri(self.parameters)
|
||||
end
|
||||
|
||||
def uri=(new_uri)
|
||||
@uri = Addressable::URI.parse(new_uri)
|
||||
@parameters.update(@uri.query_values) unless @uri.query_values.nil?
|
||||
end
|
||||
|
||||
|
||||
# Transmits the request with the given connection
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Faraday::Connection] connection
|
||||
# the connection to transmit with
|
||||
# @param [TrueValue,FalseValue] is_retry
|
||||
# True if request has been previous sent
|
||||
#
|
||||
# @return [Google::APIClient::Result]
|
||||
# result of API request
|
||||
def send(connection, is_retry = false)
|
||||
self.body.rewind if is_retry && self.body.respond_to?(:rewind)
|
||||
env = self.to_env(connection)
|
||||
logger.debug { "#{self.class} Sending API request #{env[:method]} #{env[:url].to_s} #{env[:request_headers]}" }
|
||||
http_response = connection.app.call(env)
|
||||
result = self.process_http_response(http_response)
|
||||
|
||||
logger.debug { "#{self.class} Result: #{result.status} #{result.headers}" }
|
||||
|
||||
# Resumamble slightly different than other upload protocols in that it requires at least
|
||||
# 2 requests.
|
||||
if result.status == 200 && self.upload_type == 'resumable'
|
||||
upload = result.resumable_upload
|
||||
unless upload.complete?
|
||||
logger.debug { "#{self.class} Sending upload body" }
|
||||
result = upload.send(connection)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
# Convert to an HTTP request. Returns components in order of method, URI,
|
||||
# request headers, and body
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [Array<(Symbol, Addressable::URI, Hash, [#read,#to_str])>]
|
||||
def to_http_request
|
||||
request = (
|
||||
if self.api_method
|
||||
self.api_method.generate_request(self.parameters, self.body, self.headers)
|
||||
elsif self.uri
|
||||
unless self.parameters.empty?
|
||||
self.uri.query = Addressable::URI.form_encode(self.parameters)
|
||||
end
|
||||
[self.http_method, self.uri.to_s, self.headers, self.body]
|
||||
end)
|
||||
return request
|
||||
end
|
||||
|
||||
##
|
||||
# Hashified verison of the API request
|
||||
#
|
||||
# @return [Hash]
|
||||
def to_hash
|
||||
options = {}
|
||||
if self.api_method
|
||||
options[:api_method] = self.api_method
|
||||
options[:parameters] = self.parameters
|
||||
else
|
||||
options[:http_method] = self.http_method
|
||||
options[:uri] = self.uri
|
||||
end
|
||||
options[:headers] = self.headers
|
||||
options[:body] = self.body
|
||||
options[:media] = self.media
|
||||
unless self.authorization.nil?
|
||||
options[:authorization] = self.authorization
|
||||
end
|
||||
return options
|
||||
end
|
||||
|
||||
##
|
||||
# Prepares the request for execution, building a hash of parts
|
||||
# suitable for sending to Faraday::Connection.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Faraday::Connection] connection
|
||||
# Connection for building the request
|
||||
#
|
||||
# @return [Hash]
|
||||
# Encoded request
|
||||
def to_env(connection)
|
||||
method, uri, headers, body = self.to_http_request
|
||||
http_request = connection.build_request(method) do |req|
|
||||
req.url(uri.to_s)
|
||||
req.headers.update(headers)
|
||||
req.body = body
|
||||
end
|
||||
|
||||
if self.authorization.respond_to?(:generate_authenticated_request)
|
||||
http_request = self.authorization.generate_authenticated_request(
|
||||
:request => http_request,
|
||||
:connection => connection
|
||||
)
|
||||
end
|
||||
|
||||
request_env = http_request.to_env(connection)
|
||||
end
|
||||
|
||||
##
|
||||
# Convert HTTP response to an API Result
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Faraday::Response] response
|
||||
# HTTP response
|
||||
#
|
||||
# @return [Google::APIClient::Result]
|
||||
# Processed API response
|
||||
def process_http_response(response)
|
||||
Result.new(self, response)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
##
|
||||
# Adjust headers & body for media uploads
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Hash] options
|
||||
# @option options [Hash, Array] :parameters
|
||||
# Request parameters for the API method.
|
||||
# @option options [Google::APIClient::UploadIO] :media
|
||||
# File to upload, if media upload request
|
||||
# @option options [#to_json, #to_hash] :body_object
|
||||
# Main body of the API request. Typically hash or object that can
|
||||
# be serialized to JSON
|
||||
# @option options [#read, #to_str] :body
|
||||
# Raw body to send in POST/PUT requests
|
||||
def initialize_media_upload(options)
|
||||
self.media = options[:media]
|
||||
case self.upload_type
|
||||
when "media"
|
||||
if options[:body] || options[:body_object]
|
||||
raise ArgumentError, "Can not specify body & body object for simple uploads"
|
||||
end
|
||||
self.headers['Content-Type'] ||= self.media.content_type
|
||||
self.headers['Content-Length'] ||= self.media.length.to_s
|
||||
self.body = self.media
|
||||
when "multipart"
|
||||
unless options[:body_object]
|
||||
raise ArgumentError, "Multipart requested but no body object"
|
||||
end
|
||||
metadata = StringIO.new(serialize_body(options[:body_object]))
|
||||
build_multipart([Faraday::UploadIO.new(metadata, 'application/json', 'file.json'), self.media])
|
||||
when "resumable"
|
||||
file_length = self.media.length
|
||||
self.headers['X-Upload-Content-Type'] = self.media.content_type
|
||||
self.headers['X-Upload-Content-Length'] = file_length.to_s
|
||||
if options[:body_object]
|
||||
self.headers['Content-Type'] ||= 'application/json'
|
||||
self.body = serialize_body(options[:body_object])
|
||||
else
|
||||
self.body = ''
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Assemble a multipart message from a set of parts
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [Array<[#read,#to_str]>] parts
|
||||
# Array of parts to encode.
|
||||
# @param [String] mime_type
|
||||
# MIME type of the message
|
||||
# @param [String] boundary
|
||||
# Boundary for separating each part of the message
|
||||
def build_multipart(parts, mime_type = 'multipart/related', boundary = MULTIPART_BOUNDARY)
|
||||
env = Faraday::Env.new
|
||||
env.request = Faraday::RequestOptions.new
|
||||
env.request.boundary = boundary
|
||||
env.request_headers = {'Content-Type' => "#{mime_type};boundary=#{boundary}"}
|
||||
multipart = Faraday::Request::Multipart.new
|
||||
self.body = multipart.create_multipart(env, parts.map {|part| [nil, part]})
|
||||
self.headers.update(env[:request_headers])
|
||||
end
|
||||
|
||||
##
|
||||
# Serialize body object to JSON
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @param [#to_json,#to_hash] body
|
||||
# object to serialize
|
||||
#
|
||||
# @return [String]
|
||||
# JSON
|
||||
def serialize_body(body)
|
||||
return body.to_json if body.respond_to?(:to_json)
|
||||
return MultiJson.dump(body.to_hash) if body.respond_to?(:to_hash)
|
||||
raise TypeError, 'Could not convert body object to JSON.' +
|
||||
'Must respond to :to_json or :to_hash.'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# This class wraps a result returned by an API call.
|
||||
class Result
|
||||
extend Forwardable
|
||||
|
||||
##
|
||||
# Init the result
|
||||
#
|
||||
# @param [Google::APIClient::Request] request
|
||||
# The original request
|
||||
# @param [Faraday::Response] response
|
||||
# Raw HTTP Response
|
||||
def initialize(request, response)
|
||||
@request = request
|
||||
@response = response
|
||||
@media_upload = reference if reference.kind_of?(ResumableUpload)
|
||||
end
|
||||
|
||||
# @return [Google::APIClient::Request] Original request object
|
||||
attr_reader :request
|
||||
# @return [Faraday::Response] HTTP response
|
||||
attr_reader :response
|
||||
# @!attribute [r] reference
|
||||
# @return [Google::APIClient::Request] Original request object
|
||||
# @deprecated See {#request}
|
||||
alias_method :reference, :request # For compatibility with pre-beta clients
|
||||
|
||||
# @!attribute [r] status
|
||||
# @return [Fixnum] HTTP status code
|
||||
# @!attribute [r] headers
|
||||
# @return [Hash] HTTP response headers
|
||||
# @!attribute [r] body
|
||||
# @return [String] HTTP response body
|
||||
def_delegators :@response, :status, :headers, :body
|
||||
|
||||
# @!attribute [r] resumable_upload
|
||||
# @return [Google::APIClient::ResumableUpload] For resuming media uploads
|
||||
def resumable_upload
|
||||
@media_upload ||= (
|
||||
options = self.reference.to_hash.merge(
|
||||
:uri => self.headers['location'],
|
||||
:media => self.reference.media
|
||||
)
|
||||
Google::APIClient::ResumableUpload.new(options)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Get the content type of the response
|
||||
# @!attribute [r] media_type
|
||||
# @return [String]
|
||||
# Value of content-type header
|
||||
def media_type
|
||||
_, content_type = self.headers.detect do |h, v|
|
||||
h.downcase == 'Content-Type'.downcase
|
||||
end
|
||||
if content_type
|
||||
return content_type[/^([^;]*);?.*$/, 1].strip.downcase
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Check if request failed
|
||||
#
|
||||
# @!attribute [r] error?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if result of operation is an error
|
||||
def error?
|
||||
return self.response.status >= 400
|
||||
end
|
||||
|
||||
##
|
||||
# Check if request was successful
|
||||
#
|
||||
# @!attribute [r] success?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if result of operation was successful
|
||||
def success?
|
||||
return !self.error?
|
||||
end
|
||||
|
||||
##
|
||||
# Extracts error messages from the response body
|
||||
#
|
||||
# @!attribute [r] error_message
|
||||
# @return [String]
|
||||
# error message, if available
|
||||
def error_message
|
||||
if self.data?
|
||||
if self.data.respond_to?(:error) &&
|
||||
self.data.error.respond_to?(:message)
|
||||
# You're going to get a terrible error message if the response isn't
|
||||
# parsed successfully as an error.
|
||||
return self.data.error.message
|
||||
elsif self.data['error'] && self.data['error']['message']
|
||||
return self.data['error']['message']
|
||||
end
|
||||
end
|
||||
return self.body
|
||||
end
|
||||
|
||||
##
|
||||
# Check for parsable data in response
|
||||
#
|
||||
# @!attribute [r] data?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if body can be parsed
|
||||
def data?
|
||||
!(self.body.nil? || self.body.empty? || self.media_type != 'application/json')
|
||||
end
|
||||
|
||||
##
|
||||
# Return parsed version of the response body.
|
||||
#
|
||||
# @!attribute [r] data
|
||||
# @return [Object, Hash, String]
|
||||
# Object if body parsable from API schema, Hash if JSON, raw body if unable to parse
|
||||
def data
|
||||
return @data ||= (begin
|
||||
if self.data?
|
||||
media_type = self.media_type
|
||||
data = self.body
|
||||
case media_type
|
||||
when 'application/json'
|
||||
data = MultiJson.load(data)
|
||||
# Strip data wrapper, if present
|
||||
data = data['data'] if data.has_key?('data')
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Content-Type not supported for parsing: #{media_type}"
|
||||
end
|
||||
if @request.api_method && @request.api_method.response_schema
|
||||
# Automatically parse using the schema designated for the
|
||||
# response of this API method.
|
||||
data = @request.api_method.response_schema.new(data)
|
||||
data
|
||||
else
|
||||
# Otherwise, return the raw unparsed value.
|
||||
# This value must be indexable like a Hash.
|
||||
data
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
##
|
||||
# Get the token used for requesting the next page of data
|
||||
#
|
||||
# @!attribute [r] next_page_token
|
||||
# @return [String]
|
||||
# next page token
|
||||
def next_page_token
|
||||
if self.data.respond_to?(:next_page_token)
|
||||
return self.data.next_page_token
|
||||
elsif self.data.respond_to?(:[])
|
||||
return self.data["nextPageToken"]
|
||||
else
|
||||
raise TypeError, "Data object did not respond to #next_page_token."
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Build a request for fetching the next page of data
|
||||
#
|
||||
# @return [Google::APIClient::Request]
|
||||
# API request for retrieving next page
|
||||
def next_page
|
||||
merged_parameters = Hash[self.reference.parameters].merge({
|
||||
self.page_token_param => self.next_page_token
|
||||
})
|
||||
# Because Requests can be coerced to Hashes, we can merge them,
|
||||
# preserving all context except the API method parameters that we're
|
||||
# using for pagination.
|
||||
return Google::APIClient::Request.new(
|
||||
Hash[self.reference].merge(:parameters => merged_parameters)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Get the token used for requesting the previous page of data
|
||||
#
|
||||
# @!attribute [r] prev_page_token
|
||||
# @return [String]
|
||||
# previous page token
|
||||
def prev_page_token
|
||||
if self.data.respond_to?(:prev_page_token)
|
||||
return self.data.prev_page_token
|
||||
elsif self.data.respond_to?(:[])
|
||||
return self.data["prevPageToken"]
|
||||
else
|
||||
raise TypeError, "Data object did not respond to #next_page_token."
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Build a request for fetching the previous page of data
|
||||
#
|
||||
# @return [Google::APIClient::Request]
|
||||
# API request for retrieving previous page
|
||||
def prev_page
|
||||
merged_parameters = Hash[self.reference.parameters].merge({
|
||||
self.page_token_param => self.prev_page_token
|
||||
})
|
||||
# Because Requests can be coerced to Hashes, we can merge them,
|
||||
# preserving all context except the API method parameters that we're
|
||||
# using for pagination.
|
||||
return Google::APIClient::Request.new(
|
||||
Hash[self.reference].merge(:parameters => merged_parameters)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Pagination scheme used by this request/response
|
||||
#
|
||||
# @!attribute [r] pagination_type
|
||||
# @return [Symbol]
|
||||
# currently always :token
|
||||
def pagination_type
|
||||
return :token
|
||||
end
|
||||
|
||||
##
|
||||
# Name of the field that contains the pagination token
|
||||
#
|
||||
# @!attribute [r] page_token_param
|
||||
# @return [String]
|
||||
# currently always 'pageToken'
|
||||
def page_token_param
|
||||
return "pageToken"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/service/stub_generator'
|
||||
require 'google/api_client/service/resource'
|
||||
require 'google/api_client/service/request'
|
||||
require 'google/api_client/service/result'
|
||||
require 'google/api_client/service/batch'
|
||||
require 'google/api_client/service/simple_file_store'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
##
|
||||
# Experimental new programming interface at the API level.
|
||||
# Hides Google::APIClient. Designed to be easier to use, with less code.
|
||||
#
|
||||
# @example
|
||||
# calendar = Google::APIClient::Service.new('calendar', 'v3')
|
||||
# result = calendar.events.list('calendarId' => 'primary').execute()
|
||||
class Service
|
||||
include Google::APIClient::Service::StubGenerator
|
||||
extend Forwardable
|
||||
|
||||
DEFAULT_CACHE_FILE = 'discovery.cache'
|
||||
|
||||
# Cache for discovered APIs.
|
||||
@@discovered = {}
|
||||
|
||||
##
|
||||
# Creates a new Service.
|
||||
#
|
||||
# @param [String, Symbol] api_name
|
||||
# The name of the API this service will access.
|
||||
# @param [String, Symbol] api_version
|
||||
# The version of the API this service will access.
|
||||
# @param [Hash] options
|
||||
# The configuration parameters for the service.
|
||||
# @option options [Symbol, #generate_authenticated_request] :authorization
|
||||
# (:oauth_1)
|
||||
# The authorization mechanism used by the client. The following
|
||||
# mechanisms are supported out-of-the-box:
|
||||
# <ul>
|
||||
# <li><code>:two_legged_oauth_1</code></li>
|
||||
# <li><code>:oauth_1</code></li>
|
||||
# <li><code>:oauth_2</code></li>
|
||||
# </ul>
|
||||
# @option options [Boolean] :auto_refresh_token (true)
|
||||
# The setting that controls whether or not the api client attempts to
|
||||
# refresh authorization when a 401 is hit in #execute. If the token does
|
||||
# not support it, this option is ignored.
|
||||
# @option options [String] :application_name
|
||||
# The name of the application using the client.
|
||||
# @option options [String] :application_version
|
||||
# The version number of the application using the client.
|
||||
# @option options [String] :host ("www.googleapis.com")
|
||||
# The API hostname used by the client. This rarely needs to be changed.
|
||||
# @option options [String] :port (443)
|
||||
# The port number used by the client. This rarely needs to be changed.
|
||||
# @option options [String] :discovery_path ("/discovery/v1")
|
||||
# The discovery base path. This rarely needs to be changed.
|
||||
# @option options [String] :ca_file
|
||||
# Optional set of root certificates to use when validating SSL connections.
|
||||
# By default, a bundled set of trusted roots will be used.
|
||||
# @option options [#generate_authenticated_request] :authorization
|
||||
# The authorization mechanism for requests. Used only if
|
||||
# `:authenticated` is `true`.
|
||||
# @option options [TrueClass, FalseClass] :authenticated (default: true)
|
||||
# `true` if requests must be signed or somehow
|
||||
# authenticated, `false` otherwise.
|
||||
# @option options [TrueClass, FalseClass] :gzip (default: true)
|
||||
# `true` if gzip enabled, `false` otherwise.
|
||||
# @option options [Faraday::Connection] :connection
|
||||
# A custom connection to be used for all requests.
|
||||
# @option options [ActiveSupport::Cache::Store, :default] :discovery_cache
|
||||
# A cache store to place the discovery documents for loaded APIs.
|
||||
# Avoids unnecessary roundtrips to the discovery service.
|
||||
# :default loads the default local file cache store.
|
||||
def initialize(api_name, api_version, options = {})
|
||||
@api_name = api_name.to_s
|
||||
if api_version.nil?
|
||||
raise ArgumentError,
|
||||
"API version must be set"
|
||||
end
|
||||
@api_version = api_version.to_s
|
||||
if options && !options.respond_to?(:to_hash)
|
||||
raise ArgumentError,
|
||||
"expected options Hash, got #{options.class}"
|
||||
end
|
||||
|
||||
params = {}
|
||||
[:application_name, :application_version, :authorization, :host, :port,
|
||||
:discovery_path, :auto_refresh_token, :key, :user_ip,
|
||||
:ca_file].each do |option|
|
||||
if options.include? option
|
||||
params[option] = options[option]
|
||||
end
|
||||
end
|
||||
|
||||
@client = Google::APIClient.new(params)
|
||||
|
||||
@connection = options[:connection] || @client.connection
|
||||
|
||||
@options = options
|
||||
|
||||
# Initialize cache store. Default to SimpleFileStore if :cache_store
|
||||
# is not provided and we have write permissions.
|
||||
if options.include? :cache_store
|
||||
@cache_store = options[:cache_store]
|
||||
else
|
||||
cache_exists = File.exist?(DEFAULT_CACHE_FILE)
|
||||
if (cache_exists && File.writable?(DEFAULT_CACHE_FILE)) ||
|
||||
(!cache_exists && File.writable?(Dir.pwd))
|
||||
@cache_store = Google::APIClient::Service::SimpleFileStore.new(
|
||||
DEFAULT_CACHE_FILE)
|
||||
end
|
||||
end
|
||||
|
||||
# Attempt to read API definition from memory cache.
|
||||
# Not thread-safe, but the worst that can happen is a cache miss.
|
||||
unless @api = @@discovered[[api_name, api_version]]
|
||||
# Attempt to read API definition from cache store, if there is one.
|
||||
# If there's a miss or no cache store, call discovery service.
|
||||
if !@cache_store.nil?
|
||||
@api = @cache_store.fetch("%s/%s" % [api_name, api_version]) do
|
||||
@client.discovered_api(api_name, api_version)
|
||||
end
|
||||
else
|
||||
@api = @client.discovered_api(api_name, api_version)
|
||||
end
|
||||
@@discovered[[api_name, api_version]] = @api
|
||||
end
|
||||
|
||||
generate_call_stubs(self, @api)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the authorization mechanism used by the service.
|
||||
#
|
||||
# @return [#generate_authenticated_request] The authorization mechanism.
|
||||
def_delegators :@client, :authorization, :authorization=
|
||||
|
||||
##
|
||||
# The setting that controls whether or not the service attempts to
|
||||
# refresh authorization when a 401 is hit during an API call.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def_delegators :@client, :auto_refresh_token, :auto_refresh_token=
|
||||
|
||||
##
|
||||
# The application's API key issued by the API console.
|
||||
#
|
||||
# @return [String] The API key.
|
||||
def_delegators :@client, :key, :key=
|
||||
|
||||
##
|
||||
# The Faraday/HTTP connection used by this service.
|
||||
#
|
||||
# @return [Faraday::Connection]
|
||||
attr_accessor :connection
|
||||
|
||||
##
|
||||
# The cache store used for storing discovery documents.
|
||||
#
|
||||
# @return [ActiveSupport::Cache::Store,
|
||||
# Google::APIClient::Service::SimpleFileStore,
|
||||
# nil]
|
||||
attr_reader :cache_store
|
||||
|
||||
##
|
||||
# Prepares a Google::APIClient::BatchRequest object to make batched calls.
|
||||
# @param [Array] calls
|
||||
# Optional array of Google::APIClient::Service::Request to initialize
|
||||
# the batch request with.
|
||||
# @param [Proc] block
|
||||
# Callback for every call's response. Won't be called if a call defined
|
||||
# a callback of its own.
|
||||
#
|
||||
# @yield [Google::APIClient::Service::Result]
|
||||
# block to be called when result ready
|
||||
def batch(calls = nil, &block)
|
||||
Google::APIClient::Service::BatchRequest.new(self, calls, &block)
|
||||
end
|
||||
|
||||
##
|
||||
# Executes an API request.
|
||||
# Do not call directly; this method is only used by Request objects when
|
||||
# executing.
|
||||
#
|
||||
# @param [Google::APIClient::Service::Request,
|
||||
# Google::APIClient::Service::BatchCall] request
|
||||
# The request to be executed.
|
||||
def execute(request)
|
||||
if request.instance_of? Google::APIClient::Service::Request
|
||||
params = {:api_method => request.method,
|
||||
:parameters => request.parameters,
|
||||
:connection => @connection}
|
||||
if request.respond_to? :body
|
||||
if request.body.respond_to? :to_hash
|
||||
params[:body_object] = request.body
|
||||
else
|
||||
params[:body] = request.body
|
||||
end
|
||||
end
|
||||
if request.respond_to? :media
|
||||
params[:media] = request.media
|
||||
end
|
||||
[:authenticated, :gzip].each do |option|
|
||||
if @options.include? option
|
||||
params[option] = @options[option]
|
||||
end
|
||||
end
|
||||
result = @client.execute(params)
|
||||
return Google::APIClient::Service::Result.new(request, result)
|
||||
elsif request.instance_of? Google::APIClient::Service::BatchRequest
|
||||
@client.execute(request.base_batch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/service/result'
|
||||
require 'google/api_client/batch'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
|
||||
##
|
||||
# Helper class to contain the result of an individual batched call.
|
||||
#
|
||||
class BatchedCallResult < Result
|
||||
# @return [Fixnum] Index of the call
|
||||
def call_index
|
||||
return @base_result.response.call_id.to_i - 1
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
#
|
||||
class BatchRequest
|
||||
##
|
||||
# Creates a new batch request.
|
||||
# This class shouldn't be instantiated directly, but rather through
|
||||
# Service.batch.
|
||||
#
|
||||
# @param [Array] calls
|
||||
# List of Google::APIClient::Service::Request to be made.
|
||||
# @param [Proc] block
|
||||
# Callback for every call's response. Won't be called if a call
|
||||
# defined a callback of its own.
|
||||
#
|
||||
# @yield [Google::APIClient::Service::Result]
|
||||
# block to be called when result ready
|
||||
def initialize(service, calls, &block)
|
||||
@service = service
|
||||
@base_batch = Google::APIClient::BatchRequest.new
|
||||
@global_callback = block if block_given?
|
||||
|
||||
if calls && calls.length > 0
|
||||
calls.each do |call|
|
||||
add(call)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Add a new call to the batch request.
|
||||
#
|
||||
# @param [Google::APIClient::Service::Request] call
|
||||
# the call to be added.
|
||||
# @param [Proc] block
|
||||
# callback for this call's response.
|
||||
#
|
||||
# @return [Google::APIClient::Service::BatchRequest]
|
||||
# the BatchRequest, for chaining
|
||||
#
|
||||
# @yield [Google::APIClient::Service::Result]
|
||||
# block to be called when result ready
|
||||
def add(call, &block)
|
||||
if !block_given? && @global_callback.nil?
|
||||
raise BatchError, 'Request needs a block'
|
||||
end
|
||||
callback = block || @global_callback
|
||||
base_call = {
|
||||
:api_method => call.method,
|
||||
:parameters => call.parameters
|
||||
}
|
||||
@base_batch.add(base_call) do |base_result|
|
||||
result = Google::APIClient::Service::BatchedCallResult.new(
|
||||
call, base_result)
|
||||
callback.call(result)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
##
|
||||
# Executes the batch request.
|
||||
def execute
|
||||
@service.execute(self)
|
||||
end
|
||||
|
||||
attr_reader :base_batch
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
##
|
||||
# Handles an API request.
|
||||
# This contains a full definition of the request to be made (including
|
||||
# method name, parameters, body and media). The remote API call can be
|
||||
# invoked with execute().
|
||||
class Request
|
||||
##
|
||||
# Build a request.
|
||||
# This class should not be directly instantiated in user code;
|
||||
# instantiation is handled by the stub methods created on Service and
|
||||
# Resource objects.
|
||||
#
|
||||
# @param [Google::APIClient::Service] service
|
||||
# The parent Service instance that will execute the request.
|
||||
# @param [Google::APIClient::Method] method
|
||||
# The Method instance that describes the API method invoked by the
|
||||
# request.
|
||||
# @param [Hash] parameters
|
||||
# A Hash of parameter names and values to be sent in the API call.
|
||||
def initialize(service, method, parameters)
|
||||
@service = service
|
||||
@method = method
|
||||
@parameters = parameters
|
||||
@body = nil
|
||||
@media = nil
|
||||
|
||||
metaclass = (class << self; self; end)
|
||||
|
||||
# If applicable, add "body", "body=" and resource-named methods for
|
||||
# retrieving and setting the HTTP body for this request.
|
||||
# Examples of setting the body for files.insert in the Drive API:
|
||||
# request.body = object
|
||||
# request.execute
|
||||
# OR
|
||||
# request.file = object
|
||||
# request.execute
|
||||
# OR
|
||||
# request.body(object).execute
|
||||
# OR
|
||||
# request.file(object).execute
|
||||
# Examples of retrieving the body for files.insert in the Drive API:
|
||||
# object = request.body
|
||||
# OR
|
||||
# object = request.file
|
||||
if method.request_schema
|
||||
body_name = method.request_schema.data['id'].dup
|
||||
body_name[0] = body_name[0].chr.downcase
|
||||
body_name_equals = (body_name + '=').to_sym
|
||||
body_name = body_name.to_sym
|
||||
|
||||
metaclass.send(:define_method, :body) do |*args|
|
||||
if args.length == 1
|
||||
@body = args.first
|
||||
return self
|
||||
elsif args.length == 0
|
||||
return @body
|
||||
else
|
||||
raise ArgumentError,
|
||||
"wrong number of arguments (#{args.length}; expecting 0 or 1)"
|
||||
end
|
||||
end
|
||||
|
||||
metaclass.send(:define_method, :body=) do |body|
|
||||
@body = body
|
||||
end
|
||||
|
||||
metaclass.send(:alias_method, body_name, :body)
|
||||
metaclass.send(:alias_method, body_name_equals, :body=)
|
||||
end
|
||||
|
||||
# If applicable, add "media" and "media=" for retrieving and setting
|
||||
# the media object for this request.
|
||||
# Examples of setting the media object:
|
||||
# request.media = object
|
||||
# request.execute
|
||||
# OR
|
||||
# request.media(object).execute
|
||||
# Example of retrieving the media object:
|
||||
# object = request.media
|
||||
if method.media_upload
|
||||
metaclass.send(:define_method, :media) do |*args|
|
||||
if args.length == 1
|
||||
@media = args.first
|
||||
return self
|
||||
elsif args.length == 0
|
||||
return @media
|
||||
else
|
||||
raise ArgumentError,
|
||||
"wrong number of arguments (#{args.length}; expecting 0 or 1)"
|
||||
end
|
||||
end
|
||||
|
||||
metaclass.send(:define_method, :media=) do |media|
|
||||
@media = media
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Returns the parent service capable of executing this request.
|
||||
#
|
||||
# @return [Google::APIClient::Service] The parent service.
|
||||
attr_reader :service
|
||||
|
||||
##
|
||||
# Returns the Method instance that describes the API method invoked by
|
||||
# the request.
|
||||
#
|
||||
# @return [Google::APIClient::Method] The API method description.
|
||||
attr_reader :method
|
||||
|
||||
##
|
||||
# Contains the Hash of parameter names and values to be sent as the
|
||||
# parameters for the API call.
|
||||
#
|
||||
# @return [Hash] The request parameters.
|
||||
attr_accessor :parameters
|
||||
|
||||
##
|
||||
# Executes the request.
|
||||
def execute
|
||||
@service.execute(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
##
|
||||
# Handles an API resource.
|
||||
# Simple class that contains API methods and/or child resources.
|
||||
class Resource
|
||||
include Google::APIClient::Service::StubGenerator
|
||||
|
||||
##
|
||||
# Build a resource.
|
||||
# This class should not be directly instantiated in user code; resources
|
||||
# are instantiated by the stub generation mechanism on Service creation.
|
||||
#
|
||||
# @param [Google::APIClient::Service] service
|
||||
# The Service instance this resource belongs to.
|
||||
# @param [Google::APIClient::API, Google::APIClient::Resource] root
|
||||
# The node corresponding to this resource.
|
||||
def initialize(service, root)
|
||||
@service = service
|
||||
generate_call_stubs(service, root)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
##
|
||||
# Handles an API result.
|
||||
# Wraps around the Google::APIClient::Result class, making it easier to
|
||||
# handle the result (e.g. pagination) and keeping it in line with the rest
|
||||
# of the Service programming interface.
|
||||
class Result
|
||||
extend Forwardable
|
||||
|
||||
##
|
||||
# Init the result.
|
||||
#
|
||||
# @param [Google::APIClient::Service::Request] request
|
||||
# The original request
|
||||
# @param [Google::APIClient::Result] base_result
|
||||
# The base result to be wrapped
|
||||
def initialize(request, base_result)
|
||||
@request = request
|
||||
@base_result = base_result
|
||||
end
|
||||
|
||||
# @!attribute [r] status
|
||||
# @return [Fixnum] HTTP status code
|
||||
# @!attribute [r] headers
|
||||
# @return [Hash] HTTP response headers
|
||||
# @!attribute [r] body
|
||||
# @return [String] HTTP response body
|
||||
def_delegators :@base_result, :status, :headers, :body
|
||||
|
||||
# @return [Google::APIClient::Service::Request] Original request object
|
||||
attr_reader :request
|
||||
|
||||
##
|
||||
# Get the content type of the response
|
||||
# @!attribute [r] media_type
|
||||
# @return [String]
|
||||
# Value of content-type header
|
||||
def_delegators :@base_result, :media_type
|
||||
|
||||
##
|
||||
# Check if request failed
|
||||
#
|
||||
# @!attribute [r] error?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if result of operation is an error
|
||||
def_delegators :@base_result, :error?
|
||||
|
||||
##
|
||||
# Check if request was successful
|
||||
#
|
||||
# @!attribute [r] success?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if result of operation was successful
|
||||
def_delegators :@base_result, :success?
|
||||
|
||||
##
|
||||
# Extracts error messages from the response body
|
||||
#
|
||||
# @!attribute [r] error_message
|
||||
# @return [String]
|
||||
# error message, if available
|
||||
def_delegators :@base_result, :error_message
|
||||
|
||||
##
|
||||
# Check for parsable data in response
|
||||
#
|
||||
# @!attribute [r] data?
|
||||
# @return [TrueClass, FalseClass]
|
||||
# true if body can be parsed
|
||||
def_delegators :@base_result, :data?
|
||||
|
||||
##
|
||||
# Return parsed version of the response body.
|
||||
#
|
||||
# @!attribute [r] data
|
||||
# @return [Object, Hash, String]
|
||||
# Object if body parsable from API schema, Hash if JSON, raw body if unable to parse
|
||||
def_delegators :@base_result, :data
|
||||
|
||||
##
|
||||
# Pagination scheme used by this request/response
|
||||
#
|
||||
# @!attribute [r] pagination_type
|
||||
# @return [Symbol]
|
||||
# currently always :token
|
||||
def_delegators :@base_result, :pagination_type
|
||||
|
||||
##
|
||||
# Name of the field that contains the pagination token
|
||||
#
|
||||
# @!attribute [r] page_token_param
|
||||
# @return [String]
|
||||
# currently always 'pageToken'
|
||||
def_delegators :@base_result, :page_token_param
|
||||
|
||||
##
|
||||
# Get the token used for requesting the next page of data
|
||||
#
|
||||
# @!attribute [r] next_page_token
|
||||
# @return [String]
|
||||
# next page tokenx =
|
||||
def_delegators :@base_result, :next_page_token
|
||||
|
||||
##
|
||||
# Get the token used for requesting the previous page of data
|
||||
#
|
||||
# @!attribute [r] prev_page_token
|
||||
# @return [String]
|
||||
# previous page token
|
||||
def_delegators :@base_result, :prev_page_token
|
||||
|
||||
# @!attribute [r] resumable_upload
|
||||
def resumable_upload
|
||||
# TODO(sgomes): implement resumable_upload for Service::Result
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
##
|
||||
# Build a request for fetching the next page of data
|
||||
#
|
||||
# @return [Google::APIClient::Service::Request]
|
||||
# API request for retrieving next page
|
||||
def next_page
|
||||
request = @request.clone
|
||||
# Make a deep copy of the parameters.
|
||||
request.parameters = Marshal.load(Marshal.dump(request.parameters))
|
||||
request.parameters[page_token_param] = self.next_page_token
|
||||
return request
|
||||
end
|
||||
|
||||
##
|
||||
# Build a request for fetching the previous page of data
|
||||
#
|
||||
# @return [Google::APIClient::Service::Request]
|
||||
# API request for retrieving previous page
|
||||
def prev_page
|
||||
request = @request.clone
|
||||
# Make a deep copy of the parameters.
|
||||
request.parameters = Marshal.load(Marshal.dump(request.parameters))
|
||||
request.parameters[page_token_param] = self.prev_page_token
|
||||
return request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
|
||||
# Simple file store to be used in the event no ActiveSupport cache store
|
||||
# is provided. This is not thread-safe, and does not support a number of
|
||||
# features (such as expiration), but it's useful for the simple purpose of
|
||||
# caching discovery documents to disk.
|
||||
# Implements the basic cache methods of ActiveSupport::Cache::Store in a
|
||||
# limited fashion.
|
||||
class SimpleFileStore
|
||||
|
||||
# Creates a new SimpleFileStore.
|
||||
#
|
||||
# @param [String] file_path
|
||||
# The path to the cache file on disk.
|
||||
# @param [Object] options
|
||||
# The options to be used with this SimpleFileStore. Not implemented.
|
||||
def initialize(file_path, options = nil)
|
||||
@file_path = file_path.to_s
|
||||
end
|
||||
|
||||
# Returns true if a key exists in the cache.
|
||||
#
|
||||
# @param [String] name
|
||||
# The name of the key. Will always be converted to a string.
|
||||
# @param [Object] options
|
||||
# The options to be used with this query. Not implemented.
|
||||
def exist?(name, options = nil)
|
||||
read_file
|
||||
@cache.nil? ? nil : @cache.include?(name.to_s)
|
||||
end
|
||||
|
||||
# Fetches data from the cache and returns it, using the given key.
|
||||
# If the key is missing and no block is passed, returns nil.
|
||||
# If the key is missing and a block is passed, executes the block, sets
|
||||
# the key to its value, and returns it.
|
||||
#
|
||||
# @param [String] name
|
||||
# The name of the key. Will always be converted to a string.
|
||||
# @param [Object] options
|
||||
# The options to be used with this query. Not implemented.
|
||||
# @yield [String]
|
||||
# optional block with the default value if the key is missing
|
||||
def fetch(name, options = nil)
|
||||
read_file
|
||||
if block_given?
|
||||
entry = read(name.to_s, options)
|
||||
if entry.nil?
|
||||
value = yield name.to_s
|
||||
write(name.to_s, value)
|
||||
return value
|
||||
else
|
||||
return entry
|
||||
end
|
||||
else
|
||||
return read(name.to_s, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches data from the cache, using the given key.
|
||||
# Returns nil if the key is missing.
|
||||
#
|
||||
# @param [String] name
|
||||
# The name of the key. Will always be converted to a string.
|
||||
# @param [Object] options
|
||||
# The options to be used with this query. Not implemented.
|
||||
def read(name, options = nil)
|
||||
read_file
|
||||
@cache.nil? ? nil : @cache[name.to_s]
|
||||
end
|
||||
|
||||
# Writes the value to the cache, with the key.
|
||||
#
|
||||
# @param [String] name
|
||||
# The name of the key. Will always be converted to a string.
|
||||
# @param [Object] value
|
||||
# The value to be written.
|
||||
# @param [Object] options
|
||||
# The options to be used with this query. Not implemented.
|
||||
def write(name, value, options = nil)
|
||||
read_file
|
||||
@cache = {} if @cache.nil?
|
||||
@cache[name.to_s] = value
|
||||
write_file
|
||||
return nil
|
||||
end
|
||||
|
||||
# Deletes an entry in the cache.
|
||||
# Returns true if an entry is deleted.
|
||||
#
|
||||
# @param [String] name
|
||||
# The name of the key. Will always be converted to a string.
|
||||
# @param [Object] options
|
||||
# The options to be used with this query. Not implemented.
|
||||
def delete(name, options = nil)
|
||||
read_file
|
||||
return nil if @cache.nil?
|
||||
if @cache.include? name.to_s
|
||||
@cache.delete name.to_s
|
||||
write_file
|
||||
return true
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Read the entire cache file from disk.
|
||||
# Will avoid reading if there have been no changes.
|
||||
def read_file
|
||||
if !File.exists? @file_path
|
||||
@cache = nil
|
||||
else
|
||||
# Check for changes after our last read or write.
|
||||
if @last_change.nil? || File.mtime(@file_path) > @last_change
|
||||
File.open(@file_path) do |file|
|
||||
@cache = Marshal.load(file)
|
||||
@last_change = file.mtime
|
||||
end
|
||||
end
|
||||
end
|
||||
return @cache
|
||||
end
|
||||
|
||||
# Write the entire cache contents to disk.
|
||||
def write_file
|
||||
File.open(@file_path, 'w') do |file|
|
||||
Marshal.dump(@cache, file)
|
||||
end
|
||||
@last_change = File.mtime(@file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
class Service
|
||||
##
|
||||
# Auxiliary mixin to generate resource and method stubs.
|
||||
# Used by the Service and Service::Resource classes to generate both
|
||||
# top-level and nested resources and methods.
|
||||
module StubGenerator
|
||||
def generate_call_stubs(service, root)
|
||||
metaclass = (class << self; self; end)
|
||||
|
||||
# Handle resources.
|
||||
root.discovered_resources.each do |resource|
|
||||
method_name = Google::INFLECTOR.underscore(resource.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) do
|
||||
Google::APIClient::Service::Resource.new(service, resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Handle methods.
|
||||
root.discovered_methods.each do |method|
|
||||
method_name = Google::INFLECTOR.underscore(method.name).to_sym
|
||||
if !self.respond_to?(method_name)
|
||||
metaclass.send(:define_method, method_name) do |*args|
|
||||
if args.length > 1
|
||||
raise ArgumentError,
|
||||
"wrong number of arguments (#{args.length} for 1)"
|
||||
elsif !args.first.respond_to?(:to_hash) && !args.first.nil?
|
||||
raise ArgumentError,
|
||||
"expected parameter Hash, got #{args.first.class}"
|
||||
else
|
||||
return Google::APIClient::Service::Request.new(
|
||||
service, method, args.first
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'google/api_client/auth/pkcs12'
|
||||
require 'google/api_client/auth/jwt_asserter'
|
||||
require 'google/api_client/auth/key_utils'
|
||||
require 'google/api_client/auth/compute_service_account'
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
# Used to prevent the class/module from being loaded more than once
|
||||
if !defined?(::Google::APIClient::VERSION)
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
module VERSION
|
||||
MAJOR = 0
|
||||
MINOR = 7
|
||||
TINY = 1
|
||||
PATCH = nil
|
||||
STRING = [MAJOR, MINOR, TINY, PATCH].compact.join('.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
module Google
|
||||
if defined?(ActiveSupport::Inflector)
|
||||
INFLECTOR = ActiveSupport::Inflector
|
||||
else
|
||||
begin
|
||||
require 'extlib/inflection'
|
||||
INFLECTOR = Extlib::Inflection
|
||||
rescue LoadError
|
||||
require 'active_support/inflector'
|
||||
INFLECTOR = ActiveSupport::Inflector
|
||||
end
|
||||
end
|
||||
end
|
||||
Binary file not shown.
|
|
@ -0,0 +1,33 @@
|
|||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus posuere urna bibendum diam vulputate fringilla. Fusce elementum fermentum justo id aliquam. Integer vel felis ut arcu elementum lacinia. Duis congue urna eget nisl dapibus tristique molestie turpis sollicitudin. Vivamus in justo quam. Proin condimentum mollis tortor at molestie. Cras luctus, nunc a convallis iaculis, est risus consequat nisi, sit amet sollicitudin metus mi a urna. Aliquam accumsan, massa quis condimentum varius, sapien massa faucibus nibh, a dignissim magna nibh a lacus. Nunc aliquet, nunc ac pulvinar consectetur, sapien lacus hendrerit enim, nec dapibus lorem mi eget risus. Praesent vitae justo eget dolor blandit ullamcorper. Duis id nibh vitae sem aliquam vehicula et ac massa. In neque elit, molestie pulvinar viverra at, vestibulum quis velit.
|
||||
|
||||
Mauris sit amet placerat enim. Duis vel tellus ac dui auctor tincidunt id nec augue. Donec ut blandit turpis. Mauris dictum urna id urna vestibulum accumsan. Maecenas sagittis urna vitae erat facilisis gravida. Phasellus tellus augue, commodo ut iaculis vitae, interdum ut dolor. Proin at dictum lorem. Quisque pellentesque neque ante, vitae rutrum elit. Pellentesque sit amet erat orci. Praesent justo diam, tristique eu tempus ut, vestibulum eget dui. Maecenas et elementum justo. Cras a augue a elit porttitor placerat eget ut magna.
|
||||
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam adipiscing tellus in arcu bibendum volutpat. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed laoreet faucibus tristique. Duis metus eros, molestie eget dignissim in, imperdiet fermentum nulla. Vestibulum laoreet lorem eu justo vestibulum lobortis. Praesent pharetra leo vel mauris rhoncus commodo sollicitudin ante auctor. Ut sagittis, tortor nec placerat rutrum, neque ipsum cursus nisl, ut lacinia magna risus ac risus. Sed volutpat commodo orci, sodales fermentum dui accumsan eu. Donec egestas ullamcorper elit at condimentum. In euismod sodales posuere. Nullam lacinia tempus molestie. Etiam vitae ullamcorper dui. Fusce congue suscipit arcu, at consectetur diam gravida id. Quisque augue urna, commodo eleifend volutpat vitae, tincidunt ac ligula. Curabitur eget orci nisl, vel placerat ipsum.
|
||||
|
||||
Curabitur rutrum euismod nisi, consectetur varius tortor condimentum non. Pellentesque rhoncus nisi eu purus ultricies suscipit. Morbi ante nisi, varius nec molestie bibendum, pharetra quis enim. Proin eget nunc ante. Cras aliquam enim vel nunc laoreet ut facilisis nunc interdum. Fusce libero ipsum, posuere eget blandit quis, bibendum vitae quam. Integer dictum faucibus lacus eget facilisis. Duis adipiscing tortor magna, vel tincidunt risus. In non augue eu nisl sodales cursus vel eget nisi. Maecenas dignissim lectus elementum eros fermentum gravida et eget leo. Aenean quis cursus arcu. Mauris posuere purus non diam mattis vehicula. Integer nec orci velit.
|
||||
|
||||
Integer ac justo ac magna adipiscing condimentum vitae tincidunt dui. Morbi augue arcu, blandit nec interdum sit amet, condimentum vel nisl. Nulla vehicula tincidunt laoreet. Aliquam ornare elementum urna, sed vehicula magna porta id. Vestibulum dictum ultrices tortor sit amet tincidunt. Praesent bibendum, metus vel volutpat interdum, nisl nunc cursus libero, vel congue ligula mi et felis. Nulla mollis elementum nulla, in accumsan risus consequat at. Suspendisse potenti. Vestibulum enim lorem, dignissim ut porta vestibulum, porta eget mi. Fusce a elit ac dui sodales gravida. Pellentesque sed elit at dui dapibus mattis a non arcu.
|
||||
|
||||
Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In nec posuere augue. Praesent non suscipit arcu. Sed nibh risus, lacinia ut molestie vitae, tristique eget turpis. Sed pretium volutpat arcu, non rutrum leo volutpat sed. Maecenas quis neque nisl, sit amet ornare dolor. Nulla pharetra pulvinar tellus sed eleifend. Aliquam eget mattis nulla. Nulla dictum vehicula velit, non facilisis lorem volutpat id. Fusce scelerisque sem vitae purus dapibus lobortis. Mauris ac turpis nec nibh consequat porttitor. Ut sit amet iaculis lorem. Vivamus blandit erat ac odio venenatis fringilla a sit amet ante. Quisque ut urna sed augue laoreet sagittis.
|
||||
|
||||
Integer nisl urna, bibendum id lobortis in, tempor non velit. Fusce sed volutpat quam. Suspendisse eu placerat purus. Maecenas quis feugiat lectus. Sed accumsan malesuada dui, a pretium purus facilisis quis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc ac purus id lacus malesuada placerat et in nunc. Ut imperdiet tincidunt est, at consectetur augue egestas hendrerit. Pellentesque eu erat a dui dignissim adipiscing. Integer quis leo non felis placerat eleifend. Fusce luctus mi a lorem mattis eget accumsan libero posuere. Sed pellentesque, odio id pharetra tempus, enim quam placerat metus, auctor aliquam elit mi facilisis quam. Nam at velit et eros rhoncus accumsan.
|
||||
|
||||
Donec tellus diam, fringilla ac viverra fringilla, rhoncus sit amet purus. Cras et ligula sed nibh tempor gravida. Aliquam id tempus mauris. Ut convallis quam sed arcu varius eget mattis magna tincidunt. Aliquam et suscipit est. Sed metus augue, tristique sed accumsan eget, euismod et augue. Nam augue sapien, placerat vel facilisis eu, tempor id risus. Aliquam mollis egestas mi. Fusce scelerisque convallis mauris quis blandit. Mauris nec ante id lacus sagittis tincidunt ornare vehicula dui. Curabitur tristique mattis nunc, vel cursus libero viverra feugiat. Suspendisse at sapien velit, a lacinia dolor. Vivamus in est non odio feugiat lacinia sodales ut magna.
|
||||
|
||||
Donec interdum ligula id ipsum dapibus consectetur. Pellentesque vitae posuere ligula. Morbi rhoncus bibendum eleifend. Suspendisse fringilla nunc at elit malesuada vitae ullamcorper lorem laoreet. Suspendisse a ante at ipsum iaculis cursus. Duis accumsan ligula quis nibh luctus pretium. Duis ultrices scelerisque dolor, et vulputate lectus commodo ut.
|
||||
|
||||
Vestibulum ac tincidunt lorem. Vestibulum lorem massa, dictum a scelerisque ut, convallis vitae eros. Morbi ipsum nisl, lacinia non tempor nec, lobortis id diam. Fusce quis magna nunc. Proin ultricies congue justo sed mattis. Vestibulum sit amet arcu tellus. Quisque ultricies porta massa iaculis vehicula. Vestibulum sollicitudin tempor urna vel sodales. Pellentesque ultricies tellus vel metus porta nec iaculis sapien mollis. Maecenas ullamcorper, metus eget imperdiet sagittis, odio orci dapibus neque, in vulputate nunc nibh non libero. Donec velit quam, lobortis quis tempus a, hendrerit id arcu.
|
||||
|
||||
Donec nec ante at tortor dignissim mattis. Curabitur vehicula tincidunt magna id sagittis. Proin euismod dignissim porta. Curabitur non turpis purus, in rutrum nulla. Nam turpis nulla, tincidunt et hendrerit non, posuere nec enim. Curabitur leo enim, lobortis ut placerat id, condimentum nec massa. In bibendum, lectus sit amet molestie commodo, felis massa rutrum nisl, ac fermentum ligula lacus in ipsum.
|
||||
|
||||
Pellentesque mi nulla, scelerisque vitae tempus id, consequat a augue. Quisque vel nisi sit amet ipsum faucibus laoreet sed vitae lorem. Praesent nunc tortor, volutpat ac commodo non, pharetra sed neque. Curabitur nec felis at mi blandit aliquet eu ornare justo. Mauris dignissim purus quis nisl porttitor interdum. Aenean id ipsum enim, blandit commodo justo. Quisque facilisis elit quis velit commodo scelerisque lobortis sapien condimentum. Cras sit amet porttitor velit. Praesent nec tempor arcu.
|
||||
|
||||
Donec varius mi adipiscing elit semper vel feugiat ipsum dictum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non quam nisl, ac mattis justo. Vestibulum sed massa eget velit tristique auctor ut ac sapien. Curabitur aliquet ligula eget dui ornare at scelerisque mauris faucibus. Vestibulum id mauris metus, sed vestibulum nibh. Nulla egestas dictum blandit. Mauris vitae nibh at dui mollis lobortis. Phasellus sem leo, euismod at fringilla quis, mollis in nibh. Aenean vel lacus et elit pharetra elementum. Aliquam at ligula id sem bibendum volutpat. Pellentesque quis elit a massa dapibus viverra ut et lorem. Donec nulla eros, iaculis nec commodo vel, suscipit sit amet tortor. Integer tempor, elit at viverra imperdiet, velit sapien laoreet nunc, id laoreet ligula risus vel risus. Nullam sed tortor metus.
|
||||
|
||||
In nunc orci, tempor vulputate pretium vel, suscipit quis risus. Suspendisse accumsan facilisis felis eget posuere. Donec a faucibus felis. Proin nibh erat, sollicitudin quis vestibulum id, tincidunt quis justo. In sed purus eu nisi dignissim condimentum. Sed mattis dapibus lorem id vulputate. Suspendisse nec elit a augue interdum consequat quis id magna. In eleifend aliquam tempor. In in lacus augue.
|
||||
|
||||
Ut euismod sollicitudin lorem, id aliquam magna dictum sed. Nunc fringilla lobortis nisi sed consectetur. Nulla facilisi. Aenean nec lobortis augue. Curabitur ullamcorper dapibus libero, vel pellentesque arcu sollicitudin non. Praesent varius, turpis nec sollicitudin bibendum, elit tortor rhoncus lacus, gravida luctus leo nisi in felis. Ut metus eros, molestie non faucibus vel, condimentum ac elit.
|
||||
|
||||
Suspendisse nisl justo, lacinia sit amet interdum nec, tincidunt placerat urna. Suspendisse potenti. In et odio sed purus malesuada cursus sed nec lectus. Cras commodo, orci sit amet hendrerit iaculis, nunc urna facilisis tellus, vel laoreet odio nulla quis nibh. Maecenas ut justo ut lacus posuere sodales. Vestibulum facilisis fringilla diam at volutpat. Proin a hendrerit urna. Aenean placerat pulvinar arcu, sit amet lobortis neque eleifend in. Aenean risus nulla, facilisis ut tincidunt vitae, fringilla at ligula. Praesent eleifend est at sem lacinia auctor. Nulla ornare nunc in erat laoreet blandit.
|
||||
|
||||
Suspendisse pharetra leo ac est porta consequat. Nunc sem nibh, gravida vel aliquam a, ornare in tortor. Nulla vel sapien et felis placerat pellentesque id scelerisque nisl. Praesent et posuere.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Bag Attributes
|
||||
friendlyName: privatekey
|
||||
localKeyID: 54 69 6D 65 20 31 33 35 31 38 38 38 31 37 38 36 39 36
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQDYDyPb3GhyFx5i/wxS/jFsO6wSLys1ehAk6QZoBXGlg7ETVrIJ
|
||||
HYh9gXQUno4tJiQoaO8wOvleIRrqI0LkiftCXKWVSrzOiV+O9GkKx1byw1yAIZus
|
||||
QdwMT7X0O9hrZLZwhICWC9s6cGhnlCVxLIP/+JkVK7hxEq/LxoSszNV77wIDAQAB
|
||||
AoGAa2G69L7quil7VMBmI6lqbtyJfNAsrXtpIq8eG/z4qsZ076ObAKTI/XeldcoH
|
||||
57CZL+xXVKU64umZMt0rleJuGXdlauEUbsSx+biGewRfGTgC4rUSjmE539rBvmRW
|
||||
gaKliorepPMp/+B9CcG/2YfDPRvG/2cgTXJHVvneo+xHL4ECQQD2Jx5Mvs8z7s2E
|
||||
jY1mkpRKqh4Z7rlitkAwe1NXcVC8hz5ASu7ORyTl8EPpKAfRMYl1ofK/ozT1URXf
|
||||
kL5nChPfAkEA4LPUJ6cqrY4xrrtdGaM4iGIxzen5aZlKz/YNlq5LuQKbnLLHMuXU
|
||||
ohp/ynpqNWbcAFbmtGSMayxGKW5+fJgZ8QJAUBOZv82zCmn9YcnK3juBEmkVMcp/
|
||||
dKVlbGAyVJgAc9RrY+78kQ6D6mmnLgpfwKYk2ae9mKo3aDbgrsIfrtWQcQJAfFGi
|
||||
CEpJp3orbLQG319ZsMM7MOTJdC42oPZOMFbAWFzkAX88DKHx0bn9h+XQizkccSej
|
||||
Ppz+v3DgZJ3YZ1Cz0QJBALiqIokZ+oa3AY6oT0aiec6txrGvNPPbwOsrBpFqGNbu
|
||||
AByzWWBoBi40eKMSIR30LqN9H8YnJ91Aoy1njGYyQaw=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
describe Google::APIClient::BatchRequest do
|
||||
CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)
|
||||
|
||||
after do
|
||||
# Reset client to not-quite-pristine state
|
||||
CLIENT.key = nil
|
||||
CLIENT.user_ip = nil
|
||||
end
|
||||
|
||||
it 'should raise an error if making an empty batch request' do
|
||||
batch = Google::APIClient::BatchRequest.new
|
||||
|
||||
(lambda do
|
||||
CLIENT.execute(batch)
|
||||
end).should raise_error(Google::APIClient::BatchError)
|
||||
end
|
||||
|
||||
it 'should allow query parameters in batch requests' do
|
||||
batch = Google::APIClient::BatchRequest.new
|
||||
batch.add(:uri => 'https://example.com', :parameters => {
|
||||
'a' => '12345'
|
||||
})
|
||||
method, uri, headers, body = batch.to_http_request
|
||||
body.read.should include("/?a=12345")
|
||||
end
|
||||
|
||||
describe 'with the discovery API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@discovery = CLIENT.discovered_api('discovery', 'v1')
|
||||
end
|
||||
|
||||
describe 'with two valid requests' do
|
||||
before do
|
||||
@call1 = {
|
||||
:api_method => @discovery.apis.get_rest,
|
||||
:parameters => {
|
||||
'api' => 'plus',
|
||||
'version' => 'v1'
|
||||
}
|
||||
}
|
||||
|
||||
@call2 = {
|
||||
:api_method => @discovery.apis.get_rest,
|
||||
:parameters => {
|
||||
'api' => 'discovery',
|
||||
'version' => 'v1'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'should execute both when using a global callback' do
|
||||
block_called = 0
|
||||
ids = ['first_call', 'second_call']
|
||||
expected_ids = ids.clone
|
||||
batch = Google::APIClient::BatchRequest.new do |result|
|
||||
block_called += 1
|
||||
result.status.should == 200
|
||||
expected_ids.should include(result.response.call_id)
|
||||
expected_ids.delete(result.response.call_id)
|
||||
end
|
||||
|
||||
batch.add(@call1, ids[0])
|
||||
batch.add(@call2, ids[1])
|
||||
|
||||
CLIENT.execute(batch)
|
||||
block_called.should == 2
|
||||
end
|
||||
|
||||
it 'should execute both when using individual callbacks' do
|
||||
batch = Google::APIClient::BatchRequest.new
|
||||
|
||||
call1_returned, call2_returned = false, false
|
||||
batch.add(@call1) do |result|
|
||||
call1_returned = true
|
||||
result.status.should == 200
|
||||
end
|
||||
batch.add(@call2) do |result|
|
||||
call2_returned = true
|
||||
result.status.should == 200
|
||||
end
|
||||
|
||||
CLIENT.execute(batch)
|
||||
call1_returned.should == true
|
||||
call2_returned.should == true
|
||||
end
|
||||
|
||||
it 'should raise an error if using the same call ID more than once' do
|
||||
batch = Google::APIClient::BatchRequest.new
|
||||
|
||||
(lambda do
|
||||
batch.add(@call1, 'my_id')
|
||||
batch.add(@call2, 'my_id')
|
||||
end).should raise_error(Google::APIClient::BatchError)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a valid request and an invalid one' do
|
||||
before do
|
||||
@call1 = {
|
||||
:api_method => @discovery.apis.get_rest,
|
||||
:parameters => {
|
||||
'api' => 'plus',
|
||||
'version' => 'v1'
|
||||
}
|
||||
}
|
||||
|
||||
@call2 = {
|
||||
:api_method => @discovery.apis.get_rest,
|
||||
:parameters => {
|
||||
'api' => 0,
|
||||
'version' => 1
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'should execute both when using a global callback' do
|
||||
block_called = 0
|
||||
ids = ['first_call', 'second_call']
|
||||
expected_ids = ids.clone
|
||||
batch = Google::APIClient::BatchRequest.new do |result|
|
||||
block_called += 1
|
||||
expected_ids.should include(result.response.call_id)
|
||||
expected_ids.delete(result.response.call_id)
|
||||
if result.response.call_id == ids[0]
|
||||
result.status.should == 200
|
||||
else
|
||||
result.status.should >= 400
|
||||
result.status.should < 500
|
||||
end
|
||||
end
|
||||
|
||||
batch.add(@call1, ids[0])
|
||||
batch.add(@call2, ids[1])
|
||||
|
||||
CLIENT.execute(batch)
|
||||
block_called.should == 2
|
||||
end
|
||||
|
||||
it 'should execute both when using individual callbacks' do
|
||||
batch = Google::APIClient::BatchRequest.new
|
||||
|
||||
call1_returned, call2_returned = false, false
|
||||
batch.add(@call1) do |result|
|
||||
call1_returned = true
|
||||
result.status.should == 200
|
||||
end
|
||||
batch.add(@call2) do |result|
|
||||
call2_returned = true
|
||||
result.status.should >= 400
|
||||
result.status.should < 500
|
||||
end
|
||||
|
||||
CLIENT.execute(batch)
|
||||
call1_returned.should == true
|
||||
call2_returned.should == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the calendar API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@calendar = CLIENT.discovered_api('calendar', 'v3')
|
||||
end
|
||||
|
||||
describe 'with two valid requests' do
|
||||
before do
|
||||
event1 = {
|
||||
'summary' => 'Appointment 1',
|
||||
'location' => 'Somewhere',
|
||||
'start' => {
|
||||
'dateTime' => '2011-01-01T10:00:00.000-07:00'
|
||||
},
|
||||
'end' => {
|
||||
'dateTime' => '2011-01-01T10:25:00.000-07:00'
|
||||
},
|
||||
'attendees' => [
|
||||
{
|
||||
'email' => 'myemail@mydomain.tld'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
event2 = {
|
||||
'summary' => 'Appointment 2',
|
||||
'location' => 'Somewhere as well',
|
||||
'start' => {
|
||||
'dateTime' => '2011-01-02T10:00:00.000-07:00'
|
||||
},
|
||||
'end' => {
|
||||
'dateTime' => '2011-01-02T10:25:00.000-07:00'
|
||||
},
|
||||
'attendees' => [
|
||||
{
|
||||
'email' => 'myemail@mydomain.tld'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@call1 = {
|
||||
:api_method => @calendar.events.insert,
|
||||
:parameters => {'calendarId' => 'myemail@mydomain.tld'},
|
||||
:body => MultiJson.dump(event1),
|
||||
:headers => {'Content-Type' => 'application/json'}
|
||||
}
|
||||
|
||||
@call2 = {
|
||||
:api_method => @calendar.events.insert,
|
||||
:parameters => {'calendarId' => 'myemail@mydomain.tld'},
|
||||
:body => MultiJson.dump(event2),
|
||||
:headers => {'Content-Type' => 'application/json'}
|
||||
}
|
||||
end
|
||||
|
||||
it 'should convert to a correct HTTP request' do
|
||||
batch = Google::APIClient::BatchRequest.new { |result| }
|
||||
batch.add(@call1, '1').add(@call2, '2')
|
||||
request = batch.to_env(CLIENT.connection)
|
||||
boundary = Google::APIClient::BatchRequest::BATCH_BOUNDARY
|
||||
request[:method].to_s.downcase.should == 'post'
|
||||
request[:url].to_s.should == 'https://www.googleapis.com/batch'
|
||||
request[:request_headers]['Content-Type'].should == "multipart/mixed;boundary=#{boundary}"
|
||||
# TODO - Fix headers
|
||||
#expected_body = /--#{Regexp.escape(boundary)}\nContent-Type: +application\/http\nContent-ID: +<[\w-]+\+1>\n\nPOST +https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/myemail@mydomain.tld\/events +HTTP\/1.1\nContent-Type: +application\/json\n\n#{Regexp.escape(@call1[:body])}\n\n--#{boundary}\nContent-Type: +application\/http\nContent-ID: +<[\w-]+\+2>\n\nPOST +https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/myemail@mydomain.tld\/events HTTP\/1.1\nContent-Type: +application\/json\n\n#{Regexp.escape(@call2[:body])}\n\n--#{Regexp.escape(boundary)}--/
|
||||
#request[:body].read.gsub("\r", "").should =~ expected_body
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,652 @@
|
|||
# encoding:utf-8
|
||||
|
||||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'faraday'
|
||||
require 'multi_json'
|
||||
require 'compat/multi_json'
|
||||
require 'signet/oauth_1/client'
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
describe Google::APIClient do
|
||||
include ConnectionHelpers
|
||||
CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)
|
||||
|
||||
after do
|
||||
# Reset client to not-quite-pristine state
|
||||
CLIENT.key = nil
|
||||
CLIENT.user_ip = nil
|
||||
end
|
||||
|
||||
it 'should raise a type error for bogus authorization' do
|
||||
(lambda do
|
||||
Google::APIClient.new(:application_name => 'API Client Tests', :authorization => 42)
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should not be able to retrieve the discovery document for a bogus API' do
|
||||
(lambda do
|
||||
CLIENT.discovery_document('bogus')
|
||||
end).should raise_error(Google::APIClient::TransmissionError)
|
||||
(lambda do
|
||||
CLIENT.discovered_api('bogus')
|
||||
end).should raise_error(Google::APIClient::TransmissionError)
|
||||
end
|
||||
|
||||
it 'should raise an error for bogus services' do
|
||||
(lambda do
|
||||
CLIENT.discovered_api(42)
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should raise an error for bogus services' do
|
||||
(lambda do
|
||||
CLIENT.preferred_version(42)
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should raise an error for bogus methods' do
|
||||
(lambda do
|
||||
CLIENT.execute(42)
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should not return a preferred version for bogus service names' do
|
||||
CLIENT.preferred_version('bogus').should == nil
|
||||
end
|
||||
|
||||
describe 'with the prediction API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
# The prediction API no longer exposes a v1, so we have to be
|
||||
# careful about looking up the wrong API version.
|
||||
@prediction = CLIENT.discovered_api('prediction', 'v1.2')
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI' do
|
||||
CLIENT.discovery_uri('prediction').should ===
|
||||
'https://www.googleapis.com/discovery/v1/apis/prediction/v1/rest'
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI if :user_ip is set' do
|
||||
CLIENT.user_ip = '127.0.0.1'
|
||||
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/discovery/v1/apis/prediction/v1.2/rest?userIp=127.0.0.1') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
CLIENT.execute(
|
||||
:http_method => 'GET',
|
||||
:uri => CLIENT.discovery_uri('prediction', 'v1.2'),
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI if :key is set' do
|
||||
CLIENT.key = 'qwerty'
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:http_method => 'GET',
|
||||
:uri => CLIENT.discovery_uri('prediction', 'v1.2'),
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI if both are set' do
|
||||
CLIENT.key = 'qwerty'
|
||||
CLIENT.user_ip = '127.0.0.1'
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/discovery/v1/apis/prediction/v1.2/rest?key=qwerty&userIp=127.0.0.1') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:http_method => 'GET',
|
||||
:uri => CLIENT.discovery_uri('prediction', 'v1.2'),
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should correctly generate API objects' do
|
||||
CLIENT.discovered_api('prediction', 'v1.2').name.should == 'prediction'
|
||||
CLIENT.discovered_api('prediction', 'v1.2').version.should == 'v1.2'
|
||||
CLIENT.discovered_api(:prediction, 'v1.2').name.should == 'prediction'
|
||||
CLIENT.discovered_api(:prediction, 'v1.2').version.should == 'v1.2'
|
||||
end
|
||||
|
||||
it 'should discover methods' do
|
||||
CLIENT.discovered_method(
|
||||
'prediction.training.insert', 'prediction', 'v1.2'
|
||||
).name.should == 'insert'
|
||||
CLIENT.discovered_method(
|
||||
:'prediction.training.insert', :prediction, 'v1.2'
|
||||
).name.should == 'insert'
|
||||
CLIENT.discovered_method(
|
||||
'prediction.training.delete', 'prediction', 'v1.2'
|
||||
).name.should == 'delete'
|
||||
end
|
||||
|
||||
it 'should define the origin API in discovered methods' do
|
||||
CLIENT.discovered_method(
|
||||
'prediction.training.insert', 'prediction', 'v1.2'
|
||||
).api.name.should == 'prediction'
|
||||
end
|
||||
|
||||
it 'should not find methods that are not in the discovery document' do
|
||||
CLIENT.discovered_method(
|
||||
'prediction.bogus', 'prediction', 'v1.2'
|
||||
).should == nil
|
||||
end
|
||||
|
||||
it 'should raise an error for bogus methods' do
|
||||
(lambda do
|
||||
CLIENT.discovered_method(42, 'prediction', 'v1.2')
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should raise an error for bogus methods' do
|
||||
(lambda do
|
||||
CLIENT.execute(:api_method => CLIENT.discovered_api('prediction', 'v1.2'))
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should correctly determine the preferred version' do
|
||||
CLIENT.preferred_version('prediction').version.should_not == 'v1'
|
||||
CLIENT.preferred_version(:prediction).version.should_not == 'v1'
|
||||
end
|
||||
|
||||
it 'should return a batch path' do
|
||||
CLIENT.discovered_api('prediction', 'v1.2').batch_path.should_not be_nil
|
||||
end
|
||||
|
||||
it 'should generate valid requests' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
env[:body].should == ''
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate valid requests when parameter value includes semicolon' do
|
||||
conn = stub_connection do |stub|
|
||||
# semicolon (;) in parameter value was being converted to
|
||||
# bare ampersand (&) in 0.4.7. ensure that it gets converted
|
||||
# to a CGI-escaped semicolon (%3B) instead.
|
||||
stub.post('/prediction/v1.2/training?data=12345%3B67890') do |env|
|
||||
env[:body].should == ''
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345;67890'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate valid requests when multivalued parameters are passed' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=1&data=2') do |env|
|
||||
env.params['data'].should include('1', '2')
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => ['1', '2']},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate requests against the correct URIs' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate requests against the correct URIs' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should allow modification to the base URIs for testing purposes' do
|
||||
# Using a new client instance here to avoid caching rebased discovery doc
|
||||
prediction_rebase =
|
||||
Google::APIClient.new(:application_name => 'API Client Tests').discovered_api('prediction', 'v1.2')
|
||||
prediction_rebase.method_base =
|
||||
'https://testing-domain.example.com/prediction/v1.2/'
|
||||
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training') do |env|
|
||||
env[:url].host.should == 'testing-domain.example.com'
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
request = CLIENT.execute(
|
||||
:api_method => prediction_rebase.training.insert,
|
||||
:parameters => {'data' => '123'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate OAuth 1 requests' do
|
||||
CLIENT.authorization = :oauth_1
|
||||
CLIENT.authorization.token_credential_key = '12345'
|
||||
CLIENT.authorization.token_credential_secret = '12345'
|
||||
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
env[:request_headers].should have_key('Authorization')
|
||||
env[:request_headers]['Authorization'].should =~ /^OAuth/
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should generate OAuth 2 requests' do
|
||||
CLIENT.authorization = :oauth_2
|
||||
CLIENT.authorization.access_token = '12345'
|
||||
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
env[:request_headers].should have_key('Authorization')
|
||||
env[:request_headers]['Authorization'].should =~ /^Bearer/
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
request = CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should not be able to execute improperly authorized requests' do
|
||||
CLIENT.authorization = :oauth_1
|
||||
CLIENT.authorization.token_credential_key = '12345'
|
||||
CLIENT.authorization.token_credential_secret = '12345'
|
||||
result = CLIENT.execute(
|
||||
@prediction.training.insert,
|
||||
{'data' => '12345'}
|
||||
)
|
||||
result.response.status.should == 401
|
||||
end
|
||||
|
||||
it 'should not be able to execute improperly authorized requests' do
|
||||
CLIENT.authorization = :oauth_2
|
||||
CLIENT.authorization.access_token = '12345'
|
||||
result = CLIENT.execute(
|
||||
@prediction.training.insert,
|
||||
{'data' => '12345'}
|
||||
)
|
||||
result.response.status.should == 401
|
||||
end
|
||||
|
||||
it 'should not be able to execute improperly authorized requests' do
|
||||
(lambda do
|
||||
CLIENT.authorization = :oauth_1
|
||||
CLIENT.authorization.token_credential_key = '12345'
|
||||
CLIENT.authorization.token_credential_secret = '12345'
|
||||
result = CLIENT.execute!(
|
||||
@prediction.training.insert,
|
||||
{'data' => '12345'}
|
||||
)
|
||||
end).should raise_error(Google::APIClient::ClientError)
|
||||
end
|
||||
|
||||
it 'should not be able to execute improperly authorized requests' do
|
||||
(lambda do
|
||||
CLIENT.authorization = :oauth_2
|
||||
CLIENT.authorization.access_token = '12345'
|
||||
result = CLIENT.execute!(
|
||||
@prediction.training.insert,
|
||||
{'data' => '12345'}
|
||||
)
|
||||
end).should raise_error(Google::APIClient::ClientError)
|
||||
end
|
||||
|
||||
it 'should correctly handle unnamed parameters' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training') do |env|
|
||||
env[:request_headers].should have_key('Content-Type')
|
||||
env[:request_headers]['Content-Type'].should == 'application/json'
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
CLIENT.authorization = :oauth_2
|
||||
CLIENT.authorization.access_token = '12345'
|
||||
CLIENT.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:body => MultiJson.dump({"id" => "bucket/object"}),
|
||||
:headers => {'Content-Type' => 'application/json'},
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the plus API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@plus = CLIENT.discovered_api('plus')
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI' do
|
||||
CLIENT.discovery_uri('plus').should ===
|
||||
'https://www.googleapis.com/discovery/v1/apis/plus/v1/rest'
|
||||
end
|
||||
|
||||
it 'should find APIs that are in the discovery document' do
|
||||
CLIENT.discovered_api('plus').name.should == 'plus'
|
||||
CLIENT.discovered_api('plus').version.should == 'v1'
|
||||
CLIENT.discovered_api(:plus).name.should == 'plus'
|
||||
CLIENT.discovered_api(:plus).version.should == 'v1'
|
||||
end
|
||||
|
||||
it 'should find methods that are in the discovery document' do
|
||||
# TODO(bobaman) Fix this when the RPC names are correct
|
||||
CLIENT.discovered_method(
|
||||
'plus.activities.list', 'plus'
|
||||
).name.should == 'list'
|
||||
end
|
||||
|
||||
it 'should define the origin API in discovered methods' do
|
||||
CLIENT.discovered_method(
|
||||
'plus.activities.list', 'plus'
|
||||
).api.name.should == 'plus'
|
||||
end
|
||||
|
||||
it 'should not find methods that are not in the discovery document' do
|
||||
CLIENT.discovered_method('plus.bogus', 'plus').should == nil
|
||||
end
|
||||
|
||||
it 'should generate requests against the correct URIs' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/plus/v1/people/107807692475771887386/activities/public') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
request = CLIENT.execute(
|
||||
:api_method => @plus.activities.list,
|
||||
:parameters => {
|
||||
'userId' => '107807692475771887386', 'collection' => 'public'
|
||||
},
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should correctly validate parameters' do
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @plus.activities.list,
|
||||
:parameters => {'alt' => 'json'},
|
||||
:authenticated => false
|
||||
)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should correctly validate parameters' do
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @plus.activities.list,
|
||||
:parameters => {
|
||||
'userId' => '107807692475771887386', 'collection' => 'bogus'
|
||||
},
|
||||
:authenticated => false
|
||||
).to_env(CLIENT.connection)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the adsense API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@adsense = CLIENT.discovered_api('adsense', 'v1.3')
|
||||
end
|
||||
|
||||
it 'should correctly determine the discovery URI' do
|
||||
CLIENT.discovery_uri('adsense', 'v1.3').to_s.should ===
|
||||
'https://www.googleapis.com/discovery/v1/apis/adsense/v1.3/rest'
|
||||
end
|
||||
|
||||
it 'should find APIs that are in the discovery document' do
|
||||
CLIENT.discovered_api('adsense', 'v1.3').name.should == 'adsense'
|
||||
CLIENT.discovered_api('adsense', 'v1.3').version.should == 'v1.3'
|
||||
end
|
||||
|
||||
it 'should return a batch path' do
|
||||
CLIENT.discovered_api('adsense', 'v1.3').batch_path.should_not be_nil
|
||||
end
|
||||
|
||||
it 'should find methods that are in the discovery document' do
|
||||
CLIENT.discovered_method(
|
||||
'adsense.reports.generate', 'adsense', 'v1.3'
|
||||
).name.should == 'generate'
|
||||
end
|
||||
|
||||
it 'should not find methods that are not in the discovery document' do
|
||||
CLIENT.discovered_method('adsense.bogus', 'adsense', 'v1.3').should == nil
|
||||
end
|
||||
|
||||
it 'should generate requests against the correct URIs' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/adclients') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @adsense.adclients.list,
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should not be able to execute requests without authorization' do
|
||||
result = CLIENT.execute(
|
||||
:api_method => @adsense.adclients.list,
|
||||
:authenticated => false
|
||||
)
|
||||
result.response.status.should == 401
|
||||
end
|
||||
|
||||
it 'should fail when validating missing required parameters' do
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:authenticated => false
|
||||
)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should succeed when validating parameters in a correct call' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/reports?dimension=DATE&endDate=2010-01-01&metric=PAGE_VIEWS&startDate=2000-01-01') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:parameters => {
|
||||
'startDate' => '2000-01-01',
|
||||
'endDate' => '2010-01-01',
|
||||
'dimension' => 'DATE',
|
||||
'metric' => 'PAGE_VIEWS'
|
||||
},
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
end).should_not raise_error
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should fail when validating parameters with invalid values' do
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:parameters => {
|
||||
'startDate' => '2000-01-01',
|
||||
'endDate' => '2010-01-01',
|
||||
'dimension' => 'BAD_CHARACTERS=-&*(£&',
|
||||
'metric' => 'PAGE_VIEWS'
|
||||
},
|
||||
:authenticated => false
|
||||
)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should succeed when validating repeated parameters in a correct call' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/reports?dimension=DATE&dimension=PRODUCT_CODE'+
|
||||
'&endDate=2010-01-01&metric=CLICKS&metric=PAGE_VIEWS&'+
|
||||
'startDate=2000-01-01') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:parameters => {
|
||||
'startDate' => '2000-01-01',
|
||||
'endDate' => '2010-01-01',
|
||||
'dimension' => ['DATE', 'PRODUCT_CODE'],
|
||||
'metric' => ['PAGE_VIEWS', 'CLICKS']
|
||||
},
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
end).should_not raise_error
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should fail when validating incorrect repeated parameters' do
|
||||
(lambda do
|
||||
CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:parameters => {
|
||||
'startDate' => '2000-01-01',
|
||||
'endDate' => '2010-01-01',
|
||||
'dimension' => ['DATE', 'BAD_CHARACTERS=-&*(£&'],
|
||||
'metric' => ['PAGE_VIEWS', 'CLICKS']
|
||||
},
|
||||
:authenticated => false
|
||||
)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should generate valid requests when multivalued parameters are passed' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/reports?dimension=DATE&dimension=PRODUCT_CODE'+
|
||||
'&endDate=2010-01-01&metric=CLICKS&metric=PAGE_VIEWS&'+
|
||||
'startDate=2000-01-01') do |env|
|
||||
env.params['dimension'].should include('DATE', 'PRODUCT_CODE')
|
||||
env.params['metric'].should include('CLICKS', 'PAGE_VIEWS')
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
request = CLIENT.execute(
|
||||
:api_method => @adsense.reports.generate,
|
||||
:parameters => {
|
||||
'startDate' => '2000-01-01',
|
||||
'endDate' => '2010-01-01',
|
||||
'dimension' => ['DATE', 'PRODUCT_CODE'],
|
||||
'metric' => ['PAGE_VIEWS', 'CLICKS']
|
||||
},
|
||||
:authenticated => false,
|
||||
:connection => conn
|
||||
)
|
||||
conn.verify
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the Drive API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@drive = CLIENT.discovered_api('drive', 'v1')
|
||||
end
|
||||
|
||||
it 'should include media upload info methods' do
|
||||
@drive.files.insert.media_upload.should_not == nil
|
||||
end
|
||||
|
||||
it 'should include accepted media types' do
|
||||
@drive.files.insert.media_upload.accepted_types.should_not be_empty
|
||||
end
|
||||
|
||||
it 'should have an upload path' do
|
||||
@drive.files.insert.media_upload.uri_template.should_not == nil
|
||||
end
|
||||
|
||||
it 'should have a max file size' do
|
||||
@drive.files.insert.media_upload.max_size.should_not == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
describe Google::APIClient::Gzip do
|
||||
|
||||
def create_connection(&block)
|
||||
Faraday.new do |b|
|
||||
b.response :gzip
|
||||
b.adapter :test do |stub|
|
||||
stub.get '/', &block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'should ignore non-zipped content' do
|
||||
conn = create_connection do |env|
|
||||
[200, {}, 'Hello world']
|
||||
end
|
||||
result = conn.get('/')
|
||||
result.body.should == "Hello world"
|
||||
end
|
||||
|
||||
it 'should decompress gziped content' do
|
||||
conn = create_connection do |env|
|
||||
[200, { 'Content-Encoding' => 'gzip'}, Base64.decode64('H4sICLVGwlEAA3RtcADzSM3JyVcozy/KSeECANXgObcMAAAA')]
|
||||
end
|
||||
result = conn.get('/')
|
||||
result.body.should == "Hello world\n"
|
||||
end
|
||||
|
||||
describe 'with API Client' do
|
||||
|
||||
before do
|
||||
@client = Google::APIClient.new(:application_name => 'test')
|
||||
@client.authorization = nil
|
||||
end
|
||||
|
||||
|
||||
it 'should send gzip in user agent' do
|
||||
conn = create_connection do |env|
|
||||
agent = env[:request_headers]['User-Agent']
|
||||
agent.should_not be_nil
|
||||
agent.should include 'gzip'
|
||||
[200, {}, 'Hello world']
|
||||
end
|
||||
@client.execute(:uri => 'http://www.example.com/', :connection => conn)
|
||||
end
|
||||
|
||||
it 'should send gzip in accept-encoding' do
|
||||
conn = create_connection do |env|
|
||||
encoding = env[:request_headers]['Accept-Encoding']
|
||||
encoding.should_not be_nil
|
||||
encoding.should include 'gzip'
|
||||
[200, {}, 'Hello world']
|
||||
end
|
||||
@client.execute(:uri => 'http://www.example.com/', :connection => conn)
|
||||
end
|
||||
|
||||
it 'should not send gzip in accept-encoding if disabled for request' do
|
||||
conn = create_connection do |env|
|
||||
encoding = env[:request_headers]['Accept-Encoding']
|
||||
encoding.should_not include('gzip') unless encoding.nil?
|
||||
[200, {}, 'Hello world']
|
||||
end
|
||||
response = @client.execute(:uri => 'http://www.example.com/', :gzip => false, :connection => conn)
|
||||
puts response.status
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
fixtures_path = File.expand_path('../../../fixtures', __FILE__)
|
||||
|
||||
describe Google::APIClient::UploadIO do
|
||||
it 'should reject invalid file paths' do
|
||||
(lambda do
|
||||
media = Google::APIClient::UploadIO.new('doesnotexist', 'text/plain')
|
||||
end).should raise_error
|
||||
end
|
||||
|
||||
describe 'with a file' do
|
||||
before do
|
||||
@file = File.expand_path('files/sample.txt', fixtures_path)
|
||||
@media = Google::APIClient::UploadIO.new(@file, 'text/plain')
|
||||
end
|
||||
|
||||
it 'should report the correct file length' do
|
||||
@media.length.should == File.size(@file)
|
||||
end
|
||||
|
||||
it 'should have a mime type' do
|
||||
@media.content_type.should == 'text/plain'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with StringIO' do
|
||||
before do
|
||||
@content = "hello world"
|
||||
@media = Google::APIClient::UploadIO.new(StringIO.new(@content), 'text/plain', 'test.txt')
|
||||
end
|
||||
|
||||
it 'should report the correct file length' do
|
||||
@media.length.should == @content.length
|
||||
end
|
||||
|
||||
it 'should have a mime type' do
|
||||
@media.content_type.should == 'text/plain'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Google::APIClient::RangedIO do
|
||||
before do
|
||||
@source = StringIO.new("1234567890abcdef")
|
||||
@io = Google::APIClient::RangedIO.new(@source, 1, 5)
|
||||
end
|
||||
|
||||
it 'should return the correct range when read entirely' do
|
||||
@io.read.should == "23456"
|
||||
end
|
||||
|
||||
it 'should maintain position' do
|
||||
@io.read(1).should == '2'
|
||||
@io.read(2).should == '34'
|
||||
@io.read(2).should == '56'
|
||||
end
|
||||
|
||||
it 'should allow rewinds' do
|
||||
@io.read(2).should == '23'
|
||||
@io.rewind()
|
||||
@io.read(2).should == '23'
|
||||
end
|
||||
|
||||
it 'should allow setting position' do
|
||||
@io.pos = 3
|
||||
@io.read.should == '56'
|
||||
end
|
||||
|
||||
it 'should not allow position to be set beyond range' do
|
||||
@io.pos = 10
|
||||
@io.read.should == ''
|
||||
end
|
||||
|
||||
it 'should return empty string when read amount is zero' do
|
||||
@io.read(0).should == ''
|
||||
end
|
||||
|
||||
it 'should return empty string at EOF if amount is nil' do
|
||||
@io.read
|
||||
@io.read.should == ''
|
||||
end
|
||||
|
||||
it 'should return nil at EOF if amount is positive int' do
|
||||
@io.read
|
||||
@io.read(1).should == nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe Google::APIClient::ResumableUpload do
|
||||
CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)
|
||||
|
||||
after do
|
||||
# Reset client to not-quite-pristine state
|
||||
CLIENT.key = nil
|
||||
CLIENT.user_ip = nil
|
||||
end
|
||||
|
||||
before do
|
||||
@drive = CLIENT.discovered_api('drive', 'v1')
|
||||
@file = File.expand_path('files/sample.txt', fixtures_path)
|
||||
@media = Google::APIClient::UploadIO.new(@file, 'text/plain')
|
||||
@uploader = Google::APIClient::ResumableUpload.new(
|
||||
:media => @media,
|
||||
:api_method => @drive.files.insert,
|
||||
:uri => 'https://www.googleapis.com/upload/drive/v1/files/12345')
|
||||
end
|
||||
|
||||
it 'should consider 20x status as complete' do
|
||||
request = @uploader.to_http_request
|
||||
@uploader.process_http_response(mock_result(200))
|
||||
@uploader.complete?.should == true
|
||||
end
|
||||
|
||||
it 'should consider 30x status as incomplete' do
|
||||
request = @uploader.to_http_request
|
||||
@uploader.process_http_response(mock_result(308))
|
||||
@uploader.complete?.should == false
|
||||
@uploader.expired?.should == false
|
||||
end
|
||||
|
||||
it 'should consider 40x status as fatal' do
|
||||
request = @uploader.to_http_request
|
||||
@uploader.process_http_response(mock_result(404))
|
||||
@uploader.expired?.should == true
|
||||
end
|
||||
|
||||
it 'should detect changes to location' do
|
||||
request = @uploader.to_http_request
|
||||
@uploader.process_http_response(mock_result(308, 'location' => 'https://www.googleapis.com/upload/drive/v1/files/abcdef'))
|
||||
@uploader.uri.to_s.should == 'https://www.googleapis.com/upload/drive/v1/files/abcdef'
|
||||
end
|
||||
|
||||
it 'should resume from the saved range reported by the server' do
|
||||
@uploader.chunk_size = 200
|
||||
@uploader.to_http_request # Send bytes 0-199, only 0-99 saved
|
||||
@uploader.process_http_response(mock_result(308, 'range' => '0-99'))
|
||||
method, url, headers, body = @uploader.to_http_request # Send bytes 100-299
|
||||
headers['Content-Range'].should == "bytes 100-299/#{@media.length}"
|
||||
headers['Content-length'].should == "200"
|
||||
end
|
||||
|
||||
it 'should resync the offset after 5xx errors' do
|
||||
@uploader.chunk_size = 200
|
||||
@uploader.to_http_request
|
||||
@uploader.process_http_response(mock_result(500)) # Invalidates range
|
||||
method, url, headers, body = @uploader.to_http_request # Resync
|
||||
headers['Content-Range'].should == "bytes */#{@media.length}"
|
||||
headers['Content-length'].should == "0"
|
||||
@uploader.process_http_response(mock_result(308, 'range' => '0-99'))
|
||||
method, url, headers, body = @uploader.to_http_request # Send next chunk at correct range
|
||||
headers['Content-Range'].should == "bytes 100-299/#{@media.length}"
|
||||
headers['Content-length'].should == "200"
|
||||
end
|
||||
|
||||
def mock_result(status, headers = {})
|
||||
reference = Google::APIClient::Reference.new(:api_method => @drive.files.insert)
|
||||
double('result', :status => status, :headers => headers, :reference => reference)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
describe Google::APIClient::Request do
|
||||
CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)
|
||||
|
||||
it 'should normalize parameter names to strings' do
|
||||
request = Google::APIClient::Request.new(:uri => 'https://www.google.com', :parameters => {
|
||||
:a => '1', 'b' => '2'
|
||||
})
|
||||
request.parameters['a'].should == '1'
|
||||
request.parameters['b'].should == '2'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
describe Google::APIClient::Result do
|
||||
CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT)
|
||||
|
||||
describe 'with the plus API' do
|
||||
before do
|
||||
CLIENT.authorization = nil
|
||||
@plus = CLIENT.discovered_api('plus', 'v1')
|
||||
@reference = Google::APIClient::Reference.new({
|
||||
:api_method => @plus.activities.list,
|
||||
:parameters => {
|
||||
'userId' => 'me',
|
||||
'collection' => 'public',
|
||||
'maxResults' => 20
|
||||
}
|
||||
})
|
||||
@request = @reference.to_http_request
|
||||
|
||||
# Response double
|
||||
@response = double("response")
|
||||
@response.stub(:status).and_return(200)
|
||||
@response.stub(:headers).and_return({
|
||||
'etag' => '12345',
|
||||
'x-google-apiary-auth-scopes' =>
|
||||
'https://www.googleapis.com/auth/plus.me',
|
||||
'content-type' => 'application/json; charset=UTF-8',
|
||||
'date' => 'Mon, 23 Apr 2012 00:00:00 GMT',
|
||||
'cache-control' => 'private, max-age=0, must-revalidate, no-transform',
|
||||
'server' => 'GSE',
|
||||
'connection' => 'close'
|
||||
})
|
||||
end
|
||||
|
||||
describe 'with a next page token' do
|
||||
before do
|
||||
@response.stub(:body).and_return(
|
||||
<<-END_OF_STRING
|
||||
{
|
||||
"kind": "plus#activityFeed",
|
||||
"etag": "FOO",
|
||||
"nextPageToken": "NEXT+PAGE+TOKEN",
|
||||
"selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?",
|
||||
"nextLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN",
|
||||
"title": "Plus Public Activity Feed for ",
|
||||
"updated": "2012-04-23T00:00:00.000Z",
|
||||
"id": "123456790",
|
||||
"items": []
|
||||
}
|
||||
END_OF_STRING
|
||||
)
|
||||
@result = Google::APIClient::Result.new(@reference, @response)
|
||||
end
|
||||
|
||||
it 'should indicate a successful response' do
|
||||
@result.error?.should be_false
|
||||
end
|
||||
|
||||
it 'should return the correct next page token' do
|
||||
@result.next_page_token.should == 'NEXT+PAGE+TOKEN'
|
||||
end
|
||||
|
||||
it 'should escape the next page token when calling next_page' do
|
||||
reference = @result.next_page
|
||||
Hash[reference.parameters].should include('pageToken')
|
||||
Hash[reference.parameters]['pageToken'].should == 'NEXT+PAGE+TOKEN'
|
||||
url = reference.to_env(CLIENT.connection)[:url]
|
||||
url.to_s.should include('pageToken=NEXT%2BPAGE%2BTOKEN')
|
||||
end
|
||||
|
||||
it 'should return content type correctly' do
|
||||
@result.media_type.should == 'application/json'
|
||||
end
|
||||
|
||||
it 'should return the result data correctly' do
|
||||
@result.data?.should be_true
|
||||
@result.data.class.to_s.should ==
|
||||
'Google::APIClient::Schema::Plus::V1::ActivityFeed'
|
||||
@result.data.kind.should == 'plus#activityFeed'
|
||||
@result.data.etag.should == 'FOO'
|
||||
@result.data.nextPageToken.should == 'NEXT+PAGE+TOKEN'
|
||||
@result.data.selfLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?'
|
||||
@result.data.nextLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?' +
|
||||
'maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN'
|
||||
@result.data.title.should == 'Plus Public Activity Feed for '
|
||||
@result.data.id.should == "123456790"
|
||||
@result.data.items.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'without a next page token' do
|
||||
before do
|
||||
@response.stub(:body).and_return(
|
||||
<<-END_OF_STRING
|
||||
{
|
||||
"kind": "plus#activityFeed",
|
||||
"etag": "FOO",
|
||||
"selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?",
|
||||
"title": "Plus Public Activity Feed for ",
|
||||
"updated": "2012-04-23T00:00:00.000Z",
|
||||
"id": "123456790",
|
||||
"items": []
|
||||
}
|
||||
END_OF_STRING
|
||||
)
|
||||
@result = Google::APIClient::Result.new(@reference, @response)
|
||||
end
|
||||
|
||||
it 'should not return a next page token' do
|
||||
@result.next_page_token.should == nil
|
||||
end
|
||||
|
||||
it 'should return content type correctly' do
|
||||
@result.media_type.should == 'application/json'
|
||||
end
|
||||
|
||||
it 'should return the result data correctly' do
|
||||
@result.data?.should be_true
|
||||
@result.data.class.to_s.should ==
|
||||
'Google::APIClient::Schema::Plus::V1::ActivityFeed'
|
||||
@result.data.kind.should == 'plus#activityFeed'
|
||||
@result.data.etag.should == 'FOO'
|
||||
@result.data.selfLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?'
|
||||
@result.data.title.should == 'Plus Public Activity Feed for '
|
||||
@result.data.id.should == "123456790"
|
||||
@result.data.items.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with JSON error response' do
|
||||
before do
|
||||
@response.stub(:body).and_return(
|
||||
<<-END_OF_STRING
|
||||
{
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "parseError",
|
||||
"message": "Parse Error"
|
||||
}
|
||||
],
|
||||
"code": 400,
|
||||
"message": "Parse Error"
|
||||
}
|
||||
}
|
||||
END_OF_STRING
|
||||
)
|
||||
@response.stub(:status).and_return(400)
|
||||
@result = Google::APIClient::Result.new(@reference, @response)
|
||||
end
|
||||
|
||||
it 'should return error status correctly' do
|
||||
@result.error?.should be_true
|
||||
end
|
||||
|
||||
it 'should return the correct error message' do
|
||||
@result.error_message.should == 'Parse Error'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with 204 No Content response' do
|
||||
before do
|
||||
@response.stub(:body).and_return('')
|
||||
@response.stub(:status).and_return(204)
|
||||
@response.stub(:headers).and_return({})
|
||||
@result = Google::APIClient::Result.new(@reference, @response)
|
||||
end
|
||||
|
||||
it 'should indicate no data is available' do
|
||||
@result.data?.should be_false
|
||||
end
|
||||
|
||||
it 'should return nil for data' do
|
||||
@result.data.should == nil
|
||||
end
|
||||
|
||||
it 'should return nil for media_type' do
|
||||
@result.media_type.should == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# Copyright 2012 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
|
||||
fixtures_path = File.expand_path('../../../fixtures', __FILE__)
|
||||
|
||||
describe Google::APIClient::KeyUtils do
|
||||
it 'should read PKCS12 files from the filesystem' do
|
||||
pending "Reading from PKCS12 not supported on jruby" if RUBY_PLATFORM == 'java'
|
||||
path = File.expand_path('files/privatekey.p12', fixtures_path)
|
||||
key = Google::APIClient::KeyUtils.load_from_pkcs12(path, 'notasecret')
|
||||
key.should_not == nil
|
||||
end
|
||||
|
||||
it 'should read PKCS12 files from loaded files' do
|
||||
pending "Reading from PKCS12 not supported on jruby" if RUBY_PLATFORM == 'java'
|
||||
path = File.expand_path('files/privatekey.p12', fixtures_path)
|
||||
content = File.read(path)
|
||||
key = Google::APIClient::KeyUtils.load_from_pkcs12(content, 'notasecret')
|
||||
key.should_not == nil
|
||||
end
|
||||
|
||||
it 'should read PEM files from the filesystem' do
|
||||
path = File.expand_path('files/secret.pem', fixtures_path)
|
||||
key = Google::APIClient::KeyUtils.load_from_pem(path, 'notasecret')
|
||||
key.should_not == nil
|
||||
end
|
||||
|
||||
it 'should read PEM files from loaded files' do
|
||||
path = File.expand_path('files/secret.pem', fixtures_path)
|
||||
content = File.read(path)
|
||||
key = Google::APIClient::KeyUtils.load_from_pem(content, 'notasecret')
|
||||
key.should_not == nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe Google::APIClient::JWTAsserter do
|
||||
include ConnectionHelpers
|
||||
|
||||
before do
|
||||
@key = OpenSSL::PKey::RSA.new 2048
|
||||
end
|
||||
|
||||
it 'should generate valid JWTs' do
|
||||
asserter = Google::APIClient::JWTAsserter.new('client1', 'scope1 scope2', @key)
|
||||
jwt = asserter.to_authorization.to_jwt
|
||||
jwt.should_not == nil
|
||||
|
||||
claim = JWT.decode(jwt, @key.public_key, true)
|
||||
claim["iss"].should == 'client1'
|
||||
claim["scope"].should == 'scope1 scope2'
|
||||
end
|
||||
|
||||
it 'should allow impersonation' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/o/oauth2/token') do |env|
|
||||
params = Addressable::URI.form_unencode(env[:body])
|
||||
JWT.decode(params.assoc("assertion").last, @key.public_key)
|
||||
params.assoc("grant_type").should == ['grant_type','urn:ietf:params:oauth:grant-type:jwt-bearer']
|
||||
[200, {}, '{
|
||||
"access_token" : "1/abcdef1234567890",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}']
|
||||
end
|
||||
end
|
||||
asserter = Google::APIClient::JWTAsserter.new('client1', 'scope1 scope2', @key)
|
||||
auth = asserter.authorize('user1@email.com', { :connection => conn })
|
||||
auth.should_not == nil?
|
||||
auth.person.should == 'user1@email.com'
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should send valid access token request' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/o/oauth2/token') do |env|
|
||||
params = Addressable::URI.form_unencode(env[:body])
|
||||
JWT.decode(params.assoc("assertion").last, @key.public_key)
|
||||
params.assoc("grant_type").should == ['grant_type','urn:ietf:params:oauth:grant-type:jwt-bearer']
|
||||
[200, {}, '{
|
||||
"access_token" : "1/abcdef1234567890",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}']
|
||||
end
|
||||
end
|
||||
asserter = Google::APIClient::JWTAsserter.new('client1', 'scope1 scope2', @key)
|
||||
auth = asserter.authorize(nil, { :connection => conn })
|
||||
auth.should_not == nil?
|
||||
auth.access_token.should == "1/abcdef1234567890"
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should be refreshable' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/o/oauth2/token') do |env|
|
||||
params = Addressable::URI.form_unencode(env[:body])
|
||||
JWT.decode(params.assoc("assertion").last, @key.public_key)
|
||||
params.assoc("grant_type").should == ['grant_type','urn:ietf:params:oauth:grant-type:jwt-bearer']
|
||||
[200, {}, '{
|
||||
"access_token" : "1/abcdef1234567890",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}']
|
||||
end
|
||||
stub.post('/o/oauth2/token') do |env|
|
||||
params = Addressable::URI.form_unencode(env[:body])
|
||||
JWT.decode(params.assoc("assertion").last, @key.public_key)
|
||||
params.assoc("grant_type").should == ['grant_type','urn:ietf:params:oauth:grant-type:jwt-bearer']
|
||||
[200, {}, '{
|
||||
"access_token" : "1/0987654321fedcba",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}']
|
||||
end
|
||||
end
|
||||
asserter = Google::APIClient::JWTAsserter.new('client1', 'scope1 scope2', @key)
|
||||
auth = asserter.authorize(nil, { :connection => conn })
|
||||
auth.should_not == nil?
|
||||
auth.access_token.should == "1/abcdef1234567890"
|
||||
|
||||
auth.fetch_access_token!(:connection => conn)
|
||||
auth.access_token.should == "1/0987654321fedcba"
|
||||
|
||||
conn.verify
|
||||
end
|
||||
end
|
||||
|
||||
describe Google::APIClient::ComputeServiceAccount do
|
||||
include ConnectionHelpers
|
||||
|
||||
it 'should query metadata server' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/computeMetadata/v1beta1/instance/service-accounts/default/token') do |env|
|
||||
env.url.host.should == 'metadata'
|
||||
[200, {}, '{
|
||||
"access_token" : "1/abcdef1234567890",
|
||||
"token_type" : "Bearer",
|
||||
"expires_in" : 3600
|
||||
}']
|
||||
end
|
||||
end
|
||||
service_account = Google::APIClient::ComputeServiceAccount.new
|
||||
auth = service_account.fetch_access_token!({ :connection => conn })
|
||||
auth.should_not == nil?
|
||||
auth["access_token"].should == "1/abcdef1234567890"
|
||||
conn.verify
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,586 @@
|
|||
# encoding:utf-8
|
||||
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/service'
|
||||
|
||||
fixtures_path = File.expand_path('../../../fixtures', __FILE__)
|
||||
|
||||
describe Google::APIClient::Service do
|
||||
include ConnectionHelpers
|
||||
|
||||
APPLICATION_NAME = 'API Client Tests'
|
||||
|
||||
it 'should error out when called without an API name or version' do
|
||||
(lambda do
|
||||
Google::APIClient::Service.new
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should error out when called without an API version' do
|
||||
(lambda do
|
||||
Google::APIClient::Service.new('foo')
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'should error out when the options hash is not a hash' do
|
||||
(lambda do
|
||||
Google::APIClient::Service.new('foo', 'v1', 42)
|
||||
end).should raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
describe 'with the AdSense Management API' do
|
||||
|
||||
it 'should make a valid call for a method with no parameters' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/adclients') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
adsense = Google::APIClient::Service.new(
|
||||
'adsense',
|
||||
'v1.3',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
|
||||
req = adsense.adclients.list.execute()
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should make a valid call for a method with parameters' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/adclients/1/adunits') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
adsense = Google::APIClient::Service.new(
|
||||
'adsense',
|
||||
'v1.3',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
req = adsense.adunits.list(:adClientId => '1').execute()
|
||||
end
|
||||
|
||||
it 'should make a valid call for a deep method' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/adsense/v1.3/accounts/1/adclients') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
adsense = Google::APIClient::Service.new(
|
||||
'adsense',
|
||||
'v1.3',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
req = adsense.accounts.adclients.list(:accountId => '1').execute()
|
||||
end
|
||||
|
||||
describe 'with no connection' do
|
||||
before do
|
||||
@adsense = Google::APIClient::Service.new('adsense', 'v1.3',
|
||||
{:application_name => APPLICATION_NAME, :cache_store => nil})
|
||||
end
|
||||
|
||||
it 'should return a resource when using a valid resource name' do
|
||||
@adsense.accounts.should be_a(Google::APIClient::Service::Resource)
|
||||
end
|
||||
|
||||
it 'should throw an error when using an invalid resource name' do
|
||||
(lambda do
|
||||
@adsense.invalid_resource
|
||||
end).should raise_error
|
||||
end
|
||||
|
||||
it 'should return a request when using a valid method name' do
|
||||
req = @adsense.adclients.list
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'adsense.adclients.list'
|
||||
req.parameters.should be_nil
|
||||
end
|
||||
|
||||
it 'should throw an error when using an invalid method name' do
|
||||
(lambda do
|
||||
@adsense.adclients.invalid_method
|
||||
end).should raise_error
|
||||
end
|
||||
|
||||
it 'should return a valid request with parameters' do
|
||||
req = @adsense.adunits.list(:adClientId => '1')
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'adsense.adunits.list'
|
||||
req.parameters.should_not be_nil
|
||||
req.parameters[:adClientId].should == '1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the Prediction API' do
|
||||
|
||||
it 'should make a valid call with an object body' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.5/trainedmodels?project=1') do |env|
|
||||
env.body.should == '{"id":"1"}'
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
prediction = Google::APIClient::Service.new(
|
||||
'prediction',
|
||||
'v1.5',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
req = prediction.trainedmodels.insert(:project => '1').body({'id' => '1'}).execute()
|
||||
conn.verify
|
||||
end
|
||||
|
||||
it 'should make a valid call with a text body' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.5/trainedmodels?project=1') do |env|
|
||||
env.body.should == '{"id":"1"}'
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
prediction = Google::APIClient::Service.new(
|
||||
'prediction',
|
||||
'v1.5',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
req = prediction.trainedmodels.insert(:project => '1').body('{"id":"1"}').execute()
|
||||
conn.verify
|
||||
end
|
||||
|
||||
describe 'with no connection' do
|
||||
before do
|
||||
@prediction = Google::APIClient::Service.new('prediction', 'v1.5',
|
||||
{:application_name => APPLICATION_NAME, :cache_store => nil})
|
||||
end
|
||||
|
||||
it 'should return a valid request with a body' do
|
||||
req = @prediction.trainedmodels.insert(:project => '1').body({'id' => '1'})
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'prediction.trainedmodels.insert'
|
||||
req.body.should == {'id' => '1'}
|
||||
req.parameters.should_not be_nil
|
||||
req.parameters[:project].should == '1'
|
||||
end
|
||||
|
||||
it 'should return a valid request with a body when using resource name' do
|
||||
req = @prediction.trainedmodels.insert(:project => '1').training({'id' => '1'})
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'prediction.trainedmodels.insert'
|
||||
req.training.should == {'id' => '1'}
|
||||
req.parameters.should_not be_nil
|
||||
req.parameters[:project].should == '1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the Drive API' do
|
||||
|
||||
before do
|
||||
@metadata = {
|
||||
'title' => 'My movie',
|
||||
'description' => 'The best home movie ever made'
|
||||
}
|
||||
@file = File.expand_path('files/sample.txt', fixtures_path)
|
||||
@media = Google::APIClient::UploadIO.new(@file, 'text/plain')
|
||||
end
|
||||
|
||||
it 'should make a valid call with an object body and media upload' do
|
||||
conn = stub_connection do |stub|
|
||||
stub.post('/upload/drive/v1/files?uploadType=multipart') do |env|
|
||||
env.body.should be_a Faraday::CompositeReadIO
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
drive = Google::APIClient::Service.new(
|
||||
'drive',
|
||||
'v1',
|
||||
{
|
||||
:application_name => APPLICATION_NAME,
|
||||
:authenticated => false,
|
||||
:connection => conn,
|
||||
:cache_store => nil
|
||||
}
|
||||
)
|
||||
req = drive.files.insert(:uploadType => 'multipart').body(@metadata).media(@media).execute()
|
||||
conn.verify
|
||||
end
|
||||
|
||||
describe 'with no connection' do
|
||||
before do
|
||||
@drive = Google::APIClient::Service.new('drive', 'v1',
|
||||
{:application_name => APPLICATION_NAME, :cache_store => nil})
|
||||
end
|
||||
|
||||
it 'should return a valid request with a body and media upload' do
|
||||
req = @drive.files.insert(:uploadType => 'multipart').body(@metadata).media(@media)
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'drive.files.insert'
|
||||
req.body.should == @metadata
|
||||
req.media.should == @media
|
||||
req.parameters.should_not be_nil
|
||||
req.parameters[:uploadType].should == 'multipart'
|
||||
end
|
||||
|
||||
it 'should return a valid request with a body and media upload when using resource name' do
|
||||
req = @drive.files.insert(:uploadType => 'multipart').file(@metadata).media(@media)
|
||||
req.should be_a(Google::APIClient::Service::Request)
|
||||
req.method.id.should == 'drive.files.insert'
|
||||
req.file.should == @metadata
|
||||
req.media.should == @media
|
||||
req.parameters.should_not be_nil
|
||||
req.parameters[:uploadType].should == 'multipart'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with the Discovery API' do
|
||||
it 'should make a valid end-to-end request' do
|
||||
discovery = Google::APIClient::Service.new('discovery', 'v1',
|
||||
{:application_name => APPLICATION_NAME, :authenticated => false,
|
||||
:cache_store => nil})
|
||||
result = discovery.apis.get_rest(:api => 'discovery', :version => 'v1').execute
|
||||
result.should_not be_nil
|
||||
result.data.name.should == 'discovery'
|
||||
result.data.version.should == 'v1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe Google::APIClient::Service::Result do
|
||||
|
||||
describe 'with the plus API' do
|
||||
before do
|
||||
@plus = Google::APIClient::Service.new('plus', 'v1',
|
||||
{:application_name => APPLICATION_NAME, :cache_store => nil})
|
||||
@reference = Google::APIClient::Reference.new({
|
||||
:api_method => @plus.activities.list.method,
|
||||
:parameters => {
|
||||
'userId' => 'me',
|
||||
'collection' => 'public',
|
||||
'maxResults' => 20
|
||||
}
|
||||
})
|
||||
@request = @plus.activities.list(:userId => 'me', :collection => 'public',
|
||||
:maxResults => 20)
|
||||
|
||||
# Response double
|
||||
@response = double("response")
|
||||
@response.stub(:status).and_return(200)
|
||||
@response.stub(:headers).and_return({
|
||||
'etag' => '12345',
|
||||
'x-google-apiary-auth-scopes' =>
|
||||
'https://www.googleapis.com/auth/plus.me',
|
||||
'content-type' => 'application/json; charset=UTF-8',
|
||||
'date' => 'Mon, 23 Apr 2012 00:00:00 GMT',
|
||||
'cache-control' => 'private, max-age=0, must-revalidate, no-transform',
|
||||
'server' => 'GSE',
|
||||
'connection' => 'close'
|
||||
})
|
||||
end
|
||||
|
||||
describe 'with a next page token' do
|
||||
before do
|
||||
@body = <<-END_OF_STRING
|
||||
{
|
||||
"kind": "plus#activityFeed",
|
||||
"etag": "FOO",
|
||||
"nextPageToken": "NEXT+PAGE+TOKEN",
|
||||
"selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?",
|
||||
"nextLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN",
|
||||
"title": "Plus Public Activity Feed for ",
|
||||
"updated": "2012-04-23T00:00:00.000Z",
|
||||
"id": "123456790",
|
||||
"items": []
|
||||
}
|
||||
END_OF_STRING
|
||||
@response.stub(:body).and_return(@body)
|
||||
base_result = Google::APIClient::Result.new(@reference, @response)
|
||||
@result = Google::APIClient::Service::Result.new(@request, base_result)
|
||||
end
|
||||
|
||||
it 'should indicate a successful response' do
|
||||
@result.error?.should be_false
|
||||
end
|
||||
|
||||
it 'should return the correct next page token' do
|
||||
@result.next_page_token.should == 'NEXT+PAGE+TOKEN'
|
||||
end
|
||||
|
||||
it 'generate a correct request when calling next_page' do
|
||||
next_page_request = @result.next_page
|
||||
next_page_request.parameters.should include('pageToken')
|
||||
next_page_request.parameters['pageToken'].should == 'NEXT+PAGE+TOKEN'
|
||||
@request.parameters.each_pair do |param, value|
|
||||
next_page_request.parameters[param].should == value
|
||||
end
|
||||
end
|
||||
|
||||
it 'should return content type correctly' do
|
||||
@result.media_type.should == 'application/json'
|
||||
end
|
||||
|
||||
it 'should return the body correctly' do
|
||||
@result.body.should == @body
|
||||
end
|
||||
|
||||
it 'should return the result data correctly' do
|
||||
@result.data?.should be_true
|
||||
@result.data.class.to_s.should ==
|
||||
'Google::APIClient::Schema::Plus::V1::ActivityFeed'
|
||||
@result.data.kind.should == 'plus#activityFeed'
|
||||
@result.data.etag.should == 'FOO'
|
||||
@result.data.nextPageToken.should == 'NEXT+PAGE+TOKEN'
|
||||
@result.data.selfLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?'
|
||||
@result.data.nextLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?' +
|
||||
'maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN'
|
||||
@result.data.title.should == 'Plus Public Activity Feed for '
|
||||
@result.data.id.should == "123456790"
|
||||
@result.data.items.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'without a next page token' do
|
||||
before do
|
||||
@body = <<-END_OF_STRING
|
||||
{
|
||||
"kind": "plus#activityFeed",
|
||||
"etag": "FOO",
|
||||
"selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?",
|
||||
"title": "Plus Public Activity Feed for ",
|
||||
"updated": "2012-04-23T00:00:00.000Z",
|
||||
"id": "123456790",
|
||||
"items": []
|
||||
}
|
||||
END_OF_STRING
|
||||
@response.stub(:body).and_return(@body)
|
||||
base_result = Google::APIClient::Result.new(@reference, @response)
|
||||
@result = Google::APIClient::Service::Result.new(@request, base_result)
|
||||
end
|
||||
|
||||
it 'should not return a next page token' do
|
||||
@result.next_page_token.should == nil
|
||||
end
|
||||
|
||||
it 'should return content type correctly' do
|
||||
@result.media_type.should == 'application/json'
|
||||
end
|
||||
|
||||
it 'should return the body correctly' do
|
||||
@result.body.should == @body
|
||||
end
|
||||
|
||||
it 'should return the result data correctly' do
|
||||
@result.data?.should be_true
|
||||
@result.data.class.to_s.should ==
|
||||
'Google::APIClient::Schema::Plus::V1::ActivityFeed'
|
||||
@result.data.kind.should == 'plus#activityFeed'
|
||||
@result.data.etag.should == 'FOO'
|
||||
@result.data.selfLink.should ==
|
||||
'https://www.googleapis.com/plus/v1/people/foo/activities/public?'
|
||||
@result.data.title.should == 'Plus Public Activity Feed for '
|
||||
@result.data.id.should == "123456790"
|
||||
@result.data.items.should be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with JSON error response' do
|
||||
before do
|
||||
@body = <<-END_OF_STRING
|
||||
{
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "parseError",
|
||||
"message": "Parse Error"
|
||||
}
|
||||
],
|
||||
"code": 400,
|
||||
"message": "Parse Error"
|
||||
}
|
||||
}
|
||||
END_OF_STRING
|
||||
@response.stub(:body).and_return(@body)
|
||||
@response.stub(:status).and_return(400)
|
||||
base_result = Google::APIClient::Result.new(@reference, @response)
|
||||
@result = Google::APIClient::Service::Result.new(@request, base_result)
|
||||
end
|
||||
|
||||
it 'should return error status correctly' do
|
||||
@result.error?.should be_true
|
||||
end
|
||||
|
||||
it 'should return the correct error message' do
|
||||
@result.error_message.should == 'Parse Error'
|
||||
end
|
||||
|
||||
it 'should return the body correctly' do
|
||||
@result.body.should == @body
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with 204 No Content response' do
|
||||
before do
|
||||
@response.stub(:body).and_return('')
|
||||
@response.stub(:status).and_return(204)
|
||||
@response.stub(:headers).and_return({})
|
||||
base_result = Google::APIClient::Result.new(@reference, @response)
|
||||
@result = Google::APIClient::Service::Result.new(@request, base_result)
|
||||
end
|
||||
|
||||
it 'should indicate no data is available' do
|
||||
@result.data?.should be_false
|
||||
end
|
||||
|
||||
it 'should return nil for data' do
|
||||
@result.data.should == nil
|
||||
end
|
||||
|
||||
it 'should return nil for media_type' do
|
||||
@result.media_type.should == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Google::APIClient::Service::BatchRequest do
|
||||
describe 'with the discovery API' do
|
||||
before do
|
||||
@discovery = Google::APIClient::Service.new('discovery', 'v1',
|
||||
{:application_name => APPLICATION_NAME, :authorization => nil,
|
||||
:cache_store => nil})
|
||||
end
|
||||
|
||||
describe 'with two valid requests' do
|
||||
before do
|
||||
@calls = [
|
||||
@discovery.apis.get_rest(:api => 'plus', :version => 'v1'),
|
||||
@discovery.apis.get_rest(:api => 'discovery', :version => 'v1')
|
||||
]
|
||||
end
|
||||
|
||||
it 'should execute both when using a global callback' do
|
||||
block_called = 0
|
||||
batch = @discovery.batch(@calls) do |result|
|
||||
block_called += 1
|
||||
result.status.should == 200
|
||||
end
|
||||
|
||||
batch.execute
|
||||
block_called.should == 2
|
||||
end
|
||||
|
||||
it 'should execute both when using individual callbacks' do
|
||||
call1_returned, call2_returned = false, false
|
||||
batch = @discovery.batch
|
||||
|
||||
batch.add(@calls[0]) do |result|
|
||||
call1_returned = true
|
||||
result.status.should == 200
|
||||
result.call_index.should == 0
|
||||
end
|
||||
|
||||
batch.add(@calls[1]) do |result|
|
||||
call2_returned = true
|
||||
result.status.should == 200
|
||||
result.call_index.should == 1
|
||||
end
|
||||
|
||||
batch.execute
|
||||
call1_returned.should == true
|
||||
call2_returned.should == true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a valid request and an invalid one' do
|
||||
before do
|
||||
@calls = [
|
||||
@discovery.apis.get_rest(:api => 'plus', :version => 'v1'),
|
||||
@discovery.apis.get_rest(:api => 'invalid', :version => 'invalid')
|
||||
]
|
||||
end
|
||||
|
||||
it 'should execute both when using a global callback' do
|
||||
block_called = 0
|
||||
batch = @discovery.batch(@calls) do |result|
|
||||
block_called += 1
|
||||
if result.call_index == 0
|
||||
result.status.should == 200
|
||||
else
|
||||
result.status.should >= 400
|
||||
result.status.should < 500
|
||||
end
|
||||
end
|
||||
|
||||
batch.execute
|
||||
block_called.should == 2
|
||||
end
|
||||
|
||||
it 'should execute both when using individual callbacks' do
|
||||
call1_returned, call2_returned = false, false
|
||||
batch = @discovery.batch
|
||||
|
||||
batch.add(@calls[0]) do |result|
|
||||
call1_returned = true
|
||||
result.status.should == 200
|
||||
result.call_index.should == 0
|
||||
end
|
||||
|
||||
batch.add(@calls[1]) do |result|
|
||||
call2_returned = true
|
||||
result.status.should >= 400
|
||||
result.status.should < 500
|
||||
result.call_index.should == 1
|
||||
end
|
||||
|
||||
batch.execute
|
||||
call1_returned.should == true
|
||||
call2_returned.should == true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# encoding:utf-8
|
||||
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client/service/simple_file_store'
|
||||
|
||||
describe Google::APIClient::Service::SimpleFileStore do
|
||||
|
||||
FILE_NAME = 'test.cache'
|
||||
|
||||
before(:all) do
|
||||
File.delete(FILE_NAME) if File.exists?(FILE_NAME)
|
||||
end
|
||||
|
||||
describe 'with no cache file' do
|
||||
before(:each) do
|
||||
File.delete(FILE_NAME) if File.exists?(FILE_NAME)
|
||||
@cache = Google::APIClient::Service::SimpleFileStore.new(FILE_NAME)
|
||||
end
|
||||
|
||||
it 'should return nil when asked if a key exists' do
|
||||
@cache.exist?('invalid').should be_nil
|
||||
File.exists?(FILE_NAME).should be_false
|
||||
end
|
||||
|
||||
it 'should return nil when asked to read a key' do
|
||||
@cache.read('invalid').should be_nil
|
||||
File.exists?(FILE_NAME).should be_false
|
||||
end
|
||||
|
||||
it 'should return nil when asked to fetch a key' do
|
||||
@cache.fetch('invalid').should be_nil
|
||||
File.exists?(FILE_NAME).should be_false
|
||||
end
|
||||
|
||||
it 'should create a cache file when asked to fetch a key with a default' do
|
||||
@cache.fetch('new_key') do
|
||||
'value'
|
||||
end.should == 'value'
|
||||
File.exists?(FILE_NAME).should be_true
|
||||
end
|
||||
|
||||
it 'should create a cache file when asked to write a key' do
|
||||
@cache.write('new_key', 'value')
|
||||
File.exists?(FILE_NAME).should be_true
|
||||
end
|
||||
|
||||
it 'should return nil when asked to delete a key' do
|
||||
@cache.delete('invalid').should be_nil
|
||||
File.exists?(FILE_NAME).should be_false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an existing cache' do
|
||||
before(:each) do
|
||||
File.delete(FILE_NAME) if File.exists?(FILE_NAME)
|
||||
@cache = Google::APIClient::Service::SimpleFileStore.new(FILE_NAME)
|
||||
@cache.write('existing_key', 'existing_value')
|
||||
end
|
||||
|
||||
it 'should return true when asked if an existing key exists' do
|
||||
@cache.exist?('existing_key').should be_true
|
||||
end
|
||||
|
||||
it 'should return false when asked if a nonexistent key exists' do
|
||||
@cache.exist?('invalid').should be_false
|
||||
end
|
||||
|
||||
it 'should return the value for an existing key when asked to read it' do
|
||||
@cache.read('existing_key').should == 'existing_value'
|
||||
end
|
||||
|
||||
it 'should return nil for a nonexistent key when asked to read it' do
|
||||
@cache.read('invalid').should be_nil
|
||||
end
|
||||
|
||||
it 'should return the value for an existing key when asked to read it' do
|
||||
@cache.read('existing_key').should == 'existing_value'
|
||||
end
|
||||
|
||||
it 'should return nil for a nonexistent key when asked to fetch it' do
|
||||
@cache.fetch('invalid').should be_nil
|
||||
end
|
||||
|
||||
it 'should return and save the default value for a nonexistent key when asked to fetch it with a default' do
|
||||
@cache.fetch('new_key') do
|
||||
'value'
|
||||
end.should == 'value'
|
||||
@cache.read('new_key').should == 'value'
|
||||
end
|
||||
|
||||
it 'should remove an existing value and return true when asked to delete it' do
|
||||
@cache.delete('existing_key').should be_true
|
||||
@cache.read('existing_key').should be_nil
|
||||
end
|
||||
|
||||
it 'should return false when asked to delete a nonexistent key' do
|
||||
@cache.delete('invalid').should be_false
|
||||
end
|
||||
|
||||
it 'should convert keys to strings when storing them' do
|
||||
@cache.write(:symbol_key, 'value')
|
||||
@cache.read('symbol_key').should == 'value'
|
||||
end
|
||||
|
||||
it 'should convert keys to strings when reading them' do
|
||||
@cache.read(:existing_key).should == 'existing_value'
|
||||
end
|
||||
|
||||
it 'should convert keys to strings when fetching them' do
|
||||
@cache.fetch(:existing_key).should == 'existing_value'
|
||||
end
|
||||
|
||||
it 'should convert keys to strings when deleting them' do
|
||||
@cache.delete(:existing_key).should be_true
|
||||
@cache.read('existing_key').should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
File.delete(FILE_NAME) if File.exists?(FILE_NAME)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# Copyright 2010 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require 'faraday'
|
||||
require 'signet/oauth_1/client'
|
||||
require 'google/api_client'
|
||||
require 'google/api_client/version'
|
||||
|
||||
shared_examples_for 'configurable user agent' do
|
||||
include ConnectionHelpers
|
||||
|
||||
it 'should allow the user agent to be modified' do
|
||||
client.user_agent = 'Custom User Agent/1.2.3'
|
||||
client.user_agent.should == 'Custom User Agent/1.2.3'
|
||||
end
|
||||
|
||||
it 'should allow the user agent to be set to nil' do
|
||||
client.user_agent = nil
|
||||
client.user_agent.should == nil
|
||||
end
|
||||
|
||||
it 'should not allow the user agent to be used with bogus values' do
|
||||
(lambda do
|
||||
client.user_agent = 42
|
||||
client.execute(:uri=>'https://www.google.com/')
|
||||
end).should raise_error(TypeError)
|
||||
end
|
||||
|
||||
it 'should transmit a User-Agent header when sending requests' do
|
||||
client.user_agent = 'Custom User Agent/1.2.3'
|
||||
|
||||
conn = stub_connection do |stub|
|
||||
stub.get('/') do |env|
|
||||
headers = env[:request_headers]
|
||||
headers.should have_key('User-Agent')
|
||||
headers['User-Agent'].should == client.user_agent
|
||||
[200, {}, ['']]
|
||||
end
|
||||
end
|
||||
client.execute(:uri=>'https://www.google.com/', :connection => conn)
|
||||
conn.verify
|
||||
end
|
||||
end
|
||||
|
||||
describe Google::APIClient do
|
||||
include ConnectionHelpers
|
||||
|
||||
let(:client) { Google::APIClient.new(:application_name => 'API Client Tests') }
|
||||
|
||||
it 'should make its version number available' do
|
||||
Google::APIClient::VERSION::STRING.should be_instance_of(String)
|
||||
end
|
||||
|
||||
it 'should default to OAuth 2' do
|
||||
Signet::OAuth2::Client.should === client.authorization
|
||||
end
|
||||
|
||||
describe 'configure for no authentication' do
|
||||
before do
|
||||
client.authorization = nil
|
||||
end
|
||||
it_should_behave_like 'configurable user agent'
|
||||
end
|
||||
|
||||
describe 'configured for OAuth 1' do
|
||||
before do
|
||||
client.authorization = :oauth_1
|
||||
client.authorization.token_credential_key = 'abc'
|
||||
client.authorization.token_credential_secret = '123'
|
||||
end
|
||||
|
||||
it 'should use the default OAuth1 client configuration' do
|
||||
client.authorization.temporary_credential_uri.to_s.should ==
|
||||
'https://www.google.com/accounts/OAuthGetRequestToken'
|
||||
client.authorization.authorization_uri.to_s.should include(
|
||||
'https://www.google.com/accounts/OAuthAuthorizeToken'
|
||||
)
|
||||
client.authorization.token_credential_uri.to_s.should ==
|
||||
'https://www.google.com/accounts/OAuthGetAccessToken'
|
||||
client.authorization.client_credential_key.should == 'anonymous'
|
||||
client.authorization.client_credential_secret.should == 'anonymous'
|
||||
end
|
||||
|
||||
it_should_behave_like 'configurable user agent'
|
||||
end
|
||||
|
||||
describe 'configured for OAuth 2' do
|
||||
before do
|
||||
client.authorization = :oauth_2
|
||||
client.authorization.access_token = '12345'
|
||||
end
|
||||
|
||||
# TODO
|
||||
it_should_behave_like 'configurable user agent'
|
||||
end
|
||||
|
||||
describe 'when executing requests' do
|
||||
before do
|
||||
@prediction = client.discovered_api('prediction', 'v1.2')
|
||||
client.authorization = :oauth_2
|
||||
@connection = stub_connection do |stub|
|
||||
stub.post('/prediction/v1.2/training?data=12345') do |env|
|
||||
env[:request_headers]['Authorization'].should == 'Bearer 12345'
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
@connection.verify
|
||||
end
|
||||
|
||||
it 'should use default authorization' do
|
||||
client.authorization.access_token = "12345"
|
||||
client.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:connection => @connection
|
||||
)
|
||||
end
|
||||
|
||||
it 'should use request scoped authorization when provided' do
|
||||
client.authorization.access_token = "abcdef"
|
||||
new_auth = Signet::OAuth2::Client.new(:access_token => '12345')
|
||||
client.execute(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'},
|
||||
:authorization => new_auth,
|
||||
:connection => @connection
|
||||
)
|
||||
end
|
||||
|
||||
it 'should accept options with batch/request style execute' do
|
||||
client.authorization.access_token = "abcdef"
|
||||
new_auth = Signet::OAuth2::Client.new(:access_token => '12345')
|
||||
request = client.generate_request(
|
||||
:api_method => @prediction.training.insert,
|
||||
:parameters => {'data' => '12345'}
|
||||
)
|
||||
client.execute(
|
||||
request,
|
||||
:authorization => new_auth,
|
||||
:connection => @connection
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
it 'should accept options in array style execute' do
|
||||
client.authorization.access_token = "abcdef"
|
||||
new_auth = Signet::OAuth2::Client.new(:access_token => '12345')
|
||||
client.execute(
|
||||
@prediction.training.insert, {'data' => '12345'}, '', {},
|
||||
{ :authorization => new_auth, :connection => @connection }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when retiries enabled' do
|
||||
before do
|
||||
client.retries = 2
|
||||
end
|
||||
|
||||
after do
|
||||
@connection.verify
|
||||
end
|
||||
|
||||
it 'should follow redirects' do
|
||||
client.authorization = nil
|
||||
@connection = stub_connection do |stub|
|
||||
stub.get('/foo') do |env|
|
||||
[302, {'location' => 'https://www.google.com/bar'}, '{}']
|
||||
end
|
||||
stub.get('/bar') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
client.execute(
|
||||
:uri => 'https://www.gogole.com/foo',
|
||||
:connection => @connection
|
||||
)
|
||||
end
|
||||
|
||||
it 'should refresh tokens on 401 tokens' do
|
||||
client.authorization.access_token = '12345'
|
||||
expect(client.authorization).to receive(:fetch_access_token!)
|
||||
|
||||
@connection = stub_connection do |stub|
|
||||
stub.get('/foo') do |env|
|
||||
[401, {}, '{}']
|
||||
end
|
||||
stub.get('/foo') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
client.execute(
|
||||
:uri => 'https://www.gogole.com/foo',
|
||||
:connection => @connection
|
||||
)
|
||||
end
|
||||
|
||||
it 'should retry on 500 errors' do
|
||||
client.authorization = nil
|
||||
|
||||
@connection = stub_connection do |stub|
|
||||
stub.get('/foo') do |env|
|
||||
[500, {}, '{}']
|
||||
end
|
||||
stub.get('/foo') do |env|
|
||||
[200, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
client.execute(
|
||||
:uri => 'https://www.gogole.com/foo',
|
||||
:connection => @connection
|
||||
).status.should == 200
|
||||
|
||||
end
|
||||
|
||||
it 'should fail after max retries' do
|
||||
client.authorization = nil
|
||||
count = 0
|
||||
@connection = stub_connection do |stub|
|
||||
stub.get('/foo') do |env|
|
||||
count += 1
|
||||
[500, {}, '{}']
|
||||
end
|
||||
end
|
||||
|
||||
client.execute(
|
||||
:uri => 'https://www.gogole.com/foo',
|
||||
:connection => @connection
|
||||
).status.should == 500
|
||||
count.should == 3
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
||||
$LOAD_PATH.uniq!
|
||||
|
||||
require 'rspec'
|
||||
require 'faraday'
|
||||
|
||||
Faraday::Adapter.load_middleware(:test)
|
||||
|
||||
module Faraday
|
||||
class Connection
|
||||
def verify
|
||||
if app.kind_of?(Faraday::Adapter::Test)
|
||||
app.stubs.verify_stubbed_calls
|
||||
else
|
||||
raise TypeError, "Expected test adapter"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionHelpers
|
||||
def stub_connection(&block)
|
||||
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
|
||||
block.call(stub)
|
||||
end
|
||||
connection = Faraday.new do |builder|
|
||||
builder.options.params_encoder = Faraday::FlatParamsEncoder
|
||||
builder.adapter(:test, stubs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module JSONMatchers
|
||||
class EqualsJson
|
||||
def initialize(expected)
|
||||
@expected = JSON.parse(expected)
|
||||
end
|
||||
def matches?(target)
|
||||
@target = JSON.parse(target)
|
||||
@target.eql?(@expected)
|
||||
end
|
||||
def failure_message
|
||||
"expected #{@target.inspect} to be #{@expected}"
|
||||
end
|
||||
def negative_failure_message
|
||||
"expected #{@target.inspect} not to be #{@expected}"
|
||||
end
|
||||
end
|
||||
|
||||
def be_json(expected)
|
||||
EqualsJson.new(expected)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
end
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
require 'rubygems/package_task'
|
||||
require 'rake/clean'
|
||||
|
||||
CLOBBER.include('pkg')
|
||||
|
||||
namespace :gem do
|
||||
GEM_SPEC = Gem::Specification.new do |s|
|
||||
unless s.respond_to?(:add_development_dependency)
|
||||
puts 'The gem spec requires a newer version of RubyGems.'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
s.name = PKG_NAME
|
||||
s.version = PKG_VERSION
|
||||
s.author = PKG_AUTHOR
|
||||
s.email = PKG_AUTHOR_EMAIL
|
||||
s.summary = PKG_SUMMARY
|
||||
s.description = PKG_DESCRIPTION
|
||||
s.license = 'Apache 2.0'
|
||||
s.files = PKG_FILES.to_a
|
||||
|
||||
s.extra_rdoc_files = %w( README.md )
|
||||
s.rdoc_options.concat ['--main', 'README.md']
|
||||
|
||||
# Dependencies used in the main library
|
||||
s.add_runtime_dependency('signet', '>= 0.5.0')
|
||||
s.add_runtime_dependency('addressable', '>= 2.3.2')
|
||||
s.add_runtime_dependency('uuidtools', '>= 2.1.0')
|
||||
s.add_runtime_dependency('autoparse', '>= 0.3.3')
|
||||
s.add_runtime_dependency('faraday', '>= 0.9.0')
|
||||
s.add_runtime_dependency('multi_json', '>= 1.0.0')
|
||||
s.add_runtime_dependency('extlib', '>= 0.9.15')
|
||||
s.add_runtime_dependency('jwt', '>= 0.1.5')
|
||||
s.add_runtime_dependency('retriable', '>= 1.4')
|
||||
# Dependencies used in the CLI
|
||||
s.add_runtime_dependency('launchy', '>= 2.1.1')
|
||||
|
||||
# Dependencies used in the examples
|
||||
s.add_development_dependency('rake', '>= 0.9.0')
|
||||
s.add_development_dependency('rspec', '>= 2.11.0')
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.homepage = PKG_HOMEPAGE
|
||||
end
|
||||
|
||||
Gem::PackageTask.new(GEM_SPEC) do |p|
|
||||
p.gem_spec = GEM_SPEC
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
desc 'Show information about the gem'
|
||||
task :debug do
|
||||
puts GEM_SPEC.to_ruby
|
||||
end
|
||||
|
||||
desc "Generates .gemspec file"
|
||||
task :gemspec do
|
||||
spec_string = GEM_SPEC.to_ruby
|
||||
|
||||
begin
|
||||
Thread.new { eval("$SAFE = 3\n#{spec_string}", binding) }.join
|
||||
rescue
|
||||
abort "unsafe gemspec: #{$!}"
|
||||
else
|
||||
File.open("#{GEM_SPEC.name}.gemspec", 'w') do |file|
|
||||
file.write spec_string
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Install the gem'
|
||||
task :install => ['clobber', 'gem:package'] do
|
||||
sh "#{SUDO} gem install --local pkg/#{GEM_SPEC.full_name}"
|
||||
end
|
||||
|
||||
desc 'Uninstall the gem'
|
||||
task :uninstall do
|
||||
installed_list = Gem.source_index.find_name(PKG_NAME)
|
||||
if installed_list &&
|
||||
(installed_list.collect { |s| s.version.to_s}.include?(PKG_VERSION))
|
||||
sh(
|
||||
"#{SUDO} gem uninstall --version '#{PKG_VERSION}' " +
|
||||
"--ignore-dependencies --executables #{PKG_NAME}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Reinstall the gem'
|
||||
task :reinstall => [:uninstall, :install]
|
||||
end
|
||||
|
||||
desc 'Alias to gem:package'
|
||||
task 'gem' => 'gem:package'
|
||||
|
||||
task 'gem:release' => 'gem:gemspec'
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
namespace :git do
|
||||
namespace :tag do
|
||||
desc 'List tags from the Git repository'
|
||||
task :list do
|
||||
tags = `git tag -l`
|
||||
tags.gsub!("\r", '')
|
||||
tags = tags.split("\n").sort {|a, b| b <=> a }
|
||||
puts tags.join("\n")
|
||||
end
|
||||
|
||||
desc 'Create a new tag in the Git repository'
|
||||
task :create do
|
||||
changelog = File.open('CHANGELOG.md', 'r') { |file| file.read }
|
||||
puts '-' * 80
|
||||
puts changelog
|
||||
puts '-' * 80
|
||||
puts
|
||||
|
||||
v = ENV['VERSION'] or abort 'Must supply VERSION=x.y.z'
|
||||
abort "Versions don't match #{v} vs #{PKG_VERSION}" if v != PKG_VERSION
|
||||
|
||||
git_status = `git status`
|
||||
if git_status !~ /nothing to commit \(working directory clean\)/
|
||||
abort "Working directory isn't clean."
|
||||
end
|
||||
|
||||
tag = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
msg = "Release #{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
existing_tags = `git tag -l #{PKG_NAME}-*`.split('\n')
|
||||
if existing_tags.include?(tag)
|
||||
warn('Tag already exists, deleting...')
|
||||
unless system "git tag -d #{tag}"
|
||||
abort 'Tag deletion failed.'
|
||||
end
|
||||
end
|
||||
puts "Creating git tag '#{tag}'..."
|
||||
unless system "git tag -a -m \"#{msg}\" #{tag}"
|
||||
abort 'Tag creation failed.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
task 'gem:release' => 'git:tag:create'
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
namespace :metrics do
|
||||
task :lines do
|
||||
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
|
||||
for file_name in FileList['lib/**/*.rb']
|
||||
f = File.open(file_name)
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
puts "L: #{sprintf('%4d', lines)}, " +
|
||||
"LOC #{sprintf('%4d', codelines)} | #{file_name}"
|
||||
total_lines += lines
|
||||
total_codelines += codelines
|
||||
|
||||
lines, codelines = 0, 0
|
||||
end
|
||||
|
||||
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
require 'rake/clean'
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
CLOBBER.include('coverage', 'specdoc')
|
||||
|
||||
namespace :spec do
|
||||
RSpec::Core::RakeTask.new(:all) do |t|
|
||||
t.pattern = FileList['spec/**/*_spec.rb']
|
||||
t.rspec_opts = ['--color', '--format', 'documentation']
|
||||
t.rcov = false
|
||||
end
|
||||
|
||||
desc 'Generate HTML Specdocs for all specs.'
|
||||
RSpec::Core::RakeTask.new(:specdoc) do |t|
|
||||
specdoc_path = File.expand_path('../../specdoc', __FILE__)
|
||||
|
||||
t.rspec_opts = %W( --format html --out #{File.join(specdoc_path, 'index.html')} )
|
||||
t.fail_on_error = false
|
||||
end
|
||||
|
||||
RSpec::Core::RakeTask.new(:rcov) do |t|
|
||||
if RCOV_ENABLED
|
||||
if `which rcov`.strip == ""
|
||||
STDERR.puts(
|
||||
"Please install rcov and ensure that its binary is in the PATH:"
|
||||
)
|
||||
STDERR.puts("sudo gem install rcov")
|
||||
exit(1)
|
||||
end
|
||||
t.rcov = true
|
||||
else
|
||||
t.rcov = false
|
||||
end
|
||||
t.rcov_opts = %w(
|
||||
--exclude gems/
|
||||
--exclude spec/
|
||||
--exclude lib/google/api_client/environment.rb
|
||||
--exclude lib/compat
|
||||
)
|
||||
end
|
||||
|
||||
namespace :rcov do
|
||||
desc 'Browse the code coverage report.'
|
||||
task :browse => 'spec:rcov' do
|
||||
require 'launchy'
|
||||
Launchy::Browser.run('coverage/index.html')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if RCOV_ENABLED
|
||||
desc 'Alias to spec:rcov'
|
||||
task 'spec' => 'spec:rcov'
|
||||
else
|
||||
desc 'Alias to spec:all'
|
||||
task 'spec' => 'spec:all'
|
||||
end
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
require 'rake'
|
||||
require 'rake/clean'
|
||||
|
||||
CLOBBER.include('wiki')
|
||||
|
||||
CACHE_PREFIX =
|
||||
"http://www.gmodules.com/gadgets/proxy/container=default&debug=0&nocache=0/"
|
||||
|
||||
namespace :wiki do
|
||||
desc 'Autogenerate wiki pages'
|
||||
task :supported_apis do
|
||||
output = <<-WIKI
|
||||
#summary The list of supported APIs
|
||||
|
||||
The Google API Client for Ruby is a small flexible client library for accessing
|
||||
the following Google APIs.
|
||||
|
||||
WIKI
|
||||
preferred_apis = {}
|
||||
require 'google/api_client'
|
||||
client = Google::APIClient.new
|
||||
for api in client.discovered_apis
|
||||
if !preferred_apis.has_key?(api.name)
|
||||
preferred_apis[api.name] = api
|
||||
elsif api.preferred
|
||||
preferred_apis[api.name] = api
|
||||
end
|
||||
end
|
||||
for api_name, api in preferred_apis
|
||||
if api.documentation.to_s != "" && api.title != ""
|
||||
output += (
|
||||
"||#{CACHE_PREFIX}#{api['icons']['x16']}||" +
|
||||
"[#{api.documentation} #{api.title}]||" +
|
||||
"#{api.description}||\n"
|
||||
)
|
||||
end
|
||||
end
|
||||
output.gsub!(/-32\./, "-16.")
|
||||
wiki_path = File.expand_path(
|
||||
File.join(File.dirname(__FILE__), '../wiki/'))
|
||||
Dir.mkdir(wiki_path) if !File.exist?(wiki_path)
|
||||
File.open(File.join(wiki_path, 'SupportedAPIs.wiki'), 'w') do |file|
|
||||
file.write(output)
|
||||
end
|
||||
end
|
||||
|
||||
task 'generate' => ['wiki:supported_apis']
|
||||
end
|
||||
|
||||
begin
|
||||
$LOAD_PATH.unshift(
|
||||
File.expand_path(File.join(File.dirname(__FILE__), '../yard/lib'))
|
||||
)
|
||||
$LOAD_PATH.unshift(File.expand_path('.'))
|
||||
$LOAD_PATH.uniq!
|
||||
|
||||
require 'yard'
|
||||
require 'yard/rake/wikidoc_task'
|
||||
|
||||
namespace :wiki do
|
||||
desc 'Generate Wiki Documentation with YARD'
|
||||
YARD::Rake::WikidocTask.new do |yardoc|
|
||||
yardoc.name = 'reference'
|
||||
yardoc.options = [
|
||||
'--verbose',
|
||||
'--markup', 'markdown',
|
||||
'-e', 'yard/lib/yard-google-code.rb',
|
||||
'-p', 'yard/templates',
|
||||
'-f', 'wiki',
|
||||
'-o', 'wiki'
|
||||
]
|
||||
yardoc.files = [
|
||||
'lib/**/*.rb', 'ext/**/*.c', '-', 'README.md', 'CHANGELOG.md'
|
||||
]
|
||||
end
|
||||
|
||||
task 'generate' => ['wiki:reference', 'wiki:supported_apis']
|
||||
end
|
||||
rescue LoadError
|
||||
# If yard isn't available, it's not the end of the world
|
||||
warn('YARD unavailable. Cannot fully generate wiki.')
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
require 'rake'
|
||||
require 'rake/clean'
|
||||
|
||||
CLOBBER.include('doc', '.yardoc')
|
||||
CLOBBER.uniq!
|
||||
|
||||
begin
|
||||
require 'yard'
|
||||
require 'yard/rake/yardoc_task'
|
||||
|
||||
namespace :doc do
|
||||
desc 'Generate Yardoc documentation'
|
||||
YARD::Rake::YardocTask.new do |yardoc|
|
||||
yardoc.name = 'yard'
|
||||
yardoc.options = ['--verbose', '--markup', 'markdown']
|
||||
yardoc.files = [
|
||||
'lib/**/*.rb', 'ext/**/*.c', '-',
|
||||
'README.md', 'CONTRIB.md', 'CHANGELOG.md', 'LICENSE'
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Alias to doc:yard'
|
||||
task 'doc' => 'doc:yard'
|
||||
rescue LoadError
|
||||
# If yard isn't available, it's not the end of the world
|
||||
desc 'Alias to doc:rdoc'
|
||||
task 'doc' => 'doc:rdoc'
|
||||
end
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
[](http://rubygems.org/gems/react-rails)
|
||||
[](https://travis-ci.org/reactjs/react-rails)
|
||||
[](https://gemnasium.com/reactjs/react-rails)
|
||||
[](https://codeclimate.com/github/reactjs/react-rails)
|
||||
[](https://codeclimate.com/github/reactjs/react-rails/coverage)
|
||||
|
||||
* * *
|
||||
|
||||
# react-rails
|
||||
|
||||
|
||||
`react-rails` makes it easy to use [React](http://facebook.github.io/react/) and [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html)
|
||||
in your Ruby on Rails (3.2+) application. `react-rails` can:
|
||||
|
||||
- Provide [various `react` builds](#reactjs-builds) to your asset bundle
|
||||
- Transform [`.jsx` in the asset pipeline](#jsx)
|
||||
- [Render components into views and mount them](#rendering--mounting) via view helper & `react_ujs`
|
||||
- [Render components server-side](#server-rendering) with `prerender: true`
|
||||
- [Generate components](#component-generator) with a Rails generator
|
||||
- [Be extended](#extending-react-rails) with custom renderers, transformers and view helpers
|
||||
|
||||
## Installation
|
||||
|
||||
Add `react-rails` to your gemfile:
|
||||
|
||||
```ruby
|
||||
gem 'react-rails', '~> 1.3.0'
|
||||
```
|
||||
|
||||
Next, run the installation script:
|
||||
|
||||
```bash
|
||||
rails g react:install
|
||||
```
|
||||
|
||||
This will:
|
||||
- create a `components.js` manifest file and a `app/assets/javascripts/components/` directory,
|
||||
where you will put your components
|
||||
- place the following in your `application.js`:
|
||||
|
||||
```js
|
||||
//= require react
|
||||
//= require react_ujs
|
||||
//= require components
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### React.js builds
|
||||
|
||||
You can pick which React.js build (development, production, with or without [add-ons]((http://facebook.github.io/react/docs/addons.html)))
|
||||
to serve in each environment by adding a config. Here are the defaults:
|
||||
|
||||
```ruby
|
||||
# config/environments/development.rb
|
||||
MyApp::Application.configure do
|
||||
config.react.variant = :development
|
||||
end
|
||||
|
||||
# config/environments/production.rb
|
||||
MyApp::Application.configure do
|
||||
config.react.variant = :production
|
||||
end
|
||||
```
|
||||
|
||||
To include add-ons, use this config:
|
||||
|
||||
```ruby
|
||||
MyApp::Application.configure do
|
||||
config.react.addons = true # defaults to false
|
||||
end
|
||||
```
|
||||
|
||||
After restarting your Rails server, `//= require react` will provide the build of React.js which
|
||||
was specified by the configurations.
|
||||
|
||||
`react-rails` offers a few other options for versions & builds of React.js.
|
||||
See [VERSIONS.md](https://github.com/reactjs/react-rails/blob/master/VERSIONS.md) for more info about
|
||||
using the `react-source` gem or dropping in your own copies of React.js.
|
||||
|
||||
### JSX
|
||||
|
||||
After installing `react-rails`, restart your server. Now, `.js.jsx` files will be transformed in the asset pipeline.
|
||||
|
||||
`react-rails` currently ships with two transformers, to convert jsx code -
|
||||
|
||||
* `BabelTransformer` using [Babel](http://babeljs.io), which is the default transformer.
|
||||
* `JSXTransformer` using `JSXTransformer.js`
|
||||
|
||||
You can use the deprecated `JSXTransformer` by setting it in an application config:
|
||||
|
||||
```ruby
|
||||
config.react.jsx_transformer_class = React::JSX::JSXTransformer
|
||||
```
|
||||
|
||||
#### BabelTransformer options
|
||||
|
||||
You can use babel's [transformers](http://babeljs.io/docs/advanced/transformers/) and [custom plugins](http://babeljs.io/docs/advanced/plugins/),
|
||||
and pass [options](http://babeljs.io/docs/usage/options/) to the babel transpiler adding following configurations:
|
||||
|
||||
```ruby
|
||||
config.react.jsx_transform_options = {
|
||||
blacklist: ['spec.functionName', 'validation.react'], # default options
|
||||
optional: ["transformerName"], # pass extra babel options
|
||||
whitelist: ["useStrict"] # even more options
|
||||
}
|
||||
```
|
||||
Under the hood, `react-rails` uses [ruby-babel-transpiler](https://github.com/babel/ruby-babel-transpiler), for transformation.
|
||||
|
||||
#### JSXTransformer options
|
||||
|
||||
You can use JSX `--harmony` or `--strip-types` options by adding a configuration:
|
||||
|
||||
```ruby
|
||||
config.react.jsx_transform_options = {
|
||||
harmony: true,
|
||||
strip_types: true, # for removing Flow type annotations
|
||||
asset_path: "path/to/JSXTransformer.js", # if your JSXTransformer is somewhere else
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering & mounting
|
||||
|
||||
`react-rails` includes a view helper (`react_component`) and an unobtrusive JavaScript driver (`react_ujs`)
|
||||
which work together to put React components on the page. You should require the UJS driver
|
||||
in your manifest after `react` (and after `turbolinks` if you use [Turbolinks](https://github.com/rails/turbolinks)).
|
||||
|
||||
The __view helper__ puts a `div` on the page with the requested component class & props. For example:
|
||||
|
||||
```erb
|
||||
<%= react_component('HelloMessage', name: 'John') %>
|
||||
<!-- becomes: -->
|
||||
<div data-react-class="HelloMessage" data-react-props="{"name":"John"}"></div>
|
||||
```
|
||||
|
||||
On page load, the __`react_ujs` driver__ will scan the page and mount components using `data-react-class`
|
||||
and `data-react-props`.
|
||||
|
||||
If Turbolinks is present components are mounted on the `page:change` event, and unmounted on `page:before-unload`.
|
||||
__Turbolinks >= 2.4.0__ is recommended because it exposes better events.
|
||||
|
||||
The view helper's signature is:
|
||||
|
||||
```ruby
|
||||
react_component(component_class_name, props={}, html_options={})
|
||||
```
|
||||
|
||||
- `component_class_name` is a string which names a globally-accessible component class. It may have dots (eg, `"MyApp.Header.MenuItem"`).
|
||||
- `props` is either an object that responds to `#to_json` or an already-stringified JSON object (eg, made with Jbuilder, see note below).
|
||||
- `html_options` may include:
|
||||
- `tag:` to use an element other than a `div` to embed `data-react-class` and `-props`.
|
||||
- `prerender: true` to render the component on the server.
|
||||
- `**other` Any other arguments (eg `class:`, `id:`) are passed through to [`content_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag).
|
||||
|
||||
|
||||
### Server rendering
|
||||
|
||||
To render components on the server, pass `prerender: true` to `react_component`:
|
||||
|
||||
```erb
|
||||
<%= react_component('HelloMessage', {name: 'John'}, {prerender: true}) %>
|
||||
<!-- becomes: -->
|
||||
<div data-react-class="HelloMessage" data-react-props="{"name":"John"}">
|
||||
<h1>Hello, John!</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
_(It will be also be mounted by the UJS on page load.)_
|
||||
|
||||
There are some requirements for this to work:
|
||||
|
||||
- `react-rails` must load your code. By convention it uses `components.js`, which was created
|
||||
by the install task. This file must include your components _and_ their dependencies (eg, Underscore.js).
|
||||
- Your components must be accessible in the global scope.
|
||||
If you are using `.js.jsx.coffee` files then the wrapper function needs to be taken into account:
|
||||
|
||||
```coffee
|
||||
# @ is `window`:
|
||||
@Component = React.createClass
|
||||
render: ->
|
||||
`<ExampleComponent videos={this.props.videos} />`
|
||||
```
|
||||
- Your code can't reference `document`. Prerender processes don't have access to `document`,
|
||||
so jQuery and some other libs won't work in this environment :(
|
||||
|
||||
You can configure your pool of JS virtual machines and specify where it should load code:
|
||||
|
||||
```ruby
|
||||
# config/environments/application.rb
|
||||
# These are the defaults if you dont specify any yourself
|
||||
MyApp::Application.configure do
|
||||
# Settings for the pool of renderers:
|
||||
config.react.server_renderer_pool_size ||= 1 # ExecJS doesn't allow more than one on MRI
|
||||
config.react.server_renderer_timeout ||= 20 # seconds
|
||||
config.react.server_renderer = React::ServerRendering::SprocketsRenderer
|
||||
config.react.server_renderer_options = {
|
||||
files: ["react.js", "components.js"], # files to load for prerendering
|
||||
replay_console: true, # if true, console.* will be replayed client-side
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
- On MRI, use `therubyracer` for the best performance (see [discussion](https://github.com/reactjs/react-rails/pull/290))
|
||||
- On MRI, you'll get a deadlock with `pool_size` > 1
|
||||
- If you're using JRuby, you can increase `pool_size` to have real multi-threaded rendering.
|
||||
|
||||
### Rendering components instead of views
|
||||
|
||||
Components can also be prerendered directly from a controller action with the custom `component` renderer. For example:
|
||||
|
||||
```ruby
|
||||
class TodoController < ApplicationController
|
||||
def index
|
||||
@todos = Todo.all
|
||||
render component: 'TodoList', props: { todos: @todos }, tag: 'span'
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This custom renderer behaves the same as a normal view renderer and accepts the usual arguments - `content_type`, `layout`, `location` and `status`.
|
||||
By default, your current layout will be used and the component, rather than a view, will be rendered in place of `yield`.
|
||||
|
||||
### Component generator
|
||||
|
||||
`react-rails` ships with a Rails generator to help you get started with a simple component scaffold.
|
||||
You can run it using `rails generate react:component ComponentName (--es6)`.
|
||||
The generator takes an optional list of arguments for default propTypes,
|
||||
which follow the conventions set in the [Reusable Components](http://facebook.github.io/react/docs/reusable-components.html)
|
||||
section of the React documentation.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
rails generate react:component Post title:string body:string published:bool published_by:instanceOf{Person}
|
||||
```
|
||||
|
||||
would generate the following in `app/assets/javascripts/components/post.js.jsx`:
|
||||
|
||||
```jsx
|
||||
var Post = React.createClass({
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
body: React.PropTypes.string,
|
||||
published: React.PropTypes.bool,
|
||||
publishedBy: React.PropTypes.instanceOf(Person)
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div>Title: {this.props.title}</div>
|
||||
<div>Body: {this.props.body}</div>
|
||||
<div>Published: {this.props.published}</div>
|
||||
<div>Published By: {this.props.publishedBy}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
**--es6** : Generate the same component but using cutting edge es6 class
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
rails generate react:component Label label:string --es6
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
The generator can use the following arguments to create basic propTypes:
|
||||
|
||||
* any
|
||||
* array
|
||||
* bool
|
||||
* element
|
||||
* func
|
||||
* number
|
||||
* object
|
||||
* node
|
||||
* shape
|
||||
* string
|
||||
|
||||
The following additional arguments have special behavior:
|
||||
|
||||
* `instanceOf` takes an optional class name in the form of {className}.
|
||||
* `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`.
|
||||
* `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`.
|
||||
|
||||
Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes
|
||||
to prevent your terminal from expanding them into an argument list.
|
||||
|
||||
### Jbuilder & react-rails
|
||||
|
||||
If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash,
|
||||
not an array. This is not the Rails default -- you should add the root node yourself. For example:
|
||||
|
||||
```ruby
|
||||
# BAD: returns a stringified array
|
||||
json.array!(@messages) do |message|
|
||||
json.extract! message, :id, :name
|
||||
json.url message_url(message, format: :json)
|
||||
end
|
||||
|
||||
# GOOD: returns a stringified hash
|
||||
json.messages(@messages) do |message|
|
||||
json.extract! message, :id, :name
|
||||
json.url message_url(message, format: :json)
|
||||
end
|
||||
```
|
||||
|
||||
## CoffeeScript
|
||||
|
||||
It is possible to use JSX with CoffeeScript. To use CoffeeScript, create files with an extension `.js.jsx.coffee`.
|
||||
We also need to embed JSX code inside backticks so that CoffeeScript ignores the syntax it doesn't understand.
|
||||
Here's an example:
|
||||
|
||||
```coffee
|
||||
Component = React.createClass
|
||||
render: ->
|
||||
`<ExampleComponent videos={this.props.videos} />`
|
||||
```
|
||||
|
||||
## Extending `react-rails`
|
||||
|
||||
You can extend some of the core functionality of `react-rails` by injecting new implementations during configuration.
|
||||
|
||||
### Custom Server Renderer
|
||||
|
||||
`react-rails` depends on a renderer class for rendering components on the server. You can provide a custom renderer class to `config.react.server_renderer`. The class must implement:
|
||||
|
||||
- `#initialize(options={})`, which accepts the hash from `config.react.server_renderer_options`
|
||||
- `#render(component_name, props, prerender_options)` to return a string of HTML
|
||||
|
||||
`react-rails` provides two renderer classes: `React::ServerRendering::ExecJSRenderer` and `React::ServerRendering::SprocketsRenderer`.
|
||||
|
||||
`ExecJSRenderer` offers two other points for extension:
|
||||
|
||||
- `#before_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _before_ calling `React.render`
|
||||
- `#after_render(component_name, props, prerender_options)` to return a string of JavaScript to execute _after_ calling `React.render`
|
||||
|
||||
Any subclass of `ExecJSRenderer` may use those hooks (for example, `SprocketsRenderer` uses them to handle `console.*` on the server).
|
||||
|
||||
### Custom View Helper
|
||||
|
||||
`react-rails` uses a "helper implementation" class to generate the output of the `react_component` helper. The helper is initialized once per request and used for each `react_component` call during that request. You can provide a custom helper class to `config.react.view_helper_implementation`. The class must implement:
|
||||
|
||||
- `#react_component(name, props = {}, options = {}, &block)` to return a string to inject into the Rails view
|
||||
- `#setup(controller_instance)`, called when the helper is initialized at the start of the request
|
||||
- `#teardown(controller_instance)`, called at the end of the request
|
||||
|
||||
`react-rails` provides one implementation, `React::Rails::ComponentMount`.
|
||||
|
||||
### Custom JSX Transformer
|
||||
|
||||
`react-rails` uses a transformer class to transform JSX for the browser. The transformer is initialized once, at boot. You can provide a custom transformer to `config.react.jsx_transformer_class`. The transformer must implement:
|
||||
|
||||
- `#initialize(options)`, where options is the value passed to `config.react.jsx_transform_options`
|
||||
- `#transform(code_string)` to return a string of transformed code
|
||||
|
||||
`react-rails` provides two transformers, `React::JSX::JSXTransformer` and `React::JSX::BabelTransformer`.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,119 @@
|
|||
/*globals React, Turbolinks*/
|
||||
|
||||
// Unobtrusive scripting adapter for React
|
||||
;(function(document, window) {
|
||||
// jQuery is optional. Use it to support legacy browsers.
|
||||
var $ = (typeof window.jQuery !== 'undefined') && window.jQuery;
|
||||
|
||||
// create the namespace
|
||||
window.ReactRailsUJS = {
|
||||
CLASS_NAME_ATTR: 'data-react-class',
|
||||
PROPS_ATTR: 'data-react-props',
|
||||
RAILS_ENV_DEVELOPMENT: <%= Rails.env == "development" %>,
|
||||
// helper method for the mount and unmount methods to find the
|
||||
// `data-react-class` DOM elements
|
||||
findDOMNodes: function(searchSelector) {
|
||||
// we will use fully qualified paths as we do not bind the callbacks
|
||||
var selector, parent;
|
||||
|
||||
switch (typeof searchSelector) {
|
||||
case 'undefined':
|
||||
selector = '[' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']';
|
||||
parent = document;
|
||||
break;
|
||||
case 'object':
|
||||
selector = '[' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']';
|
||||
parent = searchSelector;
|
||||
break;
|
||||
case 'string':
|
||||
selector = searchSelector + ' [' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']';
|
||||
parent = document;
|
||||
break
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if ($) {
|
||||
return $(selector, parent);
|
||||
} else {
|
||||
return parent.querySelectorAll(selector);
|
||||
}
|
||||
},
|
||||
|
||||
mountComponents: function(searchSelector) {
|
||||
var nodes = window.ReactRailsUJS.findDOMNodes(searchSelector);
|
||||
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
var className = node.getAttribute(window.ReactRailsUJS.CLASS_NAME_ATTR);
|
||||
|
||||
// Assume className is simple and can be found at top-level (window).
|
||||
// Fallback to eval to handle cases like 'My.React.ComponentName'.
|
||||
var constructor = window[className] || eval.call(window, className);
|
||||
var propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR);
|
||||
var props = propsJson && JSON.parse(propsJson);
|
||||
|
||||
// Prefer ReactDOM if defined (introduced in 0.14)
|
||||
var renderer = (typeof ReactDOM == "object") ? ReactDOM : React;
|
||||
|
||||
renderer.render(React.createElement(constructor, props), node);
|
||||
}
|
||||
},
|
||||
|
||||
unmountComponents: function(searchSelector) {
|
||||
var nodes = window.ReactRailsUJS.findDOMNodes(searchSelector);
|
||||
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
var node = nodes[i];
|
||||
|
||||
// Prefer ReactDOM if defined (introduced in 0.14)
|
||||
var renderer = (typeof ReactDOM == "object") ? ReactDOM : React;
|
||||
renderer.unmountComponentAtNode(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// functions not exposed publicly
|
||||
function handleTurbolinksEvents () {
|
||||
var handleEvent;
|
||||
var unmountEvent;
|
||||
|
||||
if ($) {
|
||||
handleEvent = function(eventName, callback) {
|
||||
$(document).on(eventName, callback);
|
||||
};
|
||||
|
||||
} else {
|
||||
handleEvent = function(eventName, callback) {
|
||||
document.addEventListener(eventName, callback);
|
||||
};
|
||||
}
|
||||
|
||||
if (Turbolinks.EVENTS) {
|
||||
unmountEvent = Turbolinks.EVENTS.BEFORE_UNLOAD;
|
||||
} else {
|
||||
unmountEvent = 'page:receive';
|
||||
Turbolinks.pagesCached(0);
|
||||
|
||||
if (window.ReactRailsUJS.RAILS_ENV_DEVELOPMENT) {
|
||||
console.warn('The Turbolinks cache has been disabled (Turbolinks >= 2.4.0 is recommended). See https://github.com/reactjs/react-rails/issues/87 for more information.');
|
||||
}
|
||||
}
|
||||
handleEvent('page:change', function() {window.ReactRailsUJS.mountComponents()});
|
||||
handleEvent(unmountEvent, function() {window.ReactRailsUJS.unmountComponents()});
|
||||
}
|
||||
|
||||
function handleNativeEvents() {
|
||||
if ($) {
|
||||
$(function() {window.ReactRailsUJS.mountComponents()});
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function() {window.ReactRailsUJS.mountComponents()});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
|
||||
handleTurbolinksEvents();
|
||||
} else {
|
||||
handleNativeEvents();
|
||||
}
|
||||
})(document, window);
|
||||
21642
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/development-with-addons/react.js
vendored
Normal file
21642
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/development-with-addons/react.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
19602
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/development/react.js
vendored
Normal file
19602
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/development/react.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
18
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/production-with-addons/react.js
vendored
Normal file
18
gems/rails6.1/react-rails-1.3.3/lib/assets/react-source/production-with-addons/react.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,132 @@
|
|||
module React
|
||||
module Generators
|
||||
class ComponentGenerator < ::Rails::Generators::NamedBase
|
||||
source_root File.expand_path '../../templates', __FILE__
|
||||
desc <<-DESC.strip_heredoc
|
||||
Description:
|
||||
Scaffold a react component into app/assets/javascripts/components.
|
||||
The generated component will include a basic render function and a PropTypes
|
||||
hash to help with development.
|
||||
|
||||
Available field types:
|
||||
|
||||
Basic prop types do not take any additional arguments. If you do not specify
|
||||
a prop type, the generic node will be used. The basic types available are:
|
||||
|
||||
any
|
||||
array
|
||||
bool
|
||||
element
|
||||
func
|
||||
number
|
||||
object
|
||||
node
|
||||
shape
|
||||
string
|
||||
|
||||
Special PropTypes take additional arguments in {}, and must be enclosed in
|
||||
single quotes to keep bash from expanding the arguments in {}.
|
||||
|
||||
instanceOf
|
||||
takes an optional class name in the form of {className}
|
||||
|
||||
oneOf
|
||||
behaves like an enum, and takes an optional list of strings that will
|
||||
be allowed in the form of 'name:oneOf{one,two,three}'.
|
||||
|
||||
oneOfType.
|
||||
oneOfType takes an optional list of react and custom types in the form of
|
||||
'model:oneOfType{string,number,OtherType}'
|
||||
|
||||
Examples:
|
||||
rails g react:component person name
|
||||
rails g react:component restaurant name:string rating:number owner:instanceOf{Person}
|
||||
rails g react:component food 'kind:oneOf{meat,cheese,vegetable}'
|
||||
rails g react:component events 'location:oneOfType{string,Restaurant}'
|
||||
DESC
|
||||
|
||||
argument :attributes,
|
||||
:type => :array,
|
||||
:default => [],
|
||||
:banner => "field[:type] field[:type] ..."
|
||||
|
||||
class_option :es6,
|
||||
type: :boolean,
|
||||
default: false,
|
||||
desc: 'Output es6 class based component'
|
||||
|
||||
REACT_PROP_TYPES = {
|
||||
"node" => 'React.PropTypes.node',
|
||||
"bool" => 'React.PropTypes.bool',
|
||||
"boolean" => 'React.PropTypes.bool',
|
||||
"string" => 'React.PropTypes.string',
|
||||
"number" => 'React.PropTypes.number',
|
||||
"object" => 'React.PropTypes.object',
|
||||
"array" => 'React.PropTypes.array',
|
||||
"shape" => 'React.PropTypes.shape({})',
|
||||
"element" => 'React.PropTypes.element',
|
||||
"func" => 'React.PropTypes.func',
|
||||
"function" => 'React.PropTypes.func',
|
||||
"any" => 'React.PropTypes.any',
|
||||
|
||||
"instanceOf" => ->(type) {
|
||||
'React.PropTypes.instanceOf(%s)' % type.to_s.camelize
|
||||
},
|
||||
|
||||
"oneOf" => ->(*options) {
|
||||
enums = options.map{|k| "'#{k.to_s}'"}.join(',')
|
||||
'React.PropTypes.oneOf([%s])' % enums
|
||||
},
|
||||
|
||||
"oneOfType" => ->(*options) {
|
||||
types = options.map{|k| "#{lookup(k.to_s, k.to_s)}" }.join(',')
|
||||
'React.PropTypes.oneOfType([%s])' % types
|
||||
},
|
||||
}
|
||||
|
||||
def create_component_file
|
||||
extension = options[:es6] ? "es6.jsx" : "js.jsx"
|
||||
file_path = File.join('app/assets/javascripts/components', "#{file_name}.#{extension}")
|
||||
template("component.#{extension}", file_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_attributes!
|
||||
self.attributes = (attributes || []).map do |attr|
|
||||
name, type, options = "", "", ""
|
||||
options_regex = /(?<options>{.*})/
|
||||
|
||||
name, type = attr.split(':')
|
||||
|
||||
if matchdata = options_regex.match(type)
|
||||
options = matchdata[:options]
|
||||
type = type.gsub(options_regex, '')
|
||||
end
|
||||
|
||||
{ :name => name, :type => lookup(type, options) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.lookup(type = "node", options = "")
|
||||
react_prop_type = REACT_PROP_TYPES[type]
|
||||
if react_prop_type.blank?
|
||||
if type =~ /^[[:upper:]]/
|
||||
react_prop_type = REACT_PROP_TYPES['instanceOf']
|
||||
else
|
||||
react_prop_type = REACT_PROP_TYPES['node']
|
||||
end
|
||||
end
|
||||
|
||||
options = options.to_s.gsub(/[{}]/, '').split(',')
|
||||
|
||||
react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call
|
||||
react_prop_type
|
||||
end
|
||||
|
||||
def lookup(type = "node", options = "")
|
||||
self.class.lookup(type, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
module React
|
||||
module Generators
|
||||
class InstallGenerator < ::Rails::Generators::Base
|
||||
source_root File.expand_path '../../templates', __FILE__
|
||||
|
||||
desc 'Create default react.js folder layout and prep application.js'
|
||||
|
||||
class_option :skip_git,
|
||||
type: :boolean,
|
||||
aliases: '-g',
|
||||
default: false,
|
||||
desc: 'Skip Git keeps'
|
||||
|
||||
def create_directory
|
||||
empty_directory 'app/assets/javascripts/components'
|
||||
create_file 'app/assets/javascripts/components/.gitkeep' unless options[:skip_git]
|
||||
end
|
||||
|
||||
def inject_react
|
||||
require_react = "//= require react\n"
|
||||
|
||||
if manifest.exist?
|
||||
manifest_contents = File.read(manifest)
|
||||
|
||||
if manifest_contents.include? 'require turbolinks'
|
||||
inject_into_file manifest, require_react, {after: "//= require turbolinks\n"}
|
||||
elsif manifest_contents.include? 'require_tree'
|
||||
inject_into_file manifest, require_react, {before: '//= require_tree'}
|
||||
else
|
||||
append_file manifest, require_react
|
||||
end
|
||||
else
|
||||
create_file manifest, require_react
|
||||
end
|
||||
end
|
||||
|
||||
def inject_components
|
||||
inject_into_file manifest, "//= require components\n", {after: "//= require react\n"}
|
||||
end
|
||||
|
||||
def inject_react_ujs
|
||||
inject_into_file manifest, "//= require react_ujs\n", {after: "//= require react\n"}
|
||||
end
|
||||
|
||||
def create_components
|
||||
components_js = "//= require_tree ./components\n"
|
||||
components_file = File.join(*%w(app assets javascripts components.js))
|
||||
create_file components_file, components_js
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def manifest
|
||||
Pathname.new(destination_root).join('app/assets/javascripts', 'application.js')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
class <%= file_name.camelize %> extends React.Component {
|
||||
render () {
|
||||
<% if attributes.size > 0 -%>
|
||||
return (
|
||||
<div>
|
||||
<% attributes.each do |attribute| -%>
|
||||
<div><%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>}</div>
|
||||
<% end -%>
|
||||
</div>
|
||||
);
|
||||
<% else -%>
|
||||
return <div />;
|
||||
<% end -%>
|
||||
}
|
||||
}
|
||||
|
||||
<% if attributes.size > 0 -%>
|
||||
<%= file_name.camelize %>.propTypes = {
|
||||
<% attributes.each_with_index do |attribute, idx| -%>
|
||||
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %>
|
||||
<% end -%>
|
||||
};
|
||||
<% end -%>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
var <%= file_name.camelize %> = React.createClass({
|
||||
<% if attributes.size > 0 -%>
|
||||
propTypes: {
|
||||
<% attributes.each_with_index do |attribute, idx| -%>
|
||||
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %>
|
||||
<% end -%>
|
||||
},
|
||||
<% end -%>
|
||||
|
||||
render: function() {
|
||||
<% if attributes.size > 0 -%>
|
||||
return (
|
||||
<div>
|
||||
<% attributes.each do |attribute| -%>
|
||||
<div><%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>}</div>
|
||||
<% end -%>
|
||||
</div>
|
||||
);
|
||||
<% else -%>
|
||||
return <div />;
|
||||
<% end -%>
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
require "react/version"
|
||||
require 'react/jsx'
|
||||
require 'react/rails'
|
||||
require 'react/server_rendering'
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
require 'execjs'
|
||||
require 'react/jsx/template'
|
||||
require 'react/jsx/jsx_transformer'
|
||||
require 'react/jsx/babel_transformer'
|
||||
require 'rails'
|
||||
|
||||
module React
|
||||
module JSX
|
||||
DEFAULT_TRANSFORMER = BabelTransformer
|
||||
mattr_accessor :transform_options, :transformer_class, :transformer
|
||||
|
||||
# You can assign `config.react.jsx_transformer_class = `
|
||||
# to provide your own transformer. It must implement:
|
||||
# - #initialize(options)
|
||||
# - #transform(code) => new code
|
||||
self.transformer_class = DEFAULT_TRANSFORMER
|
||||
|
||||
def self.transform(code)
|
||||
self.transformer ||= transformer_class.new(transform_options)
|
||||
self.transformer.transform(code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
require 'babel/transpiler'
|
||||
module React
|
||||
module JSX
|
||||
class BabelTransformer
|
||||
DEPRECATED_OPTIONS = [:harmony, :strip_types, :asset_path]
|
||||
DEFAULT_TRANSFORM_OPTIONS = { blacklist: ['spec.functionName', 'validation.react', 'strict'] }
|
||||
def initialize(options)
|
||||
if (options.keys & DEPRECATED_OPTIONS).any?
|
||||
ActiveSupport::Deprecation.warn("Setting config.react.jsx_transform_options for :harmony, :strip_types, and :asset_path keys is now deprecated and has no effect with the default Babel Transformer."+
|
||||
"Please use new Babel Transformer options :whitelist, :plugin instead.")
|
||||
end
|
||||
|
||||
@transform_options = DEFAULT_TRANSFORM_OPTIONS.merge(options)
|
||||
end
|
||||
|
||||
def transform(code)
|
||||
Babel::Transpiler.transform(code, @transform_options)['code']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
module React
|
||||
module JSX
|
||||
class JSXTransformer
|
||||
DEFAULT_ASSET_PATH = 'JSXTransformer.js'
|
||||
|
||||
def initialize(options)
|
||||
@transform_options = {
|
||||
stripTypes: options.fetch(:strip_types, false),
|
||||
harmony: options.fetch(:harmony, false),
|
||||
}
|
||||
|
||||
@asset_path = options.fetch(:asset_path, DEFAULT_ASSET_PATH)
|
||||
|
||||
# If execjs uses therubyracer, there is no 'global'. Make sure
|
||||
# we have it so JSX script can work properly.
|
||||
js_code = 'var global = global || this;' + jsx_transform_code
|
||||
@context = ExecJS.compile(js_code)
|
||||
end
|
||||
|
||||
|
||||
def transform(code)
|
||||
result = @context.call('JSXTransformer.transform', code, @transform_options)
|
||||
result["code"]
|
||||
end
|
||||
|
||||
def jsx_transform_code
|
||||
# search for transformer file using sprockets - allows user to override
|
||||
# this file in his own application
|
||||
::Rails.application.assets[@asset_path].to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
require 'tilt'
|
||||
|
||||
module React
|
||||
module JSX
|
||||
|
||||
class Template < Tilt::Template
|
||||
self.default_mime_type = 'application/javascript'
|
||||
|
||||
def prepare
|
||||
end
|
||||
|
||||
def evaluate(scope, locals, &block)
|
||||
@output ||= JSX::transform(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
require 'react/rails/asset_variant'
|
||||
require 'react/rails/engine'
|
||||
require 'react/rails/railtie'
|
||||
require 'react/rails/controller_lifecycle'
|
||||
require 'react/rails/version'
|
||||
require 'react/rails/component_mount'
|
||||
require 'react/rails/view_helper'
|
||||
require 'react/rails/controller_renderer'
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
module React
|
||||
module Rails
|
||||
class AssetVariant
|
||||
GEM_ROOT = Pathname.new('../../../../').expand_path(__FILE__)
|
||||
attr_reader :react_build, :react_directory, :jsx_directory
|
||||
|
||||
def initialize(options={})
|
||||
# We want to include different files in dev/prod. The development builds
|
||||
# contain console logging for invariants and logging to help catch
|
||||
# common mistakes. These are all stripped out in the production build.
|
||||
@react_build = options[:variant] == :production ? 'production' : 'development'
|
||||
options[:addons] && @react_build += '-with-addons'
|
||||
|
||||
@react_directory = GEM_ROOT.join('lib/assets/react-source/').join(@react_build).to_s
|
||||
@jsx_directory = GEM_ROOT.join('lib/assets/javascripts/').to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
module React
|
||||
module Rails
|
||||
# This is the default view helper implementation.
|
||||
# It just inserts HTML into the DOM (see {#react_component}).
|
||||
#
|
||||
# You can extend this class or provide your own implementation
|
||||
# by assigning it to `config.react.view_helper_implementation`.
|
||||
class ComponentMount
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
attr_accessor :output_buffer
|
||||
|
||||
# ControllerLifecycle calls these hooks
|
||||
# You can use them in custom helper implementations
|
||||
def setup(env)
|
||||
end
|
||||
|
||||
def teardown(env)
|
||||
end
|
||||
|
||||
# Render a UJS-type HTML tag annotated with data attributes, which
|
||||
# are used by react_ujs to actually instantiate the React component
|
||||
# on the client.
|
||||
def react_component(name, props = {}, options = {}, &block)
|
||||
options = {:tag => options} if options.is_a?(Symbol)
|
||||
|
||||
prerender_options = options[:prerender]
|
||||
if prerender_options
|
||||
block = Proc.new{ concat React::ServerRendering.render(name, props, prerender_options) }
|
||||
end
|
||||
|
||||
html_options = options.reverse_merge(:data => {})
|
||||
html_options[:data].tap do |data|
|
||||
data[:react_class] = name
|
||||
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
|
||||
end
|
||||
html_tag = html_options[:tag] || :div
|
||||
|
||||
# remove internally used properties so they aren't rendered to DOM
|
||||
html_options.except!(:tag, :prerender)
|
||||
|
||||
content_tag(html_tag, '', html_options, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
module React
|
||||
module Rails
|
||||
module ControllerLifecycle
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# use old names to support Rails 3
|
||||
before_action :setup_react_component_helper
|
||||
after_action :teardown_react_component_helper
|
||||
attr_reader :__react_component_helper
|
||||
end
|
||||
|
||||
def setup_react_component_helper
|
||||
new_helper = React::Rails::ViewHelper.helper_implementation_class.new
|
||||
new_helper.setup(self)
|
||||
@__react_component_helper = new_helper
|
||||
end
|
||||
|
||||
def teardown_react_component_helper
|
||||
@__react_component_helper.teardown(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
class React::Rails::ControllerRenderer
|
||||
include React::Rails::ViewHelper
|
||||
include ActionView::Helpers::TagHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
attr_accessor :output_buffer
|
||||
|
||||
def initialize(options={})
|
||||
controller = options[:controller]
|
||||
@__react_component_helper = controller.__react_component_helper
|
||||
end
|
||||
|
||||
def call(name, options, &block)
|
||||
props = options.fetch(:props, {})
|
||||
options = options.slice(:data, :tag).merge(prerender: true)
|
||||
react_component(name, props, options, &block)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
module React
|
||||
module Rails
|
||||
class Engine < ::Rails::Engine
|
||||
initializer "react_rails.setup_engine", :group => :all do |app|
|
||||
sprockets_env = app.assets || Sprockets # Sprockets 3.x expects this in a different place
|
||||
sprockets_env.register_engine(".jsx", React::JSX::Template)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
require 'rails'
|
||||
|
||||
module React
|
||||
module Rails
|
||||
class Railtie < ::Rails::Railtie
|
||||
config.react = ActiveSupport::OrderedOptions.new
|
||||
# Sensible defaults. Can be overridden in application.rb
|
||||
config.react.variant = (::Rails.env.production? ? :production : :development)
|
||||
config.react.addons = false
|
||||
config.react.jsx_transform_options = {}
|
||||
config.react.jsx_transformer_class = nil # defaults to BabelTransformer
|
||||
# Server rendering:
|
||||
config.react.server_renderer_pool_size = 1 # increase if you're on JRuby
|
||||
config.react.server_renderer_timeout = 20 # seconds
|
||||
config.react.server_renderer = nil # defaults to SprocketsRenderer
|
||||
config.react.server_renderer_options = {} # SprocketsRenderer provides defaults
|
||||
# View helper implementation:
|
||||
config.react.view_helper_implementation = nil # Defaults to ComponentMount
|
||||
|
||||
# Watch .jsx files for changes in dev, so we can reload the JS VMs with the new JS code.
|
||||
initializer "react_rails.add_watchable_files", group: :all do |app|
|
||||
app.config.watchable_files.concat Dir["#{app.root}/app/assets/javascripts/**/*.jsx*"]
|
||||
end
|
||||
|
||||
# Include the react-rails view helper lazily
|
||||
initializer "react_rails.setup_view_helpers", group: :all do |app|
|
||||
|
||||
app.config.react.jsx_transformer_class ||= React::JSX::DEFAULT_TRANSFORMER
|
||||
React::JSX.transformer_class = app.config.react.jsx_transformer_class
|
||||
React::JSX.transform_options = app.config.react.jsx_transform_options
|
||||
|
||||
app.config.react.view_helper_implementation ||= React::Rails::ComponentMount
|
||||
React::Rails::ViewHelper.helper_implementation_class = app.config.react.view_helper_implementation
|
||||
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
include ::React::Rails::ControllerLifecycle
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:action_view) do
|
||||
include ::React::Rails::ViewHelper
|
||||
end
|
||||
end
|
||||
|
||||
initializer "react_rails.add_component_renderer", group: :all do |app|
|
||||
ActionController::Renderers.add :component do |component_name, options|
|
||||
renderer = ::React::Rails::ControllerRenderer.new(controller: self)
|
||||
html = renderer.call(component_name, options)
|
||||
render_options = options.merge(inline: html)
|
||||
render(render_options)
|
||||
end
|
||||
end
|
||||
|
||||
initializer "react_rails.bust_cache", group: :all do |app|
|
||||
asset_variant = React::Rails::AssetVariant.new({
|
||||
variant: app.config.react.variant,
|
||||
addons: app.config.react.addons,
|
||||
})
|
||||
|
||||
sprockets_env = app.assets || app.config.assets # sprockets-rails 3.x attaches this at a different config
|
||||
sprockets_env.version = [sprockets_env.version, "react-#{asset_variant.react_build}",].compact.join('-')
|
||||
|
||||
end
|
||||
|
||||
initializer "react_rails.set_variant", after: :engines_blank_point, group: :all do |app|
|
||||
asset_variant = React::Rails::AssetVariant.new({
|
||||
variant: app.config.react.variant,
|
||||
addons: app.config.react.addons,
|
||||
})
|
||||
|
||||
app.config.assets.paths << asset_variant.react_directory
|
||||
app.config.assets.paths << asset_variant.jsx_directory
|
||||
end
|
||||
|
||||
config.after_initialize do |app|
|
||||
# The class isn't accessible in the configure block, so assign it here if it wasn't overridden:
|
||||
app.config.react.server_renderer ||= React::ServerRendering::SprocketsRenderer
|
||||
|
||||
React::ServerRendering.pool_size = app.config.react.server_renderer_pool_size
|
||||
React::ServerRendering.pool_timeout = app.config.react.server_renderer_timeout
|
||||
React::ServerRendering.renderer_options = app.config.react.server_renderer_options
|
||||
React::ServerRendering.renderer = app.config.react.server_renderer
|
||||
|
||||
React::ServerRendering.reset_pool
|
||||
# Reload renderers in dev when files change
|
||||
ActiveSupport::Reloader.to_prepare { React::ServerRendering.reset_pool }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module React
|
||||
module Rails
|
||||
# If you change this, make sure to update VERSIONS.md
|
||||
VERSION = '1.3.3'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
module React
|
||||
module Rails
|
||||
module ViewHelper
|
||||
# This class will be used for inserting tags into HTML.
|
||||
# It should implement:
|
||||
# - #setup(controller_instance)
|
||||
# - #teardown(controller_instance)
|
||||
# - #react_component(name, props, options &block)
|
||||
# The default is {React::Rails::ComponentMount}
|
||||
mattr_accessor :helper_implementation_class
|
||||
|
||||
# Render a React component into the view
|
||||
# using the {helper_implementation_class}
|
||||
#
|
||||
# If called during a Rails controller-managed request, use the instance
|
||||
# created by the controller.
|
||||
#
|
||||
# Otherwise, make a new instance.
|
||||
def react_component(*args, &block)
|
||||
helper_obj = @__react_component_helper ||= helper_implementation_class.new
|
||||
helper_obj.react_component(*args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
require 'connection_pool'
|
||||
require 'react/server_rendering/exec_js_renderer'
|
||||
require 'react/server_rendering/sprockets_renderer'
|
||||
|
||||
module React
|
||||
module ServerRendering
|
||||
mattr_accessor :renderer, :renderer_options,
|
||||
:pool_size, :pool_timeout
|
||||
|
||||
def self.reset_pool
|
||||
options = {size: pool_size, timeout: pool_timeout}
|
||||
@@pool = ConnectionPool.new(options) { create_renderer }
|
||||
end
|
||||
|
||||
def self.render(component_name, props, prerender_options)
|
||||
@@pool.with do |renderer|
|
||||
renderer.render(component_name, props, prerender_options)
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_renderer
|
||||
renderer.new(renderer_options)
|
||||
end
|
||||
|
||||
class PrerenderError < RuntimeError
|
||||
def initialize(component_name, props, js_message)
|
||||
message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}",
|
||||
js_message.backtrace.join("\n")].join("\n")
|
||||
super(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# A bare-bones renderer for React.js + Exec.js
|
||||
# - No Rails dependency
|
||||
# - No browser concerns
|
||||
module React
|
||||
module ServerRendering
|
||||
class ExecJSRenderer
|
||||
def initialize(options={})
|
||||
js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!")
|
||||
@context = ExecJS.compile(GLOBAL_WRAPPER + js_code)
|
||||
end
|
||||
|
||||
def render(component_name, props, prerender_options)
|
||||
render_function = prerender_options.fetch(:render_function, "renderToString")
|
||||
js_code = <<-JS
|
||||
(function () {
|
||||
#{before_render(component_name, props, prerender_options)}
|
||||
var result = React.#{render_function}(React.createElement(#{component_name}, #{props}));
|
||||
#{after_render(component_name, props, prerender_options)}
|
||||
return result;
|
||||
})()
|
||||
JS
|
||||
@context.eval(js_code).html_safe
|
||||
rescue ExecJS::ProgramError => err
|
||||
raise React::ServerRendering::PrerenderError.new(component_name, props, err)
|
||||
end
|
||||
|
||||
# Hooks for inserting JS before/after rendering
|
||||
def before_render(component_name, props, prerender_options); ""; end
|
||||
def after_render(component_name, props, prerender_options); ""; end
|
||||
|
||||
# Handle Node.js & other ExecJS contexts
|
||||
GLOBAL_WRAPPER = <<-JS
|
||||
var global = global || this;
|
||||
var self = self || this;
|
||||
var window = window || this;
|
||||
JS
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Extends ExecJSRenderer for the Rails environment
|
||||
# - builds JS code out of the asset pipeline
|
||||
# - stringifies props
|
||||
# - implements console replay
|
||||
module React
|
||||
module ServerRendering
|
||||
class SprocketsRenderer < ExecJSRenderer
|
||||
def initialize(options={})
|
||||
@replay_console = options.fetch(:replay_console, true)
|
||||
filenames = options.fetch(:files, ["react.js", "components.js"])
|
||||
js_code = CONSOLE_POLYFILL.dup
|
||||
|
||||
filenames.each do |filename|
|
||||
js_code << ::Rails.application.assets[filename].to_s
|
||||
end
|
||||
|
||||
super(options.merge(code: js_code))
|
||||
end
|
||||
|
||||
def render(component_name, props, prerender_options)
|
||||
# pass prerender: :static to use renderToStaticMarkup
|
||||
react_render_method = if prerender_options == :static
|
||||
"renderToStaticMarkup"
|
||||
else
|
||||
"renderToString"
|
||||
end
|
||||
|
||||
if !props.is_a?(String)
|
||||
props = props.to_json
|
||||
end
|
||||
|
||||
super(component_name, props, {render_function: react_render_method})
|
||||
end
|
||||
|
||||
def after_render(component_name, props, prerender_options)
|
||||
@replay_console ? CONSOLE_REPLAY : ""
|
||||
end
|
||||
|
||||
# Reimplement console methods for replaying on the client
|
||||
CONSOLE_POLYFILL = <<-JS
|
||||
var console = { history: [] };
|
||||
['error', 'log', 'info', 'warn'].forEach(function (fn) {
|
||||
console[fn] = function () {
|
||||
console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)});
|
||||
};
|
||||
});
|
||||
JS
|
||||
|
||||
# Replay message from console history
|
||||
CONSOLE_REPLAY = <<-JS
|
||||
(function (history) {
|
||||
if (history && history.length > 0) {
|
||||
result += '\\n<scr'+'ipt>';
|
||||
history.forEach(function (msg) {
|
||||
result += '\\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');';
|
||||
});
|
||||
result += '\\n</scr'+'ipt>';
|
||||
}
|
||||
})(console.history);
|
||||
JS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module React
|
||||
VERSION = "1.3.3"
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
require File.expand_path('../lib/react/version', __FILE__)
|
||||
|
||||
Gem::Specification.new do |gem|
|
||||
gem.authors = ["Seth Call"]
|
||||
gem.email = ["seth@jamkazam.com"]
|
||||
gem.description = %q{Common library for JamKazam Ruby code}
|
||||
gem.summary = %q{Common library for JamKazam Ruby code}
|
||||
gem.homepage = ""
|
||||
|
||||
gem.files = `git ls-files`.split($\)
|
||||
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
||||
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
||||
gem.name = "react-rails"
|
||||
gem.require_paths = ["lib"]
|
||||
gem.version = React::VERSION
|
||||
end
|
||||
23
ruby/Gemfile
23
ruby/Gemfile
|
|
@ -4,11 +4,22 @@ unless ENV["LOCAL_DEV"] == "1"
|
|||
source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/'
|
||||
end
|
||||
|
||||
ruby_version = ENV["JAM_RUBY_VERSION"]
|
||||
ruby_version = "2.3.1" if ruby_version.nil?
|
||||
#ruby_version = ENV["JAM_RUBY_VERSION"]
|
||||
#ruby_version = "2.3.1" if ruby_version.nil?
|
||||
|
||||
ruby_version = "2.7.0"
|
||||
ruby ruby_version
|
||||
|
||||
#---------------------------------------------------------------
|
||||
#NOTE: upgrading rails to 6.1.0
|
||||
#version of following gems had to be changed
|
||||
#in order to be compatible with the dependent gems.
|
||||
#the special attention (tests) need to be paid in areas these
|
||||
#gems are been used to verify the desired behaviour is preserved
|
||||
#---------------------------------------------------------------
|
||||
#faker
|
||||
|
||||
|
||||
# Look for $WORKSPACE, otherwise use "workspace" as dev path.
|
||||
devenv = ENV["BUILD_NUMBER"].nil?
|
||||
if devenv
|
||||
|
|
@ -25,9 +36,9 @@ end
|
|||
gem 'pg' #, '0.17.1', :platform => [:mri, :mswin, :mingw]
|
||||
#gem 'jdbc_postgres', :platform => [:jruby]
|
||||
|
||||
gem 'activerecord', '= 6.0.6'
|
||||
#gem 'railties', '= 5.2.5'
|
||||
gem 'actionmailer', '= 6.0.6'
|
||||
gem 'activerecord', '= 6.1.0'
|
||||
gem 'railties', '= 6.1.0'
|
||||
gem 'actionmailer', '= 6.1.0'
|
||||
gem 'rails-observers'
|
||||
#gem 'rails-observers', '0.1.2'
|
||||
#gem 'protected_attributes' # needed to support attr_accessible
|
||||
|
|
@ -99,7 +110,7 @@ group :test do
|
|||
gem "rspec", "2.11"
|
||||
gem 'spork', '0.9.0'
|
||||
gem 'database_cleaner' #, '1.4.1'
|
||||
gem 'faker', '1.3.0'
|
||||
gem 'faker', '2.1.2'
|
||||
gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/"
|
||||
gem 'timecop'
|
||||
gem 'rspec-prof'
|
||||
|
|
|
|||
|
|
@ -14,41 +14,42 @@ GEM
|
|||
specs:
|
||||
aasm (5.4.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actionmailer (6.0.6)
|
||||
actionpack (= 6.0.6)
|
||||
actionview (= 6.0.6)
|
||||
activejob (= 6.0.6)
|
||||
actionmailer (6.1.0)
|
||||
actionpack (= 6.1.0)
|
||||
actionview (= 6.1.0)
|
||||
activejob (= 6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.6)
|
||||
actionview (= 6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
actionpack (6.1.0)
|
||||
actionview (= 6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actionview (6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
actionview (6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
activejob (6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
activerecord (6.0.6)
|
||||
activemodel (= 6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
activemodel (6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
activerecord (6.1.0)
|
||||
activemodel (= 6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
activerecord-import (0.4.1)
|
||||
activerecord (>= 3.0)
|
||||
activesupport (6.0.6)
|
||||
activesupport (6.1.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2, >= 2.2.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
amq-client (1.0.4)
|
||||
|
|
@ -70,7 +71,7 @@ GEM
|
|||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
builder (3.2.4)
|
||||
carrierwave (2.2.2)
|
||||
carrierwave (2.2.3)
|
||||
activemodel (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
addressable (~> 2.6)
|
||||
|
|
@ -100,14 +101,14 @@ GEM
|
|||
diff-lcs (1.1.3)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dry-inflector (0.2.0)
|
||||
dry-inflector (1.0.0)
|
||||
elastic-transport (8.1.0)
|
||||
faraday (< 3)
|
||||
multi_json
|
||||
elasticsearch (8.5.1)
|
||||
elasticsearch (8.5.2)
|
||||
elastic-transport (~> 8)
|
||||
elasticsearch-api (= 8.5.1)
|
||||
elasticsearch-api (8.5.1)
|
||||
elasticsearch-api (= 8.5.2)
|
||||
elasticsearch-api (8.5.2)
|
||||
multi_json
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
|
|
@ -118,8 +119,8 @@ GEM
|
|||
excon (0.94.0)
|
||||
factory_girl (4.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
faker (1.3.0)
|
||||
i18n (~> 0.5)
|
||||
faker (2.1.2)
|
||||
i18n (>= 0.8)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
|
|
@ -144,40 +145,28 @@ GEM
|
|||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
ffi (1.15.5)
|
||||
fog (1.24.0)
|
||||
fog-brightbox
|
||||
fog-core (~> 1.23)
|
||||
fog-json
|
||||
fog-radosgw (>= 0.0.2)
|
||||
fog-sakuracloud (>= 0.0.4)
|
||||
fog-softlayer
|
||||
ipaddress (~> 0.5)
|
||||
nokogiri (~> 1.5, >= 1.5.11)
|
||||
fog (0.7.2)
|
||||
builder
|
||||
excon (>= 0.6.1)
|
||||
formatador (>= 0.1.3)
|
||||
json
|
||||
mime-types
|
||||
net-ssh (>= 2.1.3)
|
||||
nokogiri (>= 1.4.4)
|
||||
ruby-hmac
|
||||
fog-brightbox (1.8.0)
|
||||
dry-inflector
|
||||
fog-core (>= 1.45, < 3.0)
|
||||
fog-json
|
||||
fog-core (1.45.0)
|
||||
fog-core (2.3.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
excon (~> 0.71)
|
||||
formatador (>= 0.2, < 2.0)
|
||||
mime-types
|
||||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-radosgw (0.0.5)
|
||||
fog-core (>= 1.21.0)
|
||||
fog-json
|
||||
fog-xml (>= 0.0.1)
|
||||
fog-sakuracloud (1.7.5)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-softlayer (1.1.4)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml (0.1.4)
|
||||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
formatador (0.3.0)
|
||||
formatador (1.1.0)
|
||||
fugit (1.7.2)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
|
|
@ -193,7 +182,7 @@ GEM
|
|||
httparty (0.20.0)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (0.9.5)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.8.0)
|
||||
ice_cube (~> 0.16)
|
||||
|
|
@ -201,7 +190,6 @@ GEM
|
|||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.3.5)
|
||||
json (1.8.6)
|
||||
kickbox (2.0.5)
|
||||
|
|
@ -222,17 +210,18 @@ GEM
|
|||
mime-types-data (3.2022.0105)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.6.1)
|
||||
minitest (5.15.0)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.16.3)
|
||||
mono_logger (1.1.1)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.2.3)
|
||||
mustermann (2.0.2)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-ssh (7.0.1)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.12.5)
|
||||
mini_portile2 (~> 2.6.1)
|
||||
nokogiri (1.13.9)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
oj (3.13.23)
|
||||
orm_adapter (0.5.0)
|
||||
|
|
@ -245,11 +234,11 @@ GEM
|
|||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
public_suffix (4.0.7)
|
||||
public_suffix (5.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.4)
|
||||
rack-protection (2.2.2)
|
||||
rack-protection (3.0.4)
|
||||
rack
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
|
|
@ -260,12 +249,12 @@ GEM
|
|||
loofah (~> 2.3)
|
||||
rails-observers (0.1.5)
|
||||
activemodel (>= 4.0)
|
||||
railties (6.0.6)
|
||||
actionpack (= 6.0.6)
|
||||
activesupport (= 6.0.6)
|
||||
railties (6.1.0)
|
||||
actionpack (= 6.1.0)
|
||||
activesupport (= 6.1.0)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
thor (~> 1.0)
|
||||
rake (13.0.6)
|
||||
recurly (2.18.16)
|
||||
redis (5.0.5)
|
||||
|
|
@ -318,6 +307,7 @@ GEM
|
|||
rspec-prof (0.0.5)
|
||||
rspec
|
||||
ruby-prof
|
||||
ruby-hmac (0.4.0)
|
||||
ruby-prof (1.4.3)
|
||||
ruby-protocol-buffers (1.2.2)
|
||||
ruby-vips (2.1.4)
|
||||
|
|
@ -339,29 +329,28 @@ GEM
|
|||
simplecov-html (0.7.1)
|
||||
simplecov-rcov (0.3.1)
|
||||
simplecov (>= 0.4.1)
|
||||
sinatra (2.2.2)
|
||||
mustermann (~> 2.0)
|
||||
rack (~> 2.2)
|
||||
rack-protection (= 2.2.2)
|
||||
sinatra (3.0.4)
|
||||
mustermann (~> 3.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-protection (= 3.0.4)
|
||||
tilt (~> 2.0)
|
||||
spork (0.9.0)
|
||||
sprockets (4.1.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
ssrf_filter (1.0.8)
|
||||
stripe (5.55.0)
|
||||
stripe-ruby-mock (3.0.1)
|
||||
ssrf_filter (1.1.1)
|
||||
stripe (8.0.0)
|
||||
stripe-ruby-mock (2.5.8)
|
||||
dante (>= 0.2.0)
|
||||
multi_json (~> 1.0)
|
||||
stripe (> 5, < 6)
|
||||
stripe (>= 2.0.3)
|
||||
thor (1.2.1)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.11)
|
||||
time_difference (0.5.0)
|
||||
activesupport
|
||||
timecop (0.9.5)
|
||||
tzinfo (1.2.10)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo (2.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
|
|
@ -377,12 +366,12 @@ GEM
|
|||
zip-codes (0.2.1)
|
||||
|
||||
PLATFORMS
|
||||
-darwin-21
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
aasm
|
||||
actionmailer (= 6.0.6)
|
||||
activerecord (= 6.0.6)
|
||||
actionmailer (= 6.1.0)
|
||||
activerecord (= 6.1.0)
|
||||
activerecord-import (~> 0.4.1)
|
||||
amqp (= 1.0.2)
|
||||
auto_strip_attributes
|
||||
|
|
@ -397,7 +386,7 @@ DEPENDENCIES
|
|||
email_validator
|
||||
eventmachine (= 1.0.4)
|
||||
factory_girl (= 4.5.0)
|
||||
faker (= 1.3.0)
|
||||
faker (= 2.1.2)
|
||||
faraday
|
||||
fog
|
||||
fog-brightbox
|
||||
|
|
@ -417,6 +406,7 @@ DEPENDENCIES
|
|||
protected_attributes_continued
|
||||
pry
|
||||
rails-observers
|
||||
railties (= 6.1.0)
|
||||
recurly (= 2.18.16)
|
||||
redis
|
||||
redis-namespace
|
||||
|
|
@ -449,7 +439,7 @@ DEPENDENCIES
|
|||
zip-codes
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.5.7p206
|
||||
ruby 2.7.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.26
|
||||
2.1.2
|
||||
|
|
|
|||
|
|
@ -420,8 +420,8 @@ FactoryGirl.define do
|
|||
|
||||
|
||||
factory :promo_buzz, :class => JamRuby::PromoBuzz do
|
||||
text_short Faker::Lorem.characters(10)
|
||||
text_long Faker::Lorem.paragraphs(3).join("\n")
|
||||
text_short Faker::Lorem.characters(number: 10)
|
||||
text_long Faker::Lorem.paragraphs(number: 3).join("\n")
|
||||
end
|
||||
|
||||
factory :icecast_limit, :class => JamRuby::IcecastLimit do
|
||||
|
|
@ -435,38 +435,38 @@ FactoryGirl.define do
|
|||
end
|
||||
|
||||
factory :icecast_admin_authentication, :class => JamRuby::IcecastAdminAuthentication do
|
||||
source_pass Faker::Lorem.characters(10)
|
||||
admin_user Faker::Lorem.characters(10)
|
||||
admin_pass Faker::Lorem.characters(10)
|
||||
relay_user Faker::Lorem.characters(10)
|
||||
relay_pass Faker::Lorem.characters(10)
|
||||
source_pass Faker::Lorem.characters(number: 10)
|
||||
admin_user Faker::Lorem.characters(number: 10)
|
||||
admin_pass Faker::Lorem.characters(number: 10)
|
||||
relay_user Faker::Lorem.characters(number: 10)
|
||||
relay_pass Faker::Lorem.characters(number: 10)
|
||||
end
|
||||
|
||||
factory :icecast_directory, :class => JamRuby::IcecastDirectory do
|
||||
yp_url_timeout 15
|
||||
yp_url Faker::Lorem.characters(10)
|
||||
yp_url Faker::Lorem.characters(number: 10)
|
||||
end
|
||||
|
||||
factory :icecast_master_server_relay, :class => JamRuby::IcecastMasterServerRelay do
|
||||
master_server Faker::Lorem.characters(10)
|
||||
master_server Faker::Lorem.characters(number: 10)
|
||||
master_server_port 8000
|
||||
master_update_interval 120
|
||||
master_username Faker::Lorem.characters(10)
|
||||
master_pass Faker::Lorem.characters(10)
|
||||
master_username Faker::Lorem.characters(number: 10)
|
||||
master_pass Faker::Lorem.characters(number: 10)
|
||||
relays_on_demand 1
|
||||
end
|
||||
|
||||
factory :icecast_path, :class => JamRuby::IcecastPath do
|
||||
base_dir Faker::Lorem.characters(10)
|
||||
log_dir Faker::Lorem.characters(10)
|
||||
pid_file Faker::Lorem.characters(10)
|
||||
web_root Faker::Lorem.characters(10)
|
||||
admin_root Faker::Lorem.characters(10)
|
||||
base_dir Faker::Lorem.characters(number: 10)
|
||||
log_dir Faker::Lorem.characters(number: 10)
|
||||
pid_file Faker::Lorem.characters(number: 10)
|
||||
web_root Faker::Lorem.characters(number: 10)
|
||||
admin_root Faker::Lorem.characters(number: 10)
|
||||
end
|
||||
|
||||
factory :icecast_logging, :class => JamRuby::IcecastLogging do
|
||||
access_log Faker::Lorem.characters(10)
|
||||
error_log Faker::Lorem.characters(10)
|
||||
access_log Faker::Lorem.characters(number: 10)
|
||||
error_log Faker::Lorem.characters(number: 10)
|
||||
log_level 3
|
||||
log_archive nil
|
||||
log_size 10000
|
||||
|
|
@ -478,18 +478,18 @@ FactoryGirl.define do
|
|||
|
||||
factory :icecast_mount, :class => JamRuby::IcecastMount do
|
||||
sequence(:name) { |n| "/mount_#{n}" }
|
||||
source_username Faker::Lorem.characters(10)
|
||||
source_pass Faker::Lorem.characters(10)
|
||||
source_username Faker::Lorem.characters(number: 10)
|
||||
source_pass Faker::Lorem.characters(number: 10)
|
||||
max_listeners 100
|
||||
max_listener_duration 3600
|
||||
fallback_mount Faker::Lorem.characters(10)
|
||||
fallback_mount Faker::Lorem.characters(number: 10)
|
||||
fallback_override 1
|
||||
fallback_when_full 1
|
||||
is_public -1
|
||||
stream_name Faker::Lorem.characters(10)
|
||||
stream_description Faker::Lorem.characters(10)
|
||||
stream_url Faker::Lorem.characters(10)
|
||||
genre Faker::Lorem.characters(10)
|
||||
stream_name Faker::Lorem.characters(number: 10)
|
||||
stream_description Faker::Lorem.characters(number: 10)
|
||||
stream_url Faker::Lorem.characters(number: 10)
|
||||
genre Faker::Lorem.characters(number: 10)
|
||||
hidden 0
|
||||
association :server, factory: :icecast_server_with_overrides
|
||||
|
||||
|
|
@ -522,19 +522,19 @@ FactoryGirl.define do
|
|||
|
||||
factory :icecast_relay, :class => JamRuby::IcecastRelay do
|
||||
port 8000
|
||||
mount Faker::Lorem.characters(10)
|
||||
server Faker::Lorem.characters(10)
|
||||
mount Faker::Lorem.characters(number: 10)
|
||||
server Faker::Lorem.characters(number: 10)
|
||||
on_demand 1
|
||||
end
|
||||
|
||||
factory :icecast_user_authentication, :class => JamRuby::IcecastUserAuthentication do
|
||||
authentication_type 'url'
|
||||
unused_username Faker::Lorem.characters(10)
|
||||
unused_pass Faker::Lorem.characters(10)
|
||||
mount_add Faker::Lorem.characters(10)
|
||||
mount_remove Faker::Lorem.characters(10)
|
||||
listener_add Faker::Lorem.characters(10)
|
||||
listener_remove Faker::Lorem.characters(10)
|
||||
unused_username Faker::Lorem.characters(number: 10)
|
||||
unused_pass Faker::Lorem.characters(number: 10)
|
||||
mount_add Faker::Lorem.characters(number: 10)
|
||||
mount_remove Faker::Lorem.characters(number: 10)
|
||||
listener_add Faker::Lorem.characters(number: 10)
|
||||
listener_remove Faker::Lorem.characters(number: 10)
|
||||
auth_header 'icecast-auth-user: 1'
|
||||
timelimit_header 'icecast-auth-timelimit:'
|
||||
end
|
||||
|
|
@ -563,18 +563,18 @@ FactoryGirl.define do
|
|||
|
||||
factory :icecast_mount_template, :class => JamRuby::IcecastMountTemplate do
|
||||
sequence(:name) { |n| "name-#{n}" }
|
||||
source_username Faker::Lorem.characters(10)
|
||||
source_pass Faker::Lorem.characters(10)
|
||||
source_username Faker::Lorem.characters(number: 10)
|
||||
source_pass Faker::Lorem.characters(number: 10)
|
||||
max_listeners 100
|
||||
max_listener_duration 3600
|
||||
fallback_mount Faker::Lorem.characters(10)
|
||||
fallback_mount Faker::Lorem.characters(number: 10)
|
||||
fallback_override 1
|
||||
fallback_when_full 1
|
||||
is_public -1
|
||||
stream_name Faker::Lorem.characters(10)
|
||||
stream_description Faker::Lorem.characters(10)
|
||||
stream_url Faker::Lorem.characters(10)
|
||||
genre Faker::Lorem.characters(10)
|
||||
stream_name Faker::Lorem.characters(number: 10)
|
||||
stream_description Faker::Lorem.characters(number: 10)
|
||||
stream_url Faker::Lorem.characters(number: 10)
|
||||
genre Faker::Lorem.characters(number: 10)
|
||||
hidden 0
|
||||
association :authentication, :factory => :icecast_user_authentication
|
||||
end
|
||||
|
|
@ -638,7 +638,7 @@ FactoryGirl.define do
|
|||
|
||||
factory :email_batch, :class => JamRuby::EmailBatch do
|
||||
subject Faker::Lorem.sentence
|
||||
body "#{JamRuby::EmailBatch::VAR_FIRST_NAME} " + Faker::Lorem.paragraphs(3).join("\n")
|
||||
body "#{JamRuby::EmailBatch::VAR_FIRST_NAME} " + Faker::Lorem.paragraphs(number: 3).join("\n")
|
||||
test_emails 4.times.collect { Faker::Internet.safe_email }.join(',')
|
||||
end
|
||||
|
||||
|
|
@ -655,7 +655,7 @@ FactoryGirl.define do
|
|||
|
||||
factory :notification_text_message do
|
||||
description 'TEXT_MESSAGE'
|
||||
message Faker::Lorem.characters(10)
|
||||
message Faker::Lorem.characters(number: 10)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -853,7 +853,7 @@ FactoryGirl.define do
|
|||
factory :broadcast_notification, :class => JamRuby::BroadcastNotification do
|
||||
title Faker::Lorem.sentence[0...50]
|
||||
message Faker::Lorem.paragraph[0...200]
|
||||
button_label Faker::Lorem.words(2).join(' ')[0...14]
|
||||
button_label Faker::Lorem.words(number: 2).join(' ')[0...14]
|
||||
frequency 3
|
||||
end
|
||||
|
||||
|
|
@ -882,7 +882,7 @@ FactoryGirl.define do
|
|||
end
|
||||
|
||||
factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do
|
||||
legalese Faker::Lorem.paragraphs(6).join("\n\n")
|
||||
legalese Faker::Lorem.paragraphs(number: 6).join("\n\n")
|
||||
end
|
||||
|
||||
factory :affiliate_distribution, class: 'JamRuby::AffiliateDistribution' do
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ describe IcecastAdminAuthentication do
|
|||
end
|
||||
|
||||
it "save" do
|
||||
admin.source_pass = Faker::Lorem.characters(10)
|
||||
admin.admin_user = Faker::Lorem.characters(10)
|
||||
admin.admin_pass = Faker::Lorem.characters(10)
|
||||
admin.relay_user = Faker::Lorem.characters(10)
|
||||
admin.relay_pass = Faker::Lorem.characters(10)
|
||||
admin.source_pass = Faker::Lorem.characters(number: 10)
|
||||
admin.admin_user = Faker::Lorem.characters(number: 10)
|
||||
admin.admin_pass = Faker::Lorem.characters(number: 10)
|
||||
admin.relay_user = Faker::Lorem.characters(number: 10)
|
||||
admin.relay_pass = Faker::Lorem.characters(number: 10)
|
||||
|
||||
admin.save.should be_true
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe IcecastDirectory do
|
|||
end
|
||||
|
||||
it "save" do
|
||||
dir.yp_url = Faker::Lorem.characters(10)
|
||||
dir.yp_url = Faker::Lorem.characters(number: 10)
|
||||
dir.yp_url_timeout = 20
|
||||
dir.save.should be_true
|
||||
dir.dumpXml(builder)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ describe IcecastLogging do
|
|||
end
|
||||
|
||||
it "save" do
|
||||
logging.access_log = Faker::Lorem.characters(10)
|
||||
logging.error_log = Faker::Lorem.characters(10)
|
||||
logging.access_log = Faker::Lorem.characters(number: 10)
|
||||
logging.error_log = Faker::Lorem.characters(number: 10)
|
||||
logging.log_level = 4
|
||||
logging.log_size = 20000
|
||||
logging.save!
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue