command_post 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +28 -0
  5. data/README.md +310 -0
  6. data/Rakefile +1 -0
  7. data/command_post.gemspec +21 -0
  8. data/lib/command_post/command/command.rb +375 -0
  9. data/lib/command_post/command_post.rb +5 -0
  10. data/lib/command_post/db/connection.rb +12 -0
  11. data/lib/command_post/db/postgresql.sql +45 -0
  12. data/lib/command_post/event_sourcing/aggregate.rb +82 -0
  13. data/lib/command_post/event_sourcing/aggregate_event.rb +107 -0
  14. data/lib/command_post/identity/identity.rb +75 -0
  15. data/lib/command_post/identity/sequence_generator.rb +44 -0
  16. data/lib/command_post/persistence/aggregate_pointer.rb +18 -0
  17. data/lib/command_post/persistence/auto_load.rb +70 -0
  18. data/lib/command_post/persistence/data_validation.rb +113 -0
  19. data/lib/command_post/persistence/persistence.rb +157 -0
  20. data/lib/command_post/persistence/schema_validation.rb +137 -0
  21. data/lib/command_post/util/hash_util.rb +23 -0
  22. data/lib/command_post/util/string_util.rb +18 -0
  23. data/lib/command_post/version.rb +3 -0
  24. data/spec/command_post/command/command_spec.rb +0 -0
  25. data/spec/command_post/identity/identity_lookup_value_aggregate_id_spec.rb +89 -0
  26. data/spec/command_post/identity/identity_lookup_value_checksum_spec.rb +108 -0
  27. data/spec/command_post/identity/identity_lookup_value_field_spec.rb +83 -0
  28. data/spec/command_post/persistence/data_validation_spec.rb +74 -0
  29. data/spec/command_post/persistence/nested_remote_spec.rb +0 -0
  30. data/spec/command_post/persistence/schema_validation_spec.rb +269 -0
  31. data/spec/command_post/require.rb +9 -0
  32. data/spec/spec_helper.rb +0 -0
  33. metadata +112 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4f04cb399a1838d6da45137f30818164cb57b193
