reportinator 0.1.0 → 0.2.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.
@@ -0,0 +1,267 @@
1
+ # Creating my First Report
2
+ Let's start by considering what we want our output to be.
3
+ Say we want a multiplication table, like such:
4
+ | nx1 | nx2 | nx3 | nx4 | nx5 |
5
+ |-----|-----|-----|-----|-----|
6
+ | 1 | 2 | 3 | 4 | 5 |
7
+ | 2 | 4 | 6 | 8 | 10 |
8
+ | 3 | 6 | 9 | 12 | 15 |
9
+ | 4 | 8 | 12 | 16 | 20 |
10
+ | 5 | 10 | 15 | 20 | 25 |
11
+
12
+ Make a new file in your `app/reports` directory.
13
+ Name it `multiplication.report.json`
14
+
15
+ Set it's type to ":preset". The ":preset" type takes one parameter, "data",
16
+ and returns any values passed inside it, but with their values parsed.
17
+ We put a colon in front of the word "preset", such that the value parser
18
+ knows to turn it into a symbol.
19
+
20
+ ```
21
+ {
22
+ "type": ":preset"
23
+ }
24
+ ```
25
+
26
+ Next we need to give it parameters to be passed into the report.
27
+ ":preset" only accepts the "data" parameter.
28
+ Add "data" to a "params" object, and set it to be an empty array.
29
+
30
+ ```
31
+ {
32
+ "type": ":preset",
33
+ "params": {
34
+ "data": []
35
+ }
36
+ }
37
+ ```
38
+
39
+ You can now try running this report:
40
+
41
+ ```
42
+ > Reportinator.report("multiplication")
43
+ => []
44
+ ```
45
+
46
+ If all went to plan, you should have gotten an empty array.
47
+ Let's now add some data to this bad boy.
48
+
49
+ ```
50
+ {
51
+ "type": ":preset",
52
+ "params": {
53
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
54
+ }
55
+ }
56
+ ```
57
+ ```
58
+ > Reportinator.report("multiplication")
59
+ => [["nx1", "nx2", "nx3", "nx4", "nx5"]]
60
+ ```
61
+
62
+ Now we could add the other rows ourselves, by adding more rows to "data":
63
+
64
+ ```
65
+ {
66
+ "type": ":preset",
67
+ "params": {
68
+ "data": [
69
+ ["nx1","nx2","nx3","nx4","nx5"],
70
+ [1, 2, 3, 4, 5],
71
+ [2, 4, 6, 8, 10],
72
+ [3, 6, 9, 12, 15],
73
+ [4, 8, 12, 16, 20],
74
+ [5, 10, 15, 20, 25]
75
+ ]
76
+ }
77
+ }
78
+ ```
79
+ ```
80
+ > Reportinator.report("multiplication")
81
+ =>
82
+ [
83
+ ["nx1", "nx2", "nx3", "nx4", "nx5"],
84
+ [1, 2, 3, 4, 5],
85
+ [2, 4, 6, 8, 10],
86
+ [3, 6, 9, 12, 15],
87
+ [4, 8, 12, 16, 20],
88
+ [5, 10, 15, 20, 25]
89
+ ]
90
+ ```
91
+
92
+ However, there is a cleaner way of doing this.
93
+ Move your entire report object inside of an array.
94
+ This allows us to string reports together in the same template.
95
+
96
+ ```
97
+ [
98
+ {
99
+ "type": ":preset",
100
+ "params": {
101
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
102
+ }
103
+ }
104
+ ]
105
+ ```
106
+
107
+ Add a new report object underneath the first.
108
+ This time, the type will be ":model".
109
+
110
+ ":model" reports take two parameters:
111
+ 1. "target"
112
+ 2. "method_list"
113
+
114
+ Add both these keys to the "params" of the second report object.
115
+ Set both to be an empty array.
116
+
117
+ ```
118
+ [
119
+ {
120
+ "type": ":preset",
121
+ "params": {
122
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
123
+ }
124
+ },
125
+ {
126
+ "type": ":model",
127
+ "params": {
128
+ "target": [],
129
+ "method_list": []
130
+ }
131
+ }
132
+ ]
133
+ ```
134
+
135
+ Model reports take a target, as specified in "target", and run methods against it,
136
+ specified in "method_list", saving the outputs of each to the row.
137
+
138
+ If the target is enumerable, said methods will run on each enumeration of the target,
139
+ each enumeration adding a new row to the report.
140
+
141
+ A method is specified by either a symbol, array or hash.
142
+ Lets take the string "100" as our target.
143
+
144
+ If our method was to be `":reverse"`, it would be the same as running"
145
+
146
+ ```
147
+ > "100".reverse
148
+ => "001"
149
+ ```
150
+
151
+ We can chain methods using an array. For example: `[":reverse", ":to_i"]`
152
+
153
+ ```
154
+ > "100".reverse.to_i
155
+ => 1
156
+ ```
157
+
158
+ Methods inside a hash allow for parameters to be passed to the method.
159
+ The value of the hash are passed as the parameters, and an array is passed
160
+ as multiple parameters.
161
+
162
+ Eg. `{"gsub": ["0", "1"]}`
163
+
164
+ ```
165
+ > "100".gsub("0", "1")
166
+ => "111"
167
+ ```
168
+
169
+ In Ruby, it turns out the multiplication "*" sign is a method.
170
+ Using this, we can write a much smarter report.
171
+
172
+ ```
173
+ [
174
+ {
175
+ "type": ":preset",
176
+ "params": {
177
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
178
+ }
179
+ },
180
+ {
181
+ "type": ":model",
182
+ "params": {
183
+ "target": [1, 2, 3, 4, 5],
184
+ "method_list": [{"*": 1},{"*": 2},{"*": 3},{"*": 4},{"*": 5}]
185
+ }
186
+ }
187
+ ]
188
+ ```
189
+
190
+ The "*" is behaving exactly the same way as our "gsub" example earlier.
191
+
192
+ If we run our report again:
193
+
194
+ ```
195
+ > Reportinator.report("multiplication")
196
+ =>
197
+ [
198
+ ["nx1", "nx2", "nx3", "nx4", "nx5"],
199
+ [1, 2, 3, 4, 5],
200
+ [2, 4, 6, 8, 10],
201
+ [3, 6, 9, 12, 15],
202
+ [4, 8, 12, 16, 20],
203
+ [5, 10, 15, 20, 25]
204
+ ]
205
+ ```
206
+
207
+ The result should be exactly the same.
208
+
209
+ This is pretty good, but we can do better!
210
+ Notice how the "target" was an array? As it is enumerable,
211
+ we could run our methods against each element within it.
212
+
213
+ But what if we wanted to have 10 rows? Or 50? Soon our array is going to get pretty long.
214
+
215
+ This is where a range would be perfect. Set the start value to 1, the end to whatever number we need,
216
+ and then we go from there.
217
+
218
+ Unfortunately, we can't use a range in JSON.
219
+
220
+ ... or can we?
221
+
222
+ Reportinator has a bunch of handy built in functions, for converting strings.
223
+ We have already seen ":symbol" to make a string into a symbol.
224
+
225
+ We won't explore all the functions now, but we will explore "!r".
226
+ Or more specifically, "!rn", which auto converts strings into numbers as well.
227
+
228
+ We can make a range simply by writing "!rn 1,5". It takes the number before the comma,
229
+ as the start of the range, and the one after as the end.
230
+
231
+ We can test this with the actual parse method:
232
+
233
+ ```
234
+ > Reportinator.parse("!rn 1, 5")
235
+ => (1..5)
236
+ ```
237
+
238
+ Let's add this now as the target of our report:
239
+
240
+ ```
241
+ [
242
+ {
243
+ "type": ":preset",
244
+ "params": {
245
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
246
+ }
247
+ },
248
+ {
249
+ "type": ":model",
250
+ "params": {
251
+ "target": "!rn 1,5",
252
+ "method_list": [{"*": 1},{"*": 2},{"*": 3},{"*": 4},{"*": 5}]
253
+ }
254
+ }
255
+ ]
256
+ ```
257
+
258
+ Finally, rather than peering at the console to see if it worked,
259
+ lets put it into a csv.
260
+
261
+ ```
262
+ > Reportinator.output("multiplication.csv","multiplication")
263
+ => "multiplication.csv"
264
+ ```
265
+
266
+ Open the csv up in your spreadsheet viewer of choice, and revel
267
+ in your brand new report!
@@ -0,0 +1,24 @@
1
+ module Reportinator
2
+ class Base
3
+ include ActiveModel::API
4
+ include ActiveModel::Attributes
5
+
6
+ require_rel "."
7
+
8
+ def self.config
9
+ Reportinator.config
10
+ end
11
+
12
+ def config
13
+ self.class.config
14
+ end
15
+
16
+ def self.logger
17
+ Reportinator.logger
18
+ end
19
+
20
+ def logger
21
+ self.class.logger
22
+ end
23
+ end
24
+ end
@@ -6,10 +6,25 @@ module Reportinator
6
6
  }
