easy_exports 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9e4470d18b361781bf745a2ca0dc563b82ccf603b456a55ad75a96113d3975c
4
- data.tar.gz: 2576218f1f50c7743f1ef0a9cbada4e4ebe452f20dfb12dec4613aedcb0f23d4
3
+ metadata.gz: ed1cc375db87028330d34ded26b7e247a1ff4382267254118e06b994d5ae66d6
4
+ data.tar.gz: 4529a792f705691f04ea2b707a00503866d324ca2388bb5587ffd61563a8f98c
5
5
  SHA512:
6
- metadata.gz: 401cf20c2ee26d378b6f31d02acb64e34bf49761c417f65e0954d9132f45855feb8fcc13be73948cbdbb5e1b941df1e30363a23828ed14bdec8601e1a13c4309
7
- data.tar.gz: 2c5e04b944b9551843802c46ce131f5d0c7ff0e732d1db1f3fc07ef93a9e2227b67771e3a287da54b123d8b4e254e20206e41b9f8a70728cf79f4a3d287fb835
6
+ metadata.gz: c5ca2b24a5a23f685d04dfd020cff2dea3bab1d550f40c1467cfbc04608cfaf701bd69fd6eb91652a1f4ede40f8608fee8f87ac4bd22ff3742155b7238f19eab
7
+ data.tar.gz: 565ddc57bd713f6097aad5717ddb9c95d39fc18d7e3a5db2a81b5d489098c0fdf17723b7b83d9b018f25ef88c6ecb7edebfc7c364fb6aa1b5add5ae7db80ad5c
data/README.md CHANGED
@@ -1,8 +1,5 @@
1
1
  # EasyExports
