magic-report 0.6.0 → 0.7.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: 59c479e22412f34a53caaa404a9febf1186cefe0c0678a9f5c00a4b119f774bf
4
- data.tar.gz: a0d46b4b7226ab1b0c74cd5adc6bd385ef54817ba2e6deb5591b27527e36f507
3
+ metadata.gz: d55f3c2c7537067d5b4e825ec2dae5fb4856243aa436c0fc4a24c411124cf826
4
+ data.tar.gz: f6cb0bd78b371f3649a9fd4cd9df00841a56dfacb4e67bed3074c02e796ab6a3
5
5
  SHA512:
6
- metadata.gz: 715c2887cca5db182f97d578140f9bb44204b70881fc81b0c88ca25033ffd0291d515f08e7e1d8f299c3836324940256622dd63f45e0d90578953a3cbd39f1ef
7
- data.tar.gz: 549a76ff9458405ae70035692cbc88db8c9708a81bd950bc5370c588f5d91316e3753335b30ff4a35c9d5fd55ee211dab7683290b4af3e772fe6b8f43fc71d9a
6
+ metadata.gz: ef278542203ac22032ec90f68d29cda496d224f18ff38c9a2e200e41b1f6fd1dfd5d8628e0dfd0b0bf545df9ec0d2022214fba08df1fa6e5c254a4bc3269bbc4
7
+ data.tar.gz: 55c7b038ec1a3d61e546d58a194e0bf668ffca9989a279df8d8564d052836f579fdb129e203eaed8f913b5aeb6d8f2dacbd0c0da635a7cf51920ded140cf394e
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MagicReport
4
+ class Report
5
+ class Builder
6
+ class Field
7
+ def self.build(report, name, processor)
8
+ new(report, name, processor)
9
+ end
10
+
11
+ attr_reader :report, :name, :processor
12
+
13
+ def initialize(report, name, processor)
14
+ @report = report
15
+ @name = name
16
+ @processor = processor
17
+ end
18
+
19
+ def process(model)
20
+ value = extract_value(model)
21
+
22
+ if processor.is_a? Proc
23
+ processor.call(model, value)
24
+ elsif processor.is_a? Symbol
25
+ # Refactor
26
+ report.new(nil).send(processor, model, value)
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def extract_value(model)
35
+ model.send(name)
36
+ rescue
37
+ nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MagicReport
4
+ class Report
5
+ class Builder
6
+ class HasMany < HasOne
7
+ def process_rows(model, row)
8
+ extract_relation(model).map.with_index do |rel, idx|
9
+ # If we're on a first entry, then we should fill first row
10
+ # Otherwise will be created a new row
11
+ if idx.zero?
12
+ options[:class].new(rel, row).rows
13
+ else
14
+ options[:class].new(rel).rows
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MagicReport
4
+ class Report
5
+ class Builder
6
+ class HasOne
7
+ def self.build(report, name, options, &extension)
8
+ new(report, name, options, extension)
9
+ end
10
+
11
+ attr_reader :report, :name, :options, :extension, :prefix
12
+
13
+ def initialize(report, name, options, extension)
14
+ @report = report
15
+ @name = name
16
+ @options = options
17
+ @extension = extension
18
+ @prefix = I18n.t!("#{report.i18n_scope}.#{report.i18n_key}.#{name}_relation")
19
+
20
+ unless options[:class]
21
+ klass = ::MagicReport::Report.clone
22
+ klass.define_singleton_method(:name) { "#{report.name}/#{name}" }
23
+ klass.class_eval(&extension)
24
+
25
+ options[:class] = klass
26
+ end
27
+ end
28
+
29
+ def process(model)
30
+ relation = extract_relation(model)
31
+
32
+ # Refactor here values method call
33
+ options[:class].new(relation).values
34
+ end
35
+
36
+ def process_rows(model, row)
37
+ relation = extract_relation(model)
38
+
39
+ # Refactor here values method call
40
+ options[:class].new(relation, row).rows
41
+ end
42
+
43
+ def build_row
44
+ options[:class].build_row(prefix)
45
+ end
46
+
47
+ private
48
+
49
+ def extract_relation(model)
50
+ model.send(name)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MagicReport
4
+ class Report
5
+ module Reflection
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_fields, instance_writer: false, default: {}
10
+ class_attribute :_has_one, instance_writer: false, default: {}
11
+ class_attribute :_has_many, instance_writer: false, default: {}
12
+ end
13
+
14
+ class << self
15
+ def add_field(klass, name, reflection)
16
+ klass._fields = klass._fields.except(name).merge!(name => reflection)
17
+ end
18
+
19
+ def add_has_one(klass, name, reflection)
20
+ klass._has_one = klass._has_one.except(name).merge!(name => reflection)
21
+ end
22
+
23
+ def add_has_many(klass, name, reflection)
24
+ klass._has_many = klass._has_many.except(name).merge!(name => reflection)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -3,61 +3,50 @@
3
3
  module MagicReport
