activekit 0.3.1 → 0.4.0

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: c73cb76ead023d8565b199c3fc7a287108c058423c558a15382f150de64d549a
4
- data.tar.gz: a1772c9e23aba4cc64bd21756c09770fa386d241f43d78ef346d3c94cd6c823d
3
+ metadata.gz: 2dffaf7d844bba5ea65c34b98f3872cf70cb44c76963fb0d7a96bb39df174acb
4
+ data.tar.gz: 5c64d866b07f8460145f0a8069ac316e96d9ee15985fcce13e9a598fd2f6dedc
5
5
  SHA512:
6
- metadata.gz: 43e7e2a9318cb06bd2580d5d8a1a7ce34336de43bf1f66afed0b447e66daecc634713864483a4ff48f113392d5f160afe677600f2b5e83f473204603b7595d78
7
- data.tar.gz: 8f73c668f085980288cfbea6743a83d5999807789160cdbcbd01cb345f39cba386d9d80ec2d2af344a8e3a05a3e37ed0d2ce5642a77d7c67c1bed62ec07ecbb6
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
@@ -5,7 +5,7 @@ module ActiveKit
5
5
  @record = record
6
6
  @name = name
7
7
 
8
- @scoped_class = @record.class.where(scope).order("#{@name}": :asc)
8
+ @scoped_class = @record.class.where(scope).reorder("#{@name}": :asc)
9
9
  @reharmonize = false
10
10
  @positioning = Positioning.new
11
11
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.3.1'
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.1
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