subledger 0.7.7

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.
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