4
4
  class Report
5
5
  class Row
6
- attr_accessor :data, :inner_rows, :nested_rows
6
+ class Column
7
+ attr_reader :key, :heading, :prefix, :value
8
+
9
+ def initialize(key, heading, prefix)
10
+ @key = key
11
+ @heading = heading
12
+ @prefix = prefix
13
+ @value = nil
14
+ end
7
15
 
8
- def initialize
9
- @data = {}
10
- @inner_rows = {}
11
- @nested_rows = {}
12
- end
16
+ def assign_value(value)
17
+ @value = value
18
+ end
13
19
 
14
- def add(field:, value:)
15
- data[field] = value
20
+ def full_heading
21
+ prefix ? "#{prefix} #{heading}" : heading
22
+ end
16
23
  end
17
24
 
18
- def add_inner_row(field:, row:)
19
- inner_rows[field] = row
20
- end
25
+ attr_reader :columns, :nested_rows
21
26
 
22
- def add_nested_row(field:, row:)
23
- nested_rows[field] ||= []
24
- nested_rows[field].push(row)
27
+ def initialize
28
+ @columns = []
29
+ @nested_rows = {}
25
30
  end
26
31
 
27
- def values
28
- data.values
32
+ def add_column_value(key:, value:)
33
+ @columns.find { |column| column.key == key }.assign_value(value)
29
34
  end
30
35
 
31
- def complex?
32
- nested_rows.any?
36
+ def add_nested_row(key:, row:)
37
+ nested_rows[key] = row
33
38
  end
34
39
 
35
- def each_nested_row(&block)
36
- nested_rows.keys.flat_map do |key|
37
- nested_rows[key].map do |nested_row|
38
- block.call(nested_row)
39
- end
40
- end
40
+ def register_column(key, heading, prefix = nil)
41
+ @columns << Column.new(key, heading, prefix)
41
42
  end
42
43
 
43
- def each_inner_row(&block)
44
- inner_rows.values.map do |inner_row|
45
- block.call(inner_row)
46
- end
44
+ def headings
45
+ (columns.map(&:full_heading) + nested_rows.values.map(&:headings)).flatten
47
46
  end
48
47
 
49
- def to_h
50
- @to_h ||= begin
51
- original_hash = inner_rows.any? ? data.merge(each_inner_row { |inner_row| inner_row.to_h }.reduce({}, :merge)) : data
52
-
53
- if complex?
54
- each_nested_row do |nested_row|
55
- original_hash.merge(nested_row.to_h)
56
- end
57
- else
58
- original_hash
59
- end
60
- end
48
+ def to_a
49
+ (columns.map(&:value) + nested_rows.values.map(&:to_a)).flatten
61
50
  end
62
51
  end
63
52
  end
@@ -2,107 +2,111 @@
2
2
 
3
3
  module MagicReport
4
4
  class Report
5
- include ClassHelpers
5
+ include Reflection
6
6
 
7
- attr_reader :fields, :has_one, :has_many, :name, :nested_field, :prefix, :result
7
+ attr_reader :model, :row
8
8
 
9
- def initialize(fields: nil, has_one: nil, has_many: nil, name: nil, prefix: nil, nested_field: nil)
10
- @fields = fields || fields_from_class
11
- @has_one = has_one || has_one_from_class
12
- @has_many = has_many || has_many_from_class
13
- @name = name || name_from_class
14
-
15
- @prefix = prefix
16
- @nested_field = nested_field
9
+ def initialize(model, row = nil)
10
+ @model = model
11
+ @row = row || self.class.build_row
17
12
  end
