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,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,6 @@
1
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
2
+ inflect.uncountable 'anonymous'
3
+
4
+ inflect.acronym 'API'
5
+ inflect.acronym 'APIs'
6
+ 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