money_tracking 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +61 -0
- data/Rakefile +2 -0
- data/bin/money +6 -0
- data/lib/money_tracking.rb +6 -0
- data/lib/money_tracking/cli.rb +88 -0
- data/lib/money_tracking/cli/commands.rb +7 -0
- data/lib/money_tracking/cli/create_command.rb +21 -0
- data/lib/money_tracking/cli/delete_command.rb +15 -0
- data/lib/money_tracking/cli/list_command.rb +16 -0
- data/lib/money_tracking/cli/runner.rb +54 -0
- data/lib/money_tracking/cli/update_command.rb +33 -0
- data/lib/money_tracking/cli/views.rb +15 -0
- data/lib/money_tracking/cli/views/empty.rb +11 -0
- data/lib/money_tracking/cli/views/expense_created.rb +11 -0
- data/lib/money_tracking/cli/views/expense_deleted.rb +15 -0
- data/lib/money_tracking/cli/views/expense_item.rb +34 -0
- data/lib/money_tracking/cli/views/expense_list.rb +11 -0
- data/lib/money_tracking/cli/views/expense_not_found.rb +15 -0
- data/lib/money_tracking/cli/views/expense_not_updated.rb +19 -0
- data/lib/money_tracking/cli/views/expense_updated.rb +15 -0
- data/lib/money_tracking/data_store/file_store.rb +81 -0
- data/lib/money_tracking/data_store/protocol.rb +20 -0
- data/lib/money_tracking/domain.rb +12 -0
- data/lib/money_tracking/domain/expense.rb +92 -0
- data/lib/money_tracking/domain/expense_factory.rb +15 -0
- data/lib/money_tracking/domain/expense_finder.rb +13 -0
- data/lib/money_tracking/version.rb +3 -0
- data/money_tracking.gemspec +25 -0
- data/spec/aruba_helper.rb +31 -0
- data/spec/integration/cli_spec.rb +129 -0
- data/spec/money_tracking/cli/create_command_spec.rb +38 -0
- data/spec/money_tracking/cli/delete_command_spec.rb +44 -0
- data/spec/money_tracking/cli/list_command_spec.rb +34 -0
- data/spec/money_tracking/cli/update_command_spec.rb +92 -0
- data/spec/money_tracking/cli/views/expense_item_spec.rb +60 -0
- data/spec/money_tracking/cli/views/expense_list_spec.rb +40 -0
- data/spec/money_tracking/data_store/acts_as_a_data_store.rb +144 -0
- data/spec/money_tracking/data_store/file_store_spec.rb +16 -0
- data/spec/money_tracking/domain/expense_factory_spec.rb +58 -0
- data/spec/money_tracking/domain/expense_finder_spec.rb +59 -0
- data/spec/money_tracking/domain/expense_spec.rb +125 -0
- data/spec/spec_helper.rb +32 -0
- metadata +148 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module MoneyTracking
|
2
|
+
module Cli
|
3
|
+
module Views
|
4
|
+
class ExpenseItem < Struct.new(:expense)
|
5
|
+
def to_s
|
6
|
+
"#{id} - #{created_at}: #{amount} #{currency} #{tags}".strip
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def id; expense[:id] end
|
12
|
+
def currency; expense[:currency] end
|
13
|
+
|
14
|
+
def created_at
|
15
|
+
time_value(expense[:created_at])
|
16
|
+
end
|
17
|
+
|
18
|
+
def amount
|
19
|
+
sprintf("%.2f", expense[:amount].to_f.round(2))
|
20
|
+
end
|
21
|
+
|
22
|
+
def tags
|
23
|
+
return "" if expense[:tags].empty?
|
24
|
+
"[#{expense[:tags].sort.join(" ")}]"
|
25
|
+
end
|
26
|
+
|
27
|
+
def time_value(value)
|
28
|
+
return value unless value.is_a?(Time)
|
29
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module MoneyTracking
|
2
|
+
module Cli
|
3
|
+
module Views
|
4
|
+
class ExpenseNotUpdated < Struct.new(:expense)
|
5
|
+
def self.not_found
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(expense)
|
10
|
+
raise Error.new(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"Not updated."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module MoneyTracking
|
5
|
+
module DataStore
|
6
|
+
class FileStore < Protocol
|
7
|
+
def initialize(dir)
|
8
|
+
@dir = dir
|
9
|
+
`mkdir -p #{dir}`
|
10
|
+
end
|
11
|
+
|
12
|
+
def create(fields)
|
13
|
+
Record.new(dir, fields).save.id
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(id)
|
17
|
+
Record.new(dir).load(id).fields
|
18
|
+
rescue Errno::ENOENT
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def list
|
23
|
+
Record.list(dir).map { |record| record.fields }
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(id, fields)
|
27
|
+
Record.new(dir).load(id).update(fields)
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(id)
|
31
|
+
Record.new(dir).load(id).delete
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_accessor :dir
|
37
|
+
|
38
|
+
class Record < Struct.new(:dir, :fields)
|
39
|
+
def self.list(dir)
|
40
|
+
Dir["#{dir}/*.yml"]
|
41
|
+
.map { |path| File.basename(path, ".yml") }
|
42
|
+
.map { |id| new(dir).load(id) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def id
|
46
|
+
@id ||= SecureRandom.hex(4)
|
47
|
+
end
|
48
|
+
|
49
|
+
def load(id)
|
50
|
+
@id = id
|
51
|
+
self.fields = YAML.load_file(filename).merge(id: id)
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def save
|
56
|
+
File.open(filename, "w") { |f| f.write(to_yaml) }
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def update(fields)
|
61
|
+
self.fields = fields
|
62
|
+
save
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete
|
66
|
+
File.delete(filename)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def to_yaml
|
72
|
+
fields.to_yaml
|
73
|
+
end
|
74
|
+
|
75
|
+
def filename
|
76
|
+
"#{dir}/#{id}.yml"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module MoneyTracking
|
2
|
+
module Domain
|
3
|
+
class Expense
|
4
|
+
SIMPLE_FIELDS = %i[amount currency]
|
5
|
+
|
6
|
+
def initialize(store, fields)
|
7
|
+
@store = store
|
8
|
+
@fields = fields
|
9
|
+
@id = fields[:id]
|
10
|
+
end
|
11
|
+
|
12
|
+
def create
|
13
|
+
set_created_at
|
14
|
+
@id = store.create(raw)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(updated_fields)
|
19
|
+
simple_update(updated_fields)
|
20
|
+
update_tags(updated_fields)
|
21
|
+
store.update(id, raw)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete
|
26
|
+
store.delete(id)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_view(view_factory)
|
31
|
+
view_factory.new(raw)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :store, :fields, :id
|
37
|
+
|
38
|
+
def raw
|
39
|
+
return fields unless id
|
40
|
+
fields.merge(id: id)
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_created_at
|
44
|
+
fields[:created_at] ||= Time.now
|
45
|
+
end
|
46
|
+
|
47
|
+
def simple_update(updated_fields)
|
48
|
+
SIMPLE_FIELDS.each do |name|
|
49
|
+
fields[name] = updated_fields[name] if updated_fields[name]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_tags(updated_fields)
|
54
|
+
fields[:tags] = TagUpdate.new(
|
55
|
+
fields[:tags],
|
56
|
+
updated_fields[:add_tags],
|
57
|
+
updated_fields[:rm_tags],
|
58
|
+
).value
|
59
|
+
end
|
60
|
+
|
61
|
+
class TagUpdate < Struct.new(:tags, :add_tags, :rm_tags)
|
62
|
+
def value
|
63
|
+
tags - rm_tags + add_tags
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def rm_tags
|
69
|
+
super || []
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_tags
|
73
|
+
super || []
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class ExpenseNotFound
|
79
|
+
def build_view(view_factory)
|
80
|
+
view_factory.not_found.new(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
def update(updated_fields)
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MoneyTracking
|
2
|
+
module Domain
|
3
|
+
class ExpenseFactory < Struct.new(:store)
|
4
|
+
def create(raw_expense)
|
5
|
+
build(raw_expense).create
|
6
|
+
end
|
7
|
+
|
8
|
+
def build(raw_expense, id = nil)
|
9
|
+
return ExpenseNotFound.new unless raw_expense
|
10
|
+
return build(raw_expense.merge(id: id)) if id
|
11
|
+
Expense.new(store, raw_expense)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module MoneyTracking
|
2
|
+
module Domain
|
3
|
+
class ExpenseFinder < Struct.new(:store, :expense_factory)
|
4
|
+
def list
|
5
|
+
store.list.map { |raw| expense_factory.build(raw) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def read(expense_id)
|
9
|
+
expense_factory.build(store.read(expense_id), expense_id)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'money_tracking/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "money_tracking"
|
8
|
+
spec.version = MoneyTracking::VERSION
|
9
|
+
spec.authors = ["Oleksii Fedorov"]
|
10
|
+
spec.email = ["waterlink000@gmail.com"]
|
11
|
+
spec.summary = %q{CLI tool for tracking your expenses.}
|
12
|
+
spec.description = %q{CLI tool for tracking your expenses.}
|
13
|
+
spec.homepage = "https://github.com/waterlink/money_tracking"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "thor"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "aruba/api"
|
2
|
+
require "aruba/reporting"
|
3
|
+
require "aruba/in_process"
|
4
|
+
|
5
|
+
require "money_tracking/cli/runner"
|
6
|
+
|
7
|
+
Aruba::InProcess.main_class = MoneyTracking::Cli::Runner
|
8
|
+
Aruba.process = Aruba::InProcess
|
9
|
+
|
10
|
+
# https://github.com/cucumber/aruba/issues/236
|
11
|
+
class Aruba::InProcess
|
12
|
+
def output
|
13
|
+
stdout + stderr
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ENV["TEST_HOME"] ||= "#{Dir.getwd}/tmp/aruba/"
|
18
|
+
|
19
|
+
module ArubaHelper
|
20
|
+
include Aruba::Api
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.before(:each) do
|
24
|
+
@aruba_io_wait_seconds = 5
|
25
|
+
@aruba_timeout_seconds = 5
|
26
|
+
|
27
|
+
restore_env
|
28
|
+
clean_current_dir
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "aruba_helper"
|
2
|
+
|
3
|
+
RSpec.describe "Cli presentation layer" do
|
4
|
+
include ArubaHelper
|
5
|
+
|
6
|
+
example "Basic example" do
|
7
|
+
money "expenses list"
|
8
|
+
expect(output).to match("Empty.")
|
9
|
+
|
10
|
+
money "expenses", "create", "73.9", "euro", "food"
|
11
|
+
expect(output).to match(%r{Created new expense with id ([\d\w]{8}).})
|
12
|
+
|
13
|
+
id = output.scan(/[\d\w]{8}/)[0]
|
14
|
+
|
15
|
+
money "expenses", "list"
|
16
|
+
expect(output).to match(
|
17
|
+
%r{#{id} - ....-..-.. ..:..:..: 73\.90 euro \[food\]}
|
18
|
+
)
|
19
|
+
|
20
|
+
money "expenses", "update", id, "--amount", "73.95"
|
21
|
+
expect(output).to match("Updated expense #{id}.")
|
22
|
+
|
23
|
+
money "expenses", "list"
|
24
|
+
expect(output).to match(
|
25
|
+
%r{#{id} - ....-..-.. ..:..:..: 73\.95 euro \[food\]}
|
26
|
+
)
|
27
|
+
|
28
|
+
money "expenses", "delete", id
|
29
|
+
expect(output).to match("Deleted expense #{id}.")
|
30
|
+
|
31
|
+
money "expenses list"
|
32
|
+
expect(output).to match("Empty.")
|
33
|
+
end
|
34
|
+
|
35
|
+
example "Not found on update" do
|
36
|
+
money "expenses", "update", "hello_world", "--amount", "99.99", expected_exit_status: 1
|
37
|
+
expect(output).to match("Not found.")
|
38
|
+
end
|
39
|
+
|
40
|
+
example "Not found on delete" do
|
41
|
+
money "expenses", "delete", "hello_world", expected_exit_status: 1
|
42
|
+
expect(output).to match("Not found.")
|
43
|
+
end
|
44
|
+
|
45
|
+
example "Not updated on update" do
|
46
|
+
money "expenses", "create", "25", "euro", "internet"
|
47
|
+
id = output.scan(/[\d\w]{8}/)[0]
|
48
|
+
|
49
|
+
money "expenses", "update", id, expected_exit_status: 1
|
50
|
+
expect(output).to match("Not updated.")
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "update functionality" do
|
54
|
+
let!(:id) do
|
55
|
+
money "expenses", "create", "75.39", "euro", "food", "italian"
|
56
|
+
output.scan(/[\d\w]{8}/)[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
example "updating amount only" do
|
60
|
+
money "expenses", "update", id, "--amount", "74.99"
|
61
|
+
money "expenses", "list"
|
62
|
+
expect(output).to match(
|
63
|
+
%r{#{id} - ....-..-.. ..:..:..: 74\.99 euro \[food italian\]}
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
example "updating currency only" do
|
68
|
+
money "expenses", "update", id, "--currency", "eur"
|
69
|
+
money "expenses", "list"
|
70
|
+
expect(output).to match(
|
71
|
+
%r{#{id} - ....-..-.. ..:..:..: 75\.39 eur \[food italian\]}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
example "adding tags only" do
|
76
|
+
money "expenses", "update", id, "--add-tags", "tasty", "pricey"
|
77
|
+
money "expenses", "list"
|
78
|
+
expect(output).to match(
|
79
|
+
%r{#{id} - ....-..-.. ..:..:..: 75\.39 euro \[food italian pricey tasty\]}
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
example "removing tags only" do
|
84
|
+
money "expenses", "update", id, "--rm-tags", "food"
|
85
|
+
money "expenses", "list"
|
86
|
+
expect(output).to match(
|
87
|
+
%r{#{id} - ....-..-.. ..:..:..: 75\.39 euro \[italian\]}
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
example "removing and adding tags only" do
|
92
|
+
money "expenses", "update", id, "--rm-tags", "food", "--add-tags", "restaurant", "awesome"
|
93
|
+
money "expenses", "list"
|
94
|
+
expect(output).to match(
|
95
|
+
%r{#{id} - ....-..-.. ..:..:..: 75\.39 euro \[awesome italian restaurant\]}
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
example "updating all fields at once" do
|
100
|
+
money "expenses", "update", id,
|
101
|
+
"--rm-tags", "food",
|
102
|
+
"--amount", "95.49",
|
103
|
+
"--currency=dollar",
|
104
|
+
"--add-tags", "restaurant", "awesome"
|
105
|
+
|
106
|
+
money "expenses", "list"
|
107
|
+
expect(output).to match(
|
108
|
+
%r{#{id} - ....-..-.. ..:..:..: 95\.49 dollar \[awesome italian restaurant\]}
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def money(*args, expected_exit_status: 0)
|
116
|
+
command = money_command(*args)
|
117
|
+
run_simple(command, false)
|
118
|
+
@last_output = output_from(command)
|
119
|
+
assert_exit_status(expected_exit_status)
|
120
|
+
end
|
121
|
+
|
122
|
+
def money_command(*args)
|
123
|
+
"money #{args.join(" ")}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def output
|
127
|
+
@last_output
|
128
|
+
end
|
129
|
+
end
|