18
13
 
19
- def process(input)
20
- @result = ::MagicReport::Report::Process.new(self).call(input)
21
- end
14
+ def rows
15
+ @rows ||= begin
16
+ rows = []
22
17
 
23
- def as_csv
24
- @as_csv ||= begin
25
- csv = ::MagicReport::Report::Csv.new(self)
26
- csv.generate
18
+ _fields.each_value do |field|
19
+ row.add_column_value(key: field.name, value: field.process(model))
20
+ end
27
21
 
28
- csv
29
- end
30
- end
22
+ _has_one.each_value do |has_one|
23
+ simple_row = row.nested_rows[has_one.name]
24
+
25
+ has_one.process_rows(model, simple_row)
26
+ end
27
+
28
+ rows.push(row)
31
29
 
32
- def as_attachment
33
- {
34
- mime_type: "text/csv",
35
- content: as_csv.io.read
36
- }
30
+ _has_many.each_value do |has_many|
31
+ simple_row = row.nested_rows[has_many.name]
32
+
33
+ resik = has_many.process_rows(model, simple_row)
34
+
35
+ resik.shift.map do |resik_row|
36
+ new_row = self.class.build_row
37
+
38
+ # TODO: copy ID here
39
+ # copy_primary_attributes(new_row, row)
40
+
41
+ new_row.nested_rows[has_many.name] = resik_row
42
+
43
+ rows.push(new_row)
44
+ end
45
+ end
46
+
47
+ rows
48
+ end
37
49
  end
38
50
 
39
51
  def headings
40
- @headings ||= (fields.map { |field| t(field.key) } + has_one.map { |association| association.report.headings } + has_many.map { |association| association.report.headings }).flatten
52
+ row.headings
41
53
  end
42
54
 
43
55
  class << self
44
- def t(key)
45
- ::MagicReport::Utils.t(name: name, key: key)
56
+ # Default i18n scope for locales
57
+ #
58
+ # en:
59
+ # magic_report:
60
+ #
61
+ def i18n_scope
62
+ :magic_report
46
63
  end
47
64
 
48
- def fields(*attrs)
49
- @fields ||= []
50
-
51
- Types::SymbolArray[attrs].each do |key|
52
- @fields << Configuration::Field.new(key: key)
53
- end
65
+ def i18n_key
66
+ name.underscore.to_sym
54
67
  end
55
68
 
56
- def field(*attrs)
57
- key, processor = attrs
69
+ def field(name, processor = nil)
70
+ reflection = Builder::Field.build(self, name, processor)
58
71
 
59
- @fields ||= []
60
- @fields << Configuration::Field.new(key: key, processor: processor)
72
+ Reflection.add_field(self, name, reflection)
61
73
  end
62
74
 
63
- def has_one(attribute, opts = {}, &block)
64
- @has_one ||= []
65
-
66
- coerced_attribute = Types::Coercible::Symbol[attribute]
75
+ def fields(*names)
76
+ names.each { |name| field(name) }
77
+ end
67
78
 
68
- klass = ::MagicReport::Utils.derive_class(opts, &block)
79
+ def has_one(name, **options, &extension)
80
+ reflection = Builder::HasOne.build(self, name, options, &extension)
69
81
 
70
- opts[:name] = ::MagicReport::Utils.underscore(opts[:name].to_s) if opts[:name]
82
+ Reflection.add_has_one(self, name, reflection)
83
+ end
71
84
 
72
- if (prefix = opts[:prefix])
73
- opts[:prefix] = new(name: ::MagicReport::Utils.underscore(name.to_s)).instance_exec(&prefix)
74
- end
85
+ def has_many(name, **options, &extension)
86
+ reflection = Builder::HasMany.build(self, name, options, &extension)
75
87
 
76
- @has_one << Configuration::HasOne.new(klass: klass, opts: opts, key: coerced_attribute)
88
+ Reflection.add_has_many(self, name, reflection)
77
89
  end
78
90
 
79
- def has_many(attribute, opts = {}, &block)
80
- @has_many ||= []
91
+ # Building empty row for current report
92
+ # This row doesn't include outer reports
93
+ def build_row(prefix = nil)
94
+ row = ::MagicReport::Report::Row.new
81
95
 
