pretty-api 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +112 -0
- data/.standalone_migrations +6 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/Rakefile +10 -0
- data/internal.md +25 -0
- data/lib/pretty-api.rb +1 -0
- data/lib/pretty_api/active_record/associations.rb +73 -0
- data/lib/pretty_api/active_record/orm.rb +9 -0
- data/lib/pretty_api/errors/nested_errors.rb +58 -0
- data/lib/pretty_api/helpers.rb +21 -0
- data/lib/pretty_api/parameters/nested_attributes.rb +82 -0
- data/lib/pretty_api/utils/hash.rb +13 -0
- data/lib/pretty_api/version.rb +3 -0
- data/lib/pretty_api.rb +17 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e020194708396186f0a862e8fcdfe24b4bba1d9ce4449f7261bef0c2a76451d2
|
4
|
+
data.tar.gz: fdf19f29e2a101b15a6f2bf4e48fcc84a5ae5bef7469c2eeea6490272cbe66e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3a4a6b834fc2d8b2c27be2d27bae71b6c366584227071299dcdf0fbd58fcde1d9225a9051dc470acfbe1213bed65e8556eb7c4cc6dd03df83f2df2f233b3a9ca
|
7
|
+
data.tar.gz: 883fd45dfb069e6fe71cde4603265ebab372bfbf0f64007b3aa1e76b164c1a9fdec5f5b143799162a0f8d4477746ea0c33d999b02b569af28bbe99808e92c6ab
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
rubocop-rails-omakase: rubocop.yml
|
3
|
+
|
4
|
+
inherit_mode:
|
5
|
+
merge:
|
6
|
+
- Exclude
|
7
|
+
|
8
|
+
require:
|
9
|
+
- rubocop-rails
|
10
|
+
- rubocop-performance
|
11
|
+
- rubocop-rspec
|
12
|
+
|
13
|
+
AllCops:
|
14
|
+
DisabledByDefault: false
|
15
|
+
DisplayCopNames: true
|
16
|
+
NewCops: enable
|
17
|
+
Exclude:
|
18
|
+
- config/initializers/devise.rb
|
19
|
+
- lib/templates/**/*
|
20
|
+
- public/**/*
|
21
|
+
|
22
|
+
FactoryBot/ConsistentParenthesesStyle:
|
23
|
+
EnforcedStyle: omit_parentheses
|
24
|
+
|
25
|
+
Layout/LineLength:
|
26
|
+
Exclude:
|
27
|
+
- config/initializers/simple_form_bootstrap.rb
|
28
|
+
|
29
|
+
Layout/SpaceInsideArrayLiteralBrackets:
|
30
|
+
Enabled: true
|
31
|
+
EnforcedStyle: no_space
|
32
|
+
EnforcedStyleForEmptyBrackets: no_space
|
33
|
+
|
34
|
+
Lint/EmptyBlock:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Lint/UnusedBlockArgument:
|
38
|
+
Enabled: false
|
39
|
+
|
40
|
+
Metrics/AbcSize:
|
41
|
+
Max: 20
|
42
|
+
Exclude:
|
43
|
+
- db/migrate/**/*
|
44
|
+
- db/seed/**/*
|
45
|
+
|
46
|
+
Metrics/BlockLength:
|
47
|
+
Enabled: false
|
48
|
+
|
49
|
+
Metrics/CyclomaticComplexity:
|
50
|
+
Max: 10
|
51
|
+
|
52
|
+
Metrics/MethodLength:
|
53
|
+
Max: 15
|
54
|
+
Exclude:
|
55
|
+
- db/migrate/**/*
|
56
|
+
- null
|
57
|
+
|
58
|
+
Metrics/PerceivedComplexity:
|
59
|
+
Max: 10
|
60
|
+
|
61
|
+
RSpec/AnyInstance:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
RSpec/EmptyExampleGroup:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
RSpec/ExampleLength:
|
68
|
+
Max: 10
|
69
|
+
|
70
|
+
RSpec/MultipleMemoizedHelpers:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
RSpec/NestedGroups:
|
74
|
+
Enabled: false
|
75
|
+
|
76
|
+
RSpec/RepeatedExample:
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
Rails/NotNullColumn:
|
80
|
+
Enabled: false
|
81
|
+
|
82
|
+
Rails/OutputSafety:
|
83
|
+
Exclude:
|
84
|
+
- app/decorators/**/*
|
85
|
+
- app/helpers/**/*
|
86
|
+
|
87
|
+
Style/Documentation:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
Style/EmptyMethod:
|
91
|
+
EnforcedStyle: expanded
|
92
|
+
|
93
|
+
Style/FrozenStringLiteralComment:
|
94
|
+
Enabled: false
|
95
|
+
|
96
|
+
Style/StringLiterals:
|
97
|
+
EnforcedStyle: double_quotes
|
98
|
+
|
99
|
+
Layout/IndentationConsistency:
|
100
|
+
Enabled: true
|
101
|
+
EnforcedStyle: normal
|
102
|
+
|
103
|
+
Layout/IndentationWidth:
|
104
|
+
Enabled: true
|
105
|
+
Width: 2
|
106
|
+
|
107
|
+
Layout/MultilineMethodCallIndentation:
|
108
|
+
Enabled: true
|
109
|
+
EnforcedStyle: indented_relative_to_receiver
|
110
|
+
|
111
|
+
Gemspec/RequireMFA:
|
112
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 James St-Pierre
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
# PrettyApi
|
2
|
+
|
3
|
+
Build API that feels like home using native built in Ruby on Rails and ActiveRecord `accepts_nested_attributes_for` but
|
4
|
+
without all the boilerplate code that makes your Javascript dirty.
|
5
|
+
|
6
|
+
## Comparison
|
7
|
+
|
8
|
+
Exemple with an organization that has 2 services. Let's say we would like to update one service and destroy the other.
|
9
|
+
|
10
|
+
#### Without PrettyAPI
|
11
|
+
|
12
|
+
```json
|
13
|
+
{
|
14
|
+
"organization": {
|
15
|
+
"id": 1,
|
16
|
+
"services_attributes": [
|
17
|
+
{
|
18
|
+
"id": 1,
|
19
|
+
"name": "Service to destroy",
|
20
|
+
"_destroy": true
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"id": 2,
|
24
|
+
"name": "Service to update"
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
28
|
+
}
|
29
|
+
```
|
30
|
+
|
31
|
+
### With PrettyAPI
|
32
|
+
|
33
|
+
You can omit `_attributes` from your attributes and you can omit everything that you would like to destroy as well.
|
34
|
+
|
35
|
+
```json
|
36
|
+
{
|
37
|
+
"organization": {
|
38
|
+
"id": 1,
|
39
|
+
"services": [
|
40
|
+
{
|
41
|
+
"id": 2,
|
42
|
+
"name": "Service to update"
|
43
|
+
}
|
44
|
+
]
|
45
|
+
}
|
46
|
+
}
|
47
|
+
```
|
48
|
+
|
49
|
+
More exemples
|
50
|
+
|
51
|
+
```javascript
|
52
|
+
{
|
53
|
+
organization: {
|
54
|
+
services: [] // Delete all services
|
55
|
+
}
|
56
|
+
|
57
|
+
organization: {
|
58
|
+
// Fully omit "services" attribute to leave as is
|
59
|
+
}
|
60
|
+
}
|
61
|
+
```
|
62
|
+
|
63
|
+
## How it works
|
64
|
+
|
65
|
+
Because Rails is a framework built on conventions over configurations, it is possible to use reflections on your
|
66
|
+
ActiveRecord models to automatically detect which attributes are expected to be "nested" by declaring properly your
|
67
|
+
nested attributes using `accepts_nested_attributes_for`.
|
68
|
+
|
69
|
+
## Why
|
70
|
+
|
71
|
+
In the past I have built many applications that were using frontend frameworks such as React, VueJS and Svelte built on
|
72
|
+
top of a Ruby on Rails API. Here are the things that always have irritated me
|
73
|
+
|
74
|
+
1. Transforming all my attributes to `_attributes` when the time comes to send my data to the API.
|
75
|
+
2. Keeping destroyed objects in my Arrays only to tell Rails to destroy them by sending `{ id: 1, _destroy: true }`
|
76
|
+
|
77
|
+
I have tried the approach of working directly with object instances but ActiveRecord behaves unexpectedly by saving
|
78
|
+
every associations as soon as you assign the parameters. Here is an exemple
|
79
|
+
|
80
|
+
```
|
81
|
+
params[:services]
|
82
|
+
=> { services: [] }
|
83
|
+
|
84
|
+
@organization.assign_attributes(params)
|
85
|
+
=> DELETE FROM services WHERE organization_id = 1;
|
86
|
+
```
|
87
|
+
|
88
|
+
You don't even have time to check for validation or do anything that ActiveRecord already destroyed every services in
|
89
|
+
the database that belongs to your organization.
|
90
|
+
|
91
|
+
## Installation
|
92
|
+
|
93
|
+
Add this line to your Gemfile:
|
94
|
+
|
95
|
+
gem "pretty-api"
|
96
|
+
|
97
|
+
## Configuration
|
98
|
+
|
99
|
+
You can optionally create an initializer to configure these options
|
100
|
+
```
|
101
|
+
# Destroy associations that are omitted in your payload
|
102
|
+
PretttyApi.destroy_missing_associations = true
|
103
|
+
```
|
104
|
+
|
105
|
+
## Usage
|
106
|
+
|
107
|
+
```
|
108
|
+
class OrganizationsController < ApplicationController
|
109
|
+
include PrettyApi::Helpers
|
110
|
+
|
111
|
+
def create
|
112
|
+
@organization = Organization.new
|
113
|
+
@organization.assign_attributes(pretty_nested_attributes(@organization, organization_params))
|
114
|
+
...
|
115
|
+
end
|
116
|
+
|
117
|
+
def update
|
118
|
+
@organization = Organization.find(...)
|
119
|
+
@organization.assign_attributes(pretty_nested_attributes(@organization, organization_params))
|
120
|
+
...
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def organization_params
|
126
|
+
params.require(:organization).permit(:name, services: [:id, :name])
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
## This gem needs more testing
|
132
|
+
|
133
|
+
While this gem has some unit tests, it hasn't been battle tested yet.
|
134
|
+
|
135
|
+
## Beta feature
|
136
|
+
|
137
|
+
This is a new feature that I am testing to see if I can make validation errors
|
138
|
+
over API way easier to work with. By default ActiveRecord doesn't tell you which records in your
|
139
|
+
associations are invalid. This is making very hard to highlight the proper input field
|
140
|
+
in forms when you are working with nested forms.
|
141
|
+
|
142
|
+
```
|
143
|
+
include PrettyApi::Helpers
|
144
|
+
|
145
|
+
@organization.valid?
|
146
|
+
=> false
|
147
|
+
|
148
|
+
pretty_nested_errors(@organization)
|
149
|
+
=> {
|
150
|
+
name: ["can't be blank"],
|
151
|
+
organizations: {
|
152
|
+
1 => { name: ["can't be blank"] }
|
153
|
+
3 => { name: ["can't be blank"] }
|
154
|
+
}
|
155
|
+
}
|
156
|
+
```
|
157
|
+
|
158
|
+
Note: There is a "somewhat" similar feature in ActiveRecord that is not well documented.
|
159
|
+
However, while the keys are indexed, they are in string format making it hard
|
160
|
+
to work with.
|
161
|
+
|
162
|
+
```
|
163
|
+
# Per association:
|
164
|
+
has_many :my_associations, index_errors: true
|
165
|
+
|
166
|
+
# Globally:
|
167
|
+
config.active_record.index_nested_attribute_errors = true
|
168
|
+
|
169
|
+
# Before
|
170
|
+
product.error.messages
|
171
|
+
=> {:"variants.display_name"=>["can't be blank"], :"variants.price"=>["can't be blank"]}
|
172
|
+
|
173
|
+
# After
|
174
|
+
product.error.messages
|
175
|
+
{:"variants[0].display_name"=>["can't be blank"], :"variants[1].price"=>["can't be blank"]}
|
176
|
+
```
|
177
|
+
|
178
|
+
|
179
|
+
## Development
|
180
|
+
|
181
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
182
|
+
|
183
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
184
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
185
|
+
which will create a git tag for the version, push git commits and the created tag, and push the `.gem`
|
186
|
+
file to [rubygems.org](https://rubygems.org).
|
187
|
+
|
188
|
+
## Contributing
|
189
|
+
|
190
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jamesst20/pretty_api.
|
191
|
+
|
192
|
+
## License
|
193
|
+
|
194
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/internal.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Internal documentation
|
2
|
+
|
3
|
+
This documentation is not meant to be read by anybody. This document will be used for self documentations
|
4
|
+
to avoid forgetting important implementations motivations.
|
5
|
+
|
6
|
+
This gem can get a little confusing sometimes because `accepts_nested_attributes_for` can be used with any kind
|
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.
|
9
|
+
|
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.
|
15
|
+
|
16
|
+
### Notes on the pretty nested attributes implementation
|
17
|
+
|
18
|
+
We must never rely on parameters indexes to extract a record association. The order is not guaranteed and this could
|
19
|
+
lead to odd behavior or potential accidental data loss. We must rely on the primary key of the record against the
|
20
|
+
given parameter. If not done properly, it would append in the parameters `{ id: ..., _destroy: true}` wrongly thinking
|
21
|
+
some associations dont exist. I believe ActiveRecord would raise a RecordNotFound exception to protect against this
|
22
|
+
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.
|
data/lib/pretty-api.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "pretty_api" # rubocop:disable Naming/FileName
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module PrettyApi
|
2
|
+
module ActiveRecord
|
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
|
15
|
+
|
16
|
+
next unless depth[key] < PrettyApi.max_nested_attributes_depth
|
17
|
+
|
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
|
29
|
+
|
30
|
+
depth[key] += 1
|
31
|
+
result = { association[:name] => nested_attributes_tree_array(association[:model], depth) }
|
32
|
+
depth[key] -= 1
|
33
|
+
|
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
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.attribute_destroy_allowed?(model, attribute)
|
53
|
+
model.nested_attributes_options[attribute.to_sym][:allow_destroy] == true
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.attribute_association(model, attribute)
|
57
|
+
model.reflect_on_association(attribute).chain.last
|
58
|
+
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
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PrettyApi
|
2
|
+
module Errors
|
3
|
+
class NestedErrors
|
4
|
+
def self.parsed_nested_errors(record, attrs)
|
5
|
+
errors = record_only_errors(record)
|
6
|
+
|
7
|
+
return errors if attrs.blank?
|
8
|
+
|
9
|
+
parse_deep_nested_errors(record, attrs, errors)
|
10
|
+
|
11
|
+
PrettyApi::Utils::Hash.deep_compact_blank(errors)
|
12
|
+
end
|
13
|
+
|
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
|
26
|
+
|
27
|
+
def self.parse_association_errors(record, attr, nested_attrs, result, parent_record)
|
28
|
+
association = record.send(attr)
|
29
|
+
|
30
|
+
return if association.blank?
|
31
|
+
return if association == parent_record
|
32
|
+
|
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)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.parse_has_many_errors(record, associations, attr, nested_attrs, result)
|
41
|
+
result[attr] = {}
|
42
|
+
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?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
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?
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.record_only_errors(record)
|
54
|
+
record.errors.as_json.reject { |k, _v| k.to_s.include?(".") }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PrettyApi
|
2
|
+
module Helpers
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def pretty_nested_attributes(record, params, attrs = nil)
|
7
|
+
params = params.to_h.with_indifferent_access
|
8
|
+
|
9
|
+
attrs ||= PrettyApi::ActiveRecord::Associations.nested_attributes_tree(record.class)
|
10
|
+
|
11
|
+
PrettyApi::Parameters::NestedAttributes.parse_nested_attributes(record, params, attrs)
|
12
|
+
end
|
13
|
+
|
14
|
+
def pretty_nested_errors(record, attrs = nil)
|
15
|
+
attrs ||= PrettyApi::ActiveRecord::Associations.nested_attributes_tree(record.class)
|
16
|
+
|
17
|
+
PrettyApi::Errors::NestedErrors.parsed_nested_errors(record, attrs)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module PrettyApi
|
2
|
+
module Parameters
|
3
|
+
class NestedAttributes
|
4
|
+
def self.parse_nested_attributes(record, params, attrs)
|
5
|
+
return {} if params.blank?
|
6
|
+
|
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
|
15
|
+
end
|
16
|
+
|
17
|
+
params
|
18
|
+
end
|
19
|
+
|
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) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.parse_has_many_association(record, params, assoc_key, nested_assoc)
|
37
|
+
params[assoc_key].each do |p|
|
38
|
+
assoc_primary_key = record.try(:class).try(:primary_key)
|
39
|
+
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)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
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)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.include_associations_to_destroy(record, params, attr)
|
49
|
+
return unless PrettyApi.destroy_missing_associations && record.present?
|
50
|
+
|
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)
|
56
|
+
|
57
|
+
assoc_type = PrettyApi::ActiveRecord::Associations.association_type(association)
|
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])
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.include_has_many_to_destroy(record, params, attr, primary_key)
|
64
|
+
ids_to_destroy = PrettyApi::ActiveRecord::Orm
|
65
|
+
.where_not(record.send(attr), primary_key, params[attr].pluck(primary_key))
|
66
|
+
.pluck(primary_key)
|
67
|
+
|
68
|
+
params[attr].push(*ids_to_destroy.map { |id| { primary_key => id, _destroy: true } })
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.include_has_one_to_destroy(record, params, attr, primary_key)
|
72
|
+
association_id = record.send(attr).try(primary_key)
|
73
|
+
|
74
|
+
return if association_id.blank?
|
75
|
+
return if params[attr].try(:[], primary_key).present?
|
76
|
+
|
77
|
+
params[attr] ||= {}
|
78
|
+
params[attr].merge!({ primary_key => association_id, _destroy: true })
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module PrettyApi
|
2
|
+
module Utils
|
3
|
+
class Hash
|
4
|
+
def self.deep_compact_blank(hash)
|
5
|
+
hash.each_with_object({}) do |(k, v), new_hash|
|
6
|
+
v = deep_compact_blank(v) if v.is_a? ::Hash
|
7
|
+
v = v.compact_blank if v.is_a? ::Array
|
8
|
+
new_hash[k] = v if v.present?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/pretty_api.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
require_relative "pretty_api/version"
|
4
|
+
require_relative "pretty_api/utils/hash"
|
5
|
+
require_relative "pretty_api/active_record/orm"
|
6
|
+
require_relative "pretty_api/errors/nested_errors"
|
7
|
+
require_relative "pretty_api/helpers"
|
8
|
+
require_relative "pretty_api/active_record/associations"
|
9
|
+
require_relative "pretty_api/parameters/nested_attributes"
|
10
|
+
|
11
|
+
module PrettyApi
|
12
|
+
singleton_class.attr_accessor :destroy_missing_associations
|
13
|
+
self.destroy_missing_associations = true
|
14
|
+
|
15
|
+
singleton_class.attr_accessor :max_nested_attributes_depth
|
16
|
+
self.max_nested_attributes_depth = 1
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pretty-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James St-Pierre
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
description: Simplify the usage of accepts_nested_attributes_for in Rails applications.
|
28
|
+
email:
|
29
|
+
- Jamesst20@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rspec"
|
35
|
+
- ".rubocop.yml"
|
36
|
+
- ".standalone_migrations"
|
37
|
+
- CHANGELOG.md
|
38
|
+
- LICENSE.txt
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- internal.md
|
42
|
+
- lib/pretty-api.rb
|
43
|
+
- lib/pretty_api.rb
|
44
|
+
- lib/pretty_api/active_record/associations.rb
|
45
|
+
- lib/pretty_api/active_record/orm.rb
|
46
|
+
- lib/pretty_api/errors/nested_errors.rb
|
47
|
+
- lib/pretty_api/helpers.rb
|
48
|
+
- lib/pretty_api/parameters/nested_attributes.rb
|
49
|
+
- lib/pretty_api/utils/hash.rb
|
50
|
+
- lib/pretty_api/version.rb
|
51
|
+
homepage: https://github.com/jamesst20/pretty_api
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata:
|
55
|
+
homepage_uri: https://github.com/jamesst20/pretty_api
|
56
|
+
source_code_uri: https://github.com/jamesst20/pretty_api
|
57
|
+
changelog_uri: https://github.com/jamesst20/pretty_api/blob/master/CHANGELOG.md
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 3.0.0
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubygems_version: 3.5.9
|
74
|
+
signing_key:
|
75
|
+
specification_version: 4
|
76
|
+
summary: Pretty API for Rails
|
77
|
+
test_files: []
|