reportinator 0.1.1 → 0.3.2

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -7
  3. data/Gemfile.lock +11 -1
  4. data/README.md +201 -291
  5. data/app/reports/example.report.json +7 -3
  6. data/app/reports/multiplication.report.json +1 -1
  7. data/app/reports/multiplication_v2.report.json +15 -0
  8. data/data/schema/report_schema.json +76 -0
  9. data/docs/0_first_report.md +267 -0
  10. data/lib/reportinator/base.rb +2 -1
  11. data/lib/reportinator/config.rb +30 -0
  12. data/lib/reportinator/function.rb +33 -0
  13. data/lib/reportinator/functions/array/flatten.rb +12 -0
  14. data/lib/reportinator/functions/array/helper.rb +77 -0
  15. data/lib/reportinator/functions/array/join.rb +11 -0
  16. data/lib/reportinator/functions/array/method.rb +9 -0
  17. data/lib/reportinator/functions/array/range.rb +11 -0
  18. data/lib/reportinator/functions/array/snippet.rb +30 -0
  19. data/lib/reportinator/functions/array/string.rb +10 -0
  20. data/lib/reportinator/functions/array.rb +43 -0
  21. data/lib/reportinator/functions/string/addition.rb +11 -0
  22. data/lib/reportinator/functions/string/constant.rb +9 -0
  23. data/lib/reportinator/functions/string/date.rb +9 -0
  24. data/lib/reportinator/functions/string/join.rb +10 -0
  25. data/lib/reportinator/functions/string/logical.rb +14 -0
  26. data/lib/reportinator/functions/string/number.rb +33 -0
  27. data/lib/reportinator/functions/string/range.rb +14 -0
  28. data/lib/reportinator/functions/string/symbol.rb +9 -0
  29. data/lib/reportinator/functions/string/variable.rb +12 -0
  30. data/lib/reportinator/functions/string.rb +29 -0
  31. data/lib/reportinator/helpers.rb +29 -0
  32. data/lib/reportinator/parser.rb +25 -0
  33. data/lib/reportinator/parsers/method.rb +8 -3
  34. data/lib/reportinator/parsers/report.rb +47 -0
  35. data/lib/reportinator/parsers/value.rb +15 -112
  36. data/lib/reportinator/report/column.rb +25 -0
  37. data/lib/reportinator/report/loader.rb +71 -0
  38. data/lib/reportinator/report/report.rb +33 -0
  39. data/lib/reportinator/report/row.rb +42 -0
  40. data/lib/reportinator/report/template.rb +108 -0
  41. data/lib/reportinator/{report.rb → report_type.rb} +4 -1
  42. data/lib/reportinator/types/model.rb +15 -7
  43. data/lib/reportinator/types/preset.rb +23 -2
  44. data/lib/reportinator/version.rb +1 -1
  45. data/lib/reportinator.rb +23 -9
  46. metadata +48 -5
  47. data/lib/reportinator/loader.rb +0 -112