82
- coerced_attribute = Types::Coercible::Symbol[attribute]
83
-
84
- klass = ::MagicReport::Utils.derive_class(opts, &block)
96
+ _fields.each_value do |field|
97
+ row.register_column(field.name, I18n.t!("#{i18n_scope}.#{i18n_key}.#{field.name}".tr("/", ".")), prefix)
98
+ end
85
99
 
86
- opts[:name] = ::MagicReport::Utils.underscore(opts[:name].to_s) if opts[:name]
100
+ _has_one.each_value do |has_one|
101
+ row.add_nested_row(key: has_one.name, row: has_one.build_row)
102
+ end
87
103
 
88
- if (prefix = opts[:prefix])
89
- opts[:prefix] = new(name: ::MagicReport::Utils.underscore(name.to_s)).instance_exec(&prefix)
104
+ _has_many.each_value do |has_many|
105
+ row.add_nested_row(key: has_many.name, row: has_many.build_row)
90
106
  end
91
107
 
92
- @has_many << Configuration::HasMany.new(klass: klass, opts: opts, key: coerced_attribute)
108
+ row
93
109
  end
94
110
  end
95
-
96
- def resolve_path(key)
97
- nested_field ? "#{nested_field}.#{key}".to_sym : key
98
- end
99
-
100
- private
101
-
102
- def t(key)
103
- translated = ::MagicReport::Utils.t(name: name, key: key)
104
-
105
- prefix ? "#{prefix} #{translated}" : translated
106
- end
107
111
  end
108
112
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MagicReport
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/magic_report.rb CHANGED
@@ -3,6 +3,10 @@
3
3
  require "dry-types"
4
4
  require "i18n"
5
5
 
6
+ require "active_support"
7
+ require "active_support/core_ext/class/attribute"
8
+ require "active_support/inflector"
9
+
6
10
  require "magic_report/version"
7
11
 
8
12
  module MagicReport
@@ -14,12 +18,11 @@ module MagicReport
14
18
  SymbolArray = Array.of(Types::Coercible::Symbol)
15
19
  end
16
20
 
17
- require "magic_report/utils"
21
+ require "magic_report/report/builder/field"
22
+ require "magic_report/report/builder/has_one"
23
+ require "magic_report/report/builder/has_many"
18
24
 
19
- require "magic_report/report/class_helpers"
20
- require "magic_report/report/configuration"
21
- require "magic_report/report/process"
25
+ require "magic_report/report/reflection"
22
26
  require "magic_report/report/row"
23
- require "magic_report/report/csv"
24
27
  require "magic_report/report"
25
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magic-report
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Pankratev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-12 00:00:00.000000000 Z
11
+ date: 2022-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-types
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: i18n
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -47,12 +61,11 @@ files:
47
61
  - README.md
48
62
  - lib/magic_report.rb
49
63
  - lib/magic_report/report.rb
50
- - lib/magic_report/report/class_helpers.rb
51
- - lib/magic_report/report/configuration.rb
52
- - lib/magic_report/report/csv.rb
53
- - lib/magic_report/report/process.rb
64
+ - lib/magic_report/report/builder/field.rb
65
+ - lib/magic_report/report/builder/has_many.rb
66
+ - lib/magic_report/report/builder/has_one.rb
67
+ - lib/magic_report/report/reflection.rb
54
68
  - lib/magic_report/report/row.rb
55
- - lib/magic_report/utils.rb
56
69
  - lib/magic_report/version.rb
57
70
  homepage: https://github.com/thefaded/magic-report
