vorpal 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +15 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +167 -0
- data/Rakefile +2 -0
- data/lib/simple_serializer/simple_deserializer.rb +67 -0
- data/lib/simple_serializer/simple_serializer.rb +72 -0
- data/lib/vorpal/aggregate_repository.rb +445 -0
- data/lib/vorpal/config_builder.rb +148 -0
- data/lib/vorpal/configs.rb +274 -0
- data/lib/vorpal/configuration.rb +61 -0
- data/lib/vorpal/hash_initialization.rb +10 -0
- data/lib/vorpal/identity_map.rb +26 -0
- data/lib/vorpal/version.rb +3 -0
- data/lib/vorpal.rb +5 -0
- data/spec/integration_spec_helper.rb +41 -0
- data/spec/vorpal/aggregate_repository_spec.rb +759 -0
- data/spec/vorpal/class_config_builder_spec.rb +11 -0
- data/vorpal.gemspec +28 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NjI3OWU1NDlmYjJjZjAyOTZkZmU5NThmZjJjMDI4NzVlNGE1MjFjZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZGJlYjljY2ViYTk0MzdhMWYxZjEyMWU5NmZjMGUxMzU2YTI3MzRmMw==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZDAzMmU1MmVlNzYwYTlmNTk0NGIxMGExZTgwMDIwN2I5YzQ4NTNiNTQ5ZGRi
|
10
|
+
ZjRiYjhjNDJiM2RmN2I4MGRiMzY3NTNiOTE0ZGUwMzhjZGE5ZWE1Y2EyZTk2
|
11
|
+
Y2JmMmY3YzUyZDZkODYxYzVlMzhkNTg3ZThiMDIzY2ZlNzNhYzU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZTZhZWExODIxYWQzNTlkNDJkMzg0OWI4NWVlOTMxYzZlNzk1ZTQzZmZlYjEz
|
14
|
+
NGRhM2Q1MjIxYjQ4N2M5NWRiMjc4YmNmMjQ5MWNiYTEwZWU3NGYwYjhjNzkx
|
15
|
+
OTc0YzFiODY3NWNkZTMyOWJiODVhYzQyZGJkYjRlYzAwMGFiMjk=
|
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private -m markdown lib/**/*.*
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sean Kirby
|
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,167 @@
|
|
1
|
+
# Vorpal
|
2
|
+
|
3
|
+
Separate your domain model from your delivery mechanism.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
Vorpal is a [Data Mapper](http://martinfowler.com/eaaCatalog/dataMapper.html)-style ORM (object relational mapper) framelet that persists POROs (plain old Ruby objects) to a relational DB.
|
7
|
+
|
8
|
+
We say 'framelet' because it doesn't attempt to give you all the goodies that ORMs usually provide. Instead, it layers on top of an existing ORM and allows you to use the simplicity of the Active Record pattern where appropriate and the power of the Data Mapper pattern when you need it.
|
9
|
+
|
10
|
+
3 things set it apart from existing Ruby ORMs (ActiveRecord and Datamapper):
|
11
|
+
|
12
|
+
1. It keeps persistence concerns separate from domain logic. In other words, your domain models don't have to extend ActiveRecord::Base (or something else) in order to get saved to a DB.
|
13
|
+
1. It works with [Aggregates](http://martinfowler.com/bliki/DDD_Aggregate.html) rather than individual objects.
|
14
|
+
1. It plays nicely with ActiveRecord objects!
|
15
|
+
|
16
|
+
[Perpetuity](https://github.com/jgaskins/perpetuity) has a great introduction.
|
17
|
+
|
18
|
+
Talk about EDR? Victor has a good explanation of why domain model and delivery mechanism should be separated.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'vorpal'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install vorpal
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
Start with a domain model of POROs that form an aggregate:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
class Tree; end
|
41
|
+
|
42
|
+
class Trunk
|
43
|
+
include Virtus.model
|
44
|
+
|
45
|
+
attribute :id, Integer
|
46
|
+
attribute :length, Decimal
|
47
|
+
attribute :tree, Tree
|
48
|
+
end
|
49
|
+
|
50
|
+
class Branch
|
51
|
+
include Virtus.model
|
52
|
+
|
53
|
+
attribute :id, Integer
|
54
|
+
attribute :length, Decimal
|
55
|
+
attribute :tree, Tree
|
56
|
+
end
|
57
|
+
|
58
|
+
class Tree
|
59
|
+
include Virtus.model
|
60
|
+
|
61
|
+
attribute :id, Integer
|
62
|
+
attribute :name, String
|
63
|
+
attribute :trunk, Trunk
|
64
|
+
attribute :branches, Array[Branch]
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
Along with a relational model:
|
69
|
+
|
70
|
+
```sql
|
71
|
+
CREATE TABLE trees
|
72
|
+
(
|
73
|
+
id serial NOT NULL,
|
74
|
+
name text,
|
75
|
+
trunk_id integer
|
76
|
+
)
|
77
|
+
|
78
|
+
CREATE TABLE trunks
|
79
|
+
(
|
80
|
+
id serial NOT NULL,
|
81
|
+
length numeric
|
82
|
+
)
|
83
|
+
|
84
|
+
CREATE TABLE branches
|
85
|
+
(
|
86
|
+
id serial NOT NULL,
|
87
|
+
length numeric,
|
88
|
+
tree_id integer
|
89
|
+
)
|
90
|
+
```
|
91
|
+
|
92
|
+
Create a repository configured to persist the aggregate to the relational model:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
repository = Persistence::Configuration.define do
|
96
|
+
map Tree do
|
97
|
+
fields :name
|
98
|
+
belongs_to :trunk
|
99
|
+
has_many :branches
|
100
|
+
end
|
101
|
+
|
102
|
+
map Trunk do
|
103
|
+
fields :length
|
104
|
+
has_one :tree
|
105
|
+
end
|
106
|
+
|
107
|
+
map Branch do
|
108
|
+
fields :length
|
109
|
+
belongs_to :tree
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
Why don't we use DDD language? I see no mention of aggregates and entities!
|
114
|
+
|
115
|
+
And use it:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
repository.persist(big_tree)
|
119
|
+
|
120
|
+
small_tree = repository.load(small_tree_id, Tree)
|
121
|
+
|
122
|
+
repository.destroy(dead_tree)
|
123
|
+
```
|
124
|
+
|
125
|
+
Show implementation of a repository using the aggregate repository!!!
|
126
|
+
|
127
|
+
Talk about aggregate boundary.
|
128
|
+
|
129
|
+
### With ActiveRecord
|
130
|
+
TBD
|
131
|
+
|
132
|
+
## API Documentation
|
133
|
+
|
134
|
+
(http://rubydoc.info/github/nulogy/vorpal/master/frames)
|
135
|
+
|
136
|
+
## Caveats
|
137
|
+
It also does not do some things that you might expect from other ORMs:
|
138
|
+
|
139
|
+
1. There is no lazy loading of associations. This might sound like a big deal, but with [correctly designed aggregates](http://dddcommunity.org/library/vernon_2011/) it turns out to be very minor.
|
140
|
+
1. There is no managing of transactions. It is the strong opinion of the authors that managing transactions is an application-level concern.
|
141
|
+
1. No support for validations. Validations are not a persistence concern.
|
142
|
+
1. Only supports primary keys called `id`.
|
143
|
+
1. Only supports PostgreSQL.
|
144
|
+
1. Requires domain entities to have a special implementation of `#initialize`.
|
145
|
+
1. Has a dependency on ActiveRecord.
|
146
|
+
1. No facilities for querying the DB.
|
147
|
+
1. Identity map only applies to a single `#load` or `#load_all` call.
|
148
|
+
1. Clients cannot specify primary key values.
|
149
|
+
|
150
|
+
## Future Enhancements
|
151
|
+
* Aggregate updated_at.
|
152
|
+
* Support for other DBMSs.
|
153
|
+
* Identity map for an entire application transaction.
|
154
|
+
* Value objects.
|
155
|
+
* Remove dependency on ActiveRecord (optimistic locking? connection pooling?)
|
156
|
+
* Application-generated primary key ids.
|
157
|
+
* More efficient object loading (use fewer queries.)
|
158
|
+
* Do not require special `#initialize` method? Provide a hook for an instance factory?
|
159
|
+
* Single table inheritance?
|
160
|
+
|
161
|
+
## Contributing
|
162
|
+
|
163
|
+
1. Fork it ( https://github.com/nulogy/vorpal/fork )
|
164
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
165
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
166
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
167
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Simple framelet for deserialization
|
2
|
+
#
|
3
|
+
# class SomeDeserializer < SimpleDeserializer
|
4
|
+
# data_attributes :site_id, :name, :category_id, :integration_key
|
5
|
+
#
|
6
|
+
# def integration_key(old_integration_key)
|
7
|
+
# "XX#{@data[:other_attr]}XX#{old_integration_key}XX"
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# def set_category_id(category_id)
|
11
|
+
# object.category = InventoryStatusCategory.from_id(category_id)
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Usage:
|
16
|
+
#
|
17
|
+
# SomeDeserializer.deserialize(object, data)
|
18
|
+
# SomeDeserializer.new(object, data).deserialize
|
19
|
+
#
|
20
|
+
# SomeDeserializer.deserialize_array([object1, object2, ...], [data1, data2, ...])
|
21
|
+
#
|
22
|
+
class SimpleDeserializer
|
23
|
+
class << self
|
24
|
+
attr_accessor :_attributes
|
25
|
+
|
26
|
+
def inherited(base)
|
27
|
+
base._attributes = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def data_attributes(*attrs)
|
31
|
+
@_attributes.concat attrs
|
32
|
+
|
33
|
+
attrs.each do |attr|
|
34
|
+
define_method attr do |datum|
|
35
|
+
datum
|
36
|
+
end unless method_defined?(attr)
|
37
|
+
|
38
|
+
define_method "set_#{attr}" do |datum|
|
39
|
+
object.send("#{attr}=", send(attr, datum))
|
40
|
+
end unless method_defined?("set_#{attr}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def deserialize_array(objects, data)
|
45
|
+
objects.zip(data).map { |obj, datum| deserialize(obj, datum) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def deserialize(object, data)
|
49
|
+
self.new(object, data).deserialize
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_accessor :object
|
54
|
+
|
55
|
+
def initialize(object, data)
|
56
|
+
@object = object
|
57
|
+
@data = data
|
58
|
+
end
|
59
|
+
|
60
|
+
def deserialize
|
61
|
+
self.class._attributes.dup.each do |name|
|
62
|
+
next unless @data.has_key?(name)
|
63
|
+
send("set_#{name}", @data[name])
|
64
|
+
end
|
65
|
+
object
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Simple framelet for serialization.
|
2
|
+
#
|
3
|
+
# API compatible with ActiveModel::Serializer but without all the complexity
|
4
|
+
# and dependence on ActiveModel
|
5
|
+
#
|
6
|
+
# class SomeSerializer < SimpleSerializer
|
7
|
+
# attributes :id, :name, :category_id, :errors
|
8
|
+
#
|
9
|
+
# def category_id
|
10
|
+
# object.category.try(:id)
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# def errors
|
14
|
+
# ActiveModelErrorsSerializer.serialize_errors(object.errors, true) if object.errors.any?
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Usage:
|
19
|
+
#
|
20
|
+
# SomeSerializer.serialize(object)
|
21
|
+
# SomeSerializer.new(object).serialize
|
22
|
+
#
|
23
|
+
# SomeSerializer.serialize_array([object])
|
24
|
+
#
|
25
|
+
class SimpleSerializer
|
26
|
+
class << self
|
27
|
+
attr_accessor :_attributes
|
28
|
+
|
29
|
+
def inherited(base)
|
30
|
+
base._attributes = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes(*attrs)
|
34
|
+
@_attributes.concat attrs
|
35
|
+
|
36
|
+
attrs.each do |attr|
|
37
|
+
define_method attr do
|
38
|
+
object.send(attr)
|
39
|
+
end unless method_defined?(attr)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialize_array(objects)
|
44
|
+
objects.map { |obj| serialize(obj) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def serialize(object)
|
48
|
+
self.new(object).serialize
|
49
|
+
end
|
50
|
+
|
51
|
+
alias :as_json :serialize
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_accessor :object
|
55
|
+
|
56
|
+
def initialize(object, _={})
|
57
|
+
@object = object
|
58
|
+
end
|
59
|
+
|
60
|
+
def attributes
|
61
|
+
self.class._attributes.dup.each_with_object({}) do |name, hash|
|
62
|
+
hash[name] = send(name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def serialize(_={})
|
67
|
+
return nil if object.nil?
|
68
|
+
attributes
|
69
|
+
end
|
70
|
+
alias :as_json :serialize
|
71
|
+
end
|
72
|
+
|