rvgp 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +23 -0
- data/LICENSE +504 -0
- data/README.md +223 -0
- data/Rakefile +32 -0
- data/bin/rvgp +8 -0
- data/lib/rvgp/application/config.rb +159 -0
- data/lib/rvgp/application/descendant_registry.rb +122 -0
- data/lib/rvgp/application/status_output.rb +139 -0
- data/lib/rvgp/application.rb +170 -0
- data/lib/rvgp/base/command.rb +457 -0
- data/lib/rvgp/base/grid.rb +531 -0
- data/lib/rvgp/base/reader.rb +29 -0
- data/lib/rvgp/base/reconciler.rb +434 -0
- data/lib/rvgp/base/validation.rb +261 -0
- data/lib/rvgp/commands/cashflow.rb +160 -0
- data/lib/rvgp/commands/grid.rb +70 -0
- data/lib/rvgp/commands/ireconcile.rb +95 -0
- data/lib/rvgp/commands/new_project.rb +296 -0
- data/lib/rvgp/commands/plot.rb +41 -0
- data/lib/rvgp/commands/publish_gsheets.rb +83 -0
- data/lib/rvgp/commands/reconcile.rb +58 -0
- data/lib/rvgp/commands/rotate_year.rb +202 -0
- data/lib/rvgp/commands/validate_journal.rb +59 -0
- data/lib/rvgp/commands/validate_system.rb +44 -0
- data/lib/rvgp/commands.rb +160 -0
- data/lib/rvgp/dashboard.rb +252 -0
- data/lib/rvgp/fakers/fake_feed.rb +245 -0
- data/lib/rvgp/fakers/fake_journal.rb +57 -0
- data/lib/rvgp/fakers/fake_reconciler.rb +88 -0
- data/lib/rvgp/fakers/faker_helpers.rb +25 -0
- data/lib/rvgp/gem.rb +80 -0
- data/lib/rvgp/journal/commodity.rb +453 -0
- data/lib/rvgp/journal/complex_commodity.rb +214 -0
- data/lib/rvgp/journal/currency.rb +101 -0
- data/lib/rvgp/journal/journal.rb +141 -0
- data/lib/rvgp/journal/posting.rb +156 -0
- data/lib/rvgp/journal/pricer.rb +267 -0
- data/lib/rvgp/journal.rb +24 -0
- data/lib/rvgp/plot/gnuplot.rb +478 -0
- data/lib/rvgp/plot/google-drive/output_csv.rb +44 -0
- data/lib/rvgp/plot/google-drive/output_google_sheets.rb +434 -0
- data/lib/rvgp/plot/google-drive/sheet.rb +67 -0
- data/lib/rvgp/plot.rb +293 -0
- data/lib/rvgp/pta/hledger.rb +237 -0
- data/lib/rvgp/pta/ledger.rb +308 -0
- data/lib/rvgp/pta.rb +311 -0
- data/lib/rvgp/reconcilers/csv_reconciler.rb +424 -0
- data/lib/rvgp/reconcilers/journal_reconciler.rb +41 -0
- data/lib/rvgp/reconcilers/shorthand/finance_gem_hacks.rb +48 -0
- data/lib/rvgp/reconcilers/shorthand/international_atm.rb +152 -0
- data/lib/rvgp/reconcilers/shorthand/investment.rb +144 -0
- data/lib/rvgp/reconcilers/shorthand/mortgage.rb +195 -0
- data/lib/rvgp/utilities/grid_query.rb +190 -0
- data/lib/rvgp/utilities/yaml.rb +131 -0
- data/lib/rvgp/utilities.rb +44 -0
- data/lib/rvgp/validations/balance_validation.rb +68 -0
- data/lib/rvgp/validations/duplicate_tags_validation.rb +48 -0
- data/lib/rvgp/validations/uncategorized_validation.rb +15 -0
- data/lib/rvgp.rb +66 -0
- data/resources/README.MD/2022-cashflow-google.png +0 -0
- data/resources/README.MD/2022-cashflow.png +0 -0
- data/resources/README.MD/all-wealth-growth-google.png +0 -0
- data/resources/README.MD/all-wealth-growth.png +0 -0
- data/resources/gnuplot/default.yml +80 -0
- data/resources/i18n/en.yml +192 -0
- data/resources/iso-4217-currencies.json +171 -0
- data/resources/skel/Rakefile +5 -0
- data/resources/skel/app/grids/cashflow_grid.rb +27 -0
- data/resources/skel/app/grids/monthly_income_and_expenses_grid.rb +25 -0
- data/resources/skel/app/grids/wealth_growth_grid.rb +35 -0
- data/resources/skel/app/plots/cashflow.yml +33 -0
- data/resources/skel/app/plots/monthly-income-and-expenses.yml +17 -0
- data/resources/skel/app/plots/wealth-growth.yml +20 -0
- data/resources/skel/config/csv-format-acme-checking.yml +9 -0
- data/resources/skel/config/google-secrets.yml +5 -0
- data/resources/skel/config/rvgp.yml +0 -0
- data/resources/skel/journals/prices.db +0 -0
- data/rvgp.gemspec +6 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty.xml +383 -0
- data/test/assets/ledger_total_monthly_liabilities_with_empty2.xml +428 -0
- data/test/test_command_base.rb +61 -0
- data/test/test_commodity.rb +270 -0
- data/test/test_csv_reconciler.rb +60 -0
- data/test/test_currency.rb +24 -0
- data/test/test_fake_feed.rb +228 -0
- data/test/test_fake_journal.rb +98 -0
- data/test/test_fake_reconciler.rb +60 -0
- data/test/test_journal_parse.rb +545 -0
- data/test/test_ledger.rb +102 -0
- data/test/test_plot.rb +133 -0
- data/test/test_posting.rb +50 -0
- data/test/test_pricer.rb +139 -0
- data/test/test_pta_adapter.rb +575 -0
- data/test/test_utilities.rb +45 -0
- 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,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
|