komoju 0.0.0 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -14
- data/.gitmodules +3 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +1 -1
- data/README.md +14 -2
- data/Rakefile +9 -0
- data/bin/generate-client +11 -0
- data/komoju.gemspec +15 -6
- data/lib/komoju/client.rb +460 -0
- data/lib/komoju/version.rb +1 -1
- data/lib/komoju.rb +10 -2
- data/test/client_test.rb +8 -0
- data/test/helper.rb +1 -0
- data/vendor/heroics/lib/heroics/cli.rb +88 -0
- data/vendor/heroics/lib/heroics/client.rb +109 -0
- data/vendor/heroics/lib/heroics/client_generator.rb +99 -0
- data/vendor/heroics/lib/heroics/command.rb +67 -0
- data/vendor/heroics/lib/heroics/errors.rb +6 -0
- data/vendor/heroics/lib/heroics/link.rb +120 -0
- data/vendor/heroics/lib/heroics/naming.rb +19 -0
- data/vendor/heroics/lib/heroics/resource.rb +30 -0
- data/vendor/heroics/lib/heroics/schema.rb +444 -0
- data/vendor/heroics/lib/heroics/version.rb +3 -0
- data/vendor/heroics/lib/heroics.rb +22 -0
- data/vendor/heroics/test/cli_test.rb +236 -0
- data/vendor/heroics/test/client_generator_test.rb +34 -0
- data/vendor/heroics/test/client_test.rb +215 -0
- data/vendor/heroics/test/command_test.rb +214 -0
- data/vendor/heroics/test/helper.rb +204 -0
- data/vendor/heroics/test/link_test.rb +398 -0
- data/vendor/heroics/test/naming_test.rb +45 -0
- data/vendor/heroics/test/resource_test.rb +35 -0
- data/vendor/heroics/test/schema_test.rb +287 -0
- data/vendor/heroics/test/version_test.rb +9 -0
- data/vendor/heroics/test.rb +42 -0
- metadata +139 -8
@@ -0,0 +1,88 @@
|
|
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
|
+
# Create a CLI from a JSON schema.
|
65
|
+
#
|
66
|
+
# @param name [String] The name of the CLI.
|
67
|
+
# @param output [IO] The stream to write to.
|
68
|
+
# @param schema [Hash] The JSON schema to use with the CLI.
|
69
|
+
# @param url [String] The URL used by the generated CLI when it makes
|
70
|
+
# requests.
|
71
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
72
|
+
# - default_headers: Optionally, a set of headers to include in every
|
73
|
+
# request made by the CLI. Default is no custom headers.
|
74
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
75
|
+
# is no caching.
|
76
|
+
# @return [CLI] A CLI with commands generated from the JSON schema.
|
77
|
+
def self.cli_from_schema(name, output, schema, url, options={})
|
78
|
+
client = client_from_schema(schema, url, options)
|
79
|
+
commands = {}
|
80
|
+
schema.resources.each do |resource_schema|
|
81
|
+
resource_schema.links.each do |link_schema|
|
82
|
+
command = Command.new(name, link_schema, client, output)
|
83
|
+
commands[command.name] = command
|
84
|
+
end
|
85
|
+
end
|
86
|
+
CLI.new(name, commands, output)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,109 @@
|
|
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
|
+
# @param url [String] The URL used by this client.
|
9
|
+
def initialize(resources, url)
|
10
|
+
@resources = resources
|
11
|
+
@url = url
|
12
|
+
end
|
13
|
+
|
14
|
+
# Find a resource.
|
15
|
+
#
|
16
|
+
# @param name [String] The name of the resource to find.
|
17
|
+
# @raise [NoMethodError] Raised if the name doesn't match a known resource.
|
18
|
+
# @return [Resource] The resource matching the name.
|
19
|
+
def method_missing(name)
|
20
|
+
resource = @resources[name.to_s]
|
21
|
+
if resource.nil?
|
22
|
+
raise NoMethodError.new("undefined method `#{name}' for #{to_s}")
|
23
|
+
end
|
24
|
+
resource
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get a simple human-readable representation of this client instance.
|
28
|
+
def inspect
|
29
|
+
url = URI.parse(@url)
|
30
|
+
unless url.password.nil?
|
31
|
+
url.password = 'REDACTED'
|
32
|
+
end
|
33
|
+
"#<Heroics::Client url=\"#{url.to_s}\">"
|
34
|
+
end
|
35
|
+
alias to_s inspect
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create an HTTP client from a JSON schema.
|
39
|
+
#
|
40
|
+
# @param schema [Schema] The JSON schema to build an HTTP client for.
|
41
|
+
# @param url [String] The URL the generated client should use when making
|
42
|
+
# requests. Include the username and password to use with HTTP basic
|
43
|
+
# auth.
|
44
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
45
|
+
# - default_headers: Optionally, a set of headers to include in every
|
46
|
+
# request made by the client. Default is no custom headers.
|
47
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
48
|
+
# is no caching.
|
49
|
+
# @return [Client] A client with resources and links from the JSON schema.
|
50
|
+
def self.client_from_schema(schema, url, options={})
|
51
|
+
resources = {}
|
52
|
+
schema.resources.each do |resource_schema|
|
53
|
+
links = {}
|
54
|
+
resource_schema.links.each do |link_schema|
|
55
|
+
links[link_schema.name] = Link.new(url, link_schema, options)
|
56
|
+
end
|
57
|
+
resources[resource_schema.name] = Resource.new(links)
|
58
|
+
end
|
59
|
+
Client.new(resources, url)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create an HTTP client with OAuth credentials from a JSON schema.
|
63
|
+
#
|
64
|
+
# @param oauth_token [String] The OAuth token to pass using the `Bearer`
|
65
|
+
# authorization mechanism.
|
66
|
+
# @param schema [Schema] The JSON schema to build an HTTP client for.
|
67
|
+
# @param url [String] The URL the generated client should use when making
|
68
|
+
# requests.
|
69
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
70
|
+
# - default_headers: Optionally, a set of headers to include in every
|
71
|
+
# request made by the client. Default is no custom headers.
|
72
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
73
|
+
# is no caching.
|
74
|
+
# @return [Client] A client with resources and links from the JSON schema.
|
75
|
+
def self.oauth_client_from_schema(oauth_token, schema, url, options={})
|
76
|
+
authorization = "Bearer #{oauth_token}"
|
77
|
+
# Don't mutate user-supplied data.
|
78
|
+
options = Marshal.load(Marshal.dump(options))
|
79
|
+
if !options.has_key?(:default_headers)
|
80
|
+
options[:default_headers] = {}
|
81
|
+
end
|
82
|
+
options[:default_headers].merge!({"Authorization" => authorization})
|
83
|
+
client_from_schema(schema, url, options)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create an HTTP client with Token credentials from a JSON schema.
|
87
|
+
#
|
88
|
+
# @param oauth_token [String] The token to pass using the `Bearer`
|
89
|
+
# authorization mechanism.
|
90
|
+
# @param schema [Schema] The JSON schema to build an HTTP client for.
|
91
|
+
# @param url [String] The URL the generated client should use when making
|
92
|
+
# requests.
|
93
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
94
|
+
# - default_headers: Optionally, a set of headers to include in every
|
95
|
+
# request made by the client. Default is no custom headers.
|
96
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
97
|
+
# is no caching.
|
98
|
+
# @return [Client] A client with resources and links from the JSON schema.
|
99
|
+
def self.token_client_from_schema(token, schema, url, options={})
|
100
|
+
authorization = "Token token=#{token}"
|
101
|
+
# Don't mutate user-supplied data.
|
102
|
+
options = Marshal.load(Marshal.dump(options))
|
103
|
+
if !options.has_key?(:default_headers)
|
104
|
+
options[:default_headers] = {}
|
105
|
+
end
|
106
|
+
options[:default_headers].merge!({"Authorization" => authorization})
|
107
|
+
client_from_schema(schema, url, options)
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Heroics
|
2
|
+
# Generate a static client that uses Heroics under the hood. This is a good
|
3
|
+
# option if you want to ship a gem or generate API documentation using Yard.
|
4
|
+
#
|
5
|
+
# @param module_name [String] The name of the module, as rendered in a Ruby
|
6
|
+
# source file, to use for the generated client.
|
7
|
+
# @param schema [Schema] The schema instance to generate the client from.
|
8
|
+
# @param url [String] The URL for the API service.
|
9
|
+
# @param options [Hash] Configuration for links. Possible keys include:
|
10
|
+
# - default_headers: Optionally, a set of headers to include in every
|
11
|
+
# request made by the client. Default is no custom headers.
|
12
|
+
# - cache: Optionally, a Moneta-compatible cache to store ETags. Default
|
13
|
+
# is no caching.
|
14
|
+
def self.generate_client(module_name, schema, url, options)
|
15
|
+
filename = File.dirname(__FILE__) + '/views/client.erb'
|
16
|
+
eruby = Erubis::Eruby.new(File.read(filename))
|
17
|
+
context = build_context(module_name, schema, url, options)
|
18
|
+
eruby.evaluate(context)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Process the schema to build up the context needed to render the source
|
24
|
+
# template.
|
25
|
+
def self.build_context(module_name, schema, url, options)
|
26
|
+
resources = []
|
27
|
+
schema.resources.each do |resource_schema|
|
28
|
+
links = []
|
29
|
+
resource_schema.links.each do |link_schema|
|
30
|
+
links << GeneratorLink.new(link_schema.name.gsub('-', '_'),
|
31
|
+
link_schema.description,
|
32
|
+
link_schema.parameter_details)
|
33
|
+
end
|
34
|
+
resources << GeneratorResource.new(resource_schema.name.gsub('-', '_'),
|
35
|
+
resource_schema.description,
|
36
|
+
links)
|
37
|
+
end
|
38
|
+
|
39
|
+
{
|
40
|
+
module_name: module_name,
|
41
|
+
url: url,
|
42
|
+
default_headers: options.fetch(:default_headers, {}),
|
43
|
+
cache: options.fetch(:cache, {}),
|
44
|
+
description: schema.description,
|
45
|
+
schema: MultiJson.dump(schema.schema, pretty:true),
|
46
|
+
resources: resources
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
# A representation of a resource for use when generating source code in the
|
51
|
+
# template.
|
52
|
+
class GeneratorResource
|
53
|
+
attr_reader :name, :description, :links
|
54
|
+
|
55
|
+
def initialize(name, description, links)
|
56
|
+
@name = name
|
57
|
+
@description = description
|
58
|
+
@links = links
|
59
|
+
end
|
60
|
+
|
61
|
+
# The name of the resource class in generated code.
|
62
|
+
def class_name
|
63
|
+
Heroics.camel_case(name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# A representation of a link for use when generating source code in the
|
68
|
+
# template.
|
69
|
+
class GeneratorLink
|
70
|
+
attr_reader :name, :description, :parameters
|
71
|
+
|
72
|
+
def initialize(name, description, parameters)
|
73
|
+
@name = name
|
74
|
+
@description = description
|
75
|
+
@parameters = parameters
|
76
|
+
end
|
77
|
+
|
78
|
+
# List of parameters for the method signature
|
79
|
+
def signatures
|
80
|
+
@parameters.map { |info| info.signature }.join(', ')
|
81
|
+
end
|
82
|
+
|
83
|
+
# The list of parameters to render in generated source code for the method
|
84
|
+
# signature for the link.
|
85
|
+
def parameter_list
|
86
|
+
@parameters.map { |info| info.name }.join(', ')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Convert a lower_case_name to CamelCase.
|
91
|
+
def self.camel_case(text)
|
92
|
+
return text if text !~ /_/ && text =~ /[A-Z]+.*/
|
93
|
+
text = text.split('_').map{ |element| element.capitalize }.join
|
94
|
+
[/^Ssl/, /^Http/, /^Xml/].each do |replace|
|
95
|
+
text.sub!(replace) { |match| match.upcase }
|
96
|
+
end
|
97
|
+
text
|
98
|
+
end
|
99
|
+
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
|
@@ -0,0 +1,120 @@
|
|
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
|
+
@root_url, @path_prefix = unpack_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
|
+
path = "#{@path_prefix}#{path}" unless @path_prefix == '/'
|
48
|
+
headers = @default_headers
|
49
|
+
if body
|
50
|
+
headers = headers.merge({'Content-Type' => @link_schema.content_type})
|
51
|
+
body = @link_schema.encode(body)
|
52
|
+
end
|
53
|
+
cache_key = "#{path}:#{headers.hash}"
|
54
|
+
if @link_schema.method == :get
|
55
|
+
etag = @cache["etag:#{cache_key}"]
|
56
|
+
headers = headers.merge({'If-None-Match' => etag}) if etag
|
57
|
+
end
|
58
|
+
|
59
|
+
connection = Excon.new(@root_url, thread_safe_sockets: true)
|
60
|
+
response = connection.request(method: @link_schema.method, path: path,
|
61
|
+
headers: headers, body: body,
|
62
|
+
expects: [200, 201, 202, 204, 206, 304])
|
63
|
+
content_type = response.headers['Content-Type']
|
64
|
+
if response.status == 304
|
65
|
+
MultiJson.load(@cache["data:#{cache_key}"])
|
66
|
+
elsif content_type && content_type =~ /application\/.*json/
|
67
|
+
etag = response.headers['ETag']
|
68
|
+
if etag
|
69
|
+
@cache["etag:#{cache_key}"] = etag
|
70
|
+
@cache["data:#{cache_key}"] = response.body
|
71
|
+
end
|
72
|
+
body = MultiJson.load(response.body)
|
73
|
+
if response.status == 206
|
74
|
+
next_range = response.headers['Next-Range']
|
75
|
+
Enumerator.new do |yielder|
|
76
|
+
while true do
|
77
|
+
# Yield the results we got in the body.
|
78
|
+
body.each do |item|
|
79
|
+
yielder << item
|
80
|
+
end
|
81
|
+
|
82
|
+
# Only make a request to get the next page if we have a valid
|
83
|
+
# next range.
|
84
|
+
break unless next_range
|
85
|
+
headers = headers.merge({'Range' => next_range})
|
86
|
+
response = connection.request(method: @link_schema.method,
|
87
|
+
path: path, headers: headers,
|
88
|
+
expects: [200, 201, 206])
|
89
|
+
body = MultiJson.load(response.body)
|
90
|
+
next_range = response.headers['Next-Range']
|
91
|
+
end
|
92
|
+
end
|
93
|
+
else
|
94
|
+
body
|
95
|
+
end
|
96
|
+
elsif !response.body.empty?
|
97
|
+
response.body
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Unpack the URL and split it into a root URL and a path prefix, if one
|
104
|
+
# exists.
|
105
|
+
#
|
106
|
+
# @param url [String] The complete base URL to use when making requests.
|
107
|
+
# @return [String,String] A (root URL, path) prefix pair.
|
108
|
+
def unpack_url(url)
|
109
|
+
root_url = []
|
110
|
+
path_prefix = ''
|
111
|
+
parts = URI.split(url)
|
112
|
+
root_url << "#{parts[0]}://"
|
113
|
+
root_url << "#{parts[1]}@" unless parts[1].nil?
|
114
|
+
root_url << "#{parts[2]}"
|
115
|
+
root_url << ":#{parts[3]}" unless parts[3].nil?
|
116
|
+
path_prefix = parts[5]
|
117
|
+
return root_url.join(''), path_prefix
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Heroics
|
2
|
+
# Process a name to make it suitable for use as a Ruby method name.
|
3
|
+
#
|
4
|
+
# @param name [String] The name to process.
|
5
|
+
# @return [String] The new name with capitals converted to lowercase, and
|
6
|
+
# dashes and spaces converted to underscores.
|
7
|
+
def self.ruby_name(name)
|
8
|
+
name.downcase.gsub(/[- ]/, '_')
|
9
|
+
end
|
10
|
+
|
11
|
+
# Process a name to make it suitable for use as a pretty command name.
|
12
|
+
#
|
13
|
+
# @param name [String] The name to process.
|
14
|
+
# @return [String] The new name with capitals converted to lowercase, and
|
15
|
+
# underscores and spaces converted to dashes.
|
16
|
+
def self.pretty_name(name)
|
17
|
+
name.downcase.gsub(/[_ ]/, '-')
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Heroics
|
2
|
+
# A resource with methods mapped to API links.
|
3
|
+
class Resource
|
4
|
+
# Instantiate a resource.
|
5
|
+
#
|
6
|
+
# @param links [Hash<String,Link>] A hash that maps method names to links.
|
7
|
+
def initialize(links)
|
8
|
+
@links = links
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Find a link and invoke it.
|
14
|
+
#
|
15
|
+
# @param name [String] The name of the method to invoke.
|
16
|
+
# @param parameters [Array] The arguments to pass to the method. This
|
17
|
+
# should always be a `Hash` mapping parameter names to values.
|
18
|
+
# @raise [NoMethodError] Raised if the name doesn't match a known link.
|
19
|
+
# @return [String,Array,Hash] The response received from the server. JSON
|
20
|
+
# responses are automatically decoded into Ruby objects.
|
21
|
+
def method_missing(name, *parameters)
|
22
|
+
link = @links[name.to_s]
|
23
|
+
if link.nil?
|
24
|
+
address = "<#{self.class.name}:0x00#{(self.object_id << 1).to_s(16)}>"
|
25
|
+
raise NoMethodError.new("undefined method `#{name}' for ##{address}")
|
26
|
+
end
|
27
|
+
link.run(*parameters)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|