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 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