@@ -0,0 +1,14 @@
1
+ module Reportinator
2
+ class LogicalStringFunction < StringFunction
3
+ PREFIXES = ["@true", "@false", "@nil", "@null"]
4
+
5
+ def output
6
+ case prefix
7
+ when "@true" then true
8
+ when "@false" then false
9
+ when "@nil", "@null" then nil
10
+ else element
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ module Reportinator
2
+ class NumberStringFunction < StringFunction
3
+ PREFIXES = ["!n", "!nf", "!ni"]
4
+
5
+ attr_writer :parsed_body
6
+
7
+ def output
8
+ return parse_float if prefix == "!nf"
9
+ return parse_integer if prefix == "!ni"
10
+ parse_number
11
+ end
12
+
13
+ def parsed_body
14
+ to_parse = body
15
+ to_parse.strip! if to_parse.instance_of? String
16
+ @parsed_body ||= parse_value(body).to_s
17
+ end
18
+
19
+ def parse_float
20
+ parsed_body.to_f
21
+ end
22
+
23
+ def parse_integer
24
+ parsed_body.to_i
25
+ end
26
+
27
+ def parse_number
28
+ float = (parsed_body =~ /\d\.\d/)
29
+ return parse_float if float.present?
30
+ parse_integer
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ module Reportinator
2
+ class RangeStringFunction < StringFunction
3
+ PREFIXES = ["!r", "!rd", "!rn"]
4
+
5
+ def output
6
+ values = body.split(",").map { |value| parse_value(value.strip) }
7
+ case prefix
8
+ when "!rn" then values.map! { |subvalue| NumberStringFunction.parse("!n #{subvalue}") }
9
+ when "!rd" then values.map! { |subvalue| DateStringFunction.parse("!d #{subvalue}") }
10
+ end
11
+ Range.new(*values)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module Reportinator
2
+ class SymbolStringFunction < StringFunction
3
+ PREFIXES = [":"]
4
+
5
+ def output
6
+ body.to_sym
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Reportinator
2
+ class VariableStringFunction < StringFunction
3
+ PREFIXES = ["$"]
4
+
5
+ def output
6
+ variables = metadata[:variables]
7
+ variable = body.to_sym
8
+ return element unless variables.present? && variables.include?(variable)
9
+ variables[variable]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module Reportinator
2
+ class StringFunction < Function
3
+ PREFIXES = []
4
+
5
+ attribute :prefix
6
+ attribute :body
7
+
8
+ def self.accepts? input
9
+ return false unless input.instance_of? String
10
+ return false if self::PREFIXES.empty?
11
+ self::PREFIXES.each do |prefix|
12
+ return true if input.start_with? prefix
13
+ end
14
+ false
15
+ end
16
+
17
+ def get
18
+ raise "Function missing output!" unless respond_to? :output
19
+ set_attributes
20
+ output
21
+ end
22
+
23
+ def set_attributes
24
+ prefix = get_prefix(element)
25
+ self.prefix = prefix
26
+ self.body = element.sub(prefix, "")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module Reportinator
2
+ module Helpers
3
+ def merge_hash(target, source)
4
+ target = target.present? ? target : {}
5
+ source = source.present? ? source : {}
6
+ merge_hash!(target, source)
7
+ end
8
+
9
+ def merge_hash!(target, source)
10
+ raise "Target: #{target} is not a hash" unless target.instance_of?(Hash)
11
+ raise "Source: #{source} is not a hash" unless source.instance_of?(Hash)
12
+ target.merge(source) do |key, old_value, new_value|
13
+ if old_value.instance_of?(Hash) && new_value.instance_of?(Hash)
14
+ merge_hash!(old_value, new_value)
15
+ elsif new_value.present?
16
+ new_value
17
+ else
18
+ old_value
19
+ end
20
+ end
21
+ end
22
+
23
+ def symbolize_attributes(target)
24
+ raise "Missing attributes" unless target.respond_to? :attributes
25
+ raise "Invalid attributes" unless target.attributes.instance_of? Hash
26
+ target.attributes.transform_keys { |key| key.to_sym }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Reportinator
2
+ class Parser < Base
3
+ cattr_writer :prefix_list
4
+
5
+ def self.get_prefix_list
6
+ config.configured_functions.map { |function| function::PREFIXES }.flatten
7
+ end
8
+
9
+ def self.prefix_list
10
+ @prefix_list ||= get_prefix_list
11
+ end
12
+
13
+ def prefix_list
14
+ self.class.prefix_list
15
+ end
16
+
17
+ def escape_value(value)
18
+ return value unless value.is_a? String
19
+ prefix_list.each do |escape|
20
+ return value.prepend("?/") if value.strip.start_with?(escape)
21
+ end
22
+ value
23
+ end
24
+ end
25
+ end
@@ -1,14 +1,20 @@
1
1
  module Reportinator
2
- class MethodParser < Base
2
+ class MethodParser < Parser
3
3
  attribute :target
4
4
  attribute :method
5
5
 
6
6
  def self.parse(target, method)
7
7
  new(target: target, method: method).output
