stacker_bee 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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