pretty-api 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e020194708396186f0a862e8fcdfe24b4bba1d9ce4449f7261bef0c2a76451d2
4
- data.tar.gz: fdf19f29e2a101b15a6f2bf4e48fcc84a5ae5bef7469c2eeea6490272cbe66e5
3
+ metadata.gz: 7ae1f5a395773d7742e4e8d35c5bd8ab123c474b5e0bef974b3f493b3149e97f
4
+ data.tar.gz: c501cd773a268868814c2c8ebe08dd425eea7be219c2f3d72e52cb292305aa0e
5
5
  SHA512:
6
- metadata.gz: 3a4a6b834fc2d8b2c27be2d27bae71b6c366584227071299dcdf0fbd58fcde1d9225a9051dc470acfbe1213bed65e8556eb7c4cc6dd03df83f2df2f233b3a9ca
7
- data.tar.gz: 883fd45dfb069e6fe71cde4603265ebab372bfbf0f64007b3aa1e76b164c1a9fdec5f5b143799162a0f8d4477746ea0c33d999b02b569af28bbe99808e92c6ab
6
+ metadata.gz: f6826d7e3534689df805027042f6e65282a29fcb08456c8384f5598b9c7727613de31e55d7daa92509e874ce2dd3dc2bb11c7f0b734bcfa3d3ffb1e197ad3e99
7
+ data.tar.gz: bf162d88d8828702df78a4e731f0dd4c64bdc1bebdd94cd61cece7f2a0047a622111de4460b513343cb5d38f3459643d12e8bd9918ab0f24e85611082caf2bf9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-06-01
4
+
5
+ - Support for ActionController::Parameters from Rails
6
+ - Support for self-referencing associations
7
+ - Support for circular accepts_nested_attributes_for
8
+ - Fix inaccurate destroy for nested attributes
9
+ - More specs
10
+
3
11
  ## [0.1.0] - 2024-05-30
4
12
 
5
13
  - Initial release
data/internal.md CHANGED
@@ -5,13 +5,26 @@ to avoid forgetting important implementations motivations.
5
5
 
6
6
  This gem can get a little confusing sometimes because `accepts_nested_attributes_for` can be used with any kind
7
7
  of associations (has_many, belongs_to, has_one, has_many_though, ...) and it can also be used to self reference itself
8
- or another association that reference itself. We must handle properly these use case to avoid infinite loop.
8
+ or another association that reference itself. We must handle properly these use case properly to avoid infinite loop.
9
9
 
10
10
  We are able to extract the dependency tree of an association with the internal method `nested_attributes_tree`. This
11
- method is implemented in two formats: one that returns an array structure, one that returns an hash structure. The library
12
- itself doesn't make any use of two formats, however we do want to support to type of structure for better user
13
- experience and compatibility. This is mostly useful for unit testings but this allow users to use the structure they
14
- want when they pass manually the association tree to the public helpers method.
11
+ method can be bypassed by passing explicitly your own structure. As of today, the structure looks like this
12
+
13
+ ```ruby
14
+ {
15
+ CompanyCar => {
16
+ organization: { allow_destroy: true, model: Organization, type: :belongs_to }
17
+ },
18
+ Organization => {
19
+ company_car: { allow_destroy: true, model: CompanyCar, type: :has_one },
20
+ services: { allow_destroy: true, model: Service, type: :has_many }
21
+ },
22
+ Phone => {},
23
+ Service => {
24
+ phones: { allow_destroy: true, model: Phone, type: :has_many }
25
+ }
26
+ }
27
+ ```
15
28
 
16
29
  ### Notes on the pretty nested attributes implementation
17
30
 
@@ -20,6 +33,3 @@ lead to odd behavior or potential accidental data loss. We must rely on the prim
20
33
  given parameter. If not done properly, it would append in the parameters `{ id: ..., _destroy: true}` wrongly thinking
21
34
  some associations dont exist. I believe ActiveRecord would raise a RecordNotFound exception to protect against this
22
35
  scenario, however we have unit tests to protect against this scenario to avoid code regression in the future.
23
-
24
- Internally it infers automatically the dependency tree of a record by calling `nested_attributes_tree` or by receiving
25
- an hash or an array by the user explicitly. This is subject to the spoken hash or array format spoken earlier.
@@ -1,52 +1,25 @@
1
1
  module PrettyApi
2
2
  module ActiveRecord
3
3
  class Associations
