cardiac 0.2.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/LICENSE +22 -0
- data/Rakefile +66 -0
- data/cardiac-0.2.0.pre2.gem +0 -0
- data/cardiac.gemspec +48 -0
- data/lib/cardiac/declarations.rb +70 -0
- data/lib/cardiac/errors.rb +65 -0
- data/lib/cardiac/log_subscriber.rb +55 -0
- data/lib/cardiac/model/attributes.rb +146 -0
- data/lib/cardiac/model/base.rb +161 -0
- data/lib/cardiac/model/callbacks.rb +47 -0
- data/lib/cardiac/model/declarations.rb +106 -0
- data/lib/cardiac/model/dirty.rb +117 -0
- data/lib/cardiac/model/locale/en.yml +7 -0
- data/lib/cardiac/model/operations.rb +49 -0
- data/lib/cardiac/model/persistence.rb +171 -0
- data/lib/cardiac/model/querying.rb +129 -0
- data/lib/cardiac/model/validations.rb +124 -0
- data/lib/cardiac/model.rb +17 -0
- data/lib/cardiac/operation_builder.rb +75 -0
- data/lib/cardiac/operation_handler.rb +215 -0
- data/lib/cardiac/railtie.rb +20 -0
- data/lib/cardiac/reflections.rb +85 -0
- data/lib/cardiac/representation.rb +124 -0
- data/lib/cardiac/resource/adapter.rb +178 -0
- data/lib/cardiac/resource/builder.rb +107 -0
- data/lib/cardiac/resource/codec_methods.rb +58 -0
- data/lib/cardiac/resource/config_methods.rb +39 -0
- data/lib/cardiac/resource/extension_methods.rb +115 -0
- data/lib/cardiac/resource/request_methods.rb +138 -0
- data/lib/cardiac/resource/subresource.rb +88 -0
- data/lib/cardiac/resource/uri_methods.rb +176 -0
- data/lib/cardiac/resource.rb +77 -0
- data/lib/cardiac/util.rb +120 -0
- data/lib/cardiac/version.rb +3 -0
- data/lib/cardiac.rb +61 -0
- data/spec/rails-3.2/Gemfile +9 -0
- data/spec/rails-3.2/Gemfile.lock +136 -0
- data/spec/rails-3.2/Rakefile +10 -0
- data/spec/rails-3.2/app_root/app/assets/javascripts/application.js +15 -0
- data/spec/rails-3.2/app_root/app/assets/stylesheets/application.css +13 -0
- data/spec/rails-3.2/app_root/app/controllers/application_controller.rb +3 -0
- data/spec/rails-3.2/app_root/app/helpers/application_helper.rb +2 -0
- data/spec/rails-3.2/app_root/app/views/layouts/application.html.erb +14 -0
- data/spec/rails-3.2/app_root/config/application.rb +29 -0
- data/spec/rails-3.2/app_root/config/boot.rb +13 -0
- data/spec/rails-3.2/app_root/config/database.yml +25 -0
- data/spec/rails-3.2/app_root/config/environment.rb +5 -0
- data/spec/rails-3.2/app_root/config/environments/development.rb +10 -0
- data/spec/rails-3.2/app_root/config/environments/production.rb +11 -0
- data/spec/rails-3.2/app_root/config/environments/test.rb +11 -0
- data/spec/rails-3.2/app_root/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails-3.2/app_root/config/initializers/inflections.rb +15 -0
- data/spec/rails-3.2/app_root/config/initializers/mime_types.rb +5 -0
- data/spec/rails-3.2/app_root/config/initializers/secret_token.rb +7 -0
- data/spec/rails-3.2/app_root/config/initializers/session_store.rb +8 -0
- data/spec/rails-3.2/app_root/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails-3.2/app_root/config/locales/en.yml +5 -0
- data/spec/rails-3.2/app_root/config/routes.rb +2 -0
- data/spec/rails-3.2/app_root/db/test.sqlite3 +0 -0
- data/spec/rails-3.2/app_root/log/test.log +2403 -0
- data/spec/rails-3.2/app_root/public/404.html +26 -0
- data/spec/rails-3.2/app_root/public/422.html +26 -0
- data/spec/rails-3.2/app_root/public/500.html +25 -0
- data/spec/rails-3.2/app_root/public/favicon.ico +0 -0
- data/spec/rails-3.2/app_root/script/rails +6 -0
- data/spec/rails-3.2/spec/spec_helper.rb +25 -0
- data/spec/rails-4.0/Gemfile +9 -0
- data/spec/rails-4.0/Gemfile.lock +132 -0
- data/spec/rails-4.0/Rakefile +10 -0
- data/spec/rails-4.0/app_root/app/assets/javascripts/application.js +15 -0
- data/spec/rails-4.0/app_root/app/assets/stylesheets/application.css +13 -0
- data/spec/rails-4.0/app_root/app/controllers/application_controller.rb +3 -0
- data/spec/rails-4.0/app_root/app/helpers/application_helper.rb +2 -0
- data/spec/rails-4.0/app_root/app/views/layouts/application.html.erb +14 -0
- data/spec/rails-4.0/app_root/config/application.rb +28 -0
- data/spec/rails-4.0/app_root/config/boot.rb +13 -0
- data/spec/rails-4.0/app_root/config/database.yml +25 -0
- data/spec/rails-4.0/app_root/config/environment.rb +5 -0
- data/spec/rails-4.0/app_root/config/environments/development.rb +9 -0
- data/spec/rails-4.0/app_root/config/environments/production.rb +11 -0
- data/spec/rails-4.0/app_root/config/environments/test.rb +10 -0
- data/spec/rails-4.0/app_root/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/rails-4.0/app_root/config/initializers/inflections.rb +15 -0
- data/spec/rails-4.0/app_root/config/initializers/mime_types.rb +5 -0
- data/spec/rails-4.0/app_root/config/initializers/secret_token.rb +7 -0
- data/spec/rails-4.0/app_root/config/initializers/session_store.rb +8 -0
- data/spec/rails-4.0/app_root/config/initializers/wrap_parameters.rb +14 -0
- data/spec/rails-4.0/app_root/config/locales/en.yml +5 -0
- data/spec/rails-4.0/app_root/config/routes.rb +2 -0
- data/spec/rails-4.0/app_root/db/test.sqlite3 +0 -0
- data/spec/rails-4.0/app_root/log/development.log +50 -0
- data/spec/rails-4.0/app_root/log/test.log +2399 -0
- data/spec/rails-4.0/app_root/public/404.html +26 -0
- data/spec/rails-4.0/app_root/public/422.html +26 -0
- data/spec/rails-4.0/app_root/public/500.html +25 -0
- data/spec/rails-4.0/app_root/public/favicon.ico +0 -0
- data/spec/rails-4.0/app_root/script/rails +6 -0
- data/spec/rails-4.0/spec/spec_helper.rb +25 -0
- data/spec/shared/cardiac/declarations_spec.rb +103 -0
- data/spec/shared/cardiac/model/base_spec.rb +446 -0
- data/spec/shared/cardiac/operation_builder_spec.rb +96 -0
- data/spec/shared/cardiac/operation_handler_spec.rb +82 -0
- data/spec/shared/cardiac/representation/reflection_spec.rb +73 -0
- data/spec/shared/cardiac/resource/adapter_spec.rb +83 -0
- data/spec/shared/cardiac/resource/builder_spec.rb +52 -0
- data/spec/shared/cardiac/resource/codec_methods_spec.rb +63 -0
- data/spec/shared/cardiac/resource/config_methods_spec.rb +52 -0
- data/spec/shared/cardiac/resource/extension_methods_spec.rb +215 -0
- data/spec/shared/cardiac/resource/request_methods_spec.rb +186 -0
- data/spec/shared/cardiac/resource/uri_methods_spec.rb +212 -0
- data/spec/shared/support/client_execution.rb +28 -0
- data/spec/spec_helper.rb +24 -0
- metadata +463 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
module Cardiac
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Cardiac::Model finder methods.
|
5
|
+
# Most of this has been "borrowed" from ActiveRecord.
|
6
|
+
module Validations
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include ActiveModel::Validations
|
9
|
+
|
10
|
+
included do
|
11
|
+
|
12
|
+
##
|
13
|
+
# :method: remote_errors_class=
|
14
|
+
# Set this on your class to customize the errors instance to build.
|
15
|
+
# The default value is ::ActiveModel::Errors
|
16
|
+
class_attribute :remote_errors_class
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def create!(attributes = nil, &block)
|
22
|
+
if attributes.is_a?(Array)
|
23
|
+
attributes.collect { |attr| create!(attr, &block) }
|
24
|
+
else
|
25
|
+
object = new(attributes)
|
26
|
+
yield(object) if block_given?
|
27
|
+
object.save!
|
28
|
+
object
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def save(options={})
|
34
|
+
perform_validations(options) && super && remote_errors.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def save!(options={})
|
38
|
+
raise RecordInvalid.new(self) unless perform_validations(options)
|
39
|
+
super
|
40
|
+
ensure
|
41
|
+
raise RecordInvalid.new(self) unless remote_errors.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Runs all the validations within the specified context. Returns +true+ if
|
45
|
+
# no errors are found, +false+ otherwise.
|
46
|
+
#
|
47
|
+
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
|
48
|
+
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
|
49
|
+
#
|
50
|
+
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
|
51
|
+
# some <tt>:on</tt> option will only run in the specified context.
|
52
|
+
def valid?(context = nil)
|
53
|
+
context ||= (new_record? ? :create : :update)
|
54
|
+
output = super(context)
|
55
|
+
errors.empty? && output
|
56
|
+
end
|
57
|
+
|
58
|
+
# Stores the errors returned by the remote, after performing any unpacking/decoding.
|
59
|
+
# To customize the options used to add the error:
|
60
|
+
#
|
61
|
+
# <code>assign_remote_errors(data,options: {foo: :bar})</code>
|
62
|
+
#
|
63
|
+
def assign_remote_errors(data,options={})
|
64
|
+
decode_remote_errors(data,options).each do |key,values|
|
65
|
+
Array.wrap(values).each do |value|
|
66
|
+
remote_errors.add key, value.to_s, *([options[:options]] if Hash===options[:options])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
alias remote_errors= assign_remote_errors
|
72
|
+
|
73
|
+
# Like ActiveModel::Validations#errors, but used for remote errors.
|
74
|
+
def remote_errors
|
75
|
+
@remote_errors ||= (self.class.remote_errors_class || ::ActiveModel::Errors).new(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
# Like ActiveModel::Validations#perform_validations, but also checks remote_errors.
|
81
|
+
#
|
82
|
+
# Remote_errors are not cleared before execution, but you could easily do that in a callback:
|
83
|
+
#
|
84
|
+
# <code>before_validation { remote_errors.clear }</code>
|
85
|
+
#
|
86
|
+
def perform_validations(options={})
|
87
|
+
options[:validate] == false || (valid?(options[:context]) && remote_errors.empty?)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Overridden to unpack remote errors from the data.
|
91
|
+
def decode_remote_attributes(data,options={})
|
92
|
+
self.remote_errors = data.delete('errors')
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
# Decodes errors returned by the remote.
|
97
|
+
#
|
98
|
+
# If the remote did not return a Hash, the data is wrapped in a single key: <code>:base</code>
|
99
|
+
# If no remote errors are present, an empty Hash is returned.
|
100
|
+
def decode_remote_errors(data, options={})
|
101
|
+
data.present? ? (Hash===data ? data : {base: data}) : {}
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Overridden to set @new_record back to true if there are remote errors.
|
107
|
+
def create_record
|
108
|
+
super
|
109
|
+
@new_record ||= ! remote_errors.empty?
|
110
|
+
|
111
|
+
# Return success, if we are no longer a new record.
|
112
|
+
! @new_record
|
113
|
+
end
|
114
|
+
|
115
|
+
# Overridden to set @destroyed back to false if there are remote errors.
|
116
|
+
def delete_record
|
117
|
+
super
|
118
|
+
@destroyed &&= remote_errors.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support/dependencies/autoload'
|
2
|
+
|
3
|
+
module Cardiac
|
4
|
+
module Model
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Attributes
|
8
|
+
autoload :Base
|
9
|
+
autoload :Callbacks
|
10
|
+
autoload :Declarations
|
11
|
+
autoload :Dirty
|
12
|
+
autoload :Operations
|
13
|
+
autoload :Persistence
|
14
|
+
autoload :Querying
|
15
|
+
autoload :Validations
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Cardiac
|
2
|
+
class OperationBuilder < ResourceBuilder
|
3
|
+
attr_writer :klass
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
# Checks if the given HTTP method is allowed.
|
8
|
+
def http_method_allowed?(verb=@base.method_value)
|
9
|
+
@base.allowed_http_methods.include?(verb)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
# Overridden to support :call when a verb is implied.
|
15
|
+
def respond_to_missing?(name,include_private=false)
|
16
|
+
unless name == :call then super else
|
17
|
+
@base.method_value.present? && http_method_allowed?(@base.method_value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Overridden to respond to HTTP verbs.
|
22
|
+
def check_builder_method?(name)
|
23
|
+
super(name) || http_method_allowed?(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Overridden to respond to HTTP verbs.
|
27
|
+
def method_missing name, *args, &block
|
28
|
+
name = @base.send(:build_method) if name == :call
|
29
|
+
if http_method_allowed? name
|
30
|
+
call!(name, *args, &block)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# This builder does not actually perform calls, but does record the HTTP verb.
|
37
|
+
def call!(name, *args, &block)
|
38
|
+
if @base.method_value==name then self else
|
39
|
+
build! :http_method, name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Overridden to assign the :klass.
|
44
|
+
def build!(name, *args, &block)
|
45
|
+
b = super
|
46
|
+
b.klass = @klass
|
47
|
+
b
|
48
|
+
end
|
49
|
+
|
50
|
+
# Overridden to assign the :klass.
|
51
|
+
def extend!(*extensions, &extension_block)
|
52
|
+
b = super
|
53
|
+
b.klass = @klass
|
54
|
+
b
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class OperationProxy < OperationBuilder
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Overridden to actually perform the call and return the payload.
|
63
|
+
def call!(name, *args, &block)
|
64
|
+
built = super
|
65
|
+
resolved = __adapter__.new(@klass, built.to_resource)
|
66
|
+
resolved.call!(*args, &block)
|
67
|
+
resolved.result.payload
|
68
|
+
end
|
69
|
+
|
70
|
+
def __adapter__
|
71
|
+
@__adapter__ ||= ::Cardiac::ResourceAdapter
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'active_support/configurable'
|
2
|
+
require 'active_support/rescuable'
|
3
|
+
require 'active_support/callbacks'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/client'
|
6
|
+
require 'rack/cache'
|
7
|
+
require 'stringio'
|
8
|
+
|
9
|
+
module Cardiac
|
10
|
+
|
11
|
+
# This is what is returned by the OperationHandler.
|
12
|
+
class OperationResult
|
13
|
+
attr_accessor :response, :payload
|
14
|
+
|
15
|
+
def initialize(handled, response, payload=nil)
|
16
|
+
@transmitted = !! handled.transmitted?
|
17
|
+
@completed = !! handled.completed?
|
18
|
+
@aborted = !! handled.aborted?
|
19
|
+
@response = response
|
20
|
+
@payload = payload
|
21
|
+
end
|
22
|
+
|
23
|
+
def transmitted?; @transmitted end
|
24
|
+
def completed?; @completed end
|
25
|
+
def aborted?; @aborted end
|
26
|
+
end
|
27
|
+
|
28
|
+
# A base operation handler.
|
29
|
+
class OperationHandler
|
30
|
+
include ActiveSupport::Configurable
|
31
|
+
include ActiveSupport::Rescuable
|
32
|
+
include ActiveSupport::Callbacks
|
33
|
+
|
34
|
+
rescue_from Errno::ETIMEDOUT, Errno::ECONNREFUSED, with: :service_unavailable
|
35
|
+
rescue_from RequestFailedError, with: :unwrap_client_exception
|
36
|
+
|
37
|
+
config_accessor(:unwrap_client_exceptions) { false }
|
38
|
+
config_accessor(:mock_response_on_connection_error) { true }
|
39
|
+
|
40
|
+
define_callbacks :transmission, :abort, :complete
|
41
|
+
|
42
|
+
DEFAULT_RESPONSE_HANDLER = Proc.new do |response|
|
43
|
+
raise RequestFailedError, response unless response.successful?
|
44
|
+
response
|
45
|
+
end
|
46
|
+
|
47
|
+
class Client < Rack::Client::Simple
|
48
|
+
class SwitchHeaders
|
49
|
+
def initialize(app,match,switch)
|
50
|
+
@app, @match, @switch = app, match, switch
|
51
|
+
end
|
52
|
+
def call(env)
|
53
|
+
status, headers, body = @app.call(env)
|
54
|
+
[status, Hash[ headers.to_a.map{|k,v| [@match===k ? @switch+$' : k, v] }], body]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
use SwitchHeaders, /^X-HideRack-/, 'X-Rack-'
|
59
|
+
use SwitchHeaders, /^X-Rack-/, 'X-Rack-Client-'
|
60
|
+
use Rack::Cache,
|
61
|
+
'rack-cache.ignore_headers' => ['Set-Cookie','X-Content-Digest']
|
62
|
+
use Rack::Head
|
63
|
+
use Rack::ConditionalGet
|
64
|
+
use Rack::ETag
|
65
|
+
use SwitchHeaders, /^X-Rack-/, 'X-HideRack-'
|
66
|
+
|
67
|
+
def self.new
|
68
|
+
super Rack::Client::Handler::NetHTTP
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.request(*args)
|
72
|
+
@instance ||= new
|
73
|
+
@instance.request(*args)
|
74
|
+
end
|
75
|
+
|
76
|
+
def http_user_agent
|
77
|
+
"cardiac #{Cardiac::VERSION} (rack-client #{Rack::Client::VERSION})"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_accessor :verb, :url, :headers, :payload, :options, :response_handler, :result
|
82
|
+
|
83
|
+
def initialize client_options, payload=nil, &response_handler
|
84
|
+
@verb = client_options[:method] or raise InvalidOperationError, 'no HTTP verb was specified'
|
85
|
+
@url = client_options[:url] or raise InvalidOperationError, 'no URL was specified'
|
86
|
+
@headers = client_options[:headers]
|
87
|
+
@payload = payload
|
88
|
+
@options = client_options.except(:method, :url, :headers)
|
89
|
+
@response_handler = response_handler || DEFAULT_RESPONSE_HANDLER
|
90
|
+
end
|
91
|
+
|
92
|
+
def transmit!
|
93
|
+
# Reset any old state before actually performing the transmission.
|
94
|
+
self.result = @aborted = @transmitted = nil
|
95
|
+
|
96
|
+
# Perform the actual request and receive the response.
|
97
|
+
run_callbacks :transmission do
|
98
|
+
self.result = nil
|
99
|
+
begin
|
100
|
+
self.result = @response_handler.call(perform_request)
|
101
|
+
|
102
|
+
# A response was received, so consider it transmitted.
|
103
|
+
@transmitted = true
|
104
|
+
rescue Exception => exception
|
105
|
+
|
106
|
+
# An exception was received, so consider it untransmitted.
|
107
|
+
@transmitted = false
|
108
|
+
|
109
|
+
# The exception may still be handled, to prevent the operation from aborting.
|
110
|
+
abort! exception
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# If we get here, then we must have a result to return.
|
115
|
+
complete!
|
116
|
+
|
117
|
+
ensure
|
118
|
+
# Always clear out our result instance before returning it.
|
119
|
+
self.result = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
# Checks if the request was transmitted and a response was received.
|
123
|
+
def transmitted?
|
124
|
+
@transmitted
|
125
|
+
end
|
126
|
+
|
127
|
+
# Checks if the operation was aborted due to an exception being thrown.
|
128
|
+
# Even though this does not involve the interpretation a response body, an
|
129
|
+
# aborted transmission could instead be considered completed by handling the exception.
|
130
|
+
def aborted?
|
131
|
+
@aborted
|
132
|
+
end
|
133
|
+
|
134
|
+
# Checks if the operation was completed.
|
135
|
+
# This means that either the operation was transmitted, or an exception was handled successfully.
|
136
|
+
def completed?
|
137
|
+
@aborted.nil? ? @transmitted : !@aborted
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
def abort! exception
|
143
|
+
# Start out by assuming we will abort.
|
144
|
+
@aborted = true
|
145
|
+
|
146
|
+
# Now we can run the abort callbacks.
|
147
|
+
run_callbacks :abort do
|
148
|
+
if rescue_with_handler(exception)
|
149
|
+
@aborted = false
|
150
|
+
raise OperationAbortError
|
151
|
+
elsif Exception===result
|
152
|
+
self.result, exception = nil, self.result
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Now we can re-raise the unhandled exception.
|
157
|
+
raise exception
|
158
|
+
|
159
|
+
rescue OperationAbortError => e
|
160
|
+
raise e if exception == e # just in case
|
161
|
+
end
|
162
|
+
|
163
|
+
# Performs the completion of an operation, returning an OperationResult
|
164
|
+
def complete!(response=self.result)
|
165
|
+
run_callbacks :complete do
|
166
|
+
OperationResult.new(self, response)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Customized to only consider an exception handler successful if and only if
|
171
|
+
# it sets a result on this instance that is not itself an Exception.
|
172
|
+
def rescue_with_handler exception
|
173
|
+
if handler = handler_for_rescue(exception)
|
174
|
+
self.result = handler.arity != 0 ? handler.call(exception) : handler.call
|
175
|
+
|
176
|
+
# Fail if the result is missing or is an Exception.
|
177
|
+
result.present? && ! result.is_a?(Exception)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def perform_request
|
184
|
+
Client.request @verb.to_s.upcase, @url.to_s, @headers.try(:stringify_keys) || {}, @payload
|
185
|
+
end
|
186
|
+
|
187
|
+
# Handles RequestFailedError exceptions, optionally unwrapping them.
|
188
|
+
# This is a special case since we must still show that the operation was "transmitted"
|
189
|
+
# even if we will be aborting this operation.
|
190
|
+
def unwrap_client_exception e
|
191
|
+
# If we receive a "wrapped" exception, then a response was definitely received.
|
192
|
+
# However, the operation will still abort unless we are unwrapping client exceptions since
|
193
|
+
# any user-supplied exception handler would have overridden this one.
|
194
|
+
#
|
195
|
+
# Thus, we will set this flag early so that even an unhandled client exception that
|
196
|
+
# aborts the operation will still correctly show that the response was transmitted.
|
197
|
+
@transmitted = true
|
198
|
+
|
199
|
+
# The configuration determines if we should use a non-20x response as a result.
|
200
|
+
self.result = e.response if unwrap_client_exceptions && e.respond_to?(:response)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Handles I/O exceptions and other similar cases that do not involve the HTTP protocol.
|
204
|
+
# Optionally, these conditions could also be made to provide a mock response on a connection error.
|
205
|
+
# This would prevent the operation from aborting but still show that the response was not transmitted.
|
206
|
+
def service_unavailable e
|
207
|
+
self.result = build_mock_response e, '503' if mock_response_on_connection_error
|
208
|
+
end
|
209
|
+
|
210
|
+
# Internal method.
|
211
|
+
def build_mock_response body, code, version='1.0', headers={}
|
212
|
+
Rack::Client::Simple::CollapsedResponse.new code, headers, StringIO.new(body)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'active_attr/railtie'
|
2
|
+
require "cardiac/model/base"
|
3
|
+
require 'cardiac/log_subscriber'
|
4
|
+
|
5
|
+
module Cardiac
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
|
8
|
+
initializer "cardiac.logger" do
|
9
|
+
ActiveSupport.on_load(:cardiac) { self.logger ||= ::Rails.logger }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Make the console output logging to STDERR (unless AR already did it).
|
13
|
+
console do |app|
|
14
|
+
unless defined? ::ActiveRecord::Base
|
15
|
+
console = ActiveSupport::Logger.new(STDERR)
|
16
|
+
Rails.logger.extend ActiveSupport::Logger.broadcast(console)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'cardiac'
|
5
|
+
require 'cardiac/resource'
|
6
|
+
|
7
|
+
module Cardiac
|
8
|
+
|
9
|
+
class BaseReflection
|
10
|
+
attr_reader :macro, :uri, :http_verb, :options
|
11
|
+
alias http_verb? http_verb
|
12
|
+
alias http_method http_verb
|
13
|
+
alias http_method? http_verb?
|
14
|
+
|
15
|
+
def initialize(resource_or_uri, http_verb=nil)
|
16
|
+
@macro, @http_verb = self.class.build_macro, http_verb
|
17
|
+
|
18
|
+
resource_or_uri.to_resource if resource_or_uri.respond_to?(:to_resource)
|
19
|
+
|
20
|
+
case resource_or_uri
|
21
|
+
when Cardiac::Resource
|
22
|
+
@http_verb ||= resource_or_uri.method_value
|
23
|
+
@uri = resource_or_uri.to_uri
|
24
|
+
_options = resource_or_uri.send(:build_client_options, @http_verb)
|
25
|
+
when URI
|
26
|
+
@uri = resource_or_uri.dup
|
27
|
+
_options = {}
|
28
|
+
else
|
29
|
+
@uri = resource_or_uri.to_uri if resource_or_uri.respond_to?(:to_uri)
|
30
|
+
@http_verb ||= resource_or_uri.http_verb if resource_or_uri.respond_to?(:http_verb)
|
31
|
+
@options ||= resource_or_uri.options if resource_or_uri.respond_to?(:options)
|
32
|
+
end
|
33
|
+
@options = _options.dup
|
34
|
+
@options.symbolize_keys!
|
35
|
+
@options[:method] = @http_verb
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_uri
|
39
|
+
@uri.dup
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_url
|
43
|
+
@uri.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_reflection
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.build_macro(name=self.name)
|
53
|
+
name.to_s.demodulize.sub(/Reflection$/,'').underscore.to_sym unless Symbol===name
|
54
|
+
name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ChainReflection
|
59
|
+
attr_reader :macro, :base_reflection, :handler_chain, :block
|
60
|
+
|
61
|
+
def initialize(macro, base, *handler_chain, &block)
|
62
|
+
@macro, @base_reflection, @handler_chain, @block = macro, base, handler_chain, block
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ResourceReflection < BaseReflection
|
67
|
+
attr_reader :adapter_klass, :encoder_reflection, :decoder_reflections
|
68
|
+
|
69
|
+
def initialize(resource, http_verb=nil)
|
70
|
+
super resource, http_verb
|
71
|
+
|
72
|
+
@decoder_reflections = resource.send(:build_decoders).map do |search,handler_chain|
|
73
|
+
ChainReflection.new :decode, Representation::Reflection.new(search), *handler_chain
|
74
|
+
end
|
75
|
+
|
76
|
+
@encoder_reflection = ChainReflection.new :encode, *(resource.send(:build_encoder).tap do |search_and_chain|
|
77
|
+
search_and_chain[0] = Representation::Reflection.new(search_and_chain.first || :url_encoded)
|
78
|
+
end)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class OperationReflection < ResourceReflection
|
83
|
+
attr_reader :handler_klass
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'uri'
|
3
|
+
require 'active_support/core_ext/object/to_param'
|
4
|
+
require 'active_support/core_ext/object/to_query'
|
5
|
+
require 'rack/utils'
|
6
|
+
require 'multi_json'
|
7
|
+
|
8
|
+
module Cardiac
|
9
|
+
module Representation
|
10
|
+
|
11
|
+
# Looking up coders, mimes, etc.
|
12
|
+
module LookupMethods
|
13
|
+
def coder_for(search)
|
14
|
+
search = $1.to_s.classify if search =~ /\.?([a-z][a-z0-9_]*)$/i
|
15
|
+
const_get search.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def mime_types(options={})
|
19
|
+
options[:types] || MIME::Types
|
20
|
+
end
|
21
|
+
|
22
|
+
def mimes_for(search, options={})
|
23
|
+
options = {complete: true, platform: false}.merge!(options)
|
24
|
+
case search
|
25
|
+
when /\.?([^\/\.])$/
|
26
|
+
mime_types(options).of(search.to_s, options[:platform])
|
27
|
+
when Symbol
|
28
|
+
mime_types(options)[search.to_s, options]
|
29
|
+
else
|
30
|
+
mime_types(options)[search, options]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Basic reflection of a representation type.
|
36
|
+
class Reflection < Struct.new(:extension, :types, :default_type, :coder)
|
37
|
+
def initialize(extension,default_type=nil,*extra_types)
|
38
|
+
types = __codecs__.mimes_for(extension) + extra_types
|
39
|
+
if default_type.nil?
|
40
|
+
default_type = types.first
|
41
|
+
elsif ! types.include? default_type
|
42
|
+
types.unshift default_type
|
43
|
+
end
|
44
|
+
super extension.to_sym, types, default_type, __codecs__.coder_for(extension)
|
45
|
+
end
|
46
|
+
|
47
|
+
delegate :coder_for, :mimes_for, to: :__codecs__
|
48
|
+
private :coder_for, :mimes_for
|
49
|
+
|
50
|
+
def matches?(mime_type)
|
51
|
+
types.any?{|type| type.like? mime_type}
|
52
|
+
end
|
53
|
+
|
54
|
+
def __codecs__
|
55
|
+
@__codecs__ ||= ::Cardiac::Representation::Codecs
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module Codecs
|
60
|
+
extend LookupMethods
|
61
|
+
|
62
|
+
module FormEncoded
|
63
|
+
module_function
|
64
|
+
|
65
|
+
def encode(value,options={})
|
66
|
+
String===value ? value : URI.encode_www_form(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
def decode(value,options={})
|
70
|
+
Hash===value ? value.stringify_keys : Hash[URI.decode_www_form(value)]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module UrlEncoded
|
75
|
+
|
76
|
+
# In Ruby 1.9.3, super is not available from module_function(s).
|
77
|
+
# It is simplest to just define all extensions here, then.
|
78
|
+
module Extensions
|
79
|
+
include ::Cardiac::RackUtils
|
80
|
+
include FormEncoded
|
81
|
+
alias encode_form encode
|
82
|
+
alias decode_form decode
|
83
|
+
end
|
84
|
+
|
85
|
+
include Extensions
|
86
|
+
extend Extensions
|
87
|
+
|
88
|
+
module_function
|
89
|
+
|
90
|
+
def encode(value,options={})
|
91
|
+
Hash===value ? build_nested_query(value) : encode_form(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def decode(value,options={})
|
95
|
+
decode_form(value).inject({}) do |params,(key,value)|
|
96
|
+
normalize_params params, key, value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module Json
|
102
|
+
module_function
|
103
|
+
def encode(value,options={})
|
104
|
+
MultiJson.dump(value.as_json(options), options) unless value.kind_of?(String)
|
105
|
+
end
|
106
|
+
|
107
|
+
def decode(value,options={})
|
108
|
+
::ActiveSupport::JSON.decode(value, options)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
module Xml
|
113
|
+
module_function
|
114
|
+
def encode(value,options={})
|
115
|
+
value.to_xml(options)
|
116
|
+
end
|
117
|
+
|
118
|
+
def decode(value,options={})
|
119
|
+
Hash.from_xml(value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|