reportinator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bb46422febcfaac711a20fbb21418ed224bc22e59468e9295dfccdd0c7ca309e
4
+ data.tar.gz: afba11ce1ecc80668797392b5c24eeffc715d349b78f93fa7d6e7ea8dfdc096a
5
+ SHA512:
6
+ metadata.gz: 8c604e8d6b30dff8df130dd6af08fcbc7a6ee0a4bb4a9e3c39478e97f2d3a2bb073c4543dcd2f366c4a39b5b15c97925d64bcba4060ed344c4c95de616507d91
7
+ data.tar.gz: 678d1ed69042d72e8f0c146abcc0a55becb31a7961f034bb58d63a1f2999100443f13e91fd71129bcaab33616a1bc0361bbac7dd62ae5003c21159574c2c4ef0
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-10-04
4
+ - Initial release
5
+
6
+ ### Added
7
+ - Value parser
8
+ - Method parser
9
+ - Preset report type
10
+ - Model report type
11
+ - Readme with report tutorial
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dev@moxvallix.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in reportinator.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "standard", "~> 1.3"
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reportinator (0.1.0)
5
+ activemodel (~> 7.0)
6
+ require_all (~> 3.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (7.0.4)
12
+ activesupport (= 7.0.4)
13
+ activesupport (7.0.4)
14
+ concurrent-ruby (~> 1.0, >= 1.0.2)
15
+ i18n (>= 1.6, < 2)
16
+ minitest (>= 5.1)
17
+ tzinfo (~> 2.0)
18
+ ast (2.4.2)
19
+ concurrent-ruby (1.1.10)
20
+ diff-lcs (1.5.0)
21
+ i18n (1.12.0)
22
+ concurrent-ruby (~> 1.0)
23
+ json (2.6.2)
24
+ minitest (5.16.3)
25
+ parallel (1.22.1)
26
+ parser (3.1.2.1)
27
+ ast (~> 2.4.1)
28
+ rainbow (3.1.1)
29
+ rake (13.0.6)
30
+ regexp_parser (2.6.0)
31
+ require_all (3.0.0)
32
+ rexml (3.2.5)
33
+ rspec (3.11.0)
34
+ rspec-core (~> 3.11.0)
35
+ rspec-expectations (~> 3.11.0)
36
+ rspec-mocks (~> 3.11.0)
37
+ rspec-core (3.11.0)
38
+ rspec-support (~> 3.11.0)
39
+ rspec-expectations (3.11.1)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.11.0)
42
+ rspec-mocks (3.11.1)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.11.0)
45
+ rspec-support (3.11.1)
46
+ rubocop (1.35.1)
47
+ json (~> 2.3)
48
+ parallel (~> 1.10)
49
+ parser (>= 3.1.2.1)
50
+ rainbow (>= 2.2.2, < 4.0)
51
+ regexp_parser (>= 1.8, < 3.0)
52
+ rexml (>= 3.2.5, < 4.0)
53
+ rubocop-ast (>= 1.20.1, < 2.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (>= 1.4.0, < 3.0)
56
+ rubocop-ast (1.21.0)
57
+ parser (>= 3.1.1.0)
58
+ rubocop-performance (1.14.3)
59
+ rubocop (>= 1.7.0, < 2.0)
60
+ rubocop-ast (>= 0.4.0)
61
+ ruby-progressbar (1.11.0)
62
+ standard (1.16.1)
63
+ rubocop (= 1.35.1)
64
+ rubocop-performance (= 1.14.3)
65
+ tzinfo (2.0.5)
66
+ concurrent-ruby (~> 1.0)
67
+ unicode-display_width (2.3.0)
68
+
69
+ PLATFORMS
70
+ x86_64-linux
71
+
72
+ DEPENDENCIES
73
+ rake (~> 13.0)
74
+ reportinator!
75
+ rspec (~> 3.0)
76
+ standard (~> 1.3)
77
+
78
+ BUNDLED WITH
79
+ 2.3.11
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Moxvallix
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,397 @@
1
+ # Reportinator
2
+ ## Behold! My Report-inator!
3
+
4
+ **Warning: this gem can execute methods based on strings defined in a JSON file. Use at your own caution.**
5
+ **This gem has not been security audited, and should not be used in a production environment!**
6
+
7
+ Reportinator is a gem that allows you to easily define a report using a JSON file.
8
+ Report templates can reference other report templates, allowing for report "partials" to easily be re-used.
9
+
10
+ ## Installation
11
+
12
+ Install the gem and add to the application's Gemfile by executing:
13
+
14
+ $ bundle add reportinator
15
+
16
+ If bundler is not being used to manage dependencies, install the gem by executing:
17
+
18
+ $ gem install reportinator
19
+
20
+ ## Usage
21
+ ### Creating my first Report
22
+ Let's start by considering what we want our output to be.
23
+ Say we want a multiplication table, like such:
24
+ | nx1 | nx2 | nx3 | nx4 | nx5 |
25
+ |-----|-----|-----|-----|-----|
26
+ | 1 | 2 | 3 | 4 | 5 |
27
+ | 2 | 4 | 6 | 8 | 10 |
28
+ | 3 | 6 | 9 | 12 | 15 |
29
+ | 4 | 8 | 12 | 16 | 20 |
30
+ | 5 | 10 | 15 | 20 | 25 |
31
+
32
+ Make a new file in your `app/reports` directory.
33
+ Name it `multiplication.report.json`
34
+
35
+ Set it's type to ":preset". The ":preset" type takes one parameter, "data",
36
+ and returns any values passed inside it, but with their values parsed.
37
+ We put a colon in front of the word "preset", such that the value parser
38
+ knows to turn it into a symbol.
39
+
40
+ ```
41
+ {
42
+ "type": ":preset"
43
+ }
44
+ ```
45
+
46
+ Next we need to give it parameters to be passed into the report.
47
+ ":preset" only accepts the "data" parameter.
48
+ Add "data" to a "params" object, and set it to be an empty array.
49
+
50
+ ```
51
+ {
52
+ "type": ":preset",
53
+ "params": {
54
+ "data": []
55
+ }
56
+ }
57
+ ```
58
+
59
+ You can now try running this report:
60
+
61
+ ```
62
+ > Reportinator.report("multiplication")
63
+ => []
64
+ ```
65
+
66
+ If all went to plan, you should have gotten an empty array.
67
+ Let's now add some data to this bad boy.
68
+
69
+ ```
70
+ {
71
+ "type": ":preset",
72
+ "params": {
73
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
74
+ }
75
+ }
76
+ ```
77
+ ```
78
+ > Reportinator.report("multiplication")
79
+ => [["nx1", "nx2", "nx3", "nx4", "nx5"]]
80
+ ```
81
+
82
+ Now we could add the other rows ourselves, by adding more rows to "data":
83
+
84
+ ```
85
+ {
86
+ "type": ":preset",
87
+ "params": {
88
+ "data": [
89
+ ["nx1","nx2","nx3","nx4","nx5"],
90
+ [1, 2, 3, 4, 5],
91
+ [2, 4, 6, 8, 10],
92
+ [3, 6, 9, 12, 15],
93
+ [4, 8, 12, 16, 20],
94
+ [5, 10, 15, 20, 25]
95
+ ]
96
+ }
97
+ }
98
+ ```
99
+ ```
100
+ > Reportinator.report("multiplication")
101
+ =>
102
+ [
103
+ ["nx1", "nx2", "nx3", "nx4", "nx5"],
104
+ [1, 2, 3, 4, 5],
105
+ [2, 4, 6, 8, 10],
106
+ [3, 6, 9, 12, 15],
107
+ [4, 8, 12, 16, 20],
108
+ [5, 10, 15, 20, 25]
109
+ ]
110
+ ```
111
+
112
+ However, there is a cleaner way of doing this.
113
+ Move your entire report object inside of an array.
114
+ This allows us to string reports together in the same template.
115
+
116
+ ```
117
+ [
118
+ {
119
+ "type": ":preset",
120
+ "params": {
121
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
122
+ }
123
+ }
124
+ ]
125
+ ```
126
+
127
+ Add a new report object underneath the first.
128
+ This time, the type will be ":model".
129
+
130
+ ":model" reports take two parameters:
131
+ 1. "target"
132
+ 2. "method_list"
133
+
134
+ Add both these keys to the "params" of the second report object.
135
+ Set both to be an empty array.
136
+
137
+ ```
138
+ [
139
+ {
140
+ "type": ":preset",
141
+ "params": {
142
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
143
+ }
144
+ },
145
+ {
146
+ "type": ":model",
147
+ "params": {
148
+ "target": [],
149
+ "method_list": []
150
+ }
151
+ }
152
+ ]
153
+ ```
154
+
155
+ Model reports take a target, as specified in "target", and run methods against it,
156
+ specified in "method_list", saving the outputs of each to the row.
157
+
158
+ If the target is enumerable, said methods will run on each enumeration of the target,
159
+ each enumeration adding a new row to the report.
160
+
161
+ A method is specified by either a symbol, array or hash.
162
+ Lets take the string "100" as our target.
163
+
164
+ If our method was to be `":reverse"`, it would be the same as running"
165
+
166
+ ```
167
+ > "100".reverse
168
+ => "001"
169
+ ```
170
+
171
+ We can chain methods using an array. For example: `[":reverse", ":to_i"]`
172
+
173
+ ```
174
+ > "100".reverse.to_i
175
+ => 1
176
+ ```
177
+
178
+ Methods inside a hash allow for parameters to be passed to the method.
179
+ The value of the hash are passed as the parameters, and an array is passed
180
+ as multiple parameters.
181
+
182
+ Eg. `{"gsub": ["0", "1"]}`
183
+
184
+ ```
185
+ > "100".gsub("0", "1")
186
+ => "111"
187
+ ```
188
+
189
+ In Ruby, it turns out the multiplication "*" sign is a method.
190
+ Using this, we can write a much smarter report.
191
+
192
+ ```
193
+ [
194
+ {
195
+ "type": ":preset",
196
+ "params": {
197
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
198
+ }
199
+ },
200
+ {
201
+ "type": ":model",
202
+ "params": {
203
+ "target": [1, 2, 3, 4, 5],
204
+ "method_list": [{"*": 1},{"*": 2},{"*": 3},{"*": 4},{"*": 5}]
205
+ }
206
+ }
207
+ ]
208
+ ```
209
+
210
+ The "*" is behaving exactly the same way as our "gsub" example earlier.
211
+
212
+ If we run our report again:
213
+
214
+ ```
215
+ > Reportinator.report("multiplication")
216
+ =>
217
+ [
218
+ ["nx1", "nx2", "nx3", "nx4", "nx5"],
219
+ [1, 2, 3, 4, 5],
220
+ [2, 4, 6, 8, 10],
221
+ [3, 6, 9, 12, 15],
222
+ [4, 8, 12, 16, 20],
223
+ [5, 10, 15, 20, 25]
224
+ ]
225
+ ```
226
+
227
+ The result should be exactly the same.
228
+
229
+ This is pretty good, but we can do better!
230
+ Notice how the "target" was an array? As it is enumerable,
231
+ we could run our methods against each element within it.
232
+
233
+ But what if we wanted to have 10 rows? Or 50? Soon our array is going to get pretty long.
234
+
235
+ This is where a range would be perfect. Set the start value to 1, the end to whatever number we need,
236
+ and then we go from there.
237
+
238
+ Unfortunately, we can't use a range in JSON.
239
+
240
+ ... or can we?
241
+
242
+ Reportinator has a bunch of handy built in functions, for converting strings.
243
+ We have already seen ":symbol" to make a string into a symbol.
244
+
245
+ We won't explore all the functions now, but we will explore "!r".
246
+ Or more specifically, "!rn", which auto converts strings into numbers as well.
247
+
248
+ We can make a range simply by writing "!rn 1,5". It takes the number before the comma,
249
+ as the start of the range, and the one after as the end.
250
+
251
+ We can test this with the actual parse method:
252
+
253
+ ```
254
+ > Reportinator::ValueParser.parse("!rn 1, 5")
255
+ => (1..5)
256
+ ```
257
+
258
+ Let's add this now as the target of our report:
259
+
260
+ ```
261
+ [
262
+ {
263
+ "type": ":preset",
264
+ "params": {
265
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
266
+ }
267
+ },
268
+ {
269
+ "type": ":model",
270
+ "params": {
271
+ "target": "!rn 1,5",
272
+ "method_list": [{"*": 1},{"*": 2},{"*": 3},{"*": 4},{"*": 5}]
273
+ }
274
+ }
275
+ ]
276
+ ```
277
+
278
+ Finally, rather than peering at the console to see if it worked,
279
+ lets put it into a csv.
280
+
281
+ ```
282
+ > Reportinator.output("multiplication.csv","multiplication")
283
+ => "multiplication.csv"
284
+ ```
285
+
286
+ Open the csv up in your spreadsheet viewer of choice, and revel
287
+ in your brand new report!
288
+ ### Reports in more detail
289
+ #### The Report Template Object
290
+ A Report template has four attributes:
291
+
292
+ | key | type | description |
293
+ |-----------|--------|----------------------------------------------------|
294
+ | type | symbol | specifies the report type to use |
295
+ | variables | hash | defines variables to be used with the `$` function |
296
+ | template | string | references another template to load and merge with |
297
+ | params | hash | report specific parameters |
298
+
299
+ #### Reportinator String Parse Cheatsheet
300
+ | prefix | example | output |
301
+ |--------|-----------------------------|--------------------------------------------|
302
+ | `:` | ":symbol" | :symbol |
303
+ | `&` | "&Constant" | Constant |
304
+ | `$` | "$variable" | Value of key `variable` in variables hash. |
305
+ | `!a` | "!a 1,2,3" | 6 |
306
+ | `!d` | "!d 1970-01-01" | 1970-01-01 00:00:00 |
307
+ | `!n` | "!n 100" | 100 |
308
+ | `!r` | "!r a,z" | ("a".."z") |
309
+ | `!rd` | "!rd 1970-01-01,1979-01-01" | (1970-01-01 00:00:00..1979-01-01 00:00:00) |
310
+ | `!rn` | "!rn 1,100" | (1..100) |
311
+
312
+ #### Reportinator Method Parse Cheatsheet
313
+ When an array has a string as it's first value, and that string has the `#` prefix,
314
+ that string is parsed, and the result becomes the target of the following methods.
315
+
316
+ Hashes within the array take the first key in the hash as the method,
317
+ and the first value as parameters for that method. If the first value
318
+ is an array, each item in the array is sent as a seperate parameter.
319
+
320
+ Subsequent symbols in the array are sent as methods to the target.
321
+ | method array | ruby equivalent |
322
+ |------------------------------------------------|-------------------------------|
323
+ | `["#&Date", ":today"]` | Date.today |
324
+ | `["#&Date", ":today", ":to_s"]` | Date.today.to_s |
325
+ | `["#&Date", ":today", {"strftime": "%b, %Y"}]` | Date.today.strftime("%b, %Y") |
326
+ | `["#&Range", {"new": [1,100]}]` | Range.new(1, 100) |
327
+
328
+ ### Where to put my Reports?
329
+ By default, Reportinator checks `app/reports` for reports.
330
+ It checks for files named `*.json` and `*.report.json`
331
+ More locations and suffixes can be added in the config.
332
+
333
+ ### Getting your Report's Output
334
+ `Reportinator.report(template, params)` will output a two dimensional array.
335
+ If you picture this as a table, each sub array is a row.
336
+ `params` is optional.
337
+
338
+ `Reportinator.output(template, params, filename)` will output the report to a csv,
339
+ in the configured output directory.
340
+ `params` and `filename` are optional.
341
+
342
+ Template is the name of the template file, minus the ".json" suffix.
343
+ Here is how templates are resolved:
344
+ - "profit" => "app/reports/profit.json"
345
+ - "users/joined" => "app/reports/users/joined.json"
346
+
347
+ Params is a hash, accepting the same keys as a template would.
348
+ Params are merged with those provided by the template, overriding any conflicts.
349
+
350
+ ### Configuring Reportinator
351
+ ```
352
+ Reportinator.configuration do |config|
353
+ config.output_directory = "my/report/dir"
354
+ config.report_directories = ["first/directory","other/directory"]
355
+ config.report_suffixes = ["custom.json", "txt"]
356
+ config.report_types = {
357
+ my_type: "MyModule::MyReportType"
358
+ }
359
+ end
360
+ ```
361
+ Configuration set will not override the default configuration.
362
+ The keys used in report types, eg. `my_type`, will be the same used in the "type" field
363
+ of the reports.
364
+
365
+ ### Making a Custom Report Type
366
+ The requirements to make a Report are very simple.
367
+ 1. The report must inherit from `Reportinator::Report`
368
+ 2. The report must provide a `data` method, which returns a one or two dimensional array.
369
+
370
+ For example, this is the entire code for the Preset Report:
371
+ ```
372
+ module Reportinator
373
+ class PresetReport < Report
374
+ attribute :data, default: []
375
+ end
376
+ end
377
+ ```
378
+ Once a report has been written, it must be registed as a report type.
379
+ See the configuration section for more details.
380
+
381
+ ## Development
382
+
383
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
384
+
385
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
386
+
387
+ ## Contributing
388
+
389
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/reportinator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/reportinator/blob/master/CODE_OF_CONDUCT.md).
390
+
391
+ ## License
392
+
393
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
394
+
395
+ ## Code of Conduct
396
+
397
+ Everyone interacting in the Reportinator project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/reportinator/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "type": ":preset",
4
+ "variables": {"variable": "i am a variable"},
5
+ "params": {
6
+ "data": [
7
+ "string", ":symbol", "&Reportinator",
8
+ "$variable", "!i 1234", "!a !i 2, 2",
9
+ ["#!d 1970-01-01", {"strftime": "%b, %Y"}], "!r !i 1, !i 5"
10
+ ]
11
+ }
12
+ },
13
+ {
14
+ "type": ":model",
15
+ "params": {
16
+ "target": ["#&Reportinator", ":config"],
17
+ "method_list": [":configured_directories", ":configured_suffixes", ":configured_types"]
18
+ }
19
+ }
20
+ ]
@@ -0,0 +1,15 @@
1
+ [
2
+ {
3
+ "type": ":preset",
4
+ "params": {
5
+ "data": ["nx1","nx2","nx3","nx4","nx5"]
6
+ }
7
+ },
8
+ {
9
+ "type": ":model",
10
+ "params": {
11
+ "target": "!rn 1,5",
12
+ "method_list": [{"*": 1},{"*": 2},{"*": 3},{"*": 4},{"*": 5}]
13
+ }
14
+ }
15
+ ]
@@ -0,0 +1,28 @@
1
+ module Reportinator
2
+ class Config < Base
3
+ DEFAULT_TYPES = {
4
+ model: "Reportinator::ModelReport",
5
+ preset: "Reportinator::PresetReport"
6
+ }
7
+ DEFAULT_REPORT_DIRS = ["reports", "app/reports"]
8
+ DEFAULT_REPORT_SUFFIXES = ["report.json", "json"]
9
+
10
+ attribute :report_directories, default: []
11
+ attribute :report_suffixes, default: []
12
+ attribute :report_types, default: {}
13
+ attribute :output_directory, default: "reports"
14
+
15
+ def configured_directories
16
+ DEFAULT_REPORT_DIRS + report_directories
17
+ end
18
+
19
+ def configured_suffixes
20
+ DEFAULT_REPORT_SUFFIXES + report_suffixes
21
+ end
22
+
23
+ def configured_types
24
+ types = DEFAULT_TYPES
25
+ types.merge(report_types)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,112 @@
1
+ module Reportinator
2
+ class Loader < Base
3
+ attribute :type
4
+ attribute :template
5
+ attribute :variables
6
+ attribute :params
7
+
8
+ def self.data_from_template(template, additional_params = {})
9
+ template_data = load_template(template, additional_params)
10
+ return split_rows(template_data.data) unless template_data.instance_of?(Array)
11
+ output = []
12
+ template_data.each { |report| output += report.data }
13
+ split_rows(output)
14
+ end
15
+
16
+ def self.load_template(template, additional_params = {})
17
+ data = parse_template(template)
18
+ return load_multiple(data, additional_params) if data.instance_of?(Array)
19
+ load_singular(data, additional_params)
20
+ end
21
+
22
+ def self.load_multiple(data, additional_params)
23
+ data.map { |report| load_singular(report, additional_params) }
24
+ end
25
+
26
+ def self.load_singular(data, additional_params)
27
+ data.merge!(additional_params) { |key, old_value, new_value| merge_values(new_value, old_value) }
28
+ filtered_data = filter_params(data, attribute_names)
29
+ variables = filtered_data[:variables]
30
+ parsed_data = ValueParser.parse(filtered_data, variables)
31
+ new(parsed_data).report
32
+ end
33
+
34
+ def self.find_template(template)
35
+ suffixes = config.configured_suffixes
36
+ directories = config.configured_directories
37
+ template_files = suffixes.map { |suffix| (suffix.present? ? "#{template}.#{suffix}" : template) }
38
+ template_paths = directories.map { |dir| template_files.map { |file| "#{dir}/#{file}" } }
39
+ template_paths.flatten!
40
+ template_paths.each do |path|
41
+ return path if File.exist? path
42
+ end
43
+ raise "Missing template: #{template}. Searched: #{template_paths}"
44
+ end
45
+
46
+ def self.parse_template(template)
47
+ file = find_template(template)
48
+ begin
49
+ json = File.read(file)
50
+ JSON.parse(json, symbolize_names: true)
51
+ rescue
52
+ raise "Error parsing template file: #{file}"
53
+ end
54
+ end
55
+
56
+ def self.split_rows(data)
57
+ data = data.instance_of?(Array) ? data : [data]
58
+ rows = []
59
+ temp = []
60
+ data.each do |col|
61
+ if col.instance_of?(Array)
62
+ rows << temp unless temp.empty?
63
+ temp = []
64
+ rows << col
65
+ else
66
+ temp << col
67
+ end
68
+ end
69
+ rows << temp unless temp.empty?
70
+ rows
71
+ end
72
+
73
+ def self.filter_params(params, allowed_params)
74
+ filtered_params = params.select { |param| allowed_params.include? param.to_s }
75
+ if params.size > filtered_params.size
76
+ invalid_params = (params.keys - filtered_params.keys).map { |key| key.to_s }
77
+ logger.warn "Invalid attributes found: #{invalid_params} Valid attributes are: #{allowed_params}"
78
+ end
79
+ filtered_params
80
+ end
81
+
82
+ def self.merge_values(new_value, old_value)
83
+ return old_value.merge(new_value) if old_value.is_a?(Hash) && new_value.is_a?(Hash)
84
+ new_value
85
+ end
86
+
87
+ def report
88
+ if template.present?
89
+ additional_params = {type: type, variables: variables, params: params}
90
+ self.class.load_template(template, additional_params.compact)
91
+ else
92
+ attribute_list = report_class.attribute_names
93
+ filtered_params = self.class.filter_params(params, attribute_list)
94
+ report_class.new(filtered_params)
95
+ end
96
+ end
97
+
98
+ def report_class
99
+ types = config.configured_types
100
+ raise "Invalid type: #{type}" unless types.include? type
101
+ types[type].constantize
102
+ end
103
+
104
+ def template_file
105
+ self.class.find_template(template)
106
+ end
107
+
108
+ def template_data
109
+ self.class.parse_template(template)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,61 @@
1
+ module Reportinator
2
+ class MethodParser < Base
3
+ attribute :target
4
+ attribute :method
5
+
6
+ def self.parse(target, method)
7
+ new(target: target, method: method).output
8
+ end
9
+
10
+ def output
11
+ return send_value(target, method) if method_class == Symbol
12
+ return parse_array_method if method_class == Array
13
+ return parse_hash_method if method_class == Hash
14
+ nil
15
+ end
16
+
17
+ def method_class
18
+ method.class
19
+ end
20
+
21
+ def parse_array_method
22
+ raise "Not an array" unless method_class == Array
23
+ valid = false
24
+ output = target
25
+ method.each do |m|
26
+ value = parse_method(output, m)
27
+ next unless value.present?
28
+ valid = true
29
+ output = value
30
+ end
31
+ return output if valid
32
+ nil
33
+ end
34
+
35
+ def parse_hash_method
36
+ raise "Not a hash" unless method_class == Hash
37
+ data = method.first
38
+ method = data[0]
39
+ value = data[1]
40
+ send_value(target, method, value)
41
+ end
42
+
43
+ private
44
+
45
+ def send_value(target, method, value = nil)
46
+ method = method.to_s
47
+ return nil unless target_has_method?(target, method)
48
+ return target.send(method) unless value.present?
49
+ return target.send(method, *value) if value.instance_of?(Array)
50
+ target.send(method, value)
51
+ end
52
+
53
+ def target_has_method?(target, method)
54
+ target.methods.include?(method.to_sym)
55
+ end
56
+
57
+ def parse_method(target, method)
58
+ self.class.parse(target, method)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,144 @@
1
+ module Reportinator
2
+ class ValueParser < Base
3
+ VALUE_FUNCTIONS = %i[a d n rn rd r]
4
+
5
+ attribute :element
6
+ attribute :variables, default: {}
7
+
8
+ def self.parse(element, variables = {})
9
+ variables = variables.present? ? variables : {}
10
+ new(element: element, variables: variables).output
11
+ end
12
+
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)
19
+ MethodParser.parse(parsed_target, parsed_values)
20
+ end
21
+
22
+ def output
23
+ return parse_array if element_class == Array
24
+ return parse_hash if element_class == Hash
25
+ return parse_string if element_class == String
26
+ element
27
+ end
28
+
29
+ def parse_array
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
+ element.map { |value| parse_value(value) }
34
+ end
35
+
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
+ def parse_hash
44
+ raise "Not a hash" unless element_class == Hash
45
+ element.transform_values { |value| parse_value(value) }
46
+ end
47
+
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
+ def element_class
58
+ element.class
59
+ end
60
+
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
+ 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)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,51 @@
1
+ module Reportinator
2
+ class Report < Base
3
+ attribute :title
4
+
5
+ def self.generate(params)
6
+ output = {}
7
+ params.each do |key, value|
8
+ raise "Missing hash!" unless value.instance_of?(Hash)
9
+ value[:name] ||= key
10
+ output[key] = new(value).data
11
+ end
12
+ output
13
+ end
14
+
15
+ def self.generate_to_csv(params)
16
+ params.each do |key, value|
17
+ raise "Missing hash!" unless value.instance_of?(Hash)
18
+ value[:name] ||= key
19
+ report = new(value)
20
+ csv = report.generate_csv
21
+ puts "Report generated to #{csv}"
22
+ end
23
+ true
24
+ end
25
+
26
+ def report_title
27
+ return nil unless title.present?
28
+ parsed_title = ValueParser.parse(title)
29
+ parsed_title = parsed_title.join if parsed_title.instance_of?(Array)
30
+ parsed_title
31
+ end
32
+
33
+ def generate_csv(path)
34
+ write_to_csv(path, data)
35
+ end
36
+
37
+ def error_message
38
+ errors.full_messages.to_s
39
+ end
40
+
41
+ private
42
+
43
+ def write_to_csv(path, data)
44
+ return path if File.exist?(path)
45
+ CSV.open(path, "wb") do |csv|
46
+ data.each { |row| csv << row }
47
+ end
48
+ path
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ module Reportinator
2
+ class ModelReport < Report
3
+ attribute :target
4
+ attribute :method_list, default: []
5
+
6
+ validates :target, presence: true
7
+
8
+ def data
9
+ return get_model_data(target) unless target.methods.include? :first
10
+ records_data
11
+ end
12
+
13
+ def records_data
14
+ records = target
15
+ records = target.all if target.methods.include? :all
16
+ output = []
17
+ records.each do |model|
18
+ value = get_model_data(model)
19
+ output << value
20
+ end
21
+ output
22
+ end
23
+
24
+ def get_model_data(target)
25
+ method_list.map do |method|
26
+ ValueParser.parse_and_execute(target, method)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module Reportinator
2
+ class PresetReport < Report
3
+ attribute :data, default: []
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reportinator
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reportinator/version"
4
+ require "csv"
5
+ require "json"
6
+ require "fileutils"
7
+ require "active_support"
8
+ require "active_model"
9
+ require "require_all"
10
+
11
+ module Reportinator
12
+ class Error < StandardError; end
13
+ class << self
14
+ attr_writer :config
15
+ attr_writer :logger
16
+ end
17
+
18
+ def self.config
19
+ @config ||= Config.new
20
+ end
21
+
22
+ def self.configure
23
+ yield(config)
24
+ end
25
+
26
+ def self.logger
27
+ @logger || ActiveSupport::Logger.new($stdout)
28
+ end
29
+
30
+ def self.report(template, additional_params = {})
31
+ Loader.data_from_template(template, additional_params)
32
+ end
33
+
34
+ def self.output(template, additional_params = {}, filename = "")
35
+ filename = (filename.present? ? filename : "#{template}.csv")
36
+ path = "#{config.output_directory}/#{filename}"
37
+ FileUtils.mkdir_p(File.dirname(path))
38
+ data = Loader.data_from_template(template, additional_params)
39
+ CSV.open(path, "wb") do |csv|
40
+ data.each do |row|
41
+ csv << row
42
+ end
43
+ end
44
+ path
45
+ end
46
+
47
+ class Base
48
+ include ActiveModel::API
49
+ include ActiveModel::Attributes
50
+
51
+ require_all "lib/reportinator"
52
+
53
+ def self.config
54
+ Reportinator.config
55
+ end
56
+
57
+ def config
58
+ self.class.config
59
+ end
60
+
61
+ def self.logger
62
+ Reportinator.logger
63
+ end
64
+
65
+ def logger
66
+ self.class.logger
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,4 @@
1
+ module Reportinator
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reportinator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Moxvallix
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: require_all
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description: Behold! My Reportinator! Reportinator makes reports from JSON files.
42
+ email:
43
+ - dev@moxvallix.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - ".standard.yml"
50
+ - CHANGELOG.md
51
+ - CODE_OF_CONDUCT.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - app/reports/example.report.json
58
+ - app/reports/multiplication.report.json
59
+ - lib/reportinator.rb
60
+ - lib/reportinator/config.rb
61
+ - lib/reportinator/loader.rb
62
+ - lib/reportinator/parsers/method.rb
63
+ - lib/reportinator/parsers/value.rb
64
+ - lib/reportinator/report.rb
65
+ - lib/reportinator/types/model.rb
66
+ - lib/reportinator/types/preset.rb
67
+ - lib/reportinator/version.rb
68
+ - sig/reportinator.rbs
69
+ homepage: https://github.com/moxvallix/reportinator
70
+ licenses:
71
+ - MIT
72
+ metadata:
73
+ rubygems_mfa_required: 'true'
74
+ homepage_uri: https://github.com/moxvallix/reportinator
75
+ source_code_uri: https://github.com/moxvallix/reportinator
76
+ changelog_uri: https://github.com/moxvallix/reportinator
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.6.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.3.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Easily generate reports using a JSON template.
96
+ test_files: []