pretty-api 0.2.0 → 0.3.0

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