cardiac 0.2.0.pre2
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 +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,178 @@
|
|
|
1
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
|
2
|
+
require 'active_support/callbacks'
|
|
3
|
+
require 'active_support/configurable'
|
|
4
|
+
require 'active_support/rescuable'
|
|
5
|
+
|
|
6
|
+
module Cardiac
|
|
7
|
+
|
|
8
|
+
# An adapter for performing operations on a resource.
|
|
9
|
+
class ResourceAdapter
|
|
10
|
+
include ::ActiveSupport::Callbacks
|
|
11
|
+
include ::Cardiac::Representation::LookupMethods
|
|
12
|
+
|
|
13
|
+
define_callbacks :resolve, :prepare, :encode, :execute, :decode
|
|
14
|
+
|
|
15
|
+
attr_accessor :klass, :resource, :payload, :result
|
|
16
|
+
|
|
17
|
+
delegate :request_has_body?, :response_has_body?, to: :resource
|
|
18
|
+
delegate :encoder_reflection, :decoder_reflections, to: '@reflection'
|
|
19
|
+
delegate :transmitted?, :aborted?, :completed?, :response, to: :result, allow_nil: true
|
|
20
|
+
|
|
21
|
+
# Use instrumentation to perform logging.
|
|
22
|
+
# @see ActiveSupport::Notifications
|
|
23
|
+
delegate :instrumenter, to: '::ActiveSupport::Notifications'
|
|
24
|
+
delegate :logger, to: '::Cardiac::Model::Base'
|
|
25
|
+
|
|
26
|
+
def initialize(klass,base,payload=nil)
|
|
27
|
+
@klass = klass
|
|
28
|
+
@reflection = base.to_reflection if base.respond_to?(:to_reflection)
|
|
29
|
+
run_callbacks :resolve do
|
|
30
|
+
@resource = base.to_resource if base.respond_to?(:to_resource)
|
|
31
|
+
end
|
|
32
|
+
@reflection ||= @resource.to_reflection if resolved?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def __client_options__
|
|
36
|
+
if resolved?
|
|
37
|
+
@__client_options__ ||= resource.send(:build_client_options).tap do |h|
|
|
38
|
+
h = (h[:headers] ||= {})
|
|
39
|
+
|
|
40
|
+
# Content-Type
|
|
41
|
+
if content_type = h.delete(:content_type).presence
|
|
42
|
+
content_type = mimes_for(content_type).first
|
|
43
|
+
else
|
|
44
|
+
content_type = encoder_reflection.base_reflection.default_type
|
|
45
|
+
end
|
|
46
|
+
h['content_type'] = content_type.try(:content_type) || 'application/x-www-form-urlencoded'
|
|
47
|
+
|
|
48
|
+
# Accept
|
|
49
|
+
if accept = h.delete(:accepts).presence and Array===accept
|
|
50
|
+
accept = accept.map{|ext| mimes_for(ext.to_s.strip).first }
|
|
51
|
+
else
|
|
52
|
+
accept = decoder_reflections.map{|dr| dr.base_reflection.default_type }.compact
|
|
53
|
+
end
|
|
54
|
+
h['accept'] = accept.empty? ? '*/*; q=0.5, application/json' : accept.join('; ')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Convenience method to return the current HTTP verb
|
|
60
|
+
def http_verb
|
|
61
|
+
if defined? @__client_options__
|
|
62
|
+
@__client_options__[:method].to_s.upcase
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Performs a remote call by performing the remaining phases in the lifecycle of this adapter.
|
|
67
|
+
def call! *arguments, &block
|
|
68
|
+
self.result = nil
|
|
69
|
+
|
|
70
|
+
resolved? or raise UnresolvableResourceError
|
|
71
|
+
prepared? or prepare! or raise InvalidOperationError
|
|
72
|
+
encode! *arguments
|
|
73
|
+
execute!
|
|
74
|
+
ensure
|
|
75
|
+
decode! if completed?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def resolved?
|
|
79
|
+
resource.present?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def prepared?
|
|
83
|
+
__client_options__.present?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
protected
|
|
87
|
+
|
|
88
|
+
def prepare! verb=nil
|
|
89
|
+
run_callbacks :prepare do
|
|
90
|
+
self.resource = resource.http_method(verb) if verb
|
|
91
|
+
end
|
|
92
|
+
__client_options__.symbolize_keys!
|
|
93
|
+
prepared?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def encode! *arguments
|
|
97
|
+
|
|
98
|
+
# Allow the payload to be overridden by a single argument.
|
|
99
|
+
if arguments.length == 1
|
|
100
|
+
self.payload = arguments.first
|
|
101
|
+
elsif arguments.length > 1
|
|
102
|
+
raise ArgumentError, "wrong number of arguments (#{arguments.length} for 0..1)"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build the remaining portion of the operation using the given payload.
|
|
106
|
+
if request_has_body?
|
|
107
|
+
raise InvalidOperationError, "#{http_verb} requires a payload" if payload.nil?
|
|
108
|
+
run_callbacks :encode do
|
|
109
|
+
self.payload = encoder_reflection.base_reflection.coder.encode(payload)
|
|
110
|
+
end
|
|
111
|
+
elsif payload.present?
|
|
112
|
+
raise InvalidOperationError, "#{http_verb} does not support a payload"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def execute!
|
|
117
|
+
event = event_attributes
|
|
118
|
+
instrumenter.instrument "operation.cardiac", event do
|
|
119
|
+
run_callbacks :execute do
|
|
120
|
+
handler = __handler__.new(__client_options__, payload, &__client_handler__)
|
|
121
|
+
self.result = handler.transmit!
|
|
122
|
+
event[:response_headers] = result.response.headers if result && result.response
|
|
123
|
+
completed?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
rescue => e
|
|
127
|
+
message = "#{e.class.name}: #{e.message}: #{resource.to_url}"
|
|
128
|
+
logger.error message if logger
|
|
129
|
+
raise e
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def decode! response=self.response
|
|
133
|
+
return unless response_has_body?
|
|
134
|
+
|
|
135
|
+
unless content_type = response.content_type.presence
|
|
136
|
+
raise ProtocolError, 'missing Content-type in response'
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
unless decoder = decoder_reflections.find{|dr| dr.base_reflection.matches?(content_type) }
|
|
140
|
+
raise ResourceError, "no decoder for #{content_type.inspect} response"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
run_callbacks :decode do
|
|
144
|
+
result.payload = decoder.base_reflection.coder.decode(response.body.to_s)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def model_name
|
|
151
|
+
__klass_get(:model_name).try(:to_s)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def event_attributes
|
|
155
|
+
h = { name: model_name, verb: http_verb, url: resource.to_url, payload: payload }
|
|
156
|
+
ctx = __klass_get :operation_context
|
|
157
|
+
ctx = { context: ctx } unless ctx.present? && Hash===ctx
|
|
158
|
+
h.reverse_merge! ctx if ctx
|
|
159
|
+
h.keep_if{|key,value| key==:verb || key==:url || value.present? }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def __codecs__
|
|
163
|
+
@__codecs__ ||= ::Cardiac::Representation::Codecs
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def __handler__
|
|
167
|
+
@__handler__ ||= ::Cardiac::OperationHandler
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def __client_handler__
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def __klass_get method_name
|
|
174
|
+
@klass.public_send(method_name) if @klass && @klass.respond_to?(method_name, false)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Cardiac
|
|
2
|
+
class ResourceBuilder < Proxy
|
|
3
|
+
|
|
4
|
+
def initialize base, *extensions, &extension_block
|
|
5
|
+
raise ArgumentError unless Resource === base
|
|
6
|
+
@base, @extensions, @extension_block = base, extensions, extension_block
|
|
7
|
+
@extensions.compact!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns a copy of our subresource.
|
|
11
|
+
def to_resource
|
|
12
|
+
__subresource__.dup
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Resolves this builder's extension module, and extends the builder with it.
|
|
16
|
+
def __extension_module__
|
|
17
|
+
@__extension_module__ ||= build_extensions_for_module!{|mod| __extend__ mod }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Resolves this builder's extensions.
|
|
21
|
+
def __extensions__
|
|
22
|
+
@__extensions__ ||= @extensions.dup.tap do |exts|
|
|
23
|
+
exts.unshift @base.__extension_module__ if @base.__extension_module__
|
|
24
|
+
exts.push ::Module.new(&@extension_block) if @extension_block
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Resolves this builder to a subresource.
|
|
29
|
+
def __subresource__
|
|
30
|
+
@__subresource__ ||= Subresource.new(@base.to_resource)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extending(*extensions,&extension_block)
|
|
34
|
+
extend! extensions, &extension_block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
# Includes this builder's extensions into the given module, returning that module.
|
|
40
|
+
# If no module is supplied, then a new module will be built.
|
|
41
|
+
def build_extensions_for_module!(mod=nil,&block)
|
|
42
|
+
if __extensions__.any?
|
|
43
|
+
mod ||= ::Module.new
|
|
44
|
+
mod.send :include, *__extensions__
|
|
45
|
+
block.call mod if block
|
|
46
|
+
end
|
|
47
|
+
mod
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def method_missing name, *args, &block
|
|
53
|
+
name = name.to_sym
|
|
54
|
+
|
|
55
|
+
# Always delegate builder methods.
|
|
56
|
+
if check_builder_method? name
|
|
57
|
+
build! name, *args, &block
|
|
58
|
+
|
|
59
|
+
# Only delegate extension methods if the extension module has not been built yet.
|
|
60
|
+
# This allows extensions to be removed by preventing method_missing from calling them.
|
|
61
|
+
elsif @__extension_module__.nil? && check_extension_method?(name)
|
|
62
|
+
#__extension_module__.instance_method(name).bind(self).call(*args, &block)
|
|
63
|
+
__public_send__(name, *args, &block) || self
|
|
64
|
+
|
|
65
|
+
# Otherwise, the method has not been implemented.
|
|
66
|
+
else
|
|
67
|
+
raise ::NotImplementedError, "#{name.inspect} is not implemented for this builder"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Clones our subresource, calls it with the given args, then returns a new builder for it.
|
|
72
|
+
def build! name, *args, &block
|
|
73
|
+
subr = to_resource
|
|
74
|
+
subr.send name, *args, &block
|
|
75
|
+
__class__.new subr, *(@extensions+[subr.__extension_module__])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Clones our subresource, extends it with the given extensions, then creates a new builder with an extension block
|
|
79
|
+
def extend! extensions, &extension_block
|
|
80
|
+
subr = to_resource.extending(*extensions)
|
|
81
|
+
subr = DeclarationBuilder.new(subr).extension_exec(&extension_block) if extension_block
|
|
82
|
+
__class__.new subr, *(@extensions+[subr.__extension_module__])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Checks if this builder can respond to a symbol.
|
|
86
|
+
def respond_to_missing?(name, include_private=false)
|
|
87
|
+
check_builder_method?(name) || check_extension_method?(name)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Checks if the given builder method is allowed.
|
|
91
|
+
def check_builder_method? name
|
|
92
|
+
name = name.to_sym
|
|
93
|
+
name!=:subresource && name!=:operation && (@base.allowed_builder_methods.include?(name))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Checks if the given extension method is allowed.
|
|
97
|
+
def check_extension_method? name
|
|
98
|
+
__extension_module__.method_defined? name if __extensions__.any?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# def method_added name, &block
|
|
102
|
+
# @extensions.send :define_method, name, &block
|
|
103
|
+
# remove_method name if method_defined? name
|
|
104
|
+
# end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Cardiac
|
|
2
|
+
module CodecMethods
|
|
3
|
+
DEFAULT_DECODERS = [:url_encoded, :xml, :json].freeze
|
|
4
|
+
|
|
5
|
+
# Representation decoder selection and customization.
|
|
6
|
+
def decoders(search,*rest,&handler) self.decoders_values += check_decoders(rest.unshift(search),handler) ; self end
|
|
7
|
+
def reset_decoders(*rest,&handler) decoders_values.replace check_decoders(rest,handler) ; self end
|
|
8
|
+
|
|
9
|
+
# Representation encoder selection and customization.
|
|
10
|
+
def encoder(search, &handler)
|
|
11
|
+
self.encoder_search_value = search.presence or raise ArgumentError
|
|
12
|
+
self.encoder_handler_value = handler
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
# Delegates mime type lookups to Representation::Codecs.mimes_for
|
|
19
|
+
def lookup_type(search,options={})
|
|
20
|
+
::Cardiac::Representation::Codecs.mimes_for(search,options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_encoder(*previous)
|
|
24
|
+
previous << encoder_handler_value if encoder_handler_value
|
|
25
|
+
[encoder_search_value, previous]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds a hash of decoder chains, keyed by symbol.
|
|
29
|
+
#
|
|
30
|
+
# In the decoder_values, zero or more symbols will precede each Proc in the chain,
|
|
31
|
+
# specifying which decoder(s) it applies to. If no symbols precede the Proc, then it will
|
|
32
|
+
# be applicable to ALL decoders, even subsequently specified ones. The order of
|
|
33
|
+
# Proc objects in the chain are preserved, regardless of which decoders they are applicable to.
|
|
34
|
+
#
|
|
35
|
+
def build_decoders(base_decoders=DEFAULT_DECODERS, base_handler=nil)
|
|
36
|
+
all_chain, decoders = [], {}
|
|
37
|
+
decoders_values.each do |value|
|
|
38
|
+
case value
|
|
39
|
+
when Proc
|
|
40
|
+
decoders.each_value{|chain| chain << value }
|
|
41
|
+
all_chain << value
|
|
42
|
+
when Symbol
|
|
43
|
+
decoders[value] = all_chain.dup
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
base_decoders.each{|decoder| decoders[decoder] = all_chain.dup } if decoders.empty?
|
|
47
|
+
decoders
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def check_decoders(decoders,handler=nil)
|
|
53
|
+
raise ArgumentError unless decoders.all?{|k| Symbol===k }
|
|
54
|
+
decoders << handler if handler
|
|
55
|
+
decoders
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
require 'active_support/configurable'
|
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
|
|
6
|
+
module Cardiac
|
|
7
|
+
module ConfigMethods
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
include ActiveSupport::Configurable
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
config_accessor :default_options
|
|
14
|
+
self.default_options = {}.with_indifferent_access
|
|
15
|
+
|
|
16
|
+
config_accessor :allowed_http_methods
|
|
17
|
+
self.allowed_http_methods = Net::HTTP.constants.map{|k| Net::HTTP::const_get(k)::METHOD.downcase.to_sym rescue nil }.compact
|
|
18
|
+
|
|
19
|
+
config_accessor :allowed_builder_methods
|
|
20
|
+
|
|
21
|
+
config_accessor :unwrap_client_exceptions
|
|
22
|
+
self.unwrap_client_exceptions = Cardiac::OperationHandler.unwrap_client_exceptions
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reconfig
|
|
26
|
+
config.clear ; config
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reconfigure
|
|
30
|
+
config.clear ; configure
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
protected
|
|
34
|
+
|
|
35
|
+
def build_config(base_config={})
|
|
36
|
+
base_config.merge config
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/remove_method'
|
|
2
|
+
|
|
3
|
+
module Cardiac
|
|
4
|
+
|
|
5
|
+
module ExtensionMethods
|
|
6
|
+
|
|
7
|
+
# Defines an operation on this resource.
|
|
8
|
+
def operation(name, implementation)
|
|
9
|
+
self.operations_values << check_operation(name, implementation)
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Defines a subresource on this resource.
|
|
14
|
+
def subresource(name, implementation, &extension_block)
|
|
15
|
+
self.subresources_values << check_subresource(name, implementation, extension_block)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Declares an extension module to be included into this resource's extension module.
|
|
20
|
+
def extending(*modules, &extension_block)
|
|
21
|
+
self.extensions_values += check_extensions(modules, extension_block)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Lazily builds the extension module, then redefines this method as an attr_reader.
|
|
26
|
+
def __extension_module__
|
|
27
|
+
build_extension_module if extensions_values.any? || operations_values.any? || subresources_values.any?
|
|
28
|
+
ensure
|
|
29
|
+
singleton_class.class_eval "attr_reader :__extension_module__"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Checks if an extension is defined.
|
|
33
|
+
def __extension_defined__?(name)
|
|
34
|
+
__extension_module__.method_defined? name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def build_extension_module
|
|
40
|
+
@__extension_module__ = apply_extensions_to_module!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_operations_for_module mod
|
|
44
|
+
operations_values.inject({}.with_indifferent_access){|h,(k,v)| h[k]=v ; h }.
|
|
45
|
+
each{|name,block| mod.redefine_method(name, &block) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_subresources_for_module mod, subm=Module.new
|
|
49
|
+
[subm, subresources_values.
|
|
50
|
+
inject({}.with_indifferent_access){|h,(k,v,e)| h[k]=[v,e] ; h }.
|
|
51
|
+
each do |name,(block,extension_block)|
|
|
52
|
+
subm.redefine_method(name, &block)
|
|
53
|
+
if extension_block
|
|
54
|
+
stash_object_in_method! subm, :"__#{name}_extensions__", extension_block
|
|
55
|
+
end
|
|
56
|
+
end]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_extensions_for_module mod
|
|
60
|
+
extensions_values.each{|ext| mod.send :include, ext }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def check_operation name, implementation
|
|
66
|
+
raise ArgumentError unless Proc===implementation
|
|
67
|
+
raise ArgumentError unless String===name || Symbol===name
|
|
68
|
+
name = name.to_sym
|
|
69
|
+
return [name, implementation] unless subresources_values.any?{|v| v.first==name }
|
|
70
|
+
raise ArgumentError, ":#{name} has already been defined as a subresource"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_subresource name, implementation, extension_block
|
|
74
|
+
raise ArgumentError unless Proc===implementation
|
|
75
|
+
raise ArgumentError unless String===name || Symbol===name
|
|
76
|
+
raise ArgumentError unless extension_block.nil? || extension_block.arity==0
|
|
77
|
+
name = name.to_sym
|
|
78
|
+
return [name, implementation, extension_block] unless operations_values.any?{|v| v.first==name }
|
|
79
|
+
raise ArgumentError, ":#{name} has already been defined as an operation"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_extensions modules, extension_block
|
|
83
|
+
raise ArgumentError unless modules.all?{|mod| Module===mod}
|
|
84
|
+
raise ArgumentError unless extension_block.nil? || extension_block.arity==0
|
|
85
|
+
modules << Module.new(&extension_block) if extension_block
|
|
86
|
+
modules
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def apply_extensions_to_module! mod=Module.new
|
|
90
|
+
build_extensions_for_module mod
|
|
91
|
+
|
|
92
|
+
subm, subr = build_subresources_for_module mod
|
|
93
|
+
if subm && subr && subr.any?
|
|
94
|
+
mod.send :include, subm
|
|
95
|
+
subr.each do |name,(_,extension_block)|
|
|
96
|
+
mod.module_eval <<-EOVR
|
|
97
|
+
def #{name}(*args)
|
|
98
|
+
super(*args)#{".extending(&__#{name}_extensions__)" if extension_block}
|
|
99
|
+
end
|
|
100
|
+
EOVR
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
build_operations_for_module mod
|
|
105
|
+
mod
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stash_object_in_method! mod, name, object
|
|
109
|
+
mod.module_exec object do |_object|
|
|
110
|
+
define_method(name) { object }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
4
|
+
|
|
5
|
+
module Cardiac
|
|
6
|
+
module RequestMethods
|
|
7
|
+
|
|
8
|
+
# The default accepts header type identifiers.
|
|
9
|
+
DEFAULT_ACCEPTS = [:json, :xml]
|
|
10
|
+
|
|
11
|
+
# HTTP method selection.
|
|
12
|
+
def http_method(k) self.method_value = check_http_method(k) ; self end
|
|
13
|
+
|
|
14
|
+
# Header selection.
|
|
15
|
+
def headers(h,*rest) self.headers_values += check_headers(rest.unshift(h)) ; self end
|
|
16
|
+
def reset_headers(*h) headers_values.replace check_headers(h) ; self end
|
|
17
|
+
def header(key, value)
|
|
18
|
+
raise ArgumentError unless String===key or Symbol===key
|
|
19
|
+
raise ArgumentError if TrueClass===value
|
|
20
|
+
self.headers_values << (FalseClass===value ? key : {key => value})
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
def accepts(search,*rest) self.accepts_values += check_accepts(rest.unshift(search)) ; self end
|
|
24
|
+
|
|
25
|
+
# Request option selection.
|
|
26
|
+
def options(o,*rest) self.options_values += apply_request_options!(check_options(rest.unshift(o))) ; self end
|
|
27
|
+
def reset_options(*o) options_values.replace apply_request_options!(check_options(o)) ; self end
|
|
28
|
+
def option(key, value)
|
|
29
|
+
raise ArgumentError unless String===key or Symbol===key
|
|
30
|
+
raise ArgumentError if TrueClass===value
|
|
31
|
+
self.options_values << (FalseClass===value ? key : {key => value})
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Relative resource selection.
|
|
36
|
+
def at(rel,options={})
|
|
37
|
+
super(rel).options(options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Checking if the configured HTTP verb has requests that support a body.
|
|
41
|
+
def request_has_body?
|
|
42
|
+
net_http_request_klass::REQUEST_HAS_BODY
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Checking if the configured HTTP verb has responses that support a body.
|
|
46
|
+
def response_has_body?
|
|
47
|
+
net_http_request_klass::RESPONSE_HAS_BODY
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def build_options(base_options={})
|
|
53
|
+
options_values.inject base_options.with_indifferent_access do |o,v|
|
|
54
|
+
case v
|
|
55
|
+
when Hash then o.deep_merge!(v)
|
|
56
|
+
when String,Symbol then o.delete(v) ; o
|
|
57
|
+
end
|
|
58
|
+
end.symbolize_keys
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_headers(base_headers={})
|
|
62
|
+
base_headers = base_headers.with_indifferent_access
|
|
63
|
+
base_headers[:content_type] = encoder_search_value if encoder_search_value.present?
|
|
64
|
+
base_headers[:accepts] = build_accepts
|
|
65
|
+
|
|
66
|
+
headers_values.inject base_headers.with_indifferent_access do |h,v|
|
|
67
|
+
case v
|
|
68
|
+
when Hash then h.deep_merge!(v)
|
|
69
|
+
when String,Symbol then h.delete(v) ; h
|
|
70
|
+
end
|
|
71
|
+
end.symbolize_keys
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_accepts
|
|
75
|
+
(accepts_values.presence || DEFAULT_ACCEPTS).dup
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_http_method
|
|
79
|
+
if method_value.nil?
|
|
80
|
+
raise InvalidOperationError, 'no HTTP method specified'
|
|
81
|
+
elsif ! net_http_request_klass
|
|
82
|
+
raise InvalidOperationError, "unsupported HTTP method: #{method_value.to_s.upcase}"
|
|
83
|
+
elsif ! allowed_http_methods.include?(method_value)
|
|
84
|
+
raise InvalidOperationError, "disallowed HTTP method: #{method_value.to_s.upcase}"
|
|
85
|
+
end
|
|
86
|
+
method_value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def check_http_method(http_method)
|
|
92
|
+
if http_method.present?
|
|
93
|
+
raise ArgumentError unless Symbol===http_method || String===http_method
|
|
94
|
+
http_method.downcase.to_sym
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def check_options(options)
|
|
99
|
+
raise ArgumentError unless options.all?{|o| Hash===o }
|
|
100
|
+
options
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def check_headers(headers)
|
|
104
|
+
raise ArgumentError unless headers.all?{|h| Hash===h }
|
|
105
|
+
headers
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def check_accepts(searches)
|
|
109
|
+
raise ArgumentError unless searches.all?{|a| Symbol===a || String===a }
|
|
110
|
+
searches
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply_request_option! key, value
|
|
114
|
+
case key
|
|
115
|
+
when :method, :http_method ; http_method(value)
|
|
116
|
+
when :headers ; headers(value)
|
|
117
|
+
when :accepts ; accepts(value)
|
|
118
|
+
when :params ; query(value)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apply_request_options! options
|
|
123
|
+
options.map{|o| o.reject{|k,v| apply_request_option!(k,v) }.presence }.compact
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def net_http_request_klass
|
|
127
|
+
if method_value.present?
|
|
128
|
+
verb = method_value.to_s.upcase
|
|
129
|
+
if @request_klass && @request_klass::METHOD == verb
|
|
130
|
+
@request_klass
|
|
131
|
+
else
|
|
132
|
+
klass = Net::HTTP.const_get(verb.capitalize) rescue nil
|
|
133
|
+
@request_klass = klass if klass && klass::METHOD == verb
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|