lhc 0.2.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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/Gemfile +11 -0
  4. data/README.md +116 -0
  5. data/Rakefile +25 -0
  6. data/docs/configuration.md +57 -0
  7. data/docs/exceptions.md +68 -0
  8. data/docs/interceptors.md +90 -0
  9. data/docs/request.md +24 -0
  10. data/docs/response.md +19 -0
  11. data/lhc.gemspec +30 -0
  12. data/lib/lhc.rb +16 -0
  13. data/lib/lhc/concerns/lhc/basic_methods.rb +42 -0
  14. data/lib/lhc/config.rb +45 -0
  15. data/lib/lhc/endpoint.rb +53 -0
  16. data/lib/lhc/error.rb +63 -0
  17. data/lib/lhc/errors/client_error.rb +73 -0
  18. data/lib/lhc/errors/server_error.rb +28 -0
  19. data/lib/lhc/errors/timeout.rb +4 -0
  20. data/lib/lhc/errors/unknown_error.rb +4 -0
  21. data/lib/lhc/interceptor.rb +9 -0
  22. data/lib/lhc/interceptor_processor.rb +24 -0
  23. data/lib/lhc/request.rb +91 -0
  24. data/lib/lhc/response.rb +52 -0
  25. data/lib/lhc/version.rb +3 -0
  26. data/script/ci/build.sh +19 -0
  27. data/spec/basic_methods/delete_spec.rb +34 -0
  28. data/spec/basic_methods/get_spec.rb +37 -0
  29. data/spec/basic_methods/post_spec.rb +42 -0
  30. data/spec/basic_methods/put_spec.rb +48 -0
  31. data/spec/basic_methods/request_spec.rb +19 -0
  32. data/spec/config/endpoints_spec.rb +49 -0
  33. data/spec/config/placeholders_spec.rb +32 -0
  34. data/spec/dummy/README.rdoc +28 -0
  35. data/spec/dummy/Rakefile +6 -0
  36. data/spec/dummy/app/assets/images/.keep +0 -0
  37. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  38. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  39. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  40. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  41. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy/app/mailers/.keep +0 -0
  43. data/spec/dummy/app/models/.keep +0 -0
  44. data/spec/dummy/app/models/concerns/.keep +0 -0
  45. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  46. data/spec/dummy/bin/bundle +3 -0
  47. data/spec/dummy/bin/rails +4 -0
  48. data/spec/dummy/bin/rake +4 -0
  49. data/spec/dummy/config.ru +4 -0
  50. data/spec/dummy/config/application.rb +14 -0
  51. data/spec/dummy/config/boot.rb +5 -0
  52. data/spec/dummy/config/environment.rb +5 -0
  53. data/spec/dummy/config/environments/development.rb +34 -0
  54. data/spec/dummy/config/environments/production.rb +75 -0
  55. data/spec/dummy/config/environments/test.rb +39 -0
  56. data/spec/dummy/config/initializers/assets.rb +8 -0
  57. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  59. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  60. data/spec/dummy/config/initializers/inflections.rb +16 -0
  61. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  62. data/spec/dummy/config/initializers/session_store.rb +3 -0
  63. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  64. data/spec/dummy/config/locales/en.yml +23 -0
  65. data/spec/dummy/config/routes.rb +56 -0
  66. data/spec/dummy/config/secrets.yml +22 -0
  67. data/spec/dummy/lib/assets/.keep +0 -0
  68. data/spec/dummy/log/.keep +0 -0
  69. data/spec/dummy/public/404.html +67 -0
  70. data/spec/dummy/public/422.html +67 -0
  71. data/spec/dummy/public/500.html +66 -0
  72. data/spec/dummy/public/favicon.ico +0 -0
  73. data/spec/endpoint/compile_spec.rb +25 -0
  74. data/spec/endpoint/placeholders_spec.rb +14 -0
  75. data/spec/endpoint/remove_interpolated_params_spec.rb +16 -0
  76. data/spec/error/find_spec.rb +56 -0
  77. data/spec/error/response_spec.rb +17 -0
  78. data/spec/error/timeout_spec.rb +14 -0
  79. data/spec/interceptors/after_request_spec.rb +21 -0
  80. data/spec/interceptors/after_response_spec.rb +42 -0
  81. data/spec/interceptors/before_request_spec.rb +21 -0
  82. data/spec/interceptors/before_response_spec.rb +21 -0
  83. data/spec/interceptors/default_interceptors_spec.rb +15 -0
  84. data/spec/interceptors/define_spec.rb +29 -0
  85. data/spec/interceptors/response_competition_spec.rb +40 -0
  86. data/spec/interceptors/return_response_spec.rb +39 -0
  87. data/spec/rails_helper.rb +4 -0
  88. data/spec/request/error_handling_spec.rb +52 -0
  89. data/spec/request/headers_spec.rb +11 -0
  90. data/spec/request/option_dup_spec.rb +12 -0
  91. data/spec/request/parallel_requests_spec.rb +15 -0
  92. data/spec/request/url_patterns_spec.rb +19 -0
  93. data/spec/response/body_spec.rb +16 -0
  94. data/spec/response/code_spec.rb +16 -0
  95. data/spec/response/data_spec.rb +18 -0
  96. data/spec/response/headers_spec.rb +18 -0
  97. data/spec/response/success_spec.rb +12 -0
  98. data/spec/response/time_spec.rb +16 -0
  99. data/spec/spec_helper.rb +4 -0
  100. data/spec/support/fixtures/json/feedback.json +11 -0
  101. data/spec/support/fixtures/json/feedbacks.json +164 -0
  102. data/spec/support/fixtures/json/localina_content_ad.json +23 -0
  103. data/spec/support/load_json.rb +3 -0
  104. data/spec/support/reset_config.rb +7 -0
  105. data/spec/timeouts/no_signal_spec.rb +13 -0
  106. data/spec/timeouts/timings_spec.rb +59 -0
  107. metadata +313 -0
