embedded 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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).
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'embedded/scope'
2
+ require 'embedded/model'
3
+
4
+ module Embedded
5
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Embedded
2
+ VERSION = '0.1.0'
3
+ 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: []