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
data/lib/daylight/api.rb
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Daylight API Client Library
|
|
3
|
+
#
|
|
4
|
+
# Use this client in your Ruby/Rails applications for ease of use access to the
|
|
5
|
+
# Client API.
|
|
6
|
+
#
|
|
7
|
+
# Unlike typical ActiveResource clients, the Daylight API Client has been
|
|
8
|
+
# designed to be used similarly to ActiveRecord with scopes and the ability to
|
|
9
|
+
# chain queries.
|
|
10
|
+
#
|
|
11
|
+
# ClientAPI::Post.all
|
|
12
|
+
# ClientAPI::Post.where(code:'iad1')
|
|
13
|
+
# ClientAPI::Post.published # scope
|
|
14
|
+
# ClientAPI::Post.find(1).comments # associations
|
|
15
|
+
# ClientAPI::Post.find(1).public_commenters # remote method on model
|
|
16
|
+
# ClientAPI::Post.find(1).commenters.
|
|
17
|
+
# where(username: 'reidmix') # chaining
|
|
18
|
+
#
|
|
19
|
+
# Build your client models using Daylight::API, it is a wrapper with extended
|
|
20
|
+
# functionality to ActiveResource::Base
|
|
21
|
+
#
|
|
22
|
+
# class ClientAPI::Post < Daylight::API
|
|
23
|
+
# scopes :internal
|
|
24
|
+
#
|
|
25
|
+
# belongs_to :user
|
|
26
|
+
# has_many :comments
|
|
27
|
+
# has_many :commenters, through: :comments
|
|
28
|
+
#
|
|
29
|
+
# remote :public_commenters, class_name: 'client_api/user'
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Once all your client models are built, setup your API Client Library and
|
|
33
|
+
# startup via `setup!` (in an intitializer):
|
|
34
|
+
#
|
|
35
|
+
# require 'client_api'
|
|
36
|
+
#
|
|
37
|
+
# Daylight::API.setup!({
|
|
38
|
+
# namespace: 'client_api',
|
|
39
|
+
# password: 'test',
|
|
40
|
+
# endpoint: 'http://api.example.org/
|
|
41
|
+
# })
|
|
42
|
+
|
|
43
|
+
class Daylight::API < ActiveResource::Base
|
|
44
|
+
include Daylight::ReadOnly
|
|
45
|
+
include Daylight::Refinements
|
|
46
|
+
include Daylight::Associations
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
attr_reader :version, :versions, :namespace
|
|
50
|
+
cattr_accessor :request_root_in_json
|
|
51
|
+
alias_method :endpoint, :site
|
|
52
|
+
|
|
53
|
+
DEFAULT_CONFIG = {
|
|
54
|
+
namespace: 'API',
|
|
55
|
+
endpoint: 'http://localhost',
|
|
56
|
+
versions: %w[v1]
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Setup and configure the Daylight API. Must be called before Client API use.
|
|
61
|
+
# Will use the following defaults:
|
|
62
|
+
#
|
|
63
|
+
# Daylight::API.setup!({
|
|
64
|
+
# namespace: 'API',
|
|
65
|
+
# password: nil,
|
|
66
|
+
# endpoint: 'http://localhost',
|
|
67
|
+
# versions: ['v1'],
|
|
68
|
+
# version: 'v1'
|
|
69
|
+
# timeout: 60 # in seconds
|
|
70
|
+
# })
|
|
71
|
+
#
|
|
72
|
+
# Daylight currenly requires that your API is within a module `namespace`
|
|
73
|
+
#
|
|
74
|
+
# The `endpoint` sets ActiveResource#site configuration.
|
|
75
|
+
# The `password` is the HTTP Authentication password.
|
|
76
|
+
#
|
|
77
|
+
# Daylight assumes you're versioning your API, you can supply the `versions`
|
|
78
|
+
# that are supported by your API and which `version` is active.
|
|
79
|
+
#
|
|
80
|
+
# By default, ActiveResource#request_root_in_json is set to true.
|
|
81
|
+
# You can turn this off with the `request_root_in_json` configuration.
|
|
82
|
+
#
|
|
83
|
+
# A convenience for versioned APIs is to alias the active Client API models
|
|
84
|
+
# to versionless constance. For example
|
|
85
|
+
#
|
|
86
|
+
# ClientAPI::V1::Post
|
|
87
|
+
#
|
|
88
|
+
# Aliased to:
|
|
89
|
+
#
|
|
90
|
+
# ClientAPI::Post
|
|
91
|
+
#
|
|
92
|
+
# This functionalitity is turned on using the `alias_apis` configuration.
|
|
93
|
+
|
|
94
|
+
def setup! options={}
|
|
95
|
+
config = options.with_indifferent_access.reverse_merge(DEFAULT_CONFIG)
|
|
96
|
+
|
|
97
|
+
self.namespace = config[:namespace]
|
|
98
|
+
self.password = config[:password]
|
|
99
|
+
self.endpoint = config[:endpoint]
|
|
100
|
+
self.versions = config[:versions].freeze
|
|
101
|
+
self.version = config[:version] || config[:versions].last # specify or use most recent version
|
|
102
|
+
self.timeout = config[:timeout] if config[:timeout] # default read_timeout is 60
|
|
103
|
+
|
|
104
|
+
# Only "parent" elements required to emit a root node
|
|
105
|
+
self.request_root_in_json = config[:request_root_in_json] || true
|
|
106
|
+
|
|
107
|
+
headers['X-Daylight-Framework'] = Daylight::VERSION
|
|
108
|
+
|
|
109
|
+
alias_apis unless config[:no_alias_apis]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Find a single resource from the default URL
|
|
114
|
+
#
|
|
115
|
+
# Fixes bug to short-circuit and return `nil` if scope/id is nil.
|
|
116
|
+
# ActiveResource::Base will perform the call and return with an error.
|
|
117
|
+
|
|
118
|
+
def find_single(scope, options)
|
|
119
|
+
return if scope.nil?
|
|
120
|
+
super
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
##
|
|
125
|
+
# Whether to show root for the request
|
|
126
|
+
#
|
|
127
|
+
# API requires JSON request to emit a root node named after the object’s
|
|
128
|
+
# type this is different from `include_root_in_json` where _every_
|
|
129
|
+
# `ActiveResource` supplies its root.
|
|
130
|
+
#
|
|
131
|
+
# This causes problems with `accepts_nessted_attributes_for` where the
|
|
132
|
+
# *_attributes do not need it (and is broken by having a root elmenet)
|
|
133
|
+
#
|
|
134
|
+
# Turned on by default when transmitting JSON requests.
|
|
135
|
+
#
|
|
136
|
+
# See:
|
|
137
|
+
# encode
|
|
138
|
+
def request_root_in_json?
|
|
139
|
+
request_root_in_json && format.extension == 'json'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
attr_writer :versions, :namespace
|
|
144
|
+
alias_method :endpoint=, :site=
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Set the `version` and make sure it's a member of the supported versions
|
|
148
|
+
|
|
149
|
+
def version= v
|
|
150
|
+
unless versions.include?(v)
|
|
151
|
+
raise "Unsupported version #{v} is not one of #{versions.join(', ')}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
@version = v.upcase
|
|
155
|
+
version_path = "/#{v.downcase}/".gsub(/\/+/, '/')
|
|
156
|
+
|
|
157
|
+
set_prefix version_path
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Alias the configured client API constants to be references without a
|
|
162
|
+
# version number for the active version:
|
|
163
|
+
#
|
|
164
|
+
# For example, if the active version is 'v1':
|
|
165
|
+
#
|
|
166
|
+
# API::Post # => API::V1::Post
|
|
167
|
+
#
|
|
168
|
+
# Assumes all your model classes are loaded (defined)
|
|
169
|
+
|
|
170
|
+
def alias_apis
|
|
171
|
+
api_classes = "#{namespace}::#{version}".constantize.constants
|
|
172
|
+
api_namespace = namespace.constantize
|
|
173
|
+
|
|
174
|
+
api_classes.each do |api_class|
|
|
175
|
+
api_namespace.const_set(api_class, "#{namespace}::#{version}::#{api_class}".constantize)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
true
|
|
179
|
+
rescue => e
|
|
180
|
+
logger.error("Could not alias_apis #{e.class}:\n\t#{e.message}") if logger
|
|
181
|
+
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
attr_reader :metadata
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# Extends ActiveResource to allow for saving metadata from the responses on
|
|
190
|
+
# the `meta` key. Will store this metadata on the `metadata` attribute.
|
|
191
|
+
#---
|
|
192
|
+
# Does this extension by overwritting ActiveResource::Base#initialize method
|
|
193
|
+
# Concern cannot call `super` from module to base class (we think)
|
|
194
|
+
|
|
195
|
+
def initialize(attributes={}, persisted = false)
|
|
196
|
+
extract_metadata!(attributes)
|
|
197
|
+
|
|
198
|
+
super
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
##
|
|
202
|
+
# Get the list of nested_resources from the metadata attribute.
|
|
203
|
+
# If there are none then an empty array is supplied.
|
|
204
|
+
#
|
|
205
|
+
# See:
|
|
206
|
+
# metadata
|
|
207
|
+
|
|
208
|
+
def nested_resources
|
|
209
|
+
@nested_resources ||= metadata[:nested_resources] || []
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
# Used to assist `find_or_create_resource_for` to use embedded attributes
|
|
214
|
+
# to new Daylight::API model objects.
|
|
215
|
+
#
|
|
216
|
+
# See:
|
|
217
|
+
# find_or_create_resource_for
|
|
218
|
+
|
|
219
|
+
class HashResourcePassthrough
|
|
220
|
+
def self.new(value, _)
|
|
221
|
+
# load values using ActiveResource::Base and extract them as attributes
|
|
222
|
+
Daylight::API.new(value.duplicable? ? value.dup : value).attributes
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
##
|
|
227
|
+
# When an association is supplied via a hash of `*_attributes` then create
|
|
228
|
+
# (a set) of new Client API objects instead of leaving as a hash.
|
|
229
|
+
|
|
230
|
+
def find_or_create_resource_for name
|
|
231
|
+
# if the key is attributes attributes for a configured association
|
|
232
|
+
if /(?:_attributes)\z/ =~ name && reflections.key?($`.to_sym)
|
|
233
|
+
HashResourcePassthrough
|
|
234
|
+
else
|
|
235
|
+
super
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
# Returns the serialized string representation of the resource in the configured
|
|
241
|
+
# serialization format specified in ActiveResource::Base.format.
|
|
242
|
+
#
|
|
243
|
+
# For JSON formatted requests default option is to include the root element
|
|
244
|
+
# depending on the `request_root_in_json` configuration.
|
|
245
|
+
|
|
246
|
+
def encode(options={})
|
|
247
|
+
super(self.class.request_root_in_json? ? { :root => self.class.element_name }.merge(options) : options)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
protected
|
|
251
|
+
##
|
|
252
|
+
# Override `ActiveResource` method so it strips the meta attributes for create and update actions.
|
|
253
|
+
#
|
|
254
|
+
# Solves problem where `remove_root` was not performing because meta was still in the response.
|
|
255
|
+
# For GET objects, this is handled by `initialize` but that's too late in this case.
|
|
256
|
+
#
|
|
257
|
+
# See:
|
|
258
|
+
# ActiveResource::Base#load_attributes_from_response
|
|
259
|
+
|
|
260
|
+
def load_attributes_from_response(response)
|
|
261
|
+
if response_loadable?(response)
|
|
262
|
+
decoded_body = self.class.format.decode(response.body)
|
|
263
|
+
extract_metadata!(decoded_body)
|
|
264
|
+
load(decoded_body, true, true)
|
|
265
|
+
@persisted = true
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
private
|
|
270
|
+
|
|
271
|
+
##
|
|
272
|
+
# Does this response actaully have a body?
|
|
273
|
+
|
|
274
|
+
def response_loadable?(response)
|
|
275
|
+
response_code_allows_body?(response.code) &&
|
|
276
|
+
(response['Content-Length'].nil? || response['Content-Length'] != "0") &&
|
|
277
|
+
!response.body.nil? &&
|
|
278
|
+
response.body.strip.size > 0
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
##
|
|
282
|
+
# Extract meta attribute from attributes and save it
|
|
283
|
+
|
|
284
|
+
def extract_metadata!(attributes)
|
|
285
|
+
if Hash === attributes && attributes.has_key?('meta')
|
|
286
|
+
# save and strip any metadata supplied in the response
|
|
287
|
+
metadata = (attributes.delete('meta')||{}).with_indifferent_access
|
|
288
|
+
metadata.merge!(metadata.delete(self.class.element_name) || {})
|
|
289
|
+
end
|
|
290
|
+
@metadata = metadata || {}
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Support for quering associations between client objects
|
|
3
|
+
#
|
|
4
|
+
module Daylight::Associations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
private
|
|
9
|
+
# All chains create a new instance of the ResourceProxy for the supplied resource
|
|
10
|
+
def resource_proxy_for reflection, resource
|
|
11
|
+
Daylight::ResourceProxy[reflection.klass].new({reflection.name => resource})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# builds the path to the associated collection based on the has_many reflection
|
|
15
|
+
def association_path(reflection)
|
|
16
|
+
prefix = self.class.prefix
|
|
17
|
+
collection_name = self.class.collection_name
|
|
18
|
+
member_id = URI.parser.escape id.to_s
|
|
19
|
+
extension = reflection.klass.format.extension
|
|
20
|
+
|
|
21
|
+
"#{prefix}#{collection_name}/#{member_id}/#{reflection.name}.#{extension}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call_remote(remoted_method, model)
|
|
25
|
+
response = get(remoted_method)
|
|
26
|
+
# strip the root, but take into consideration metadata
|
|
27
|
+
if Hash === response && response.has_key?(remoted_method.to_s)
|
|
28
|
+
response = response[remoted_method.to_s]
|
|
29
|
+
end
|
|
30
|
+
case response
|
|
31
|
+
when Array
|
|
32
|
+
model.send(:instantiate_collection, response)
|
|
33
|
+
when Hash
|
|
34
|
+
model.send(:instantiate_record, response)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module ClassMethods
|
|
40
|
+
|
|
41
|
+
def reflection_names
|
|
42
|
+
reflections.keys.map(&:to_s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Support for the :through option so that the server-side handles the association.
|
|
47
|
+
#
|
|
48
|
+
# Post.first.comments #=> GET /posts/1/comments.json
|
|
49
|
+
#
|
|
50
|
+
# Also adds the setter for assocations:
|
|
51
|
+
#
|
|
52
|
+
# Post.first.comments = [new_comment]
|
|
53
|
+
#
|
|
54
|
+
# See:
|
|
55
|
+
# ActiveResource::Associations#has_many
|
|
56
|
+
|
|
57
|
+
def has_many name, options={}
|
|
58
|
+
through = options.delete(:through).to_s
|
|
59
|
+
return super unless through == 'associated'
|
|
60
|
+
|
|
61
|
+
create_reflection(:has_many, name, options).tap do |reflection|
|
|
62
|
+
nested_attribute_key = "#{reflection.name}_attributes"
|
|
63
|
+
|
|
64
|
+
# setup the resource_proxy to fetch the results
|
|
65
|
+
define_cached_method reflection.name, cache_key: nested_attribute_key do
|
|
66
|
+
# return a empty collection if this is a new record
|
|
67
|
+
return self.send("#{reflection.name}=", []) if new?
|
|
68
|
+
|
|
69
|
+
resource_proxy = resource_proxy_for(reflection, self)
|
|
70
|
+
resource_proxy.from(association_path(reflection))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# define setter that places the value directly in the attributes using
|
|
74
|
+
# the nested_attributes functionality server-side
|
|
75
|
+
define_method "#{reflection.name}=" do |value|
|
|
76
|
+
self.attributes[nested_attribute_key] = value
|
|
77
|
+
instance_variable_set(:"@#{reflection.name}", value)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Adds a setter to the original `belongs_to` method that uses nested_attributes.
|
|
85
|
+
# Also, hands off the :through option to `belongs_to_through`.
|
|
86
|
+
#
|
|
87
|
+
# Example:
|
|
88
|
+
#
|
|
89
|
+
# comment = Comment.find(1)
|
|
90
|
+
# comment.creator = current_user
|
|
91
|
+
#
|
|
92
|
+
# See:
|
|
93
|
+
# ActiveResource::Associations#belongs_to
|
|
94
|
+
|
|
95
|
+
def belongs_to name, options={}
|
|
96
|
+
# continue to let the original do all the work.
|
|
97
|
+
super.tap do |reflection|
|
|
98
|
+
|
|
99
|
+
# Defines a setter caching the value in an instance variable for later
|
|
100
|
+
# retrieval. Stash value directly in the attributes using the
|
|
101
|
+
# nested_attributes functionality server-side.
|
|
102
|
+
define_method "#{reflection.name}=" do |value|
|
|
103
|
+
attributes[reflection.foreign_key] = value.id # set the foreign key
|
|
104
|
+
attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
|
|
105
|
+
instance_variable_set(:"@#{reflection.name}", value) # set the cached value
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Adds getter and setter methods for `has_one` associations that are
|
|
112
|
+
# through a `belongs_to` association. Assumes that the information about
|
|
113
|
+
# the association is generated in the nested attributes by a HasOneThrough
|
|
114
|
+
# serializer.
|
|
115
|
+
#
|
|
116
|
+
# In this example, if we did not go through the identity association the
|
|
117
|
+
# primary keys would be generated, but upon save, an error would be thrown
|
|
118
|
+
# because it is an unknown attribute. This only happens with `belongs_to`
|
|
119
|
+
# methods as they contain the primary_key.
|
|
120
|
+
#
|
|
121
|
+
# For example, consider `user_id` and `zone_id` primary keys:
|
|
122
|
+
#
|
|
123
|
+
# class PostSerializer < ActiveModel::Serializer
|
|
124
|
+
# embed :ids
|
|
125
|
+
#
|
|
126
|
+
# has_one :blog
|
|
127
|
+
# has_one :company, :zone through: :blog
|
|
128
|
+
# end
|
|
129
|
+
#
|
|
130
|
+
# It will generate the following json:
|
|
131
|
+
#
|
|
132
|
+
# {
|
|
133
|
+
# "post": {
|
|
134
|
+
# "id": 1,
|
|
135
|
+
# "blog_id": 2,
|
|
136
|
+
# "blog_attributes": {
|
|
137
|
+
# "id": 2,
|
|
138
|
+
# "company_id": 3
|
|
139
|
+
# }
|
|
140
|
+
# }
|
|
141
|
+
# }
|
|
142
|
+
#
|
|
143
|
+
# An ActiveResource can define `belongs_to` with :through to read from
|
|
144
|
+
# nested attributes for fetching by primary_key or setting to save.
|
|
145
|
+
#
|
|
146
|
+
# class Post < Daylight::API
|
|
147
|
+
# belongs_to :blog
|
|
148
|
+
# has_one :company, through: :blog
|
|
149
|
+
# end
|
|
150
|
+
#
|
|
151
|
+
# So that:
|
|
152
|
+
#
|
|
153
|
+
# p = Post.find(1)
|
|
154
|
+
# p.blog # => #<Blog @attributes={"id"=>1}>
|
|
155
|
+
# p.company # => #<Company @attributes={"id"=>3}>
|
|
156
|
+
#
|
|
157
|
+
# And setting these associations will work with passing validations:
|
|
158
|
+
#
|
|
159
|
+
# p.company = Company.find(1)
|
|
160
|
+
# p.save # => true
|
|
161
|
+
|
|
162
|
+
def has_one_through name, options
|
|
163
|
+
through = options.delete(:through).to_s
|
|
164
|
+
|
|
165
|
+
create_reflection(:has_one, name, options).tap do |reflection|
|
|
166
|
+
nested_attributes_key = "#{reflection.name}_attributes"
|
|
167
|
+
through_attributes_key = "#{through}_attributes"
|
|
168
|
+
|
|
169
|
+
define_cached_method reflection.name, index: through_attributes_key do
|
|
170
|
+
reflection.klass.find(attributes[through_attributes_key][reflection.foreign_key])
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
define_method "#{reflection.name}=" do |value|
|
|
174
|
+
through_attributes = attributes["#{through}_attributes"] ||= {}
|
|
175
|
+
|
|
176
|
+
through_attributes[reflection.foreign_key] = value.id
|
|
177
|
+
through_attributes[nested_attributes_key] = value
|
|
178
|
+
instance_variable_set(:"@#{reflection.name}", value)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
##
|
|
184
|
+
# Fix bug in has_one that is not creating the request correctly.
|
|
185
|
+
# Use `where` functionality as it peforms the function that is needed
|
|
186
|
+
#
|
|
187
|
+
# Allows the has_one :through association.
|
|
188
|
+
#
|
|
189
|
+
# See:
|
|
190
|
+
# has_one_through
|
|
191
|
+
|
|
192
|
+
def has_one(name, options = {})
|
|
193
|
+
return has_one_through(name, options) if options.has_key? :through
|
|
194
|
+
|
|
195
|
+
create_reflection(:has_one, name, options).tap do |reflection|
|
|
196
|
+
define_cached_method reflection.name do
|
|
197
|
+
reflection.klass.where(:"#{self.class.element_name}_id" => self.id).first
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
define_method "#{reflection.name}=" do |value|
|
|
201
|
+
attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
|
|
202
|
+
value.attributes[:"#{self.class.element_name}_id"] = self.id
|
|
203
|
+
instance_variable_set(:"@#{reflection.name}", value) # set the cached value
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# Adds a method to the model that calls the remote action for its data.
|
|
210
|
+
#
|
|
211
|
+
# Example:
|
|
212
|
+
#
|
|
213
|
+
# remote :posts_by_popularity, class_name: 'post'
|
|
214
|
+
#
|
|
215
|
+
|
|
216
|
+
def remote name, options
|
|
217
|
+
create_reflection(:remote, name, options).tap do |reflection|
|
|
218
|
+
define_cached_method reflection.name do
|
|
219
|
+
call_remote(reflection.name, reflection.klass)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
def define_cached_method method_name, options={}, &block
|
|
226
|
+
# define an uncached method to call
|
|
227
|
+
uncached_method_name = :"#{method_name}_without_cache"
|
|
228
|
+
define_method(uncached_method_name, block)
|
|
229
|
+
|
|
230
|
+
# define the cached wrapper around the uncached method
|
|
231
|
+
define_method method_name do
|
|
232
|
+
ivar_name = :"@#{method_name}"
|
|
233
|
+
cache_key = options[:cache_key] || method_name
|
|
234
|
+
attributes = options.has_key?(:index) ? @attributes[options[:index]] : @attributes
|
|
235
|
+
|
|
236
|
+
if instance_variable_defined?(ivar_name)
|
|
237
|
+
instance_variable_get(ivar_name)
|
|
238
|
+
elsif attributes.include?(cache_key)
|
|
239
|
+
attributes[cache_key]
|
|
240
|
+
else
|
|
241
|
+
instance_variable_set ivar_name, send(uncached_method_name)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Runs `alias_api` in the console during API development to re-alias the to the
|
|
3
|
+
# reloaded constants. Otherwise, they will hold onto the old un-reloaded
|
|
4
|
+
# constants in memory.
|
|
5
|
+
#
|
|
6
|
+
# This is not intended for end-users of the API, but can be used by them if
|
|
7
|
+
# needed.
|
|
8
|
+
#
|
|
9
|
+
# You must enable this functionality:
|
|
10
|
+
#
|
|
11
|
+
# require 'daylight/client_reloader'
|
|
12
|
+
#
|
|
13
|
+
# NOTE: Currently only works with IRB
|
|
14
|
+
|
|
15
|
+
module ClientReloader
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
included do
|
|
19
|
+
def reload! print=true
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
puts "Realiasing API..." if print
|
|
23
|
+
suppress_warnings do
|
|
24
|
+
Daylight::API.send(:alias_apis)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def include
|
|
31
|
+
if console && defined?(console::ExtendCommandBundle)
|
|
32
|
+
console::ExtendCommandBundle.class_eval do
|
|
33
|
+
include ClientReloader
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def console
|
|
39
|
+
# we'll figure a way to set other consoles (eg. pry) later if neccessary
|
|
40
|
+
@console ||= IRB rescue nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
ClientReloader.include
|