8
+ rescue => e
9
+ logger.error "[ERROR] #{e.class}: #{e}"
10
+ "Method Error"
8
11
  end
9
12
 
10
13
  def output
11
- return send_value(target, method) if method_class == Symbol
14
+ if method_class == Symbol
15
+ value = send_value(target, method)
16
+ return escape_value(value)
17
+ end
12
18
  return parse_array_method if method_class == Array
13
19
  return parse_hash_method if method_class == Hash
14
20
  nil
@@ -24,7 +30,6 @@ module Reportinator
24
30
  output = target
25
31
  method.each do |m|
26
32
  value = parse_method(output, m)
27
- next unless value.present?
28
33
  valid = true
29
34
  output = value
30
35
  end
@@ -0,0 +1,47 @@
1
+ module Reportinator
2
+ class ReportParser < Parser
3
+ attribute :element
4
+ attribute :data
5
+
6
+ def self.parse(element, data = nil)
7
+ set_data = (data.present? ? data : element)
8
+ new(element: element, data: set_data).output
9
+ end
10
+
11
+ def output
12
+ return parse_array if element_class == Array
13
+ return parse_hash if element_class == Hash
14
+ return parse_string if element_class == String
15
+ element
16
+ end
17
+
18
+ def parse_array
19
+ raise "Not an array" unless element_class == Array
20
+ element.map { |value| parse_value(value) }
21
+ end
22
+
23
+ def parse_hash
24
+ raise "Not a hash" unless element_class == Hash
25
+ element.transform_values { |value| parse_value(value) }
26
+ end
27
+
28
+ def parse_string
29
+ raise "Not a string" unless element_class == String
30
+ return element unless element.strip.start_with?("?")
31
+ return element.sub("?/", "") if element.strip.start_with?("?/")
32
+ # return parse_row_total if element.start_with?("?tr")
33
+ # return parse_column_total if element.start_with?("?tc")
34
+ element
35
+ end
36
+
37
+ def element_class
38
+ element.class
39
+ end
40
+
41
+ private
42
+
43
+ def parse_value(value)
44
+ self.class.parse(value, data)
45
+ end
46
+ end
47
+ end
@@ -1,144 +1,47 @@
1
1
  module Reportinator
2
- class ValueParser < Base
3
- VALUE_FUNCTIONS = %i[a d n rn rd r]
4
-
2
+ class ValueParser < Parser
5
3
  attribute :element
6
- attribute :variables, default: {}
4
+ attribute :metadata, default: {}
7
5
 
8
- def self.parse(element, variables = {})
9
- variables = variables.present? ? variables : {}
10
- new(element: element, variables: variables).output
6
+ def self.parse(element, metadata = {})
7
+ metadata = metadata.present? ? metadata : {}
8
+ new(element: element.dup, metadata: metadata).output
9
+ rescue => e
10
+ logger.error "[ERROR] #{e.class}: #{e}"
11
+ "Parsing Error"
11
12
  end
12
13
 
13
- def self.parse_and_execute(target, values, variables = {})
14
- parsed_target = target
15
- if target.instance_of?(String)
16
- parsed_target = new(element: target, variables: variables).parse_string
17
- end
18
- parsed_values = parse(values, variables)
14
+ def self.parse_and_execute(target, values, metadata = {})
15
+ parsed_target = parse(target, metadata)
16
+ parsed_values = parse(values, metadata)
19
17
  MethodParser.parse(parsed_target, parsed_values)
20
18
  end
21
19
 
22
20
  def output
21
+ config.configured_functions.each do |function|
22
+ return function.parse(element, metadata) if function.accepts? element
23
+ end
23
24
  return parse_array if element_class == Array
24
25
  return parse_hash if element_class == Hash
25
- return parse_string if element_class == String
26
26
  element
27
27
  end
28
28
 
29
29
  def parse_array
30
30
  raise "Not an array" unless element_class == Array
31
- front = element[0]
32
- return parse_executed_array if front.instance_of?(String) && front.start_with?("#")
33
31
  element.map { |value| parse_value(value) }