4
+ data.tar.gz: 7d2558ec0bcd9b90db5ca4106daa23ed7bd970af
5
+ SHA512:
6
+ metadata.gz: 70975bf1bfa0602bdd623d9f6ea934f1c30951c5a3bceff11700b3cbf9062e9d1400cd4e0df01b7cb4d9b33fd9527e9ef8acfd1b7cbca70ca6a3b9d72efc0728
7
+ data.tar.gz: 6af7f21fcd4556f6e12d26177c316c45f51a6a45542762a1ed411ca928d4e2661e7ac438da7621bbc6275abb477b91c0e4b30641e16da18513370209c5515758
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ spike/
20
+ examples/
21
+
22
+
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sequel'
4
+ gem 'pg'
5
+
6
+
7
+
8
+ group :development, :test do
9
+ gem 'starting_blocks'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ffi (1.9.0)
5
+ ffi (1.9.0-x86-mingw32)
6
+ listen (1.2.2)
7
+ rb-fsevent (>= 0.9.3)
8
+ rb-inotify (>= 0.9)
9
+ rb-kqueue (>= 0.2)
10
+ pg (0.16.0)
11
+ pg (0.16.0-x86-mingw32)
12
+ rb-fsevent (0.9.3)
13
+ rb-inotify (0.9.0)
14
+ ffi (>= 0.5.0)
15
+ rb-kqueue (0.2.0)
16
+ ffi (>= 0.5.0)
17
+ sequel (4.0.0)
18
+ starting_blocks (0.0.31)
19
+ listen (>= 1.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+ x86-mingw32
24
+
25
+ DEPENDENCIES
26
+ pg
27
+ sequel
28
+ starting_blocks
data/README.md ADDED
@@ -0,0 +1,310 @@
1
+ CommandPost
2
+ ===
3
+
4
+ CommandPost – the Command Pattern, Object Storage and Event Sourcing
5
+
6
+
7
+
8
+
9
+ CommandPost is a library that facilitates the retrieval and storage of objects. The objects are nothing more than Hashes, stored as JSON in a relational database. CommandPost has the following features to facillitate this:
10
+
11
+ * A base class (Persistence) which encapsulates a Hash so as to allow dot notation access ( myobject.myproperty instead of myobject['myproperty']) as well as computational methods on the class.
12
+ * A schema declaration syntax which insures only valid objects are saved to the database
13
+ * A module (Identity) the provides an identity to the object so that it may be persisted and retrieved later by its 'aggregate_id'.
14
+ * A fully-integrated 'at the core' event sourcing mechanism. Objects are not saved to the database until their 'change events' are first recorded to an event store. A single table (aggregate_events) holds every event
15
+ that ever occurred to every object. It is relatively easy to picture the entire system as it appeared at a point in time. This will be even easier in future versions of CommandPost.
16
+ * Say goodbye to database migrations. Two tables store all your objects. Development cycles become more nimble without the "friction" of keeping changes consistent between the code and a database. Simply add a new
17
+ 'field' to your class and it's done.
18
+ * Retrieves fully populated domain objects from the database. Application code does not own the step of wrangling hash-data into a business object. CommandPost does it for you. In fact, it's through your domain object declaration
19
+ that CommandPost even knows how to populate the hashes that eventually stored as JSON.
20
+
21
+
22
+ A word about Persitence vs Identity.
23
+
24
+ * In CommandPost, Persistence (a base-class) is what allows an "object" to be saved to the database. An object that inherits from Persistence and includes the Identity module can always be saved to the database.
25
+ * A class that ONLY inherits from Persistence, but does not include the Identity module *CAN* be saved to the database but ONLY as the property of another object which must also be an Identity object (includes the Identity module) or eventually reaches up to an Identity object
26
+
27
+
28
+ EXAMPLE: An example of Identity as contrasted against Persistence.
29
+
30
+ Consider a purchase order. It "header" information about the P.O. itself and it has many P.O. "lines".
31
+
32
+ In a traditional RDBMS, this scenario is almost always modeled as two tables: A po_header table and a po_lines table. In this case, both parts of the PO become unqiuely identifiable, individually retrievable entities by virtue of being rows on a table with po_lines most likely having a surrogate key that is an IDENTITY column.
33
+
34
+ In reality though, as Domain Driven Design points out, a purchase order line typically has no meaning, no value, unless viewed in the context of the P.O. as a whole. One *possible* way of modelling that is more in keeping with DDD is to model the P.O. as a whole (for now we'll leave out discussions of SubDomains and BoundedContexts). Why model it as a whole? Because being able to retrieve the ENTIRE po with a single read operation preserves the natural
35
+ transactional boundary of the object. For instance, suppose that we want to maintain a field on the P.O. header that is essentially derived from sort of status of the lines. For convenience we just want to compute it has storeit on the P.O. header. In an RDBMS, you would HAVE TO use a transaction if the state of a line changed in such a way that this stored field on the header. With CommandPost (which I can admit stands for Domain Driven Design Database, get it?),
36
+ with CommandPost, when the P.O. is modeled as a single object, there is only ONE write to the database, so the transaction is implicit, around the natual transactional boundaries of the object itself.
37
+
38
+ With CommandPost, you certainly COULD model our example "the RDBMS way". Let's take a look at how to model the PO in each of the two approaches.
39
+
40
+
41
+
42
+ First, the Domain-Driven-Design-inspired approach
43
+
44
+ <pre><code>
45
+
46
+ class PurachaseOrderHeader < Persistence
47
+
48
+ include Identity
49
+
50
+ def initialize
51
+ super
52
+ fields = Hash.new
53
+ fields[ 'order_date' ] = { :required => true, :type => Date, :location => :local }
54
+ fields[ 'order_number ] = { :required => true, :type => String :location => :local }
55
+ fields[ 'customer' ] = { :required => true, :type => Customer, :location => :remote, :auto_load => true }
56
+ fields[ 'order_total' ] = { :required => true, :type => Money, :location => :local }
57
+ fields[ 'order_status' ] = { :required => true, :type => String, :location => :local }
58
+ fields[ 'order_lines '] = { :required => true, :type => Array, :of => PurchaseOrderLine, :location => :local }
59
+ Address.init_schema fields
60
+ end
61
+
62
+ def set_aggregate_lookup_value
63
+ @data['aggregate_lookup_value'] = order_number
64
+ end
65
+
66
+ end
67
+
68
+ class PurachaseOrderLine < Persistence
69
+ def initialize
70
+ super
71
+ fields = Hash.new
72
+ fields[ 'header ' ] = { :required => true, :type => PurachaseOrderHeader, :location => :remote, :auto_load => true }
73
+ fields[ 'product' ] = { :required => true, :type => Product, :location => :remote, :auto_load => true }
74
+ fields[ 'finalized_price' ] = { :required => true, :type => Money, :location => :local }
75
+ fields[ 'quantity' ] = { :required => true, :type => Fixnum, :location => :local }
76
+ Address.init_schema fields
77
+ end
78
+
79
+ def extended_price
80
+
81
+ # assumes Fixnum and Money play nice :)
82
+
83
+ if header.status == 'open'
84
+ quantity * product.price
85
+ else
86
+ finalized_price
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ </code></pre>
93
+
94
+ Before getting into some code that uses this objects, let's examine this schema declaration 'syntax' in more detail.
95
+
96
+ Starting with intializer of PurchaseOrderHeader, you can see we call 'super' right off the bat. That's because 'initialize' in Persistence sets up things we'll need.
97
+ Next comes the schema declaration. Why talk about a "schema" for an object? Well, I am the first to admit I am no fan of RDBMS for anything other than reporting and when I need aggregate functions that
98
+ only RDBMSs can provide. I am also no fan of the "friction" RDBMS introduces into the development process. BUT... that doesn't mean I like to have some controls and order around things. There several SIGNIFICANT
99
+ advanatges that CommandPost storage provides over RDBMS storage. We'll cover them as they come up while discussing the mechanics of the schema declaration syntax.
100
+
101
+ First, a schema is nothing more than a list of fields, each of which has some properties. What better way, what more 'Ruby way' of accomplishing this than with a Hash? So then, a 'schema' is a Hash. Each key is a field name
102
+ and the value of each field is another hash containing a varying number of keys (properties) and values (settings) for the field.
103
+
104
+ Field names are declared AS STRINGS and NOT symbols. Personally, I like the look of symbols more, but JSON stores and returns keys as strings and so life is easier if we make our field names strings as well.
105
+
106
+ So then, the first field is 'order_date' is a 'required' field. Hopefully that requires no further explanation. They type is 'Date'. That means that the value assigned to the order_date field MUST be an object whose class is Date.
107
+ I've not yet given much thought about values that objects that descend from date or Strings that can cleanly be coerced into a Date. However, this segues into another point: unlike an RDBMS whose datatypes are fairly unintelligent
108
+ (integers, decimals, strings, dates, etc), CommandPost can have types of ANY object... well, any JSON-friendly object such Hashes, Arrays or classes that inherit from them (1) . For example, consider that most venerable of all database
109
+ fields, the Social Security Number. It turns out that not all 9-digit numbers issued by Uncle Sam are Social Security Numbers.
110
+ Some are called ITINs (Individual Taxpayer Identification Number) and are often issued to people arriving in the U.S. before they are issued an actual SSN. They may have an ITIN for years before receving an SSN.
111
+
112
+ An SSN has a variety of validations that distinguish them from issued SSNs versus random, nine-digit numbers. The same applies to ITINs. Some business cases might need to know which people in their database have
113
+ ITINs and which have SSNs. So you could, for example, create a class such as this:
114
+
115
+ <pre><code>
116
+
117
+ class GovernmentId < Hash
118
+
119
+ attr_accessible :type, :value
120
+
121
+ def initialize type, value
122
+ raise "Invalid government ID" unless validate(type,value)
123
+ ['type'] = @type = type
124
+ ['value'] = @value = value
125
+ end
126
+ def validate
127
+ return false if # validations failed..
128
+ true
129
+ end
130
+ end
131
+ </code></pre>
132
+
133
+ You could now declare a schema field as follows:
134
+
135
+ <pre><code>
136
+ fields[ 'government_id' ] = { :required => true, :type => GovernmentId, :location => :local }
137
+ </code></pre>
138
+
139
+ Returning to our purchase order example, we left off the :location keyword of 'order_date'. order_date's location is :local. This is the default for all fields but we've spelled it out here. The only other acceptable
140
+ value for :location is :remote. If :location is :remote, then :type must be set to the name of a class that includes Identity. Or, alternatively, the :type can be set to Array and the :of keyword must be set to
141
+ a class that includes Identity. It's worth noting that if a field is set to an Identity object and :location is :remote, the entire content of that object is NOT stored in the database. Rather a small Hash structure called
142
+ an 'AggregatePointer' is stored that. It is just a bit of data that says where to find the actual object in the database. However, you can easily tell CommandPost to return your object to you with all remote identity objects already
143
+ retrieved and populated. This is simply done by including the :autoload => true otpion in your schema declaration.
144
+
145
+ Moving on to the order_lines field, you can see that its type is 'Array' and the :of keyword has PurchaseOrderLines as its value. So then, this field is an Array of PurchaseOrderLine objects. The :location is set to :local.
146
+ This means that P.O. lines are NOT stored as Identity objects, but merely Persitent objects. There's no way at all to retrieve a line without going through the P.O. header itself. In this case, the content of the PurchaseOrder
147
+ object and all of its PurchaseOrderLines are stored in single object and retrieved with a single read operation.
148
+
149
+
150
+ Moving on to PurchaseOrderLine, as mentioned earlier, there are two fields whose type are Identity classes, header and product. product is a pointer to a remote Customer object. Again, when we fetch the Purchas Order object with
151
+ our single read, not only does it retrieve the 'embedded' P.O. lines, but each P.O. line will have retieved and populated the product field. Though we've not shown what a Product object looks like, it might have remote, auto_loaded
152
+ Supplier property. You get ALL of this data with a single read. Of course, you could go a little crazy with this. You don't want to retieve the entire database with every read. You can also declare the remote field to be
153
+ :auto_load => false. In this case, the cascading retrievals will not happen. Instead your object will return ONLY the data that is actually stored for remote fields... the AggregtePointer, which you can then use to manually
154
+ retrieve ONLY those elements you need at the time.
155
+
156
+ Lastly, notice the method, extended_price. First, it uses the 'status' property from the P.O. header which it gains access to by declaring a field, 'header' which is auto_loaded for each line (not 'stored', just populated within
157
+ the object). The major point illustrated here is the value of using persistent objects (< Persistence) over plain old hashes... we can declare computational methods over the data.
158
+
159
+
160
+
161
+ Now to persist this thing we would say something like this:
162
+
163
+
164
+ <pre><code>
165
+
166
+ hdr = PurchaseOrderHeader.new
167
+ hdr.order_number = OrderNumberService.next_order_number # a made up service we did not cover...
168
+ hdr.order_date = Date.today
169
+ hdr.customer = some_customer_variable # we already had this ...
170
+ hdr.order_status = 'open' # please ignore cheesy string as status
171
+
172
+ lines = Array.new
173
+
174
+ # first line...
175
+ lines << PurchaseOrderLine.new
176
+ lines.last.product = some_product_variable
177
+ lines.last.quantity = 12
178
+
179
+ # second line...
180
+ lines << PurchaseOrderLine.new
181
+ lines.last.product = some_other_product_variable
182
+ lines.last.quantity = 144
183
+
184
+ # more lines as necessary....
185
+
186
+ # add the lines to the header.
187
+
188
+ hdr.order_lines = lines
189
+
190
+ </code></pre>
191
+
192
+ Now, if this were Rails/Active Record, you might exepct to see something like:
193
+
194
+ <pre><code>
195
+ hdr.save
196
+ </code></pre>
197
+
198
+ We don't do that here. Mutating data without caputing WHY it changed is root of all evil.
199
+
200
+ Here, we used the 'Command Pattern'. In some ways, it is a bit more code (at first), but, it's makes almost impossible to write an application without understanding each business case where data is created or changed,
201
+ and then creating a "command" to carry it out.
202
+
203
+ At this point, CommandPost does not use messaging software to transport commands to command handlers. You may or may not wish to ever use messaging software.
204
+
205
+ Omitting for now the code inside of the the command, here's how we'd persist this purchase order to the CommandPost database:
206
+
207
+ <pre><code>
208
+
209
+ cmd = CommandCreateNewPurchaseOrder.new hdr
210
+ cmd.execute
211
+
212
+ </code></pre>
213
+
214
+ Now to retrieve the order number in its entirety, we can use a variety of methods:
215
+
216
+ TO RETRIEVE WHEN WE KNOW THE 'AGGREGATE_ID' (essentialy an identity column, though not contiguously sequential because the same sequence is used for ALL aggregates (Identity objects) within our system)
217
+
218
+ <pre><code>
219
+
220
+ po = PurchaseOrder.find(id)
221
+
222
+ </code></pre>
223
+
224
+
225
+ The above is just a shortcut to the longer form:
226
+
227
+ <pre><code>
228
+ po = Aggregate.get_by_aggregate_id PurchaseOrder, id
229
+ </code></pre>
230
+
231
+ Reflecting back for a minute to the PurchaseOrder class, you'll see a method after the initalizer called set_aggregate_lookup_value. This allows us to look up an aggregate by its 'real world unique identifier'.
232
+
233
+ In this case, it's the order_number. Assuming we have this value in an order_number variable:
234
+
235
+ po = Aggregate.get_aggregate_by_lookup_value PurchaseOrder, order_number
236
+
237
+
238
+
239
+
240
+ That's all for today. I'll write more soon. I wrote this in one setting so I apologize for any typos.
241
+
242
+ More soon. I hope to release this as open source on GitHub sometime before August, 2013.
243
+
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+
266
+
267
+
268
+
269
+
270
+
271
+
272
+
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+
298
+
299
+
300
+
301
+
302
+
303
+
304
+
305
+
306
+
307
+
308
+
309
+
310
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'command_post/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "command_post"
8
+ spec.version = CommandPost::VERSION
9
+ spec.authors = ["Joe Meirow"]
10
+ spec.email = ["joe.meirow@gmail.com"]
11
+ spec.description = %q{CommandPost}
12
+ spec.summary = %q{CommandPost - Object storage/retrieval, event sourcing, command pattern}
13
+ spec.homepage = "http://github.com/jmeirow/command_post"
14
+ spec.license = "MIT"
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+ spec.add_development_dependency "bundler", "~> 1.3"
20
+ spec.add_development_dependency "rake"
21
+ end