daylight 0.9.0.rc1

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