embedded 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +225 -0
- data/Rakefile +33 -0
- data/lib/embedded.rb +5 -0
- data/lib/embedded/model.rb +36 -0
- data/lib/embedded/scope.rb +32 -0
- data/lib/embedded/version.rb +3 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5b50128771dd929a10a666a66162b6d474cae251
|
4
|
+
data.tar.gz: ab834feb4633a90efd4e74e6cc8ad6605bc7685c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec6877b0001ab2025c75dd53f7aefb8dd6244d91b9a429bf262801e04317a0d8785f2d9bd3b384adc78c50bb288c65b48928de616c3a2424e9d0d206f0abc54a
|
7
|
+
data.tar.gz: 7250302a6d36404c2d77f070f9ecf092d5283a538556e4140475a504f5772ce841837e59a2b15e70a5861ae03129f633c60c91e05fbc9048e5392e37aee13505
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 jvillarejo
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Embedded
|
2
|
+
|
3
|
+
Embedded is a small rails engine to correctly persist Value Objects in Active Record Object columns
|
4
|
+
|
5
|
+
## Motivation
|
6
|
+
|
7
|
+
There objects in every domain that doesn't have an identity by itself but their equality depends on the values of their attributes.
|
8
|
+
|
9
|
+
Example: prices, any magnitude, a color, a polygon.
|
10
|
+
|
11
|
+
Defining a value objects lets you extract common behavior from your current bloated active record objects.
|
12
|
+
|
13
|
+
Every time that I did this I had to define a getter and setter for the value object and map those to the columns of the object that gets persisted, so I thought it would be better to define those value object attributes in a declarative way and let the plugin do the magic behind.
|
14
|
+
|
15
|
+
For more info about value objects check this links:
|
16
|
+
|
17
|
+
* [Value Object by Martin Fowler](https://martinfowler.com/bliki/ValueObject.html)
|
18
|
+
* [Don't forget about Value Objects](https://plainoldobjects.com/2017/03/19/dont-forget-about-value-objects)
|
19
|
+
|
20
|
+
## Features
|
21
|
+
|
22
|
+
It lets you define value objects and map them into the corresponding value object attributes columns
|
23
|
+
|
24
|
+
It lets you query by those value objects in a safe way, without monkeypatching the default activerecord classes
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
Add this line to your application's Gemfile:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
gem 'embedded'
|
31
|
+
```
|
32
|
+
|
33
|
+
Create an initializer in your rails project
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# config/initializers/embedded_initializer
|
37
|
+
ActiveRecord::Base.send(:extend, Embedded::Model)
|
38
|
+
```
|
39
|
+
|
40
|
+
Or you can extend the ApplicationRecord class
|
41
|
+
```ruby
|
42
|
+
class ApplicationRecord < ActiveRecord::Base
|
43
|
+
extend Embedded::Model
|
44
|
+
self.abstract_class = true
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
## Usage
|
50
|
+
|
51
|
+
Let's say you have a Reservation in your active record model and that reservation has a start_time, and end_time. And want you calculate the duration in hours of the period.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class Reservation < ApplicationRecord
|
55
|
+
|
56
|
+
def period_in_hours
|
57
|
+
(end_time - start_time).round / 60 / 60
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
reservation = Reservation.new(start_time: Time.now, end_time: 3.hours.ago)
|
64
|
+
reservation.period_in_hours
|
65
|
+
# => 3
|
66
|
+
```
|
67
|
+
|
68
|
+
If you want your model to have cohesion, something is not quite right when a reservation is calculating time intervals of a period, but let's keep that for a while.
|
69
|
+
|
70
|
+
You have a new requirement, you need to persist available hours for a shop, and you want to calculate the duration in hours of the available time
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class Shop < ApplicationRecord
|
74
|
+
def opening_period_in_hours
|
75
|
+
(open_time - closed_time).round / 60 / 60
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
shop = Shop.new(start_time: Time.now, end_time: 3.hours.ago)
|
82
|
+
shop.period_in_hours
|
83
|
+
# => 3
|
84
|
+
```
|
85
|
+
|
86
|
+
Now you are starting to see the problem. That behavior belongs to a TimeInterval object that has start_time and end_time and let's you calculate all durations and intervals you want.
|
87
|
+
|
88
|
+
So with embedded in hand we can do this.
|
89
|
+
|
90
|
+
We have a reservation that has an attribute scheduled_time of type TimeInterval and will map the start_time and end_time attributes to the ones in TimeInterval
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class Reservation < ApplicationRecord
|
94
|
+
embeds :scheduled_time, attrs: [:start_time, :end_time], class_name: 'TimeInterval'
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
The same here with the shop
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Shop < ApplicationRecord
|
102
|
+
embeds :available_time, attrs: [:start_time, :end_time], class_name: 'TimeInterval'
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
TimeInterval is a plain PORO, it just need the attributes that you defined in your activerecord objects mapping.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class TimeInterval
|
110
|
+
attr_reader :start_time, :end_time
|
111
|
+
|
112
|
+
def initialize(values)
|
113
|
+
@start_time = values.fetch(:start_time)
|
114
|
+
@end_time = values.fetch(:end_time)
|
115
|
+
|
116
|
+
# you can validate as you want, here or in a valid? method that you define
|
117
|
+
end
|
118
|
+
|
119
|
+
def hours
|
120
|
+
minutes / 60
|
121
|
+
end
|
122
|
+
|
123
|
+
def minutes
|
124
|
+
seconds / 60
|
125
|
+
end
|
126
|
+
|
127
|
+
def seconds
|
128
|
+
(end_time - start_time).round
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Now you can pass available time to shop constructor and check the duration directly
|
134
|
+
```ruby
|
135
|
+
t = TimeInterval.new(start_time: Time.now, end_time: 3.hours.ago)
|
136
|
+
shop = Shop.new(available_time: t)
|
137
|
+
shop.available_time.hours
|
138
|
+
# => 3
|
139
|
+
```
|
140
|
+
Also you can persist the reservation and when fetching it back of the db it will scheduled_time will be a TimeInterval
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
t = TimeInterval.new(start_time: Time.now, end_time: 3.hours.ago)
|
144
|
+
reservation = Reservation.create(scheduled_time: t)
|
145
|
+
|
146
|
+
reservation.reload
|
147
|
+
|
148
|
+
reservation.scheduled_time.hours
|
149
|
+
# => 3
|
150
|
+
```
|
151
|
+
|
152
|
+
### Database Mapping
|
153
|
+
|
154
|
+
Your table columns have to be named in a specific way so they are mapped correctly, for example:
|
155
|
+
|
156
|
+
Reservation attribute name is scheduled_time and as TimeInterval has start_time and end_time your column names must be defined as scheduled_time_start_time and scheduled_time_end_time
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class CreateReservations < ActiveRecord::Migration
|
160
|
+
def change
|
161
|
+
create_table :reservations do |t|
|
162
|
+
t.timestamp :scheduled_time_start_time
|
163
|
+
t.timestamp :scheduled_time_end_time
|
164
|
+
|
165
|
+
t.timestamps
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
Shop attribute name is available time and as TimeInterval has start_time and end_time attributes, your column names here must be defined as available_time_start_time and available_time_end_time
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class CreateShops < ActiveRecord::Migration
|
175
|
+
def change
|
176
|
+
create_table :shops do |t|
|
177
|
+
t.timestamp :available_time_start_time
|
178
|
+
t.timestamp :available_time_end_time
|
179
|
+
|
180
|
+
t.timestamps
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
### Querying
|
187
|
+
|
188
|
+
For example you have now a model that has prices in different currencies.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
price = Price.new(currency: 'BTC', amount: BigDecimal.new('2.5'))
|
192
|
+
my_gamble = BuyOrder.create(price: price, created_at: Time.new(2015,03,17))
|
193
|
+
|
194
|
+
bubble_price = Price.new(currency: 'USD', amount: BigDecimal.new('5257'))
|
195
|
+
my_intelligent_investment = SellOrder.create(price: price, created_at: Time.new(2017,10,18))
|
196
|
+
```
|
197
|
+
|
198
|
+
And we want to check the orders for a specific price we can do this.
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
price = Price.new(currency: 'BTC', amount: BigDecimal.new('2.5'))
|
202
|
+
gambles = BuyOrder.embedded.where(price: price).to_a
|
203
|
+
|
204
|
+
# => [#<Order id: 1, price_currency: "BTC", price_amount: #<BigDecimal:555e61776630,'0.25E1',18(36)>, created_at: "2017-03-17 17:11:00", updated_at: "2017-10-18 17:11:00">]
|
205
|
+
```
|
206
|
+
|
207
|
+
In order to search with values you should specify with embedded method. This decision was made because I didn't want to monkey patch the activerecord method 'where'.
|
208
|
+
|
209
|
+
With this way the embedded method returns another scope in which the method 'where' is overridden. If you want to query by the column attributes you can still use the default 'where' method.
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
jpm_orders = BuyOrder.where(price_currency: 'BTC')
|
213
|
+
jpm_orders.find_each {|o| o.trader.fire! }
|
214
|
+
```
|
215
|
+
|
216
|
+
## Contributing
|
217
|
+
|
218
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
219
|
+
|
220
|
+
- [Report bugs](https://github.com/jvillarejo/embedded/issues)
|
221
|
+
- Fix bugs and [submit pull requests](https://github.com/jvillarejo/embedded/pulls)
|
222
|
+
- Suggest or add new features
|
223
|
+
|
224
|
+
## License
|
225
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Embedded'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
task default: :test
|
data/lib/embedded.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Embedded
|
2
|
+
module Model
|
3
|
+
def embedded_column_names(embeddable_attr, attributes)
|
4
|
+
attributes.inject({}) do |hash, a|
|
5
|
+
hash.merge(:"#{embeddable_attr}_#{a}" => a)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def embedded
|
10
|
+
Embedded::Scope.new(self.all,embedded_attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def embeds(embeddable_attr, options = {})
|
14
|
+
cattr_accessor :embedded_attributes
|
15
|
+
self.embedded_attributes ||= {}
|
16
|
+
self.embedded_attributes[embeddable_attr] = options
|
17
|
+
|
18
|
+
attributes = options[:attrs]
|
19
|
+
columns = embedded_column_names(embeddable_attr,attributes)
|
20
|
+
clazz = options[:class_name] ? options[:class_name].constantize : embeddable_attr.to_s.camelcase.constantize
|
21
|
+
|
22
|
+
self.send(:define_method, embeddable_attr) do
|
23
|
+
values = columns.inject({}) do |hash,(k,v)|
|
24
|
+
hash.merge(v=>read_attribute(k))
|
25
|
+
end
|
26
|
+
clazz.new(values)
|
27
|
+
end
|
28
|
+
|
29
|
+
self.send(:define_method, :"#{embeddable_attr}=") do |v|
|
30
|
+
columns.each do |k,a|
|
31
|
+
self.write_attribute(k, v.send(a))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Embedded
|
2
|
+
class Scope
|
3
|
+
def initialize(scope,attributes)
|
4
|
+
@attributes = attributes
|
5
|
+
@scope = scope
|
6
|
+
end
|
7
|
+
|
8
|
+
def embedded_attributes_for(embeddable_attr, value = nil)
|
9
|
+
@attributes[embeddable_attr][:attrs].inject({}) do |a,attr|
|
10
|
+
a.merge(:"#{embeddable_attr}_#{attr}" => value ? value.send(attr) : nil)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def where(opts = :chain, *rest)
|
15
|
+
if opts.is_a?(Hash)
|
16
|
+
opts = opts.inject({}) do |h,(k,v)|
|
17
|
+
if @attributes[k]
|
18
|
+
h.merge(embedded_attributes_for(k,v))
|
19
|
+
else
|
20
|
+
h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
self.class.new(@scope.where(opts, *rest), @attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(method, *args, &block)
|
29
|
+
@scope.send(method,*args,&block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: embedded
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- jvillarejo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sqlite3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
description: Rails plugin that makes value objects embedded into activerecord objects
|
42
|
+
email:
|
43
|
+
- arzivian87@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- MIT-LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- lib/embedded.rb
|
52
|
+
- lib/embedded/model.rb
|
53
|
+
- lib/embedded/scope.rb
|
54
|
+
- lib/embedded/version.rb
|
55
|
+
homepage: https://github.com/jvillarejo/embedded
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 2.5.1
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Use value objects with activerecord objects
|
79
|
+
test_files: []
|