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
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
|