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,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
|