ruby_report 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6fe86c76ce5314abc47fa276b0fa779c235a76d5cfb2ec9697eb40c69d1585b2
4
+ data.tar.gz: 0a89259818e654808d7ee0de1a579fd2b3c30e2a347b75a8a041b1a1f92beb8f
5
+ SHA512:
6
+ metadata.gz: 570979045b5f739cc43b845b312311955d517e19a988e1ac1f92ddc93fe91d75aab7400120b7d2143b6cd9e5af403ff37ebdb66df9fc97b636b2064b836cb0cb
7
+ data.tar.gz: a7025a83548a80f90169c56cf0b2a4d5a903ad6611e5adc68749708618171910222bf5a96b27fc76de5a530c3f04ed747799ed2102b710993fa7cb60af31b238
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.0.1 (2018-04-17)
2
+ - Initial pre-release version. ([@klondaiker][])
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) Alexandr Zavgorodnev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Ruby Report
2
+ A simple report generator
3
+
4
+ ## Installation
5
+ Add this line to your application's `Gemfile`:
6
+
7
+ ```ruby
8
+ gem "ruby_report"
9
+ gem "caxlsx" # optional: for generate xlsx
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ ```sh
15
+ bundle install
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Create class
21
+ ```ruby
22
+ class UserReport < RubyReport::Report
23
+ columns :name, :age, :role, :created_at
24
+ end
25
+ ```
26
+
27
+ Initialize object with data
28
+ ```ruby
29
+ # data is ActiveRecords or array of objects
30
+ report = UserReport.new(data: User.all)
31
+ ```
32
+
33
+ Generate report
34
+ ```ruby
35
+ # Hash
36
+ report.to_h # {header: ["Name", "Age", "Role"], rows: [["Sasha", 18, "Student"]]}
37
+
38
+ # CSV
39
+ report.to_csv # IOString
40
+
41
+ # XLSX
42
+ report.to_xlsx(worksheet_name: "Worksheet") # IOString
43
+ ```
44
+
45
+ ## Details
46
+ Get header
47
+ ```ruby
48
+ report.header # ["Name", "Age", "Role"]
49
+ ```
50
+
51
+ Default translates for header get from i18n
52
+ ```ruby
53
+ I18n.t("ruby_reports.#{report.class.name.underscore}.headers.#{key}")
54
+ ```
55
+
56
+ Determine custom header
57
+ ```ruby
58
+ UserReport.new(data: data, header_builder: ->(key, _report) { "Custom #{key}" })
59
+ ```
60
+
61
+ Get rows
62
+ ```ruby
63
+ report.rows # [["Sasha", 18, "Student"], ["Jack", 30, "Worker"]]
64
+ ```
65
+
66
+ Select columns
67
+ ```ruby
68
+ report = UserReport.new(data: data, columns: [:name, :age])
69
+ report.headers #["Name", "Age"]
70
+ report.rows #[["Sasha", 18], ["Jack", 30]]
71
+ ```
72
+
73
+ Custom row
74
+ ```ruby
75
+ UserReport.new(data: data, row_resolver: ->(row) { row.user })
76
+ ```
77
+
78
+ Custom row builder
79
+ ```ruby
80
+ UserReport.new(data: data, row_builder: ->(_row, _key, _report) { "" })
81
+ ```
82
+
83
+ Decorators
84
+ ```ruby
85
+ class UserDecorator < RubyReport::Decorator
86
+ def role
87
+ I18n.t("roles.#{object.role}")
88
+ end
89
+ end
90
+
91
+ class UserReport < RubyReport::Report
92
+ columns :name, :age, :role, decorators: [UserDecorator]
93
+ end
94
+ ```
95
+
96
+ Formatters
97
+
98
+ ```ruby
99
+ class TimeFormatter < RubyReport::Formatter
100
+ def format(value)
101
+ return value unless [::Time, ActiveSupport::TimeWithZone].include?(value.class)
102
+ value.utc.to_formatted_s(:report)
103
+ end
104
+ end
105
+
106
+ class UserReport < RubyReport::Report
107
+ columns :name, :age, :role, :created_at, formatters: [TimeFormatter]
108
+ end
109
+ ```
110
+
111
+ Decorator and Formatter with scope
112
+ ```ruby
113
+ class UserDecorator < RubyReport::Decorator
114
+ def role
115
+ I18n.t("roles.#{object.role}", account_name: scope[:account].name)
116
+ end
117
+ end
118
+
119
+ report = UserReport.new(data: users, scope: {account: account})
120
+ ```
121
+
122
+ Append/prepend other reports
123
+
124
+ ```ruby
125
+ class AccountReport < RubyReport::Report
126
+ columns :name
127
+ end
128
+
129
+ class AddressReport < RubyReport::Report
130
+ columns :street
131
+ end
132
+
133
+ user_report.prepend_report(account_report)
134
+ user_report.add_report(address_report)
135
+ ```
136
+
137
+ XLSX with any worksheets
138
+ ```ruby
139
+ require "ruby_report/generator/xlsx"
140
+
141
+ generator = RubyReport::Generator::Xlsx.new
142
+ generator.add_report(report, worksheet_name: "Worksheet 1")
143
+ generator.add_report(other_report, worksheet_name: "Worksheet 2")
144
+ generator.generate # IOString
145
+ ```
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ module Builder
5
+ module DefaultHeaderBuilder
6
+ def self.call(key, report)
7
+ if defined?(::I18n)
8
+ ::I18n.t("ruby_report.#{underscore(report.class.name.to_s)}.headers.#{key}")
9
+ else
10
+ key.to_s
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def self.underscore(name)
17
+ word = name.gsub("::", "/")
18
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
19
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
20
+ word.downcase!
21
+ word
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ module Builder
5
+ module DefaultRowBuilder
6
+ def self.call(row, key, _report)
7
+ row.public_send(key)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ class Decorator < SimpleDelegator
5
+ attr_reader :scope
6
+
7
+ def initialize(obj, scope = {})
8
+ super(obj)
9
+ @scope = scope
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ class Formatter < SimpleDelegator
5
+ attr_reader :scope
6
+
7
+ def initialize(obj, scope = {})
8
+ super(obj)
9
+ @scope = scope
10
+ end
11
+
12
+ def method_missing(method, *args, &block)
13
+ format(super)
14
+ end
15
+
16
+ def respond_to_missing?(method, include_private)
17
+ super
18
+ end
19
+
20
+ def format(value)
21
+ value
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ module Generator
5
+ class Base
6
+ def add_report(report, **opts)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def generate
11
+ raise NotImplementedErro
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module RubyReport
6
+ module Generator
7
+ class Csv < Base
8
+ def add_report(report, **_opts)
9
+ @report = report
10
+ end
11
+
12
+ def generate
13
+ temp_file = ::Tempfile.new
14
+
15
+ ::CSV.open(temp_file.path, "wb") do |csv|
16
+ csv << report.header
17
+
18
+ report.each_row do |row|
19
+ csv << row
20
+ end
21
+ end
22
+
23
+ temp_file
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :report
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ module Generator
5
+ class Hash < Base
6
+ def add_report(report, **_opts)
7
+ @report = report
8
+ end
9
+
10
+ def generate
11
+ {header: report.header, rows: report.rows}
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :report
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "caxlsx"
4
+
5
+ module RubyReport
6
+ module Generator
7
+ class Xlsx < Base
8
+ WORKSHEET_LETTERS_COUNT = 25
9
+ ESCAPE_REGEXP = /["~#%&:;<>!=',@{|}\/?*()+\[\]$]/
10
+ XLSX_XSS_SYMBOLS = %w[+ - = @ {=].freeze
11
+
12
+ class Worksheet
13
+ SIZES = {header_height: 40, item_height: 20}.freeze
14
+ ALIGNMENT = {vertical: :center, horizontal: :left, wrap_text: true}.freeze
15
+ COLORS = {tb: "0a9700", white: "ff"}.freeze
16
+
17
+ def initialize(workbook, name)
18
+ @workbook = workbook
19
+ @worksheet = @workbook.add_worksheet(name: name)
20
+ @styles = ::OpenStruct.new(
21
+ item: @workbook.styles.add_style(alignment: ALIGNMENT),
22
+ header: @workbook.styles.add_style(
23
+ bg_color: COLORS.fetch(:tb),
24
+ fg_color: COLORS.fetch(:white),
25
+ alignment: ALIGNMENT
26
+ )
27
+ )
28
+ end
29
+
30
+ def add_header(data)
31
+ worksheet.add_row(
32
+ data, style: styles.header, height: SIZES.fetch(:header_height)
33
+ )
34
+ end
35
+
36
+ def add_row(data)
37
+ worksheet.add_row(
38
+ data, style: styles.item, height: SIZES.fetch(:item_height)
39
+ )
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :workbook, :worksheet, :styles
45
+ end
46
+
47
+ def initialize
48
+ @package = ::Axlsx::Package.new.tap { |package| package.use_shared_strings = true }
49
+ @workbook = package.workbook
50
+ @reports = []
51
+ end
52
+
53
+ def add_report(report, worksheet_name:)
54
+ reports << {
55
+ report: report,
56
+ worksheet: Worksheet.new(workbook, sanitize_worksheet_name(worksheet_name)),
57
+ }
58
+ end
59
+
60
+ def generate
61
+ reports.each do |report|
62
+ worksheet = report[:worksheet]
63
+ report = report[:report]
64
+
65
+ worksheet.add_header(report.header)
66
+
67
+ report.each_row do |row|
68
+ next if row.empty?
69
+
70
+ worksheet.add_row(sanitize_row(row))
71
+ end
72
+ end
73
+
74
+ package.to_stream
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :package, :workbook, :reports
80
+
81
+ def sanitize_row(row)
82
+ row.map do |el|
83
+ el.is_a?(String) && el.start_with?(*XLSX_XSS_SYMBOLS) ? "'#{el}" : el
84
+ end
85
+ end
86
+
87
+ def sanitize_worksheet_name(name)
88
+ truncate(name.gsub(ESCAPE_REGEXP, ""), WORKSHEET_LETTERS_COUNT)
89
+ end
90
+
91
+ def truncate(string, length)
92
+ string.length > length ? string[0...length] : string
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ class Report
5
+ class << self
6
+ attr_reader :columns_set, :decorators, :formatters
7
+
8
+ def columns(*keys, decorators: ::RubyReport::Decorator, formatters: ::RubyReport::Formatter)
9
+ @columns_set = Set.new(keys)
10
+ @decorators = decorators
11
+ @formatters = formatters
12
+ end
13
+ end
14
+
15
+ def initialize(
16
+ data:, scope: nil, columns: nil,
17
+ header_builder: ::RubyReport::Builder::DefaultHeaderBuilder,
18
+ row_builder: ::RubyReport::Builder::DefaultRowBuilder,
19
+ row_resolver: ->(row) { row }
20
+ )
21
+ @data = data
22
+ @scope = scope
23
+ @columns =
24
+ if columns.nil?
25
+ self.class.columns_set || raise("Columns not defined")
26
+ else
27
+ Set.new(columns)
28
+ end
29
+ @header_builder = header_builder
30
+ @row_builder = row_builder
31
+ @row_resolver = row_resolver
32
+ @reports = [self]
33
+ end
34
+
35
+ def add_report(report)
36
+ reports << report
37
+ end
38
+
39
+ def prepend_report(report)
40
+ reports.unshift(report)
41
+ end
42
+
43
+ def header
44
+ @header ||= reports.flat_map do |report|
45
+ if report == self
46
+ report.build_header
47
+ else
48
+ report.header
49
+ end
50
+ end
51
+ end
52
+
53
+ def rows
54
+ @rows ||= each_row.to_a
55
+ end
56
+
57
+ def each_row
58
+ return enum_for(:each_row) unless block_given?
59
+
60
+ method =
61
+ if data.respond_to?(:find_each)
62
+ :find_each
63
+ else
64
+ :each
65
+ end
66
+
67
+ data.public_send(method) do |row|
68
+ yield collect_row(row)
69
+ end
70
+ end
71
+
72
+ def to_with(generator, **opts)
73
+ report = generator.new
74
+ report.add_report(self, **opts)
75
+ report.generate
76
+ end
77
+
78
+ [:hash, :csv, :xlsx].each do |type|
79
+ define_method("to_#{type}") do |**opts|
80
+ require "ruby_report/generator/#{type}"
81
+
82
+ to_with(
83
+ Object.const_get("::RubyReport::Generator::#{type.to_s.capitalize}"),
84
+ **opts
85
+ )
86
+ end
87
+ end
88
+
89
+ alias_method :to_h, :to_hash
90
+
91
+ def build_header
92
+ columns.map do |key|
93
+ header_builder.call(key, self)
94
+ end
95
+ end
96
+
97
+ def build_row(row)
98
+ current_row = row_resolver.call(row)
99
+
100
+ return [] unless current_row
101
+
102
+ Array(decorators).each { |decorator| current_row = decorator.new(current_row, scope) }
103
+ Array(formatters).each { |formatter| current_row = formatter.new(current_row, scope) }
104
+
105
+ columns.map do |key|
106
+ row_builder.call(current_row, key, self)
107
+ end
108
+ end
109
+
110
+ def collect_row(row)
111
+ result_row = []
112
+
113
+ reports.each do |report|
114
+ result_row +=
115
+ if report == self
116
+ report.build_row(row)
117
+ else
118
+ report.collect_row(row)
119
+ end
120
+ end
121
+
122
+ result_row
123
+ end
124
+
125
+ private
126
+
127
+ attr_reader :data, :scope, :columns, :header_builder, :row_builder, :row_resolver, :reports
128
+
129
+ def decorators
130
+ self.class.decorators
131
+ end
132
+
133
+ def formatters
134
+ self.class.formatters
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReport
4
+ VERSION = "0.0.0"
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_report/builder/default_header_builder"
4
+ require "ruby_report/builder/default_row_builder"
5
+
6
+ require "ruby_report/generator/base"
7
+
8
+ require "delegate"
9
+ require "ruby_report/decorator"
10
+ require "ruby_report/formatter"
11
+
12
+ require "ruby_report/report"
13
+
14
+ module RubyReport
15
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_report
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexandr Zavgorodnev
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-04-20 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.16'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: pry
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.14.2
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.14.2
68
+ - !ruby/object:Gem::Dependency
69
+ name: caxlsx
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 4.0.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 4.0.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: creek
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.6.3
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 2.6.3
96
+ description: A simple report generator
97
+ email: klondaiker@bk.ru
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - CHANGELOG.md
103
+ - LICENSE.txt
104
+ - README.md
105
+ - lib/ruby_report.rb
106
+ - lib/ruby_report/builder/default_header_builder.rb
107
+ - lib/ruby_report/builder/default_row_builder.rb
108
+ - lib/ruby_report/decorator.rb
109
+ - lib/ruby_report/formatter.rb
110
+ - lib/ruby_report/generator/base.rb
111
+ - lib/ruby_report/generator/csv.rb
112
+ - lib/ruby_report/generator/hash.rb
113
+ - lib/ruby_report/generator/xlsx.rb
114
+ - lib/ruby_report/report.rb
115
+ - lib/ruby_report/version.rb
116
+ homepage: https://github/klondaiker/ruby_report
117
+ licenses:
118
+ - MIT
119
+ metadata: {}
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 2.7.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.6.2
135
+ specification_version: 4
136
+ summary: 0.0.0
137
+ test_files: []