degu 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ require 'degu/renum/enumerated_value'
2
+
3
+ module Degu
4
+ module Renum
5
+ module EnumeratedValueTypeFactory
6
+ class << self
7
+ def create nest, type_name, values, &block
8
+ klass = create_class nest, type_name
9
+ create_values klass, values, &block
10
+ end
11
+
12
+ def create_class nest, type_name
13
+ klass = Class.new EnumeratedValue
14
+ nest.const_set(type_name, klass)
15
+ klass
16
+ end
17
+
18
+ def create_values klass, values, &block
19
+ setup_for_definition_in_block(klass) if values == :defined_in_block
20
+ klass.class_eval &block if block_given?
21
+ if values == :defined_in_block
22
+ begin
23
+ klass.block_defined_values.each do |value_name, init_args, instance_block|
24
+ value = klass.new(value_name)
25
+ klass.const_set(value_name, value)
26
+ value.instance_eval &instance_block if instance_block
27
+ value.init *init_args if value.respond_to? :init
28
+ end
29
+ ensure
30
+ teardown_from_definition_in_block(klass)
31
+ end
32
+ else
33
+ values.each do |name|
34
+ klass.const_set(name, klass.new(name))
35
+ end
36
+ end
37
+ klass.values.freeze
38
+ end
39
+
40
+ def setup_for_definition_in_block klass
41
+ klass.class_eval do
42
+ def self.block_defined_values
43
+ @block_defined_values ||= []
44
+ end
45
+
46
+ def self.method_missing value_name, *init_args, &instance_block
47
+ block_defined_values << [value_name, init_args, instance_block]
48
+ end
49
+ end
50
+ end
51
+
52
+ def teardown_from_definition_in_block klass
53
+ class << klass
54
+ remove_method :block_defined_values
55
+ remove_method :method_missing
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/degu/renum.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'degu/renum/enumerated_value_type_factory'
2
+
3
+ module Degu
4
+ # Requiring 'renum' mixes the Renum module into both the main Object and
5
+ # Module, so it can be called from anywhere that you might reasonably
6
+ # define an enumeration with an implicit receiver.
7
+ module Renum
8
+
9
+ # Figures out whether the new enumeration will live in Object or the
10
+ # receiving Module, then delegates to EnumeratedValueTypeFactory#create for
11
+ # all the real work.
12
+ def enum(type_name, values = :defined_in_block, &block)
13
+ nest = self.is_a?(Module) ? self : Object
14
+ EnumeratedValueTypeFactory.create(nest, type_name, values, &block)
15
+ end
16
+ end
17
+ end
data/lib/degu/rude.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Degu
2
+ end
3
+ require 'degu/polite'
4
+ class Object
5
+ include Degu::Renum
6
+ end
7
+ if defined?(ActiveRecord::Base)
8
+ class ActiveRecord::Base
9
+ include Degu::HasEnum
10
+ include Degu::HasSet
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Degu
2
+ # Degu version
3
+ VERSION = '0.0.4'
4
+ VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
5
+ VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
+ VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
7
+ VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
8
+ end
data/lib/degu.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Degu
2
+ end
3
+ require 'degu/rude'
@@ -0,0 +1,281 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ enum :Status, [ :NOT_STARTED, :IN_PROGRESS, :COMPLETE ]
4
+
5
+ enum :Fuzzy, [ :FooBar, :BarFoo ]
6
+
7
+ describe "basic enum" do
8
+
9
+ it "creates a class for the value type" do
10
+ Status.should be_an_instance_of(Class)
11
+ end
12
+
13
+ it "makes each value an instance of the value type" do
14
+ Status::NOT_STARTED.should be_an_instance_of(Status)
15
+ end
16
+
17
+ it "exposes array of values" do
18
+ Status.values.should == [Status::NOT_STARTED, Status::IN_PROGRESS, Status::COMPLETE]
19
+ Status.all.should == [Status::NOT_STARTED, Status::IN_PROGRESS, Status::COMPLETE]
20
+ end
21
+
22
+ it "groks first and last to retreive value" do
23
+ Status.first.should == Status::NOT_STARTED
24
+ Status.last.should == Status::COMPLETE
25
+ end
26
+
27
+ it "enumerates over values" do
28
+ Status.map {|s| s.name}.should == %w[NOT_STARTED IN_PROGRESS COMPLETE]
29
+ end
30
+
31
+ it "indexes values" do
32
+ Status[2].should == Status::COMPLETE
33
+ Color[0].should == Color::RED
34
+ Status['2'].should == Status::COMPLETE
35
+ Color['0'].should == Color::RED
36
+ end
37
+
38
+ it "provides index lookup on values" do
39
+ Status::IN_PROGRESS.index.should == 1
40
+ Color::GREEN.index.should == 1
41
+ end
42
+
43
+ it "provides an id on values" do
44
+ Status::IN_PROGRESS.id.should == 1
45
+ Color::GREEN.id.should == 1
46
+ end
47
+
48
+
49
+ it "provides name lookup on values" do
50
+ Status.with_name('IN_PROGRESS').should == Status::IN_PROGRESS
51
+ Color.with_name('GREEN').should == Color::GREEN
52
+ Color.with_name('IN_PROGRESS').should be_nil
53
+ end
54
+
55
+ it "provides fuzzy name lookup on values" do
56
+ Fuzzy[0].should == Fuzzy::FooBar
57
+ Fuzzy[1].should == Fuzzy::BarFoo
58
+ Fuzzy[:FooBar].should == Fuzzy::FooBar
59
+ Fuzzy[:BarFoo].should == Fuzzy::BarFoo
60
+ Fuzzy['FooBar'].should == Fuzzy::FooBar
61
+ Fuzzy['BarFoo'].should == Fuzzy::BarFoo
62
+ Fuzzy[:foo_bar].should == Fuzzy::FooBar
63
+ Fuzzy[:bar_foo].should == Fuzzy::BarFoo
64
+ Fuzzy['foo_bar'].should == Fuzzy::FooBar
65
+ Fuzzy['bar_foo'].should == Fuzzy::BarFoo
66
+ Fuzzy[Fuzzy::FooBar].should == Fuzzy::FooBar
67
+ Fuzzy[Fuzzy::BarFoo].should == Fuzzy::BarFoo
68
+ end
69
+
70
+ it "provides a reasonable to_s for values" do
71
+ Status::NOT_STARTED.to_s.should == "Status::NOT_STARTED"
72
+ end
73
+
74
+ it "makes values comparable" do
75
+ Color::RED.should < Color::GREEN
76
+ end
77
+ end
78
+
79
+ module MyNamespace
80
+ enum :FooValue, %w( Bar Baz Bat )
81
+ end
82
+
83
+ describe "nested enum" do
84
+ it "is namespaced in the containing module or class" do
85
+ MyNamespace::FooValue::Bar.class.should == MyNamespace::FooValue
86
+ end
87
+ end
88
+
89
+ enum :Color, [ :RED, :GREEN, :BLUE ] do
90
+ def abbr
91
+ name[0..0]
92
+ end
93
+ end
94
+
95
+ describe "enum with a block" do
96
+ it "can define additional instance methods" do
97
+ Color::RED.abbr.should == "R"
98
+ end
99
+ end
100
+
101
+ enum :Size do
102
+ Small("Really really tiny")
103
+ Medium("Sort of in the middle")
104
+ Large("Quite big")
105
+ Unknown()
106
+
107
+ attr_reader :description
108
+
109
+ def init description = nil
110
+ @description = description || "NO DESCRIPTION GIVEN"
111
+ end
112
+ end
113
+
114
+ enum :HairColor do
115
+ BLONDE()
116
+ BRUNETTE()
117
+ RED()
118
+ end
119
+
120
+ describe "enum with no values array and values declared in the block" do
121
+ it "provides another way to declare values where an init method can take extra params" do
122
+ Size::Small.description.should == "Really really tiny"
123
+ end
124
+
125
+ it "works the same as the basic form with respect to ordering" do
126
+ Size.values.should == [Size::Small, Size::Medium, Size::Large, Size::Unknown]
127
+ end
128
+
129
+ it "responds as expected to arbitrary method calls, in spite of using method_missing for value definition" do
130
+ lambda { Size.ExtraLarge() }.should raise_error(NoMethodError)
131
+ end
132
+
133
+ it "supports there being no extra data and no init() method defined, if you don't need them" do
134
+ HairColor::BLONDE.name.should == "BLONDE"
135
+ end
136
+
137
+ it "calls the init method even if no arguments are provided" do
138
+ Size::Unknown.description.should == "NO DESCRIPTION GIVEN"
139
+ end
140
+ end
141
+
142
+ enum :Rating do
143
+ NotRated()
144
+
145
+ ThumbsDown do
146
+ def description
147
+ "real real bad"
148
+ end
149
+ end
150
+
151
+ ThumbsUp do
152
+ def description
153
+ "so so good"
154
+ end
155
+
156
+ def thumbs_up_only_method
157
+ "this method is only defined on ThumbsUp"
158
+ end
159
+ end
160
+
161
+ def description
162
+ raise NotImplementedError
163
+ end
164
+ end
165
+
166
+ describe "an enum with instance-specific method definitions" do
167
+ it "allows each instance to have its own behavior" do
168
+ Rating::ThumbsDown.description.should == "real real bad"
169
+ Rating::ThumbsUp.description.should == "so so good"
170
+ end
171
+
172
+ it "uses the implementation given at the top level if no alternate definition is given for an instance" do
173
+ lambda { Rating::NotRated.description }.should raise_error(NotImplementedError)
174
+ end
175
+
176
+ it "allows definition of a method on just one instance" do
177
+ Rating::ThumbsUp.thumbs_up_only_method.should == "this method is only defined on ThumbsUp"
178
+ lambda { Rating::NotRated.thumbs_up_only_method }.should raise_error(NoMethodError)
179
+ end
180
+ end
181
+
182
+ describe "<=> comparison issue that at one time was causing segfaults on MRI" do
183
+ it "doesn't cause the ruby process to bomb!" do
184
+ Color::RED.should < Color::GREEN
185
+ Color::RED.should_not > Color::GREEN
186
+ Color::RED.should < Color::BLUE
187
+ end
188
+ end
189
+
190
+ modify_frozen_error =
191
+ begin
192
+ [].freeze << true
193
+ rescue => e
194
+ e.class
195
+ end
196
+
197
+ describe "prevention of subtle and annoying bugs" do
198
+ it "prevents you modifying the values array" do
199
+ lambda { Color.values << 'some crazy value' }.should raise_error(modify_frozen_error, /can't modify frozen/)
200
+ end
201
+
202
+ it "prevents you modifying the name hash" do
203
+ lambda { Color.values_by_name['MAGENTA'] = 'some crazy value' }.should raise_error(modify_frozen_error, /can't modify frozen/)
204
+ end
205
+
206
+ it "prevents you modifying the name of a value" do
207
+ lambda { Color::RED.name << 'dish-Brown' }.should raise_error(modify_frozen_error, /can't modify frozen/)
208
+ end
209
+ end
210
+
211
+ enum :Foo1 do
212
+ field :foo
213
+ field :bar, :default => 'my bar'
214
+ field :baz do |obj|
215
+ obj.__id__
216
+ end
217
+
218
+ Baz(
219
+ :foo => 'my foo'
220
+ )
221
+ end
222
+
223
+ enum :Foo2 do
224
+ field :foo
225
+ field :bar, :default => 'my bar'
226
+ field :baz do |obj|
227
+ obj.__id__
228
+ end
229
+
230
+
231
+ Baz(
232
+ :foo => 'my foo'
233
+ )
234
+
235
+ def init(opts = {})
236
+ super
237
+ end
238
+ end
239
+
240
+ describe "use field method to specify methods and defaults" do
241
+ it "should define methods with defaults for fields" do
242
+ Foo1::Baz.foo.should == "my foo"
243
+ Foo2::Baz.foo.should == "my foo"
244
+ Foo1::Baz.bar.should == "my bar"
245
+ Foo2::Baz.bar.should == "my bar"
246
+ Foo1::Baz.baz.should == Foo1::Baz.__id__
247
+ Foo2::Baz.baz.should == Foo2::Baz.__id__
248
+ end
249
+ end
250
+
251
+ describe "serialize and deserialize via Marshal" do
252
+ it "should define methods with defaults for fields" do
253
+ Marshal.load(Marshal.dump(Status::NOT_STARTED)).should == Status::NOT_STARTED
254
+ end
255
+ end
256
+
257
+ if defined?(::JSON)
258
+ describe "serialize and deserialize via JSON" do
259
+ it "should define methods with defaults for fields" do
260
+ JSON(JSON(Status::NOT_STARTED)).should == Status::NOT_STARTED
261
+ end
262
+
263
+ it "should not serialize fields by default" do
264
+ foo1_json = JSON(Foo1::Baz)
265
+ foo1_hash = JSON.parse(foo1_json, :create_additions => false)
266
+ foo1_hash.keys.sort.should == %w[name json_class].sort
267
+ end
268
+
269
+ it "should serialize all fields if desired" do
270
+ foo1_json = Foo1::Baz.to_json(:fields => true)
271
+ foo1_hash = JSON.parse(foo1_json, :create_additions => false)
272
+ foo1_hash.keys.sort.should == %w[name json_class bar baz foo].sort
273
+ end
274
+
275
+ it "should serialize requested fields" do
276
+ foo1_json = Foo1::Baz.to_json(:fields => [ :bar, 'baz' ])
277
+ foo1_hash = JSON.parse(foo1_json, :create_additions => false)
278
+ foo1_hash.keys.sort.should == %w[name json_class bar baz].sort
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,3 @@
1
+ require 'rspec'
2
+
3
+ require 'degu'
@@ -0,0 +1,184 @@
1
+ require 'test_helper'
2
+
3
+ class HasEnumTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ setup_db
7
+ end
8
+
9
+ def teardown
10
+ teardown_db
11
+ end
12
+
13
+ def test_should_have_enum
14
+ assert ClassWithEnum.respond_to?(:has_enum)
15
+ product_enum = ClassWithEnum.new(:product => Product::Silver)
16
+ assert_equal Product::Silver, product_enum.product
17
+ end
18
+
19
+ def test_should_have_enum_column_value_set_to_enum_name
20
+ product_enum = ClassWithEnum.new
21
+ product_enum.product = Product::Gold
22
+ assert_equal Product::Gold.name, product_enum.product_type
23
+ end
24
+
25
+ def test_should_be_nullable
26
+ product_enum = ClassWithEnum.new
27
+ product_enum.product = Product::Silver
28
+ product_enum.reset_enum_changed
29
+ assert_equal false, product_enum.product_has_changed?
30
+ product_enum.product = nil
31
+ assert_nil product_enum.product
32
+ assert_equal true, product_enum.product_has_changed?
33
+ end
34
+
35
+ def test_should_be_nil_if_spaces_string
36
+ product_enum = ClassWithEnum.new
37
+ product_enum.product = Product::Silver
38
+ product_enum.reset_enum_changed
39
+ assert_equal false, product_enum.product_has_changed?
40
+ product_enum.product = " "
41
+ assert_nil product_enum.product
42
+ assert_equal true, product_enum.product_has_changed?
43
+ end
44
+
45
+ def test_should_be_setable_by_string
46
+ product_enum = ClassWithEnum.new
47
+ product_enum.product = Product::Silver
48
+ product_enum.reset_enum_changed
49
+ assert_equal false, product_enum.product_has_changed?
50
+ product_enum.product = 'Gold'
51
+ assert_equal Product::Gold, product_enum.product
52
+ assert_equal true, product_enum.product_has_changed?
53
+ end
54
+
55
+ def test_should_be_setable_by_symbol
56
+ product_enum = ClassWithEnum.new
57
+ product_enum.product = Product::Silver
58
+ product_enum.reset_enum_changed
59
+ assert_equal false, product_enum.product_has_changed?
60
+ product_enum.product = :gold
61
+ assert_equal Product::Gold, product_enum.product
62
+ assert_equal true, product_enum.product_has_changed?
63
+ end
64
+
65
+ def test_should_be_setable_by_number
66
+ product_enum = ClassWithEnum.new
67
+ product_enum.product = Product::Silver
68
+ product_enum.reset_enum_changed
69
+ assert_equal false, product_enum.product_has_changed?
70
+ product_enum.product = 1
71
+ assert_equal Product::Gold, product_enum.product
72
+ assert_equal true, product_enum.product_has_changed?
73
+ end
74
+
75
+ def test_should_accept_only_defined_enums
76
+ product_enum = ClassWithEnum.new
77
+
78
+ assert_raise(ArgumentError) { product_enum.product = Fakes::NOT_DEFINIED }
79
+ assert_raise(ArgumentError) { product_enum.product = "Product::Titanium" }
80
+ assert_raise(ArgumentError) { product_enum.product = Product }
81
+ assert_raise(ArgumentError) { product_enum.product = :symbol }
82
+ end
83
+
84
+ def test_should_not_set_enum_in_setter_if_new_enum_is_equal_to_current_enum
85
+ product_enum = ClassWithEnum.new(:product => Product::Silver)
86
+ assert product_enum.product=Product::Gold
87
+ end
88
+
89
+ def test_should_have_custom_column_name
90
+ enum_with_custom_column_name = ClassWithCustomNameEnum.new
91
+ enum_with_custom_column_name.product = Product::Gold
92
+ assert_equal Product::Gold, enum_with_custom_column_name.product
93
+ end
94
+
95
+ def test_should_mark_enum_as_changed_if_enum_column_was_set_directly
96
+ product_enum = ClassWithEnum.new
97
+ assert !product_enum.product_has_changed?
98
+ product_enum.product_type = "Gold"
99
+ assert product_enum.product_has_changed?
100
+ end
101
+
102
+ def test_should_only_change_enum_column_if_other_value_is_set
103
+ product_enum = ClassWithEnum.new
104
+ product_enum.product_type = "Gold"
105
+ product_enum.instance_variable_set("@enum_changed", false)
106
+ product_enum.product_type = "Gold"
107
+ assert !product_enum.product_has_changed?
108
+ end
109
+
110
+ def test_should_have_custom_column_value_set_to_enum_name
111
+ enum_with_custom_column_name = ClassWithCustomNameEnum.new
112
+ enum_with_custom_column_name.product = Product::Gold
113
+ assert_equal Product::Gold.name, enum_with_custom_column_name.product_enum
114
+ end
115
+
116
+ def test_should_raise_class_not_found_exception_if_enum_class_not_found
117
+ assert_raise(NameError) { ClassWithoutEnum.has_enum :foo }
118
+ end
119
+
120
+ def test_should_raise_argument_error_if_enum_is_no_renum_enum
121
+ assert_raise(ArgumentError) { ClassWithoutEnum.has_enum :array }
122
+ end
123
+
124
+ def test_should_know_if_enum_has_changed
125
+ product_enum = ClassWithEnum.new(:product => Product::Silver)
126
+ assert product_enum.product_has_changed?
127
+ product_enum.save
128
+ product_enum.reload
129
+ product_enum.product = Product::Gold
130
+ assert product_enum.product_has_changed?
131
+ end
132
+
133
+ def test_should_not_fail_if_no_enum_was_set_yet
134
+ enum_mixin = ClassWithEnum.new
135
+ assert_nothing_raised(TypeError) { enum_mixin.product }
136
+ end
137
+
138
+ def test_should_not_change_if_same_enum_was_assigned
139
+ enum_mixin = ClassWithEnum.new(:product => Product::Silver)
140
+ enum_mixin.save
141
+ enum_mixin.reload
142
+ enum_mixin.product = Product::Silver
143
+ assert !enum_mixin.product_has_changed?
144
+ end
145
+
146
+ def test_should_have_reset_changed_state_after_save
147
+ enum_mixin = ClassWithEnum.new(:product => Product::Silver)
148
+ enum_mixin.save
149
+ enum_mixin.reload
150
+ assert !enum_mixin.product_has_changed?
151
+ end
152
+
153
+ def test_should_not_be_able_to_set_invalid_enum
154
+ enum_mixin = ClassWithEnum.new
155
+ assert_raise(NameError) { enum_mixin.product = Product::Platin }
156
+ end
157
+
158
+ def test_should_not_be_able_to_set_invalid_enum_2
159
+ enum_mixin = ClassWithEnum.new
160
+ enum_mixin[:product_type] = "Platin"
161
+ enum_mixin.valid?
162
+ assert enum_mixin.errors[:product_type]
163
+ end
164
+
165
+ def test_should_not_return_validation_error_if_enum_is_nil
166
+ enum_mixin = ClassWithEnum.new
167
+ enum_mixin.valid?
168
+ assert_equal [], enum_mixin.errors[:product_type]
169
+ end
170
+
171
+ def test_should_return_nil_if_enum_is_of_wrong_type
172
+ enum_mixin = ClassWithEnum.new
173
+ enum_mixin[:product_type] = "Platin"
174
+ assert_nil enum_mixin.product
175
+ end
176
+
177
+ def test_should_execute_all_callbacks
178
+ enum_mixin = ClassWithEnum.new
179
+ enum_mixin.product = Product::Gold
180
+ assert enum_mixin.save
181
+ assert enum_mixin.callback1_executed
182
+ end
183
+
184
+ end