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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/LICENSE +22 -0
  4. data/Rakefile +66 -0
  5. data/cardiac-0.2.0.pre2.gem +0 -0
  6. data/cardiac.gemspec +48 -0
  7. data/lib/cardiac/declarations.rb +70 -0
  8. data/lib/cardiac/errors.rb +65 -0
  9. data/lib/cardiac/log_subscriber.rb +55 -0
  10. data/lib/cardiac/model/attributes.rb +146 -0
  11. data/lib/cardiac/model/base.rb +161 -0
  12. data/lib/cardiac/model/callbacks.rb +47 -0
  13. data/lib/cardiac/model/declarations.rb +106 -0
  14. data/lib/cardiac/model/dirty.rb +117 -0
  15. data/lib/cardiac/model/locale/en.yml +7 -0
  16. data/lib/cardiac/model/operations.rb +49 -0
  17. data/lib/cardiac/model/persistence.rb +171 -0
  18. data/lib/cardiac/model/querying.rb +129 -0
  19. data/lib/cardiac/model/validations.rb +124 -0
  20. data/lib/cardiac/model.rb +17 -0
  21. data/lib/cardiac/operation_builder.rb +75 -0
  22. data/lib/cardiac/operation_handler.rb +215 -0
  23. data/lib/cardiac/railtie.rb +20 -0
  24. data/lib/cardiac/reflections.rb +85 -0
  25. data/lib/cardiac/representation.rb +124 -0
  26. data/lib/cardiac/resource/adapter.rb +178 -0
  27. data/lib/cardiac/resource/builder.rb +107 -0
  28. data/lib/cardiac/resource/codec_methods.rb +58 -0
  29. data/lib/cardiac/resource/config_methods.rb +39 -0
  30. data/lib/cardiac/resource/extension_methods.rb +115 -0
  31. data/lib/cardiac/resource/request_methods.rb +138 -0
  32. data/lib/cardiac/resource/subresource.rb +88 -0
  33. data/lib/cardiac/resource/uri_methods.rb +176 -0
  34. data/lib/cardiac/resource.rb +77 -0
  35. data/lib/cardiac/util.rb +120 -0
  36. data/lib/cardiac/version.rb +3 -0
  37. data/lib/cardiac.rb +61 -0
  38. data/spec/rails-3.2/Gemfile +9 -0
  39. data/spec/rails-3.2/Gemfile.lock +136 -0
  40. data/spec/rails-3.2/Rakefile +10 -0
  41. data/spec/rails-3.2/app_root/app/assets/javascripts/application.js +15 -0
  42. data/spec/rails-3.2/app_root/app/assets/stylesheets/application.css +13 -0
  43. data/spec/rails-3.2/app_root/app/controllers/application_controller.rb +3 -0
  44. data/spec/rails-3.2/app_root/app/helpers/application_helper.rb +2 -0
  45. data/spec/rails-3.2/app_root/app/views/layouts/application.html.erb +14 -0
  46. data/spec/rails-3.2/app_root/config/application.rb +29 -0
  47. data/spec/rails-3.2/app_root/config/boot.rb +13 -0
  48. data/spec/rails-3.2/app_root/config/database.yml +25 -0
  49. data/spec/rails-3.2/app_root/config/environment.rb +5 -0
  50. data/spec/rails-3.2/app_root/config/environments/development.rb +10 -0
  51. data/spec/rails-3.2/app_root/config/environments/production.rb +11 -0
  52. data/spec/rails-3.2/app_root/config/environments/test.rb +11 -0
  53. data/spec/rails-3.2/app_root/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/rails-3.2/app_root/config/initializers/inflections.rb +15 -0
  55. data/spec/rails-3.2/app_root/config/initializers/mime_types.rb +5 -0
  56. data/spec/rails-3.2/app_root/config/initializers/secret_token.rb +7 -0
  57. data/spec/rails-3.2/app_root/config/initializers/session_store.rb +8 -0
  58. data/spec/rails-3.2/app_root/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/rails-3.2/app_root/config/locales/en.yml +5 -0
  60. data/spec/rails-3.2/app_root/config/routes.rb +2 -0
  61. data/spec/rails-3.2/app_root/db/test.sqlite3 +0 -0
  62. data/spec/rails-3.2/app_root/log/test.log +2403 -0
  63. data/spec/rails-3.2/app_root/public/404.html +26 -0
  64. data/spec/rails-3.2/app_root/public/422.html +26 -0
  65. data/spec/rails-3.2/app_root/public/500.html +25 -0
  66. data/spec/rails-3.2/app_root/public/favicon.ico +0 -0
  67. data/spec/rails-3.2/app_root/script/rails +6 -0
  68. data/spec/rails-3.2/spec/spec_helper.rb +25 -0
  69. data/spec/rails-4.0/Gemfile +9 -0
  70. data/spec/rails-4.0/Gemfile.lock +132 -0
  71. data/spec/rails-4.0/Rakefile +10 -0
  72. data/spec/rails-4.0/app_root/app/assets/javascripts/application.js +15 -0
  73. data/spec/rails-4.0/app_root/app/assets/stylesheets/application.css +13 -0
  74. data/spec/rails-4.0/app_root/app/controllers/application_controller.rb +3 -0
  75. data/spec/rails-4.0/app_root/app/helpers/application_helper.rb +2 -0
  76. data/spec/rails-4.0/app_root/app/views/layouts/application.html.erb +14 -0
  77. data/spec/rails-4.0/app_root/config/application.rb +28 -0
  78. data/spec/rails-4.0/app_root/config/boot.rb +13 -0
  79. data/spec/rails-4.0/app_root/config/database.yml +25 -0
  80. data/spec/rails-4.0/app_root/config/environment.rb +5 -0
  81. data/spec/rails-4.0/app_root/config/environments/development.rb +9 -0
  82. data/spec/rails-4.0/app_root/config/environments/production.rb +11 -0
  83. data/spec/rails-4.0/app_root/config/environments/test.rb +10 -0
  84. data/spec/rails-4.0/app_root/config/initializers/backtrace_silencers.rb +7 -0
  85. data/spec/rails-4.0/app_root/config/initializers/inflections.rb +15 -0
  86. data/spec/rails-4.0/app_root/config/initializers/mime_types.rb +5 -0
  87. data/spec/rails-4.0/app_root/config/initializers/secret_token.rb +7 -0
  88. data/spec/rails-4.0/app_root/config/initializers/session_store.rb +8 -0
  89. data/spec/rails-4.0/app_root/config/initializers/wrap_parameters.rb +14 -0
  90. data/spec/rails-4.0/app_root/config/locales/en.yml +5 -0
  91. data/spec/rails-4.0/app_root/config/routes.rb +2 -0
  92. data/spec/rails-4.0/app_root/db/test.sqlite3 +0 -0
  93. data/spec/rails-4.0/app_root/log/development.log +50 -0
  94. data/spec/rails-4.0/app_root/log/test.log +2399 -0
  95. data/spec/rails-4.0/app_root/public/404.html +26 -0
  96. data/spec/rails-4.0/app_root/public/422.html +26 -0
  97. data/spec/rails-4.0/app_root/public/500.html +25 -0
  98. data/spec/rails-4.0/app_root/public/favicon.ico +0 -0
  99. data/spec/rails-4.0/app_root/script/rails +6 -0
  100. data/spec/rails-4.0/spec/spec_helper.rb +25 -0
  101. data/spec/shared/cardiac/declarations_spec.rb +103 -0
  102. data/spec/shared/cardiac/model/base_spec.rb +446 -0
  103. data/spec/shared/cardiac/operation_builder_spec.rb +96 -0
  104. data/spec/shared/cardiac/operation_handler_spec.rb +82 -0
  105. data/spec/shared/cardiac/representation/reflection_spec.rb +73 -0
  106. data/spec/shared/cardiac/resource/adapter_spec.rb +83 -0
  107. data/spec/shared/cardiac/resource/builder_spec.rb +52 -0
  108. data/spec/shared/cardiac/resource/codec_methods_spec.rb +63 -0
  109. data/spec/shared/cardiac/resource/config_methods_spec.rb +52 -0
  110. data/spec/shared/cardiac/resource/extension_methods_spec.rb +215 -0
  111. data/spec/shared/cardiac/resource/request_methods_spec.rb +186 -0
  112. data/spec/shared/cardiac/resource/uri_methods_spec.rb +212 -0
  113. data/spec/shared/support/client_execution.rb +28 -0
  114. data/spec/spec_helper.rb +24 -0
  115. 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