data/docs/response.md ADDED
@@ -0,0 +1,19 @@
1
+ Response
2
+ ===
3
+
4
+ ```ruby
5
+ response.request #<LHC::Request> the associated request.
6
+
7
+ response.data #<OpenStruct> in case response body contains parsable JSON.
8
+ response.data.something.nested
9
+
10
+ response.body #<String>
11
+
12
+ response.code #<Fixnum>
13
+
14
+ response.headers #<Hash>
15
+
16
+ response.time #<Fixnum> Provides response time in ms.
17
+
18
+ response.timeout? #true|false
19
+ ```
data/lhc.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ # Maintain your gem's version:
4
+ require "lhc/version"
5
+
6
+ # Describe your gem and declare its dependencies:
7
+ Gem::Specification.new do |s|
8
+ s.name = "lhc"
9
+ s.version = LHC::VERSION
10
+ s.authors = ['local.ch']
11
+ s.email = ['ws-operations@local.ch']
12
+ s.homepage = 'https://github.com/local-ch/lhc'
13
+ s.summary = 'LocalHttpServices'
14
+ s.description = 'Rails gem wrapping typhoeus and providing additional features (like interceptors)'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- spec/*`.split("\n")
18
+ s.require_paths = ['lib']
19
+
20
+ s.requirements << 'Ruby >= 1.9.2'
21
+ s.required_ruby_version = '>= 1.9.2'
22
+
23
+ s.add_dependency 'typhoeus'
24
+
25
+ s.add_development_dependency 'rspec-rails', '>= 3.0.0'
26
+ s.add_development_dependency 'rails', '~> 4.1.1'
27
+ s.add_development_dependency 'webmock'
28
+ s.add_development_dependency 'geminabox'
29
+ s.add_development_dependency 'pry'
30
+ end
data/lib/lhc.rb ADDED
@@ -0,0 +1,16 @@
1
+ Dir[File.dirname(__FILE__) + '/lhc/concerns/lhc/*.rb'].sort.each {|file| require file }
2
+
3
+ module LHC
4
+ include BasicMethods
5
+
6
+ def self.config
7
+ LHC::Config.instance
8
+ end
9
+
10
+ def self.configure
11
+ LHC::Config.instance.reset
12
+ yield config
13
+ end
14
+ end
15
+
16
+ Gem.find_files('lhc/**/*.rb').sort.each { |path| require path }
@@ -0,0 +1,42 @@
1
+ require 'active_support'
2
+
3
+ module LHC
4
+
5
+ module BasicMethods
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ def request(options)
11
+ if options.is_a? Array
12
+ parallel_requests(options)
13
+ else
14
+ LHC::Request.new(options).response
15
+ end
16
+ end
17
+
18
+ [:get, :post, :put, :delete].each do |http_method|
19
+ define_method(http_method) do |url, options = {}|
20
+ request(options.merge(
21
+ url: url,
22
+ method: http_method
23
+ ))
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parallel_requests(options)
30
+ hydra = Typhoeus::Hydra.hydra
31
+ requests = []
32
+ options.each do |options|
33
+ request = LHC::Request.new(options, false)
34
+ requests << request
35
+ hydra.queue request.raw
36
+ end
37
+ hydra.run
38
+ requests.map(&:response)
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/lhc/config.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'singleton'
2
+
3
+ class LHC::Config
4
+ include Singleton
5
+
6
+ def initialize
7
+ @endpoints = {}
8
+ @placeholders = {}
9
+ end
10
+
11
+ def endpoint(name, url, options = {})
12
+ name = name.to_sym
13
+ fail 'Endpoint already exists for that name' if @endpoints[name]
14
+ @endpoints[name] = LHC::Endpoint.new(url, options)
15
+ end
16
+
17
+ def endpoints
18
+ @endpoints.dup
19
+ end
20
+
21
+ def placeholder(name, value)
22
+ name = name.to_sym
23
+ fail 'Placeholder already exists for that name' if @placeholders[name]
24
+ @placeholders[name] = value
25
+ end
26
+
27
+ def placeholders
28
+ @placeholders.dup
29
+ end
30
+
31
+ def interceptors
32
+ (@interceptors || []).dup
33
+ end
34
+
35
+ def interceptors=(interceptors)
36
+ fail 'Default interceptors already set and can only be set once' if @interceptors
37
+ @interceptors = interceptors
38
+ end
39
+
40
+ def reset
41
+ @endpoints = {}
42
+ @placeholders = {}
43
+ @interceptors = nil
44
+ end
45
+ end
@@ -0,0 +1,53 @@
1
+ # An endpoint is an url that leads to a backend resource.
2
+ # The url can also be an url-template.
3
+ class LHC::Endpoint
4
+
5
+ PLACEHOLDER = /\:[A-Z,a-z,_,-]+/
6
+
7
+ attr_accessor :url, :options
8
+
9
+ def initialize(url, options = nil)
10
+ self.url = url
11
+ self.options = options
12
+ end
13
+
14
+ def compile(params)
15
+ url.gsub(PLACEHOLDER) do |match|
16
+ replacement = if params.is_a? Proc
17
+ params.call(match)
18
+ else
19
+ find_value(match, params)
20
+ end
21
+ replacement || fail("Compilation incomplete. Unable to find value for #{match.gsub(':', '')}.")
22
+ end
23
+ end
24
+
25
+ # Removes keys from provided params hash
26
+ # when they are used for interpolation.
27
+ def remove_interpolated_params!(params)
28
+ params ||= {}
29
+ removed = {}
30
+ url.scan(PLACEHOLDER) do |match|
31
+ match = match.gsub(/^\:/, '')
32
+ if value = find_value(match, params)
33
+ removed[match.to_sym] = value
34
+ params.delete(match.to_sym)
35
+ end
36
+ end
37
+ removed
38
+ end
39
+
40
+ # Returns all placeholders found in the url-template.
41
+ # They are alphabetically sorted.
42
+ def placeholders
43
+ url.scan(PLACEHOLDER).sort
44
+ end
45
+
46
+ # Find a value for a placeholder either in the configuration
47
+ # or in the provided params.
48
+ def find_value(match, params)
49
+ params ||= {}
50
+ match = match.gsub(/^\:/, '').to_sym
51
+ params[match] || LHC.config.placeholders[match]
52
+ end
53
+ end
data/lib/lhc/error.rb ADDED
@@ -0,0 +1,63 @@
1
+ class LHC::Error < StandardError
2
+
3
+ attr_accessor :response
4
+
5
+ def self.map
6
+ {
7
+ 400 => LHC::BadRequest,
8
+ 401 => LHC::Unauthorized,
9
+ 402 => LHC::PaymentRequired,
10
+ 403 => LHC::Forbidden,
11
+ 404 => LHC::NotFound,
12
+ 405 => LHC::MethodNotAllowed,
13
+ 406 => LHC::NotAcceptable,
14
+ 407 => LHC::ProxyAuthenticationRequired,
15
+ 408 => LHC::RequestTimeout,
16
+ 409 => LHC::Conflict,
17
+ 410 => LHC::Gone,
18
+ 411 => LHC::LengthRequired,
19
+ 412 => LHC::PreconditionFailed,
20
+ 413 => LHC::RequestEntityTooLarge,
21
+ 414 => LHC::RequestUriToLong,
22
+ 415 => LHC::UnsupportedMediaType,
23
+ 416 => LHC::RequestedRangeNotSatisfiable,
24
+ 417 => LHC::ExpectationFailed,
25
+ 422 => LHC::UnprocessableEntity,
26
+ 423 => LHC::Locked,
27
+ 424 => LHC::FailedDependency,
28
+ 426 => LHC::UpgradeRequired,
29
+
30
+ 500 => LHC::InternalServerError,
31
+ 501 => LHC::NotImplemented,
32
+ 502 => LHC::BadGateway,
33
+ 503 => LHC::ServiceUnavailable,
34
+ 504 => LHC::GatewayTimeout,
35
+ 505 => LHC::HttpVersionNotSupported,
36
+ 507 => LHC::InsufficientStorage,
37
+ 510 => LHC::NotExtended
38
+ }
39
+ end
40
+
41
+ def self.find(response)
42
+ return LHC::Timeout if response.timeout?
43
+ status_code = response.code.to_s[0..2].to_i
44
+ error = map[status_code]
45
+ error ||= LHC::UnknownError
46
+ error
47
+ end
48
+
49
+ def initialize(message, response)
50
+ super(message)
51
+ self.response = response
52
+ end
53
+
54
+ def to_s
55
+ request = response.request
56
+ debug = []
57
+ debug << "#{request.method} #{request.url}"
58
+ debug << "Params: #{request.options}"
59
+ debug << "Response Code: #{response.code}"
60
+ debug << response.body
61
+ debug.join("\n")
62
+ end
63
+ end
@@ -0,0 +1,73 @@
1
+ require File.dirname(__FILE__) + '/../error'
2
+
3
+ class LHC::ClientError < LHC::Error
4
+ end
5
+
6
+ class LHC::BadRequest < LHC::ClientError
7
+ end
8
+
9
+ class LHC::Unauthorized < LHC::ClientError
10
+ end
11
+
12
+ class LHC::PaymentRequired < LHC::ClientError
13
+ end
14
+
15
+ class LHC::Forbidden < LHC::ClientError
16
+ end
17
+
18
+ class LHC::Forbidden < LHC::ClientError
19
+ end
20
+
21
+ class LHC::NotFound < LHC::ClientError
22
+ end
23
+
24
+ class LHC::MethodNotAllowed < LHC::ClientError
25
+ end
26
+
27
+ class LHC::NotAcceptable < LHC::ClientError
28
+ end
29
+
30
+ class LHC::ProxyAuthenticationRequired < LHC::ClientError
31
+ end
32
+
33
+ class LHC::RequestTimeout < LHC::ClientError
34
+ end
35
+
36
+ class LHC::Conflict < LHC::ClientError
37
+ end
38
+
39
+ class LHC::Gone < LHC::ClientError
40
+ end
41
+
42
+ class LHC::LengthRequired < LHC::ClientError
43
+ end
44
+
45
+ class LHC::PreconditionFailed < LHC::ClientError
46
+ end
47
+
48
+ class LHC::RequestEntityTooLarge < LHC::ClientError
49
+ end
50
+
51
+ class LHC::RequestUriToLong < LHC::ClientError
52
+ end
53
+
54
+ class LHC::UnsupportedMediaType < LHC::ClientError
55
+ end
56
+
57
+ class LHC::RequestedRangeNotSatisfiable < LHC::ClientError
58
+ end
59
+
60
+ class LHC::ExpectationFailed < LHC::ClientError
61
+ end
62
+
63
+ class LHC::UnprocessableEntity < LHC::ClientError
64
+ end
65
+
66
+ class LHC::Locked < LHC::ClientError
67
+ end
68
+
69
+ class LHC::FailedDependency < LHC::ClientError
70
+ end
71
+
72
+ class LHC::UpgradeRequired < LHC::ClientError
73
+ end
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) + '/../error'
2
+
3
+ class LHC::ServerError < LHC::Error
4
+ end
5
+
6
+ class LHC::InternalServerError < LHC::ServerError
7
+ end
8
+
9
+ class LHC::NotImplemented < LHC::ServerError
10
+ end
11
+
12
+ class LHC::BadGateway < LHC::ServerError
13
+ end
14
+
15
+ class LHC::ServiceUnavailable < LHC::ServerError
16
+ end
17
+
18
+ class LHC::GatewayTimeout < LHC::ServerError
19
+ end
20
+
21
+ class LHC::HttpVersionNotSupported < LHC::ServerError
22
+ end
23
+
24
+ class LHC::InsufficientStorage < LHC::ServerError
25
+ end
26
+
27
+ class LHC::NotExtended < LHC::ServerError
28
+ end
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + '/../error'
2
+
3
+ class LHC::Timeout < LHC::Error
4
+ end
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + '/../error'
2
+
3
+ class LHC::UnknownError < LHC::Error
4
+ end
@@ -0,0 +1,9 @@
1
+ class LHC::Interceptor
2
+
3
+ def before_request(request); end
4
+ def after_request(request); end
5
+
6
+ def before_response(request); end
7
+ def after_response(response); end
8
+
9
+ end
@@ -0,0 +1,24 @@
1
+ # Handles interceptions during the lifecycle of a request
2
+ class LHC::InterceptorProcessor
3
+
4
+ attr_accessor :interceptors
5
+
6
+ # Intitalizes the processor and determines if global or local interceptors are used
7
+ def initialize(target)
8
+ options = target.options if target.is_a? LHC::Request
9
+ options ||= target.request.options if target.is_a? LHC::Response
10
+ self.interceptors = (options[:interceptors] || LHC.config.interceptors).map{ |i| i.new }
11
+ end
12
+
13
+ # Forwards messages to interceptors and handles provided responses.
14
+ def intercept(name, target)
15
+ interceptors.each do |interceptor|
16
+ result = interceptor.send(name, target)
17
+ if result.is_a? LHC::Response
18
+ fail 'Response already set from another interceptor' if @response
19
+ request = target.is_a?(LHC::Request) ? target : target.request
20
+ @response = request.response = result
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ require 'typhoeus'
2
+
3
+ # The request is doing an http-request using typhoeus.
4
+ # It provides functionalities to access and alter request data
5
+ # and it communicates with interceptors.
6
+ class LHC::Request
7
+
8
+ TYPHOEUS_OPTIONS = [:params, :method, :body, :headers]
9
+
10
+ attr_accessor :response, :options, :raw
11
+
12
+ def initialize(options, self_executing = true)
13
+ self.options = options.deep_dup
14
+ use_configured_endpoint!
15
+ generate_url_from_template!
16
+ self.iprocessor = LHC::InterceptorProcessor.new(self)
17
+ self.raw = create_request
18
+ return unless self_executing
19
+ iprocessor.intercept(:before_request, self)
20
+ raw.run if !response
21
+ end
22
+
23
+ def url
24
+ raw.base_url || options[:url]
25
+ end
26
+
27
+ def method
28
+ (raw.options[:method] || options[:method] || :get).to_sym
29
+ end
30
+
31
+ def headers
32
+ raw.options.fetch(:headers, nil) || raw.options[:headers] = {}
33
+ end
34
+
35
+ def params
36
+ raw.options.fetch(:params, nil) || raw.options[:params] = {}
37
+ end
38
+
39
+ private
40
+
41
+ attr_accessor :iprocessor
42
+
43
+ def create_request
44
+ request = Typhoeus::Request.new(options[:url], typhoeusize(options))
45
+ request.on_headers do
46
+ iprocessor.intercept(:after_request, self)
47
+ iprocessor.intercept(:before_response, self)
48
+ end
49
+ request.on_complete { |response| on_complete(response) }
50
+ request
51
+ end
52
+
53
+ def typhoeusize(options)
54
+ options = options.deep_dup
55
+ easy = Ethon::Easy.new
56
+ options.delete(:url)
57
+ options.each do |key, v|
58
+ next if TYPHOEUS_OPTIONS.include? key
59
+ method = "#{key}="
60
+ options.delete key unless easy.respond_to?(method)
61
+ end
62
+ options
63
+ end
64
+
65
+ # Get configured endpoint and use it for doing the request.
66
+ # Explicit request options are overriding configured options.
67
+ def use_configured_endpoint!
68
+ return unless (endpoint = LHC.config.endpoints[options[:url]])
69
+ endpoint.options.deep_merge!(options)
70
+ options.deep_merge!(endpoint.options)
71
+ options[:url] = endpoint.url
72
+ end
73
+
74
+ # Generates URL from a URL template
75
+ def generate_url_from_template!
76
+ endpoint = LHC::Endpoint.new(options[:url])
77
+ options[:url] = endpoint.compile(options[:params])
78
+ endpoint.remove_interpolated_params!(options[:params])
79
+ end
80
+
81
+ def on_complete(response)
82
+ self.response ||= LHC::Response.new(response, self)
83
+ iprocessor.intercept(:after_response, self.response)
84
+ throw_error unless self.response.success?
85
+ end
86
+
87
+ def throw_error
88
+ error = LHC::Error.find(response)
89
+ fail error.new(error, response)
90
+ end
91
+ end