4
- def self.nested_attributes_tree(model, structure = :array)
5
- if structure == :array
6
- nested_attributes_tree_array(model)
7
- elsif structure == :hash
8
- nested_attributes_tree_hash(model)
9
- end
10
- end
11
-
12
- def self.nested_attributes_tree_hash(model, depth = {})
13
- nested_attributes_descriptions(model).index_by { |a| a[:id] }.each_with_object({}) do |(key, assoc), result|
14
- depth[key] ||= 0
4
+ def self.nested_attributes_tree(model, result = {})
5
+ model.nested_attributes_options.each_key do |association_name|
6
+ result[model] ||= {}
15
7
 
16
- next unless depth[key] < PrettyApi.max_nested_attributes_depth
8
+ association = attribute_association(model, association_name)
9
+ association_class = association.class_name.constantize
17
10
 
18
- depth[key] += 1
19
- result[assoc[:name]] = nested_attributes_tree_hash(assoc[:model], depth)
20
- depth[key] -= 1
21
- end
22
- end
23
-
24
- def self.nested_attributes_tree_array(model, depth = {})
25
- nested_attributes_descriptions(model).index_by { |a| a[:id] }.map do |(key, association)|
26
- depth[key] ||= 0
27
-
28
- next nil if depth[key] >= PrettyApi.max_nested_attributes_depth
11
+ result[model][association_name] = {
12
+ model: association_class,
13
+ type: association.macro,
14
+ allow_destroy: attribute_destroy_allowed?(model, association_name)
15
+ }
29
16
 
30
- depth[key] += 1
31
- result = { association[:name] => nested_attributes_tree_array(association[:model], depth) }
32
- depth[key] -= 1
17
+ next if result.key?(association_class)
33
18
 
34
- result.compact_blank.blank? ? association[:name] : result
35
- end.compact_blank
36
- end
37
-
38
- def self.nested_attributes_descriptions(model)
39
- model.nested_attributes_options.keys.map do |association_name|
40
- association_model = attribute_association_class(model, association_name)
41
- {
42
- id: "#{model}_#{association_name}",
43
- name: association_name,
44
- model: association_model,
45
- associations: association_model.nested_attributes_options.keys.map do |n|
46
- attribute_association_class(association_model, n)
47
- end
48
- }
19
+ result[association_class] = {}
20
+ nested_attributes_tree(association_class, result)
49
21
  end
22
+ result
50
23
  end
51
24
 
52
25
  def self.attribute_destroy_allowed?(model, attribute)
@@ -56,18 +29,6 @@ module PrettyApi
56
29
  def self.attribute_association(model, attribute)
57
30
  model.reflect_on_association(attribute).chain.last
58
31
  end
59
-
60
- def self.attribute_association_class(model, attribute)
61
- model.reflect_on_association(attribute).class_name.constantize
62
- end
63
-
64
- def self.association_type(association)
65
- association.macro
66
- end
67
-
68
- def self.association_primary_key(association)
69
- association.class_name.constantize.primary_key
70
- end
71
32
  end
72
33
  end
73
34
  end
@@ -1,56 +1,53 @@
1
1
  module PrettyApi
2
2
  module Errors
3
3
  class NestedErrors
4
- def self.parsed_nested_errors(record, attrs)
4
+ def initialize(nested_tree:)
5
+ @nested_tree = nested_tree
6
+ end
7
+
8
+ def parse(record)
5
9
  errors = record_only_errors(record)
6
10
 
7
- return errors if attrs.blank?
11
+ return errors if nested_tree.blank?
8
12
 
9
- parse_deep_nested_errors(record, attrs, errors)
13
+ parse_deep_nested_errors(record, nested_tree[record.class], errors)
10
14
 
11
15
  PrettyApi::Utils::Hash.deep_compact_blank(errors)
12
16
  end
13
17
 
14
- def self.parse_deep_nested_errors(record, attrs, result, parent_record = nil)
15
- case attrs
16
- when Hash
17
- attrs.each do |key, value|
18
- parse_association_errors(record, key, value, result, parent_record)
19
- end
20
- when Array
21
- attrs.each { |value| parse_deep_nested_errors record, value, result, parent_record }
22
- else
23
- parse_association_errors(record, attrs, nil, result, parent_record)
24
- end
25
- end
18
+ private
19
+
20
+ attr_reader :nested_tree
26
21
 
27
- def self.parse_association_errors(record, attr, nested_attrs, result, parent_record)
28
- association = record.send(attr)
22
+ def parse_deep_nested_errors(record, attrs, result, parent_record = nil)
23
+ attrs.each do |assoc_key, assoc_info|
24
+ association = record.send(assoc_key)
29
25
 