58
71
  licenses:
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MagicReport
4
- class Report
5
- module ClassHelpers
6
- def name_from_class
7
- ::MagicReport::Utils.underscore(self.class.name)
8
- end
9
-
10
- def fields_from_class
11
- self.class.instance_variable_get(:@fields) || []
12
- end
13
-
14
- def has_one_from_class
15
- self.class.instance_variable_get(:@has_one) || []
16
- end
17
-
18
- def has_many_from_class
19
- self.class.instance_variable_get(:@has_many) || []
20
- end
21
- end
22
- end
23
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MagicReport
4
- class Report
5
- module Configuration
6
- class Field
7
- attr_reader :key, :processor
8
-
9
- def initialize(key:, processor: nil)
10
- @key = key
11
- @processor = processor
12
- end
13
-
14
- def process(entity)
15
- processor ? processor.call(entity) : entity.send(key)
16
- end
17
- end
18
-
19
- class HasOne
20
- attr_reader :klass, :opts, :prefix, :key
21
-
22
- def initialize(klass:, opts:, key:)
23
- @klass = klass
24
- @prefix = opts[:prefix]
25
- @key = key
26
- @opts = opts
27
- end
28
-
29
- def process_entity(entity)
30
- report.process(entity)
31
- end
32
-
33
- def report
34
- @report ||= init_report
35
- end
36
-
37
- private
38
-
39
- # { class: Exports::Supplier, prefix: lambda }
40
-
41
- def init_report
42
- klass.new(report_params)
43
- end
44
-
45
- def report_opts
46
- opts
47
- end
48
-
49
- def report_params
50
- opts.reject { |k, v| %i[class].include? k }.merge(nested_field: key)
51
- end
52
- end
53
-
54
- class HasMany < HasOne; end
55
- end
56
- end
57
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "csv"
4
-
5
- module MagicReport
6
- class Report
7
- class Csv
8
- attr_reader :report, :file, :csv
9
-
10
- def initialize(report)
11
- @report = report
12
- @file = Tempfile.new
13
- @csv = ::CSV.new(@file, write_headers: true)
14
- end
15
-
16
- def generate
17
- write_headers
18
-
19
- [report.result].flatten.each do |row|
20
- value = row.to_h
21
-
22
- if value.is_a?(Array)
23
- value.each { |nested_row| csv << nested_row.values }
24
- else
25
- csv << value.values
26
- end
27
- end
28
- end
29
-
30
- # Don't forget to unlink in production code
31
- def unlink
32
- file.close
33
- file.unlink
34
- end
35
-
36
- def io
37
- io = csv.to_io
38
- io.rewind
39
- io
40
- end
41
-
42
- private
43
-
44
- def write_headers
45
- csv << report.headings
46
- end
47
- end
48
- end
49
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MagicReport
4
- class Report
5
- class Process
6
- attr_reader :report
7
-
8
- def initialize(report)
9
- @report = report
10
- end
11
-
12
- def call(input)
13
- if input.is_a? Enumerable
14
- input.map do |entity|
15
- process_entity(entity)
16
- end
17
- else
18
- process_entity(input)
19
- end
20
- end
21
-
22
- private
23
-
24
- def process_entity(entity)
25
- row = Row.new
26
-
27
- process_fields(entity, row)
28
- process_has_one(entity, row)
29
- process_has_many(entity, row)
30
-
31
- row
32
- end
33
-
34
- def process_fields(entity, row)
35
- report.fields.each do |field|
36
- row.add(field: report.resolve_path(field.key), value: field.process(entity))
37
- end
38
- end
39
-
40
- def process_has_one(entity, row)
41
- report.has_one.each do |association|
42
- inner_row = association.process_entity(entity.send(association.key))
43
-
44
- row.add_inner_row(field: report.resolve_path(association.key), row: inner_row)
45
- end
46
- end
47
-
48
- def process_has_many(entity, row)
49
- report.has_many.each do |association|
50
- entity.send(association.key).each do |entity|
51
- nested_row = association.process_entity(entity)
52
-
53
- row.add_nested_row(field: report.resolve_path(association.key), row: nested_row)
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MagicReport
4
- module Utils
5
- def underscore(klass)
6
- return "ded" unless klass
7
-
8
- klass.gsub(/::/, "/")
9
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
10
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
11
- .tr("-", "_")
12
- .tr("/", ".")
13
- .downcase
14
- end
15
-
16
- def derive_class(opts, &block)
17
- if block
18
- raise "name option must be provided" unless opts[:name]
19
-
20
- cloned_klass = ::MagicReport::Report.clone
21
- cloned_klass.define_singleton_method(:name) { opts[:name] }
22
- cloned_klass.class_eval(&block)
23
- cloned_klass
24
- else
25
- opts[:class]
26
- end
27
- end
28
-
29
- # @param name is the report name
30
- # @key is a field
31
- def t(name:, key:)
32
- I18n.translate!("magic_report.#{name}.#{key}")
33
- end
34
-
35
- module_function :underscore
36
- module_function :derive_class
37
- module_function :t
38
- end
39
- end