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