30
- return if association.blank?
31
- return if association == parent_record
26
+ next if association.blank?
27
+ next if association == parent_record
32
28
 
33
- if association.respond_to? :to_a
34
- parse_has_many_errors(record, association, attr, nested_attrs, result)
35
- else
36
- parse_has_one_errors(record, association, attr, nested_attrs, result)
29
+ if association.respond_to? :to_a
30
+ parse_has_many_errors(record, association, assoc_key, assoc_info, result)
31
+ else
32
+ parse_has_one_errors(record, association, assoc_key, assoc_info, result)
33
+ end
37
34
  end
38
35
  end
39
36
 
40
- def self.parse_has_many_errors(record, associations, attr, nested_attrs, result)
41
- result[attr] = {}
37
+ def parse_has_many_errors(record, associations, assoc_key, assoc_info, result)
38
+ result[assoc_key] = {}
42
39
  associations.each_with_index do |association, i|
43
- result[attr][i] = record_only_errors(association)
44
- parse_deep_nested_errors association, nested_attrs, result[attr][i], record if nested_attrs.present?
40
+ result[assoc_key][i] = record_only_errors(association)
41
+ parse_deep_nested_errors association, nested_tree[assoc_info[:model]], result[assoc_key][i], record
45
42
  end
46
43
  end
47
44
 
48
- def self.parse_has_one_errors(record, association, attr, nested_attrs, result)
49
- result[attr] = record_only_errors(association)
50
- parse_deep_nested_errors association, nested_attrs, result[attr], record if nested_attrs.present?
45
+ def parse_has_one_errors(record, association, assoc_key, assoc_info, result)
46
+ result[assoc_key] = record_only_errors(association)
47
+ parse_deep_nested_errors association, nested_tree[assoc_info[:model]], result[assoc_key], record
51
48
  end
52
49
 
53
- def self.record_only_errors(record)
50
+ def record_only_errors(record)
54
51
  record.errors.as_json.reject { |k, _v| k.to_s.include?(".") }
55
52
  end
56
53
  end
@@ -8,13 +8,13 @@ module PrettyApi
8
8
 
9
9
  attrs ||= PrettyApi::ActiveRecord::Associations.nested_attributes_tree(record.class)
10
10
 
11
- PrettyApi::Parameters::NestedAttributes.parse_nested_attributes(record, params, attrs)
11
+ PrettyApi::Parameters::NestedAttributes.new(nested_tree: attrs).parse(record, params)
12
12
  end
13
13
 
14
14
  def pretty_nested_errors(record, attrs = nil)
15
15
  attrs ||= PrettyApi::ActiveRecord::Associations.nested_attributes_tree(record.class)
16
16
 
17
- PrettyApi::Errors::NestedErrors.parsed_nested_errors(record, attrs)
17
+ PrettyApi::Errors::NestedErrors.new(nested_tree: attrs).parse(record)
18
18
  end
19
19
  end
20
20
  end
@@ -1,66 +1,66 @@
1
1
  module PrettyApi
2
2
  module Parameters
3
3
  class NestedAttributes
4
- def self.parse_nested_attributes(record, params, attrs)
4
+ def initialize(nested_tree:)
5
+ @nested_tree = nested_tree
6
+ end
7
+
8
+ def parse(record, params)
9
+ parse_nested_attributes(record, params, nested_tree[record.class])
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :nested_tree
15
+
16
+ def parse_nested_attributes(record, params, attrs)
5
17
  return {} if params.blank?
6
18
 
7
- case attrs
8
- when Hash, Array
9
- parse_deep_nested_attributes(record, params, attrs)
10
- when String, Symbol
11
- if params.key?(attrs)
12
- include_associations_to_destroy(record, params, attrs)
13
- params["#{attrs}_attributes"] = params.delete(attrs)
14
- end
19
+ attrs.each do |assoc_key, assoc_info|
20
+ next unless params.key?(assoc_key)
21
+
22
+ parse_deep_nested_attributes(record, params, assoc_key, assoc_info)
23
+
24
+ include_associations_to_destroy(record, params, assoc_key, assoc_info)
25
+ params["#{assoc_key}_attributes"] = params.delete(assoc_key)
15
26
  end
16
27
 
17
28
  params
18
29
  end
19
30
 
