stacker_bee 1.0.0

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +7 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +112 -0
  9. data/Rakefile +6 -0
  10. data/bin/stacker_bee +97 -0
  11. data/config.default.yml +3 -0
  12. data/config/4.2.json +67126 -0
  13. data/lib/stacker_bee.rb +16 -0
  14. data/lib/stacker_bee/api.rb +44 -0
  15. data/lib/stacker_bee/body_parser.rb +23 -0
  16. data/lib/stacker_bee/client.rb +105 -0
  17. data/lib/stacker_bee/configuration.rb +6 -0
  18. data/lib/stacker_bee/connection.rb +31 -0
  19. data/lib/stacker_bee/middleware/logger.rb +48 -0
  20. data/lib/stacker_bee/middleware/signed_query.rb +29 -0
  21. data/lib/stacker_bee/rash.rb +95 -0
  22. data/lib/stacker_bee/request.rb +41 -0
  23. data/lib/stacker_bee/request_error.rb +39 -0
  24. data/lib/stacker_bee/response.rb +29 -0
  25. data/lib/stacker_bee/utilities.rb +18 -0
  26. data/lib/stacker_bee/version.rb +3 -0
  27. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/.yml +33 -0
  28. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_nil_request_parameter/properly_executes_the_request.yml +33 -0
  29. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_an_Array/.yml +35 -0
  30. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_and_empty_string/properly_executes_the_request.yml +33 -0
  31. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/.yml +37 -0
  32. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/should_log_response_as_error.yml +37 -0
  33. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first/.yml +33 -0
  34. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/.yml +33 -0
  35. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_account_type_/.yml +33 -0
  36. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_accounttype_/.yml +33 -0
  37. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_log_request.yml +33 -0
  38. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_not_log_response_as_error.yml +33 -0
  39. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/space_character_in_a_request_parameter/properly_signs_the_request.yml +32 -0
  40. data/spec/fixtures/4.2.json +67126 -0
  41. data/spec/fixtures/simple.json +871 -0
  42. data/spec/integration/request_spec.rb +116 -0
  43. data/spec/spec_helper.rb +58 -0
  44. data/spec/units/stacker_bee/api_spec.rb +24 -0
  45. data/spec/units/stacker_bee/client_spec.rb +181 -0
  46. data/spec/units/stacker_bee/configuration_spec.rb +7 -0
  47. data/spec/units/stacker_bee/connection_spec.rb +45 -0
  48. data/spec/units/stacker_bee/middleware/logger_spec.rb +55 -0
  49. data/spec/units/stacker_bee/rash_spec.rb +87 -0
  50. data/spec/units/stacker_bee/request_error_spec.rb +44 -0
  51. data/spec/units/stacker_bee/request_spec.rb +49 -0
  52. data/spec/units/stacker_bee/response_spec.rb +79 -0
  53. data/spec/units/stacker_bee/utilities_spec.rb +25 -0
  54. data/spec/units/stacker_bee_spec.rb +6 -0
  55. data/stacker_bee.gemspec +30 -0
  56. metadata +225 -0
