heroics 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/Rakefile +11 -0
- data/TODO +3 -0
- data/bin/heroku-api +20 -0
- data/heroics.gemspec +32 -0
- data/lib/heroics.rb +18 -0
- data/lib/heroics/cli.rb +92 -0
- data/lib/heroics/client.rb +65 -0
- data/lib/heroics/command.rb +67 -0
- data/lib/heroics/errors.rb +6 -0
- data/lib/heroics/link.rb +100 -0
- data/lib/heroics/naming.rb +19 -0
- data/lib/heroics/resource.rb +30 -0
- data/lib/heroics/schema.rb +242 -0
- data/lib/heroics/version.rb +3 -0
- data/test.rb +42 -0
- data/test/cli_test.rb +282 -0
- data/test/client_test.rb +162 -0
- data/test/command_test.rb +214 -0
- data/test/helper.rb +175 -0
- data/test/link_test.rb +349 -0
- data/test/naming_test.rb +45 -0
- data/test/resource_test.rb +35 -0
- data/test/schema_test.rb +207 -0
- data/test/version_test.rb +9 -0
- metadata +212 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2013 geemus
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Heroics
|
|
2
|
+
|
|
3
|
+
TODO: Write a gem description
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
gem 'heroics'
|
|
10
|
+
|
|
11
|
+
And then execute:
|
|
12
|
+
|
|
13
|
+
$ bundle
|
|
14
|
+
|
|
15
|
+
Or install it yourself as:
|
|
16
|
+
|
|
17
|
+
$ gem install heroics
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
TODO: Write usage instructions here
|
|
22
|
+
|
|
23
|
+
The interface is designed to match the workings of the (Heroku Platform API)[https://devcenter.heroku.com/articles/platform-api-reference].
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
heroics = Heroics.new(token: ENV['HEROKU_API_TOKEN'])
|
|
27
|
+
|
|
28
|
+
# apps
|
|
29
|
+
heroics.apps.create(name: 'example') # returns new app named 'example'
|
|
30
|
+
heroics.apps.list # returns list of all apps
|
|
31
|
+
heroics.apps.info('example') # returns app with id or name of 'example'
|
|
32
|
+
|
|
33
|
+
app = heroics.apps('example') # returns local reference to app with id or name 'example'
|
|
34
|
+
app.update(name: 'rename') # returns updated app
|
|
35
|
+
app.delete # returns deleted app
|
|
36
|
+
|
|
37
|
+
# addons
|
|
38
|
+
app = heroics.apps('example') # returns local reference to app with id or name 'example'
|
|
39
|
+
app.addons.create(plan: { name: 'heroku-postgresql:dev' }) # returns new add-on with plan:name 'heroku-postgresql:dev'
|
|
40
|
+
app.addons.list # returns list of all add-ons for app with id or name of 'example'
|
|
41
|
+
|
|
42
|
+
addon = app.addons.info('heroku-postgresql:dev') # returns add-on with id or name 'heroku-postgresql:dev'
|
|
43
|
+
addon.update(plan: { name: 'heroku-postgresql:basic' }) # returns updated add-on
|
|
44
|
+
addon.delete # returns deleted add-on
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Contributing
|
|
48
|
+
|
|
49
|
+
1. Fork it
|
|
50
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
51
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
52
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
53
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
|
|
4
|
+
Rake::TestTask.new do |task|
|
|
5
|
+
task.verbose = true
|
|
6
|
+
task.ruby_opts << '-r turn/autorun'
|
|
7
|
+
task.ruby_opts << '-I test'
|
|
8
|
+
task.test_files = FileList['test/**/*_test.rb', 'test/**/*_spec.rb']
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
task :default => :test
|
data/TODO
ADDED
data/bin/heroku-api
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'heroics'
|
|
4
|
+
require 'netrc'
|
|
5
|
+
|
|
6
|
+
netrc = Netrc.read
|
|
7
|
+
username, token = netrc['api.heroku.com']
|
|
8
|
+
if username && token
|
|
9
|
+
username = username.split('@').first
|
|
10
|
+
url = "https://#{username}:#{token}@api.heroku.com/schema"
|
|
11
|
+
options = {
|
|
12
|
+
default_headers: {'Accept' => 'application/vnd.heroku+json; version=3'},
|
|
13
|
+
cache: Moneta.new(:File, dir: "#{Dir.home}/.heroics/heroku-api")}
|
|
14
|
+
cli = Heroics.cli_from_schema_url('heroku-api', STDOUT, url, options)
|
|
15
|
+
cli.run(*ARGV)
|
|
16
|
+
else
|
|
17
|
+
puts "ERROR Couldn't find credentials for api.heroku.com in ~/.netrc."
|
|
18
|
+
puts " Login with the Heroku Toolbelt by running 'heroku login'."
|
|
19
|
+
1
|
|
20
|
+
end
|
data/heroics.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
|
|
6
|
+
require 'heroics/version'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |spec|
|
|
9
|
+
spec.name = 'heroics'
|
|
10
|
+
spec.version = Heroics::VERSION
|
|
11
|
+
spec.authors = ['geemus', 'jkakar']
|
|
12
|
+
spec.email = ['geemus@gmail.com', 'jkakar@kakar.ca']
|
|
13
|
+
spec.description = 'A Ruby client for HTTP APIs described using a JSON schema'
|
|
14
|
+
spec.summary = 'A Ruby client for HTTP APIs described using a JSON schema'
|
|
15
|
+
spec.homepage = ''
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
spec.files = `git ls-files`.split($/)
|
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
20
|
+
spec.test_files = spec.files.grep('^(test|spec|features)/')
|
|
21
|
+
spec.require_paths = ['lib']
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
|
24
|
+
spec.add_development_dependency 'minitest', '4.7.4'
|
|
25
|
+
spec.add_development_dependency 'rake'
|
|
26
|
+
spec.add_development_dependency 'turn'
|
|
27
|
+
|
|
28
|
+
spec.add_dependency 'excon'
|
|
29
|
+
spec.add_dependency 'netrc'
|
|
30
|
+
spec.add_dependency 'moneta'
|
|
31
|
+
spec.add_dependency 'multi_json'
|
|
32
|
+
end
|
data/lib/heroics.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'excon'
|
|
2
|
+
require 'moneta'
|
|
3
|
+
require 'multi_json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
|
|
7
|
+
# Heroics is an HTTP client for an API described by a JSON schema.
|
|
8
|
+
module Heroics
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require 'heroics/errors'
|
|
12
|
+
require 'heroics/naming'
|
|
13
|
+
require 'heroics/link'
|
|
14
|
+
require 'heroics/resource'
|
|
15
|
+
require 'heroics/client'
|
|
16
|
+
require 'heroics/schema'
|
|
17
|
+
require 'heroics/command'
|
|
18
|
+
require 'heroics/cli'
|
data/lib/heroics/cli.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Heroics
|
|
2
|
+
class CLI
|
|
3
|
+
# Instantiate a CLI for an API described by a JSON schema.
|
|
4
|
+
#
|
|
5
|
+
# @param name [String] The name of the CLI.
|
|
6
|
+
# @param schema [Schema] The JSON schema describing the API.
|
|
7
|
+
# @param client [Client] A client generated from the JSON schema.
|
|
8
|
+
# @param output [IO] The stream to write to.
|
|
9
|
+
def initialize(name, commands, output)
|
|
10
|
+
@name = name
|
|
11
|
+
@commands = commands
|
|
12
|
+
@output = output
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Run a command.
|
|
16
|
+
#
|
|
17
|
+
# @param parameters [Array] The parameters to use when running the
|
|
18
|
+
# command. The first parameters is the name of the command and the
|
|
19
|
+
# remaining parameters are passed to it.
|
|
20
|
+
def run(*parameters)
|
|
21
|
+
name = parameters.shift
|
|
22
|
+
if name.nil? || name == 'help'
|
|
23
|
+
if command_name = parameters.first
|
|
24
|
+
command = @commands[command_name]
|
|
25
|
+
command.usage
|
|
26
|
+
else
|
|
27
|
+
usage
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
command = @commands[name]
|
|
31
|
+
if command.nil?
|
|
32
|
+
@output.write("There is no command called '#{name}'.\n")
|
|
33
|
+
else
|
|
34
|
+
command.run(*parameters)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Write usage information to the output stream.
|
|
42
|
+
def usage
|
|
43
|
+
if @commands.empty?
|
|
44
|
+
@output.write 'No commands are available.'
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@output.write <<-USAGE
|
|
49
|
+
Usage: #{@name} <command> [<parameter> [...]] [<body>]
|
|
50
|
+
|
|
51
|
+
Help topics, type "#{@name} help <topic>" for more details:
|
|
52
|
+
|
|
53
|
+
USAGE
|
|
54
|
+
|
|
55
|
+
name_width = @commands.keys.max_by { |key| key.size }.size
|
|
56
|
+
@commands.sort.each do |name, command|
|
|
57
|
+
name = name.ljust(name_width)
|
|
58
|
+
description = command.description
|
|
59
|
+
@output.puts(" #{name} #{description}")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.cli_from_schema(name, output, schema, url, options={})
|
|
65
|
+
client = client_from_schema(schema, url, options)
|
|
66
|
+
commands = {}
|
|
67
|
+
schema.resources.each do |resource_schema|
|
|
68
|
+
resource_schema.links.each do |link_schema|
|
|
69
|
+
command = Command.new(name, link_schema, client, output)
|
|
70
|
+
commands[command.name] = command
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
CLI.new(name, commands, output)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Download a JSON schema and create a CLI with it.
|
|
77
|
+
#
|
|
78
|
+
# @param name [String] The name of the CLI.
|
|
79
|
+
# @param output [IO] The stream to write to.
|
|
80
|
+
# @param url [String] The URL for the schema. The URL will be used by the
|
|
81
|
+
# generated CLI when it makes requests.
|
|
82
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
|
83
|
+
# - default_headers: Optionally, a set of headers to include in every
|
|
84
|
+
# request made by the CLI. Default is no custom headers.
|
|
85
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
|
86
|
+
# is no caching.
|
|
87
|
+
# @return [CLI] A CLI with commands generated from the JSON schema.
|
|
88
|
+
def self.cli_from_schema_url(name, output, url, options={})
|
|
89
|
+
schema = download_schema(url, options)
|
|
90
|
+
cli_from_schema(name, output, schema, url, options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Heroics
|
|
2
|
+
# An HTTP client with methods mapped to API resources.
|
|
3
|
+
class Client
|
|
4
|
+
# Instantiate an HTTP client.
|
|
5
|
+
#
|
|
6
|
+
# @param resources [Hash<String,Resource>] A hash that maps method names
|
|
7
|
+
# to resources.
|
|
8
|
+
def initialize(resources)
|
|
9
|
+
@resources = resources
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Find a resource.
|
|
13
|
+
#
|
|
14
|
+
# @param name [String] The name of the resource to find.
|
|
15
|
+
# @raise [NoMethodError] Raised if the name doesn't match a known resource.
|
|
16
|
+
# @return [Resource] The resource matching the name.
|
|
17
|
+
def method_missing(name)
|
|
18
|
+
resource = @resources[name.to_s]
|
|
19
|
+
if resource.nil?
|
|
20
|
+
address = "<#{self.class.name}:0x00#{(self.object_id << 1).to_s(16)}>"
|
|
21
|
+
raise NoMethodError.new("undefined method `#{name}' for ##{address}")
|
|
22
|
+
end
|
|
23
|
+
resource
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Create an HTTP client from a JSON schema.
|
|
28
|
+
#
|
|
29
|
+
# @param schema [Schema] The JSON schema to build an HTTP client for.
|
|
30
|
+
# @param url [String] The URL the generated client should use when making
|
|
31
|
+
# requests. Include the username and password to use with HTTP basic
|
|
32
|
+
# auth.
|
|
33
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
|
34
|
+
# - default_headers: Optionally, a set of headers to include in every
|
|
35
|
+
# request made by the client. Default is no custom headers.
|
|
36
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
|
37
|
+
# is no caching.
|
|
38
|
+
# @return [Client] A client with resources and links from the JSON schema.
|
|
39
|
+
def self.client_from_schema(schema, url, options={})
|
|
40
|
+
resources = {}
|
|
41
|
+
schema.resources.each do |resource_schema|
|
|
42
|
+
links = {}
|
|
43
|
+
resource_schema.links.each do |link_schema|
|
|
44
|
+
links[link_schema.name] = Link.new(url, link_schema, options)
|
|
45
|
+
end
|
|
46
|
+
resources[resource_schema.name] = Resource.new(links)
|
|
47
|
+
end
|
|
48
|
+
Client.new(resources)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Download a JSON schema and create an HTTP client with it.
|
|
52
|
+
#
|
|
53
|
+
# @param url [String] The URL for the schema. The URL will be used by the
|
|
54
|
+
# generated client when it makes requests.
|
|
55
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
|
56
|
+
# - default_headers: Optionally, a set of headers to include in every
|
|
57
|
+
# request made by the client. Default is no custom headers.
|
|
58
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
|
59
|
+
# is no caching.
|
|
60
|
+
# @return [Client] A client with resources and links from the JSON schema.
|
|
61
|
+
def self.client_from_schema_url(url, options={})
|
|
62
|
+
schema = download_schema(url, options)
|
|
63
|
+
client_from_schema(schema, URI::join(url, '/').to_s, options)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Heroics
|
|
2
|
+
class Command
|
|
3
|
+
# Instantiate a command.
|
|
4
|
+
#
|
|
5
|
+
# @param cli_name [String] The name of the CLI.
|
|
6
|
+
# @param link_schema [LinkSchema] The schema for the underlying link this
|
|
7
|
+
# command represents.
|
|
8
|
+
# @param client [Client] The client to use when making requests.
|
|
9
|
+
# @param output [IO] The stream to write output to.
|
|
10
|
+
def initialize(cli_name, link_schema, client, output)
|
|
11
|
+
@cli_name = cli_name
|
|
12
|
+
@link_schema = link_schema
|
|
13
|
+
@client = client
|
|
14
|
+
@output = output
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The command name.
|
|
18
|
+
def name
|
|
19
|
+
"#{@link_schema.pretty_resource_name}:#{@link_schema.pretty_name}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The command description.
|
|
23
|
+
def description
|
|
24
|
+
@link_schema.description
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Write usage information to the output stream.
|
|
28
|
+
def usage
|
|
29
|
+
parameters = @link_schema.parameters.map { |parameter| "<#{parameter}>" }
|
|
30
|
+
parameters = parameters.empty? ? '' : " #{parameters.join(' ')}"
|
|
31
|
+
example_body = @link_schema.example_body
|
|
32
|
+
body_parameter = example_body.nil? ? '' : ' <body>'
|
|
33
|
+
@output.write <<-USAGE
|
|
34
|
+
Usage: #{@cli_name} #{name}#{parameters}#{body_parameter}
|
|
35
|
+
|
|
36
|
+
Description:
|
|
37
|
+
#{description}
|
|
38
|
+
USAGE
|
|
39
|
+
if example_body
|
|
40
|
+
example_body = MultiJson.dump(example_body, pretty: true)
|
|
41
|
+
example_body = example_body.lines.map do |line|
|
|
42
|
+
" #{line}"
|
|
43
|
+
end.join
|
|
44
|
+
@output.write <<-USAGE
|
|
45
|
+
|
|
46
|
+
Body example:
|
|
47
|
+
#{example_body}
|
|
48
|
+
USAGE
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run the command and write the results to the output stream.
|
|
53
|
+
#
|
|
54
|
+
# @param parameters [Array] The parameters to pass when making a request
|
|
55
|
+
# to run the command.
|
|
56
|
+
def run(*parameters)
|
|
57
|
+
resource_name = @link_schema.resource_name
|
|
58
|
+
name = @link_schema.name
|
|
59
|
+
result = @client.send(resource_name).send(name, *parameters)
|
|
60
|
+
result = result.to_a if result.instance_of?(Enumerator)
|
|
61
|
+
if result && !result.instance_of?(String)
|
|
62
|
+
result = MultiJson.dump(result, pretty: true)
|
|
63
|
+
end
|
|
64
|
+
@output.puts(result) unless result.nil?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/heroics/link.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module Heroics
|
|
2
|
+
# A link invokes requests with an HTTP server.
|
|
3
|
+
class Link
|
|
4
|
+
# Instantiate a link.
|
|
5
|
+
#
|
|
6
|
+
# @param url [String] The URL to use when making requests. Include the
|
|
7
|
+
# username and password to use with HTTP basic auth.
|
|
8
|
+
# @param link_schema [LinkSchema] The schema for this link.
|
|
9
|
+
# @param options [Hash] Configuration for the link. Possible keys
|
|
10
|
+
# include:
|
|
11
|
+
# - default_headers: Optionally, a set of headers to include in every
|
|
12
|
+
# request made by the client. Default is no custom headers.
|
|
13
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags.
|
|
14
|
+
# Default is no caching.
|
|
15
|
+
def initialize(url, link_schema, options={})
|
|
16
|
+
@url = url
|
|
17
|
+
@link_schema = link_schema
|
|
18
|
+
@default_headers = options[:default_headers] || {}
|
|
19
|
+
@cache = options[:cache] || Moneta.new(:Null)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Make a request to the server.
|
|
23
|
+
#
|
|
24
|
+
# JSON content received with an ETag is cached. When the server returns a
|
|
25
|
+
# *304 Not Modified* status code content is loaded and returned from the
|
|
26
|
+
# cache. The cache considers headers, in addition to the URL path, when
|
|
27
|
+
# creating keys so that requests to the same path, such as for paginated
|
|
28
|
+
# results, don't cause cache collisions.
|
|
29
|
+
#
|
|
30
|
+
# When the server returns a *206 Partial Content* status code the result
|
|
31
|
+
# is assumed to be an array and an enumerator is returned. The enumerator
|
|
32
|
+
# yields results from the response until they've been consumed at which
|
|
33
|
+
# point, if additional content is available from the server, it blocks and
|
|
34
|
+
# makes a request to fetch the subsequent page of data. This behaviour
|
|
35
|
+
# continues until the client stops iterating the enumerator or the dataset
|
|
36
|
+
# from the server has been entirely consumed.
|
|
37
|
+
#
|
|
38
|
+
# @param parameters [Array] The list of parameters to inject into the
|
|
39
|
+
# path. A request body can be passed as the final parameter and will
|
|
40
|
+
# always be converted to JSON before being transmitted.
|
|
41
|
+
# @raise [ArgumentError] Raised if either too many or too few parameters
|
|
42
|
+
# were provided.
|
|
43
|
+
# @return [String,Object,Enumerator] A string for text responses, an
|
|
44
|
+
# object for JSON responses, or an enumerator for list responses.
|
|
45
|
+
def run(*parameters)
|
|
46
|
+
path, body = @link_schema.format_path(parameters)
|
|
47
|
+
headers = @default_headers
|
|
48
|
+
if body
|
|
49
|
+
headers = headers.merge({'Content-Type' => 'application/json'})
|
|
50
|
+
body = MultiJson.dump(body)
|
|
51
|
+
end
|
|
52
|
+
cache_key = "#{path}:#{headers.hash}"
|
|
53
|
+
if @link_schema.method == :get
|
|
54
|
+
etag = @cache["etag:#{cache_key}"]
|
|
55
|
+
headers = headers.merge({'If-None-Match' => etag}) if etag
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
connection = Excon.new(@url)
|
|
59
|
+
response = connection.request(method: @link_schema.method, path: path,
|
|
60
|
+
headers: headers, body: body,
|
|
61
|
+
expects: [200, 201, 202, 206, 304])
|
|
62
|
+
content_type = response.headers['Content-Type']
|
|
63
|
+
if response.status == 304
|
|
64
|
+
MultiJson.load(@cache["data:#{cache_key}"])
|
|
65
|
+
elsif content_type && content_type.include?('application/json')
|
|
66
|
+
etag = response.headers['ETag']
|
|
67
|
+
if etag
|
|
68
|
+
@cache["etag:#{cache_key}"] = etag
|
|
69
|
+
@cache["data:#{cache_key}"] = response.body
|
|
70
|
+
end
|
|
71
|
+
body = MultiJson.load(response.body)
|
|
72
|
+
if response.status == 206
|
|
73
|
+
next_range = response.headers['Next-Range']
|
|
74
|
+
Enumerator.new do |yielder|
|
|
75
|
+
while true do
|
|
76
|
+
# Yield the results we got in the body.
|
|
77
|
+
body.each do |item|
|
|
78
|
+
yielder << item
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Only make a request to get the next page if we have a valid
|
|
82
|
+
# next range.
|
|
83
|
+
break unless next_range
|
|
84
|
+
headers = headers.merge({'Range' => next_range})
|
|
85
|
+
response = connection.request(method: @link_schema.method,
|
|
86
|
+
path: path, headers: headers,
|
|
87
|
+
expects: [200, 201, 206])
|
|
88
|
+
body = MultiJson.load(response.body)
|
|
89
|
+
next_range = response.headers['Next-Range']
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
body
|
|
94
|
+
end
|
|
95
|
+
elsif !response.body.empty?
|
|
96
|
+
response.body
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|