34
32
  end
35
33
 
36
- def parse_executed_array
37
- raise "Not an executable array" unless element[0].start_with?("#")
38
- values = element
39
- target = values.delete_at(0).sub("#", "")
40
- parse_and_execute_value(target, values)
41
- end
42
-
43
34
  def parse_hash
44
35
  raise "Not a hash" unless element_class == Hash
45
36
  element.transform_values { |value| parse_value(value) }
46
37
  end
47
38
 
48
- def parse_string
49
- raise "Not a string" unless element_class == String
50
- return element.sub(":", "").to_sym if element.start_with?(":")
51
- return element.sub("&", "").constantize if element.start_with?("&")
52
- return parse_variable(element) if element.start_with?("$")
53
- return parse_function(element) if element.start_with?("!")
54
- element
55
- end
56
-
57
39
  def element_class
58
40
  element.class
59
41
  end
60
42
 
61
- private
62
-
63
- def parse_variable(value)
64
- key = value.sub("$", "").to_sym
65
- variables[key]
66
- end
67
-
68
- def parse_function(value)
69
- input = value.strip
70
- function = function_type(input)
71
- return value unless function.present?
72
- input.sub!(function_prefix(function), "")
73
- output = run_function(function, input)
74
- output.nil? ? value : output
75
- end
76
-
77
- def run_function(function, input)
78
- case function
79
- when :a then addition_function(input)
80
- when :d then date_function(input)
81
- when :n then number_function(input)
82
- when :r then range_function(input)
83
- when :rn then range_function(input, :number)
84
- when :rd then range_function(input, :date)
85
- end
86
- end
87
-
88
- def function_type(value)
89
- VALUE_FUNCTIONS.each do |function|
90
- return function if value.start_with?(function_prefix(function))
91
- end
92
- false
93
- end
94
-
95
- def function_prefix(function)
96
- "!#{function}"
97
- end
98
-
99
- def addition_function(value)
100
- values = parse_function_array(value)
101
- values.map! { |value| number_function(value) }
102
- values.sum(0)
103
- rescue
104
- 0
105
- end
106
-
107
- def date_function(value)
108
- Time.parse(value)
109
- rescue
110
- Time.now
111
- end
112
-
113
- def number_function(value)
114
- float = (value =~ /\d\.\d/)
115
- return value.to_f if float.present?
116
- value.to_i
117
- rescue
118
- 0
119
- end
120
-
121
- def range_function(value, type = :any)
122
- values = parse_function_array(value)
123
- case type
124
- when :number then values.map! { |subvalue| number_function(subvalue) }
125
- when :date then values.map! { |subvalue| date_function(subvalue) }
126
- end
127
- Range.new(*values)
128
- rescue
129
- Range(0..1)
130
- end
131
-
132
- def parse_function_array(value)
133
- value.split(",").map { |value| parse_value(value.strip) }
134
- end
135
-
136
43
  def parse_value(value)
137
- self.class.parse(value, variables)
138
- end
139
-
140
- def parse_and_execute_value(target, value)
141
- self.class.parse_and_execute(target, value, variables)
44
+ self.class.parse(value, metadata)
142
45
  end
143
46
  end
144
47
  end