2
- Short description and motivation.
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,211 @@ 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)` 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
+
67
+ 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.
68
+
69
+ ```ruby
70
+ user_exportable_attributes = {"User"=>["id", "first name"], "Phones"=>["id", "number"], "Emails"=>["id", "address"]}
71
+
72
+ exports_object = User.generate_exports(user_exportable_attributes)
73
+ # => EasyExports::Export(Object)
74
+
75
+ exports_data = exports_object.data
76
+ # =>
77
+ # [
78
+ # {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>1, "phones_number"=>"(473) 693-8745", "emails_id"=>5, "emails_address"=>"blake_armstrong@bahringer.test"},
79
+ # {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>2, "phones_number"=>"594-299-0722", "emails_id"=>6, "emails_address"=>"dulce@mertz.example"},
80
+ # {"user_id"=>1, "user_first_name"=>"Sydney", "phones_id"=>3, "phones_number"=>"1-609-662-2028", "emails_id"=>nil, "emails_address"=>nil},
81
+ # {"user_id"=>2, "user_first_name"=>"Stan", "phones_id"=>4, "phones_number"=>"951-671-9548", "emails_id"=>7, "emails_address"=>"dominick@durgan.example"},
82
+ # {"user_id"=>2, "user_first_name"=>"Stan", "phones_id"=>5, "phones_number"=>"1-698-432-7489", "emails_id"=>nil, "emails_address"=>nil}
83
+ # ]
84
+
85
+ exports_csv_string = exports_object.csv_string
86
+ # =>
87
+ # "user_id,user_first_name,phones_id,phones_number,emails_id,emails_address\n
88
+ # 1,Sydney,1,(473) 693-8745,5,blake_armstrong@bahringer.test\n
89
+ # 1,Sydney,2,594-299-0722,6,dulce@mertz.example\n
90
+ # 1,Sydney,3,1-609-662-2028,,\n
91
+ # 2,Stan,4,951-671-9548,7,dominick@durgan.example\n
92
+ # 2,Stan,5,1-698-432-7489,,\n"
93
+
94
+ # Writing csv_string to a file to visualize the generated export
95
+ File.open(file_path, 'w') do |file|
96
+ file.write(exports_csv_string)
97
+ end
98
+ ```
99
+ <div align="center">
100
+ <img width="600" alt="csv_with_emails" src="https://github.com/SydDaps/easy_exports/assets/51008616/8b9df43c-419d-4ca0-8cb1-6217025b8050">
101
+ </div>
102
+
103
+ Exported CSV showcases data for:
104
+
105
+ - User "Sydney" with 3 phones and 2 emails
106
+ - User "Stan" with 2 phones and 1 email.
107
+ - The main CSV header includes association names and attribute names.
108
+
109
+
110
+ ### Exportable Attributes Aliases
111
+
112
+ Configure an alternative association name for exportable attributes using the `exportable_association_aliases(aliases)` model method.
113
+
114
+ - Invoke this method below all association definitions.
115
+ - `aliases` should be a hash in the pattern: `{valid_association_name or model_name: "alternative_name"}`.
116
+ - Ensure all hash arguments are snake-cased.
117
+
118
+ ```ruby
119
+ # Example Model with exportable_association_aliases
120
+
121
+ # User model with columns: first_name, last_name, created_at, updated_at
122
+ class User < ApplicationRecord
123
+ has_many :phones
124
+
125
+ exportable_association_aliases phones: :mobile_phones
126
+ end
127
+
128
+ # Exportable attributes for the User model will now be
129
+ User.exportable_attributes
130
+ # =>
131
+ # {
132
+ # "User" => ["id", "first name", "last name", "created at", "updated at"],
133
+ # "Mobile phones" => ["id", "number", "user id", "created at", "updated at"]
134
+ # }
135
+ ```
136
+ 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.
137
+
138
+ ### Excluding Specific Exportable Attributes
139
+
140
+ Configure associations to exclude certain attributes from exportable attributes using the `exclude_exportable_attributes(association_attributes)` model method.
141
+
142
+ - Invoke this method below all association declarations.
143
+ - `association_attributes` should follow the pattern `{valid_association_name or model_name: [valid_attributes_to_remove]}`.
144
+ - For removing attributes across all associations and the model itself, use the "all" key as the association_name.
145
+
146
+ ```ruby
147
+ # Example Model with exclude_exportable_attributes
148
+
149
+ # User model with columns: first_name, last_name, created_at, updated_at
150
+ class User < ApplicationRecord
151
+ has_many :phones
152
+
153
+ exclude_exportable_attributes all: [:id], user: [:last_name], phones: [:user_id]
154
+ end
155
+
156
+ # Exportable attributes for the User model will now be
157
+ User.exportable_attributes
158
+ # =>
159
+ # {
160
+ # "User" => ["first name", "created at", "updated at"],
161
+ # "Phones" => ["number", "created at", "updated at"]
162
+ # }
163
+ ```
164
+ In this example, note that:
165
+ - All associations exclude the id attribute.
166
+ - The User model excludes the last_name attribute.
167
+ - The Phones association excludes the user_id attribute.
168
+
169
+
170
+ ### Excluding Specific Exportable Attribute Associations
171
+
172
+ Configure model's exportable attributes to exclude certain associations using the `associations_to_exclude(associations)` model method.
173
+
174
+ - Apply this method below all association declarations.
175
+ - `associations` should follow the pattern `['association_name']`.
23
176
 
24
- ## Contributing
25
- Contribution directions go here.
177
+ ```ruby
178
+ # Example Model with associations_to_exclude
179
+
180
+ # User model with columns: first_name, last_name, created_at, updated_at
181
+ class User < ApplicationRecord
182
+ has_many :phones
183
+
184
+ associations_to_exclude [:phones]
185
+ end
186
+
187
+ # Exportable attributes for the User model will now be
188
+ User.exportable_attributes
189
+ # =>
190
+ # {
191
+ # "User" => ["id", "first name", "last name", "created at", "updated at"]
192
+ # }
193
+ ```
194
+ In this example, the attributes of the phones association are excluded from the exportable attributes of the User model.
195
+
196
+
197
+ ### Adding Custom Attribute to Exportable Attributes
198
+
199
+ Leverage a handy Rails method to transform a model instance method into an attribute, incorporating it into the exportable attributes.
200
+
201
+ ```ruby
202
+ # Example Model with Custom Virtual Attribute
203
+
204
+ # User model with columns: first_name, last_name, created_at, updated_at
205
+ class User < ApplicationRecord
206
+ attribute :total_number_of_phones
207
+
208
+ has_many :phones
209
+
210
+ associations_to_exclude [:phones]
211
+
212
+ def total_number_of_phones
213
+ phones.size
214
+ end
215
+ end
216
+
217
+ # Exportable attributes for the User model will now include
218
+ User.exportable_attributes
219
+ # =>
220
+ # {
221
+ # "User" => ["id", "first name", "last name", "created at", "updated at", "total number of phones"]
222
+ # }
223
+ ```
224
+ 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
225
 
27
226
  ## License
28
227
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,53 @@
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
+ objects.empty? ? [nil] : objects.map { |object| parse_attribute_value(object.send(attribute)) }.flatten
30
+ end
31
+
32
+ def parse_attribute_value(value)
33
+ return DateTime.parse(value.to_s).strftime('%Y-%m-%d %H:%M:%S') if value.is_a?(ActiveSupport::TimeWithZone)
34
+
35
+ return "'#{value}" if value.is_a?(String) && value&.start_with?('0')
36
+
37
+ value
38
+ end
39
+
40
+ def objects_for_attribute(association_name, record)
41
+ object = association_name == underscored_self_name ? record : record.send(association_name)
42
+ object.respond_to?(:each) ? object : [object].compact
43
+ end
44
+
45
+ def export_header(association_name, attribute)
46
+ association_alias = associations_aliases_store[underscored_self_name]
47
+ association_alias = association_alias.blank? ? nil : association_alias[association_name]
48
+
49
+ "#{association_alias || association_name}_#{attribute}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
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)
11
+ validate_association_attributes(selected_attributes, 'to_exported_data')
12
+
13
+ records_with_preloaded_associations(ids, selected_attributes)
14
+ end
15
+
16
+ def association_attributes(association_name)
17
+ association_name = if association_name == underscored_self_name
18
+ association_name.classify
19
+ else
20
+ reflect_on_all_associations.find do |association|
21
+ association.name.to_s == association_name
22
+ end&.class_name
23
+ end
24
+
25
+ association_name.constantize.attribute_names
26
+ end
27
+
28
+ def records_with_preloaded_associations(ids, selected_attributes)
29
+ records = ids.blank? ? all : where(id: ids)
30
+
31
+ associations_to_preload = selected_attributes.keys
32
+ associations_to_preload.delete(underscored_self_name)
33
+
34
+ ActiveRecord::Associations::Preloader.new(
35
+ records: records,
36
+ associations: associations_to_preload
37
+ ).call
38
+
39
+ records
40
+ end
41
+ end
42
+ end
43
+ end
@@ -50,6 +50,4 @@ module EasyExports
50
50
  end
51
51
  end
52
52
  end
53
- end
54
-
55
- ActiveRecord::Base.include EasyExports::ExcludeExportableAttributesConfigurations
53
+ end
@@ -42,4 +42,4 @@ module EasyExports
42
42
  end
43
43
  end
44
44
 
45
- ActiveRecord::Base.include EasyExports::ExportableAssociationAliasesConfigurations
45
+ # ActiveRecord::Base.include EasyExports::ExportableAssociationAliasesConfigurations
@@ -32,5 +32,3 @@ module EasyExports
32
32
  end
33
33
  end
34
34
  end
35
-
36
- ActiveRecord::Base.include EasyExports::ExportableAttributeResolvers
@@ -4,11 +4,19 @@ module EasyExports
4
4
  module ExportableAttributes
5
5
  extend ActiveSupport::Concern
6
6
 
7
- class_methods do
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
+ #TODO: cache attributes(make option)
12
+
13
+ include EasyExports::ExportableAssociationAliasesConfigurations
14
+ include EasyExports::ExportableAttributeResolvers
15
+ include EasyExports::ExcludeExportableAttributesConfigurations
16
+ include EasyExports::ExportsGenerable
17
+ end
11
18
 
19
+ class_methods do
12
20
  def exportable_attributes
13
21
  self_with_associations.each_with_object({}) do |association, attributes|
14
22
  association_name = association.name.to_s.downcase
@@ -41,7 +49,7 @@ module EasyExports
41
49
  end
42
50
 
43
51
  def humanize_attribute_names(attributes)
44
- attributes.map { |attribute| attribute.humanize(keep_id_suffix: true).downcase }
52
+ attributes.map { |attribute| attribute.humanize(keep_id_suffix: true).downcase }.sort
45
53
  end
46
54
  end
47
55
  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
13
  def generate_exports(fields_to_export = {}, ids = [])
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
22
  records = fetch_records(ids, selected_attributes)
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)
@@ -33,90 +40,31 @@ module EasyExports
33
40
  end
34
41
  end
35
42
 
36
- def generate_export_row_template(selected_attributes)
37
- selected_attributes.each_with_object({}) do |(association_name, attributes), export_row|
38
- attributes.each do |attribute|
39
- export_row.merge!("#{association_name}_#{attribute}" => nil)
40
- end
41
- end
42
- end
43
+ def rearrange_selected_attributes(selected_attributes)
44
+ selected_attributes = selected_attributes.to_a
45
+ self_alias_name = associations_aliases_store[underscored_self_name]&.fetch(underscored_self_name, nil)
46
+ self_name = self_alias_name || underscored_self_name
43
47
 
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
58
-
59
- association_name.constantize.attribute_names
60
- end
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)
48
+ self_exportable_attributes = selected_attributes.find do |selected_attribute|
49
+ selected_attribute.first == self_name
50
+ end
67
51
 
68
- ActiveRecord::Associations::Preloader.new(
69
- records: records,
70
- associations: associations_to_preload
71
- ).call
52
+ if self_exportable_attributes.blank? || selected_attributes[0] == self_exportable_attributes
53
+ return selected_attributes.to_h
54
+ end
72
55
 
73
- records
56
+ selected_attributes.delete(self_exportable_attributes)
57
+ selected_attributes.unshift(self_exportable_attributes).to_h
74
58
  end
75
59
 
76
- def value_from_selected_attributes(selected_attributes, record, export_row_template)
77
- selected_attributes.each_with_object([export_row_template]) do |(association_name, attributes), export_rows|
78
- objects = objects_for_attribute(association_name, record)
79
-
60
+ def generate_export_row_template(selected_attributes)
61
+ selected_attributes.each_with_object({}) do |(association_name, attributes), export_row|
80
62
  attributes.each do |attribute|
81
- attribute_values = resolve_attributes(attribute, objects)
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
63
+ export_row.merge!("#{association_name}_#{attribute}" => nil)
88
64
  end
89
65
  end
90
66
  end
91
67
 
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
68
  def revert_exportable_attributes_aliases(attributes_with_aliases)
121
69
  attributes_with_aliases.transform_keys { |key| reversed_associations_name_aliases[key] || key }
122
70
  end
@@ -138,5 +86,3 @@ module EasyExports
138
86
  end
139
87
  end
140
88
  end
141
-
142
- ActiveRecord::Base.include EasyExports::ExportsGenerable
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyExports
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.2'
5
5
  end
data/lib/easy_exports.rb CHANGED
@@ -2,14 +2,12 @@
2
2
 
3
3
  require 'easy_exports/version'
4
4
  require 'easy_exports/railtie'
5
- require 'easy_exports/exportable_attributes'
6
5
  require 'easy_exports/exportable_attribute_resolvers'
7
6
  require 'easy_exports/exclude_associations_configurations'
8
7
  require 'easy_exports/exclude_exportable_attributes_configurations'
9
8
  require 'easy_exports/exportable_association_aliases_configurations'
10
- require 'easy_exports/exports_generable'
11
9
  require 'easy_exports/export'
12
-
13
- module EasyExports
14
- # Your code goes here...
15
- end
10
+ require 'easy_exports/data_loader'
11
+ require 'easy_exports/data_attributes_resolver'
12
+ require 'easy_exports/exports_generable'
13
+ require 'easy_exports/exportable_attributes'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_exports
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dapilah Sydney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-16 00:00:00.000000000 Z
11
+ date: 2023-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -106,6 +106,8 @@ files:
106
106
  - README.md
107
107
  - Rakefile
108
108
  - lib/easy_exports.rb
109
+ - lib/easy_exports/data_attributes_resolver.rb
110
+ - lib/easy_exports/data_loader.rb
109
111
  - lib/easy_exports/exclude_associations_configurations.rb
110
112
  - lib/easy_exports/exclude_exportable_attributes_configurations.rb
111
113
  - lib/easy_exports/export.rb