subledger 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE.txt +12 -0
  3. data/README.md +136 -0
  4. data/Rakefile +5 -0
  5. data/bin/subledger +6 -0
  6. data/lib/subledger/actor.rb +32 -0
  7. data/lib/subledger/collection_name.rb +25 -0
  8. data/lib/subledger/domain/account.rb +168 -0
  9. data/lib/subledger/domain/balance.rb +102 -0
  10. data/lib/subledger/domain/book.rb +111 -0
  11. data/lib/subledger/domain/category.rb +157 -0
  12. data/lib/subledger/domain/control.rb +180 -0
  13. data/lib/subledger/domain/formatters.rb +31 -0
  14. data/lib/subledger/domain/identity.rb +159 -0
  15. data/lib/subledger/domain/journal_entry.rb +293 -0
  16. data/lib/subledger/domain/key.rb +113 -0
  17. data/lib/subledger/domain/line.rb +272 -0
  18. data/lib/subledger/domain/org.rb +110 -0
  19. data/lib/subledger/domain/report.rb +247 -0
  20. data/lib/subledger/domain/report_rendering.rb +233 -0
  21. data/lib/subledger/domain/roles/activatable.rb +11 -0
  22. data/lib/subledger/domain/roles/archivable.rb +11 -0
  23. data/lib/subledger/domain/roles/attributable.rb +14 -0
  24. data/lib/subledger/domain/roles/collectable.rb +175 -0
  25. data/lib/subledger/domain/roles/creatable.rb +58 -0
  26. data/lib/subledger/domain/roles/describable.rb +33 -0
  27. data/lib/subledger/domain/roles/describable_report_rendering.rb +50 -0
  28. data/lib/subledger/domain/roles/identifiable.rb +15 -0
  29. data/lib/subledger/domain/roles/postable.rb +54 -0
  30. data/lib/subledger/domain/roles/progressable.rb +11 -0
  31. data/lib/subledger/domain/roles/readable.rb +34 -0
  32. data/lib/subledger/domain/roles/restable.rb +69 -0
  33. data/lib/subledger/domain/roles/storable.rb +30 -0
  34. data/lib/subledger/domain/roles/timeable.rb +18 -0
  35. data/lib/subledger/domain/roles/updatable.rb +35 -0
  36. data/lib/subledger/domain/roles/versionable.rb +35 -0
  37. data/lib/subledger/domain/roles.rb +16 -0
  38. data/lib/subledger/domain/value/credit.rb +16 -0
  39. data/lib/subledger/domain/value/debit.rb +16 -0
  40. data/lib/subledger/domain/value/zero.rb +24 -0
  41. data/lib/subledger/domain/value.rb +111 -0
  42. data/lib/subledger/domain.rb +95 -0
  43. data/lib/subledger/exception_handler.rb +65 -0
  44. data/lib/subledger/interface/client.rb +295 -0
  45. data/lib/subledger/interface/dispatcher.rb +20 -0
  46. data/lib/subledger/interface.rb +2 -0
  47. data/lib/subledger/path.rb +106 -0
  48. data/lib/subledger/rest.rb +128 -0
  49. data/lib/subledger/server.rb +3 -0
  50. data/lib/subledger/store/api/errors.rb +95 -0
  51. data/lib/subledger/store/api/roles/activate.rb +21 -0
  52. data/lib/subledger/store/api/roles/archive.rb +21 -0
  53. data/lib/subledger/store/api/roles/balance.rb +39 -0
  54. data/lib/subledger/store/api/roles/categories.rb +51 -0
  55. data/lib/subledger/store/api/roles/collect.rb +58 -0
  56. data/lib/subledger/store/api/roles/create.rb +26 -0
  57. data/lib/subledger/store/api/roles/create_and_post.rb +35 -0
  58. data/lib/subledger/store/api/roles/create_identity.rb +39 -0
  59. data/lib/subledger/store/api/roles/create_line.rb +24 -0
  60. data/lib/subledger/store/api/roles/first_and_last_line.rb +31 -0
  61. data/lib/subledger/store/api/roles/post.rb +25 -0
  62. data/lib/subledger/store/api/roles/progress.rb +21 -0
  63. data/lib/subledger/store/api/roles/read.rb +19 -0
  64. data/lib/subledger/store/api/roles/reports.rb +77 -0
  65. data/lib/subledger/store/api/roles/update.rb +24 -0
  66. data/lib/subledger/store/api/store.rb +103 -0
  67. data/lib/subledger/store/api.rb +20 -0
  68. data/lib/subledger/store.rb +236 -0
  69. data/lib/subledger/supervisor.rb +21 -0
  70. data/lib/subledger/uuid.rb +52 -0
  71. data/lib/subledger/version.rb +4 -0
  72. data/lib/subledger.rb +234 -0
  73. data/spec/spec_helper.rb +77 -0
  74. data/spec/subledger_account_spec.rb +354 -0
  75. data/spec/subledger_book_spec.rb +130 -0
  76. data/spec/subledger_category_spec.rb +203 -0
  77. data/spec/subledger_control_spec.rb +43 -0
  78. data/spec/subledger_identity_spec.rb +47 -0
  79. data/spec/subledger_journal_entry_spec.rb +417 -0
  80. data/spec/subledger_key_spec.rb +43 -0
  81. data/spec/subledger_org_spec.rb +68 -0
  82. data/spec/subledger_report_spec.rb +295 -0
  83. data/spec/subledger_spec.rb +101 -0
  84. data/subledger.gemspec +52 -0
  85. metadata +205 -0
