activekit 0.3.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1305c44f47755be521e42ebb643e3ec9d4e267d2d04199b5e1d182e444d0b275
4
- data.tar.gz: 10b756b23a70d5054c1b69f48523c575756a73c184e36ae1fc9aa23291d2a2b7
3
+ metadata.gz: 2dffaf7d844bba5ea65c34b98f3872cf70cb44c76963fb0d7a96bb39df174acb
4
+ data.tar.gz: 5c64d866b07f8460145f0a8069ac316e96d9ee15985fcce13e9a598fd2f6dedc
5
5
  SHA512:
6
- metadata.gz: 271eb9911286e8970c7ef6327ba67d912b83208d554ebfed7129a4821834373883bb14ef1acd1f43028fcf79fb062f95fda1ff48e7a53bdb2cdfce648d49a0ce
7
- data.tar.gz: 2049ff4e916f3f5ba230b060db073380e90e60981393c6dd2abf1870a127a91b2508a92d0d786448c80c17d4d2aeb0b849d06a60ce136317dec932257b7efc95
6
+ metadata.gz: f704f847169d738fa9f708246661b1dfff4594349fd3c8297b9070542046684106b5f5a831505bc5160c544fcc79d13b2c2920520e5184191832f34ff3e62979
7
+ data.tar.gz: af2af6217c992fb16c4205b88e0172947b043c2f52c93f389570989444a4726aa0fa62392e7d27b4dc48450120cb943c0e7e38c9f4606a3719d0a868badc8608
data/README.md CHANGED
@@ -3,6 +3,40 @@ Add the essential kit for rails ActiveRecord models and be happy.
3
3
 
4
4
  ## Usage
5
5
 
6
+ ### Export Attribute
7
+
8
+ Add exporting to your ActiveRecord models.
9
+ Export Attribute provides full exporting functionality for your model database records.
10
+
11
+ You can define any number of model attributes and association attributes in one model to export together.
12
+
13
+ Define the export attributes in accordance with the column name in your model like below.
14
+ ```ruby
15
+ class Product < ApplicationRecord
16
+ export_attribute :name
17
+ export_attribute :sku, heading: "SKU No."
18
+ export_attribute :image_name, value: lambda { |record| record.image&.name }, includes: :image
19
+ export_attribute :variations, value: lambda { |record| record.variations }, includes: :variations, attributes: [:name, :price, discount_value: { heading: "Discount" }]
20
+ end
21
+ ```
22
+
23
+ You can also define an export_describer to describe the details of the export instead of using the defaults.
24
+ ```ruby
25
+ class Product < ApplicationRecord
26
+ # export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }
27
+ export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database.to_sym }
28
+ export_attribute :name
29
+ export_attribute :sku, heading: "SKU No."
30
+ export_attribute :image_name, value: lambda { |record| record.image&.name }, includes: :image
31
+ export_attribute :variations, value: lambda { |record| record.variations }, includes: :variations, attributes: [:name, :price, discount_value: { heading: "Discount" }]
32
+ end
33
+ ```
34
+
35
+ The following class methods will be added to your model class to use in accordance with details provided for export_describer:
36
+ ```ruby
37
+ Product.to_csv
38
+ ```
39
+
6
40
  ### Position Attribute
7
41
 
8
42
  Add positioning to your ActiveRecord models.
@@ -9,10 +9,12 @@ module ActiveKit
9
9
  app.middleware.use ActiveKit::Position::Middleware
10
10
  end
11
11
 
12
- initializer "active_kit.position" do
12
+ initializer "active_kit.activekitable" do
13
+ require "active_kit/export/exportable"
13
14
  require "active_kit/position/positionable"
14
15
 
15
16
  ActiveSupport.on_load(:active_record) do
17
+ include ActiveKit::Export::Exportable
16
18
  include ActiveKit::Position::Positionable
17
19
  end
18
20
  end
