easy_exports 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a9e4470d18b361781bf745a2ca0dc563b82ccf603b456a55ad75a96113d3975c
4
+ data.tar.gz: 2576218f1f50c7743f1ef0a9cbada4e4ebe452f20dfb12dec4613aedcb0f23d4
5
+ SHA512:
6
+ metadata.gz: 401cf20c2ee26d378b6f31d02acb64e34bf49761c417f65e0954d9132f45855feb8fcc13be73948cbdbb5e1b941df1e30363a23828ed14bdec8601e1a13c4309
7
+ data.tar.gz: 2c5e04b944b9551843802c46ce131f5d0c7ff0e732d1db1f3fc07ef93a9e2227b67771e3a287da54b123d8b4e254e20206e41b9f8a70728cf79f4a3d287fb835
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Dapilah Sydney
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # EasyExports
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "easy_exports"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install easy_exports
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExcludeAssociationsConfigurations
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ private
9
+
10
+ # associations to exclude methods
11
+ def associations_to_exclude(associations = [])
12
+ validate_associations_to_exclude_argument(associations)
13
+
14
+ associations.map! do |association|
15
+ association_name = association.to_s.downcase
16
+
17
+ association_name.tap do |name|
18
+ if association_from_self_with_association(name).blank?
19
+ raise ArgumentError,
20
+ "associations_to_exclude array argument '#{name}' is not an association for #{underscored_self_name} model"
21
+ end
22
+ end
23
+ end
24
+
25
+ associations_to_exclude_store.merge!(
26
+ underscored_self_name => associations
27
+ )
28
+ end
29
+
30
+ def validate_associations_to_exclude_argument(argument)
31
+ raise 'Argument for associations_to_exclude has to be an array' unless argument.is_a? Array
32
+
33
+ return if argument.all? { |element| [String, Symbol].include? element.class }
34
+
35
+ raise 'Argument array for associations_to_exclude has to be either string or Symbol'
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ ActiveRecord::Base.include EasyExports::ExcludeAssociationsConfigurations
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExcludeExportableAttributesConfigurations
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ private
9
+
10
+ def exclude_exportable_attributes(association_attributes = {})
11
+ validate_exclude_exportable_attributes_argument(association_attributes)
12
+ association_attributes.transform_values! { |values| values.map(&:to_s) }
13
+
14
+ validate_association_attributes(association_attributes, 'exclude_exportable_attributes')
15
+
16
+ excluded_exportable_attributes_store.merge!(
17
+ underscored_self_name => association_attributes.stringify_keys
18
+ )
19
+ end
20
+
21
+ def validate_association_attributes(association_attributes, method = '')
22
+ association_attributes.each do |association_name, attributes|
23
+ association = association_from_self_with_association(association_name)
24
+
25
+ if association.blank?
26
+ next if association_name.to_s.downcase == 'all'
27
+
28
+ raise ArgumentError,
29
+ "#{method} argument key '#{association_name}' is not an association for #{underscored_self_name} model"
30
+ end
31
+
32
+ invalid_attributes = attributes - association.class_name.constantize.attribute_names
33
+ next if invalid_attributes.empty?
34
+
35
+ raise ArgumentError, "#{method} #{invalid_attributes.join(', ')} not defined for #{association.name}"
36
+ end
37
+ end
38
+
39
+ def validate_exclude_exportable_attributes_argument(argument, method_name = 'exclude_exportable_attributes')
40
+ raise 'Argument for exclude_exportable_attributes has to be a hash' unless argument.is_a? Hash
41
+
42
+ argument.to_a.each do |arg|
43
+ case arg
44
+ in [*, [*]] if arg[1].all? { |element| [String, Symbol].include? element.class }
45
+ next
46
+ else
47
+ raise ArgumentError, "Invalid Arguments pattern for #{method_name}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ ActiveRecord::Base.include EasyExports::ExcludeExportableAttributesConfigurations
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ class Export
5
+ attr_reader :data, :csv_string
6
+
7
+ def initialize(exported_data, exported_data_csv_string)
8
+ @data = exported_data
9
+ @csv_string = exported_data_csv_string
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExportableAssociationAliasesConfigurations
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ private
9
+
10
+ def exportable_association_aliases(aliases = {})
11
+ validate_exportable_association_aliases_argument(aliases)
12
+
13
+ aliases.transform_values!(&:to_s)
14
+ aliases.stringify_keys!
15
+
16
+ aliases.each do |association_name, _alias_name|
17
+ next if association_from_self_with_association(association_name).present?
18
+
19
+ error_message = <<~MESSAGE
20
+ exportable_association_aliases argument '#{association_name}' is not an association for #{underscored_self_name} model
21
+ MESSAGE
22
+
23
+ raise ArgumentError, error_message
24
+ end
25
+
26
+ associations_aliases_store.merge!(underscored_self_name => aliases.stringify_keys)
27
+ end
28
+
29
+ def validate_exportable_association_aliases_argument(argument)
30
+ raise 'Argument for exportable_associations_aliases has to be a hash' unless argument.is_a? Hash
31
+
32
+ argument.to_a.each do |arg|
33
+ case arg
34
+ in [*, String | Symbol]
35
+ next
36
+ else
37
+ raise 'Invalid Arguments pattern for exportable_associations_aliases'
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ ActiveRecord::Base.include EasyExports::ExportableAssociationAliasesConfigurations
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExportableAttributeResolvers
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ private
9
+
10
+ def resolve_excluded_exportable_attributes(association_name)
11
+ exclude_exportable_attributes_for_self = excluded_exportable_attributes_store[underscored_self_name]
12
+
13
+ return [] if exclude_exportable_attributes_for_self.blank?
14
+
15
+ exclude_exportable_attributes_for_self.with_indifferent_access.slice(
16
+ association_name,
17
+ 'all'
18
+ ).values.compact.flatten.uniq
19
+ end
20
+
21
+ def resolve_attributes_for_association(association)
22
+ association_attributes = association.class_name.constantize.attribute_names
23
+ association_attributes - resolve_excluded_exportable_attributes(association.name.to_s.downcase)
24
+ end
25
+
26
+ def resolve_associations_names_aliases(association_name)
27
+ association_aliases_for_self = associations_aliases_store[underscored_self_name]
28
+ return association_name if association_aliases_for_self.blank?
29
+
30
+ association_aliases_for_self[association_name] || association_name
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ ActiveRecord::Base.include EasyExports::ExportableAttributeResolvers
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExportableAttributes
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ cattr_accessor :excluded_exportable_attributes_store, default: {}, instance_writer: false
9
+ cattr_accessor :associations_aliases_store, default: {}, instance_writer: false
10
+ cattr_accessor :associations_to_exclude_store, default: {}, instance_writer: false
11
+
12
+ def exportable_attributes
13
+ self_with_associations.each_with_object({}) do |association, attributes|
14
+ association_name = association.name.to_s.downcase
15
+ next if associations_to_exclude_store[underscored_self_name]&.include? association_name
16
+
17
+ association_attributes = resolve_attributes_for_association(association)
18
+
19
+ association_name = resolve_associations_names_aliases(association_name)
20
+ attributes[humanize_model_name(association_name)] = humanize_attribute_names(association_attributes)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def self_with_associations
27
+ mock_self_as_reflection = OpenStruct.new(class_name: name, name: underscored_self_name)
28
+ reflect_on_all_associations.unshift(mock_self_as_reflection)
29
+ end
30
+
31
+ def association_from_self_with_association(association_name)
32
+ self_with_associations.find { |association| association.name.to_s.downcase == association_name.to_s.downcase }
33
+ end
34
+
35
+ def underscored_self_name
36
+ name.underscore.downcase
37
+ end
38
+
39
+ def humanize_model_name(model_name)
40
+ model_name.underscore.humanize(keep_id_suffix: true)
41
+ end
42
+
43
+ def humanize_attribute_names(attributes)
44
+ attributes.map { |attribute| attribute.humanize(keep_id_suffix: true).downcase }
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ ActiveRecord::Base.include EasyExports::ExportableAttributes
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ module ExportsGenerable
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def generate_exports(fields_to_export = {}, ids = [])
9
+ validate_exclude_exportable_attributes_argument(fields_to_export, 'generate_exports')
10
+
11
+ selected_exportable_attributes = revert_transformed_names(fields_to_export)
12
+ export_row_template = generate_export_row_template(selected_exportable_attributes)
13
+
14
+ selected_attributes = revert_exportable_attributes_aliases(selected_exportable_attributes)
15
+ records = fetch_records(ids, selected_attributes)
16
+
17
+ 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)
19
+ end.flatten
20
+
21
+ csv_string = write_exported_data_to_csv(exported_data, export_row_template)
22
+
23
+ EasyExports::Export.new(exported_data, csv_string)
24
+ end
25
+
26
+ def write_exported_data_to_csv(exported_data, export_row_template)
27
+ CSV.generate(headers: true) do |csv|
28
+ csv << export_row_template.keys
29
+
30
+ exported_data.each do |data|
31
+ csv << data.values
32
+ end
33
+ end
34
+ end
35
+
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
+
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)
67
+
68
+ ActiveRecord::Associations::Preloader.new(
69
+ records: records,
70
+ associations: associations_to_preload
71
+ ).call
72
+
73
+ records
74
+ end
75
+
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
+
80
+ 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
88
+ end
89
+ end
90
+ end
91
+
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
+ def revert_exportable_attributes_aliases(attributes_with_aliases)
121
+ attributes_with_aliases.transform_keys { |key| reversed_associations_name_aliases[key] || key }
122
+ end
123
+
124
+ def reversed_associations_name_aliases
125
+ associations_aliases = associations_aliases_store[underscored_self_name]
126
+ return {} if associations_aliases.blank?
127
+
128
+ associations_aliases_store[underscored_self_name].with_indifferent_access.to_a.map(&:reverse).to_h
129
+ end
130
+
131
+ def revert_transformed_names(fields)
132
+ fields.transform_keys! { |key| key.parameterize(separator: '_') }
133
+
134
+ fields.transform_values do |value|
135
+ value.map { |v| v.parameterize(separator: '_') }.uniq
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ ActiveRecord::Base.include EasyExports::ExportsGenerable
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ class Railtie < ::Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyExports
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easy_exports/version'
4
+ require 'easy_exports/railtie'
5
+ require 'easy_exports/exportable_attributes'
6
+ require 'easy_exports/exportable_attribute_resolvers'
7
+ require 'easy_exports/exclude_associations_configurations'
8
+ require 'easy_exports/exclude_exportable_attributes_configurations'
9
+ require 'easy_exports/exportable_association_aliases_configurations'
10
+ require 'easy_exports/exports_generable'
11
+ require 'easy_exports/export'
12
+
13
+ module EasyExports
14
+ # Your code goes here...
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # desc "Explaining what the task does"
3
+ # task :easy_exports do
4
+ # # Task goes here
5
+ # end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_exports
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dapilah Sydney
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-16 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: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: csv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faker
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.56'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.56'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.20'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.20'
83
+ - !ruby/object:Gem::Dependency
84
+ name: byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '11.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '11.1'
97
+ description: Simplify the way you fetch model data, making coding smoother and data
98
+ exporting handling a breeze
99
+ email:
100
+ - dapilah.sydney@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - MIT-LICENSE
106
+ - README.md
107
+ - Rakefile
108
+ - lib/easy_exports.rb
109
+ - lib/easy_exports/exclude_associations_configurations.rb
110
+ - lib/easy_exports/exclude_exportable_attributes_configurations.rb
111
+ - lib/easy_exports/export.rb
112
+ - lib/easy_exports/exportable_association_aliases_configurations.rb
113
+ - lib/easy_exports/exportable_attribute_resolvers.rb
114
+ - lib/easy_exports/exportable_attributes.rb
115
+ - lib/easy_exports/exports_generable.rb
116
+ - lib/easy_exports/railtie.rb
117
+ - lib/easy_exports/version.rb
118
+ - lib/tasks/easy_exports_tasks.rake
119
+ homepage: https://github.com/SydDaps/easy_exports
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ allowed_push_host: https://rubygems.org/
124
+ homepage_uri: https://github.com/SydDaps/easy_exports
125
+ source_code_uri: https://github.com/SydDaps/easy_exports
126
+ changelog_uri: https://rubygems.org/
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: 2.7.0
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.2.3
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Streamline data retrieval from Rails models
146
+ test_files: []