komoju 0.0.0 → 0.0.3
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.
- 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
|