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,157 @@
1
+ module Subledger
2
+ module Domain
3
+ class CategoryError < Error; end
4
+
5
+ class Category
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Describable
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Versionable
14
+
15
+ include Roles::Creatable
16
+ include Roles::Readable
17
+ include Roles::Updatable
18
+
19
+ include Roles::Collectable
20
+
21
+ include Roles::Activatable
22
+ include Roles::Archivable
23
+
24
+ include Roles::Restable
25
+
26
+ attr_reader :book
27
+ attr_accessor :normal_balance
28
+
29
+ def self.post_keys
30
+ [ :description, :reference, :normal_balance ]
31
+ end
32
+
33
+ def self.patch_keys
34
+ [ :id, :description, :reference, :normal_balance, :version ]
35
+ end
36
+
37
+ def self.root_klass
38
+ Category
39
+ end
40
+
41
+ def self.sub_klasses
42
+ [ active_klass, archived_klass ]
43
+ end
44
+
45
+ def self.active_klass
46
+ ActiveCategory
47
+ end
48
+
49
+ def self.archived_klass
50
+ ArchivedCategory
51
+ end
52
+
53
+ def initialize args
54
+ describable args
55
+ identifiable args
56
+ storable args
57
+ versionable args
58
+
59
+ @book = args[:book]
60
+ @normal_balance = args[:normal_balance]
61
+ end
62
+
63
+ def accounts &block
64
+ begin
65
+ store.collect_accounts_for_category self, &block
66
+ rescue Store::CollectError => e
67
+ raise CategoryError, e
68
+ end
69
+ end
70
+
71
+ class Entity < Grape::Entity
72
+ root 'categories', 'category'
73
+
74
+ expose :id, :documentation => { :type => 'string', :desc => 'category ID' }
75
+
76
+ expose :book, :format_with => :id,
77
+ :documentation => { :type => 'string', :desc => 'book ID' }
78
+
79
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
80
+
81
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
82
+
83
+ expose :normal_balance, :format_with => :normal_balance,
84
+ :documentation => { :type => 'string', :desc => 'normal balance type' }
85
+
86
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
87
+ end
88
+
89
+ private
90
+
91
+ ACCEPTABLE_NORMAL_BALANCES = [ Debit, Credit ]
92
+
93
+ def self.raise_unless_creatable args
94
+
95
+ book = args[:book]
96
+
97
+ if book.nil? or not book.kind_of? Book
98
+ raise CategoryError, ':book is required and must be a Book'
99
+ elsif UUID.invalid? book.id
100
+ raise CategoryError, ':book must have a valid :id'
101
+ end
102
+
103
+ normal_balance = args[:normal_balance]
104
+
105
+ if normal_balance.nil? or not ACCEPTABLE_NORMAL_BALANCES.include? normal_balance
106
+ raise CategoryError, ":normal_balance is required and must be one of #{ACCEPTABLE_NORMAL_BALANCES.join(', ')}"
107
+ end
108
+ end
109
+ end
110
+
111
+ class ArchivedCategory < Category
112
+ class Entity < Category::Entity
113
+ root 'archived_categories', 'archived_category'
114
+ end
115
+
116
+ def self.sub_klasses
117
+ [ archived_klass ]
118
+ end
119
+ end
120
+
121
+ class ActiveCategory < Category
122
+ class Entity < Category::Entity
123
+ root 'active_categories', 'active_category'
124
+ end
125
+
126
+ def self.sub_klasses
127
+ [ active_klass ]
128
+ end
129
+
130
+ def attach args
131
+ account = args[:account]
132
+
133
+ begin
134
+ store.attach_account_to_category :category => self,
135
+ :account => account
136
+ rescue Store::AttachError => e
137
+ raise CategoryError, e
138
+ end
139
+
140
+ account
141
+ end
142
+
143
+ def detach args
144
+ account = args[:account]
145
+
146
+ begin
147
+ store.detach_account_from_category :category => self,
148
+ :account => account
149
+ rescue Store::DetachError => e
150
+ raise CategoryError, e
151
+ end
152
+
153
+ account
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,180 @@
1
+ module Subledger
2
+ module Domain
3
+ class ControlError < Error; end
4
+
5
+ class Control
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Identifiable
11
+ include Roles::Storable
12
+ include Roles::Creatable
13
+ include Roles::Readable
14
+ include Roles::Collectable
15
+ include Roles::Activatable
16
+ include Roles::Archivable
17
+ include Roles::Restable
18
+
19
+ attr_reader :identity, :verbs, :path, :mode
20
+
21
+ def self.root_klass
22
+ Control
23
+ end
24
+
25
+ def self.post_keys
26
+ [ :id, :identity, :verbs, :path, :mode ]
27
+ end
28
+
29
+ def self.patch_keys
30
+ [ ]
31
+ end
32
+
33
+ def self.sub_klasses
34
+ [ active_klass, archived_klass ]
35
+ end
36
+
37
+ def self.active_klass
38
+ ActiveControl
39
+ end
40
+
41
+ def self.archived_klass
42
+ ArchivedControl
43
+ end
44
+
45
+ def initialize args
46
+ identifiable args
47
+ storable args
48
+
49
+ @identity = args[:identity]
50
+ @verbs = args[:verbs]
51
+ @path = args[:path]
52
+ @mode = args[:mode]
53
+ end
54
+
55
+ # def revoke
56
+ # @revoked = true
57
+ #
58
+ # self.update
59
+ #
60
+ # self
61
+ # end
62
+
63
+ # def revoked?
64
+ # @revoked == true
65
+ # end
66
+
67
+ # def is verb, url
68
+ # verb == @verbs and url == @path
69
+ # end
70
+
71
+ def match? verb, url
72
+ @verbs.split('|').include?(verb) and match_wildcard(url, @path)
73
+ end
74
+
75
+ class Entity < Grape::Entity
76
+ root 'controls', 'control'
77
+
78
+ expose :id, :documentation => { :type => 'string', :desc => 'control ID' }
79
+
80
+ expose :identity, :format_with => :id,
81
+ :documentation => { :type => 'string', :desc => 'identity ID' }
82
+
83
+ expose :verbs, :documentation => { :type => 'string', :desc => 'HTTP verbs' }
84
+
85
+ expose :path, :documentation => { :type => 'string', :desc => 'HTTP path' }
86
+
87
+ expose :mode, :documentation => { :type => 'string', :desc => 'allow or deny' }
88
+ end
89
+
90
+ private
91
+
92
+ def match_wildcard actual, authorized
93
+ actual = strip_version actual
94
+ authorized = strip_version authorized
95
+
96
+ regx = authorized.gsub(/\*(.)/, '[^\1]*\1').gsub(/\*$/, '.*')
97
+
98
+ actual == authorized or ( actual =~ /^#{regx}$/ ) == 0
99
+ end
100
+
101
+ def strip_version path
102
+ path[3..-1]
103
+ end
104
+
105
+ ACCEPTABLE_VERBS = [ 'POST', 'GET', 'PATCH' ]
106
+ ACCEPTABLE_MODES = [ :allow, :deny ]
107
+
108
+ def self.raise_unless_creatable args
109
+
110
+ identity = args[:identity]
111
+
112
+ if identity.nil? or not identity.kind_of? Identity
113
+ raise ControlError, ':identity is required and must be an Identity'
114
+ elsif UUID.invalid? identity.id
115
+ raise ControlError, ':identity must have a valid :id'
116
+ end
117
+
118
+ # TODO :verb(s) should be a domain object
119
+
120
+ verbs = args[:verbs]
121
+
122
+ if verbs.nil?
123
+ raise ControlError, ':verbs is required'
124
+ end
125
+
126
+ verbs.split('|').each do |verb|
127
+ unless ACCEPTABLE_VERBS.include? verb
128
+ raise ControlError, ":verbs must contain one or more of #{ACCEPTABLE_VERBS.join(', ')} separated by |"
129
+ end
130
+ end
131
+
132
+ # TODO :path should be a domain object
133
+
134
+ path = args[:path]
135
+
136
+ if path.nil?
137
+ raise ControlError, ':path is required'
138
+ end
139
+
140
+ api_version = path[1,2]
141
+
142
+ unless api_version == API_VERSION
143
+ raise ControlError, ":path must begin with /#{API_VERSION}/"
144
+ end
145
+
146
+ path[4,].split( '/' ).each do |part|
147
+ unless part =~ /[*_0-9A-Za-z]+/
148
+ raise ControlError, 'invalid :path'
149
+ end
150
+ end
151
+
152
+ mode = args[:mode]
153
+
154
+ if mode.nil? or not ACCEPTABLE_MODES.include? mode
155
+ raise ControlError, ":mode must contain one or more of :allow, :deny}"
156
+ end
157
+ end
158
+ end
159
+
160
+ class ArchivedControl < Control
161
+ class Entity < Control::Entity
162
+ root 'archived_controls', 'archived_control'
163
+ end
164
+
165
+ def self.sub_klasses
166
+ [ archived_klass ]
167
+ end
168
+ end
169
+
170
+ class ActiveControl < Control
171
+ class Entity < Control::Entity
172
+ root 'active_controls', 'active_control'
173
+ end
174
+
175
+ def self.sub_klasses
176
+ [ active_klass ]
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,31 @@
1
+ Grape::Entity.format_with :id do |entity|
2
+ entity.id unless entity.nil?
3
+ end
4
+
5
+ Grape::Entity.format_with :normal_balance do |value_klass|
6
+ value_klass.type unless value_klass.nil?
7
+ end
8
+
9
+ Grape::Entity.format_with :time do |time|
10
+ time.iso8601(3) unless time.nil?
11
+ end
12
+
13
+ Grape::Entity.format_with :time_full_second do |time|
14
+ time.iso8601 unless time.nil?
15
+ end
16
+
17
+ Grape::Entity.format_with :value do |value|
18
+ value.rest_hash unless value.nil?
19
+ end
20
+
21
+ Grape::Entity.format_with :progress do |value|
22
+ { :percentage => value }
23
+ end
24
+
25
+ Grape::Entity.format_with :integer do |value|
26
+ value.to_i unless value.nil?
27
+ end
28
+
29
+ Grape::Entity.format_with :balance do |value|
30
+ value.serializable_hash unless value.nil?
31
+ end
@@ -0,0 +1,159 @@
1
+ module Subledger
2
+ module Domain
3
+ class IdentityError < Error; end
4
+
5
+ class Identity
6
+
7
+ include Domain
8
+
9
+ include Roles::Attributable
10
+ include Roles::Describable
11
+ include Roles::Identifiable
12
+ include Roles::Storable
13
+ include Roles::Versionable
14
+
15
+ include Roles::Creatable
16
+ include Roles::Readable
17
+ include Roles::Updatable
18
+
19
+ include Roles::Activatable
20
+ include Roles::Archivable
21
+
22
+ include Roles::Restable
23
+
24
+ attr_accessor :email
25
+
26
+ def self.post_keys
27
+ [ :id, :email, :description, :reference ]
28
+ end
29
+
30
+ def self.patch_keys
31
+ [ :id, :email, :description, :reference, :version ]
32
+ end
33
+
34
+ def self.root_klass
35
+ Identity
36
+ end
37
+
38
+ def self.sub_klasses
39
+ [ archived_klass, active_klass ]
40
+ end
41
+
42
+ def self.archived_klass
43
+ ArchivedIdentity
44
+ end
45
+
46
+ def self.active_klass
47
+ ActiveIdentity
48
+ end
49
+
50
+ def self.create args
51
+ validate_creatability args
52
+
53
+ identity = args[:client].send active_klass.collection_name, args
54
+
55
+ args[:store].create_identity identity
56
+ end
57
+
58
+ def create
59
+ identity, key = self.class.create self.attributes
60
+
61
+ initialize identity.attributes
62
+
63
+ return self, key
64
+ end
65
+
66
+ def initialize args
67
+ describable args
68
+ identifiable args
69
+ storable args
70
+ versionable args
71
+
72
+ @email = args[:email]
73
+ end
74
+
75
+ # def by_email email
76
+ # # TODO
77
+ # end
78
+
79
+ # def by_org org
80
+ # # TODO look in OrgIdentities table
81
+ # end
82
+
83
+ def authorize verb, url
84
+ raise KeyError if @retired
85
+
86
+ controls.each do |control|
87
+ return true if control.match(verb, url) unless control.revoked?
88
+ end
89
+
90
+ false
91
+ end
92
+
93
+ # def grant args
94
+ # args[:client].controls.create args
95
+ # end
96
+
97
+ # def revoke args
98
+ # org_id = args[:org_id]
99
+ # verb = args[:verb]
100
+ # url = args[:url]
101
+ #
102
+ # revoked = false
103
+ #
104
+ # controls.each do |control|
105
+ # if control.is org_id, verb, url
106
+ # control.revoke
107
+ # revoked = true
108
+ # end
109
+ # end
110
+ #
111
+ # raise IdentityError unless revoked
112
+ #
113
+ # self
114
+ # end
115
+
116
+ class Entity < Grape::Entity
117
+ root 'identities', 'identity'
118
+
119
+ expose :id, :documentation => { :type => 'string', :desc => 'identity ID' }
120
+
121
+ expose :email, :documentation => { :type => 'string', :desc => 'identity email' }
122
+
123
+ expose :description, :documentation => { :type => 'string', :desc => 'description' }
124
+
125
+ expose :reference, :documentation => { :type => 'string', :desc => 'reference URI' }
126
+
127
+ expose :version, :documentation => { :type => 'integer', :desc => 'version' }
128
+ end
129
+
130
+ private
131
+
132
+ def self.raise_unless_creatable args
133
+ unless ValidateEmail.mx_valid? args[:email]
134
+ raise IdentityError, ':email is required and must have an MX record for the domain'
135
+ end
136
+ end
137
+ end
138
+
139
+ class ArchivedIdentity < Identity
140
+ class Entity < Identity::Entity
141
+ root 'archived_identities', 'archived_identity'
142
+ end
143
+
144
+ def self.sub_klasses
145
+ [ archived_klass ]
146
+ end
147
+ end
148
+
149
+ class ActiveIdentity < Identity
150
+ class Entity < Identity::Entity
151
+ root 'active_identities', 'active_identity'
152
+ end
153
+
154
+ def self.sub_klasses
155
+ [ active_klass ]
156
+ end
157
+ end
158
+ end
159
+ end