daylight 0.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +113 -0
  3. data/app/controllers/daylight_documentation/documentation_controller.rb +27 -0
  4. data/app/helpers/daylight_documentation/documentation_helper.rb +57 -0
  5. data/app/views/daylight_documentation/documentation/_header.haml +4 -0
  6. data/app/views/daylight_documentation/documentation/index.haml +12 -0
  7. data/app/views/daylight_documentation/documentation/model.haml +114 -0
  8. data/app/views/layouts/documentation.haml +22 -0
  9. data/config/routes.rb +8 -0
  10. data/doc/actions.md +70 -0
  11. data/doc/benchmarks.md +17 -0
  12. data/doc/contribute.md +80 -0
  13. data/doc/develop.md +1205 -0
  14. data/doc/environment.md +109 -0
  15. data/doc/example.md +3 -0
  16. data/doc/framework.md +31 -0
  17. data/doc/install.md +128 -0
  18. data/doc/principles.md +42 -0
  19. data/doc/testing.md +107 -0
  20. data/doc/usage.md +970 -0
  21. data/lib/daylight/api.rb +293 -0
  22. data/lib/daylight/associations.rb +247 -0
  23. data/lib/daylight/client_reloader.rb +45 -0
  24. data/lib/daylight/collection.rb +161 -0
  25. data/lib/daylight/errors.rb +94 -0
  26. data/lib/daylight/inflections.rb +7 -0
  27. data/lib/daylight/mock.rb +282 -0
  28. data/lib/daylight/read_only.rb +88 -0
  29. data/lib/daylight/refinements.rb +63 -0
  30. data/lib/daylight/reflection_ext.rb +67 -0
  31. data/lib/daylight/resource_proxy.rb +226 -0
  32. data/lib/daylight/version.rb +10 -0
  33. data/lib/daylight.rb +27 -0
  34. data/rails/daylight/api_controller.rb +354 -0
  35. data/rails/daylight/documentation.rb +13 -0
  36. data/rails/daylight/helpers.rb +32 -0
  37. data/rails/daylight/params.rb +23 -0
  38. data/rails/daylight/refiners.rb +186 -0
  39. data/rails/daylight/server.rb +29 -0
  40. data/rails/daylight/tasks.rb +37 -0
  41. data/rails/extensions/array_ext.rb +9 -0
  42. data/rails/extensions/autosave_association_fix.rb +49 -0
  43. data/rails/extensions/has_one_serializer_ext.rb +111 -0
  44. data/rails/extensions/inflections.rb +6 -0
  45. data/rails/extensions/nested_attributes_ext.rb +94 -0
  46. data/rails/extensions/read_only_attributes.rb +35 -0
  47. data/rails/extensions/render_json_meta.rb +99 -0
  48. data/rails/extensions/route_options.rb +47 -0
  49. data/rails/extensions/versioned_url_for.rb +22 -0
  50. data/spec/config/dependencies.rb +2 -0
  51. data/spec/config/factory_girl.rb +4 -0
  52. data/spec/config/simplecov_rcov.rb +26 -0
  53. data/spec/config/test_api.rb +1 -0
  54. data/spec/controllers/documentation_controller_spec.rb +24 -0
  55. data/spec/dummy/README.rdoc +28 -0
  56. data/spec/dummy/Rakefile +6 -0
  57. data/spec/dummy/app/assets/images/.keep +0 -0
  58. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  59. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  60. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  61. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  62. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  63. data/spec/dummy/app/mailers/.keep +0 -0
  64. data/spec/dummy/app/models/.keep +0 -0
  65. data/spec/dummy/app/models/concerns/.keep +0 -0
  66. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  67. data/spec/dummy/bin/bundle +3 -0
  68. data/spec/dummy/bin/rails +4 -0
  69. data/spec/dummy/bin/rake +4 -0
  70. data/spec/dummy/config/application.rb +24 -0
  71. data/spec/dummy/config/boot.rb +5 -0
  72. data/spec/dummy/config/database.yml +25 -0
  73. data/spec/dummy/config/environment.rb +5 -0
  74. data/spec/dummy/config/environments/development.rb +29 -0
  75. data/spec/dummy/config/environments/production.rb +80 -0
  76. data/spec/dummy/config/environments/test.rb +36 -0
  77. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  78. data/spec/dummy/config/initializers/daylight.rb +1 -0
  79. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  80. data/spec/dummy/config/initializers/inflections.rb +16 -0
  81. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  82. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  83. data/spec/dummy/config/initializers/session_store.rb +3 -0
  84. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  85. data/spec/dummy/config/locales/en.yml +23 -0
  86. data/spec/dummy/config/routes.rb +59 -0
  87. data/spec/dummy/config.ru +4 -0
  88. data/spec/dummy/lib/assets/.keep +0 -0
  89. data/spec/dummy/log/.keep +0 -0
  90. data/spec/dummy/public/404.html +58 -0
  91. data/spec/dummy/public/422.html +58 -0
  92. data/spec/dummy/public/500.html +57 -0
  93. data/spec/dummy/public/favicon.ico +0 -0
  94. data/spec/helpers/documentation_helper_spec.rb +82 -0
  95. data/spec/lib/daylight/api_spec.rb +178 -0
  96. data/spec/lib/daylight/associations_spec.rb +325 -0
  97. data/spec/lib/daylight/collection_spec.rb +235 -0
  98. data/spec/lib/daylight/errors_spec.rb +111 -0
  99. data/spec/lib/daylight/mock_spec.rb +144 -0
  100. data/spec/lib/daylight/read_only_spec.rb +118 -0
  101. data/spec/lib/daylight/refinements_spec.rb +80 -0
  102. data/spec/lib/daylight/reflection_ext_spec.rb +50 -0
  103. data/spec/lib/daylight/resource_proxy_spec.rb +325 -0
  104. data/spec/rails/daylight/api_controller_spec.rb +421 -0
  105. data/spec/rails/daylight/helpers_spec.rb +41 -0
  106. data/spec/rails/daylight/params_spec.rb +45 -0
  107. data/spec/rails/daylight/refiners_spec.rb +178 -0
  108. data/spec/rails/extensions/array_ext_spec.rb +51 -0
  109. data/spec/rails/extensions/has_one_serializer_ext_spec.rb +135 -0
  110. data/spec/rails/extensions/nested_attributes_ext_spec.rb +177 -0
  111. data/spec/rails/extensions/render_json_meta_spec.rb +140 -0
  112. data/spec/rails/extensions/route_options_spec.rb +309 -0
  113. data/spec/rails/extensions/versioned_url_for_spec.rb +46 -0
  114. data/spec/spec_helper.rb +43 -0
  115. data/spec/support/migration_helper.rb +40 -0
  116. metadata +422 -0
@@ -0,0 +1,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