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
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Mixin helpers to get specific params in an +ActionController+
|
|
3
|
+
module Daylight::Helpers
|
|
4
|
+
def scoped_params
|
|
5
|
+
params[:scopes]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def filter_params
|
|
9
|
+
params[:filters]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def order_params
|
|
13
|
+
params[:order] if params[:order].present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def limit_params
|
|
17
|
+
params[:limit] if params[:limit].present?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def offset_params
|
|
21
|
+
# non-integer offsets are allowed by offset, we do the check for you
|
|
22
|
+
Integer(params[:offset]) if params[:offset].present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def associated_params
|
|
26
|
+
params[:associated]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remoted_params
|
|
30
|
+
params[:remoted]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Mixin to simulate access to params (from Helpers) outside of ActiveController context
|
|
3
|
+
module Daylight::Params
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
class HelperProxy
|
|
7
|
+
include Daylight::Helpers
|
|
8
|
+
|
|
9
|
+
attr_accessor :params
|
|
10
|
+
|
|
11
|
+
def initialize params
|
|
12
|
+
@params = params
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
##
|
|
18
|
+
# Creates +params+ method and yields to block, undefine the param method
|
|
19
|
+
def with_helper params
|
|
20
|
+
yield HelperProxy.new(params)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Methods in which to refine a query by a model's scopes or attributes
|
|
3
|
+
module Daylight::Refiners
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
module Extension
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
##
|
|
11
|
+
# Extends subclasses of ActiveRecord::Base with the Daylight::Refiners features
|
|
12
|
+
# This hooks into the `inherited` method chain to perform this extension.
|
|
13
|
+
def inherited active_record
|
|
14
|
+
active_record.send(:include, Daylight::Refiners)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Helper to determine whether a request to use an attribute is valid or invalid
|
|
22
|
+
# Keeps track of which attributes are part of the request.
|
|
23
|
+
class AttributeSeive
|
|
24
|
+
attr_reader :valid_attribute_names, :attribute_names
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Initializes with the valid attributes and requested attributes
|
|
28
|
+
def initialize valid_attribute_names, attribute_names
|
|
29
|
+
@valid_attribute_names, @attribute_names = valid_attribute_names, [attribute_names].flatten.compact.map(&:to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# List of the invalid attributes
|
|
34
|
+
def invalid_attributes
|
|
35
|
+
@invalid_attributes ||= attribute_names - (attribute_names & valid_attribute_names)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# List of the valid attributes
|
|
40
|
+
def valid_attributes
|
|
41
|
+
@valid_attributes ||= attribute_names & valid_attribute_names
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Returns +true+ if there are any invalid attributes
|
|
46
|
+
#
|
|
47
|
+
# See:
|
|
48
|
+
# #invalid_attributes
|
|
49
|
+
def attributes_valid?
|
|
50
|
+
invalid_attributes.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Mixin refiners into an +ActiveRecord+ model
|
|
56
|
+
module ClassMethods
|
|
57
|
+
include Daylight::Params
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Returns currently registered scopes or empty Array
|
|
61
|
+
def registered_scopes
|
|
62
|
+
@registered_scopes ||= []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Remember the name of +scopes+ that are defined by the model
|
|
67
|
+
# This is a method chain and will call ActiveRecord.scope
|
|
68
|
+
def scope(name, body, &block)
|
|
69
|
+
registered_scopes << name.to_s
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Returns whether the +name+ matches a defined scope
|
|
75
|
+
def scoped?(name)
|
|
76
|
+
name.present? && registered_scopes.include?(name.to_s)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Calls defined scopes on the model and returns the resulting +ActiveRecord::Relation+.
|
|
81
|
+
# Raises +ArgumentError+ if the model scope is unknown.
|
|
82
|
+
def scoped_by *scope_names
|
|
83
|
+
seive = AttributeSeive.new(registered_scopes, scope_names)
|
|
84
|
+
raise ArgumentError, "Unknown scope: #{seive.invalid_attributes.join(',')}" unless seive.attributes_valid?
|
|
85
|
+
|
|
86
|
+
seive.valid_attributes.inject(all) do |scopes, scope_name|
|
|
87
|
+
scopes.send(scope_name)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Helper to return the defined reflection names
|
|
93
|
+
#
|
|
94
|
+
# See:
|
|
95
|
+
# filter_by
|
|
96
|
+
def reflection_names
|
|
97
|
+
reflections.keys.map(&:to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# Supplies where conditions and returns the resulting +ActiveRecord::Relation+.
|
|
102
|
+
# Raises +ArgumentError+ if the keys are not valid attributes on the model.
|
|
103
|
+
def filter_by params
|
|
104
|
+
where (params||{}).with_indifferent_access.assert_valid_keys(attribute_names + reflection_names)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Wrapper around +order+ to perform key checking to +attribute_names+
|
|
109
|
+
# Raises +ArgumentError+ if the attribute is unknown.
|
|
110
|
+
def order_by value
|
|
111
|
+
keys =
|
|
112
|
+
case value
|
|
113
|
+
when String; value.split(',').map {|column| column.strip.split(/\s+/).first }
|
|
114
|
+
when Hash; value.keys
|
|
115
|
+
when Array; value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
seive = AttributeSeive.new(self.attribute_names, keys)
|
|
119
|
+
raise ArgumentError, "Unknown attribute: #{seive.invalid_attributes.join(',')}" unless seive.attributes_valid?
|
|
120
|
+
|
|
121
|
+
order(value)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def refine_by params
|
|
125
|
+
with_helper(params) do |helper|
|
|
126
|
+
self.
|
|
127
|
+
scoped_by(helper.scoped_params).
|
|
128
|
+
filter_by(helper.filter_params).
|
|
129
|
+
order_by(helper.order_params).
|
|
130
|
+
limit(helper.limit_params).
|
|
131
|
+
offset(helper.offset_params)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def associated params
|
|
136
|
+
with_helper(params) do |helper|
|
|
137
|
+
self.
|
|
138
|
+
find(params[:id]).
|
|
139
|
+
associated(helper.associated_params).
|
|
140
|
+
scoped_by(helper.scoped_params).
|
|
141
|
+
filter_by(helper.filter_params).
|
|
142
|
+
order_by(helper.order_params).
|
|
143
|
+
limit(helper.limit_params).
|
|
144
|
+
offset(helper.offset_params)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def remoted params
|
|
149
|
+
with_helper(params) do |helper|
|
|
150
|
+
self.
|
|
151
|
+
find(params[:id]).
|
|
152
|
+
remoted(helper.remoted_params)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def remoted_methods
|
|
157
|
+
@remoted_methods ||= []
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_remoted(method)
|
|
161
|
+
if method_defined?(method)
|
|
162
|
+
remoted_methods.push(method.to_sym).uniq!
|
|
163
|
+
else
|
|
164
|
+
Rails.logger.warn "Configured remote method '#{method}' in #{self.name} routes does not exist!"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def remoted?(method)
|
|
169
|
+
remoted_methods.include? method.to_sym
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
included do
|
|
174
|
+
##
|
|
175
|
+
# Helper to follow a named association if it exists
|
|
176
|
+
def associated name
|
|
177
|
+
raise ArgumentError, "Unknown association: #{name}" unless self.class.reflection_names.include? name.to_s
|
|
178
|
+
public_send(name)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def remoted method
|
|
182
|
+
raise ArgumentError, "Unknown remote: #{method}" unless self.class.remoted?(method)
|
|
183
|
+
public_send(method)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Rails extensions, patches, fixes needed to execute a Daylight::Server
|
|
2
|
+
# In the future, these could be configurable or contributed back
|
|
3
|
+
|
|
4
|
+
require 'extensions/array_ext' # non-destructive version of `extract_options`
|
|
5
|
+
require 'extensions/inflections' # custom inflections for the ActiveSupport::Inflector
|
|
6
|
+
require 'extensions/autosave_association_fix' # fix for autosaving `inverse_of` associations
|
|
7
|
+
require 'extensions/has_one_serializer_ext' # serializer recognizes belong_to :through association
|
|
8
|
+
require 'extensions/nested_attributes_ext' # associates two previously existing records
|
|
9
|
+
require 'extensions/read_only_attributes' # serializer support for `read_only` attributes
|
|
10
|
+
require 'extensions/render_json_meta' # adds metadata to the json response
|
|
11
|
+
require 'extensions/route_options' # adds associated, remoted options to routes
|
|
12
|
+
require 'extensions/versioned_url_for' # uses versioned paths for `url_for`
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Include into Rails server to handle Daylight::API queries
|
|
16
|
+
module Daylight
|
|
17
|
+
extend ActiveSupport::Autoload
|
|
18
|
+
|
|
19
|
+
autoload :Helpers
|
|
20
|
+
autoload :Params
|
|
21
|
+
autoload :Refiners
|
|
22
|
+
autoload :APIController
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A convinience alias that will avoids any name collisions
|
|
26
|
+
APIController = Daylight::APIController unless defined?(APIController)
|
|
27
|
+
|
|
28
|
+
# Hook into ActiveRecord::Base `inherited` chain to extend subclasses
|
|
29
|
+
ActiveRecord::Base.send(:include, Daylight::Refiners::Extension)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'rake'
|
|
2
|
+
require 'rails/tasks'
|
|
3
|
+
require 'rdoc/task'
|
|
4
|
+
|
|
5
|
+
namespace :doc do
|
|
6
|
+
namespace :api do
|
|
7
|
+
|
|
8
|
+
desc 'Pre-generate the API documentation'
|
|
9
|
+
task generate: %w[environment doc:api:clean] do
|
|
10
|
+
require 'artifice'
|
|
11
|
+
require 'open-uri'
|
|
12
|
+
|
|
13
|
+
Artifice.activate_with(Rails.application.class)
|
|
14
|
+
|
|
15
|
+
Rails.application.eager_load!
|
|
16
|
+
helpers = Daylight::Documentation.routes.url_helpers
|
|
17
|
+
models = ActiveRecord::Base.descendants
|
|
18
|
+
open helpers.index_url(host: 'localhost')
|
|
19
|
+
models.each do |model|
|
|
20
|
+
open helpers.model_url(model.name.underscore, host: 'localhost')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'Clear the API documentation'
|
|
25
|
+
task clean: %w[environment] do
|
|
26
|
+
helpers = Daylight::Documentation.routes.url_helpers
|
|
27
|
+
path = helpers.index_path
|
|
28
|
+
|
|
29
|
+
# remove the index
|
|
30
|
+
FileUtils.rm_rf File.join(Rails.root, 'public', path.sub(%r{/$}, '.html'))
|
|
31
|
+
|
|
32
|
+
# and the files
|
|
33
|
+
FileUtils.rm_rf File.join(Rails.root, 'public', path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class Array
|
|
2
|
+
##
|
|
3
|
+
# Return any options without removing them from the Array.
|
|
4
|
+
# This is a non-destrcutive version of `extract_options!`
|
|
5
|
+
# Although the name is a misnomer, leaving it for consistency
|
|
6
|
+
def extract_options
|
|
7
|
+
last.is_a?(Hash) && last.extractable_options? ? last : {}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
##
|
|
2
|
+
# The problem is that autosaving with models that have `inverse_of` and
|
|
3
|
+
# `accepts_nested_attributes_for` causes SystemStackError
|
|
4
|
+
#
|
|
5
|
+
# Solution is to keep track in an instance variable on each instance whether
|
|
6
|
+
# the object has been already autosaved. On first pass it will determine if
|
|
7
|
+
# it needs saving (original behavior), later passes stop cyclic traversing by
|
|
8
|
+
# always return false.
|
|
9
|
+
#
|
|
10
|
+
# This should be removed when similar behavior is applied to ActiveRecord's
|
|
11
|
+
# `changed_for_autosave?`, likely candidate is 4.1.0 version of the gem.
|
|
12
|
+
#
|
|
13
|
+
# Original problem pulled together by:
|
|
14
|
+
# https://github.com/rails/rails/pull/8549
|
|
15
|
+
#
|
|
16
|
+
# Bug is is documented here:
|
|
17
|
+
# https://github.com/rails/rails/issues/7809
|
|
18
|
+
#
|
|
19
|
+
# Monkey patch supplied here:
|
|
20
|
+
# https://github.com/mtaylor/Rails-Inverse-Nested-Attr-Bug/blob/master/app/patches/rails/active_record/autosave_association.rb
|
|
21
|
+
#
|
|
22
|
+
# See
|
|
23
|
+
# ActiveRecord::Base#changed_for_autosave?
|
|
24
|
+
|
|
25
|
+
module AutosaveAssociationFix
|
|
26
|
+
extend ActiveSupport::Concern
|
|
27
|
+
|
|
28
|
+
# Returns whether or not this record has been changed in any way (including whether
|
|
29
|
+
# any of its nested autosave associations are likewise changed)
|
|
30
|
+
def changed_for_autosave?
|
|
31
|
+
@_changed_for_autosave_called ||= false
|
|
32
|
+
if @_changed_for_autosave_called
|
|
33
|
+
# traversing a cyclic graph of objects; stop it
|
|
34
|
+
result = false
|
|
35
|
+
else
|
|
36
|
+
begin
|
|
37
|
+
@_changed_for_autosave_called = true
|
|
38
|
+
result = super
|
|
39
|
+
ensure
|
|
40
|
+
@_changed_for_autosave_called = false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
ActiveRecord::Base.class_eval do
|
|
48
|
+
include AutosaveAssociationFix
|
|
49
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'active_model_serializers'
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# Allows `:through` options to be specified on has_one associations
|
|
5
|
+
#
|
|
6
|
+
# A `has_one` associations may be specified so that:
|
|
7
|
+
# 1. `belongs_to` which has a foreign_key
|
|
8
|
+
# 2. through another association
|
|
9
|
+
#
|
|
10
|
+
# For example:
|
|
11
|
+
#
|
|
12
|
+
# class Foo
|
|
13
|
+
# belongs_to :bar
|
|
14
|
+
# belongs_to :biz, through: :bar
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# When the serializer is specified:
|
|
18
|
+
#
|
|
19
|
+
# class FooSerializer
|
|
20
|
+
# embed :ids
|
|
21
|
+
#
|
|
22
|
+
# has_one :bar
|
|
23
|
+
# has_one :biz
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# The serializer does not know it doesn't have a direct association to "biz"
|
|
27
|
+
#
|
|
28
|
+
# In this case, the serializer will attempt to put the foreign_key for "biz"
|
|
29
|
+
# in the rendered json. If the original `Foo` object is saved it will fail
|
|
30
|
+
# because it does not know about this foreign_key.
|
|
31
|
+
#
|
|
32
|
+
# HasOneThrough adds functionality to the serializer to specify through
|
|
33
|
+
# relationships will put "biz" data in an nested attributes hash instead.
|
|
34
|
+
# When used in concert with `accepts_nested_attributes_for`, the data
|
|
35
|
+
# will be passed correctly back to the update methods on `Foo`:
|
|
36
|
+
#
|
|
37
|
+
# class FooSerializer
|
|
38
|
+
# embed :ids
|
|
39
|
+
#
|
|
40
|
+
# has_one :bar
|
|
41
|
+
# has_one :biz, through: :bar
|
|
42
|
+
# end
|
|
43
|
+
|
|
44
|
+
module ActiveModel::Serializer::Associations
|
|
45
|
+
|
|
46
|
+
class HasOneThrough < HasOne
|
|
47
|
+
def embeddable?
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def key
|
|
52
|
+
"#{through}_attributes"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def through
|
|
56
|
+
option :through
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def through_object
|
|
60
|
+
@object ||= source_serializer.object.send(through)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def attributes
|
|
64
|
+
source_serializer.node[key] || {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def primary_key
|
|
68
|
+
through_object.class.primary_key
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize
|
|
72
|
+
return unless associated_object
|
|
73
|
+
|
|
74
|
+
serialize_ids.merge({
|
|
75
|
+
:"#{@name}_attributes" => find_serializable(associated_object).serializable_hash
|
|
76
|
+
})
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def serialize_ids
|
|
80
|
+
return unless associated_object
|
|
81
|
+
|
|
82
|
+
attributes.merge({
|
|
83
|
+
:"#{primary_key}" => through_object.send(primary_key),
|
|
84
|
+
:"#{@name}_id" => associated_object.read_attribute_for_serialization(embed_key)
|
|
85
|
+
})
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
module HasOneSerializerExt
|
|
91
|
+
extend ActiveSupport::Concern
|
|
92
|
+
|
|
93
|
+
included do
|
|
94
|
+
attr_reader :node
|
|
95
|
+
|
|
96
|
+
def self.has_one(*attrs)
|
|
97
|
+
klass = if attrs.extract_options[:through]
|
|
98
|
+
ActiveModel::Serializer::Associations::HasOneThrough
|
|
99
|
+
else
|
|
100
|
+
ActiveModel::Serializer::Associations::HasOne
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
associate(klass, attrs)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Add the HasOneSerializerExt to the Serializer
|
|
109
|
+
ActiveModel::Serializer.class_eval do
|
|
110
|
+
include HasOneSerializerExt
|
|
111
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Problem: Nested attributes will will fail to associate two records if they both already exist
|
|
3
|
+
# Solution: Associate the existing records defined by 'id' attributes before updating them
|
|
4
|
+
#
|
|
5
|
+
# Idea abstracted from implementation detailed by:
|
|
6
|
+
# https://stackoverflow.com/questions/6346134/use-rails-nested-model-to-create-outer-object-and-simultaneously-edit-existi/12064875#12064875
|
|
7
|
+
module NestedAttributesExt
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
class_attribute :nested_resource_names
|
|
12
|
+
self.nested_resource_names = [].freeze
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Associate any existing records that may be missing before running any updates on them.
|
|
16
|
+
#
|
|
17
|
+
# See:
|
|
18
|
+
# ActiveRecord::NestedAttributes#assign_nested_attributes_for_collection_association
|
|
19
|
+
def assign_nested_attributes_for_collection_association association_name, attributes_collection
|
|
20
|
+
return if attributes_collection.nil?
|
|
21
|
+
|
|
22
|
+
associate_existing_records(association_name, attributes_collection)
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# Ignore any association with nil attributes
|
|
29
|
+
#
|
|
30
|
+
# See:
|
|
31
|
+
# ActiveRecord::NestedAttributes#assign_nested_attributes_for_one_to_one_association
|
|
32
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
|
33
|
+
return if attributes.nil?
|
|
34
|
+
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module ClassMethods
|
|
40
|
+
##
|
|
41
|
+
# Saves off the reflection names that the nested attributes are accepted for.
|
|
42
|
+
# Does not alter original behavoir or arguments.
|
|
43
|
+
#
|
|
44
|
+
# See:
|
|
45
|
+
# ActiveRecord::NestedAttributes#accepts_nested_attributes_for
|
|
46
|
+
def accepts_nested_attributes_for *attr_names
|
|
47
|
+
nested_resources = attr_names.dup
|
|
48
|
+
nested_resources.extract_options!
|
|
49
|
+
self.nested_resource_names = nested_resources.map(&:to_sym).freeze
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
##
|
|
58
|
+
# Determines unassociated records from existing records on the association and adds them
|
|
59
|
+
def associate_existing_records(association_name, attributes_collection)
|
|
60
|
+
|
|
61
|
+
# determine existing records, bail if there are none specified by 'id'
|
|
62
|
+
attribute_ids = attributes_collection.map {|a| (a['id'] || a[:id]) }.compact
|
|
63
|
+
return if attribute_ids.empty?
|
|
64
|
+
|
|
65
|
+
association = association(association_name)
|
|
66
|
+
primary_key = association.klass.primary_key.to_sym
|
|
67
|
+
|
|
68
|
+
# get known existing ids on the association
|
|
69
|
+
existing_record_ids = if association.loaded?
|
|
70
|
+
association.target.map(&primary_key)
|
|
71
|
+
else
|
|
72
|
+
association.scope.where(primary_key => attribute_ids).pluck(primary_key)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# unassociated records are those that are not part of existing in the association
|
|
76
|
+
unassociated_record_ids = attribute_ids.map(&:to_s) - existing_record_ids.map(&:to_s)
|
|
77
|
+
|
|
78
|
+
# we are about to set all foreign_keys, remove any foreign_key references in
|
|
79
|
+
# unassigned records attributes so they don't get clobbered
|
|
80
|
+
attributes_collection.map do |a|
|
|
81
|
+
if unassociated_record_ids.include?((a['id'] || a[:id]).to_s)
|
|
82
|
+
key = association.reflection.foreign_key
|
|
83
|
+
a.delete(key) || a.delete(key.to_sym)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# concat the unassociated records to the association
|
|
88
|
+
association.concat(association.klass.find(unassociated_record_ids))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
ActiveRecord::Base.class_eval do
|
|
93
|
+
include NestedAttributesExt
|
|
94
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module ReadOnlyAttributes
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
# place the read_only attributes along side the other class_attributes for a Serializer
|
|
6
|
+
class << self
|
|
7
|
+
class_attribute :_read_only
|
|
8
|
+
self._read_only = []
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
##
|
|
14
|
+
# Records the attribues as read only then stores them as attributes
|
|
15
|
+
#
|
|
16
|
+
# See
|
|
17
|
+
# ActiveModel::Serializer.attributes
|
|
18
|
+
def read_only(*attrs)
|
|
19
|
+
# strip predicate '?' marks of and convert them to symbols
|
|
20
|
+
normalized_attrs = attrs.map { |a| a.to_s.gsub(/\?$/,'').to_sym }
|
|
21
|
+
|
|
22
|
+
# record which attributes will be read only
|
|
23
|
+
self._read_only = _read_only.dup
|
|
24
|
+
self._read_only.push(*normalized_attrs).uniq
|
|
25
|
+
|
|
26
|
+
# pass them off to attributes to do all the work
|
|
27
|
+
attributes(*attrs)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Add the ReadOnlyAttributes to the Serializer
|
|
33
|
+
ActiveModel::Serializer.class_eval do
|
|
34
|
+
include ReadOnlyAttributes
|
|
35
|
+
end
|