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