@@ -0,0 +1,65 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveKit
4
+ module Export
5
+ module Exportable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ end
10
+
11
+ class_methods do
12
+ def exporter
13
+ @exporter ||= ActiveKit::Export::Exporter.new(current_class: self)
14
+ end
15
+
16
+ def export_describer(name, **options)
17
+ name = name.to_sym
18
+ options.deep_symbolize_keys!
19
+
20
+ unless exporter.find_describer_by(describer_name: name)
21
+ exporter.new_describer(name: name, options: options)
22
+ define_describer_method(kind: options[:kind], name: name)
23
+ end
24
+ end
25
+
26
+ def export_attribute(name, **options)
27
+ export_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless exporter.describers?
28
+
29
+ options.deep_symbolize_keys!
30
+ exporter.new_attribute(name: name.to_sym, options: options)
31
+ end
32
+
33
+ def define_describer_method(kind:, name:)
34
+ case kind
35
+ when :csv
36
+ define_singleton_method name do
37
+ describer = exporter.find_describer_by(describer_name: name)
38
+ raise "could not find describer for the describer name '#{name}'" unless describer.present?
39
+
40
+ # The 'all' relation must be captured outside the Enumerator,
41
+ # else it will get reset to all the records of the class.
42
+ all_activerecord_relation = all.includes(describer.includes)
43
+
44
+ Enumerator.new do |yielder|
45
+ ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
46
+ exporting = exporter.new_exporting(describer: describer)
47
+
48
+ # Add the headings.
49
+ yielder << CSV.generate_line(exporting.headings) if exporting.headings?
50
+
51
+ # Add the values.
52
+ # find_each will ignore any order if set earlier.
53
+ all_activerecord_relation.find_each do |record|
54
+ lines = exporting.lines_for(record: record)
55
+ lines.each { |line| yielder << CSV.generate_line(line) }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,106 @@
1
+ module ActiveKit
2
+ module Export
3
+ class Exporter
4
+ attr_reader :describers
5
+
6
+ def initialize(current_class:)
7
+ @current_class = current_class
8
+ @describers = {}
9
+ end
10
+
11
+ def find_describer_by(describer_name:)
12
+ describer_options = @describers.dig(describer_name)
13
+ return nil unless describer_options.present?
14
+
15
+ describer_attributes = describer_options[:attributes]
16
+ includes = describer_attributes.values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq
17
+ fields = build_describer_fields(describer_attributes)
18
+ hash = {
19
+ name: describer_name,
20
+ kind: describer_options[:kind],
21
+ database: describer_options[:database],
22
+ attributes: describer_attributes,
23
+ includes: includes,
24
+ fields: fields
25
+ }
26
+ OpenStruct.new(hash)
27
+ end
28
+
29
+ def new_describer(name:, options:)
30
+ options.store(:attributes, {})
31
+ @describers.store(name, options)
32
+ end
33
+
34
+ def describers?
35
+ @describers.present?
36
+ end
37
+
38
+ def new_attribute(name:, options:)
39
+ describer_names = Array(options.delete(:describers))
40
+ describer_names = @describers.keys if describer_names.blank?
41
+
42
+ describer_names.each do |describer_name|
43
+ if describer_options = @describers.dig(describer_name)
44
+ describer_options[:attributes].store(name, options)
45
+ end
46
+ end
47
+ end
48
+
49
+ def new_exporting(describer:)
50
+ Exporting.new(describer: describer)
51
+ end
52
+
53
+ private
54
+
55
+ def build_describer_fields(describer_attributes)
56
+ describer_attributes.inject({}) do |fields_hash, (name, options)|
57
+ enclosed_attributes = Array(options.dig(:attributes))
58
+
59
+ if enclosed_attributes.blank?
60
+ field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
61
+ else
62
+ field_key, field_value = get_nested_field(name, options, enclosed_attributes)
63
+ end
64
+ fields_hash.store(field_key, field_value)
65
+
66
+ fields_hash
67
+ end
68
+ end
69
+
70
+ def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
71
+ parent_heading = ancestor_heading.present? ? ancestor_heading : ""
72
+ parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
73
+ parent_value = options.dig(:value) || name
74
+
75
+ enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
76
+ unless enclosed_attribute.is_a? Hash
77
+ nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
78
+ nested_field_val = enclosed_attribute
79
+
80
+ nested_field[0].push(nested_field_key)
81
+ nested_field[1].push(nested_field_val)
82
+ else
83
+ enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
84
+ wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
85
+ if wrapped_attributes.blank?
86
+ nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
87
+ nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
88
+ else
89
+ nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
90
+ end
91
+
92
+ nested_field[0].push(nested_field_key)
93
+ nested_field[1].push(nested_field_val)
94
+ end
95
+ end
96
+
97
+ nested_field
98
+ end
99
+ end
100
+
101
+ def get_heading(options_heading)
102
+ options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,104 @@
1
+ module ActiveKit
2
+ module Export
3
+ class Exporting
4
+
5
+ def initialize(describer:)
6
+ @describer = describer
7
+ end
8
+
9
+ def headings
10
+ @headings ||= @describer.fields.keys.flatten
11
+ end
12
+
13
+ def headings?
14
+ headings.present?
15
+ end
16
+
17
+ def lines_for(record:)
18
+ row_counter, column_counter = 1, 0
19
+
20
+ @describer.fields.inject([[]]) do |rows, (heading, value)|
21
+ if value.is_a? Proc
22
+ rows[0].push(value.call(record))
23
+ column_counter += 1
24
+ elsif value.is_a?(Symbol) || value.is_a?(String)
25
+ rows[0].push(record.public_send(value))
26
+ column_counter += 1
27
+ elsif value.is_a? Array
28
+ deeprows = get_deeprows(record, heading, value, column_counter)
29
+ deeprows.each do |deeprow|
30
+ rows[row_counter] = deeprow
31
+ row_counter += 1
32
+ end
33
+
34
+ column_count = get_column_count_for(value)
35
+ column_count.times { |i| rows[0].push(nil) }
36
+ column_counter += column_count
37
+ else
38
+ raise "Could not identify '#{value}' for '#{heading}'."
39
+ end
40
+
41
+ rows
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def get_deeprows(record, heading, value, column_counter)
48
+ value_clone = value.clone
49
+ assoc_value = value_clone.shift
50
+
51
+ if assoc_value.is_a? Proc
52
+ assoc_records = assoc_value.call(record)
53
+ elsif assoc_value.is_a?(Symbol) || assoc_value.is_a?(String)
54
+ assoc_records = record.public_send(assoc_value)
55
+ else
56
+ raise "Count not identity '#{assoc_value}' for '#{heading}'."
57
+ end
58
+
59
+ subrows = []
60
+ assoc_records.each do |assoc_record|
61
+ subrow, subrow_column_counter, deeprows = [], 0, []
62
+ column_counter.times { |i| subrow.push(nil) }
63
+
64
+ subrow = value_clone.inject(subrow) do |subrow, v|
65
+ if v.is_a? Proc
66
+ subrow.push(v.call(assoc_record))
67
+ subrow_column_counter += 1
68
+ elsif v.is_a?(Symbol) || v.is_a?(String)
69
+ subrow.push(assoc_record.public_send(v))
70
+ subrow_column_counter += 1
71
+ elsif v.is_a? Array
72
+ deeprows = get_deeprows(assoc_record, heading, v, (column_counter + subrow_column_counter))
73
+
74
+ column_count = get_column_count_for(v)
75
+ column_count.times { |i| subrow.push(nil) }
76
+ subrow_column_counter += column_count
77
+ end
78
+
79
+ subrow
80
+ end
81
+
82
+ subrows.push(subrow)
83
+ deeprows.each { |deeprow| subrows.push(deeprow) }
84
+ end
85
+
86
+ subrows
87
+ end
88
+
89
+ def get_column_count_for(value)
90
+ count = 0
91
+
92
+ value.each do |v|
93
+ unless v.is_a? Array
94
+ count += 1
95
+ else
96
+ count += get_column_count_for(v)
97
+ end
98
+ end
99
+
100
+ count - 1
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,8 @@
1
+ module ActiveKit
2
+ module Export
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Exporter
6
+ autoload :Exporting
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/active_kit.rb CHANGED
@@ -4,5 +4,6 @@ require "active_kit/engine"
4
4
  module ActiveKit
5
5
  extend ActiveSupport::Autoload
6
6
 
7
+ autoload :Export
7
8
  autoload :Position
8
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activekit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - plainsource
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-01 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -51,6 +51,10 @@ files:
51
51
  - config/routes.rb
52
52
  - lib/active_kit.rb
53
53
  - lib/active_kit/engine.rb
54
+ - lib/active_kit/export.rb
55
+ - lib/active_kit/export/exportable.rb
56
+ - lib/active_kit/export/exporter.rb
57
+ - lib/active_kit/export/exporting.rb
54
58
  - lib/active_kit/position.rb
55
59
  - lib/active_kit/position/harmonize.rb
56
60
  - lib/active_kit/position/middleware.rb