@@ -0,0 +1,25 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ module Post
5
+ def post postable_journal_entry
6
+ path = Path.for_entity :anchor => postable_journal_entry
7
+
8
+ begin
9
+ json_body = http.post do |req|
10
+ req.url path
11
+
12
+ unless postable_journal_entry.post_delay.zero?
13
+ req.headers['X-Subledger-Post-Slowly'] = 'true'
14
+ end
15
+ end.body
16
+ rescue Exception => e
17
+ raise PostError, "Cannot post #{postable_journal_entry}: #{e}"
18
+ end
19
+
20
+ new_or_initialize json_body, postable_journal_entry
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ module Progress
5
+ def progress progressable
6
+
7
+ path = Path.for_entity( :anchor => progressable ) + '/progress'
8
+
9
+ begin
10
+ response_hash = parse_json(
11
+ http.get( path ).body )
12
+ rescue Exception => e
13
+ raise ProgressError, "Cannot progress #{progressable}: #{e}"
14
+ end
15
+
16
+ response_hash['progress']['percentage'].to_i
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ module Read
5
+ def read readable
6
+ path = Path.for_entity :anchor => readable
7
+
8
+ begin
9
+ json_body = http.get( path ).body
10
+ rescue Exception => e
11
+ raise ReadError, "Cannot read #{readable}: #{e}"
12
+ end
13
+
14
+ new_or_initialize json_body, readable
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,77 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ module Reports
5
+ def attach_category_to_report args
6
+ report = args[:report]
7
+ category = args[:category]
8
+ parent = args[:parent]
9
+
10
+ path = Path.for_entity( :anchor => report ) + '/attach'
11
+
12
+ attach_post_hash = { 'category' => category.id }
13
+
14
+ attach_post_hash['parent'] = parent.id unless parent.nil?
15
+
16
+ begin
17
+ json_body = http.post do |req|
18
+ req.url path
19
+ req.body = attach_post_hash
20
+ end.body
21
+ rescue Exception => e
22
+ raise AttachError, "Cannot attach #{category}: #{e}"
23
+ end
24
+
25
+ category
26
+ end
27
+
28
+ def detach_category_from_report args
29
+ category = args[:category]
30
+ report = args[:report]
31
+
32
+ path = Path.for_entity( :anchor => report ) + '/detach'
33
+
34
+ detach_post_hash = { 'category' => category.id }
35
+
36
+ begin
37
+ json_body = http.post do |req|
38
+ req.url path
39
+ req.body = detach_post_hash
40
+ end.body
41
+ rescue Exception => e
42
+ raise DetachError, "Cannot detach #{category}: #{e}"
43
+ end
44
+
45
+ category
46
+ end
47
+
48
+ def render args
49
+ at = args[:at].iso8601
50
+
51
+ building_report_rendering = args[:building_report_rendering]
52
+ report = building_report_rendering.report
53
+
54
+ path = Path.for_entity( :anchor => report ) + "/render?at=#{at}"
55
+
56
+ begin
57
+ json_body = http.post do |req|
58
+ req.url path
59
+ end.body
60
+ rescue Exception => e
61
+ raise ReportError, "Cannot render #{report}: #{e}"
62
+ end
63
+
64
+ new_or_initialize json_body, building_report_rendering
65
+ end
66
+
67
+ def collect_categories_for_report report, &block
68
+ raise ReportError, 'report#categories is not yet implemented'
69
+ end
70
+
71
+ def report_structure_for report, &block
72
+ raise ReportError, 'report#structure is not yet implemented'
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,24 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ module Update
5
+ def update updatable
6
+ path = Path.for_entity :anchor => updatable
7
+
8
+ begin
9
+ json_body = http.patch do |req|
10
+ req.url path
11
+ req.body = updatable.patch_hash
12
+ end.body
13
+ rescue UpdateConflictError => e
14
+ raise e
15
+ rescue Exception => e
16
+ raise UpdateError, "Cannot update #{updatable}: #{e}"
17
+ end
18
+
19
+ new_or_initialize json_body, updatable
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,103 @@
1
+ module Subledger
2
+ module Store
3
+ module Api
4
+ class Store
5
+
6
+ include Subledger::Store
7
+
8
+ include Balance
9
+ include Categories
10
+ include Collect
11
+ include Create
12
+ include CreateAndPost
13
+ include CreateLine
14
+ include FirstAndLastLine
15
+ include Post
16
+ include Progress
17
+ include Read
18
+ include Reports
19
+ include Update
20
+ include Activate
21
+ include Archive
22
+ include CreateIdentity
23
+
24
+ def add_initial_controls_for org
25
+ # Server handles this functionality
26
+ end
27
+
28
+ def add_initial_controls_to identity
29
+ # Server handles this functionality
30
+ end
31
+
32
+ def raise_unless_bucket_name_valid args
33
+ # No endpoints for this
34
+ end
35
+
36
+ def create_backup anchor
37
+ # No endpoints for this
38
+ end
39
+
40
+ def backup_exists? anchor
41
+ # No endpoints for this
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :http
47
+
48
+ def base_url
49
+ "#{SCHEME}#{DOMAIN}"
50
+ end
51
+
52
+ def setup
53
+ @http = Faraday.new :url => base_url do |conn|
54
+ unless auth_key.nil?
55
+ conn.basic_auth auth_key.id, auth_key.secret
56
+ end
57
+
58
+ conn.request :json
59
+ conn.response :json, :content_type => /^json$/
60
+ conn.use Subledger::Store::Api::Errors::ResponseError
61
+ conn.adapter Faraday.default_adapter
62
+ end
63
+ end
64
+
65
+ def parse_json json_body
66
+ MultiJson.load json_body
67
+ end
68
+
69
+ def klass_method_from parsed_json
70
+ parsed_json.keys.first
71
+ end
72
+
73
+ def new_or_initialize json_body, initializable
74
+ client = initializable.client
75
+
76
+ parsed_json = parse_json json_body
77
+
78
+ klass_method = klass_method_from parsed_json
79
+
80
+ response_hash = parsed_json[klass_method]
81
+
82
+ args = Rest.to_args response_hash, client
83
+
84
+ args.merge! :json => MultiJson.dump( response_hash )
85
+
86
+ if initializable.respond_to? :post_delay
87
+ args.merge! :post_delay => initializable.post_delay
88
+ end
89
+
90
+ new_item = client.send klass_method, args
91
+
92
+ if initializable.entity_name == klass_method.to_sym
93
+ initializable.send :initialize, new_item.attributes
94
+ else
95
+ initializable = new_item
96
+ end
97
+
98
+ initializable
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,20 @@
1
+ require 'faraday_middleware'
2
+
3
+ require 'subledger/store/api/roles/activate'
4
+ require 'subledger/store/api/roles/archive'
5
+ require 'subledger/store/api/roles/balance'
6
+ require 'subledger/store/api/roles/categories'
7
+ require 'subledger/store/api/roles/collect'
8
+ require 'subledger/store/api/roles/create'
9
+ require 'subledger/store/api/roles/create_and_post'
10
+ require 'subledger/store/api/roles/create_line'
11
+ require 'subledger/store/api/roles/first_and_last_line'
12
+ require 'subledger/store/api/roles/post'
13
+ require 'subledger/store/api/roles/progress'
14
+ require 'subledger/store/api/roles/read'
15
+ require 'subledger/store/api/roles/reports'
16
+ require 'subledger/store/api/roles/update'
17
+ require 'subledger/store/api/roles/create_identity'
18
+
19
+ require 'subledger/store/api/errors'
20
+ require 'subledger/store/api/store'
@@ -0,0 +1,236 @@
1
+ module Subledger
2
+ module Store
3
+
4
+ class ActivateError < Error; end
5
+ class ArchiveError < Error; end
6
+ class AttachError < Error; end
7
+ class BalanceError < Error; end
8
+ class CategoryError < Error; end
9
+ class CollectError < Error; end
10
+ class CreateError < Error; end
11
+ class CreateLineError < Error; end
12
+ class DeleteError < Error; end
13
+ class DetachError < Error; end
14
+ class FirstAndLastLineError < Error; end
15
+ class PostError < Error; end
16
+ class ProgressError < Error; end
17
+ class ReadError < Error; end
18
+ class ReportError < Error; end
19
+ class UpdateError < Error; end
20
+ class UpdateNotFoundError < Error; end
21
+ class UpdateConflictError < Error; end
22
+ class BucketValidatorError < Error; end
23
+
24
+ attr_reader :auth_key
25
+
26
+ def initialize args
27
+ @auth_key = args[:auth_key]
28
+
29
+ setup
30
+ end
31
+
32
+ def attributes
33
+ { :auth_key => auth_key }
34
+ end
35
+
36
+ def collect args
37
+ collection_name = args[:collection_name]
38
+ action = args[:action]
39
+ limit = args[:limit]
40
+ no_balances = args[:no_balances]
41
+
42
+ begin
43
+ if limit.nil? or limit < 1 or limit > 100
44
+ raise CollectError, ':limit must be 1-100'
45
+ end
46
+
47
+ collected = send action, args
48
+
49
+ if collection_name == :account_lines and not collected.empty?
50
+ if no_balances
51
+ collected.each do |line|
52
+ line.send :balance=, nil
53
+ end
54
+ else
55
+ args.merge! :account_lines => collected
56
+
57
+ set_account_line_balances args
58
+ end
59
+ end
60
+
61
+ collected
62
+ rescue Exception => e
63
+ raise CollectError, "Unable to collect #{collection_name}: #{e}"
64
+ end
65
+ end
66
+
67
+ def raise_unless_bucket_name_valid args
68
+ bucket_name = args[:bucket_name]
69
+
70
+ unless bucket_name.nil?
71
+ Celluloid::Actor[:backup_bucket_validators].validate args
72
+ end
73
+ end
74
+
75
+ def create_backup anchor
76
+ Celluloid::Actor[:backup_creators].async.create :anchor => anchor
77
+
78
+ anchor
79
+ end
80
+
81
+ def backup_exists? anchor
82
+ Celluloid::Actor[:backup_creators].exists? :anchor => anchor
83
+ end
84
+
85
+ def create_identity identity
86
+ create identity
87
+
88
+ key = add_key_to identity
89
+
90
+ add_initial_controls_to identity
91
+
92
+ return identity, key
93
+ end
94
+
95
+ def add_initial_controls_for org
96
+ client = org.client
97
+
98
+ key = read client.keys :id => auth_key.id
99
+
100
+ identity = key.identity
101
+
102
+ # TODO OrgIdentities table is not being updated yet
103
+
104
+ create client.active_controls :org => org,
105
+ :identity => identity,
106
+ :verbs => 'GET|PATCH',
107
+ :path => "/#{API_VERSION}/orgs/#{org.id}",
108
+ :mode => :allow
109
+
110
+ create client.active_controls :org => org,
111
+ :identity => identity,
112
+ :verbs => 'POST|GET|PATCH',
113
+ :path => "/#{API_VERSION}/orgs/#{org.id}/*",
114
+ :mode => :allow
115
+ end
116
+
117
+ def add_key_to identity
118
+ key = identity.client.active_keys :identity => identity
119
+
120
+ create key
121
+ end
122
+
123
+ def add_initial_controls_to identity
124
+ client = identity.client
125
+
126
+ client.active_controls.create :identity => identity,
127
+ :verbs => 'GET|PATCH',
128
+ :path => "/#{API_VERSION}/identities/#{identity.id}",
129
+ :mode => :allow
130
+
131
+ client.active_controls.create :identity => identity,
132
+ :verbs => 'GET|POST',
133
+ :path => "/#{API_VERSION}/identities/#{identity.id}/*",
134
+ :mode => :allow
135
+
136
+ client.active_controls.create :identity => identity,
137
+ :verbs => 'POST',
138
+ :path => "/#{API_VERSION}/orgs",
139
+ :mode => :allow
140
+ end
141
+
142
+ def first_and_last_line args
143
+ account = args[:account]
144
+
145
+ # eventually consistent means we could get one, but not two...
146
+ # zero is fine, it means the account has no lines!
147
+
148
+ begin
149
+ lines = [ ]
150
+
151
+ lines += first_or_last_line( args.merge! :effective_at => first_second,
152
+ :action => :starting )
153
+
154
+ lines += first_or_last_line( args.merge! :effective_at => last_second,
155
+ :action => :ending )
156
+
157
+ end while lines.length == 1 and sleep 0.025
158
+
159
+ lines
160
+ end
161
+
162
+ private
163
+
164
+ def first_or_last_line args
165
+ account = args[:account]
166
+ client = args[:client] ||= account.client
167
+
168
+ anchor = account.line :effective_at => args[:effective_at]
169
+
170
+ collect args.merge! :collection_name => :account_lines,
171
+ :anchor => anchor,
172
+ :limit => 1
173
+ end
174
+
175
+ def set_account_line_balances args
176
+ if [ :before, :ending, :preceding ].include? args[:action]
177
+ account_lines = args[:account_lines].reverse
178
+ else
179
+ account_lines = args[:account_lines]
180
+ end
181
+
182
+ first_line = account_lines.first
183
+
184
+ balance = balance_for first_line
185
+
186
+ first_line.send :balance=, balance
187
+
188
+ # Beware insane Ruby array indexing on line below
189
+
190
+ account_lines[1..-1].each do |account_line|
191
+ balance += account_line
192
+ account_line.send :balance=, balance
193
+ end
194
+ end
195
+
196
+ def set_account_line_balance account_line
197
+ account_line.send :balance=, balance_for( account_line )
198
+ end
199
+
200
+ def balance_for account_line
201
+ client = account_line.client
202
+ account = account_line.account
203
+ effective_at = account_line.effective_at
204
+
205
+ previous_ms = previous_ms_for effective_at
206
+
207
+ balance = account_balance :store => self,
208
+ :client => client,
209
+ :account => account,
210
+ :at => previous_ms
211
+
212
+ account.lines( :action => :after,
213
+ :effective_at => previous_ms,
214
+ :no_balances => true ) do |line|
215
+
216
+ balance += line
217
+
218
+ break if line == account_line
219
+ end
220
+
221
+ balance
222
+ end
223
+
224
+ def previous_ms_for effective_at
225
+ effective_at_sec = BigDecimal effective_at.tv_sec
226
+ effective_at_usec = BigDecimal effective_at.tv_usec
227
+ effective_at_frac = effective_at_usec / 1_000_000
228
+
229
+ effective_at_bd = effective_at_sec + effective_at_frac
230
+
231
+ previous_ms_bd = effective_at_bd - BigDecimal( '.001' )
232
+
233
+ Time.at( previous_ms_bd ).utc
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,21 @@
1
+ module Subledger
2
+ class Supervisor < Celluloid::SupervisionGroup
3
+
4
+ DEFAULT_SIZE = 1
5
+
6
+ SMALL = 16
7
+ MEDIUM = 32
8
+ LARGE = 48
9
+ GRANDE = 80
10
+
11
+ def self.manage klass, args
12
+ args[:size] ||= DEFAULT_SIZE
13
+
14
+ if args[:size] == 1
15
+ supervise klass, args
16
+ else
17
+ pool klass, args
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,52 @@
1
+ require 'securerandom'
2
+
3
+ module Subledger
4
+ module UUID
5
+
6
+ SPACE = 62 ** 22
7
+
8
+ CHARS = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a
9
+
10
+ VALID = /^[0-9A-Za-z]{22}$/
11
+
12
+ def self.valid? uuid
13
+ !uuid.nil? and uuid =~ VALID
14
+ end
15
+
16
+ def self.invalid? uuid
17
+ uuid.nil? or uuid !~ VALID
18
+ end
19
+
20
+ def self.as_string
21
+ to_string as_bignum
22
+ end
23
+
24
+ def self.as_bignum
25
+ SecureRandom.random_number SPACE
26
+ end
27
+
28
+ def self.to_string bignum
29
+ base_62 = ''
30
+
31
+ 22.times do
32
+ base_62 << CHARS[ bignum.modulo 62 ]
33
+ bignum /= 62
34
+ end
35
+
36
+ base_62.reverse
37
+ end
38
+
39
+ def self.to_bignum uuid
40
+ bignum = 0
41
+
42
+ power = 0
43
+
44
+ uuid.split('').reverse.each do |character|
45
+ bignum += CHARS.index( character ) * ( 62 ** power )
46
+ power += 1
47
+ end
48
+
49
+ bignum
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module Subledger
2
+ VERSION = '0.7.7'
3
+ API_VERSION = 'v2'
4
+ end