easy_exports 0.1.0 → 0.2.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/README.md +342 -6
- data/lib/easy_exports/data_attributes_resolver.rb +80 -0
- data/lib/easy_exports/data_loader.rb +32 -0
- data/lib/easy_exports/exclude_exportable_attributes_configurations.rb +1 -3
- data/lib/easy_exports/exportable_association_aliases_configurations.rb +1 -1
- data/lib/easy_exports/exportable_attribute_formatters_configurations.rb +48 -0
- data/lib/easy_exports/exportable_attribute_resolvers.rb +9 -3
- data/lib/easy_exports/exportable_attributes.rb +19 -3
- data/lib/easy_exports/exports_generable.rb +30 -80
- data/lib/easy_exports/version.rb +1 -1
- data/lib/easy_exports.rb +42 -3
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99d64117b05b63ec1f0cd4ef5b8b6a7ec3d653087678d9b4831edb87c8dd61c4
|
|
4
|
+
data.tar.gz: 884619c1077d8ff642ceb1466f4c9ffcbb50bac95277b5bf7765303f53d666f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e843ce5e1aa5edab56616992a16d989952c52343a854619d0fb46715617af59a2e4ca641d69bb8dc243d2aea63b7bdae42a164e2e39e3ce9784e471ff04f3054
|
|
7
|
+
data.tar.gz: 4915abc76083e2e755e954699b788e4d5debaef66cd764c42af7d85eb2dc56dafff68589ca81c38a71fbdb64962c8a891c1291781f8591af20702f4be8f463a4
|
data/README.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# EasyExports
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
## Usage
|
|
5
|
-
How to use my plugin.
|
|
2
|
+
EasyExports is a rails ActiveRecord ORM extension dedicated to streamlining and simplifying the model data export process by eliminating common complexities.
|
|
6
3
|
|
|
7
4
|
## Installation
|
|
8
5
|
Add this line to your application's Gemfile:
|
|
@@ -20,9 +17,348 @@ Or install it yourself as:
|
|
|
20
17
|
```bash
|
|
21
18
|
$ gem install easy_exports
|
|
22
19
|
```
|
|
20
|
+
## Usage
|
|
21
|
+
Upon installation, EasyExports seamlessly integrates with ```ActiveRecord::Base```, granting all models immediate access to its efficient export methods.
|
|
22
|
+
|
|
23
|
+
### Generating Exportable Attributes
|
|
24
|
+
|
|
25
|
+
Retrieve exportable attributes using the `exportable_attributes` method. This method retrieves attributes of the model itself and those of all its associations.
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Example Models and Exportable Attributes
|
|
29
|
+
|
|
30
|
+
# User model with columns: first_name, last_name, created_at, updated_at
|
|
31
|
+
class User < ApplicationRecord
|
|
32
|
+
has_and_belongs_to_many :emails
|
|
33
|
+
has_many :phones
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Exportable attributes for the User model
|
|
37
|
+
User.exportable_attributes
|
|
38
|
+
# =>
|
|
39
|
+
# {
|
|
40
|
+
# "User" => ["id", "first name", "last name", "created at", "updated at"],
|
|
41
|
+
# "Emails" => ["id", "address", "created at", "updated at"],
|
|
42
|
+
# "Phones" => ["id", "number", "user id", "created at", "updated at"]
|
|
43
|
+
# }
|
|
44
|
+
|
|
45
|
+
# Phone model with columns: number, user_id, created_at, updated_at
|
|
46
|
+
class Phone < ApplicationRecord
|
|
47
|
+
belongs_to :user
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Exportable attributes for the Phone model
|
|
51
|
+
Phone.exportable_attributes
|
|
52
|
+
# =>
|
|
53
|
+
# {
|
|
54
|
+
# "Phone" => ["id", "number", "user id", "created at", "updated at"],
|
|
55
|
+
# "User" => ["id", "first name", "last name", "created at", "updated at"]
|
|
56
|
+
# }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Generating Exports from Exportable Attributes
|
|
60
|
+
|
|
61
|
+
To generate exports, use the `generate_exports(exportable_attributes, ids, order:)` method.
|
|
62
|
+
|
|
63
|
+
- The `exportable_attributes` argument specifies the chosen attributes from the exportable attributes list.
|
|
64
|
+
- The `ids` argument is optional; provide IDs to export data for specific records.
|
|
65
|
+
- Omitting `ids` will trigger exports for all records of the given model.
|
|
66
|
+
- The `order:` keyword argument is optional; see [Ordering Exports](#ordering-exports) for details.
|
|
67
|
+
|
|
68
|
+
The method returns an `EasyExports::Export` object containing hash data from the records and a `csv_string` that can be written to a CSV file.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
user_exportable_attributes = {"User"=>["id", "first name"], "Phones"=>["id", "number"], "Emails"=>["id", "address"]}
|
|
72
|
+
|
|
73
|
+
exports_object = User.generate_exports(user_exportable_attributes)
|
|
74
|
+
# => EasyExports::Export(Object)
|
|
75
|
+
|
|
76
|
+
exports_data = exports_object.data
|
|
77
|
+
# =>
|
|
78
|
+
# [
|
|
79
|
+
# {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>1, "phones_number"=>"(473) 693-8745", "emails_id"=>5, "emails_address"=>"blake_armstrong@bahringer.test"},
|
|
80
|
+
# {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>2, "phones_number"=>"594-299-0722", "emails_id"=>6, "emails_address"=>"dulce@mertz.example"},
|
|
81
|
+
# {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>3, "phones_number"=>"1-609-662-2028", "emails_id"=>nil, "emails_address"=>nil},
|
|
82
|
+
# {"user_id"=>2, "user_first_name"=>"Stan", "phones_id"=>4, "phones_number"=>"951-671-9548", "emails_id"=>7, "emails_address"=>"dominick@durgan.example"},
|
|
83
|
+
# {"user_id"=>2, "user_first_name"=>"Stan", "phones_id"=>5, "phones_number"=>"1-698-432-7489", "emails_id"=>nil, "emails_address"=>nil}
|
|
84
|
+
# ]
|
|
85
|
+
|
|
86
|
+
exports_csv_string = exports_object.csv_string
|
|
87
|
+
# =>
|
|
88
|
+
# "user_id,user_first_name,phones_id,phones_number,emails_id,emails_address\n
|
|
89
|
+
# 1,Sydney,1,(473) 693-8745,5,blake_armstrong@bahringer.test\n
|
|
90
|
+
# 1,Sydney,2,594-299-0722,6,dulce@mertz.example\n
|
|
91
|
+
# 1,Sydney,3,1-609-662-2028,,\n
|
|
92
|
+
# 2,Stan,4,951-671-9548,7,dominick@durgan.example\n
|
|
93
|
+
# 2,Stan,5,1-698-432-7489,,\n"
|
|
94
|
+
|
|
95
|
+
# Writing csv_string to a file to visualize the generated export
|
|
96
|
+
File.open(file_path, 'w') do |file|
|
|
97
|
+
file.write(exports_csv_string)
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
<div align="center">
|
|
101
|
+
<img width="600" alt="csv_with_emails" src="https://github.com/SydDaps/easy_exports/assets/51008616/8b9df43c-419d-4ca0-8cb1-6217025b8050">
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
Exported CSV showcases data for:
|
|
105
|
+
|
|
106
|
+
- User "Sydney" with 3 phones and 2 emails
|
|
107
|
+
- User "Stan" with 2 phones and 1 email.
|
|
108
|
+
- The main CSV header includes association names and attribute names.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
### Ordering Exports
|
|
112
|
+
|
|
113
|
+
Control the order of rows in the generated export using the `order:` keyword argument on `generate_exports`.
|
|
114
|
+
|
|
115
|
+
- `order:` accepts any value ActiveRecord's `order` accepts — a hash, string, symbol, or array.
|
|
116
|
+
- When `order:` is omitted and the model has a `created_at` column, exports default to newest first (`created_at DESC`).
|
|
117
|
+
- When `order:` is omitted and no `created_at` column exists, no ordering is applied.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Example: explicit ordering
|
|
121
|
+
|
|
122
|
+
# Order by first_name ascending
|
|
123
|
+
User.generate_exports(user_exportable_attributes, [], order: { first_name: :asc })
|
|
124
|
+
|
|
125
|
+
# Order by a raw SQL fragment
|
|
126
|
+
User.generate_exports(user_exportable_attributes, [], order: 'last_name DESC, id ASC')
|
|
127
|
+
|
|
128
|
+
# Default ordering (newest first when created_at exists)
|
|
129
|
+
User.generate_exports(user_exportable_attributes)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For performance, records are fetched in batches of 1,000 internally while preserving the requested order across the full result set. The batch size is configurable — see [Configuration](#configuration).
|
|
133
|
+
|
|
134
|
+
### Configuration
|
|
135
|
+
|
|
136
|
+
Configure gem-wide defaults using `EasyExports.configure`.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# config/initializers/easy_exports.rb
|
|
140
|
+
EasyExports.configure do |c|
|
|
141
|
+
c.batch_size = 500 # default: 1000
|
|
142
|
+
c.sensitive_attributes += %w[internal_note ssn] # extends the default list
|
|
143
|
+
c.type_formatters[:datetime] = ->(v) { v.strftime('%d/%m/%Y') }
|
|
144
|
+
c.csv_header_formatter = ->(key) { key.humanize.upcase } # or set to nil to disable
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `batch_size` — number of records fetched per database round trip during export. Override per call with `generate_exports(..., batch_size: 2_000)`.
|
|
149
|
+
- `sensitive_attributes` — attributes auto-excluded from every export. See [Sensitive Attributes](#sensitive-attributes) for the default list and per-model opt-out.
|
|
150
|
+
- `type_formatters` — per-type value formatters applied before a raw value is written to CSV. See [Type Formatters](#type-formatters).
|
|
151
|
+
- `csv_header_formatter` — callable applied to every header in the CSV row. Defaults to `humanize.titleize` (e.g. `registration_email` → `Registration Email`). Set to `nil` to keep raw snake_case headers.
|
|
152
|
+
|
|
153
|
+
### Sensitive Attributes
|
|
154
|
+
|
|
155
|
+
EasyExports automatically excludes commonly sensitive attributes from all exports. The default list is:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
%w[
|
|
159
|
+
password_digest
|
|
160
|
+
encrypted_password
|
|
161
|
+
remember_token
|
|
162
|
+
reset_password_token
|
|
163
|
+
confirmation_token
|
|
164
|
+
session_token
|
|
165
|
+
api_key
|
|
166
|
+
secret_token
|
|
167
|
+
]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
To include these on a specific model (e.g. for admin-only exports), opt in at the class level:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
class AdminLogin < ApplicationRecord
|
|
174
|
+
self.include_sensitive_exportable_attributes = true
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Extend or replace the default list through [Configuration](#configuration).
|
|
179
|
+
|
|
180
|
+
### Type Formatters
|
|
181
|
+
|
|
182
|
+
EasyExports applies type-based formatters automatically, so you don't have to write a lambda for every boolean or timestamp column.
|
|
183
|
+
|
|
184
|
+
| Type | Matches | Shipped formatter | Example output |
|
|
185
|
+
|------|---------|-------------------|----------------|
|
|
186
|
+
| `boolean` | `true` / `false` | `->(v) { v ? 'Yes' : 'No' }` | `Yes`, `No` |
|
|
187
|
+
| `datetime` | `Date`, `Time`, `DateTime`, `ActiveSupport::TimeWithZone` | `->(v) { v.strftime('%A, %B %-d, %Y %H:%M') }` | `Sunday, January 5, 2025 09:17` |
|
|
188
|
+
| `leading_zero_string` | `String` starting with `0` | `->(v) { "'#{v}" }` | `'0244867596` (preserves leading zero in spreadsheets) |
|
|
189
|
+
| `string` | `String` not containing `@` | *(none — opt in per model)* | — |
|
|
190
|
+
|
|
191
|
+
`nil` values always pass through untouched.
|
|
192
|
+
|
|
193
|
+
**Turn off a type formatter for a specific model:**
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class Transaction < ApplicationRecord
|
|
197
|
+
disable_exportable_type_formatter :datetime # one type
|
|
198
|
+
disable_exportable_type_formatter :boolean, :datetime # multiple types
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Replace a type formatter for a specific model** — apply a different format to *all* values of that type on this one model:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
class Registration < ApplicationRecord
|
|
206
|
+
# Every datetime on Registration renders as "January 5, 2025" — no time portion
|
|
207
|
+
format_exportable_type :datetime, ->(v) { v.strftime('%B %-d, %Y') }
|
|
208
|
+
|
|
209
|
+
# Capitalize the first letter of every plain string (emails are skipped automatically)
|
|
210
|
+
format_exportable_type :string, ->(v) { v.sub(/^./, &:upcase) }
|
|
211
|
+
|
|
212
|
+
# Block form
|
|
213
|
+
format_exportable_type(:boolean) { |v| v ? '✓' : '✗' }
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Change or disable a type formatter globally** — see [Configuration](#configuration).
|
|
218
|
+
|
|
219
|
+
### Custom Value Formatters
|
|
220
|
+
|
|
221
|
+
For a specific attribute, override the default with your own formatter using `format_exportable_attribute`.
|
|
222
|
+
|
|
223
|
+
- Declare it on the class that owns the attribute. It applies whenever that attribute is exported, whether from the owner model directly or through an association.
|
|
224
|
+
- Accepts either a callable (lambda/proc) or a block.
|
|
225
|
+
- A custom formatter takes precedence over any default formatter for that attribute.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class Registration < ApplicationRecord
|
|
229
|
+
# Override the global datetime default with a shorter format for this attribute
|
|
230
|
+
format_exportable_attribute :registered_on, ->(v) { v&.strftime('%Y-%m-%d') }
|
|
231
|
+
|
|
232
|
+
# Override the global boolean default with custom glyphs
|
|
233
|
+
format_exportable_attribute :is_archived, ->(v) { v ? '✓' : '' }
|
|
234
|
+
|
|
235
|
+
# Block form
|
|
236
|
+
format_exportable_attribute(:status) { |v| v.to_s.titleize }
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Precedence** (top wins):
|
|
241
|
+
1. Per-attribute `format_exportable_attribute`
|
|
242
|
+
2. Per-model `disable_default_formatter` (if disabled → raw value)
|
|
243
|
+
3. Per-model `override_default_formatter` (if overridden → override wins)
|
|
244
|
+
4. Global `EasyExports.default_formatters[:type]`
|
|
245
|
+
5. Raw value
|
|
246
|
+
|
|
247
|
+
### Exportable Attributes Aliases
|
|
248
|
+
|
|
249
|
+
Configure an alternative association name for exportable attributes using the `exportable_association_aliases(aliases)` model method.
|
|
250
|
+
|
|
251
|
+
- Invoke this method below all association definitions.
|
|
252
|
+
- `aliases` should be a hash in the pattern: `{valid_association_name or model_name: "alternative_name"}`.
|
|
253
|
+
- Ensure all hash arguments are snake-cased.
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# Example Model with exportable_association_aliases
|
|
257
|
+
|
|
258
|
+
# User model with columns: first_name, last_name, created_at, updated_at
|
|
259
|
+
class User < ApplicationRecord
|
|
260
|
+
has_many :phones
|
|
261
|
+
|
|
262
|
+
exportable_association_aliases phones: :mobile_phones
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Exportable attributes for the User model will now be
|
|
266
|
+
User.exportable_attributes
|
|
267
|
+
# =>
|
|
268
|
+
# {
|
|
269
|
+
# "User" => ["id", "first name", "last name", "created at", "updated at"],
|
|
270
|
+
# "Mobile phones" => ["id", "number", "user id", "created at", "updated at"]
|
|
271
|
+
# }
|
|
272
|
+
```
|
|
273
|
+
With the exportable_association_aliases configured, the phones association has been renamed to "Mobile phones". This new name will appear in the export header when generating exports with this alias for exportable attributes.
|
|
274
|
+
|
|
275
|
+
### Excluding Specific Exportable Attributes
|
|
276
|
+
|
|
277
|
+
Configure associations to exclude certain attributes from exportable attributes using the `exclude_exportable_attributes(association_attributes)` model method.
|
|
278
|
+
|
|
279
|
+
- Invoke this method below all association declarations.
|
|
280
|
+
- `association_attributes` should follow the pattern `{valid_association_name or model_name: [valid_attributes_to_remove]}`.
|
|
281
|
+
- For removing attributes across all associations and the model itself, use the "all" key as the association_name.
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Example Model with exclude_exportable_attributes
|
|
285
|
+
|
|
286
|
+
# User model with columns: first_name, last_name, created_at, updated_at
|
|
287
|
+
class User < ApplicationRecord
|
|
288
|
+
has_many :phones
|
|
289
|
+
|
|
290
|
+
exclude_exportable_attributes all: [:id], user: [:last_name], phones: [:user_id]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Exportable attributes for the User model will now be
|
|
294
|
+
User.exportable_attributes
|
|
295
|
+
# =>
|
|
296
|
+
# {
|
|
297
|
+
# "User" => ["first name", "created at", "updated at"],
|
|
298
|
+
# "Phones" => ["number", "created at", "updated at"]
|
|
299
|
+
# }
|
|
300
|
+
```
|
|
301
|
+
In this example, note that:
|
|
302
|
+
- All associations exclude the id attribute.
|
|
303
|
+
- The User model excludes the last_name attribute.
|
|
304
|
+
- The Phones association excludes the user_id attribute.
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
### Excluding Specific Exportable Attribute Associations
|
|
308
|
+
|
|
309
|
+
Configure model's exportable attributes to exclude certain associations using the `associations_to_exclude(associations)` model method.
|
|
310
|
+
|
|
311
|
+
- Apply this method below all association declarations.
|
|
312
|
+
- `associations` should follow the pattern `['association_name']`.
|
|
23
313
|
|
|
24
|
-
|
|
25
|
-
|
|
314
|
+
```ruby
|
|
315
|
+
# Example Model with associations_to_exclude
|
|
316
|
+
|
|
317
|
+
# User model with columns: first_name, last_name, created_at, updated_at
|
|
318
|
+
class User < ApplicationRecord
|
|
319
|
+
has_many :phones
|
|
320
|
+
|
|
321
|
+
associations_to_exclude [:phones]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Exportable attributes for the User model will now be
|
|
325
|
+
User.exportable_attributes
|
|
326
|
+
# =>
|
|
327
|
+
# {
|
|
328
|
+
# "User" => ["id", "first name", "last name", "created at", "updated at"]
|
|
329
|
+
# }
|
|
330
|
+
```
|
|
331
|
+
In this example, the attributes of the phones association are excluded from the exportable attributes of the User model.
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
### Adding Custom Attribute to Exportable Attributes
|
|
335
|
+
|
|
336
|
+
Leverage a handy Rails method to transform a model instance method into an attribute, incorporating it into the exportable attributes.
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Example Model with Custom Virtual Attribute
|
|
340
|
+
|
|
341
|
+
# User model with columns: first_name, last_name, created_at, updated_at
|
|
342
|
+
class User < ApplicationRecord
|
|
343
|
+
attribute :total_number_of_phones
|
|
344
|
+
|
|
345
|
+
has_many :phones
|
|
346
|
+
|
|
347
|
+
associations_to_exclude [:phones]
|
|
348
|
+
|
|
349
|
+
def total_number_of_phones
|
|
350
|
+
phones.size
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Exportable attributes for the User model will now include
|
|
355
|
+
User.exportable_attributes
|
|
356
|
+
# =>
|
|
357
|
+
# {
|
|
358
|
+
# "User" => ["id", "first name", "last name", "created at", "updated at", "total number of phones"]
|
|
359
|
+
# }
|
|
360
|
+
```
|
|
361
|
+
In this example, the custom attribute "total number of phones" has been seamlessly integrated into the exportable attributes, showcasing the flexibility of Rails' capabilities.
|
|
26
362
|
|
|
27
363
|
## License
|
|
28
364
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyExports
|
|
4
|
+
module DataAttributesResolver
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def value_from_selected_attributes(selected_attributes, record, export_row_template)
|
|
11
|
+
selected_attributes.each_with_object([export_row_template]) do |(association_name, attributes), export_rows|
|
|
12
|
+
objects = objects_for_attribute(association_name, record)
|
|
13
|
+
|
|
14
|
+
attributes.each do |attribute|
|
|
15
|
+
attribute_values = resolve_attributes(attribute, objects)
|
|
16
|
+
|
|
17
|
+
attribute_values.each_with_index do |value, index|
|
|
18
|
+
export_column = export_rows[index] || export_row_template
|
|
19
|
+
|
|
20
|
+
export_rows[index] = export_column.merge(export_header(association_name, attribute) => value)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
export_row_template.merge!(export_rows.first) if association_name == underscored_self_name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve_attributes(attribute, objects)
|
|
29
|
+
return [nil] if objects.empty?
|
|
30
|
+
|
|
31
|
+
objects.map do |object|
|
|
32
|
+
raw_value = object.send(attribute)
|
|
33
|
+
format_value_for_export(object.class, attribute, raw_value)
|
|
34
|
+
end.flatten
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_value_for_export(klass, attribute, value)
|
|
38
|
+
custom = exportable_attribute_formatter_for(klass, attribute)
|
|
39
|
+
return custom.call(value) if custom
|
|
40
|
+
return value if value.nil?
|
|
41
|
+
|
|
42
|
+
type_formatter = type_formatter_for(klass, value)
|
|
43
|
+
type_formatter ? type_formatter.call(value) : value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exportable_attribute_formatter_for(klass, attribute)
|
|
47
|
+
store = exportable_attribute_formatters_store[klass.name.underscore.downcase]
|
|
48
|
+
store && store[attribute.to_s]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def type_formatter_for(klass, value)
|
|
52
|
+
klass_key = klass.name.underscore.downcase
|
|
53
|
+
disabled = disabled_type_formatters_store[klass_key] || []
|
|
54
|
+
overrides = overridden_type_formatters_store[klass_key] || {}
|
|
55
|
+
|
|
56
|
+
EasyExports::TYPE_MATCHERS.each do |type, matcher|
|
|
57
|
+
next if disabled.include?(type)
|
|
58
|
+
next unless matcher.call(value)
|
|
59
|
+
|
|
60
|
+
formatter = overrides[type] || EasyExports.type_formatters[type]
|
|
61
|
+
return formatter if formatter
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def objects_for_attribute(association_name, record)
|
|
68
|
+
object = association_name == underscored_self_name ? record : record.send(association_name)
|
|
69
|
+
object.respond_to?(:each) ? object : [object].compact
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def export_header(association_name, attribute)
|
|
73
|
+
association_alias = associations_aliases_store[underscored_self_name]
|
|
74
|
+
association_alias = association_alias.blank? ? nil : association_alias[association_name]
|
|
75
|
+
|
|
76
|
+
"#{association_alias || association_name}_#{attribute}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyExports
|
|
4
|
+
module DataLoader
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def fetch_records(ids, selected_attributes, order = nil, batch_size = EasyExports.batch_size)
|
|
11
|
+
validate_association_attributes(selected_attributes, 'to_exported_data')
|
|
12
|
+
|
|
13
|
+
scope = ids.blank? ? all : where(id: ids)
|
|
14
|
+
ordered_ids = apply_export_order(scope, order).pluck(:id)
|
|
15
|
+
associations_to_preload = selected_attributes.keys - [underscored_self_name]
|
|
16
|
+
|
|
17
|
+
ordered_ids.each_slice(batch_size).flat_map do |id_slice|
|
|
18
|
+
batch = apply_export_order(where(id: id_slice), order).to_a
|
|
19
|
+
ActiveRecord::Associations::Preloader.new(records: batch, associations: associations_to_preload).call
|
|
20
|
+
batch
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def apply_export_order(records, order)
|
|
25
|
+
return records.order(order) if order.present?
|
|
26
|
+
return records.order(created_at: :desc) if column_names.include?('created_at')
|
|
27
|
+
|
|
28
|
+
records
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EasyExports
|
|
4
|
+
module ExportableAttributeFormattersConfigurations
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def format_exportable_attribute(attribute, formatter = nil, &block)
|
|
11
|
+
formatter ||= block
|
|
12
|
+
unless formatter.respond_to?(:call)
|
|
13
|
+
raise ArgumentError, 'format_exportable_attribute requires a callable (proc or block)'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
exportable_attribute_formatters_store[underscored_self_name] ||= {}
|
|
17
|
+
exportable_attribute_formatters_store[underscored_self_name][attribute.to_s] = formatter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def disable_exportable_type_formatter(*types)
|
|
21
|
+
validate_known_type_formatter!(types, 'disable_exportable_type_formatter')
|
|
22
|
+
|
|
23
|
+
disabled_type_formatters_store[underscored_self_name] ||= []
|
|
24
|
+
disabled_type_formatters_store[underscored_self_name] |= types.map(&:to_sym)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format_exportable_type(type, formatter = nil, &block)
|
|
28
|
+
formatter ||= block
|
|
29
|
+
unless formatter.respond_to?(:call)
|
|
30
|
+
raise ArgumentError, 'format_exportable_type requires a callable (proc or block)'
|
|
31
|
+
end
|
|
32
|
+
validate_known_type_formatter!([type], 'format_exportable_type')
|
|
33
|
+
|
|
34
|
+
overridden_type_formatters_store[underscored_self_name] ||= {}
|
|
35
|
+
overridden_type_formatters_store[underscored_self_name][type.to_sym] = formatter
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_known_type_formatter!(types, method)
|
|
39
|
+
invalid = types.map(&:to_sym) - EasyExports::TYPE_MATCHERS.keys
|
|
40
|
+
return if invalid.empty?
|
|
41
|
+
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"#{method} unknown type(s): #{invalid.join(', ')}. " \
|
|
44
|
+
"Known types: #{EasyExports::TYPE_MATCHERS.keys.join(', ')}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -20,7 +20,15 @@ module EasyExports
|
|
|
20
20
|
|
|
21
21
|
def resolve_attributes_for_association(association)
|
|
22
22
|
association_attributes = association.class_name.constantize.attribute_names
|
|
23
|
-
association_attributes -
|
|
23
|
+
association_attributes -
|
|
24
|
+
resolve_excluded_exportable_attributes(association.name.to_s.downcase) -
|
|
25
|
+
resolve_sensitive_attributes_to_exclude
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve_sensitive_attributes_to_exclude
|
|
29
|
+
return [] if include_sensitive_exportable_attributes
|
|
30
|
+
|
|
31
|
+
EasyExports.sensitive_attributes.map(&:to_s)
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
def resolve_associations_names_aliases(association_name)
|
|
@@ -32,5 +40,3 @@ module EasyExports
|
|
|
32
40
|
end
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
|
-
|
|
36
|
-
ActiveRecord::Base.include EasyExports::ExportableAttributeResolvers
|
|
@@ -4,13 +4,25 @@ module EasyExports
|
|
|
4
4
|
module ExportableAttributes
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
included do
|
|
8
8
|
cattr_accessor :excluded_exportable_attributes_store, default: {}, instance_writer: false
|
|
9
9
|
cattr_accessor :associations_aliases_store, default: {}, instance_writer: false
|
|
10
10
|
cattr_accessor :associations_to_exclude_store, default: {}, instance_writer: false
|
|
11
|
+
cattr_accessor :exportable_attribute_formatters_store, default: {}, instance_writer: false
|
|
12
|
+
cattr_accessor :disabled_type_formatters_store, default: {}, instance_writer: false
|
|
13
|
+
cattr_accessor :overridden_type_formatters_store, default: {}, instance_writer: false
|
|
14
|
+
cattr_accessor :include_sensitive_exportable_attributes, default: false, instance_writer: false
|
|
15
|
+
|
|
16
|
+
include EasyExports::ExportableAssociationAliasesConfigurations
|
|
17
|
+
include EasyExports::ExportableAttributeResolvers
|
|
18
|
+
include EasyExports::ExcludeExportableAttributesConfigurations
|
|
19
|
+
include EasyExports::ExportableAttributeFormattersConfigurations
|
|
20
|
+
include EasyExports::ExportsGenerable
|
|
21
|
+
end
|
|
11
22
|
|
|
23
|
+
class_methods do
|
|
12
24
|
def exportable_attributes
|
|
13
|
-
self_with_associations.each_with_object({}) do |association, attributes|
|
|
25
|
+
@exportable_attributes ||= self_with_associations.each_with_object({}) do |association, attributes|
|
|
14
26
|
association_name = association.name.to_s.downcase
|
|
15
27
|
next if associations_to_exclude_store[underscored_self_name]&.include? association_name
|
|
16
28
|
|
|
@@ -21,6 +33,10 @@ module EasyExports
|
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
def reset_exportable_attributes_cache!
|
|
37
|
+
@exportable_attributes = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
private
|
|
25
41
|
|
|
26
42
|
def self_with_associations
|
|
@@ -41,7 +57,7 @@ module EasyExports
|
|
|
41
57
|
end
|
|
42
58
|
|
|
43
59
|
def humanize_attribute_names(attributes)
|
|
44
|
-
attributes.map { |attribute| attribute.humanize(keep_id_suffix: true).downcase }
|
|
60
|
+
attributes.map { |attribute| attribute.humanize(keep_id_suffix: true).downcase }.sort
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
end
|
|
@@ -4,18 +4,25 @@ module EasyExports
|
|
|
4
4
|
module ExportsGenerable
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
|
+
included do
|
|
8
|
+
include EasyExports::DataLoader
|
|
9
|
+
include EasyExports::DataAttributesResolver
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
class_methods do
|
|
8
|
-
def generate_exports(fields_to_export = {}, ids = [])
|
|
13
|
+
def generate_exports(fields_to_export = {}, ids = [], order: nil, batch_size: nil)
|
|
9
14
|
validate_exclude_exportable_attributes_argument(fields_to_export, 'generate_exports')
|
|
10
15
|
|
|
11
16
|
selected_exportable_attributes = revert_transformed_names(fields_to_export)
|
|
17
|
+
selected_exportable_attributes = rearrange_selected_attributes(selected_exportable_attributes)
|
|
18
|
+
|
|
12
19
|
export_row_template = generate_export_row_template(selected_exportable_attributes)
|
|
13
20
|
|
|
14
21
|
selected_attributes = revert_exportable_attributes_aliases(selected_exportable_attributes)
|
|
15
|
-
records = fetch_records(ids, selected_attributes)
|
|
22
|
+
records = fetch_records(ids, selected_attributes, order, batch_size || EasyExports.batch_size)
|
|
16
23
|
|
|
17
24
|
exported_data = records.each_with_object([]) do |record, hash_to_export|
|
|
18
|
-
hash_to_export << value_from_selected_attributes(selected_attributes, record, export_row_template)
|
|
25
|
+
hash_to_export << value_from_selected_attributes(selected_attributes, record, export_row_template.dup)
|
|
19
26
|
end.flatten
|
|
20
27
|
|
|
21
28
|
csv_string = write_exported_data_to_csv(exported_data, export_row_template)
|
|
@@ -24,8 +31,12 @@ module EasyExports
|
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def write_exported_data_to_csv(exported_data, export_row_template)
|
|
34
|
+
header_formatter = EasyExports.csv_header_formatter
|
|
35
|
+
keys = export_row_template.keys
|
|
36
|
+
headers = header_formatter ? keys.map { |k| header_formatter.call(k) } : keys
|
|
37
|
+
|
|
27
38
|
CSV.generate(headers: true) do |csv|
|
|
28
|
-
csv <<
|
|
39
|
+
csv << headers
|
|
29
40
|
|
|
30
41
|
exported_data.each do |data|
|
|
31
42
|
csv << data.values
|
|
@@ -33,90 +44,31 @@ module EasyExports
|
|
|
33
44
|
end
|
|
34
45
|
end
|
|
35
46
|
|
|
36
|
-
def
|
|
37
|
-
selected_attributes
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def fetch_records(ids, selected_attributes)
|
|
45
|
-
validate_association_attributes(selected_attributes, 'to_exported_data')
|
|
46
|
-
|
|
47
|
-
records_with_preloaded_associations(ids, selected_attributes)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def association_attributes(association_name)
|
|
51
|
-
association_name = if association_name == underscored_self_name
|
|
52
|
-
association_name.classify
|
|
53
|
-
else
|
|
54
|
-
reflect_on_all_associations.find do |association|
|
|
55
|
-
association.name.to_s == association_name
|
|
56
|
-
end&.class_name
|
|
57
|
-
end
|
|
47
|
+
def rearrange_selected_attributes(selected_attributes)
|
|
48
|
+
selected_attributes = selected_attributes.to_a
|
|
49
|
+
self_alias_name = associations_aliases_store[underscored_self_name]&.fetch(underscored_self_name, nil)
|
|
50
|
+
self_name = self_alias_name || underscored_self_name
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def records_with_preloaded_associations(ids, selected_attributes)
|
|
63
|
-
records = ids.blank? ? all : where(id: ids)
|
|
64
|
-
|
|
65
|
-
associations_to_preload = selected_attributes.keys
|
|
66
|
-
associations_to_preload.delete(underscored_self_name)
|
|
52
|
+
self_exportable_attributes = selected_attributes.find do |selected_attribute|
|
|
53
|
+
selected_attribute.first == self_name
|
|
54
|
+
end
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
).call
|
|
56
|
+
if self_exportable_attributes.blank? || selected_attributes[0] == self_exportable_attributes
|
|
57
|
+
return selected_attributes.to_h
|
|
58
|
+
end
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
selected_attributes.delete(self_exportable_attributes)
|
|
61
|
+
selected_attributes.unshift(self_exportable_attributes).to_h
|
|
74
62
|
end
|
|
75
63
|
|
|
76
|
-
def
|
|
77
|
-
selected_attributes.each_with_object(
|
|
78
|
-
objects = objects_for_attribute(association_name, record)
|
|
79
|
-
|
|
64
|
+
def generate_export_row_template(selected_attributes)
|
|
65
|
+
selected_attributes.each_with_object({}) do |(association_name, attributes), export_row|
|
|
80
66
|
attributes.each do |attribute|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
attribute_values.each_with_index do |value, index|
|
|
84
|
-
export_column = export_rows[index] || export_row_template
|
|
85
|
-
|
|
86
|
-
export_rows[index] = export_column.merge(export_header(association_name, attribute) => value)
|
|
87
|
-
end
|
|
67
|
+
export_row.merge!(export_header(association_name, attribute) => nil)
|
|
88
68
|
end
|
|
89
69
|
end
|
|
90
70
|
end
|
|
91
71
|
|
|
92
|
-
def export_header(association_name, attribute)
|
|
93
|
-
association_alias = associations_aliases_store[underscored_self_name]
|
|
94
|
-
association_alias = association_alias.blank? ? nil : association_alias[association_name]
|
|
95
|
-
|
|
96
|
-
"#{association_alias || association_name}_#{attribute}"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def resolve_attributes(attribute, objects)
|
|
100
|
-
objects.empty? ? [nil] : objects.map { |object| parse_attribute_value(object.send(attribute)) }.flatten
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def parse_attribute_value(value)
|
|
104
|
-
value_class = value.class
|
|
105
|
-
|
|
106
|
-
if value_class.eql?(ActiveSupport::TimeWithZone)
|
|
107
|
-
DateTime.parse(value.to_s).strftime('%Y-%m-%d %H:%M:%S')
|
|
108
|
-
elsif !value_class.eql?(String)
|
|
109
|
-
value
|
|
110
|
-
else
|
|
111
|
-
value.start_with?('0') ? "'#{value}" : value
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def objects_for_attribute(association_name, record)
|
|
116
|
-
object = association_name == underscored_self_name ? record : record.send(association_name)
|
|
117
|
-
object.respond_to?(:each) ? object : [object].compact
|
|
118
|
-
end
|
|
119
|
-
|
|
120
72
|
def revert_exportable_attributes_aliases(attributes_with_aliases)
|
|
121
73
|
attributes_with_aliases.transform_keys { |key| reversed_associations_name_aliases[key] || key }
|
|
122
74
|
end
|
|
@@ -138,5 +90,3 @@ module EasyExports
|
|
|
138
90
|
end
|
|
139
91
|
end
|
|
140
92
|
end
|
|
141
|
-
|
|
142
|
-
ActiveRecord::Base.include EasyExports::ExportsGenerable
|
data/lib/easy_exports/version.rb
CHANGED
data/lib/easy_exports.rb
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
|
4
|
+
|
|
3
5
|
require 'easy_exports/version'
|
|
4
6
|
require 'easy_exports/railtie'
|
|
5
|
-
require 'easy_exports/exportable_attributes'
|
|
6
7
|
require 'easy_exports/exportable_attribute_resolvers'
|
|
7
8
|
require 'easy_exports/exclude_associations_configurations'
|
|
8
9
|
require 'easy_exports/exclude_exportable_attributes_configurations'
|
|
9
10
|
require 'easy_exports/exportable_association_aliases_configurations'
|
|
10
|
-
require 'easy_exports/
|
|
11
|
+
require 'easy_exports/exportable_attribute_formatters_configurations'
|
|
11
12
|
require 'easy_exports/export'
|
|
13
|
+
require 'easy_exports/data_loader'
|
|
14
|
+
require 'easy_exports/data_attributes_resolver'
|
|
15
|
+
require 'easy_exports/exports_generable'
|
|
16
|
+
require 'easy_exports/exportable_attributes'
|
|
12
17
|
|
|
13
18
|
module EasyExports
|
|
14
|
-
|
|
19
|
+
DEFAULT_BATCH_SIZE = 1_000
|
|
20
|
+
DEFAULT_SENSITIVE_ATTRIBUTES = %w[
|
|
21
|
+
password_digest
|
|
22
|
+
encrypted_password
|
|
23
|
+
remember_token
|
|
24
|
+
reset_password_token
|
|
25
|
+
confirmation_token
|
|
26
|
+
session_token
|
|
27
|
+
api_key
|
|
28
|
+
secret_token
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
TYPE_MATCHERS = {
|
|
32
|
+
boolean: ->(v) { v == true || v == false },
|
|
33
|
+
datetime: ->(v) { v.is_a?(Date) || v.is_a?(Time) || v.is_a?(DateTime) || v.is_a?(ActiveSupport::TimeWithZone) },
|
|
34
|
+
leading_zero_string: ->(v) { v.is_a?(String) && v.start_with?('0') },
|
|
35
|
+
string: ->(v) { v.is_a?(String) && !v.include?('@') }
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
TYPE_FORMATTERS = {
|
|
39
|
+
boolean: ->(v) { v ? 'Yes' : 'No' },
|
|
40
|
+
datetime: ->(v) { v.strftime('%A, %B %-d, %Y %H:%M') },
|
|
41
|
+
leading_zero_string: ->(v) { "'#{v}" }
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
DEFAULT_CSV_HEADER_FORMATTER = ->(key) { key.to_s.humanize.titleize }
|
|
45
|
+
|
|
46
|
+
mattr_accessor :batch_size, default: DEFAULT_BATCH_SIZE
|
|
47
|
+
mattr_accessor :sensitive_attributes, default: DEFAULT_SENSITIVE_ATTRIBUTES.dup
|
|
48
|
+
mattr_accessor :type_formatters, default: TYPE_FORMATTERS.dup
|
|
49
|
+
mattr_accessor :csv_header_formatter, default: DEFAULT_CSV_HEADER_FORMATTER
|
|
50
|
+
|
|
51
|
+
def self.configure
|
|
52
|
+
yield self
|
|
53
|
+
end
|
|
15
54
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easy_exports
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dapilah Sydney
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -106,10 +105,13 @@ files:
|
|
|
106
105
|
- README.md
|
|
107
106
|
- Rakefile
|
|
108
107
|
- lib/easy_exports.rb
|
|
108
|
+
- lib/easy_exports/data_attributes_resolver.rb
|
|
109
|
+
- lib/easy_exports/data_loader.rb
|
|
109
110
|
- lib/easy_exports/exclude_associations_configurations.rb
|
|
110
111
|
- lib/easy_exports/exclude_exportable_attributes_configurations.rb
|
|
111
112
|
- lib/easy_exports/export.rb
|
|
112
113
|
- lib/easy_exports/exportable_association_aliases_configurations.rb
|
|
114
|
+
- lib/easy_exports/exportable_attribute_formatters_configurations.rb
|
|
113
115
|
- lib/easy_exports/exportable_attribute_resolvers.rb
|
|
114
116
|
- lib/easy_exports/exportable_attributes.rb
|
|
115
117
|
- lib/easy_exports/exports_generable.rb
|
|
@@ -124,7 +126,6 @@ metadata:
|
|
|
124
126
|
homepage_uri: https://github.com/SydDaps/easy_exports
|
|
125
127
|
source_code_uri: https://github.com/SydDaps/easy_exports
|
|
126
128
|
changelog_uri: https://rubygems.org/
|
|
127
|
-
post_install_message:
|
|
128
129
|
rdoc_options: []
|
|
129
130
|
require_paths:
|
|
130
131
|
- lib
|
|
@@ -139,8 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
140
|
- !ruby/object:Gem::Version
|
|
140
141
|
version: '0'
|
|
141
142
|
requirements: []
|
|
142
|
-
rubygems_version: 3.2
|
|
143
|
-
signing_key:
|
|
143
|
+
rubygems_version: 3.7.2
|
|
144
144
|
specification_version: 4
|
|
145
145
|
summary: Streamline data retrieval from Rails models
|
|
146
146
|
test_files: []
|