rvgp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +23 -0
  4. data/LICENSE +504 -0
  5. data/README.md +223 -0
  6. data/Rakefile +32 -0
  7. data/bin/rvgp +8 -0
  8. data/lib/rvgp/application/config.rb +159 -0
  9. data/lib/rvgp/application/descendant_registry.rb +122 -0
  10. data/lib/rvgp/application/status_output.rb +139 -0
  11. data/lib/rvgp/application.rb +170 -0
  12. data/lib/rvgp/base/command.rb +457 -0
  13. data/lib/rvgp/base/grid.rb +531 -0
  14. data/lib/rvgp/base/reader.rb +29 -0
  15. data/lib/rvgp/base/reconciler.rb +434 -0
  16. data/lib/rvgp/base/validation.rb +261 -0
  17. data/lib/rvgp/commands/cashflow.rb +160 -0
  18. data/lib/rvgp/commands/grid.rb +70 -0
  19. data/lib/rvgp/commands/ireconcile.rb +95 -0
  20. data/lib/rvgp/commands/new_project.rb +296 -0
  21. data/lib/rvgp/commands/plot.rb +41 -0
  22. data/lib/rvgp/commands/publish_gsheets.rb +83 -0
  23. data/lib/rvgp/commands/reconcile.rb +58 -0
  24. data/lib/rvgp/commands/rotate_year.rb +202 -0
  25. data/lib/rvgp/commands/validate_journal.rb +59 -0
  26. data/lib/rvgp/commands/validate_system.rb +44 -0
  27. data/lib/rvgp/commands.rb +160 -0
  28. data/lib/rvgp/dashboard.rb +252 -0
  29. data/lib/rvgp/fakers/fake_feed.rb +245 -0
  30. data/lib/rvgp/fakers/fake_journal.rb +57 -0
  31. data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
  32. data/lib/rvgp/fakers/faker_helpers.rb +25 -0
  33. data/lib/rvgp/gem.rb +80 -0
  34. data/lib/rvgp/journal/commodity.rb +453 -0
  35. data/lib/rvgp/journal/complex_commodity.rb +214 -0
  36. data/lib/rvgp/journal/currency.rb +101 -0
  37. data/lib/rvgp/journal/journal.rb +141 -0
  38. data/lib/rvgp/journal/posting.rb +156 -0
  39. data/lib/rvgp/journal/pricer.rb +267 -0
  40. data/lib/rvgp/journal.rb +24 -0
  41. data/lib/rvgp/plot/gnuplot.rb +478 -0
  42. data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
  43. data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
  44. data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
  45. data/lib/rvgp/plot.rb +293 -0
  46. data/lib/rvgp/pta/hledger.rb +237 -0
  47. data/lib/rvgp/pta/ledger.rb +308 -0
  48. data/lib/rvgp/pta.rb +311 -0
  49. data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
  50. data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
  51. data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
  52. data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
  53. data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
  54. data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
  55. data/lib/rvgp/utilities/grid_query.rb +190 -0
  56. data/lib/rvgp/utilities/yaml.rb +131 -0
  57. data/lib/rvgp/utilities.rb +44 -0
  58. data/lib/rvgp/validations/balance_validation.rb +68 -0
  59. data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
  60. data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
  61. data/lib/rvgp.rb +66 -0
  62. data/resources/README.MD/2022-cashflow-google.png +0 -0
  63. data/resources/README.MD/2022-cashflow.png +0 -0
  64. data/resources/README.MD/all-wealth-growth-google.png +0 -0
  65. data/resources/README.MD/all-wealth-growth.png +0 -0
  66. data/resources/gnuplot/default.yml +80 -0
  67. data/resources/i18n/en.yml +192 -0
  68. data/resources/iso-4217-currencies.json +171 -0
  69. data/resources/skel/Rakefile +5 -0
  70. data/resources/skel/app/grids/cashflow_grid.rb +27 -0
  71. data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
  72. data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
  73. data/resources/skel/app/plots/cashflow.yml +33 -0
  74. data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
  75. data/resources/skel/app/plots/wealth-growth.yml +20 -0
  76. data/resources/skel/config/csv-format-acme-checking.yml +9 -0
  77. data/resources/skel/config/google-secrets.yml +5 -0
  78. data/resources/skel/config/rvgp.yml +0 -0
  79. data/resources/skel/journals/prices.db +0 -0
  80. data/rvgp.gemspec +6 -0
  81. data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
  82. data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
  83. data/test/test_command_base.rb +61 -0
  84. data/test/test_commodity.rb +270 -0
  85. data/test/test_csv_reconciler.rb +60 -0
  86. data/test/test_currency.rb +24 -0
  87. data/test/test_fake_feed.rb +228 -0
  88. data/test/test_fake_journal.rb +98 -0
  89. data/test/test_fake_reconciler.rb +60 -0
  90. data/test/test_journal_parse.rb +545 -0
  91. data/test/test_ledger.rb +102 -0
  92. data/test/test_plot.rb +133 -0
  93. data/test/test_posting.rb +50 -0
  94. data/test/test_pricer.rb +139 -0
  95. data/test/test_pta_adapter.rb +575 -0
  96. data/test/test_utilities.rb +45 -0
  97. metadata +268 -0
