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.
- checksums.yaml +7 -0
- data/README.md +113 -0
- data/app/controllers/daylight_documentation/documentation_controller.rb +27 -0
- data/app/helpers/daylight_documentation/documentation_helper.rb +57 -0
- data/app/views/daylight_documentation/documentation/_header.haml +4 -0
- data/app/views/daylight_documentation/documentation/index.haml +12 -0
- data/app/views/daylight_documentation/documentation/model.haml +114 -0
- data/app/views/layouts/documentation.haml +22 -0
- data/config/routes.rb +8 -0
- data/doc/actions.md +70 -0
- data/doc/benchmarks.md +17 -0
- data/doc/contribute.md +80 -0
- data/doc/develop.md +1205 -0
- data/doc/environment.md +109 -0
- data/doc/example.md +3 -0
- data/doc/framework.md +31 -0
- data/doc/install.md +128 -0
- data/doc/principles.md +42 -0
- data/doc/testing.md +107 -0
- data/doc/usage.md +970 -0
- data/lib/daylight/api.rb +293 -0
- data/lib/daylight/associations.rb +247 -0
- data/lib/daylight/client_reloader.rb +45 -0
- data/lib/daylight/collection.rb +161 -0
- data/lib/daylight/errors.rb +94 -0
- data/lib/daylight/inflections.rb +7 -0
- data/lib/daylight/mock.rb +282 -0
- data/lib/daylight/read_only.rb +88 -0
- data/lib/daylight/refinements.rb +63 -0
- data/lib/daylight/reflection_ext.rb +67 -0
- data/lib/daylight/resource_proxy.rb +226 -0
- data/lib/daylight/version.rb +10 -0
- data/lib/daylight.rb +27 -0
- data/rails/daylight/api_controller.rb +354 -0
- data/rails/daylight/documentation.rb +13 -0
- data/rails/daylight/helpers.rb +32 -0
- data/rails/daylight/params.rb +23 -0
- data/rails/daylight/refiners.rb +186 -0
- data/rails/daylight/server.rb +29 -0
- data/rails/daylight/tasks.rb +37 -0
- data/rails/extensions/array_ext.rb +9 -0
- data/rails/extensions/autosave_association_fix.rb +49 -0
- data/rails/extensions/has_one_serializer_ext.rb +111 -0
- data/rails/extensions/inflections.rb +6 -0
- data/rails/extensions/nested_attributes_ext.rb +94 -0
- data/rails/extensions/read_only_attributes.rb +35 -0
- data/rails/extensions/render_json_meta.rb +99 -0
- data/rails/extensions/route_options.rb +47 -0
- data/rails/extensions/versioned_url_for.rb +22 -0
- data/spec/config/dependencies.rb +2 -0
- data/spec/config/factory_girl.rb +4 -0
- data/spec/config/simplecov_rcov.rb +26 -0
- data/spec/config/test_api.rb +1 -0
- data/spec/controllers/documentation_controller_spec.rb +24 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +24 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/daylight.rb +1 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +59 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/helpers/documentation_helper_spec.rb +82 -0
- data/spec/lib/daylight/api_spec.rb +178 -0
- data/spec/lib/daylight/associations_spec.rb +325 -0
- data/spec/lib/daylight/collection_spec.rb +235 -0
- data/spec/lib/daylight/errors_spec.rb +111 -0
- data/spec/lib/daylight/mock_spec.rb +144 -0
- data/spec/lib/daylight/read_only_spec.rb +118 -0
- data/spec/lib/daylight/refinements_spec.rb +80 -0
- data/spec/lib/daylight/reflection_ext_spec.rb +50 -0
- data/spec/lib/daylight/resource_proxy_spec.rb +325 -0
- data/spec/rails/daylight/api_controller_spec.rb +421 -0
- data/spec/rails/daylight/helpers_spec.rb +41 -0
- data/spec/rails/daylight/params_spec.rb +45 -0
- data/spec/rails/daylight/refiners_spec.rb +178 -0
- data/spec/rails/extensions/array_ext_spec.rb +51 -0
- data/spec/rails/extensions/has_one_serializer_ext_spec.rb +135 -0
- data/spec/rails/extensions/nested_attributes_ext_spec.rb +177 -0
- data/spec/rails/extensions/render_json_meta_spec.rb +140 -0
- data/spec/rails/extensions/route_options_spec.rb +309 -0
- data/spec/rails/extensions/versioned_url_for_spec.rb +46 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/migration_helper.rb +40 -0
- 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,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
|