attribute-views 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'active_record/attribute_views'
2
+ ActiveRecord::Base.send(:extend, ActiveRecord::AttributeViews)
@@ -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
@@ -0,0 +1,7 @@
1
+ class CompositeDatetime < ActiveRecord::AttributeView
2
+ def load(date, time)
3
+ end
4
+
5
+ def parse(input)
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class Event < ActiveRecord::Base
2
+ views :starts_at, :as => CompositeDatetime.new(:start_date, :start_time)
3
+ end
@@ -0,0 +1,4 @@
1
+ class FeeFormatter < ActiveRecord::AttributeView
2
+ def load(fee_in_cents)
3
+ end
4
+ end
@@ -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