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