reportinator 0.1.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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