cardiac 0.2.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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