@@ -0,0 +1,25 @@
1
+ module Reportinator
2
+ class Column < Base
3
+ OUTPUT_TYPES = {
4
+ numeric: [Numeric],
5
+ date: [Date, Time],
6
+ string: [String],
7
+ hash: [Hash],
8
+ array: [Array]
9
+ }
10
+
11
+ attribute :data
12
+ attr_writer :output
13
+
14
+ def output
15
+ @output ||= ReportParser.parse(data)
16
+ end
17
+
18
+ OUTPUT_TYPES.each do |type, classes|
19
+ define_method(:"#{type}?") {
20
+ classes.each { |c| return true if output.is_a? c }
21
+ false
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ module Reportinator
2
+ class ReportLoader < Base
3
+ attribute :template
4
+ attribute :metadata
5
+
6
+ def self.load(template, metadata = {})
7
+ loader = new(metadata: metadata)
8
+ loader.template = Template.load(template: template, metadata: metadata)
9
+ loader
10
+ end
11
+
12
+ def get_metadata
13
+ report_metadata = {}
14
+ template.parse(metadata) do |data, old_meta, new_meta|
15
+ meta = ValueParser.parse(new_meta, metadata)
16
+ report_metadata = merge_hash(meta, report_metadata) if meta.present?
17
+ end
18
+ report_metadata
19
+ end
20
+
21
+ def report
22
+ report = Report.new
23
+ reports = template.parse(metadata) do |data, old_meta, new_meta|
24
+ parse_metadata(data, old_meta, new_meta)
25
+ end
26
+ reports.compact.each do |report_template|
27
+ output = report_template.data
28
+ report.insert(output)
29
+ end
30
+ report
31
+ end
32
+
33
+ def parse_metadata(data, old_meta, new_meta)
34
+ meta = ValueParser.parse(old_meta, metadata)
35
+ if new_meta.instance_of? Hash
36
+ unparsed_meta = new_meta.select { |key| config.configured_metadata.include? key }
37
+ meta_to_parse = new_meta.reject { |key| config.configured_metadata.include? key }
38
+ parsing_meta = merge_hash(meta, unparsed_meta)
39
+ parsed_meta = ValueParser.parse(meta_to_parse, parsing_meta)
40
+ remerged_meta = merge_hash(parsed_meta, unparsed_meta)
41
+ else
42
+ remerged_meta = {}
43
+ end
44
+ report_meta = merge_hash(remerged_meta, meta)
45
+ report_from_data(data, report_meta)
46
+ end
47
+
48
+ private
49
+
50
+ def report_from_data(data, meta)
51
+ report_type = report_class_from_data(data, meta)
52
+ return nil unless report_type.present?
53
+ return report_type.new(ValueParser.parse(data[:params], meta)) if report_type::PARSE_PARAMS
54
+ report = report_type.new(data[:params])
55
+ report.metadata = meta
56
+ report
57
+ end
58
+
59
+ def report_class_from_data(data, meta)
60
+ type = ValueParser.parse(data[:type], meta)
61
+ return false unless type.present?
62
+ report_class_from_type(type)
63
+ end
64
+
65
+ def report_class_from_type(type)
66
+ types = config.configured_types
67
+ raise "Invalid type: #{type}" unless types.include? type
68
+ types[type].constantize
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,33 @@
1
+ module Reportinator
2
+ class Report < Base
3
+ attr_writer :rows
4
+
5
+ def rows
6
+ @rows ||= []
7
+ end
8
+
9
+ def insert(row, position = :last)
10
+ return insert_row(row, position) if row.instance_of? Row
11
+ raise "Invalid row data: #{row}" unless row.instance_of? Array
12
+ row.each { |r| insert_row(r) }
13
+ end
14
+
15
+ def insert_row(row, position = :last)
16
+ raise "Not a row" unless row.instance_of? Row
17
+ return rows.append(row) if position == :last
18
+ return rows.prepend(row) if position == :first
19
+ return rows.insert(position, row) if position.is_a? Numeric
20
+ raise "Invalid Position!"
21
+ end
22
+
23
+ def output
24
+ rows.map { |r| r.output }
25
+ end
26
+
27
+ def to_csv
28
+ CSV.generate do |csv|
29
+ output.each { |row| csv << row }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ module Reportinator
2
+ class Row < Base
3
+ attr_writer :columns
4
+
5
+ def columns
6
+ @columns ||= []
7
+ end
8
+
9
+ def self.create(input)
10
+ row = new
11
+ if input.instance_of? Array
12
+ input.each { |value| row.insert value }
13
+ else
14
+ row.insert(input)
15
+ end
16
+ row
17
+ end
18
+
19
+ def insert(data, position = :last)
20
+ column = create_column(data)
21
+ return columns.prepend(column) if position == :first
22
+ return columns.insert(position, column) if position.is_a? Numeric
23
+ return columns.append(column) if position == :last
24
+ raise "Invalid Position!"
25
+ end
26
+
27
+ def total
28
+ numeric_columns = columns.select { |c| c.numeric? }
29
+ numeric_columns.sum { |c| c.output }
30
+ end
31
+
32
+ def output
33
+ columns.map { |c| c.output }
34
+ end
35
+
36
+ private
37
+
38
+ def create_column(data)
39
+ Column.new(data: data)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,108 @@
1
+ module Reportinator
2
+ class Template < Base
3
+ attr_accessor :children
4
+ attribute :type
5
+ attribute :template
6
+ attribute :params
7
+ attribute :metadata
8
+
9
+ def self.load(params = {})
10
+ template = new(params)
11
+ template.register
12
+ end
13
+
14
+ def register
15
+ return load_template if template.present?
16
+ self
17
+ end
18
+
19
+ def parse(meta = {}, data = {})
20
+ output = []
21
+ new_meta = metadata
22
+ combine_meta = merge_hash(meta, new_meta)
23
+ new_data = attributes.transform_keys { |key| key.to_sym }
24
+ combine_data = merge_hash(new_data, data)
25
+ if children.present? && children.respond_to?(:to_ary)
26
+ children.each do |child|
27
+ output += child.parse(combine_meta, combine_data) do |combine_data, meta, new_meta|
28
+ yield(combine_data, meta, new_meta)
29
+ end
30
+ end
31
+ else
32
+ output << yield(combine_data, meta, new_meta)
33
+ end
34
+ output
35
+ end
36
+
37
+ private
38
+
39
+ def load_template
40
+ template_data = filter_template
41
+ data = if template_data.respond_to? :to_ary
42
+ template_data.map { |template| self.class.load(template) }
43
+ else
44
+ self.class.load(template_data)
45
+ end
46
+ self.children ||= []
47
+ if data.respond_to? :to_ary
48
+ self.children += data
49
+ else
50
+ self.children << data
51
+ end
52
+ self
53
+ end
54
+
55
+ def filter_template
56
+ template_data = parse_template
57
+ if template_data.respond_to? :to_ary
58
+ template_data.map { |template| filter_params(template) }
59
+ else
60
+ filter_params(template_data)
61
+ end
62
+ end
63
+
64
+ def find_template
65
+ raise "Template isn't a string" unless template.instance_of? String
66
+ suffixes = config.configured_suffixes
67
+ directories = config.configured_directories
68
+ template_files = suffixes.map { |suffix| (suffix.present? ? "#{template}.#{suffix}" : template) }
69
+ template_paths = directories.map { |dir| template_files.map { |file| "#{dir}/#{file}" } }
70
+ template_paths.flatten!
71
+ template_paths.each do |path|
72
+ return path if File.exist? path
73
+ end
74
+ raise "Missing template: #{template}"
75
+ end
76
+
77
+ def validate_template(json)
78
+ return true if Reportinator.schema.valid?(json)
79
+ raise "Template doesn't match schema: #{Reportinator.schema.validate(json).to_a}"
80
+ end
81
+
82
+ def parse_template
83
+ file = read_template
84
+ begin
85
+ plain_json = JSON.parse(file)
86
+ symbolised_json = JSON.parse(file, symbolize_names: true)
87
+ rescue
88
+ raise "Error parsing template file: #{file}"
89
+ end
90
+ validate_template(plain_json)
91
+ symbolised_json
92
+ end
93
+
94
+ def read_template
95
+ file = find_template
96
+ File.read(file)
97
+ end
98
+
99
+ def filter_params(params)
100
+ filtered_params = params.select { |param| attribute_names.include? param.to_s }
101
+ if params.size > filtered_params.size
102
+ invalid_params = (params.keys - filtered_params.keys).map { |key| key.to_s }
103
+ logger.warn "Invalid attributes found: #{invalid_params} Valid attributes are: #{attribute_names}"
104
+ end
105
+ filtered_params
106
+ end
107
+ end
108
+ end