ledger-rest 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +57 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +17 -0
- data/Rakefile +1 -0
- data/config.ru +5 -0
- data/ledger-rest.gemspec +19 -0
- data/ledger-rest.org +14 -0
- data/lib/ledger-rest.rb +11 -0
- data/lib/ledger-rest/app.rb +124 -0
- data/lib/ledger-rest/core_ext.rb +23 -0
- data/lib/ledger-rest/git.rb +68 -0
- data/lib/ledger-rest/ledger.rb +87 -0
- data/lib/ledger-rest/ledger/balance.rb +44 -0
- data/lib/ledger-rest/ledger/budget.rb +33 -0
- data/lib/ledger-rest/ledger/entry.rb +27 -0
- data/lib/ledger-rest/ledger/parser.rb +178 -0
- data/lib/ledger-rest/ledger/register.rb +39 -0
- data/lib/ledger-rest/ledger/transaction.rb +99 -0
- data/lib/ledger-rest/version.rb +3 -0
- data/spec/ledger-rest/ledger/parser_spec.rb +364 -0
- data/spec/ledger-rest/ledger/transaction_spec.rb +41 -0
- data/spec/spec_helper.rb +9 -0
- metadata +78 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.0.8)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
escape (0.0.4)
|
7
|
+
ffi (1.2.0)
|
8
|
+
git (1.2.5)
|
9
|
+
guard (1.5.4)
|
10
|
+
listen (>= 0.4.2)
|
11
|
+
lumberjack (>= 1.0.2)
|
12
|
+
pry (>= 0.9.10)
|
13
|
+
thor (>= 0.14.6)
|
14
|
+
guard-rspec (2.1.2)
|
15
|
+
guard (>= 1.1)
|
16
|
+
rspec (~> 2.11)
|
17
|
+
listen (0.6.0)
|
18
|
+
lumberjack (1.0.2)
|
19
|
+
method_source (0.8.1)
|
20
|
+
pry (0.9.10)
|
21
|
+
coderay (~> 1.0.5)
|
22
|
+
method_source (~> 0.8)
|
23
|
+
slop (~> 3.3.1)
|
24
|
+
rack (1.4.1)
|
25
|
+
rack-protection (1.2.0)
|
26
|
+
rack
|
27
|
+
rake (0.9.2.2)
|
28
|
+
rb-inotify (0.8.8)
|
29
|
+
ffi (>= 0.5.0)
|
30
|
+
rspec (2.12.0)
|
31
|
+
rspec-core (~> 2.12.0)
|
32
|
+
rspec-expectations (~> 2.12.0)
|
33
|
+
rspec-mocks (~> 2.12.0)
|
34
|
+
rspec-core (2.12.0)
|
35
|
+
rspec-expectations (2.12.0)
|
36
|
+
diff-lcs (~> 1.1.3)
|
37
|
+
rspec-mocks (2.12.0)
|
38
|
+
sinatra (1.3.3)
|
39
|
+
rack (~> 1.3, >= 1.3.6)
|
40
|
+
rack-protection (~> 1.2)
|
41
|
+
tilt (~> 1.3, >= 1.3.3)
|
42
|
+
slop (3.3.3)
|
43
|
+
thor (0.16.0)
|
44
|
+
tilt (1.3.3)
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
escape
|
51
|
+
git
|
52
|
+
guard-rspec
|
53
|
+
pry
|
54
|
+
rake
|
55
|
+
rb-inotify
|
56
|
+
rspec
|
57
|
+
sinatra
|
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara features specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Max Wolter,Arthur Andersen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# ledger-rest - REST assured your finances are in good hands ;-)
|
2
|
+
|
3
|
+
ledger-rest is a REST webservice to access your ledger account data
|
4
|
+
|
5
|
+
## What already works
|
6
|
+
|
7
|
+
* balance reports (``/balance``)
|
8
|
+
* register reports (``/register``)
|
9
|
+
* budget reports (``/budget``)
|
10
|
+
* version query (``/version``)
|
11
|
+
|
12
|
+
## What you need
|
13
|
+
|
14
|
+
* ruby
|
15
|
+
* sinatra
|
16
|
+
* escape
|
17
|
+
* ledger
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/config.ru
ADDED
data/ledger-rest.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ledger-rest/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "ledger-rest"
|
8
|
+
gem.version = LedgerRest::VERSION
|
9
|
+
gem.authors = ["Prof. MAAD", "Arthur Andersen"]
|
10
|
+
gem.email = ["leoc.git@gmail.com"]
|
11
|
+
gem.description = %q{Provide a REST web service for ledger.}
|
12
|
+
gem.summary = %q{Ledger via REST.}
|
13
|
+
gem.homepage = "https://github.com/leoc/ledger-rest"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
data/ledger-rest.org
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
* TODO ledger-rest
|
2
|
+
** DONE move responsibilities into classes
|
3
|
+
CLOSED: [2012-11-22 Thu 19:03]
|
4
|
+
- State "DONE" from "TODO" [2012-11-22 Thu 19:03]
|
5
|
+
** TODO implement entry GET
|
6
|
+
** TODO implement entry POST
|
7
|
+
** TODO implement transactions GET method
|
8
|
+
Get all transactions.
|
9
|
+
** TODO implement transactions POST method
|
10
|
+
Create a new transaction in the respective file.
|
11
|
+
** TODO implement transactions PUT method
|
12
|
+
Modify a transaction in the respective file.
|
13
|
+
** TODO implement transactions DELETE method
|
14
|
+
Remove a transaction from the respective file.
|
data/lib/ledger-rest.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'ledger-rest/ledger'
|
3
|
+
require 'ledger-rest/ledger/balance'
|
4
|
+
require 'ledger-rest/ledger/transaction'
|
5
|
+
require 'ledger-rest/ledger/register'
|
6
|
+
require 'ledger-rest/ledger/budget'
|
7
|
+
require 'ledger-rest/ledger/entry'
|
8
|
+
require 'ledger-rest/git'
|
9
|
+
require 'ledger-rest/core_ext'
|
10
|
+
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
module LedgerRest
|
14
|
+
class App < Sinatra::Base
|
15
|
+
CONFIG_FILE = "ledger-rest.yml"
|
16
|
+
|
17
|
+
configure do |c|
|
18
|
+
begin
|
19
|
+
config = YAML.load_file(CONFIG_FILE)
|
20
|
+
config.symbolize_keys!
|
21
|
+
rescue Exception => e
|
22
|
+
puts "Failed to load config."
|
23
|
+
end
|
24
|
+
|
25
|
+
Ledger.configure config
|
26
|
+
Git.configure config
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/version' do
|
30
|
+
content_type :json
|
31
|
+
{
|
32
|
+
"version" => LedgerRest::VERSION,
|
33
|
+
"ledger-version" => Ledger.version
|
34
|
+
}.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/balance/?:query?' do
|
38
|
+
content_type :json
|
39
|
+
Ledger::Balance.json(params[:query])
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/budget/?:query?' do
|
43
|
+
content_type :json
|
44
|
+
Ledger::Budget.json(params[:query])
|
45
|
+
end
|
46
|
+
|
47
|
+
get '/register/?:query?' do
|
48
|
+
content_type :json
|
49
|
+
Ledger::Register.json(params[:query])
|
50
|
+
end
|
51
|
+
|
52
|
+
get '/accounts/?:query?' do
|
53
|
+
content_type :json
|
54
|
+
Ledger.accounts(params[:query]).to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
get '/payees/?:query?' do
|
58
|
+
content_type :json
|
59
|
+
Ledger.payees(params[:query]).to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
# gets a potential new entry via the entry command
|
63
|
+
get '/transactions/entry/?:desc?' do
|
64
|
+
content_type :json
|
65
|
+
{ :transaction => Ledger::Entry.get(params[:desc]) }.to_json
|
66
|
+
end
|
67
|
+
|
68
|
+
# creates a new entry based on the
|
69
|
+
post '/transactions/entry/?:desc?' do
|
70
|
+
content_type :json
|
71
|
+
{ :transaction => Ledger::Entry.append(params[:desc]) }.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
get '/transactions/:meta' do
|
75
|
+
"Not yet implemented!"
|
76
|
+
end
|
77
|
+
|
78
|
+
get '/transactions' do
|
79
|
+
"Not yet implemented!"
|
80
|
+
end
|
81
|
+
|
82
|
+
post '/transactions' do
|
83
|
+
content_type :json
|
84
|
+
begin
|
85
|
+
params = JSON.parse(params[:transaction], :symbolize_names => true)
|
86
|
+
|
87
|
+
transaction = Transaction.create params
|
88
|
+
|
89
|
+
raise "Verification error" unless transaction.valid?
|
90
|
+
|
91
|
+
Git.invoke :before_write
|
92
|
+
|
93
|
+
@@ledger.append transaction
|
94
|
+
|
95
|
+
Git.invoke :after_write
|
96
|
+
|
97
|
+
[201, { transaction: transaction }.to_json]
|
98
|
+
rescue JSON::ParserError => e
|
99
|
+
[422,
|
100
|
+
{
|
101
|
+
:error => true,
|
102
|
+
:message => "Unprocessible Entity: '#{e}'"
|
103
|
+
}.to_json
|
104
|
+
]
|
105
|
+
rescue RuntimeError => e
|
106
|
+
[400,
|
107
|
+
{
|
108
|
+
:error => true,
|
109
|
+
:message => "Adding the transaction failed: '#{e}'"
|
110
|
+
}.to_json
|
111
|
+
]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
put '/transactions/:meta' do
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
delete '/transactions/:meta' do
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Hash
|
2
|
+
def symbolize_keys
|
3
|
+
hash = {}
|
4
|
+
self.each_pair do |key, val|
|
5
|
+
hash[key.to_sym] = val
|
6
|
+
end
|
7
|
+
hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def symbolize_keys!
|
11
|
+
self.keys.each do |key|
|
12
|
+
self[key.to_sym] = self.delete key
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def inject acc, &block
|
18
|
+
self.each_pair do |key, val|
|
19
|
+
acc = block.call acc, key, val
|
20
|
+
end
|
21
|
+
acc
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module LedgerRest
|
2
|
+
class Git
|
3
|
+
class << self
|
4
|
+
attr_reader :repository, :remote, :branch, :read_pull_block_time
|
5
|
+
|
6
|
+
def configure options
|
7
|
+
@repository = options[:git_repository] or File.dirname(options[:ledger_file] || 'main.ledger')
|
8
|
+
@pull_before_read = options[:git_pull_before_read] || false
|
9
|
+
@pull_before_write = options[:git_pull_before_write] || false
|
10
|
+
@push_after_write = options[:git_push_after_write] || false
|
11
|
+
@remote = options[:git_remote] || 'origin'
|
12
|
+
@branch = options[:git_branch] || 'master'
|
13
|
+
@read_pull_block_time = options[:git_read_pull_block_time] || 10*60
|
14
|
+
|
15
|
+
@last_read_pull = Time.new
|
16
|
+
if pull_before_read? or pull_before_write? or push_after_write?
|
17
|
+
@git_repo = ::Git.open(repository)
|
18
|
+
FileUtils.touch(options[:ledger_append_file])
|
19
|
+
@git_repo.add(options[:ledger_append_file])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def invoke hook
|
24
|
+
case hook
|
25
|
+
when :before_read
|
26
|
+
pull if pull_before_read? and not blocked?
|
27
|
+
when :before_write
|
28
|
+
pull if pull_before_write?
|
29
|
+
when :after_write
|
30
|
+
push if push_after_write?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def blocked_read_pull?
|
35
|
+
(Time.new - @last_read_pull) > read_pull_block_time
|
36
|
+
end
|
37
|
+
|
38
|
+
def pull_before_read?
|
39
|
+
@pull_before_read
|
40
|
+
end
|
41
|
+
|
42
|
+
def pull_before_write?
|
43
|
+
@pull_before_write
|
44
|
+
end
|
45
|
+
|
46
|
+
def push_after_write?
|
47
|
+
@push_after_write
|
48
|
+
end
|
49
|
+
|
50
|
+
# Execute the pull command.
|
51
|
+
def pull
|
52
|
+
@git_repo.pull(remote, branch)
|
53
|
+
@last_read_pull = Time.new
|
54
|
+
rescue Exception => e
|
55
|
+
$stderr.puts "Git pull failed: #{e}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Execute the push command after commiting all.
|
59
|
+
def push
|
60
|
+
@git_repo.commit_all("transaction added via ledger-rest")
|
61
|
+
@git_repo.push(remote, branch)
|
62
|
+
rescue Exception => e
|
63
|
+
$stderr.put "Git push failed: #{e}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'ledger-rest/ledger/parser'
|
3
|
+
|
4
|
+
module LedgerRest
|
5
|
+
class Ledger
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_accessor :rcfile, :bin, :file, :append_file, :home
|
9
|
+
|
10
|
+
def configure(options)
|
11
|
+
@bin = options[:ledger_bin] || "/usr/bin/ledger"
|
12
|
+
@file = options[:ledger_file] || ENV['LEDGER_FILE']
|
13
|
+
@append_file = options[:ledger_append_file] || ENV['LEDGER_FILE']
|
14
|
+
@home = options[:ledger_home] || '' # Return a budget representation
|
15
|
+
end
|
16
|
+
|
17
|
+
# Execute ledger command with given parameters
|
18
|
+
def exec(cmd, params = {})
|
19
|
+
Git.invoke :before_read
|
20
|
+
|
21
|
+
params = {
|
22
|
+
'-f' => @file
|
23
|
+
}.merge(params)
|
24
|
+
|
25
|
+
params = params.inject '' do |acc, key, val|
|
26
|
+
acc << " #{key} #{Escape.shell_single_word(val)}"
|
27
|
+
end
|
28
|
+
|
29
|
+
command = "#{bin} #{params} #{cmd}"
|
30
|
+
|
31
|
+
puts command
|
32
|
+
`#{command}`.rstrip
|
33
|
+
end
|
34
|
+
|
35
|
+
# Append a new transaction to the append_file.
|
36
|
+
def append transaction
|
37
|
+
File.open(append_file, "a+") do |f|
|
38
|
+
if f.pos == 0
|
39
|
+
last_char = "\n"
|
40
|
+
else
|
41
|
+
f.pos = f.pos-1
|
42
|
+
last_char = f.getc
|
43
|
+
end
|
44
|
+
|
45
|
+
f.write "\n" unless last_char == "\n"
|
46
|
+
f.write(transaction_string)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the ledger version.
|
51
|
+
def version
|
52
|
+
exec("--version").match(/^Ledger (.*),/)[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get a new transaction entry based on previous entries found
|
56
|
+
# in the append file.
|
57
|
+
def entry description
|
58
|
+
result = exec "entry #{description}", '-f' => append_file
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return a list of payees.
|
64
|
+
def payees(query = nil)
|
65
|
+
result = exec "payees #{query if query}"
|
66
|
+
{ :payees => result.split("\n") }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns an Array of payees with their respective usage count.
|
70
|
+
def payees_with_usage
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return an array of accounts.
|
75
|
+
def accounts(query = nil)
|
76
|
+
result = exec "accounts #{query if query}"
|
77
|
+
{ :accounts => result.split("\n") }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return an Array of accounts with their respective usage count.
|
81
|
+
def accounts_with_usage
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|