magic-report 0.6.0 → 0.7.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: 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