attribute-views 0.1.0
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.
- data/LICENSE +18 -0
- data/README.rdoc +1 -0
- data/lib/active_record/attribute_views.rb +110 -0
- data/rails/init.rb +2 -0
- data/test/cases/attribute_view_test.rb +54 -0
- data/test/cases/attribute_views_test.rb +93 -0
- data/test/models/composite_datetime.rb +7 -0
- data/test/models/event.rb +3 -0
- data/test/models/fee_formatter.rb +4 -0
- data/test/test_helper.rb +30 -0
- metadata +64 -0
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
(c) 2009 Fingertips, Manfred Stienstra <m.stienstra@fngtps.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
= Attribute-Views
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class AttributeView
|
3
|
+
attr_accessor :attributes
|
4
|
+
|
5
|
+
def initialize(*attributes)
|
6
|
+
self.attributes = attributes
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(record)
|
10
|
+
load(*attributes.map { |attribute| record.send(attribute) })
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(*values)
|
14
|
+
raise NoMethodError, "Please implement the `load' method on your view class"
|
15
|
+
end
|
16
|
+
|
17
|
+
def set(record, input)
|
18
|
+
parsed = Array(parse(input))
|
19
|
+
record.attributes = Hash[*attributes.zip(parsed).flatten]
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse(input)
|
23
|
+
raise NoMethodError, "Please implement the `parse' method on your view class"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Attribute views translate between value object in your application and columns in your database. A value object
|
28
|
+
# is basically any object that represents a value, for example an IP address or a rectangle. A database has a
|
29
|
+
# limited number of values it can represent internally. In order to store value object we need some sort of
|
30
|
+
# conversion from the value object to database columns. In order to read the value object we need a conversion
|
31
|
+
# from columns to a value object.
|
32
|
+
#
|
33
|
+
# Note that attribute views are a really flexible concept and in some cases it's perfectly fine to use a string
|
34
|
+
# as the value object. Later on we will look at an example of this.
|
35
|
+
#
|
36
|
+
# = Defining views
|
37
|
+
#
|
38
|
+
# In order to create an attribute view you need to create a class that takes care of the conversion between
|
39
|
+
# column values and your value object. The easiest way to create a view class is by subclassing from
|
40
|
+
# ActiveRecord::AttributeView and implementing two methods: <code>load</code> and <code>parse</code>.
|
41
|
+
#
|
42
|
+
# The <code>load</code> method receives the column values from the record and returns the value object. The
|
43
|
+
# <code>parse</code> method receives the value object and returns a list of column values.
|
44
|
+
#
|
45
|
+
# Let's assume we want to describe a plot in the lots table with the following schema:
|
46
|
+
#
|
47
|
+
# ActiveRecord::Schema.define do
|
48
|
+
# create_table :lots do |t|
|
49
|
+
# t.integer :price_in_cents
|
50
|
+
# t.integer :position_x
|
51
|
+
# t.integer :position_y
|
52
|
+
# t.integer :position_width
|
53
|
+
# t.integer :position_height
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# In our application we're going to handle the position of the Lot very often so we want to access it through
|
58
|
+
# a value object. We write the following value object to represent it as a rectangle:
|
59
|
+
#
|
60
|
+
# class Rectangle
|
61
|
+
# attr_accessor :x1, :y1, :x2, :y2
|
62
|
+
#
|
63
|
+
# def initialize(x1, y1, x2, y2)
|
64
|
+
# @x1, @y1, @x2, @y2 = x1, y1, x2, y2
|
65
|
+
# end
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# class Lot < ActiveRecord::Base
|
69
|
+
# views :position, :as => RectangleView.new(:position_x, :position_y, :position_width, :position_height)
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# class RectangleView < ActiveRecord::AttributeView
|
73
|
+
# def load(x, y, width, height)
|
74
|
+
# Rectangle.new(x, y, x + width, y + height)
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# def parse(rectangle)
|
78
|
+
# [rectangle.x1, rectangle.y1, (rectangle.x2 - rectangle.x1), (rectangle.y2 - rectangle.y1)]
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
module AttributeViews
|
82
|
+
def views(name, options={})
|
83
|
+
options.assert_valid_keys(:as)
|
84
|
+
raise ArgumentError, "Please specify a view object with the :as option" unless options.has_key?(:as)
|
85
|
+
|
86
|
+
view_name = "#{name}_view"
|
87
|
+
|
88
|
+
define_method(view_name) do
|
89
|
+
options[:as]
|
90
|
+
end
|
91
|
+
|
92
|
+
class_eval <<-READER_METHODS, __FILE__, __LINE__
|
93
|
+
def #{name} # def starts_at
|
94
|
+
#{view_name}.get(self) # starts_at_view.get(self)
|
95
|
+
end # end
|
96
|
+
READER_METHODS
|
97
|
+
|
98
|
+
class_eval <<-WRITER_METHODS, __FILE__, __LINE__
|
99
|
+
def #{name}_before_type_cast # def starts_at_before_type_cast
|
100
|
+
@#{name}_before_type_cast || #{name} # @starts_at_before_type_cast || starts_at
|
101
|
+
end # end
|
102
|
+
|
103
|
+
def #{name}=(value) # def starts_at=(value)
|
104
|
+
@#{name}_before_type_cast = value # @starts_at_before_type_cast = value
|
105
|
+
#{view_name}.set(self, value) # starts_at_view.set(self, value)
|
106
|
+
end # end
|
107
|
+
WRITER_METHODS
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.expand_path('../../test_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
class AttributeViewTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@view = ActiveRecord::AttributeView.new(:start_date, :start_time)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "view stores the attributes it composes" do
|
9
|
+
assert_equal [:start_date, :start_time], @view.attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
test "view gets a value object using a record" do
|
13
|
+
date = Date.new(2009, 5, 24)
|
14
|
+
time = Time.parse('23:00')
|
15
|
+
event = Event.new(:start_date => date, :start_time => time)
|
16
|
+
value = mock('Value')
|
17
|
+
|
18
|
+
@view.expects(:load).with(date, time).returns(value)
|
19
|
+
assert_equal value, @view.get(event)
|
20
|
+
end
|
21
|
+
|
22
|
+
test "view warns the programmer to inplement the load method" do
|
23
|
+
begin
|
24
|
+
@view.load
|
25
|
+
rescue NoMethodError => e
|
26
|
+
assert_equal "Please implement the `load' method on your view class", e.message
|
27
|
+
else
|
28
|
+
fail 'Should raise a NoMethodError'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
test "view sets a value object on a record" do
|
33
|
+
date = Date.new(2009, 5, 24)
|
34
|
+
time = Time.parse('23:00')
|
35
|
+
event = Event.new
|
36
|
+
input = '24 5 23:00'
|
37
|
+
|
38
|
+
@view.stubs(:parse).returns([date, time])
|
39
|
+
@view.set(event, input)
|
40
|
+
|
41
|
+
assert_equal date, event.start_date
|
42
|
+
assert_equal time, event.start_time
|
43
|
+
end
|
44
|
+
|
45
|
+
test "view warns the programmer to inplement the parse method" do
|
46
|
+
begin
|
47
|
+
@view.parse('24 5 23:00')
|
48
|
+
rescue NoMethodError => e
|
49
|
+
assert_equal "Please implement the `parse' method on your view class", e.message
|
50
|
+
else
|
51
|
+
fail 'Should raise a NoMethodError'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require File.expand_path('../../test_helper.rb', __FILE__)
|
2
|
+
|
3
|
+
class AttributeViewsClassmethodsTest < ActiveSupport::TestCase
|
4
|
+
test "views should raise argument error about invalid option keys" do
|
5
|
+
begin
|
6
|
+
Event.send(:views, :formatted_fee, :as => FeeFormatter.new(:fee_in_cents), :invalid => :should_raise)
|
7
|
+
rescue ArgumentError => e
|
8
|
+
assert_match /invalid/, e.message
|
9
|
+
else
|
10
|
+
fail 'Should raise an ArgumentError'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
test "views should raise argument error about missing :as option" do
|
15
|
+
begin
|
16
|
+
Event.send(:views, :formatted_fee)
|
17
|
+
rescue ArgumentError => e
|
18
|
+
assert_equal "Please specify a view object with the :as option", e.message
|
19
|
+
else
|
20
|
+
fail 'Should raise an ArgumentError'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
test "views should define a reader method for the view object" do
|
25
|
+
assert_respond_to Event.new, :starts_at_view
|
26
|
+
end
|
27
|
+
|
28
|
+
test "views should define a reader method for the value object" do
|
29
|
+
assert_respond_to Event.new, :starts_at
|
30
|
+
end
|
31
|
+
|
32
|
+
test "views should define a reader method for the value object before type cast" do
|
33
|
+
assert_respond_to Event.new, :starts_at_before_type_cast
|
34
|
+
end
|
35
|
+
|
36
|
+
test "views should define a writer method for the value object" do
|
37
|
+
assert_respond_to Event.new, :starts_at=
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class AttributeViewsFunctionTest < ActiveSupport::TestCase
|
42
|
+
def setup
|
43
|
+
@input = '24 May 23:00'
|
44
|
+
@value = "24 May 2009 23:00"
|
45
|
+
|
46
|
+
@date = Date.new(2009, 5, 24)
|
47
|
+
@time = Time.parse('23:00')
|
48
|
+
@event = Event.new(:start_date => @date, :start_time => @time)
|
49
|
+
end
|
50
|
+
|
51
|
+
test "view object reader should return the view object" do
|
52
|
+
assert_kind_of CompositeDatetime, @event.starts_at_view
|
53
|
+
end
|
54
|
+
|
55
|
+
test "view object reader should always return the same view object" do
|
56
|
+
assert_equal Event.new.starts_at_view, Event.new.starts_at_view
|
57
|
+
end
|
58
|
+
|
59
|
+
test "value object reader method should return the value object" do
|
60
|
+
@event.starts_at_view.expects(:load).with(@date, @time).returns(@value)
|
61
|
+
assert_equal @value, @event.starts_at
|
62
|
+
end
|
63
|
+
|
64
|
+
test "value object before type cast should be the value object when nothing was set" do
|
65
|
+
@event.starts_at_view.stubs(:load).with(@date, @time).returns(@value)
|
66
|
+
assert_equal @value, @event.starts_at_before_type_cast
|
67
|
+
end
|
68
|
+
|
69
|
+
test "value object before type cast should be the input when it was just set" do
|
70
|
+
@event.starts_at = @input
|
71
|
+
assert_equal @input, @event.starts_at_before_type_cast
|
72
|
+
end
|
73
|
+
|
74
|
+
test "value object writer should set attributes on the record" do
|
75
|
+
event = Event.new
|
76
|
+
event.starts_at_view.stubs(:parse).with(@input).returns([@date, @time])
|
77
|
+
|
78
|
+
event.starts_at = @input
|
79
|
+
assert_equal @date, event.start_date
|
80
|
+
assert_equal @time, event.start_time
|
81
|
+
end
|
82
|
+
|
83
|
+
test "value object writer should update attributes on the record" do
|
84
|
+
date = Date.new(2009, 12, 31)
|
85
|
+
time = Time.parse('01:00')
|
86
|
+
input = '31 12 01:00'
|
87
|
+
@event.starts_at_view.stubs(:parse).with(input).returns([date, time])
|
88
|
+
|
89
|
+
@event.starts_at = input
|
90
|
+
assert_equal date, @event.start_date
|
91
|
+
assert_equal time, @event.start_time
|
92
|
+
end
|
93
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems' rescue nil
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 'active_support/test_case'
|
8
|
+
|
9
|
+
require 'mocha'
|
10
|
+
|
11
|
+
plugin_dir = File.expand_path('../../', __FILE__)
|
12
|
+
$:.unshift(File.join(plugin_dir, 'lib'), File.join(plugin_dir, 'rails'))
|
13
|
+
require 'init'
|
14
|
+
|
15
|
+
test_dir = File.expand_path('../', __FILE__)
|
16
|
+
$:.unshift(File.join(test_dir, 'cases'), File.join(test_dir, 'models'))
|
17
|
+
require 'fee_formatter'
|
18
|
+
require 'composite_datetime'
|
19
|
+
require 'event'
|
20
|
+
|
21
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
22
|
+
|
23
|
+
ActiveRecord::Migration.verbose = false
|
24
|
+
ActiveRecord::Schema.define(:version => 1) do
|
25
|
+
create_table :events, :force => true do |t|
|
26
|
+
t.integer :fee_in_cents
|
27
|
+
t.date :start_date
|
28
|
+
t.time :start_time
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attribute-views
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Manfred Stienstra
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-26 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A plugin converting between value objects and record columns.
|
17
|
+
email: manfred@fngtps.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- lib/active_record/attribute_views.rb
|
29
|
+
- rails/init.rb
|
30
|
+
has_rdoc: true
|
31
|
+
homepage: http://fingertips.github.com
|
32
|
+
licenses: []
|
33
|
+
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options:
|
36
|
+
- --charset=UTF-8
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.3.5
|
55
|
+
signing_key:
|
56
|
+
specification_version: 3
|
57
|
+
summary: A plugin converting between value objects and record columns.
|
58
|
+
test_files:
|
59
|
+
- test/cases/attribute_view_test.rb
|
60
|
+
- test/cases/attribute_views_test.rb
|
61
|
+
- test/models/composite_datetime.rb
|
62
|
+
- test/models/event.rb
|
63
|
+
- test/models/fee_formatter.rb
|
64
|
+
- test/test_helper.rb
|