entity_mapper 0.0.1.pre.alpha
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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/entity_mapper.gemspec +32 -0
- data/lib/entity_mapper.rb +14 -0
- data/lib/entity_mapper/access_modes/factory.rb +16 -0
- data/lib/entity_mapper/access_modes/instance_variable.rb +17 -0
- data/lib/entity_mapper/access_modes/method.rb +17 -0
- data/lib/entity_mapper/active_record/ar_map.rb +17 -0
- data/lib/entity_mapper/active_record/default_build_strategy.rb +13 -0
- data/lib/entity_mapper/active_record/read.rb +53 -0
- data/lib/entity_mapper/active_record/update.rb +84 -0
- data/lib/entity_mapper/mapping/dsl.rb +33 -0
- data/lib/entity_mapper/mapping/has_many_relation.rb +9 -0
- data/lib/entity_mapper/mapping/has_one_relation.rb +5 -0
- data/lib/entity_mapper/mapping/model.rb +23 -0
- data/lib/entity_mapper/mapping/property.rb +27 -0
- data/lib/entity_mapper/mapping/relation.rb +21 -0
- data/lib/entity_mapper/snapshot/object_snapshot.rb +5 -0
- data/lib/entity_mapper/snapshot/take_snapshot.rb +31 -0
- data/lib/entity_mapper/snapshot_diff/calculate.rb +78 -0
- data/lib/entity_mapper/snapshot_diff/object_diff_snapshot.rb +17 -0
- data/lib/entity_mapper/transaction.rb +13 -0
- data/lib/entity_mapper/transaction/context.rb +21 -0
- data/lib/entity_mapper/transaction/tracked_aggregate.rb +27 -0
- data/lib/entity_mapper/version.rb +3 -0
- data/lib/entity_mapper/zeitwerk_inflector.rb +12 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df11413fd5a7b625f093890dded86a87bbbc1e2646c331238bdb69c449d73862
|
4
|
+
data.tar.gz: 7090bf0693acf59e74bd14efe0cbc048011abd68d0d42d0f2509456804d7e167
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 30412988950dfeac6f310dc1554d4387367c2301a837c9c6c5711b6307f3681edaf894964575660b04e09206348bde692d44967af0c7bf088b7ceccef7af1187
|
7
|
+
data.tar.gz: 8d44b370c5eba4a2f7228e3f200f8f08e9402fcf6efb9a9c59763c3244686ef3c3a054099f18bbcdb0041db1870b66c1231442389b960b729d1d357f32f4e700
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Jan Jędrychowski
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# EntityMapper
|
2
|
+
|
3
|
+
Entity Mapper gem is inspired by [.NET Entity Framework](https://docs.microsoft.com/en-us/ef/).
|
4
|
+
|
5
|
+
It's a persistence/ORM tool that maps data to and from POROs (Plain Old Ruby Objects).
|
6
|
+
|
7
|
+
Currently the only supported backend is ActiveRecord.
|
8
|
+
|
9
|
+
The gem is under heavy development and not ready for production usage yet. API is very likely to change without any announcements.
|
10
|
+
|
11
|
+
## Example
|
12
|
+
|
13
|
+
### Setup
|
14
|
+
|
15
|
+
Active Record:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class Order < ApplicationRecord
|
19
|
+
has_many :order_items
|
20
|
+
end
|
21
|
+
|
22
|
+
class OrderItem < ApplicationRecord
|
23
|
+
belongs_to :order
|
24
|
+
end
|
25
|
+
ActiveRecord::Schema.define do
|
26
|
+
create_table "order_items", force: :cascade do |t|
|
27
|
+
t.string "name"
|
28
|
+
t.integer "price_value"
|
29
|
+
t.string "price_currency"
|
30
|
+
t.integer "quantity"
|
31
|
+
t.integer "order_id"
|
32
|
+
t.datetime "created_at", null: false
|
33
|
+
t.datetime "updated_at", null: false
|
34
|
+
t.index ["order_id"], name: "index_order_items_on_order_id"
|
35
|
+
end
|
36
|
+
|
37
|
+
create_table "orders", force: :cascade do |t|
|
38
|
+
t.string "name"
|
39
|
+
t.boolean "paid"
|
40
|
+
t.datetime "created_at", null: false
|
41
|
+
t.datetime "updated_at", null: false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
Entities:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
module Entities
|
50
|
+
class Order
|
51
|
+
attr_accessor :name
|
52
|
+
|
53
|
+
def refund!
|
54
|
+
@paid = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def paid?
|
58
|
+
@paid
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_item(item)
|
62
|
+
items << item
|
63
|
+
end
|
64
|
+
|
65
|
+
def items
|
66
|
+
@items ||= []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module Entities
|
72
|
+
class OrderItem
|
73
|
+
attr_accessor :name, :quantity, :price
|
74
|
+
|
75
|
+
def initialize(name, quantity, price)
|
76
|
+
@name = name
|
77
|
+
@quantity = quantity
|
78
|
+
@price = price
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module Entities
|
84
|
+
Currency = Struct.new(:name)
|
85
|
+
end
|
86
|
+
|
87
|
+
module Entities
|
88
|
+
class Price
|
89
|
+
attr_reader :value, :currency
|
90
|
+
|
91
|
+
def initialize(value, currency)
|
92
|
+
@value = value
|
93
|
+
@currency = currency
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
Mapping:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
Mapping = EntityMapper.map do |m|
|
103
|
+
m.model Entities::Order
|
104
|
+
|
105
|
+
m.property(:name)
|
106
|
+
m.property(:paid)
|
107
|
+
|
108
|
+
m.has_many("items", ar_name: "order_items") do |item_model|
|
109
|
+
item_model.model Entities::OrderItem
|
110
|
+
item_model.property(:quantity)
|
111
|
+
item_model.property(:name)
|
112
|
+
|
113
|
+
item_model.has_one("price", ar_name: nil) do |price_model|
|
114
|
+
price_model.model Entities::Price
|
115
|
+
price_model.property(:value, :price_value)
|
116
|
+
price_model.has_one("currency", ar_name: nil) do |currency_model|
|
117
|
+
currency_model.model Entities::Currency
|
118
|
+
currency_model.property(:name, :price_currency, access: :method)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
### Reading
|
126
|
+
|
127
|
+
Given persisted model:
|
128
|
+
```ruby
|
129
|
+
ar_order = ::Order.new(name: "test-name", paid: true)
|
130
|
+
ar_order.order_items = [::OrderItem.new(name: "order-item", quantity: 3, price_value: 3, price_currency: "USD")]
|
131
|
+
ar_order.save!
|
132
|
+
```
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
EntityMapper::Transaction.call do |context|
|
136
|
+
mapped_entity = context.read(Mapping, ar_order)
|
137
|
+
pp mapped_entity
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
Prints (newlines added for visibility):
|
142
|
+
```
|
143
|
+
#<TestEntities::Order:0x00007fcd5fdd5520
|
144
|
+
@name="test-name",
|
145
|
+
@paid=true,
|
146
|
+
@items=[
|
147
|
+
#<TestEntities::OrderItem:0x00007fcd60283180
|
148
|
+
@quantity=3,
|
149
|
+
@name="order-item",
|
150
|
+
@price=#<TestEntities::Price:0x00007fcd60282e38
|
151
|
+
@value=3,
|
152
|
+
@currency=#<struct TestEntities::Currency name="USD">
|
153
|
+
>
|
154
|
+
>
|
155
|
+
]
|
156
|
+
>
|
157
|
+
```
|
158
|
+
|
159
|
+
|
160
|
+
### Updating
|
161
|
+
|
162
|
+
Given `ar_order` from the Reading section:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
EntityMapper::Transaction.call do |context|
|
166
|
+
mapped_entity = context.read(Mapping, ar_order)
|
167
|
+
|
168
|
+
mapped_entity.items.first.quantity = 5
|
169
|
+
mapped_entity.refund!
|
170
|
+
mapped_entity.add_item TestEntities::OrderItem.new("Milk", 1, TestEntities::Price.new(3, TestEntities::Currency.new("USD")))
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
This automatically persists changes. Sample SQL log:
|
175
|
+
```sql
|
176
|
+
OrderItem Update (0.5ms) UPDATE "order_items" SET "quantity" = ?, "updated_at" = ? WHERE "order_items"."id" = ? [["quantity", 5], ["updated_at", "2019-07-06 21:25:48.392611"], ["id", 1]]
|
177
|
+
OrderItem Create (0.1ms) INSERT INTO "order_items" ("name", "price_value", "price_currency", "quantity", "order_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["name", "Milk"], ["price_value", 3], ["price_currency", "USD"], ["quantity", 1], ["order_id", 1], ["created_at", "2019-07-06 21:25:48.396440"], ["updated_at", "2019-07-06 21:25:48.396440"]]
|
178
|
+
Order Update (0.1ms) UPDATE "orders" SET "paid" = ?, "updated_at" = ? WHERE "orders"."id" = ? [["paid", 0], ["updated_at", "2019-07-06 21:25:48.398639"], ["id", 1]]
|
179
|
+
```
|
180
|
+
|
181
|
+
## Contributing
|
182
|
+
|
183
|
+
Bug reports and pull requests are welcome!
|
184
|
+
|
185
|
+
## License
|
186
|
+
|
187
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "entity_mapper"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "entity_mapper/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "entity_mapper"
|
7
|
+
spec.version = EntityMapper::VERSION
|
8
|
+
spec.authors = ["Jan Jędrychowski"]
|
9
|
+
spec.email = ["jan@jedrychowski.org"]
|
10
|
+
|
11
|
+
spec.summary = "Map persisted data to and from POROs"
|
12
|
+
spec.description = "Map persisted data to and from POROs"
|
13
|
+
spec.homepage = "https://github.com/gogiel/entity_mapper"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "zeitwerk", "~> 2.1"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
spec.add_development_dependency "combustion", "~> 1.1"
|
29
|
+
spec.add_development_dependency "rspec-rails"
|
30
|
+
spec.add_development_dependency "rails", "~> 5.2.2"
|
31
|
+
spec.add_development_dependency "sqlite3"
|
32
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
require_relative "entity_mapper/zeitwerk_inflector"
|
3
|
+
|
4
|
+
loader = Zeitwerk::Loader.for_gem
|
5
|
+
loader.inflector = EntityMapper::ZeitwerkInfelctor.new(__FILE__)
|
6
|
+
loader.setup
|
7
|
+
|
8
|
+
module EntityMapper
|
9
|
+
def self.map
|
10
|
+
Mapping::Model.new.tap do |mapping|
|
11
|
+
yield Mapping::DSL.new(mapping)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module AccessModes
|
3
|
+
class Factory
|
4
|
+
def self.call(access_mode, name)
|
5
|
+
case access_mode
|
6
|
+
when :instance_variable
|
7
|
+
AccessModes::InstanceVariable.new(name)
|
8
|
+
when :method
|
9
|
+
AccessModes::Method.new(name)
|
10
|
+
else
|
11
|
+
raise "Access mode #{access_mode} not supported."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module AccessModes
|
3
|
+
class InstanceVariable
|
4
|
+
def initialize(name)
|
5
|
+
@name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_from(object)
|
9
|
+
object.instance_variable_get("@#{@name}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def write_to(object, value)
|
13
|
+
object.instance_variable_set("@#{@name}", value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module AccessModes
|
3
|
+
class Method
|
4
|
+
def initialize(name)
|
5
|
+
@name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_from(object)
|
9
|
+
object.send(@name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def write_to(object, value)
|
13
|
+
object.send("#{@name}=", value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module ActiveRecord
|
3
|
+
class DefaultBuildStrategy
|
4
|
+
def self.call(relation, parent_ar_object, _diff_snapshot)
|
5
|
+
if relation.collection?
|
6
|
+
parent_ar_object.send(relation.ar_name).new
|
7
|
+
else
|
8
|
+
parent_ar_object.send("build_#{relation.ar_name}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module ActiveRecord
|
3
|
+
class Read
|
4
|
+
def self.call(mapping, root)
|
5
|
+
new.call(mapping, root)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(mapping, root)
|
9
|
+
@ar_map = ArMap.new
|
10
|
+
[read(mapping, root), @ar_map]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def read(mapping, ar_model)
|
16
|
+
object = mapping.model_class.allocate
|
17
|
+
|
18
|
+
read_properties(mapping.properties, object, ar_model)
|
19
|
+
read_relations(mapping.relations, object, ar_model)
|
20
|
+
|
21
|
+
@ar_map.add_entity(object, ar_model)
|
22
|
+
|
23
|
+
object
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_properties(properties, object, ar_model)
|
27
|
+
properties.each do |property|
|
28
|
+
property.write_to(object, ar_model.send(property.ar_name))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def read_relations(relations, object, ar_model)
|
33
|
+
relations.each do |relation|
|
34
|
+
result = if !relation.virtual?
|
35
|
+
|
36
|
+
if relation.collection?
|
37
|
+
ar_model.send(relation.ar_name).map do |ar_object|
|
38
|
+
read(relation.mapping, ar_object)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
ar_object = ar_model.send(relation.ar_name)
|
42
|
+
read(relation.mapping, ar_object)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
read(relation.mapping, ar_model)
|
46
|
+
end
|
47
|
+
|
48
|
+
relation.write_to(object, result)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# TMP in propgress
|
2
|
+
|
3
|
+
module EntityMapper
|
4
|
+
module ActiveRecord
|
5
|
+
class Update
|
6
|
+
# @param mapping [EntityMapper::Mapping]
|
7
|
+
# @param snapshot_diff [EntityMapper::ObjectDiffSnapshot]
|
8
|
+
# @param ar_root [ActiveRecord::Base]
|
9
|
+
# @param ar_map [EntityMapper::ArMap]
|
10
|
+
def self.call(mapping, snapshot_diff, ar_root, ar_map)
|
11
|
+
new(ar_map).call(mapping, snapshot_diff, ar_root)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(ar_map)
|
15
|
+
@ar_map = ar_map
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(mapping, snapshot_diff, ar_root)
|
19
|
+
update(mapping, snapshot_diff, ar_root)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update(mapping, snapshot_diff, ar_object)
|
25
|
+
if snapshot_diff.removed? # TODO - check if is virtual
|
26
|
+
ar_object.destroy
|
27
|
+
else
|
28
|
+
map_properties(mapping.properties, snapshot_diff.object, ar_object)
|
29
|
+
map_relations(mapping.relations, snapshot_diff, ar_object)
|
30
|
+
ar_object.tap &:save!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def map_properties(properties, object, ar_object)
|
35
|
+
properties.each do |property|
|
36
|
+
ar_object.send("#{property.ar_name}=", property.read_from(object))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_relations(relations, snapshot_diff, parent_ar_object)
|
41
|
+
relations.each do |relation|
|
42
|
+
relation_snapshot = snapshot_diff.relations_map[relation]
|
43
|
+
|
44
|
+
if relation.virtual?
|
45
|
+
if relation_snapshot.new?
|
46
|
+
# TODO support STI/polymporphism
|
47
|
+
update(relation.mapping, relation_snapshot, parent_ar_object)
|
48
|
+
elsif relation_snapshot.removed?
|
49
|
+
# TODO ??
|
50
|
+
else
|
51
|
+
update(relation.mapping, relation_snapshot, parent_ar_object)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
if relation.collection?
|
55
|
+
relation_snapshot.each do |relation_item_diff_snapshot|
|
56
|
+
if relation_item_diff_snapshot.new?
|
57
|
+
# TODO support STI/polymporphism
|
58
|
+
ar_object = build(relation, parent_ar_object, relation_item_diff_snapshot)
|
59
|
+
update(relation.mapping, relation_item_diff_snapshot, ar_object)
|
60
|
+
else
|
61
|
+
ar_object = @ar_map.ar_object(relation_item_diff_snapshot.object)
|
62
|
+
update(relation.mapping, relation_item_diff_snapshot, ar_object)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
else
|
66
|
+
if relation_snapshot.new?
|
67
|
+
# TODO support STI/polymporphism
|
68
|
+
ar_object = build(relation, parent_ar_object, relation_item_diff_snapshot)
|
69
|
+
update(relation.mapping, relation_snapshot, ar_object)
|
70
|
+
else
|
71
|
+
ar_object = @ar_map.ar_object(relation_snapshot.object)
|
72
|
+
update(relation.mapping, relation_snapshot, ar_object)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def build(relation, parent_ar_object, relation_item_diff_snapshot)
|
80
|
+
relation.build_strategy.call(relation, parent_ar_object, relation_item_diff_snapshot)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module Mapping
|
3
|
+
class DSL
|
4
|
+
def initialize(mapping)
|
5
|
+
@mapping = mapping
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :mapping
|
9
|
+
|
10
|
+
def model(klass)
|
11
|
+
mapping.model_class = klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def property(name, ar_name = nil, **options)
|
15
|
+
mapping.add_property Property.new(name, ar_name || name, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_one(relation_name, ar_name:, **options)
|
19
|
+
inner_mapping = Mapping::Model.new.tap do |mapping|
|
20
|
+
yield DSL.new(mapping)
|
21
|
+
end
|
22
|
+
mapping.add_relation HasOneRelation.new(relation_name, ar_name, inner_mapping, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_many(relation_name, ar_name:, **options)
|
26
|
+
inner_mapping = Mapping::Model.new.tap do |mapping|
|
27
|
+
yield DSL.new(mapping)
|
28
|
+
end
|
29
|
+
mapping.add_relation HasManyRelation.new(relation_name, ar_name, inner_mapping, options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module EntityMapper
|
4
|
+
module Mapping
|
5
|
+
class Model
|
6
|
+
attr_accessor :model_class
|
7
|
+
attr_reader :properties, :relations
|
8
|
+
|
9
|
+
def initialize(properties: Set.new, relations: Set.new)
|
10
|
+
@properties = properties
|
11
|
+
@relations = relations
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_relation(relation)
|
15
|
+
@relations << relation
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_property(property)
|
19
|
+
@properties << property
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module Mapping
|
3
|
+
class Property
|
4
|
+
attr_reader :ar_name, :name
|
5
|
+
|
6
|
+
def initialize(name, ar_name, options)
|
7
|
+
@ar_name = ar_name
|
8
|
+
@name = name
|
9
|
+
@access = options.fetch(:access, :instance_variable)
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_from(object)
|
13
|
+
accessor.read_from(object)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_to(object, value)
|
17
|
+
accessor.write_to(object, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def accessor
|
23
|
+
@accessor ||= AccessModes::Factory.call(@access, @name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module Mapping
|
3
|
+
class Relation < Property
|
4
|
+
attr_reader :mapping, :ar_name, :build_strategy
|
5
|
+
|
6
|
+
def initialize(name, ar_name, mapping, options)
|
7
|
+
super(name, ar_name, options)
|
8
|
+
@mapping = mapping
|
9
|
+
@build_strategy = options.fetch(:build_strategy, ActiveRecord::DefaultBuildStrategy)
|
10
|
+
end
|
11
|
+
|
12
|
+
def virtual?
|
13
|
+
ar_name.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def collection?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module Snapshot
|
3
|
+
class TakeSnapshot
|
4
|
+
def call(object, mapping)
|
5
|
+
ObjectSnapshot.new(
|
6
|
+
object,
|
7
|
+
properties_map(object, mapping.properties),
|
8
|
+
relations_map(object, mapping.relations)
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
def properties_map(object, properties)
|
13
|
+
properties.each_with_object({}) do |property, hash|
|
14
|
+
hash[property] = property.read_from(object)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def relations_map(object, relations)
|
19
|
+
relations.each_with_object({}) do |relation, hash|
|
20
|
+
relation_value = relation.read_from(object)
|
21
|
+
|
22
|
+
if relation_value
|
23
|
+
hash[relation] = relation.collection? ?
|
24
|
+
relation_value.map { |relation_object| call(relation_object, relation.mapping) } :
|
25
|
+
call(relation_value, relation.mapping)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module SnapshotDiff
|
3
|
+
class Calculate
|
4
|
+
def self.call(previous_snapshot, current_snapshot)
|
5
|
+
call(previous_snapshot, current_snapshot)
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(previous_snapshot, current_snapshot)
|
9
|
+
state = diff_state(previous_snapshot, current_snapshot)
|
10
|
+
|
11
|
+
object = current_snapshot || previous_snapshot
|
12
|
+
|
13
|
+
ObjectDiffSnapshot.new(
|
14
|
+
object.object,
|
15
|
+
object.properties_map,
|
16
|
+
relations_map(previous_snapshot, current_snapshot),
|
17
|
+
state
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def diff_state(previous_snapshot, current_snapshot)
|
22
|
+
if previous_snapshot.nil?
|
23
|
+
:new
|
24
|
+
elsif current_snapshot.nil?
|
25
|
+
:removed
|
26
|
+
elsif properties_changed?(previous_snapshot, current_snapshot)
|
27
|
+
:changed
|
28
|
+
else
|
29
|
+
:unchanged
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def properties_changed?(previous_snapshot, current_snapshot)
|
34
|
+
current_snapshot.properties_map.any? do |property, value|
|
35
|
+
value != previous_snapshot.properties_map[property]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def relations_map(previous_snapshot, current_snapshot)
|
40
|
+
if previous_snapshot.nil?
|
41
|
+
current_snapshot.
|
42
|
+
relations_map.
|
43
|
+
each_with_object({}) do |(relation, relation_value), hash|
|
44
|
+
hash[relation] = relation.collection? ?
|
45
|
+
relation_value.map { |value| call(nil, value) } :
|
46
|
+
call(nil, relation_value)
|
47
|
+
end
|
48
|
+
elsif current_snapshot.nil?
|
49
|
+
previous_snapshot.
|
50
|
+
relations_map.
|
51
|
+
each_with_object({}) do |(relation, relation_value), hash|
|
52
|
+
hash[relation] = relation.collection? ?
|
53
|
+
relation_value.map { |value| call(value, nil) } :
|
54
|
+
call(relation_value, nil)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
current_snapshot.
|
58
|
+
relations_map.
|
59
|
+
each_with_object({}) do |(relation, relation_value), hash|
|
60
|
+
previous_snapshot_relation = previous_snapshot.relations_map[relation]
|
61
|
+
hash[relation] = if relation.collection?
|
62
|
+
existing_items = relation_value.map { |value| call(find_previous_snapshot(previous_snapshot_relation, value), value) }
|
63
|
+
removed_items = previous_snapshot_relation.reject { |value| find_previous_snapshot(relation_value, value) }.map { |value| call(value, nil) }
|
64
|
+
|
65
|
+
existing_items + removed_items
|
66
|
+
else
|
67
|
+
call(previous_snapshot_relation, relation_value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_previous_snapshot(previous_snapshot_relation, value)
|
74
|
+
previous_snapshot_relation.find { |v| v.object == value.object }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
module SnapshotDiff
|
3
|
+
ObjectDiffSnapshot = Struct.new(:object, :properties_map, :relations_map, :state) do
|
4
|
+
def new?
|
5
|
+
state == :new
|
6
|
+
end
|
7
|
+
|
8
|
+
def changed?
|
9
|
+
state == :changed
|
10
|
+
end
|
11
|
+
|
12
|
+
def removed?
|
13
|
+
state == :removed
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
class Transaction
|
3
|
+
class Context
|
4
|
+
def initialize(mapping)
|
5
|
+
@mapping = mapping
|
6
|
+
@tracked_aggregates = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(active_record_object)
|
10
|
+
mapped_entity, ar_map = ActiveRecord::Read.call(@mapping, active_record_object)
|
11
|
+
@tracked_aggregates << TrackedAggregate.new(mapped_entity, ar_map, active_record_object, @mapping)
|
12
|
+
|
13
|
+
mapped_entity
|
14
|
+
end
|
15
|
+
|
16
|
+
def save_changes
|
17
|
+
@tracked_aggregates.each(&:save_changes)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module EntityMapper
|
2
|
+
class Transaction
|
3
|
+
class TrackedAggregate
|
4
|
+
def initialize(aggregate, ar_map, active_record_object, mapping)
|
5
|
+
@aggregate = aggregate
|
6
|
+
@ar_map = ar_map
|
7
|
+
@active_record_object = active_record_object
|
8
|
+
@mapping = mapping
|
9
|
+
@initial_snapshot = take_snapshot
|
10
|
+
end
|
11
|
+
|
12
|
+
def save_changes
|
13
|
+
ActiveRecord::Update.call(@mapping, snapshot_diff, @active_record_object, @ar_map)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def snapshot_diff
|
19
|
+
EntityMapper::SnapshotDiff::Calculate.new.call(@initial_snapshot, take_snapshot)
|
20
|
+
end
|
21
|
+
|
22
|
+
def take_snapshot
|
23
|
+
Snapshot::TakeSnapshot.new.call(@aggregate, @mapping)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: entity_mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.pre.alpha
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jan Jędrychowski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zeitwerk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.16'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: combustion
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.1'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rails
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 5.2.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 5.2.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Map persisted data to and from POROs
|
126
|
+
email:
|
127
|
+
- jan@jedrychowski.org
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- ".travis.yml"
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE.txt
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- bin/console
|
140
|
+
- bin/setup
|
141
|
+
- entity_mapper.gemspec
|
142
|
+
- lib/entity_mapper.rb
|
143
|
+
- lib/entity_mapper/access_modes/factory.rb
|
144
|
+
- lib/entity_mapper/access_modes/instance_variable.rb
|
145
|
+
- lib/entity_mapper/access_modes/method.rb
|
146
|
+
- lib/entity_mapper/active_record/ar_map.rb
|
147
|
+
- lib/entity_mapper/active_record/default_build_strategy.rb
|
148
|
+
- lib/entity_mapper/active_record/read.rb
|
149
|
+
- lib/entity_mapper/active_record/update.rb
|
150
|
+
- lib/entity_mapper/mapping/dsl.rb
|
151
|
+
- lib/entity_mapper/mapping/has_many_relation.rb
|
152
|
+
- lib/entity_mapper/mapping/has_one_relation.rb
|
153
|
+
- lib/entity_mapper/mapping/model.rb
|
154
|
+
- lib/entity_mapper/mapping/property.rb
|
155
|
+
- lib/entity_mapper/mapping/relation.rb
|
156
|
+
- lib/entity_mapper/snapshot/object_snapshot.rb
|
157
|
+
- lib/entity_mapper/snapshot/take_snapshot.rb
|
158
|
+
- lib/entity_mapper/snapshot_diff/calculate.rb
|
159
|
+
- lib/entity_mapper/snapshot_diff/object_diff_snapshot.rb
|
160
|
+
- lib/entity_mapper/transaction.rb
|
161
|
+
- lib/entity_mapper/transaction/context.rb
|
162
|
+
- lib/entity_mapper/transaction/tracked_aggregate.rb
|
163
|
+
- lib/entity_mapper/version.rb
|
164
|
+
- lib/entity_mapper/zeitwerk_inflector.rb
|
165
|
+
homepage: https://github.com/gogiel/entity_mapper
|
166
|
+
licenses:
|
167
|
+
- MIT
|
168
|
+
metadata: {}
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
require_paths:
|
172
|
+
- lib
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">"
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: 1.3.1
|
183
|
+
requirements: []
|
184
|
+
rubyforge_project:
|
185
|
+
rubygems_version: 2.7.6
|
186
|
+
signing_key:
|
187
|
+
specification_version: 4
|
188
|
+
summary: Map persisted data to and from POROs
|
189
|
+
test_files: []
|