@@ -0,0 +1,16 @@
1
+ require_stacker_bee = if defined?(require_relative)
2
+ lambda do |path|
3
+ require_relative path
4
+ end
5
+ else # for 1.8.7
6
+ lambda do |path|
7
+ require "stacker_bee/#{path}"
8
+ end
9
+ end
10
+
11
+ %w(
12
+ version
13
+ client
14
+ ).each do |file_name|
15
+ require_stacker_bee["stacker_bee/#{file_name}"]
16
+ end
@@ -0,0 +1,44 @@
1
+ require "multi_json"
2
+ require "stacker_bee/utilities"
3
+
4
+ module StackerBee
5
+ class API
6
+ include Utilities
7
+
8
+ attr_accessor :api_path
9
+
10
+ def initialize(attrs = {})
11
+ attrs.each_pair do |key, value|
12
+ setter = "#{key}="
13
+ send(setter, value)
14
+ end
15
+ end
16
+
17
+ def [](key)
18
+ endpoints[uncase(key)]
19
+ end
20
+
21
+ def key?(key)
22
+ endpoints.key? uncase(key)
23
+ end
24
+
25
+ protected
26
+
27
+ def endpoints
28
+ @endpoints ||= read_endpoints
29
+ end
30
+
31
+ def read_endpoints
32
+ return unless api_path
33
+ json = File.read(api_path)
34
+ response = MultiJson.load(json)
35
+ apis_by_endpoint(response)
36
+ end
37
+
38
+ def apis_by_endpoint(response)
39
+ response["listapisresponse"]["api"].each_with_object({}) do |api, memo|
40
+ memo[uncase(api["name"])] = api
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ require "multi_json"
2
+ require "stacker_bee/rash"
3
+
4
+ module StackerBee
5
+ module BodyParser
6
+ attr_reader :body
7
+
8
+ def body=(raw_response)
9
+ @body = parse(raw_response.body)
10
+ end
11
+
12
+ def parse(json)
13
+ parsed = MultiJson.load(json)
14
+ fail "Cannot determine response key in #{parsed.keys}" if parsed.size > 1
15
+ case value = parsed.values.first
16
+ when Hash then Rash.new(value)
17
+ when Array then value.map { |item| Rash.new(item) }
18
+ else
19
+ value
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,105 @@
1
+ require "forwardable"
2
+ require "stacker_bee/configuration"
3
+ require "stacker_bee/api"
4
+ require "stacker_bee/connection"
5
+ require "stacker_bee/request"
6
+ require "stacker_bee/response"
7
+
8
+ module StackerBee
9
+ class Client
10
+ DEFAULT_API_PATH = File.join(
11
+ File.dirname(__FILE__), '../../config/4.2.json'
12
+ )
13
+
14
+ extend Forwardable
15
+ def_delegators :configuration,
16
+ :logger,
17
+ :logger=,
18
+ :url,
19
+ :url=,
20
+ :api_key,
21
+ :api_key=,
22
+ :secret_key,
23
+ :secret_key=
24
+
25
+ class << self
26
+
27
+ def reset!
28
+ @api, @api_path, @default_config = nil
29
+ end
30
+
31
+ def default_config
32
+ @default_config ||= {
33
+ allow_empty_string_params: false
34
+ }
35
+ end
36
+
37
+ def configuration=(config_hash)
38
+ default_config.merge!(config_hash)
39
+ end
40
+
41
+ def api_path
42
+ @api_path ||= DEFAULT_API_PATH
43
+ end
44
+
45
+ def api_path=(new_api_path)
46
+ return @api_path if @api_path == new_api_path
47
+ @api = nil
48
+ @api_path = new_api_path
49
+ end
50
+
51
+ def api
52
+ @api ||= API.new(api_path: api_path)
53
+ end
54
+ end
55
+
56
+ def initialize(config = {})
57
+ self.configuration = config
58
+ end
59
+
60
+ def configuration=(config)
61
+ @configuration = configuration_with_defaults(config)
62
+ end
63
+
64
+ def configuration
65
+ @configuration ||= configuration_with_defaults
66
+ end
67
+
68
+ def request(endpoint_name, params = {})
69
+ request = Request.new(endpoint_for(endpoint_name), api_key, params)
70
+ request.allow_empty_string_params =
71
+ configuration.allow_empty_string_params
72
+ raw_response = connection.get(request)
73
+ Response.new(raw_response)
74
+ end
75
+
76
+ def endpoint_for(name)
77
+ api = self.class.api[name]
78
+ api && api["name"]
79
+ end
80
+
81
+ def method_missing(name, *args, &block)
82
+ endpoint = endpoint_for(name)
83
+ if endpoint
84
+ request(endpoint, *args, &block)
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def respond_to?(name, include_private = false)
91
+ self.class.api.key?(name) || super
92
+ end
93
+
94
+ protected
95
+
96
+ def connection
97
+ @connection ||= Connection.new(configuration)
98
+ end
99
+
100
+ def configuration_with_defaults(config = {})
101
+ config_hash = self.class.default_config.merge(config)
102
+ Configuration.new(config_hash)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,6 @@
1
+ require "ostruct"
2
+
3
+ module StackerBee
4
+ class Configuration < OpenStruct
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ require "faraday"
2
+ require "uri"
3
+ require "stacker_bee/middleware/signed_query"
4
+ require "stacker_bee/middleware/logger"
5
+
6
+ module StackerBee
7
+ class ConnectionError < Exception
8
+ end
9
+
10
+ class Connection
11
+ attr_accessor :configuration
12
+ def initialize(configuration)
13
+ @configuration = configuration
14
+ uri = URI.parse(self.configuration.url)
15
+ @path = uri.path
16
+ uri.path = ''
17
+ fail ConnectionError, "no protocol specified" unless uri.scheme
18
+ @faraday = Faraday.new(url: uri.to_s) do |faraday|
19
+ faraday.use Middleware::SignedQuery, self.configuration.secret_key
20
+ faraday.use Middleware::Logger, self.configuration.logger
21
+ faraday.adapter Faraday.default_adapter # Net::HTTP
22
+ end
23
+ end
24
+
25
+ def get(request)
26
+ @faraday.get(@path, request.query_params)
27
+ rescue Faraday::Error::ConnectionFailed
28
+ raise ConnectionError, "Failed to connect to #{configuration.url}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+ require 'pp'
4
+
5
+ module StackerBee
6
+ module Middleware
7
+ class Logger < Faraday::Response::Middleware
8
+ extend Forwardable
9
+ PROGNAME = "StackerBee"
10
+
11
+ attr_accessor :logger
12
+
13
+ def initialize(app, _logger = nil)
14
+ super(app)
15
+ self.logger = _logger
16
+ logger.progname ||= PROGNAME
17
+ end
18
+
19
+ def logger
20
+ @logger ||= ::Logger.new($stdout)
21
+ end
22
+
23
+ def_delegators :logger, :debug, :info, :warn, :error, :fatal
24
+
25
+ def call(env)
26
+ log_request(env)
27
+ super
28
+ end
29
+
30
+ def on_complete(env)
31
+ log_response(env)
32
+ end
33
+
34
+ def log_request(env)
35
+ info "#{env[:method]} #{env[:url]}"
36
+ debug env[:request_headers].pretty_inspect
37
+ end
38
+
39
+ def log_response(env)
40
+ status_message = "Status: #{env[:status]}"
41
+ env[:status] < 400 ? info(status_message) : error(status_message)
42
+ debug env[:response_headers].pretty_inspect
43
+ debug env[:body]
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ require "faraday"
2
+ require "base64"
3
+
4
+ module StackerBee
5
+ module Middleware
6
+ class SignedQuery < Faraday::Middleware
7
+ def initialize(app, key)
8
+ @key = key
9
+ fail "Key cannot be nil" unless @key
10
+ super(app)
11
+ end
12
+
13
+ def call(env)
14
+ sign_uri(env[:url])
15
+ @app.call(env)
16
+ end
17
+
18
+ def sign_uri(uri)
19
+ downcased = uri.query.downcase
20
+ nonplussed = downcased.gsub('+', '%20')
21
+ signed = OpenSSL::HMAC.digest 'sha1', @key, nonplussed
22
+ encoded = Base64.encode64(signed).chomp
23
+ escaped = CGI.escape(encoded)
24
+
25
+ uri.query << "&signature=#{escaped}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,95 @@
1
+ require 'forwardable'
2
+ require "stacker_bee/utilities"
3
+
4
+ module StackerBee
5
+ class Rash
6
+ extend Forwardable
7
+ include Utilities
8
+
9
+ def_delegators :@hash, *[
10
+ :default, :default_proc, :each_value, :empty?, :has_value?, :hash,
11
+ :length, :size, :value?, :values, :assoc, :each, :each_key, :each_pair,
12
+ :flatten, :invert, :keys, :key, :merge, :rassoc, :to_a, :to_h, :to_hash
13
+ ]
14
+
15
+ def initialize(hash = {})
16
+ @hash = {}
17
+ hash.each_pair do |key, value|
18
+ @hash[convert_key(key)] = convert_value(value)
19
+ end
20
+ @hash.freeze
21
+ end
22
+
23
+ def ==(other)
24
+ case other
25
+ when Rash
26
+ super || @hash == other.to_hash
27
+ when Hash
28
+ self == Rash.new(other)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def select(*args, &block)
35
+ Rash.new @hash.select(*args, &block)
36
+ end
37
+
38
+ def reject(*args, &block)
39
+ Rash.new @hash.reject(*args, &block)
40
+ end
41
+
42
+ def values_at(*keys)
43
+ @hash.values_at(*keys.map { |key| convert_key(key) })
44
+ end
45
+
46
+ def fetch(key, *args, &block)
47
+ @hash.fetch(convert_key(key), *args, &block)
48
+ end
49
+
50
+ def [](key)
51
+ @hash[convert_key(key)]
52
+ end
53
+
54
+ def key?(key)
55
+ @hash.key?(convert_key(key))
56
+ end
57
+ alias_method :include?, :key?
58
+ alias_method :has_key?, :key?
59
+ alias_method :member?, :key?
60
+
61
+ def to_hash
62
+ self.class.deep_dup(@hash)
63
+ end
64
+
65
+ def inspect
66
+ "#<#{self.class} #{@hash}>"
67
+ end
68
+ alias_method :to_s, :inspect
69
+
70
+ protected
71
+
72
+ def self.deep_dup(hash)
73
+ hash.dup.tap do |duplicate|
74
+ duplicate.each_pair do |key, value|
75
+ duplicate[key] = deep_dup(value) if value.is_a?(Hash)
76
+ end
77
+ end
78
+ end
79
+
80
+ def convert_key(key)
81
+ key.kind_of?(Numeric) ? key : uncase(key)
82
+ end
83
+
84
+ def convert_value(value)
85
+ case value
86
+ when Hash
87
+ Rash.new(value)
88
+ when Array
89
+ value.map { |item| convert_value(item) }
90
+ else
91
+ value
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,41 @@
1
+ require "stacker_bee/utilities"
2
+
3
+ module StackerBee
4
+ class Request
5
+ include Utilities
6
+
7
+ RESPONSE_TYPE = "json"
8
+
9
+ attr_accessor :params
10
+ attr_writer :allow_empty_string_params
11
+
12
+ def initialize(endpoint, api_key, params = {})
13
+ params[:api_key] = api_key
14
+ params[:command] = endpoint
15
+ params[:response] = RESPONSE_TYPE
16
+ self.params = params
17
+ end
18
+
19
+ def query_params
20
+ params
21
+ .reject { |key, val| val.nil? }
22
+ .reject { |key, val| !allow_empty_string_params && val == '' }
23
+ .sort
24
+ .map { |(key, val)| [cloud_stack_key(key), cloud_stack_value(val)] }
25
+ end
26
+
27
+ def allow_empty_string_params
28
+ @allow_empty_string_params ||= false
29
+ end
30
+
31
+ private
32
+
33
+ def cloud_stack_key(key)
34
+ camel_case(key, true)
35
+ end
36
+
37
+ def cloud_stack_value(value)
38
+ value.respond_to?(:join) ? value.join(',') : value
39
+ end
40
+ end
41
+ end