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