instance_accountant 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +12 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/bin/instance_accountant +10 -0
- data/instance_accountant.gemspec +43 -0
- data/lib/instance_accountant/cli.rb +123 -0
- data/lib/instance_accountant/filer.rb +37 -0
- data/lib/instance_accountant/hour.rb +28 -0
- data/lib/instance_accountant/journal_entry.rb +116 -0
- data/lib/instance_accountant/poster.rb +52 -0
- data/lib/instance_accountant.rb +8 -0
- data/spec/helpers/minitest.rb +83 -0
- data/spec/helpers/rspec.rb +25 -0
- data/spec/helpers.rb +9 -0
- data/spec/instance_accountant/cli_spec.rb +7 -0
- data/spec/instance_accountant/filer_spec.rb +72 -0
- data/spec/instance_accountant/hour_spec.rb +136 -0
- data/spec/instance_accountant/journal_entry_spec.rb +178 -0
- data/spec/instance_accountant/poster_spec.rb +109 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 659797969092d787410c4789f8379c33b9b10ccb
|
4
|
+
data.tar.gz: 0c858e6a0c0a6ae6f7bec5877cbbf450f6de665c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a442d471310a881effe735f4b7d6ff5118af02e238e20e908178e5e845409cc51a4c123e08e9f0acebac655badec7cc0fbd4278a7e8a21d20c95d03132015af6
|
7
|
+
data.tar.gz: cb5df07e2c58e9905134bf3d1683846f1ddeaa41cffb503b68e6dc6c99d5fb36a7119693c31e86553cf64a0e2b71ac3093636bf527a796528a68c17e0141f872
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright (c) 2014, Subledger, Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
|
8
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
9
|
+
|
10
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
11
|
+
|
12
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
Subledger instance accountant Ruby gem
|
2
|
+
|
3
|
+
Installation:
|
4
|
+
|
5
|
+
gem install instance_accountant
|
6
|
+
|
7
|
+
Gemfile:
|
8
|
+
|
9
|
+
gem 'instance_accountant'
|
10
|
+
|
11
|
+
The gem provides an executable that can be invoked two ways:
|
12
|
+
|
13
|
+
If you want to account for the hourly cost of instances, use this form:
|
14
|
+
|
15
|
+
instance_accountant account \
|
16
|
+
--description 'instance usage for: %s' \
|
17
|
+
--reference 'http://test.com/journal_entry' \
|
18
|
+
--cost 0.1 \
|
19
|
+
--cost_description 'cost for: %s' \
|
20
|
+
--cost_reference 'http://test.com/cost' \
|
21
|
+
--expense_acct subledger_account_id \
|
22
|
+
--payable_acct subledger_account_id \
|
23
|
+
--key_id subledger_key_id \
|
24
|
+
--secret subledger_secret \
|
25
|
+
--org_id subledger_org_id \
|
26
|
+
--book_id subledger_book_id \
|
27
|
+
--daemon
|
28
|
+
|
29
|
+
If you want to account for both the hourly cost of instances, and hourly income instances, use this form:
|
30
|
+
|
31
|
+
instance_accountant account \
|
32
|
+
--description 'instance usage for: %s' \
|
33
|
+
--reference 'http://test.com/journal_entry' \
|
34
|
+
--cost 0.1 \
|
35
|
+
--cost_description 'cost for: %s' \
|
36
|
+
--cost_reference 'http://test.com/cost' \
|
37
|
+
--expense_acct subledger_account_id \
|
38
|
+
--payable_acct subledger_account_id \
|
39
|
+
--price 0.2 \
|
40
|
+
--price_description 'price for: %s' \
|
41
|
+
--price_reference 'http://test.com/price' \
|
42
|
+
--receivable_acct subledger_account_id \
|
43
|
+
--income_acct subledger_account_id \
|
44
|
+
--key_id subledger_key_id \
|
45
|
+
--secret subledger_secret \
|
46
|
+
--org_id subledger_org_id \
|
47
|
+
--book_id subledger_book_id \
|
48
|
+
--daemon
|
49
|
+
|
50
|
+
Note: %s in descriptions will be replaced by the ISO 8601 of the hour in UTC
|
51
|
+
|
52
|
+
Here's a complete set of options:
|
53
|
+
|
54
|
+
Options:
|
55
|
+
f, [--filepath=FILEPATH]
|
56
|
+
# Default: ~/.instance_accountant
|
57
|
+
d, --description=DESCRIPTION
|
58
|
+
# Default: instance usage for
|
59
|
+
[--reference=REFERENCE]
|
60
|
+
c, --cost=COST
|
61
|
+
[--cost-description=COST_DESCRIPTION]
|
62
|
+
# Default: cost for
|
63
|
+
[--cost-reference=COST_REFERENCE]
|
64
|
+
e, --expense-acct=EXPENSE_ACCT
|
65
|
+
p, --payable-acct=PAYABLE_ACCT
|
66
|
+
p, [--price=PRICE]
|
67
|
+
[--price-description=PRICE_DESCRIPTION]
|
68
|
+
# Default: price for
|
69
|
+
[--price-reference=PRICE_REFERENCE]
|
70
|
+
i, [--income-acct=INCOME_ACCT]
|
71
|
+
r, [--receivable-acct=RECEIVABLE_ACCT]
|
72
|
+
k, --key-id=KEY_ID
|
73
|
+
s, --secret=SECRET
|
74
|
+
o, --org-id=ORG_ID
|
75
|
+
b, --book-id=BOOK_ID
|
76
|
+
[--daemon], [--no-daemon]
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift 'lib'
|
4
|
+
|
5
|
+
require 'instance_accountant'
|
6
|
+
|
7
|
+
Gem::Specification.new do |gem|
|
8
|
+
gem.authors = [ 'Tom Mornini', 'John McKee' ]
|
9
|
+
|
10
|
+
gem.files = Dir.glob( 'lib/**/*.rb' )
|
11
|
+
|
12
|
+
gem.files << 'instance_accountant.gemspec'
|
13
|
+
gem.files << 'LICENSE.txt'
|
14
|
+
gem.files << 'Rakefile'
|
15
|
+
gem.files << 'README.md'
|
16
|
+
|
17
|
+
gem.test_files = Dir.glob( 'spec/**/*.rb' )
|
18
|
+
|
19
|
+
gem.name = 'instance_accountant'
|
20
|
+
gem.description = 'account for hourly instance usage using Subledger'
|
21
|
+
gem.summary = 'account for hourly instance usage using Subledger'
|
22
|
+
gem.version = InstanceAccountant::VERSION
|
23
|
+
gem.email = 'admin@subledger.com'
|
24
|
+
gem.licenses = ['BSD 3-Clause']
|
25
|
+
|
26
|
+
gem.homepage = 'https://github.com/subledger/instance_accountant'
|
27
|
+
|
28
|
+
gem.bindir = 'bin'
|
29
|
+
gem.executables = 'instance_accountant'
|
30
|
+
|
31
|
+
gem.add_runtime_dependency 'subledger', '~> 0.7'
|
32
|
+
gem.add_runtime_dependency 'thor', '~> 0.19'
|
33
|
+
|
34
|
+
gem.add_development_dependency 'guard', '~> 2.8'
|
35
|
+
gem.add_development_dependency 'guard-minitest', '~> 2.3'
|
36
|
+
gem.add_development_dependency 'guard-rubocop', '~> 1.1'
|
37
|
+
gem.add_development_dependency 'minitest', '~> 5.4'
|
38
|
+
gem.add_development_dependency 'rake', '~> 10.3'
|
39
|
+
gem.add_development_dependency 'rb-fsevent', '~> 0.9'
|
40
|
+
gem.add_development_dependency 'rspec-expectations', '~> 3.1'
|
41
|
+
gem.add_development_dependency 'rspec-mocks', '~> 3.1'
|
42
|
+
gem.add_development_dependency 'rubocop', '~> 0.27'
|
43
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
require 'instance_accountant'
|
5
|
+
|
6
|
+
module InstanceAccountant
|
7
|
+
class CLI < Thor
|
8
|
+
desc 'account', 'Accounts for hours in Subledger'
|
9
|
+
|
10
|
+
method_option :filepath,
|
11
|
+
aliases: :f,
|
12
|
+
default: '~/.instance_accountant'
|
13
|
+
|
14
|
+
method_option :description,
|
15
|
+
aliases: :d,
|
16
|
+
required: true,
|
17
|
+
default: 'instance usage for'
|
18
|
+
|
19
|
+
method_option :reference
|
20
|
+
|
21
|
+
method_option :cost,
|
22
|
+
aliases: :c,
|
23
|
+
required: true
|
24
|
+
method_option :cost_description,
|
25
|
+
default: 'cost for'
|
26
|
+
|
27
|
+
method_option :cost_reference
|
28
|
+
|
29
|
+
method_option :expense_acct,
|
30
|
+
aliases: :e,
|
31
|
+
required: true
|
32
|
+
|
33
|
+
method_option :payable_acct,
|
34
|
+
aliases: :p,
|
35
|
+
required: true
|
36
|
+
|
37
|
+
method_option :price,
|
38
|
+
aliases: :p
|
39
|
+
|
40
|
+
method_option :price_description,
|
41
|
+
default: 'price for'
|
42
|
+
|
43
|
+
method_option :price_reference
|
44
|
+
|
45
|
+
method_option :income_acct,
|
46
|
+
aliases: :i
|
47
|
+
|
48
|
+
method_option :receivable_acct,
|
49
|
+
aliases: :r
|
50
|
+
|
51
|
+
method_option :key_id,
|
52
|
+
aliases: :k,
|
53
|
+
required: true
|
54
|
+
|
55
|
+
method_option :secret,
|
56
|
+
aliases: :s,
|
57
|
+
required: true
|
58
|
+
|
59
|
+
method_option :org_id,
|
60
|
+
aliases: :o,
|
61
|
+
required: true
|
62
|
+
|
63
|
+
method_option :book_id,
|
64
|
+
aliases: :b,
|
65
|
+
required: true
|
66
|
+
|
67
|
+
method_option :daemon, type: :boolean
|
68
|
+
|
69
|
+
SIGNALS = []
|
70
|
+
|
71
|
+
TWENTY_MINUTES = 20 * 60
|
72
|
+
|
73
|
+
%w(ABRT ALRM HUP INT STOP TERM QUIT).each do |signal|
|
74
|
+
Signal.trap signal do
|
75
|
+
SIGNALS << signal
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def account
|
80
|
+
if price_but_not_income_and_receivable options
|
81
|
+
STDERR.puts '--price requires --income_acct and --receivable_acct'
|
82
|
+
exit 0
|
83
|
+
end
|
84
|
+
|
85
|
+
i = 0
|
86
|
+
|
87
|
+
loop do
|
88
|
+
consider_posting i, options
|
89
|
+
consider_exiting
|
90
|
+
sleep 1
|
91
|
+
i += 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def consider_posting i, options
|
98
|
+
Poster.new(options).post(Hour.new, Time.now) if posting_required? i
|
99
|
+
end
|
100
|
+
|
101
|
+
def posting_required? i
|
102
|
+
(i % TWENTY_MINUTES).zero? or signal?
|
103
|
+
end
|
104
|
+
|
105
|
+
def consider_exiting
|
106
|
+
exit 1 if signal?
|
107
|
+
exit 0 if not_daemon?
|
108
|
+
end
|
109
|
+
|
110
|
+
def signal?
|
111
|
+
SIGNALS.length > 0
|
112
|
+
end
|
113
|
+
|
114
|
+
def not_daemon?
|
115
|
+
options[:daemon].nil?
|
116
|
+
end
|
117
|
+
|
118
|
+
def price_but_not_income_and_receivable options
|
119
|
+
options[:price] and
|
120
|
+
! (options[:income_acct] and options[:receivable_acct])
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module InstanceAccountant
|
4
|
+
class Filer
|
5
|
+
def initialize options = { }
|
6
|
+
@file_klass = options[:file_klass] || File
|
7
|
+
@time_klass = options[:time_klass] || Time
|
8
|
+
|
9
|
+
@filepath = @file_klass.expand_path options[:filepath]
|
10
|
+
end
|
11
|
+
|
12
|
+
def read
|
13
|
+
@time_klass.parse @file_klass.open( @filepath,
|
14
|
+
File::RDWR | File::CREAT,
|
15
|
+
0644 ) do |file|
|
16
|
+
file.flock File::LOCK_EX
|
17
|
+
file.read
|
18
|
+
end
|
19
|
+
rescue
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def write hour
|
24
|
+
hour_string = hour.to_s
|
25
|
+
|
26
|
+
@file_klass.open( @filepath,
|
27
|
+
File::RDWR | File::CREAT,
|
28
|
+
0644 ) do |file|
|
29
|
+
file.flock File::LOCK_EX
|
30
|
+
file.rewind
|
31
|
+
file.write hour_string
|
32
|
+
end
|
33
|
+
|
34
|
+
hour_string
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module InstanceAccountant
|
5
|
+
class Hour
|
6
|
+
def initialize options = { }
|
7
|
+
@time = options[:time] ? options[:time].utc : now(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def == other
|
11
|
+
hour_string == other.hour_string
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@time.iso8601
|
16
|
+
end
|
17
|
+
|
18
|
+
def hour_string
|
19
|
+
to_s[0..12] + ':00:00Z'
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def now options
|
25
|
+
(options[:time_klass] || ::Time).now.utc
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'subledger'
|
3
|
+
|
4
|
+
module InstanceAccountant
|
5
|
+
class JournalEntry
|
6
|
+
def initialize options = { }
|
7
|
+
@options = options
|
8
|
+
|
9
|
+
@subledger = options[:subledger] || Subledger.new(@options.dup)
|
10
|
+
end
|
11
|
+
|
12
|
+
def hash_for hour, now
|
13
|
+
hour_string = hour.hour_string
|
14
|
+
|
15
|
+
lines = cost_lines hour_string
|
16
|
+
|
17
|
+
lines += price_lines(hour_string) if include_price_lines?
|
18
|
+
|
19
|
+
hash = {
|
20
|
+
effective_at: now,
|
21
|
+
description: description(hour_string),
|
22
|
+
lines: lines
|
23
|
+
}
|
24
|
+
|
25
|
+
hash.merge!(reference: @options[:reference]) if @options[:reference]
|
26
|
+
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def include_price_lines?
|
33
|
+
@options[:receivable_acct] and
|
34
|
+
@options[:income_acct] and
|
35
|
+
@options[:price]
|
36
|
+
end
|
37
|
+
|
38
|
+
def description hour_string
|
39
|
+
format @options[:description], hour_string
|
40
|
+
end
|
41
|
+
|
42
|
+
def cost_lines hour_string
|
43
|
+
lines = [ ]
|
44
|
+
|
45
|
+
common = { description: @options[:cost_description],
|
46
|
+
reference: @options[:cost_reference] }
|
47
|
+
|
48
|
+
lines << complete(
|
49
|
+
hour_string,
|
50
|
+
common.merge( account: expense_acct,
|
51
|
+
value: @subledger.debit(@options[:cost]) ) )
|
52
|
+
|
53
|
+
lines << complete(
|
54
|
+
hour_string,
|
55
|
+
common.merge( account: payable_acct,
|
56
|
+
value: @subledger.credit(@options[:cost]) ) )
|
57
|
+
|
58
|
+
lines
|
59
|
+
end
|
60
|
+
|
61
|
+
def price_lines hour_string
|
62
|
+
lines = [ ]
|
63
|
+
|
64
|
+
common = { description: @options[:price_description],
|
65
|
+
reference: @options[:price_reference] }
|
66
|
+
|
67
|
+
lines << complete(
|
68
|
+
hour_string,
|
69
|
+
common.merge( account: receivable_acct,
|
70
|
+
value: @subledger.debit(@options[:price]) ) )
|
71
|
+
|
72
|
+
lines << complete(
|
73
|
+
hour_string,
|
74
|
+
common.merge( account: income_acct,
|
75
|
+
value: @subledger.credit(@options[:price]) ) )
|
76
|
+
|
77
|
+
lines
|
78
|
+
end
|
79
|
+
|
80
|
+
def complete hour_string, args
|
81
|
+
description = args[:description].nil? ? nil : description(hour_string)
|
82
|
+
|
83
|
+
if description
|
84
|
+
args.merge! description: description
|
85
|
+
else
|
86
|
+
args.delete :description
|
87
|
+
end
|
88
|
+
|
89
|
+
reference = args[:reference]
|
90
|
+
|
91
|
+
if reference
|
92
|
+
args.merge! reference: reference
|
93
|
+
else
|
94
|
+
args.delete :reference
|
95
|
+
end
|
96
|
+
|
97
|
+
args
|
98
|
+
end
|
99
|
+
|
100
|
+
def expense_acct
|
101
|
+
@subledger.account id: @options[:expense_acct]
|
102
|
+
end
|
103
|
+
|
104
|
+
def payable_acct
|
105
|
+
@subledger.account id: @options[:payable_acct]
|
106
|
+
end
|
107
|
+
|
108
|
+
def receivable_acct
|
109
|
+
@subledger.account id: @options[:receivable_acct]
|
110
|
+
end
|
111
|
+
|
112
|
+
def income_acct
|
113
|
+
@subledger.account id: @options[:income_acct]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'subledger'
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
require 'instance_accountant/filer'
|
7
|
+
require 'instance_accountant/hour'
|
8
|
+
require 'instance_accountant/journal_entry'
|
9
|
+
|
10
|
+
module InstanceAccountant
|
11
|
+
class Poster
|
12
|
+
def initialize options = { }
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
@time_klass = @options[:time_klass] || Time
|
16
|
+
@hour_klass = @options[:hour_klass] || Hour
|
17
|
+
|
18
|
+
@subledger = @options[:subledger] || Subledger.new(@options.dup)
|
19
|
+
@filer = @options[:filer] || Filer.new(@options)
|
20
|
+
@journal_entry = @options[:journal_entry] || JournalEntry.new(@options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def post hour, now
|
24
|
+
if posted? hour
|
25
|
+
false
|
26
|
+
else
|
27
|
+
write hour, now
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def posted? hour
|
35
|
+
file_time = @filer.read
|
36
|
+
|
37
|
+
if file_time.nil?
|
38
|
+
false
|
39
|
+
else
|
40
|
+
@hour_klass.new( time: file_time ) == hour
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def write hour, now
|
45
|
+
@subledger
|
46
|
+
.journal_entry
|
47
|
+
.create_and_post( @journal_entry.hash_for hour, now )
|
48
|
+
|
49
|
+
@filer.write hour
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
gem 'minitest'
|
5
|
+
|
6
|
+
require 'minitest/spec'
|
7
|
+
|
8
|
+
require 'minitest/pride' unless ENV['SUBLEDGER_ENV'] == 'ci'
|
9
|
+
|
10
|
+
require 'minitest/autorun'
|
11
|
+
|
12
|
+
MiniTest::Test.parallelize_me!
|
13
|
+
|
14
|
+
module Kernel
|
15
|
+
def Class thing, &block
|
16
|
+
describe_set_subject thing, &block
|
17
|
+
end
|
18
|
+
|
19
|
+
def Constant thing, &block
|
20
|
+
describe_set_subject thing, &block
|
21
|
+
end
|
22
|
+
|
23
|
+
def Module thing, &block
|
24
|
+
describe_set_subject thing, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def Instance &block
|
28
|
+
describe 'instance', &block
|
29
|
+
end
|
30
|
+
|
31
|
+
def RespondsTo desc, &block
|
32
|
+
describe "responds to #{desc}", &block
|
33
|
+
end
|
34
|
+
|
35
|
+
def When desc, &block
|
36
|
+
describe "when #{desc}", &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def And desc, &block
|
40
|
+
describe "and #{desc}", &block
|
41
|
+
end
|
42
|
+
|
43
|
+
def RoleTest desc
|
44
|
+
load Subledger::ROOT + "/spec/roles/#{desc}_spec.rb"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Thanks Anton Lindqvist!
|
50
|
+
# https://github.com/mptre/minitest-implicit-subject
|
51
|
+
|
52
|
+
def describe_set_subject *args, &block
|
53
|
+
cls = describe *args, &block
|
54
|
+
|
55
|
+
subject = args.first
|
56
|
+
|
57
|
+
if subject.respond_to?(:included_modules) &&
|
58
|
+
Array(subject.included_modules).include?(Singleton)
|
59
|
+
|
60
|
+
subject = subject.instance
|
61
|
+
end
|
62
|
+
|
63
|
+
cls.subject { subject }
|
64
|
+
|
65
|
+
cls
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module MiniTest
|
70
|
+
class Spec
|
71
|
+
def self.ByRaising desc, &block
|
72
|
+
it "by raising #{desc}", &block
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.ByReturning desc, &block
|
76
|
+
it "by returning #{desc}", &block
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.ByYielding desc, &block
|
80
|
+
it "by yielding #{desc}", &block
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'rspec/expectations'
|
3
|
+
require 'rspec/mocks'
|
4
|
+
|
5
|
+
module MiniTest
|
6
|
+
class Test
|
7
|
+
include ::RSpec::Mocks::ExampleMethods
|
8
|
+
|
9
|
+
def before_setup
|
10
|
+
::RSpec::Mocks.setup
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_teardown
|
15
|
+
super
|
16
|
+
::RSpec::Mocks.verify
|
17
|
+
ensure
|
18
|
+
::RSpec::Mocks.teardown
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Spec
|
23
|
+
include RSpec::Matchers
|
24
|
+
end
|
25
|
+
end
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'helpers'
|
3
|
+
|
4
|
+
require 'instance_accountant/filer'
|
5
|
+
|
6
|
+
module InstanceAccountant
|
7
|
+
Class File do
|
8
|
+
Instance do
|
9
|
+
subject { Filer.new options }
|
10
|
+
|
11
|
+
let(:options) do
|
12
|
+
{
|
13
|
+
filepath: filepath,
|
14
|
+
file_klass: file_klass,
|
15
|
+
time_klass: time_klass
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:filepath) { double 'filepath' }
|
20
|
+
let(:expanded_filepath) { double 'expanded_filepath' }
|
21
|
+
let(:file_klass) { double 'file_klass' }
|
22
|
+
let(:time_klass) { double 'time_klass' }
|
23
|
+
let(:time_inst) { double 'time_inst' }
|
24
|
+
|
25
|
+
let(:time_string) { double 'time_string' }
|
26
|
+
let(:hour) { double 'hour' }
|
27
|
+
|
28
|
+
RespondsTo :read do
|
29
|
+
When 'it has been written before' do
|
30
|
+
ByReturning 'a Time' do
|
31
|
+
expect(file_klass)
|
32
|
+
.to receive(:expand_path)
|
33
|
+
.with(filepath)
|
34
|
+
.and_return(expanded_filepath)
|
35
|
+
|
36
|
+
expect(file_klass)
|
37
|
+
.to receive(:open)
|
38
|
+
.with(expanded_filepath, File::RDWR | File::CREAT, 0644)
|
39
|
+
.and_return(time_string)
|
40
|
+
|
41
|
+
expect(time_klass)
|
42
|
+
.to receive(:parse)
|
43
|
+
.with(time_string)
|
44
|
+
.and_return(time_inst)
|
45
|
+
|
46
|
+
subject.read.must_be_same_as time_inst
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
RespondsTo :write do
|
52
|
+
ByReturning 'the hour' do
|
53
|
+
expect(file_klass)
|
54
|
+
.to receive(:expand_path)
|
55
|
+
.with(filepath)
|
56
|
+
.and_return(expanded_filepath)
|
57
|
+
|
58
|
+
expect(hour)
|
59
|
+
.to receive(:to_s)
|
60
|
+
.and_return(time_string)
|
61
|
+
|
62
|
+
expect(file_klass)
|
63
|
+
.to receive(:open)
|
64
|
+
.with(expanded_filepath, File::RDWR | File::CREAT, 0644)
|
65
|
+
.and_return(time_string)
|
66
|
+
|
67
|
+
subject.write(hour).must_be_same_as time_string
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'helpers'
|
3
|
+
|
4
|
+
require 'instance_accountant/hour'
|
5
|
+
|
6
|
+
module InstanceAccountant
|
7
|
+
Class Hour do
|
8
|
+
Instance do
|
9
|
+
subject { Hour.new options }
|
10
|
+
|
11
|
+
let(:options) do
|
12
|
+
{
|
13
|
+
time_klass: time_klass
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:time_klass) { double 'time_klass' }
|
18
|
+
|
19
|
+
let(:time_local) { double 'time_local' }
|
20
|
+
let(:time_utc) { double 'time_utc' }
|
21
|
+
let(:time_iso) { double 'time_iso' }
|
22
|
+
let(:hour_string) { double 'hour_string' }
|
23
|
+
|
24
|
+
let(:other_hour) { double 'other_hour' }
|
25
|
+
let(:other_hour_string) { double 'other_hour_string' }
|
26
|
+
|
27
|
+
let(:other_time_local) { double 'other_time_local' }
|
28
|
+
let(:other_time_utc) { double 'other_time_utc' }
|
29
|
+
let(:other_time_iso) { double 'other_time_iso' }
|
30
|
+
|
31
|
+
RespondsTo :to_s do
|
32
|
+
ByReturning 'an hour string' do
|
33
|
+
expect(time_klass)
|
34
|
+
.to receive(:now)
|
35
|
+
.and_return(time_local)
|
36
|
+
|
37
|
+
expect(time_local)
|
38
|
+
.to receive(:utc)
|
39
|
+
.and_return(time_utc)
|
40
|
+
|
41
|
+
expect(time_utc)
|
42
|
+
.to receive(:iso8601)
|
43
|
+
.and_return(time_iso)
|
44
|
+
|
45
|
+
subject.to_s.must_equal time_iso
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
RespondsTo :hour_string do
|
50
|
+
ByReturning 'an hour string' do
|
51
|
+
expect(time_klass)
|
52
|
+
.to receive(:now)
|
53
|
+
.and_return(time_local)
|
54
|
+
|
55
|
+
expect(time_local)
|
56
|
+
.to receive(:utc)
|
57
|
+
.and_return(time_utc)
|
58
|
+
|
59
|
+
expect(time_utc)
|
60
|
+
.to receive(:iso8601)
|
61
|
+
.and_return('2014-11-05T00')
|
62
|
+
|
63
|
+
subject.hour_string.must_equal '2014-11-05T00:00:00Z'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
RespondsTo :== do
|
68
|
+
When 'other is equal' do
|
69
|
+
ByReturning true do
|
70
|
+
|
71
|
+
expect(time_klass)
|
72
|
+
.to receive(:now)
|
73
|
+
.and_return(time_local)
|
74
|
+
|
75
|
+
expect(time_local)
|
76
|
+
.to receive(:utc)
|
77
|
+
.and_return(time_utc)
|
78
|
+
|
79
|
+
expect(time_utc)
|
80
|
+
.to receive(:iso8601)
|
81
|
+
.and_return(time_iso)
|
82
|
+
|
83
|
+
expect(time_iso)
|
84
|
+
.to receive(:[])
|
85
|
+
.with(0..12)
|
86
|
+
.and_return(hour_string)
|
87
|
+
|
88
|
+
expect(other_hour)
|
89
|
+
.to receive(:hour_string)
|
90
|
+
.and_return(hour_string)
|
91
|
+
|
92
|
+
expect(hour_string)
|
93
|
+
.to receive(:+)
|
94
|
+
.with(':00:00Z')
|
95
|
+
.and_return(hour_string)
|
96
|
+
|
97
|
+
(subject == other_hour).must_equal true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
When 'other is not equal' do
|
102
|
+
ByReturning false do
|
103
|
+
|
104
|
+
expect(time_klass)
|
105
|
+
.to receive(:now)
|
106
|
+
.and_return(time_local)
|
107
|
+
|
108
|
+
expect(time_local)
|
109
|
+
.to receive(:utc)
|
110
|
+
.and_return(time_utc)
|
111
|
+
|
112
|
+
expect(time_utc)
|
113
|
+
.to receive(:iso8601)
|
114
|
+
.and_return(time_iso)
|
115
|
+
|
116
|
+
expect(time_iso)
|
117
|
+
.to receive(:[])
|
118
|
+
.with(0..12)
|
119
|
+
.and_return(hour_string)
|
120
|
+
|
121
|
+
expect(hour_string)
|
122
|
+
.to receive(:+)
|
123
|
+
.with(':00:00Z')
|
124
|
+
.and_return(hour_string)
|
125
|
+
|
126
|
+
expect(other_hour)
|
127
|
+
.to receive(:hour_string)
|
128
|
+
.and_return(other_hour_string)
|
129
|
+
|
130
|
+
(subject == other_hour).must_equal false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'helpers'
|
3
|
+
|
4
|
+
require 'instance_accountant/journal_entry'
|
5
|
+
|
6
|
+
module InstanceAccountant
|
7
|
+
Class JournalEntry do
|
8
|
+
Instance do
|
9
|
+
let(:options) do
|
10
|
+
{
|
11
|
+
subledger: subledger,
|
12
|
+
description: description,
|
13
|
+
cost: cost,
|
14
|
+
expense_acct: expense_acct,
|
15
|
+
payable_acct: payable_acct
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:price_options) do
|
20
|
+
{
|
21
|
+
price: price,
|
22
|
+
income_acct: income_acct,
|
23
|
+
receivable_acct: receivable_acct
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:subledger) { double 'subledger' }
|
28
|
+
let(:description) { 'description with time %s' }
|
29
|
+
|
30
|
+
let(:cost) { double 'cost' }
|
31
|
+
let(:expense_acct) { double 'expense_acct' }
|
32
|
+
let(:payable_acct) { double 'payable_acct' }
|
33
|
+
|
34
|
+
let(:price) { double 'price' }
|
35
|
+
let(:income_acct) { double 'income_acct' }
|
36
|
+
let(:receivable_acct) { double 'receivable_acct' }
|
37
|
+
|
38
|
+
let(:expense_debit) { double 'expense_debit' }
|
39
|
+
let(:payable_credit) { double 'payable_credit' }
|
40
|
+
|
41
|
+
let(:receivable_debit) { double 'receivable_debit' }
|
42
|
+
let(:income_credit) { double 'income_credit' }
|
43
|
+
|
44
|
+
let(:concat_desc) { 'description with time 2014-11-05T18:00:00Z' }
|
45
|
+
|
46
|
+
let(:hour) { double 'hour' }
|
47
|
+
let(:hour_string) { '2014-11-05T18:00:00Z' }
|
48
|
+
|
49
|
+
let(:now) { double 'now' }
|
50
|
+
|
51
|
+
RespondsTo :hash_for do
|
52
|
+
When 'price lines are not included' do
|
53
|
+
subject { JournalEntry.new options }
|
54
|
+
|
55
|
+
ByReturning true do
|
56
|
+
expect(hour)
|
57
|
+
.to receive(:hour_string)
|
58
|
+
.and_return(hour_string)
|
59
|
+
|
60
|
+
expect(subledger)
|
61
|
+
.to receive(:account)
|
62
|
+
.with(id: expense_acct)
|
63
|
+
.and_return(expense_acct)
|
64
|
+
|
65
|
+
expect(subledger)
|
66
|
+
.to receive(:debit)
|
67
|
+
.with(cost)
|
68
|
+
.and_return(expense_debit)
|
69
|
+
|
70
|
+
expect(subledger)
|
71
|
+
.to receive(:account)
|
72
|
+
.with(id: payable_acct)
|
73
|
+
.and_return(payable_acct)
|
74
|
+
|
75
|
+
expect(subledger)
|
76
|
+
.to receive(:credit)
|
77
|
+
.with(cost)
|
78
|
+
.and_return(payable_credit)
|
79
|
+
|
80
|
+
# expect(description)
|
81
|
+
# .to receive(:+)
|
82
|
+
# .and_return(concat_desc)
|
83
|
+
|
84
|
+
subject.hash_for(hour, now)
|
85
|
+
.must_equal effective_at: now,
|
86
|
+
description: concat_desc,
|
87
|
+
lines: [
|
88
|
+
{
|
89
|
+
account: expense_acct,
|
90
|
+
value: expense_debit
|
91
|
+
},
|
92
|
+
{
|
93
|
+
account: payable_acct,
|
94
|
+
value: payable_credit
|
95
|
+
}
|
96
|
+
]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
When 'price lines are included' do
|
101
|
+
subject { JournalEntry.new options.merge price_options }
|
102
|
+
|
103
|
+
ByReturning true do
|
104
|
+
expect(hour)
|
105
|
+
.to receive(:hour_string)
|
106
|
+
.and_return(hour_string)
|
107
|
+
|
108
|
+
expect(subledger)
|
109
|
+
.to receive(:account)
|
110
|
+
.with(id: expense_acct)
|
111
|
+
.and_return(expense_acct)
|
112
|
+
|
113
|
+
expect(subledger)
|
114
|
+
.to receive(:debit)
|
115
|
+
.with(cost)
|
116
|
+
.and_return(expense_debit)
|
117
|
+
|
118
|
+
expect(subledger)
|
119
|
+
.to receive(:account)
|
120
|
+
.with(id: payable_acct)
|
121
|
+
.and_return(payable_acct)
|
122
|
+
|
123
|
+
expect(subledger)
|
124
|
+
.to receive(:credit)
|
125
|
+
.with(cost)
|
126
|
+
.and_return(payable_credit)
|
127
|
+
|
128
|
+
expect(subledger)
|
129
|
+
.to receive(:account)
|
130
|
+
.with(id: receivable_acct)
|
131
|
+
.and_return(receivable_acct)
|
132
|
+
|
133
|
+
expect(subledger)
|
134
|
+
.to receive(:debit)
|
135
|
+
.with(price)
|
136
|
+
.and_return(receivable_debit)
|
137
|
+
|
138
|
+
expect(subledger)
|
139
|
+
.to receive(:account)
|
140
|
+
.with(id: income_acct)
|
141
|
+
.and_return(income_acct)
|
142
|
+
|
143
|
+
expect(subledger)
|
144
|
+
.to receive(:credit)
|
145
|
+
.with(price)
|
146
|
+
.and_return(income_credit)
|
147
|
+
|
148
|
+
# expect(description)
|
149
|
+
# .to receive(:+)
|
150
|
+
# .and_return(concat_desc)
|
151
|
+
|
152
|
+
subject.hash_for(hour, now)
|
153
|
+
.must_equal effective_at: now,
|
154
|
+
description: concat_desc,
|
155
|
+
lines: [
|
156
|
+
{
|
157
|
+
account: expense_acct,
|
158
|
+
value: expense_debit
|
159
|
+
},
|
160
|
+
{
|
161
|
+
account: payable_acct,
|
162
|
+
value: payable_credit
|
163
|
+
},
|
164
|
+
{
|
165
|
+
account: receivable_acct,
|
166
|
+
value: receivable_debit
|
167
|
+
},
|
168
|
+
{
|
169
|
+
account: income_acct,
|
170
|
+
value: income_credit
|
171
|
+
}
|
172
|
+
]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'helpers'
|
3
|
+
|
4
|
+
require 'instance_accountant/poster'
|
5
|
+
|
6
|
+
module InstanceAccountant
|
7
|
+
Class Poster do
|
8
|
+
Instance do
|
9
|
+
subject { Poster.new options }
|
10
|
+
|
11
|
+
let(:options) do
|
12
|
+
{
|
13
|
+
subledger: subledger,
|
14
|
+
filer: filer,
|
15
|
+
hour_klass: hour_klass,
|
16
|
+
journal_entry: journal_entry
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:subledger) { double 'subledger' }
|
21
|
+
let(:sl_journal_entry) { double 'sl_journal_entry' }
|
22
|
+
|
23
|
+
let(:filer) { double 'filer' }
|
24
|
+
let(:hour_klass) { double 'hour_klass' }
|
25
|
+
let(:journal_entry) { double 'journal_entry' }
|
26
|
+
|
27
|
+
let(:journal_entry_hash) { double 'journal_entry_hash' }
|
28
|
+
let(:hour) { double 'hour' }
|
29
|
+
let(:different_hour) { double 'different_hour' }
|
30
|
+
let(:now) { double 'now' }
|
31
|
+
|
32
|
+
RespondsTo :post do
|
33
|
+
When 'no hours have been posted' do
|
34
|
+
ByReturning true do
|
35
|
+
expect(filer)
|
36
|
+
.to receive(:read)
|
37
|
+
.and_return(nil)
|
38
|
+
|
39
|
+
expect(journal_entry)
|
40
|
+
.to receive(:hash_for)
|
41
|
+
.with(hour, now)
|
42
|
+
.and_return(journal_entry_hash)
|
43
|
+
|
44
|
+
expect(filer)
|
45
|
+
.to receive(:write)
|
46
|
+
.with(hour)
|
47
|
+
|
48
|
+
expect(subledger)
|
49
|
+
.to receive(:journal_entry)
|
50
|
+
.and_return(sl_journal_entry)
|
51
|
+
|
52
|
+
expect(sl_journal_entry)
|
53
|
+
.to receive(:create_and_post)
|
54
|
+
.with(journal_entry_hash)
|
55
|
+
|
56
|
+
subject.post(hour, now).must_equal true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
When 'the hour has not been posted' do
|
61
|
+
ByReturning true do
|
62
|
+
expect(filer)
|
63
|
+
.to receive(:read)
|
64
|
+
.and_return(different_hour)
|
65
|
+
|
66
|
+
expect(hour_klass)
|
67
|
+
.to receive(:new)
|
68
|
+
.with(time: different_hour)
|
69
|
+
.and_return(different_hour)
|
70
|
+
|
71
|
+
expect(journal_entry)
|
72
|
+
.to receive(:hash_for)
|
73
|
+
.with(hour, now)
|
74
|
+
.and_return(journal_entry_hash)
|
75
|
+
|
76
|
+
expect(subledger)
|
77
|
+
.to receive(:journal_entry)
|
78
|
+
.and_return(sl_journal_entry)
|
79
|
+
|
80
|
+
expect(sl_journal_entry)
|
81
|
+
.to receive(:create_and_post)
|
82
|
+
.with(journal_entry_hash)
|
83
|
+
|
84
|
+
expect(filer)
|
85
|
+
.to receive(:write)
|
86
|
+
.with(hour)
|
87
|
+
|
88
|
+
subject.post(hour, now).must_equal true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
When 'the hour has been posted' do
|
93
|
+
ByReturning false do
|
94
|
+
expect(filer)
|
95
|
+
.to receive(:read)
|
96
|
+
.and_return(hour)
|
97
|
+
|
98
|
+
expect(hour_klass)
|
99
|
+
.to receive(:new)
|
100
|
+
.with(time: hour)
|
101
|
+
.and_return(hour)
|
102
|
+
|
103
|
+
subject.post(hour, now).must_equal false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: instance_accountant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Mornini
|
8
|
+
- John McKee
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-05 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: subledger
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0.7'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0.7'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: thor
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.19'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.19'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: guard
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '2.8'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '2.8'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: guard-minitest
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '2.3'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.3'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: guard-rubocop
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.1'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.1'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: minitest
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '5.4'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '5.4'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rake
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '10.3'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '10.3'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rb-fsevent
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0.9'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0.9'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rspec-expectations
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '3.1'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '3.1'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: rspec-mocks
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '3.1'
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - "~>"
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '3.1'
|
154
|
+
- !ruby/object:Gem::Dependency
|
155
|
+
name: rubocop
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - "~>"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0.27'
|
161
|
+
type: :development
|
162
|
+
prerelease: false
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0.27'
|
168
|
+
description: account for hourly instance usage using Subledger
|
169
|
+
email: admin@subledger.com
|
170
|
+
executables:
|
171
|
+
- instance_accountant
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- LICENSE.txt
|
176
|
+
- README.md
|
177
|
+
- Rakefile
|
178
|
+
- bin/instance_accountant
|
179
|
+
- instance_accountant.gemspec
|
180
|
+
- lib/instance_accountant.rb
|
181
|
+
- lib/instance_accountant/cli.rb
|
182
|
+
- lib/instance_accountant/filer.rb
|
183
|
+
- lib/instance_accountant/hour.rb
|
184
|
+
- lib/instance_accountant/journal_entry.rb
|
185
|
+
- lib/instance_accountant/poster.rb
|
186
|
+
- spec/helpers.rb
|
187
|
+
- spec/helpers/minitest.rb
|
188
|
+
- spec/helpers/rspec.rb
|
189
|
+
- spec/instance_accountant/cli_spec.rb
|
190
|
+
- spec/instance_accountant/filer_spec.rb
|
191
|
+
- spec/instance_accountant/hour_spec.rb
|
192
|
+
- spec/instance_accountant/journal_entry_spec.rb
|
193
|
+
- spec/instance_accountant/poster_spec.rb
|
194
|
+
homepage: https://github.com/subledger/instance_accountant
|
195
|
+
licenses:
|
196
|
+
- BSD 3-Clause
|
197
|
+
metadata: {}
|
198
|
+
post_install_message:
|
199
|
+
rdoc_options: []
|
200
|
+
require_paths:
|
201
|
+
- lib
|
202
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: '0'
|
207
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
requirements: []
|
213
|
+
rubyforge_project:
|
214
|
+
rubygems_version: 2.4.2
|
215
|
+
signing_key:
|
216
|
+
specification_version: 4
|
217
|
+
summary: account for hourly instance usage using Subledger
|
218
|
+
test_files:
|
219
|
+
- spec/helpers/minitest.rb
|
220
|
+
- spec/helpers/rspec.rb
|
221
|
+
- spec/helpers.rb
|
222
|
+
- spec/instance_accountant/cli_spec.rb
|
223
|
+
- spec/instance_accountant/filer_spec.rb
|
224
|
+
- spec/instance_accountant/hour_spec.rb
|
225
|
+
- spec/instance_accountant/journal_entry_spec.rb
|
226
|
+
- spec/instance_accountant/poster_spec.rb
|