data/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # rvgp - A plain text accounting framework for ruby on rake.
2
+ [![Gem Version](https://badge.fury.io/rb/rvgp.svg)](https://badge.fury.io/rb/rvgp)
3
+ What follows is a workflow tool, and accounting framework, to automate:
4
+ 1. **R**econciliation of your bank-downloaded csv's into categorized pta journals. (interactively!)
5
+ 1. **V**alidation of your output, to protect against errors. (in ruby)
6
+ 1. **G**rid'ing the data - Make 'spreadsheets' based on analytic calculations from your journals. (ruby here, too)
7
+ 1. **P**lotting grids, and generate meaningful graphs of your finances. (using gnuplot, or google sheets)
8
+
9
+ Plus more! There's a bunch of tools included for: managing commodities, working with tables, parse journals... too much to list. There's plenty of ruby goodness inside to save you time with your accounting, and keep you from reinvent wheels.
10
+
11
+ ## 📑 Table of Contents
12
+
13
+ - [The Quick Pitch](#-the-quick-pitch)
14
+ - [Getting Started](#-getting-started)
15
+ - [How do project files relate?](#-how-do-project-files-relate)
16
+ - [Understanding the Workflow](#-understanding-the-workflow)
17
+ - [Documentation](#-documentation)
18
+ - [License](#-license)
19
+
20
+ ## 📽 The Quick Pitch
21
+ If you like ruby, and you want something akin to rails... but for your finances - this is what you're looking for! This tool offers an easy workflow,
22
+ for the ruby literate, to:
23
+
24
+ 1. Build PTA Journals, given a csv, and applying reconciliation rules from a provided yaml file
25
+ 2. Run validations (provided in ruby) to ensure the journals meet your expectations
26
+ 3. Generate Pretty Plots! using either gnuplot, or Google sheets. Take a look:
27
+
28
+ | Gnuplot Output |
29
+ | :--- |
30
+ | ![Cashflow](resources/README.MD/2022-cashflow.png) |
31
+ | ![Wealth Report](resources/README.MD/all-wealth-growth.png) |
32
+
33
+ Or, publish to google, and share it with your accountant:
34
+
35
+ | 2022 Cashflow (Google Sheet Screenshot) | Wealth Report (Google Sheet Screenshot) |
36
+ | :--- | :---- |
37
+ | ![Cashflow Google](resources/README.MD/2022-cashflow-google.png) | ![Wealth Report Google](resources/README.MD/all-wealth-growth-google.png) |
38
+
39
+ Plus, you get a bunch of other nice features. Like...
40
+ * A TUI cashflow output, for understanding your monthly cashflow on a dashboard
41
+ * Lots of versatility in your Plots. The code is very open ended, and supports a good number of 2d plot formats, and features
42
+ * No extraneous gem dependencies. Feel free to include activesupport in your project if you'd like. But, we're not imposing that on our requirements!
43
+ * A Reconciliation mode in vim, to split-screen edit your yaml, with a hot-loaded output pane
44
+ * Git friendly! Store your finances in an easily audited git repo.
45
+ * Automatic transactions, for generating transactions via ruby logic, instead of sourcing from a csv file.
46
+ * Additional modules for currency conversion, mortgage interest/principle calculations
47
+ * Add your own commands and tasks to the rake process, simply by adding commands them your app/commands folder
48
+ * Multi-threaded for faster throughput
49
+ * An easy quickstart generator, for setting up your first project (see the new_project command)
50
+ * Shortcuts for working with finance, currency, gnuplot, hledger, i18n and more
51
+
52
+ ## 🐦‍ Getting Started
53
+
54
+ The quickest way to get started, once you've installed the gem, is by way of the 'new_project' command.
55
+ ```
56
+ ~> rvgp -d ~/ledger new_project
57
+ Whose project is this? A person's full name or a company name will work: Yukihiro Matsumoto
58
+ You entered "Yukihiro Matsumoto". Is that correct? (Type "Yes" to continue) : Yes
59
+
60
+ 📖 New Project
61
+ Initializing Project directory ........................................... 🟢
62
+ Initializing Randomized bank feeds ....................................... 🟢
63
+ Initializing Randomized reconcilers ...................................... 🟢
64
+
65
+ The new project has been generated successfully.
66
+ Though you may want to add the following line to your ~/.bashrc:
67
+ export LEDGER_FILE="/home/matz/yukihiro-matsumoto.journal"
68
+
69
+ You're ready to begin working on this project. Try cd'ing into its directory, and running `rake`.
70
+ ~>
71
+ ```
72
+
73
+ Per the suggestion, you'll benefit from adding the LEDGER_FILE environment variable to your shell's startup script. This will keep you from having to specify the directory every time you run rvgp (or having to specify it to ledger and hledger). If you're working with more than one project, you may not want to use this feature.
74
+
75
+ > **Note**
76
+ > You can specify a company name, instead of person's name. (If that's what you're intent on managing with this project)
77
+
78
+ From here, you're all set to run your first build. cd into your project directory, and run rake. Here's roughly the output you can expect. We abridged this a bit, to cut down on ... paper :smiley: . Your output may differ, in any case, depending on how your cpu decides to schedule threads:
79
+ ```
80
+ ~> cd ~/ledger
81
+ ~/ledger> rake
82
+ 🏗️ Building Journals from Feeds
83
+ Expanding Personal AcmeBank:Checking (2018) .............................. 🟢
84
+ Expanding Personal AcmeBank:Checking (2023) .............................. 🟢
85
+
86
+ 📒 Inspecting Individual Journal Files
87
+ Validating Personal AcmeBank:Checking (2018) ............................. 🟢
88
+ 🟡 No balance checkpoints found.
89
+ Validating Personal AcmeBank:Checking (2023) ............................. 🟢
90
+ 🟡 No balance checkpoints found.
91
+
92
+ ▦ Generating Grids
93
+ Calculating Cashflows by month (2018) .................................... 🟢
94
+ Calculating Wealth Growth by month (2023) ................................ 🟢
95
+
96
+ 📈 Generating Plots
97
+ Plotting 2018-cashflow ................................................... 🟢
98
+ Plotting all-wealth-growth ............................................... 🟢
99
+
100
+ ~/ledger>
101
+ ```
102
+
103
+ The output of this process, is now visible in the `build` folder under your project's root.
104
+
105
+ > **Note**
106
+ > The "No balance checkpoints found." warning indicates that your reconciler definition, is missing a balance checkpoint. Since new_projects use 'fake' data, there's no balance checkpoint specified in the reconciler yaml. With your data, you'll want to enter the balance, and date, on some of your statements, into the 'balances' section of this file, to ensure accuracy against the financial institution's records. (Or, you can just disable this feature) More on that later. Nonetheless, these warnings are safe to ignore for now.
107
+
108
+ Now you can begin to explore your build. If you'd like to see the net worth of your project, try running `gnuplot build/plots/all-wealth-growth.gpi`. If you'd like to publish some plots to google, you can populate your config/ directory with google API settings, and run `rvgp publish_gsheets -a`. Or, just run a `hledger bal` (or `ledger bal`) to see your account balances.
109
+
110
+ From here, you're ready to start populating this project with your data, instead of the randomly generated feeds. The easiest way to get started, is to run a `rake clean` (Thus undo'ing the above rake work, and clearing the build/ directory). And to then, remove unnecessary files in your `feeds/` and `app/reconcilers/` directories. Go ahead from there, and download your banking institution's csv files into the `feeds/` directory. And create yaml files for each of them, in your `app/reconcilers` directory. Use an existing yaml file for quick reference.
111
+
112
+ Probably though, you'll want to read the rest of this README, to better understand how the project workflow is structured, and how these files work with that process.
113
+
114
+ ## 🐒 How do project files relate?
115
+ Let's take a moment, to understand the project directory structure. Here's what that looks like, in the "Yukihiro Matsumoto" project that we just created:
116
+
117
+ ```
118
+ ~/ledger> lsd *
119
+  Rakefile  yukihiro-matsumoto.journal
120
+
121
+ app:
122
+  commands  grids  plots  reconcilers  validations
123
+
124
+ build:
125
+  grids  journals  plots
126
+
127
+ config:
128
+  csv-format-acme-checking.yml  google-secrets.yml  rvgp.yml
129
+
130
+ feeds:
131
+  2018-personal-basic-checking.csv  2020-personal-basic-checking.csv  2022-personal-basic-checking.csv
132
+  2019-personal-basic-checking.csv  2021-personal-basic-checking.csv  2023-personal-basic-checking.csv
133
+
134
+ journals:
135
+  opening-balances.journal  prices.db
136
+ ~/ledger>
137
+ ```
138
+
139
+ In the root of this project, is your 'main' journal, alongside the Rakefile, and a few directories. Let's look at these directories one by one.
140
+ * **journals** These are where your 'manually entered' PTA journals belong. As your project grows, you can insert as many .journal files in here as you'd like. These will be automatically included by the yukihiro-matsumoto.journal. And, to get you started, there is an opening-balances.journal, which, nearly every project should have.
141
+ * **feeds** This folder exists to keep your 'source' files. Which, would principally be csv's that have been downloaded from banks. PTA '.journal' files are also supported. I synchronize the journal output from [cone](https://play.google.com/store/apps/details?id=info.tangential.cone&hl=en_US&gl=US), into journal files here.
142
+ * **build** This folder, is where the output of rvgp goes. There's really no good reason to write to this folder, outside the rvgp libraries. Be careful about putting anything in here that you don't want to lose. A `rake clean` erases just about anything that's in here. We'll better address this folder, by examining the app folder, a bit further down.
143
+ * **app** Here's where the magic happens. This folder contains the ruby and yaml, that reconciles the contents of your feeds, into a finished product. There are a number of subfolders, each containing logic dedicated to a specific part of the build process. (See below)
144
+ * **config** This is self explanatory. This directory exists to contain application feed settings. You can look through the default config files, for an overview of what's possible here.
145
+
146
+ Drilling into the app folder, we see the following sub-folders:
147
+
148
+ * **app/reconcilers** This folder contains yaml files, which are used to reconcile your feeds, into pta journals. These yaml files support an extensive featureset to 'match entries', and then tag and categorize those matches. The output of this reconcile, is stored in the build/journals folder, once executed.
149
+ * **app/validations** This folder contains ruby files, inside which, are tests that ensure validity of the output, for the above reconcilers. These ruby files contain classes which inherit from either the RVGP::Base::SystemValidation, or, the RVGP::Base::JournalValidation, depending on whether they validate the system as a whole (Perhaps, checking for any transactions tagged 'vacation', but which aren't also tagged with a 'location'). Or, whether they are specifically designed for a given reconciler's output file. There is no build output on these files. Validations either trigger an warning on the console, or abort a build from continuing (with an error on the console).
150
+ * **app/grids** This folder contains ruby files, containing classes which build 'grids'. Grids, are csv files, that contain calculated outputs, based on your journals. These grids can be used for many purposes, but, probably should be considered 'excel sheets' that are later plotted, or referenced by a command elsewhere. Typically, these grids are composed of 'hledger monthly' queries. However, they can just as easily be generated independent of your journals. Which is useful for tracking 'business projections' and financial models that you wrote yourself. The output of these grids, are stored in your build/grids folder.
151
+ * **app/plots** This folder contains the yaml files which contain the gnuplot and google settings that draw your plots. These settings determine what will gpi files are generated in your build/plots folder.
152
+ * **app/commands** This folder contains any additional commands you wish to extend the rvgp app with. And which are then suitable for insertion into the rake workflow. These files are ruby files, containing classes which inherit from the RVGP::Base::Command object.
153
+
154
+ These directories contain the bulk of your workload, in your rvgp projects. These components will be further documented further down in this README. In the meantime, it may help to illustrate the typical workflow cycle, that rvgp executes using these files.
155
+
156
+ > **Note**
157
+ > Feel free to add as many directories to your project root as you'd like. Useful ideas for additional directories might include: 'bank statements', 'test', 'orgs', 'documents', etc
158
+
159
+ ## 🪅 Understanding the Workflow
160
+
161
+ The significance of the Rakefile approach, to your accounting, can't be understated. This design decision offers us a number of features. The implicit dependency-tracking ensures that changes are only applied downstream in your build. A small adjustment at a given year, doesn't require an entire rebuild of the project. This offers us better performance, git-friendly accounting, and simplified auditing.
162
+
163
+ > **Note**
164
+ > There shouldn't be any reason to avoid, or instigate, a `rake clean` when working on a project. The Rakefile is very smart about figuring out what to change. However, if you feel the need to recalculate the entire project - a rake clean won't hurt.
165
+
166
+ To better understand how your files, are processed by rvgp, here's a diagram of how the rakefile processes your build. Hopefully this reduces confusion.
167
+
168
+ ```mermaid
169
+ graph TD;
170
+ subgraph Reconciliation[Reconciliation Cycle]
171
+ Reconcilers("app/reconcilers/*.yml").->JournalBuild("<br>🏗 Journal Build<br><br>");
172
+ Feeds("feeds/*.csv").->JournalBuild;
173
+ JournalBuild-->JournalOutput("build/journals/*.journal");
174
+ end
175
+ JournalOutput-->JValidations;
176
+ JValidationInput("app/validations/*.rb<br>(RVGP::Base::JournalValidation)").->JValidations;
177
+ JValidations("<br>📒 Journal Validate<br><br>");
178
+ SValidationInput("app/validations/*.rb<br>(RVGP::Base::SystemValidation)").->SValidations;
179
+ JValidations-->SValidations("<br>📚 System Validate<br><br>");
180
+ GridInput("app/grids/*.rb<br>(RVGP::Base::Grid)").->GridBuild("<br>▦ Grid Build<br><br>");
181
+ SValidations-->GridBuild;
182
+ GridBuild-->GridOutput("build/grids/*.csv");
183
+ PlotInput("app/plots/*.yml").->PlotBuild("<br>📈 Plot Build<br><br>");
184
+ GridOutput-->PlotBuild;
185
+ PlotBuild-->PlotOutput("build/plots/*.gpi");
186
+
187
+ style Reconciliation fill:#fdf6e3,stroke:#b58900;
188
+
189
+ style Reconcilers fill:#e3e4f4,stroke:#6c71c4;
190
+ style Feeds fill:#e3e4f4,stroke:#6c71c4;
191
+ style JValidationInput fill:#e3e4f4,stroke:#6c71c4;
192
+ style SValidationInput fill:#e3e4f4,stroke:#6c71c4;
193
+ style GridInput fill:#e3e4f4,stroke:#6c71c4;
194
+ style PlotInput fill:#e3e4f4,stroke:#6c71c4;
195
+
196
+ style JournalOutput fill:#d1f3f0,stroke:#2aa198;
197
+ style GridOutput fill:#d1f3f0,stroke:#2aa198;
198
+ style PlotOutput fill:#d1f3f0,stroke:#2aa198;
199
+
200
+ style JournalBuild fill:#d5e9f7,stroke:#268bd2;
201
+ style JValidations fill:#d5e9f7,stroke:#268bd2;
202
+ style SValidations fill:#d5e9f7,stroke:#268bd2;
203
+ style GridBuild fill:#d5e9f7,stroke:#268bd2;
204
+ style PlotBuild fill:#d5e9f7,stroke:#268bd2;
205
+ ```
206
+
207
+ In this lifecycle, the major tasks are circled in blue, with cyan output files in-between these tasks. Input files, that you provide, are peppered along the process, and are denoted in purple. Any `commands` that you define, are inserted in-between the blue tasks, depending on whether and where you define those commands to insert themselves.
208
+
209
+ ## 📚 Documentation
210
+
211
+ Here are some links around the yard documentation, to particularly useful component and feature references:
212
+ - [Reconciler yaml](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Reconcilers)
213
+ - [JournalValidation base class](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Base/JournalValidation)
214
+ - [SystemValidation base class](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Base/SystemValidation)
215
+ - [Grid base class](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Base/Grid)
216
+ - [Plot yaml](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Plot)
217
+ - [PTA class(es)](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Pta)
218
+ - [Commodity class](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Journal/Commodity)
219
+ - [ComplexCommodity class](https://www.rubydoc.info/gems/rvgp/0.3.2/RVGP/Journal/ComplexCommodity)
220
+
221
+ ## 📜 License
222
+
223
+ This software is licensed under the [LGPL-2.1](https://github.com/rvgp/blob/master/LICENSE) © [Chris DeRose](https://github.com/brighton36).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yard'
4
+ require 'rubocop/rake_task'
5
+ require 'bundler/gem_tasks'
6
+
7
+ require_relative 'lib/rvgp'
8
+
9
+ task default: 'all'
10
+
11
+ desc 'All'
12
+ task all: %i[test yard lint build]
13
+
14
+ desc 'Run the minitests'
15
+ task :test do
16
+ Dir[RVGP::Gem.root('test/test*.rb')].sort.each { |f| require f }
17
+ end
18
+
19
+ desc 'Check code notation'
20
+ RuboCop::RakeTask.new(:lint) do |task|
21
+ task.patterns = RVGP::Gem.ruby_files
22
+ task.formatters += ['html']
23
+ task.options += ['--fail-level', 'convention', '--out', 'rubocop.html']
24
+ end
25
+
26
+ YARD::Rake::YardocTask.new do |t|
27
+ t.files = RVGP::Gem.ruby_files.reject do |f|
28
+ %r{\A(?:(?:test|resources/skel)/.*|.*finance_gem_hacks\.rb\Z)}.match f
29
+ end
30
+ t.options = ['--no-private', '--protected', '--markup=markdown']
31
+ t.stats_options = ['--list-undoc']
32
+ end
data/bin/rvgp ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: syntax=ruby
3
+ # -*- mode: ruby -*-
4
+ # frozen_string_literal: true
5
+
6
+ require_relative '../lib/rvgp/commands'
7
+
8
+ RVGP::Commands.dispatch!(*ARGV)
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pta'
4
+
5
+ module RVGP
6
+ class Application
7
+ # This class provides the app configuration options accessors and parsing logic.
8
+ class Config
9
+ include RVGP::Pta::AvailabilityHelper
10
+ attr_reader :prices_path, :project_journal_path
11
+
12
+ # Given the provided project path, this object will parse and store the
13
+ # config/rvgp.yaml, as well as provide default values for otherwise unspecified attributes
14
+ # in this file.
15
+ # @param project_path [String] The path, to an RVGP project directory.
16
+ def initialize(project_path)
17
+ @project_path = project_path
18
+ @build_path = format('%s/build', project_path)
19
+
20
+ config_path = project_path 'config/rvgp.yml'
21
+ @yaml = RVGP::Utilities::Yaml.new config_path, project_path if File.exist? config_path
22
+
23
+ RVGP::Pta.pta_adapter = @yaml[:pta_adapter].to_sym if @yaml.key? :pta_adapter
24
+
25
+ @prices_path = @yaml.key?(:prices_path) ? @yaml[:prices_path] : project_path('journals/prices.db')
26
+
27
+ if @yaml.key?(:project_journal_path)
28
+ @project_journal_path = project_path @yaml[:project_journal_path]
29
+ else
30
+ journals_in_project_path = Dir.glob format('%s/*.journal', @project_path)
31
+ if journals_in_project_path.length != 1
32
+ raise StandardError,
33
+ format(
34
+ 'Unable to automatically determine the project journal. Probably you want one of these ' \
35
+ 'files: %<files>s. Set the project_journal_path parameter in ' \
36
+ 'your config file, to the relative pathname, of the project journal.',
37
+ files: journals_in_project_path.join(', ')
38
+ )
39
+ end
40
+ @project_journal_path = journals_in_project_path.first
41
+ end
42
+
43
+ # I'm not crazy about this default.. Mabe we should raise an error if
44
+ # this value isn't set...
45
+ @grid_starting_at = @yaml[:grid_starting_at] if @yaml.key? :grid_starting_at
46
+ @grid_starting_at ||= default_grid_starting_at
47
+
48
+ # NOTE: pta_adapter.newest_transaction_date.year works in lieu of Date.today,
49
+ # but that query takes time. (and it requires that we've already
50
+ # performed a build step at the time it's called) so, we use
51
+ # Date.today instead.
52
+ @grid_ending_at = @yaml[:grid_ending_at] if @yaml.key? :grid_ending_at
53
+ @grid_ending_at ||= default_grid_ending_at
54
+ end
55
+
56
+ # Return the contents of the provided attr, from the project's config/rvgp.yaml
57
+ # @return [Object] the value corresponding to the provided attr
58
+ def [](attr)
59
+ @yaml[attr]
60
+ end
61
+
62
+ # Returns a boolean indicating whether a value for the provided attr was specified in the project's
63
+ # config/rvgp.yaml
64
+ # @return [TrueClass, FalseClass] whether the key was specified
65
+ def key?(attr)
66
+ @yaml.key? attr
67
+ end
68
+
69
+ # Returns the starting date, for all grids that will be generated in this project.
70
+ # @return [Date] when to commence grid building
71
+ def grid_starting_at
72
+ call_or_return_date @grid_starting_at
73
+ end
74
+
75
+ # Returns the ending date, for all grids that will be generated in this project.
76
+ # @return [Date] when to finish grid building
77
+ def grid_ending_at
78
+ call_or_return_date @grid_ending_at
79
+ end
80
+
81
+ # The years, for which we will be building grids
82
+ # @return [Array<Integer>] What years to expect in our build/grids directory (and their downstream targets)
83
+ def grid_years
84
+ grid_starting_at.year.upto(grid_ending_at.year)
85
+ end
86
+
87
+ # Returns the full path, to a file or directory, in the project. If no relpath was provided, this
88
+ # method returns the full path to the project directory.
89
+ # @param relpath [optional, String] The relative path, to a filesystem object in the current project
90
+ # @return [String] The full path to the requested resource
91
+ def project_path(relpath = nil)
92
+ relpath ? [@project_path, relpath].join('/') : @project_path
93
+ end
94
+
95
+ # Returns the full path, to a file or directory, in the project's build/ directory. If no relpath was provided,
96
+ # this method returns the full path to the project build directory.
97
+ # @param relpath [optional, String] The relative path, to a filesystem object in the current project
98
+ # @return [String] The full path to the requested resource
99
+ def build_path(relpath = nil)
100
+ relpath ? [@build_path, relpath].join('/') : @build_path
101
+ end
102
+
103
+ # This is a bit of a kludge. We wanted this in a few places, so, I DRY'd it here. tldr: this returns an array
104
+ # of years (as integers), which, were groked from the file names found in the app/reconcilers directory.
105
+ # It's a rough shorthand, that, ends up being a better 'guess' of start/end dates, than Date.today
106
+ # @!visibility private
107
+ def reconciler_years
108
+ Dir.glob(project_path('app/reconcilers/*.yml')).map do |f|
109
+ ::Regexp.last_match(1).to_i if /\A(\d{4}).+/.match File.basename(f)
110
+ end.compact.uniq.sort
111
+ end
112
+
113
+ private
114
+
115
+ def default_grid_starting_at
116
+ years = reconciler_years
117
+ years.empty? ? (Date.today << 12) : Date.new(years.first, 1, 1)
118
+ end
119
+
120
+ # We want/need grid tasks that are defined by (year)-gridname. However, we want
121
+ # an exact end date, that's based off the output of the build step.
122
+ #
123
+ # So, what we do, is check for the prescence of journal files, and if they're
124
+ # not there, we just return the end of the current year.
125
+ #
126
+ # If we find the journal files, we return the last 'full month' of data
127
+ #
128
+ # NOTE: we probably could/should cache the newest_transaction_date, but,
129
+ # that would be a PITA right now
130
+ def default_grid_ending_at
131
+ # It's important that we return a lambda, so that the call_or_return()
132
+ # re-runs this code after the grids are generated
133
+ # TODO: This lambda is goofy. Let's see if we can nix it
134
+ lambda do
135
+ unless Dir[build_path('journals/*.journal')].count.positive?
136
+ years = reconciler_years
137
+ return years.empty? ? Date.today : Date.new(years.last, 12, 31)
138
+ end
139
+
140
+ # TODO: I think this is why our rake / rake clean output is mismatching atm
141
+ end_date = pta.newest_transaction_date file: project_journal_path
142
+
143
+ return end_date if end_date == Date.civil(end_date.year, end_date.month, -1)
144
+
145
+ if end_date.month == 1
146
+ Date.civil end_date.year - 1, 12, 31
147
+ else
148
+ Date.civil end_date.year, end_date.month - 1, -1
149
+ end
150
+ end
151
+ end
152
+
153
+ def call_or_return_date(value)
154
+ ret = value.respond_to?(:call) ? value.call : value
155
+ ret.is_a?(Date) ? ret : Date.strptime(ret)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RVGP
4
+ class Application
5
+ # This module contains a system by which RVGP maintains an application registry,
6
+ # of child classes, for a given parent class. These registries are stored in
7
+ # the RVGP namespace, under a provided name (usually something resembling the
8
+ # superclass name), and facilitates an easy form of child class enumeration,
9
+ # throughout the RVGP system.
10
+ #
11
+ # Thus far, the parent classes which are using this functionality, are:
12
+ # {RVGP::Base::Command}, {RVGP::Base::Grid}, {RVGP::Base::JournalValidation}, and {RVGP::Base::SystemValidation}.
13
+ #
14
+ # This means that, for example, a class which inherits from {RVGP::Base::Command},
15
+ # is added to the array of its siblings in {RVGP.commands}. Similarly, there are
16
+ # containers for {RVGP.grids}, {RVGP.journal_validations}, and {RVGP.system_validations}.
17
+ module DescendantRegistry
18
+ # This basic class resembles an array, and is used to house a regsitry of
19
+ # children classes. Typically, this class is instantiated inside of RVGP, at
20
+ # the time a child inherits from a parent.
21
+ #
22
+ # @attr_reader [Array<Object>] classes The undecorated classes that are contained in this object
23
+ class ClassRegistry
24
+ include Enumerable
25
+
26
+ attr_reader :classes
27
+
28
+ # Declare the registry, and initialize with the relevant options
29
+ # @param [Hash] opts what options to configure this registry with
30
+ # @option opts [Hash<String, Proc>] :accessors what methods to dispatch to the instances of this collection
31
+ def initialize(opts = {})
32
+ @classes = []
33
+ @accessors = opts[:accessors] || {}
34
+ end
35
+
36
+ # Call the provided block, for each element of the registry
37
+ # @yield [obj] The block you wish to call, once per element of the registry
38
+ # @return [void]
39
+ def each(&block)
40
+ classes.each(&block)
41
+ end
42
+
43
+ # Add the provided object to the {#classes} collection
44
+ # @param [Object] klass The object class, you wish to add
45
+ # @return [void]
46
+ def add(klass)
47
+ @classes << klass
48
+ end
49
+
50
+ # The names of all the classes that are defined in this registry
51
+ # @return [Array<String>]
52
+ def names
53
+ classes.collect(&:name)
54
+ end
55
+
56
+ # @!visibility private
57
+ def respond_to_missing?(name, _include_private = false)
58
+ @accessors.key? name
59
+ end
60
+
61
+ # In the case that a method is called on this registry, that isn't explicitly defined,
62
+ # this method checks the accessors provided in {#initialize} to see if there's a matching
63
+ # block, indexing to the name of the missing method. And calls that.
64
+ # @param [Symbol] name The method attempting to be called
65
+ def method_missing(name)
66
+ @accessors.key?(name) ? @accessors[name].call(self) : super(name)
67
+ end
68
+ end
69
+
70
+ # @!visibility private
71
+ def self.included(klass)
72
+ klass.extend ClassMethods
73
+ end
74
+
75
+ # This module defines the parent's class methods, which are attached to a parent
76
+ # class, at the time it includes the DescendantRegistry
77
+ module ClassMethods
78
+ # This method is the main entrypoint for all of the descendent registry features. This method
79
+ # installs a registry, into the provided namespace, given the provided options
80
+ # @param [Object] in_klass This is class, under which, this registry will be created
81
+ # @param [Symbol] with_name The name of the registry, which will be the name of the reader, created in in_klass
82
+ # @param [Hash] opts what options to configure this registry with
83
+ # @option opts [Hash<String, Proc>] :accessors A list of public methods to create, alongside their
84
+ # implementation,in the base of the newly created collection.
85
+ # @option opts [Regexp] :name_capture This regex is expected to contain a single capture, that will be used to
86
+ # construct a class name, given a classes #to_s output, and before sending
87
+ # to underscorize.
88
+ def register_descendants(in_klass, with_name, opts = {})
89
+ @descendant_registry = { klass: in_klass,
90
+ name: with_name,
91
+ name_capture: opts.key?(:name_capture) ? opts[:name_capture] : /\A.*:(.+)\Z/ }
92
+ define_singleton_method(:descendant_registry) { @descendant_registry }
93
+
94
+ in_klass.instance_eval do
95
+ iv_sym = "@#{with_name}".to_sym
96
+ instance_variable_set iv_sym, ClassRegistry.new(opts)
97
+ define_singleton_method(with_name) { instance_variable_get iv_sym }
98
+ end
99
+ end
100
+
101
+ # The name of this base class, after applying its to_s to :name_capture,
102
+ # and underscore'izing the capture
103
+ # @return [String] A string that can be used for various metaprogramming requirements of this
104
+ # DescendantRegistry
105
+ def name
106
+ name_capture = superclass.descendant_registry[:name_capture]
107
+ name = name_capture.match(to_s) ? ::Regexp.last_match(1) : to_s
108
+
109
+ # underscorize the capture:
110
+ name.scan(/[A-Z][^A-Z]*/).join('_').downcase
111
+ end
112
+
113
+ private
114
+
115
+ def inherited(descendant)
116
+ super(descendant)
117
+ @descendant_registry[:klass].send(@descendant_registry[:name]).add descendant
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end