reportinator 0.1.0 → 0.2.0

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