daylight 0.9.0.rc1

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 (116) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +113 -0
  3. data/app/controllers/daylight_documentation/documentation_controller.rb +27 -0
  4. data/app/helpers/daylight_documentation/documentation_helper.rb +57 -0
  5. data/app/views/daylight_documentation/documentation/_header.haml +4 -0
  6. data/app/views/daylight_documentation/documentation/index.haml +12 -0
  7. data/app/views/daylight_documentation/documentation/model.haml +114 -0
  8. data/app/views/layouts/documentation.haml +22 -0
  9. data/config/routes.rb +8 -0
  10. data/doc/actions.md +70 -0
  11. data/doc/benchmarks.md +17 -0
  12. data/doc/contribute.md +80 -0
  13. data/doc/develop.md +1205 -0
  14. data/doc/environment.md +109 -0
  15. data/doc/example.md +3 -0
  16. data/doc/framework.md +31 -0
  17. data/doc/install.md +128 -0
  18. data/doc/principles.md +42 -0
  19. data/doc/testing.md +107 -0
  20. data/doc/usage.md +970 -0
  21. data/lib/daylight/api.rb +293 -0
  22. data/lib/daylight/associations.rb +247 -0
  23. data/lib/daylight/client_reloader.rb +45 -0
  24. data/lib/daylight/collection.rb +161 -0
  25. data/lib/daylight/errors.rb +94 -0
  26. data/lib/daylight/inflections.rb +7 -0
  27. data/lib/daylight/mock.rb +282 -0
  28. data/lib/daylight/read_only.rb +88 -0
  29. data/lib/daylight/refinements.rb +63 -0
  30. data/lib/daylight/reflection_ext.rb +67 -0
  31. data/lib/daylight/resource_proxy.rb +226 -0
  32. data/lib/daylight/version.rb +10 -0
  33. data/lib/daylight.rb +27 -0
  34. data/rails/daylight/api_controller.rb +354 -0
  35. data/rails/daylight/documentation.rb +13 -0
  36. data/rails/daylight/helpers.rb +32 -0
  37. data/rails/daylight/params.rb +23 -0
  38. data/rails/daylight/refiners.rb +186 -0
  39. data/rails/daylight/server.rb +29 -0
  40. data/rails/daylight/tasks.rb +37 -0
  41. data/rails/extensions/array_ext.rb +9 -0
  42. data/rails/extensions/autosave_association_fix.rb +49 -0
  43. data/rails/extensions/has_one_serializer_ext.rb +111 -0
  44. data/rails/extensions/inflections.rb +6 -0
  45. data/rails/extensions/nested_attributes_ext.rb +94 -0
  46. data/rails/extensions/read_only_attributes.rb +35 -0
  47. data/rails/extensions/render_json_meta.rb +99 -0
  48. data/rails/extensions/route_options.rb +47 -0
  49. data/rails/extensions/versioned_url_for.rb +22 -0
  50. data/spec/config/dependencies.rb +2 -0
  51. data/spec/config/factory_girl.rb +4 -0
  52. data/spec/config/simplecov_rcov.rb +26 -0
  53. data/spec/config/test_api.rb +1 -0
  54. data/spec/controllers/documentation_controller_spec.rb +24 -0
  55. data/spec/dummy/README.rdoc +28 -0
  56. data/spec/dummy/Rakefile +6 -0
  57. data/spec/dummy/app/assets/images/.keep +0 -0
  58. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  59. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  60. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  61. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  62. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  63. data/spec/dummy/app/mailers/.keep +0 -0
  64. data/spec/dummy/app/models/.keep +0 -0
  65. data/spec/dummy/app/models/concerns/.keep +0 -0
  66. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  67. data/spec/dummy/bin/bundle +3 -0
  68. data/spec/dummy/bin/rails +4 -0
  69. data/spec/dummy/bin/rake +4 -0
  70. data/spec/dummy/config/application.rb +24 -0
  71. data/spec/dummy/config/boot.rb +5 -0
  72. data/spec/dummy/config/database.yml +25 -0
  73. data/spec/dummy/config/environment.rb +5 -0
  74. data/spec/dummy/config/environments/development.rb +29 -0
  75. data/spec/dummy/config/environments/production.rb +80 -0
  76. data/spec/dummy/config/environments/test.rb +36 -0
  77. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  78. data/spec/dummy/config/initializers/daylight.rb +1 -0
  79. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  80. data/spec/dummy/config/initializers/inflections.rb +16 -0
  81. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  82. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  83. data/spec/dummy/config/initializers/session_store.rb +3 -0
  84. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  85. data/spec/dummy/config/locales/en.yml +23 -0
  86. data/spec/dummy/config/routes.rb +59 -0
  87. data/spec/dummy/config.ru +4 -0
  88. data/spec/dummy/lib/assets/.keep +0 -0
  89. data/spec/dummy/log/.keep +0 -0
  90. data/spec/dummy/public/404.html +58 -0
  91. data/spec/dummy/public/422.html +58 -0
  92. data/spec/dummy/public/500.html +57 -0
  93. data/spec/dummy/public/favicon.ico +0 -0
  94. data/spec/helpers/documentation_helper_spec.rb +82 -0
  95. data/spec/lib/daylight/api_spec.rb +178 -0
  96. data/spec/lib/daylight/associations_spec.rb +325 -0
  97. data/spec/lib/daylight/collection_spec.rb +235 -0
  98. data/spec/lib/daylight/errors_spec.rb +111 -0
  99. data/spec/lib/daylight/mock_spec.rb +144 -0
  100. data/spec/lib/daylight/read_only_spec.rb +118 -0
  101. data/spec/lib/daylight/refinements_spec.rb +80 -0
  102. data/spec/lib/daylight/reflection_ext_spec.rb +50 -0
  103. data/spec/lib/daylight/resource_proxy_spec.rb +325 -0
  104. data/spec/rails/daylight/api_controller_spec.rb +421 -0
  105. data/spec/rails/daylight/helpers_spec.rb +41 -0
  106. data/spec/rails/daylight/params_spec.rb +45 -0
  107. data/spec/rails/daylight/refiners_spec.rb +178 -0
  108. data/spec/rails/extensions/array_ext_spec.rb +51 -0
  109. data/spec/rails/extensions/has_one_serializer_ext_spec.rb +135 -0
  110. data/spec/rails/extensions/nested_attributes_ext_spec.rb +177 -0
  111. data/spec/rails/extensions/render_json_meta_spec.rb +140 -0
  112. data/spec/rails/extensions/route_options_spec.rb +309 -0
  113. data/spec/rails/extensions/versioned_url_for_spec.rb +46 -0
  114. data/spec/spec_helper.rb +43 -0
  115. data/spec/support/migration_helper.rb +40 -0
  116. metadata +422 -0