20
- def self.parse_deep_nested_attributes(record, params, attrs)
21
- case attrs
22
- when Hash
23
- attrs.each do |assoc_key, nested_assoc|
24
- if params[assoc_key].is_a? Array
25
- parse_has_many_association(record, params, assoc_key, nested_assoc)
26
- else
27
- parse_has_one_association(record, params, assoc_key, nested_assoc)
28
- end
29
- parse_nested_attributes(record, params, assoc_key)
30
- end
31
- when Array
32
- attrs.each { |assoc_or_nested_assoc| parse_nested_attributes(record, params, assoc_or_nested_assoc) }
31
+ def parse_deep_nested_attributes(record, params, assoc_key, assoc_info)
32
+ if assoc_info[:type] == :has_many
33
+ parse_has_many_association(record, params, assoc_key, assoc_info)
34
+ else
35
+ parse_has_one_association(record, params, assoc_key, assoc_info)
33
36
  end
34
37
  end
35
38
 
36
- def self.parse_has_many_association(record, params, assoc_key, nested_assoc)
37
- params[assoc_key].each do |p|
39
+ def parse_has_many_association(record, params, assoc_key, assoc_info)
40
+ (params[assoc_key] || []).each do |p|
38
41
  assoc_primary_key = record.try(:class).try(:primary_key)
39
42
  assoc = record.try(assoc_key).try(:detect) { |r| r.try(assoc_primary_key) == p[assoc_primary_key] }
40
- parse_nested_attributes(assoc, p, nested_assoc)
43
+ parse_nested_attributes(assoc, p, nested_tree[assoc_info[:model]])
41
44
  end
42
45
  end
43
46
 
44
- def self.parse_has_one_association(record, params, assoc_key, nested_assoc)
45
- parse_nested_attributes(record.try(assoc_key), params[assoc_key], nested_assoc)
47
+ def parse_has_one_association(record, params, assoc_key, assoc_info)
48
+ parse_nested_attributes(record.try(assoc_key), params[assoc_key], nested_tree[assoc_info[:model]])
46
49
  end
47
50
 
48
- def self.include_associations_to_destroy(record, params, attr)
51
+ def include_associations_to_destroy(record, params, assoc_key, assoc_info)
49
52
  return unless PrettyApi.destroy_missing_associations && record.present?
50
53
 
51
- association = PrettyApi::ActiveRecord::Associations.attribute_association(record.class, attr)
52
-
53
- return unless PrettyApi::ActiveRecord::Associations.attribute_destroy_allowed?(record.class, attr)
54
-
55
- primary_key = PrettyApi::ActiveRecord::Associations.association_primary_key(association)
54
+ return unless assoc_info[:allow_destroy]
56
55
 
57
- assoc_type = PrettyApi::ActiveRecord::Associations.association_type(association)
56
+ primary_key = assoc_info[:model].primary_key
57
+ assoc_type = assoc_info[:type]
58
58
 
59
- include_has_many_to_destroy(record, params, attr, primary_key) if assoc_type == :has_many
60
- include_has_one_to_destroy(record, params, attr, primary_key) if assoc_type.in?(%i[has_one belongs_to])
59
+ include_has_many_to_destroy(record, params, assoc_key, primary_key) if assoc_type == :has_many
60
+ include_has_one_to_destroy(record, params, assoc_key, primary_key) if assoc_type.in?(%i[has_one belongs_to])
61
61
  end
62
62
 
63
- def self.include_has_many_to_destroy(record, params, attr, primary_key)
63
+ def include_has_many_to_destroy(record, params, attr, primary_key)
64
64
  ids_to_destroy = PrettyApi::ActiveRecord::Orm
65
65
  .where_not(record.send(attr), primary_key, params[attr].pluck(primary_key))
66
66
  .pluck(primary_key)
@@ -68,7 +68,7 @@ module PrettyApi
68
68
  params[attr].push(*ids_to_destroy.map { |id| { primary_key => id, _destroy: true } })
69
69
  end
70
70
 
71
- def self.include_has_one_to_destroy(record, params, attr, primary_key)
71
+ def include_has_one_to_destroy(record, params, attr, primary_key)
72
72
  association_id = record.send(attr).try(primary_key)
73
73
 
74
74
  return if association_id.blank?
@@ -1,3 +1,3 @@
1
1
  module PrettyApi
2
- VERSION = "0.2.0".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
data/lib/pretty_api.rb CHANGED
@@ -11,7 +11,4 @@ require_relative "pretty_api/parameters/nested_attributes"
11
11
  module PrettyApi
12
12
  singleton_class.attr_accessor :destroy_missing_associations
13
13
  self.destroy_missing_associations = true
14
-
15
- singleton_class.attr_accessor :max_nested_attributes_depth
16
- self.max_nested_attributes_depth = 1
17
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pretty-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James St-Pierre