7
7
  DEFAULT_REPORT_DIRS = ["reports", "app/reports"]
8
8
  DEFAULT_REPORT_SUFFIXES = ["report.json", "json"]
9
+ DEFAULT_FUNCTIONS = [
10
+ "Reportinator::HelperArrayFunction",
11
+ "Reportinator::JoinArrayFunction",
12
+ "Reportinator::MethodArrayFunction",
13
+ "Reportinator::AdditionStringFunction",
14
+ "Reportinator::ConstantStringFunction",
15
+ "Reportinator::DateStringFunction",
16
+ "Reportinator::JoinStringFunction",
17
+ "Reportinator::LogicalStringFunction",
18
+ "Reportinator::NumberStringFunction",
19
+ "Reportinator::RangeStringFunction",
20
+ "Reportinator::SymbolStringFunction",
21
+ "Reportinator::VariableStringFunction"
22
+ ]
9
23
 
10
24
  attribute :report_directories, default: []
11
25
  attribute :report_suffixes, default: []
12
26
  attribute :report_types, default: {}
27
+ attribute :parser_functions, default: []
13
28
  attribute :output_directory, default: "reports"
14
29
 
15
30
  def configured_directories
@@ -24,5 +39,10 @@ module Reportinator
24
39
  types = DEFAULT_TYPES