@@ -0,0 +1,161 @@
1
+ ##
2
+ # Alternate collection methods for +first_or_create+, +first_or_initialize+.
3
+ #
4
+ # Used to split the +original_params+ into known attributes and query params
5
+ # added by ResourceProxy.
6
+ #
7
+ # Parameters not added by ResourceProxy will still be merged into the supplied
8
+ # attributes. This is the current ActiveResource behavior.
9
+ #
10
+ # Parameters that contain known attributes (ie. +:filter+) will also be merged
11
+ # with the supplied attributes. The query parameters (ie. +:scopes+) will be
12
+ # set on +prefix_options+
13
+ #
14
+ # For example:
15
+ #
16
+ # users = User.find(:all, params: {
17
+ # filters: {last_name: {'Bonzai'}},
18
+ # scopes: ['planet_ten'],
19
+ # band: 'Hong Kong Cavaliers'
20
+ # })
21
+ #
22
+ # Return all Users in the band 'Hong Kong Cavaliers' where their last name
23
+ # is 'Bonzai'. If a User with first name 'Buckaroo' does not exist:
24
+ #
25
+ # users.first_or_create(first_name: 'Buckaroo')
26
+ #
27
+ # Or:
28
+ #
29
+ # users.first_or_initialize(first_name: 'Buckaroo').save
30
+ #
31
+ # Will issue the following request where the band is kept as a query parameter
32
+ # and the contents of the +filter+ paramter are merged with the attributes:
33
+ #
34
+ # POST: /api/v1/users.json?scopes[]=planet_ten
35
+ # DATA: {"user":{"first_name":"Buckaroo", "last_name":"Bonzai", "band":"Hong Kong Cavaliers"}}
36
+ #
37
+ # See:
38
+ # ActiveResource::Collection
39
+ module Daylight::Collection
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ attr_reader :metadata
44
+
45
+ ##
46
+ # Overwriting ActiveResource::Collection#initialize
47
+ #---
48
+ # Concern cannot call `super` from module to base class (we think)
49
+ def initialize(elements = [])
50
+ if Hash === elements && elements.has_key?('meta')
51
+ metadata = (elements.delete('meta')||{}).with_indifferent_access # save and strip any metadata supplied in the response
52
+ elements = ActiveResource::Formats.remove_root(elements) # re-evaluate removing root since we've removed a key
53
+ end
54
+
55
+ @metadata = metadata || {}
56
+ @elements = elements.each {|e| e['meta'] = @metadata } # pass metadata down to resource records
57
+ end
58
+
59
+ ##
60
+ # Any attribute metadata about how the collection was obtained that is used
61
+ # when creating a new element in that collection.
62
+ #
63
+ # See
64
+ # #first_or_create
65
+ # #first_or_initialize
66
+ def where_values
67
+ metadata[:where_values] || {}
68
+ end
69
+
70
+ ##
71
+ # Alternate +first_or_create+ which removes all ResourceProxy parameters,
72
+ # merging +known_attributes+ and setting +query_params+ on +prefix_options+
73
+ #
74
+ # All other pararmeters are handled identically to the original method.
75
+ #
76
+ # See:
77
+ # ActiveResource::Collection#first_or_create
78
+ def first_or_create attributes={}
79
+ first || create_resource(attributes.reverse_merge(where_values))
80
+ rescue NoMethodError
81
+ raise "Cannot build resource from resource type: #{resource_class.inspect}"
82
+ end
83
+
84
+ ##
85
+ # Alternate +first_or_initialize+ which removes all ResourceProxy parameters,
86
+ # merging +known_attributes+ and setting +query_params+ on +prefix_options+
87
+ #
88
+ # All other pararmeters are handled identically to the original method.
89
+ #
90
+ # See:
91
+ # ActiveResource::Collection#first_or_initialize
92
+ def first_or_initialize attributes={}
93
+ first || initialize_resource(attributes.reverse_merge(where_values))
94
+ rescue NoMethodError
95
+ raise "Cannot create resource from resource type: #{resource_class.inspect}"
96
+ end
97
+
98
+ protected
99
+ ##
100
+ # Performs the work of merging known attributes to the supplied attributes
101
+ # on the resource, setting the +prefix_options+ to the known params, attempting
102
+ # to save, and returning the resource.
103
+ def create_resource attributes={}
104
+ resource_class.new(attributes.update(known_attributes)).tap do |resource|
105
+ resource.prefix_options = query_params
106
+ resource.save
107
+ end
108
+ end
109
+
110
+ ##
111
+ # Performs the work of merging known attributes to the supplied +attributes+
112
+ # on the resource, setting the +prefix_options+ to the known params and and
113
+ # returning the initialized resource.
114
+ def initialize_resource attributes={}
115
+ resource_class.new(attributes.update(known_attributes)).tap do |resource|
116
+ resource.prefix_options = query_params
117
+ end
118
+ end
119
+
120
+ private
121
+ # Parameter values to be merged with supplied attributes
122
+ KNOWN_PARAMETER_KEYS = [:filters].freeze
123
+
124
+ # Parameters to continue to use as query parameters
125
+ QUERY_PARAMETER_KEYS = [:scopes].freeze
126
+
127
+ # Additional collection-based parameters to strip/ignore
128
+ STRIP_PARAMETER_KEYS = [:order, :limit, :offset].freeze
129
+
130
+ # All parameters that are used by ResourceProxy that should be removed from attributes
131
+ PROXY_PARAMETER_KEYS = KNOWN_PARAMETER_KEYS + QUERY_PARAMETER_KEYS + STRIP_PARAMETER_KEYS
132
+
133
+ ##
134
+ # Helper to strip all params used by ResourceProxy from +orignal_params+
135
+ def clean_params
136
+ original_params.except(*PROXY_PARAMETER_KEYS)
137
+ end
138
+
139
+ # Helper to extract query params that will be used as +prefix_option+
140
+ def query_params
141
+ original_params.slice(*QUERY_PARAMETER_KEYS.inject(&:update)) || {}
142
+ end
143
+
144
+ # Helper to extract known params that will be used as known attributes
145
+ def known_params
146
+ original_params.values_at(*KNOWN_PARAMETER_KEYS).inject(&:update) || {}
147
+ end
148
+
149
+ # Helper to get additional known attributes from +original_params+
150
+ def known_attributes
151
+ clean_params.update(known_params)
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ ##
158
+ # Hook into ActiveResource::Collection to override their methods
159
+ ActiveResource::Collection.class_eval do
160
+ include Daylight::Collection
161
+ end
@@ -0,0 +1,94 @@
1
+ module Daylight::Errors
2
+ extend ActiveSupport::Concern
3
+
4
+ ##
5
+ # Regex for Content-Type
6
+ #--
7
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
8
+ CONTENT_TYPE_FORMAT = /([^\/]+)\/([^;]+)(?:;.*)?/
9
+
10
+ included do
11
+ ##
12
+ # Error messages from the root cause
13
+ # :attr: messages
14
+ attr_reader :messages
15
+
16
+ ##
17
+ # Parses the messages from the response
18
+ def initialize response, message = nil
19
+ super
20
+ @messages = []
21
+ parse(response)
22
+ end
23
+
24
+ ##
25
+ # Attaches the root cause messaging to included Client message
26
+ def to_s
27
+ super.tap do |message|
28
+ message << " Root Cause = #{messages.join(', ')}" if messages?
29
+ end
30
+ end
31
+
32
+ def messages?
33
+ messages.present?
34
+ end
35
+
36
+ private
37
+ ##
38
+ # Sets the error messages when there is a payload on the response and a format that is handled
39
+ #--
40
+ # "application/xml; charset=utf-8"
41
+ # "application/json; charset=utf-8"
42
+ def parse response
43
+ _, subtype = CONTENT_TYPE_FORMAT.match(response.header['content-type']).captures rescue nil
44
+ return unless subtype.present? && response.body.present?
45
+
46
+ @messages =
47
+ case subtype
48
+ when 'xml'; self.class.from_xml(response.body)
49
+ when 'json'; self.class.from_json(response.body)
50
+ else
51
+ []
52
+ end
53
+ end
54
+ end
55
+
56
+ module ClassMethods
57
+ ##
58
+ # Parse payload that is in JSON
59
+ #--
60
+ # Examples:
61
+ #
62
+ # {'errors':'this is the problem'}
63
+ # {'errors':['this is problem one','this is problem two']}
64
+ def from_json(json)
65
+ decoded = ActiveSupport::JSON.decode(json) || {} rescue {}
66
+ if decoded.kind_of?(Hash) && decoded.has_key?('errors')
67
+ Array.wrap(decoded['errors'])
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ ##
74
+ # Parse payload that is in XML
75
+ #--
76
+ # Examples:
77
+ # <errors>
78
+ # <error>this is a Problem<error>
79
+ # </errors>
80
+ # <errors>
81
+ # <error>this is problem one<error>
82
+ # <error>this is problem one<error>
83
+ # </errors>
84
+ def from_xml(xml)
85
+ Array.wrap(Hash.from_xml(xml)['errors']['error']) rescue []
86
+ end
87
+ end
88
+ end
89
+
90
+ ##
91
+ # Hook into ActiveResource::ClientError to parse the payload
92
+ ActiveResource::ClientError.class_eval do
93
+ include Daylight::Errors
94
+ end
@@ -0,0 +1,7 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.acronym 'API'
3
+ inflect.acronym 'APIs'
4
+
5
+ inflect.acronym 'IP'
6
+ inflect.acronym 'IPs'
7
+ end
@@ -0,0 +1,282 @@
1
+ require 'rack'
2
+
3
+ ##
4
+ # Simple mocking framework that simplifies the process of writing tests for code that uses the Daylight client library.
5
+ #
6
+ # Works with both Rspec and TestUnit/Minitest.
7
+ #
8
+ # To start add this to your test_helper.rb or spec_helper.rb:
9
+ #
10
+ # Daylight::Mock.setup
11
+ #
12
+ # The mock will simulate responses to calls so you don't have to stub out anything, especially not the HTTP calls themselves.
13
+ # At the end of the test you can examine the calls that were made by calling *daylight_mock*.
14
+ #
15
+ # For example, this call returns a list of all the updated calls made on a *Host* object:
16
+ #
17
+ # daylight_mock.updated(:host)
18
+ #
19
+ # To get only the last request use:
20
+ #
21
+ # daylight_mock.last_updated(:host)
22
+ #
23
+ # Supported Calls: *created, updated, associated, indexed, shown, deleted*
24
+ #
25
+ module Daylight
26
+ module Mock
27
+
28
+ ##
29
+ # Represents a single mocked request-response pair.
30
+ class Handler
31
+ PathParts = Struct.new(:version, :resource, :id, :associated)
32
+ PATH_PARTS_REGEX = %r{^/([^/]+)/([^/]+)(?:/(\d+))?(?:/([^/]+))?\.json$}
33
+
34
+ attr_reader :request, :status, :response, :target_object
35
+
36
+ delegate :resource, to: :path_parts
37
+
38
+ def initialize(request)
39
+ @request = request
40
+ end
41
+
42
+ ##
43
+ # The request path split into logical parts: version, resource, id, and assocatied.
44
+ #
45
+ # Returns a PathParts Struct
46
+ def path_parts
47
+ @path_parts ||= PathParts.new(*path.match(PATH_PARTS_REGEX).captures)
48
+ end
49
+
50
+ ##
51
+ # The request's path
52
+ def path
53
+ request.uri.path
54
+ end
55
+
56
+ ##
57
+ # The mock respose body
58
+ def response_body
59
+ @response_body ||= handle_request
60
+ end
61
+
62
+ ##
63
+ # The request's POST data
64
+ def post_data
65
+ @post_data ||= JSON.parse(request.body)
66
+ end
67
+
68
+ ##
69
+ # The request's query params
70
+ def params
71
+ @params ||= Rack::Utils.parse_nested_query(request.uri.query)
72
+ end
73
+
74
+ ##
75
+ # The action to perform (based on the request path and method).
76
+ #
77
+ # Returns :associated, :shown, :indexed, :created, :updated or :deleted
78
+ def action
79
+ @action ||=
80
+ case request.method
81
+ when :get
82
+ if path_parts.associated.present?
83
+ :associated
84
+ elsif path_parts.id.present?
85
+ :shown
86
+ else
87
+ :indexed
88
+ end
89
+ when :post
90
+ :created
91
+ when :put
92
+ :updated
93
+ when :delete
94
+ :deleted
95
+ end
96
+ end
97
+
98
+ private
99
+ def model_class(model_name)
100
+ model_name.classify.constantize
101
+ end
102
+
103
+ def handle_request
104
+ send "process_#{action}"
105
+ end
106
+
107
+ def process_indexed
108
+ clazz = model_class(path_parts.resource)
109
+ list = [new_record(clazz)] * (rand(4)+1)
110
+
111
+ respond_with(body: list)
112
+ end
113
+
114
+ def process_shown
115
+ clazz = model_class(path_parts.resource)
116
+ @target_object = new_record(clazz, id: path_parts.id.to_i)
117
+
118
+ respond_with(body: @target_object)
119
+ end
120
+
121
+ def process_associated
122
+ clazz = model_class(path_parts.associated)
123
+ list = [new_record(clazz)] * (rand(4)+1)
124
+
125
+ respond_with(body: list)
126
+ end
127
+
128
+ def process_created
129
+ data = post_data[path_parts.resource.singularize]
130
+ clazz = model_class(path_parts.resource)
131
+ @target_object = new_record(clazz, data.merge(id:rand(100) + 1))
132
+
133
+ respond_with(body: @target_object, status: 201)
134
+ end
135
+
136
+ def process_updated
137
+ clazz = model_class(path_parts.resource)
138
+ data = post_data[path_parts.resource.singularize]
139
+ @target_object = new_record(clazz, data.merge(id: path_parts.id.to_i))
140
+
141
+ respond_with(status: 201)
142
+ end
143
+
144
+ def process_deleted
145
+ clazz = model_class(path_parts.resource)
146
+ @target_object = new_record(clazz, id: path_parts.id.to_i)
147
+
148
+ respond_with(status: 200)
149
+ end
150
+
151
+ def respond_with(options={})
152
+ @response = options[:body]
153
+ options[:body] &&= encode(options[:body])
154
+ options[:status] ||= 200
155
+ @status = options[:status]
156
+ options
157
+ end
158
+
159
+ def encode(response)
160
+ if @response.is_a? Enumerable
161
+ {'foo'=>@response}.to_json
162
+ else
163
+ @response.encode
164
+ end
165
+ end
166
+
167
+ def new_record(clazz, options={})
168
+ filters = params['filters'] || {}
169
+ clazz.new(options.reverse_merge(filters).reverse_merge(id: rand(100) + 1))
170
+ end
171
+ end
172
+
173
+ ##
174
+ # Keeps track of all request and response pairs.
175
+ # Stored by action and resource.
176
+ #
177
+ # Example:
178
+ #
179
+ # daylight_mock.created(:project).count.should == 3
180
+ #
181
+ # daylight_mock.last_created(:project).name.should == 'Test Project'
182
+ class Recorder
183
+ def initialize
184
+ # hashy hash hash
185
+ @storage = Hash.new do |action_hash, action|
186
+ action_hash[action] =
187
+ Hash.new do |handler_hash, handler|
188
+ handler_hash[handler] = []
189
+ end
190
+ end
191
+ end
192
+
193
+ %w[created updated associated indexed shown deleted].each do |action|
194
+ define_method action do |resource|
195
+ @storage[action.to_s][resource.to_s.pluralize]
196
+ end
197
+
198
+ define_method "last_#{action}" do |resource|
199
+ @storage[action.to_s][resource.to_s.pluralize].last
200
+ end
201
+ end
202
+
203
+ ##
204
+ # Store a Handler in the Recorder
205
+ def record(handler)
206
+ @storage[handler.action.to_s][handler.resource.to_s] << handler
207
+ end
208
+ end
209
+
210
+ ##
211
+ # Minitest hook
212
+ module Minitest
213
+ # for minitest
214
+ def before_setup
215
+ capture_api_requests
216
+ end
217
+ end
218
+
219
+ class << self
220
+
221
+ ##
222
+ # Run in the test framework's setup to start and configure Daylight::Mock.
223
+ #
224
+ # Daylight::Mock.setup
225
+ def setup
226
+ setup_rspec if Module.const_defined? "RSpec"
227
+ setup_minitest if Module.qualified_const_defined?("MiniTest::Spec")
228
+ end
229
+
230
+ private
231
+ def setup_rspec
232
+ require 'webmock/rspec'
233
+
234
+ RSpec.configure do |config|
235
+ config.include Daylight::Mock
236
+
237
+ config.before(:each) do
238
+ capture_api_requests
239
+ end
240
+ end
241
+ end
242
+
243
+ def setup_minitest
244
+ require 'webmock/minitest'
245
+
246
+ clazz = MiniTest::Test rescue MiniTest::Unit::TestCase
247
+
248
+ clazz.class_eval do
249
+ include Daylight::Mock
250
+ include Daylight::Mock::Minitest
251
+ end
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Access to Daylight::Mock::Recorder from within test definitions.
257
+ def daylight_mock
258
+ @daylight_mock ||= Recorder.new
259
+ end
260
+
261
+ private
262
+ def capture_api_requests
263
+ # capture all requests to the API server
264
+ stub_request(:any, /#{site_with_credentials}/)
265
+ .with(headers: {'X-Daylight-Framework' => /.*/})
266
+ .to_return do |request|
267
+ handler = Handler.new(request)
268
+ daylight_mock.record(handler)
269
+ handler.response_body
270
+ end
271
+ end
272
+
273
+ # Webmock prepends urls with any username and password when
274
+ # it does matching.
275
+ def site_with_credentials
276
+ @site_with_credentials ||= Daylight::API.site.dup.tap do |site|
277
+ site.userinfo = "#{Daylight::API.user}:#{Daylight::API.password}"
278
+ site.to_s
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,88 @@
1
+ ##
2
+ # Support for read_only attributes.
3
+ #
4
+ # Attributes that are read_only are specified in the metadata from the response
5
+ # from the API.
6
+ #
7
+ # Uses that information to keep from sending those read_only attributes in
8
+ # subsequent requests to the API.
9
+ #
10
+ # This is useful for computational values that are served that do not have
11
+ # corresponding column in the data store.
12
+
13
+ module Daylight::ReadOnly
14
+ ##
15
+ # Get the list of read_only attributes from the metadata attribute.
16
+ # If there are none then an empty array is supplied.
17
+ #
18
+ # See:
19
+ # metadata
20
+
21
+ def read_only
22
+ metadata[:read_only] || []
23
+ end
24
+
25
+ ##
26
+ # Adds API specific options when generating json.
27
+ # Removes read_only attributes for requests.
28
+ #
29
+ # See
30
+ # except_read_only
31
+
32
+ def as_json(options={})
33
+ super(except_read_only(options))
34
+ end
35
+
36
+ ##
37
+ # Adds API specific options when generating xml.
38
+ # Removes read_only attributes for requests.
39
+ #
40
+ # See
41
+ # except_read_only
42
+
43
+ def to_xml(options={})
44
+ super(except_read_only(options))
45
+ end
46
+
47
+ ##
48
+ # Writers for read_only attributes are not included as methods
49
+ #--
50
+ # This is how we continue to prevent these read_only attributes to be set
51
+ # internally by removing ActiveResource's ability to set their values
52
+
53
+ def respond_to?(method_name, include_priv = false)
54
+ return false if read_only?(method_name)
55
+ super
56
+ end
57
+
58
+ private
59
+ ##
60
+ # Extends `method_missing` to raise an error when attempting to set a
61
+ # read_only attribute.
62
+ #
63
+ # Otherwise it continues with the `ActiveResource::Base#method_missing`
64
+ # functionality.
65
+
66
+ def method_missing(method_name, *arguments)
67
+ if read_only?(method_name)
68
+ logger.warn "Cannot set read_only attribute: #{method_name[0...-1]}" if logger
69
+ raise NoMethodError, "Cannot set read_only attribute: #{method_name[0...-1]}"
70
+ end
71
+
72
+ super
73
+ end
74
+
75
+ ##
76
+ # Ensures that read_only attributes are merged in with `:except` options.
77
+
78
+ def except_read_only options
79
+ options.merge(except: (options[:except]||[]).push(*read_only))
80
+ end
81
+
82
+ ##
83
+ # Determines if `method_name` is writing to a read_only attribute.
84
+
85
+ def read_only? method_name
86
+ !!(method_name =~ /(?:=)$/ && read_only.include?($`))
87
+ end
88
+ end
@@ -0,0 +1,63 @@
1
+ ##
2
+ # Support for handling ActiveRecord-like refinementmes and chaining them together
3
+ # These refinements include: +where+, +find_by+, +order+, +limit+, and +offset+
4
+ # Named +scopes+ are also supported.
5
+ module Daylight::Refinements
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ delegate :where, :find_by, :order, :limit, :offset, to: :resource_proxy
10
+
11
+ attr_accessor :scope_names
12
+
13
+ # Define scopes that the class can be refined by
14
+ def scopes *names
15
+ self.scope_names ||= []
16
+
17
+ names.each do |scope|
18
+ self.scope_names << scope
19
+
20
+ # hand chaining duties off to the ResourceProxy instance
21
+ define_singleton_method scope do
22
+ resource_proxy.append_scope(scope)
23
+ end
24
+
25
+ # ResourceProxy instance also needs to respond to scopes
26
+ resource_proxy_class.send(:define_method, scope) do
27
+ append_scope(scope)
28
+ end
29
+ end
30
+
31
+ # self.scope_names.freeze
32
+ end
33
+
34
+ ##
35
+ # Use limits if no argument are supplied. Otherwise, continue to use
36
+ # the ActiveRecord version which retrieves the full result set and calls
37
+ # first.
38
+ #
39
+ # See:
40
+ # ActiveRecord::Base#first
41
+ # Daylight::ResourceProxy#first
42
+ def first *args
43
+ args.size.zero? ? resource_proxy.first : super
44
+ end
45
+
46
+ protected
47
+ # Ensure the subclasses are setup with their ResourceProxy
48
+ def inherited subclass
49
+ Daylight::ResourceProxy[subclass]
50
+ end
51
+
52
+ private
53
+ # Sets up and saves the ResourceProxy in the resource class
54
+ def resource_proxy_class
55
+ Daylight::ResourceProxy[self]
56
+ end
57
+
58
+ # All chains create a new instance of the ResourceProxy
59
+ def resource_proxy
60
+ resource_proxy_class.new
61
+ end
62
+ end
63
+ end