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 +4 -4
- data/CHANGELOG.md +8 -0
- data/internal.md +18 -8
- data/lib/pretty_api/active_record/associations.rb +14 -53
- data/lib/pretty_api/errors/nested_errors.rb +28 -31
- data/lib/pretty_api/helpers.rb +2 -2
- data/lib/pretty_api/parameters/nested_attributes.rb +38 -38
- data/lib/pretty_api/version.rb +1 -1
- data/lib/pretty_api.rb +0 -3
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ae1f5a395773d7742e4e8d35c5bd8ab123c474b5e0bef974b3f493b3149e97f
|
4
|
+
data.tar.gz: c501cd773a268868814c2c8ebe08dd425eea7be219c2f3d72e52cb292305aa0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
12
|
-
|
13
|
-
|
14
|
-
|
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,
|
5
|
-
|
6
|
-
|
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
|
-
|
8
|
+
association = attribute_association(model, association_name)
|
9
|
+
association_class = association.class_name.constantize
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
35
|
-
|
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
|
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
|
11
|
+
return errors if nested_tree.blank?
|
8
12
|
|
9
|
-
parse_deep_nested_errors(record,
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
28
|
-
|
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
|
-
|
31
|
-
|
26
|
+
next if association.blank?
|
27
|
+
next if association == parent_record
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
41
|
-
result[
|
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[
|
44
|
-
parse_deep_nested_errors association,
|
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
|
49
|
-
result[
|
50
|
-
parse_deep_nested_errors association,
|
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
|
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
|
data/lib/pretty_api/helpers.rb
CHANGED
@@ -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.
|
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.
|
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
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
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,
|
43
|
+
parse_nested_attributes(assoc, p, nested_tree[assoc_info[:model]])
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
|
-
def
|
45
|
-
parse_nested_attributes(record.try(assoc_key), params[assoc_key],
|
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
|
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
|
-
|
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
|
-
|
56
|
+
primary_key = assoc_info[:model].primary_key
|
57
|
+
assoc_type = assoc_info[:type]
|
58
58
|
|
59
|
-
include_has_many_to_destroy(record, params,
|
60
|
-
include_has_one_to_destroy(record, params,
|
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
|
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
|
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?
|
data/lib/pretty_api/version.rb
CHANGED
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
|