25
40
  types.merge(report_types)
26
41
  end
42
+
43
+ def configured_functions
44
+ functions = DEFAULT_FUNCTIONS + parser_functions
45
+ functions.map { |function| function.constantize }
46
+ end
27
47
  end
28
48
  end
@@ -0,0 +1,33 @@
1
+ module Reportinator
2
+ class Function < Base
3
+ attribute :element
4
+ attribute :variables, default: {}
5
+
6
+ def self.parse(element, variables = {})
7
+ new(element: element, variables: variables).get
8
+ end
9
+
10
+ def parse_value(value)
11
+ ValueParser.parse(value, variables)
12
+ end
13
+
14
+ def parse_and_execute_value(target, value)
15
+ ValueParser.parse_and_execute(target, value, variables)
16
+ end
17
+
18
+ def prefixes
19
+ self.class::PREFIXES
20
+ end
21
+
22
+ private
23
+
24
+ def get_prefix(value)
25
+ raise "Value is not a string" unless value.instance_of? String
26
+ sorted_prefixes = prefixes.sort.reverse
27
+ sorted_prefixes.each do |prefix|
28
+ return prefix if value.start_with? prefix
29
+ end
30
+ raise "Value #{value} is incompatible with this function!"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ module Reportinator
2
+ class HelperArrayFunction < ArrayFunction
3
+ PREFIXES = [">strf", ">offset", ">title"]
4
+
5
+ INTERVALS = {
6
+ days: {start: "at_beginning_of_day", end: "at_end_of_day"},
7
+ weeks: {start: "at_beginning_of_week", end: "at_end_of_week"},
8
+ months: {start: "at_beginning_of_month", end: "at_end_of_month"},
9
+ years: {start: "at_beginning_of_year", end: "at_end_of_year"}
10
+ }
11
+
12
+ attr_writer :parsed_target, :parsed_values
13
+
14
+ def output
15
+ return parse_strftime if prefix == ">strf"
16
+ return parse_offset if prefix == ">offset"
17
+ return parse_title if prefix == ">title"
18
+ element
19
+ end
20
+
21
+ def target_to_time
22
+ parsed_target.to_s.to_time
23
+ end
24
+
25
+ def parse_strftime
26
+ time = target_to_time
27
+ return "Invalid Time" if time.nil?
28
+ return "Invalid Format" unless parsed_values[0].instance_of? String
29
+ time.strftime parsed_values[0]
30
+ end
31
+
32
+ def parse_offset
33
+ time = target_to_time
34
+ return "Invalid Time" if time.nil?
35
+ offset = parsed_values[0]
36
+ return "Missing Offset" unless offset.present?
37
+ interval = parsed_values[1]
38
+ snap = parsed_values[2]
39
+ calculate_offset(time, offset, interval, snap)
40
+ end
41
+
42
+ def calculate_offset(time, offset, interval, snap)
43
+ interval = (interval.present? ? interval : :months)
44
+ interval = interval.to_s.pluralize.to_sym
45
+ return "Invalid Interval" unless INTERVALS.include? interval
46
+ interval_data = INTERVALS[interval]
47
+ snap = false unless interval_data.include? snap
48
+ output = time.advance({interval => offset})
49
+ output = output.send(interval_data[snap]) if snap.present?
50
+ output
51
+ end
52
+
53
+ def parse_title
54
+ to_join = [parsed_target] + parsed_values
55
+ to_join.join(" ").titleize
56
+ end
57
+
58
+ def parsed_target
59
+ @parsed_target ||= parse_target
60
+ end
61
+
62
+ def parse_target
63
+ formatted_target = (target.instance_of?(String) ? target.strip : target)
64
+ parse_value(formatted_target)
65
+ end
66
+
67
+ def parsed_values
68
+ @parsed_values ||= values.map { |value| parse_value(value) }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,11 @@
1
+ module Reportinator
2
+ class JoinArrayFunction < ArrayFunction
3
+ PREFIXES = [">join"]
4
+
5
+ def output
6
+ joiner = ValueParser.parse(target, variables)
7
+ joiner = (joiner.instance_of?(String) ? joiner : target)
8
+ values.map { |value| parse_value(value) }.join(joiner)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Reportinator
2
+ class MethodArrayFunction < ArrayFunction
3
+ PREFIXES = ["#"]
4
+
5
+ def output
6
+ parse_and_execute_value(target, values)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ module Reportinator
2
+ class ArrayFunction < Function
3
+ PREFIXES = []
4
+
5
+ attribute :prefix
6
+ attribute :target
7
+ attribute :values
8
+
9
+ def self.accepts? input
10
+ return false unless input.instance_of? Array
11
+ return false if self::PREFIXES.empty?
12
+ return false unless input[0].instance_of? String
13
+ self::PREFIXES.each do |prefix|
14
+ return true if input[0].start_with? prefix
15
+ end
16
+ false
17
+ end
18
+
19
+ def get
20
+ raise "Function missing output!" unless respond_to? :output
21
+ if set_attributes
22
+ output
23
+ else
24
+ element
25
+ end
26
+ end
27
+
28
+ def set_attributes
29
+ array = element
30
+ prefix = get_prefix(array[0])
31
+ target_value = array.delete_at(0).sub(prefix, "")
32
+ target_value = array.delete_at(0) if target_value.empty?
33
+ if target_value.to_s.empty?
34
+ false
35
+ else
36
+ self.prefix = prefix
37
+ self.target = target_value
38
+ self.values = array
39
+ true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ module Reportinator
2
+ class AdditionStringFunction < StringFunction
3
+ PREFIXES = ["!a"]
4
+
5
+ def output
6
+ values = body.split(",").map { |value| parse_value(value.strip) }
7
+ values.map! { |subvalue| NumberStringFunction.parse("!n #{subvalue}") }
8
+ values.sum(0)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Reportinator
2
+ class ConstantStringFunction < StringFunction
3
+ PREFIXES = ["&"]
4
+
5
+ def output
6
+ body.constantize
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Reportinator
2
+ class DateStringFunction < StringFunction
3
+ PREFIXES = ["!d"]
4
+
5
+ def output
6
+ Time.parse(body)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Reportinator
2
+ class JoinStringFunction < StringFunction
3
+ PREFIXES = ["!j"]
4
+
5
+ def output
6
+ values = body.split(",").map { |value| parse_value(value.strip) }
7
+ values.join(" ")
8
+ end
9
+ end
10
+ end
@@ -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,11 @@
1
+ module Reportinator
2
+ class NumberStringFunction < StringFunction
3
+ PREFIXES = ["!n"]
4
+
5
+ def output
6
+ float = (body =~ /\d\.\d/)
7
+ return body.to_f if float.present?
8
+ body.to_i
9
+ end
10
+ end
11
+ 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,10 @@
1
+ module Reportinator
2
+ class VariableStringFunction < StringFunction
3
+ PREFIXES = ["$"]
4
+
5
+ def output
6
+ return element unless variables.include? body.to_sym
7
+ variables[body.to_sym]
8
+ end
9
+ end
10
+ 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
@@ -7,10 +7,13 @@ module Reportinator
7
7
 
8
8
  def self.data_from_template(template, additional_params = {})
9
9
  template_data = load_template(template, additional_params)
10
- return split_rows(template_data.data) unless template_data.instance_of?(Array)
10
+ unless template_data.instance_of?(Array)
11
+ output = split_rows(template_data.data)
12
+ return ReportParser.parse(output)
13
+ end
11
14
  output = []
12
- template_data.each { |report| output += report.data }
13
- split_rows(output)
15
+ template_data.each { |report| output += split_rows(report.data) }
16
+ ReportParser.parse(output)
14
17
  end
15
18
 
16
19
  def self.load_template(template, additional_params = {})
@@ -24,6 +27,11 @@ module Reportinator
24
27
  end
25
28
 
26
29
  def self.load_singular(data, additional_params)
30
+ parent_variables = additional_params[:variables]
31
+ child_variables = data[:variables]
32
+ if child_variables.present?
33
+ data[:variables] = ValueParser.parse(child_variables, parent_variables)
34
+ end
27
35
  data.merge!(additional_params) { |key, old_value, new_value| merge_values(new_value, old_value) }
28
36
  filtered_data = filter_params(data, attribute_names)
29
37
  variables = filtered_data[:variables]
@@ -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