degu 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +5 -0
- data/README.rdoc +150 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/degu.gemspec +44 -0
- data/lib/degu/has_enum.rb +79 -0
- data/lib/degu/has_set.rb +110 -0
- data/lib/degu/polite.rb +6 -0
- data/lib/degu/renum/enumerated_value.rb +226 -0
- data/lib/degu/renum/enumerated_value_type_factory.rb +61 -0
- data/lib/degu/renum.rb +17 -0
- data/lib/degu/rude.rb +12 -0
- data/lib/degu/version.rb +8 -0
- data/lib/degu.rb +3 -0
- data/spec/renum_spec.rb +281 -0
- data/spec/spec_helper.rb +3 -0
- data/test/has_enum_test.rb +184 -0
- data/test/has_set_test.rb +155 -0
- data/test/test_helper.rb +138 -0
- data/test_helper.rb +86 -0
- metadata +137 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
= Degu
|
2
|
+
|
3
|
+
Degu bundles the renum Enumeration implementation with the has_enum and has_set
|
4
|
+
rails plugins.
|
5
|
+
|
6
|
+
== Renum
|
7
|
+
|
8
|
+
=== Description
|
9
|
+
|
10
|
+
Renum provides a readable but terse enum facility for Ruby. Enums are
|
11
|
+
sometimes called object constants and are analogous to the type-safe enum
|
12
|
+
pattern in Java, though obviously Ruby's flexibility means there's no such
|
13
|
+
thing as type-safety.
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
Renum allows you to do things like this:
|
18
|
+
|
19
|
+
enum :Status, %w( NOT_STARTED IN_PROGRESS COMPLETE )
|
20
|
+
|
21
|
+
enum :Size do
|
22
|
+
Small("Really really tiny")
|
23
|
+
Medium("Sort of in the middle")
|
24
|
+
Large("Quite big")
|
25
|
+
|
26
|
+
attr_reader :description
|
27
|
+
|
28
|
+
def init description
|
29
|
+
@description = description
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module MyNamespace
|
34
|
+
enum :FooValue, [ :Bar, :Baz, :Bat ]
|
35
|
+
end
|
36
|
+
|
37
|
+
Giving you something that satisfies this spec, plus a bit more:
|
38
|
+
|
39
|
+
describe "enum" do
|
40
|
+
|
41
|
+
it "creates a class for the value type" do
|
42
|
+
Status.class.should == Class
|
43
|
+
end
|
44
|
+
|
45
|
+
it "makes each value an instance of the value type" do
|
46
|
+
Status::NOT_STARTED.class.should == Status
|
47
|
+
end
|
48
|
+
|
49
|
+
it "exposes array of values" do
|
50
|
+
Status.values.should == [Status::NOT_STARTED, Status::IN_PROGRESS, Status::COMPLETE]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "provides an alternative means of declaring values where extra information can be provided for initialization" do
|
54
|
+
Size::Small.description.should == "Really really tiny"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "enumerates over values" do
|
58
|
+
Status.map {|s| s.name}.should == %w[NOT_STARTED IN_PROGRESS COMPLETE]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "indexes values" do
|
62
|
+
Status[2].should == Status::COMPLETE
|
63
|
+
end
|
64
|
+
|
65
|
+
it "provides index lookup on values" do
|
66
|
+
Status::IN_PROGRESS.index.should == 1
|
67
|
+
end
|
68
|
+
|
69
|
+
it "provides a reasonable to_s for values" do
|
70
|
+
Status::NOT_STARTED.to_s.should == "Status::NOT_STARTED"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "makes values comparable" do
|
74
|
+
Status::NOT_STARTED.should < Status::COMPLETE
|
75
|
+
end
|
76
|
+
|
77
|
+
it "allows enums to be nested in other modules or classes" do
|
78
|
+
MyNamespace::FooValue::Bar.class.should == MyNamespace::FooValue
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
=== Rails[http://www.rubyonrails.com/] Integration
|
84
|
+
|
85
|
+
To use enumerated values as ActiveRecord attribute values, use the
|
86
|
+
constantize_attribute plugin
|
87
|
+
(https://github.com/duelinmarkers/constantize_attribute/tree) (also by me).
|
88
|
+
|
89
|
+
class Vehicle < ActiveRecord::Base
|
90
|
+
enum :Status do
|
91
|
+
New()
|
92
|
+
Used()
|
93
|
+
Salvage(true)
|
94
|
+
|
95
|
+
def init(warn = false)
|
96
|
+
@warn = warn
|
97
|
+
end
|
98
|
+
|
99
|
+
def requires_warning_buyer?
|
100
|
+
@warn
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
constantize_attribute :status
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
v = Vehicle.create! :status => Vehicle::Status::New
|
109
|
+
# Now the database has the string "Vehicle::Status::New",
|
110
|
+
# but your record object exposes the Status object:
|
111
|
+
v.status.requires_warning_buyer? # => false
|
112
|
+
|
113
|
+
v.update_attribute :status, Vehicle::Status::Salvage
|
114
|
+
# Now the database has the string "Vehicle::Status::Salvage".
|
115
|
+
v.status.requires_warning_buyer? # => true
|
116
|
+
|
117
|
+
# Since constantize_attribute also accepts strings, it's easy
|
118
|
+
# to use enumerated values with forms.
|
119
|
+
v.status = "Vehicle::Status::Used"
|
120
|
+
v.status.requires_warning_buyer? # => false
|
121
|
+
|
122
|
+
=== License
|
123
|
+
|
124
|
+
This code is free to use under the terms of the MIT license.
|
125
|
+
|
126
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
127
|
+
a copy of this software and associated documentation files (the
|
128
|
+
"Software"), to deal in the Software without restriction, including
|
129
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
130
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
131
|
+
permit persons to whom the Software is furnished to do so, subject to
|
132
|
+
the following conditions:
|
133
|
+
|
134
|
+
The above copyright notice and this permission notice shall be
|
135
|
+
included in all copies or substantial portions of the Software.
|
136
|
+
|
137
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
138
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
139
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
140
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
141
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
142
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
143
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
144
|
+
|
145
|
+
=== Contact
|
146
|
+
|
147
|
+
Renum was created by John D. Hume. Comments are welcome. Send an email to
|
148
|
+
duelin dot markers at gmail or "contact me via my
|
149
|
+
blog[http://elhumidor.blogspot.com/].
|
150
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# vim: set filetype=ruby et sw=2 ts=2:
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'gem_hadar'
|
5
|
+
|
6
|
+
GemHadar do
|
7
|
+
name 'degu'
|
8
|
+
author 'Florian Frank'
|
9
|
+
email 'dev@pkw.de'
|
10
|
+
homepage "http://github.com/caroo/#{name}"
|
11
|
+
summary 'Library for enums and bitfield sets.'
|
12
|
+
description 'Library that includes enums, and rails support for enums and bitfield sets.'
|
13
|
+
test_dir 'test'
|
14
|
+
test_files Dir['test/**/*_test.rb']
|
15
|
+
spec_dir 'spec'
|
16
|
+
ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', '.rvmrc', 'coverage', '.DS_Store'
|
17
|
+
readme 'README.rdoc'
|
18
|
+
|
19
|
+
dependency 'activerecord', '~> 3.0'
|
20
|
+
|
21
|
+
development_dependency 'mocha'
|
22
|
+
development_dependency 'sqlite3'
|
23
|
+
development_dependency 'rspec'
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'Run specs and tests'
|
27
|
+
task :default => [ :test, :spec ]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.4
|
data/degu.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "degu"
|
5
|
+
s.version = "0.0.4"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Florian Frank"]
|
9
|
+
s.date = "2011-12-08"
|
10
|
+
s.description = "Library that includes enums, and rails support for enums and bitfield sets."
|
11
|
+
s.email = "dev@pkw.de"
|
12
|
+
s.extra_rdoc_files = ["README.rdoc", "lib/degu/has_enum.rb", "lib/degu/has_set.rb", "lib/degu/polite.rb", "lib/degu/renum/enumerated_value.rb", "lib/degu/renum/enumerated_value_type_factory.rb", "lib/degu/renum.rb", "lib/degu/rude.rb", "lib/degu/version.rb", "lib/degu.rb"]
|
13
|
+
s.files = [".gitignore", "Gemfile", "README.rdoc", "Rakefile", "VERSION", "degu.gemspec", "lib/degu.rb", "lib/degu/has_enum.rb", "lib/degu/has_set.rb", "lib/degu/polite.rb", "lib/degu/renum.rb", "lib/degu/renum/enumerated_value.rb", "lib/degu/renum/enumerated_value_type_factory.rb", "lib/degu/rude.rb", "lib/degu/version.rb", "spec/renum_spec.rb", "spec/spec_helper.rb", "test/has_enum_test.rb", "test/has_set_test.rb", "test/test_helper.rb", "test_helper.rb"]
|
14
|
+
s.homepage = "http://github.com/caroo/degu"
|
15
|
+
s.rdoc_options = ["--title", "Degu - Library for enums and bitfield sets.", "--main", "README.rdoc"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubygems_version = "1.8.11"
|
18
|
+
s.summary = "Library for enums and bitfield sets."
|
19
|
+
s.test_files = ["test/has_enum_test.rb", "test/has_set_test.rb"]
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
s.specification_version = 3
|
23
|
+
|
24
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
25
|
+
s.add_development_dependency(%q<gem_hadar>, ["~> 0.1.3"])
|
26
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
27
|
+
s.add_development_dependency(%q<sqlite3>, [">= 0"])
|
28
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
29
|
+
s.add_runtime_dependency(%q<activerecord>, ["~> 3.0"])
|
30
|
+
else
|
31
|
+
s.add_dependency(%q<gem_hadar>, ["~> 0.1.3"])
|
32
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
33
|
+
s.add_dependency(%q<sqlite3>, [">= 0"])
|
34
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
35
|
+
s.add_dependency(%q<activerecord>, ["~> 3.0"])
|
36
|
+
end
|
37
|
+
else
|
38
|
+
s.add_dependency(%q<gem_hadar>, ["~> 0.1.3"])
|
39
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
40
|
+
s.add_dependency(%q<sqlite3>, [">= 0"])
|
41
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
42
|
+
s.add_dependency(%q<activerecord>, ["~> 3.0"])
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "degu/renum"
|
2
|
+
|
3
|
+
module Degu
|
4
|
+
module HasEnum
|
5
|
+
def self.included(modul)
|
6
|
+
modul.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Use like this
|
12
|
+
#
|
13
|
+
# class Furniture
|
14
|
+
# has_enum :colors, :column_name => :custom_color_type
|
15
|
+
# end
|
16
|
+
def has_enum(enum_name, options={})
|
17
|
+
|
18
|
+
enum_column = options.has_key?(:column_name) ? options[:column_name].to_s : "#{enum_name}_type"
|
19
|
+
|
20
|
+
self.send("validate", "#{enum_column}_check_for_valid_type_of_enum")
|
21
|
+
|
22
|
+
# throws a NameError if Enum Class doesn't exists
|
23
|
+
enum_class = options.has_key?(:class_name) ? options[:class_name].to_s.constantize : enum_name.to_s.camelize.constantize
|
24
|
+
|
25
|
+
# Enum must be a Renum::EnumeratedValue Enum
|
26
|
+
raise ArgumentError, "expected Renum::EnumeratedValue" unless enum_class.superclass == Renum::EnumeratedValue
|
27
|
+
|
28
|
+
define_method("reset_enum_changed") do
|
29
|
+
@enum_changed = false
|
30
|
+
end
|
31
|
+
after_save :reset_enum_changed
|
32
|
+
|
33
|
+
define_method("#{enum_name}") do
|
34
|
+
begin
|
35
|
+
return self[enum_column].present? ? enum_class.const_get(self[enum_column]) : nil
|
36
|
+
rescue NameError => e
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method("#{enum_column}=") do |enum_literal|
|
42
|
+
unless enum_literal == self[enum_column]
|
43
|
+
self[enum_column] = enum_literal
|
44
|
+
@enum_changed = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
define_method("#{enum_name}=") do |enum_to_set|
|
49
|
+
old_value = self[enum_column]
|
50
|
+
enum_resolved = enum_class[enum_to_set]
|
51
|
+
if enum_to_set.to_s.strip.empty?
|
52
|
+
self[enum_column] = nil
|
53
|
+
elsif enum_resolved
|
54
|
+
self[enum_column] = enum_resolved.name
|
55
|
+
else
|
56
|
+
raise ArgumentError, "could not resolve #{enum_to_set.inspect}"
|
57
|
+
end
|
58
|
+
@enum_changed ||= self[enum_column] != old_value
|
59
|
+
end
|
60
|
+
|
61
|
+
define_method("#{enum_name}_has_changed?") do
|
62
|
+
!!@enum_changed
|
63
|
+
end
|
64
|
+
|
65
|
+
define_method("#{enum_column}_check_for_valid_type_of_enum") do
|
66
|
+
return true if self[enum_column].nil? || self[enum_column].to_s.empty?
|
67
|
+
begin
|
68
|
+
enum_class.const_get(self[enum_column])
|
69
|
+
rescue NameError => e
|
70
|
+
self.errors.add(enum_column.to_sym, "Wrong type '#{self[enum_column]}' for enum '#{enum_name}'")
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/degu/has_set.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_record'
|
6
|
+
module Degu
|
7
|
+
module HasSet
|
8
|
+
VERSION = '0.0.4'
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Use like this:
|
12
|
+
#
|
13
|
+
# class Person < ActiveRecord::Base
|
14
|
+
# has_set :interests
|
15
|
+
# end
|
16
|
+
def has_set(set_name, options = {})
|
17
|
+
|
18
|
+
set_column = options.has_key?(:column_name) ? options[:column_name].to_s : "#{set_name}_bitfield"
|
19
|
+
|
20
|
+
begin
|
21
|
+
enum_class = options.has_key?(:enum_class) ? options[:enum_class] : set_name.to_s.camelcase.constantize
|
22
|
+
rescue NameError => ne
|
23
|
+
raise NameError, "There ist no class to take the set entries from (#{ne.message})."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Extend enum_class with field_name method
|
27
|
+
enum_class.class_eval <<-EOF
|
28
|
+
def field_name
|
29
|
+
'#{set_name.to_s.singularize}_' + self.name.underscore
|
30
|
+
end
|
31
|
+
EOF
|
32
|
+
|
33
|
+
define_method("#{set_name}=") do |argument_value|
|
34
|
+
self[set_column] =
|
35
|
+
unless argument_value.nil?
|
36
|
+
set_elements =
|
37
|
+
if String === argument_value
|
38
|
+
argument_value.split(',').map(&:strip)
|
39
|
+
else
|
40
|
+
Array(argument_value)
|
41
|
+
end.map do |set_element|
|
42
|
+
enum_class[set_element]
|
43
|
+
end
|
44
|
+
set_elements.all? or raise ArgumentError, "element #{argument_value.inspect} contains invalid elements"
|
45
|
+
value = 0
|
46
|
+
set_elements.each do |set_element|
|
47
|
+
mask = 1 << set_element.bitfield_index
|
48
|
+
if mask & value == mask
|
49
|
+
next
|
50
|
+
else
|
51
|
+
value |= mask
|
52
|
+
end
|
53
|
+
end
|
54
|
+
value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
define_method(set_name) do
|
59
|
+
value = self[set_column]
|
60
|
+
case
|
61
|
+
when value.blank?
|
62
|
+
;;
|
63
|
+
when value.zero?
|
64
|
+
[]
|
65
|
+
else
|
66
|
+
set_elements = enum_class.values.select do |enum_element|
|
67
|
+
send("#{set_name.to_s.singularize}_#{enum_element.name.underscore}?")
|
68
|
+
end
|
69
|
+
# special to_s method for element-array
|
70
|
+
class << set_elements
|
71
|
+
def to_s
|
72
|
+
map(&:name) * ', '
|
73
|
+
end
|
74
|
+
end
|
75
|
+
set_elements
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: This should be a class method
|
80
|
+
define_method("available_#{set_name}") do
|
81
|
+
self.methods.grep(/#{set_name.to_s.singularize}_\w+[^\?=]$/).sort.map(&:to_s)
|
82
|
+
end
|
83
|
+
|
84
|
+
enum_class.values.each do |enum|
|
85
|
+
define_method("#{set_name.to_s.singularize}_#{enum.name.underscore}?") do
|
86
|
+
mask = 1 << enum.bitfield_index
|
87
|
+
self[set_column] & mask == mask
|
88
|
+
end
|
89
|
+
|
90
|
+
alias_method :"#{set_name.to_s.singularize}_#{enum.name.underscore}", :"#{set_name.to_s.singularize}_#{enum.name.underscore}?"
|
91
|
+
|
92
|
+
define_method("#{set_name.to_s.singularize}_#{enum.name.underscore}=") do |true_or_false|
|
93
|
+
mask = 1 << enum.bitfield_index
|
94
|
+
current_value = mask & self[set_column] == mask
|
95
|
+
true_or_false = true if true_or_false.to_s == "true" || true_or_false.respond_to?(:to_i) && true_or_false.to_i == 1
|
96
|
+
true_or_false = false if true_or_false.to_s == "false" || true_or_false.respond_to?(:to_i) && true_or_false.to_i == 0
|
97
|
+
|
98
|
+
if current_value != true_or_false
|
99
|
+
true_or_false ? self[set_column] |= mask : self[set_column] &= ~mask
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.included(modul)
|
107
|
+
modul.extend(ClassMethods)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/degu/polite.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
begin
|
3
|
+
require 'json'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
6
|
+
|
7
|
+
module Degu
|
8
|
+
module Renum
|
9
|
+
|
10
|
+
# This is the superclass of all enumeration classes.
|
11
|
+
# An enumeration class is Enumerable over its values and exposes them by numeric index via [].
|
12
|
+
# Values are also comparable, sorting into the order in which they're declared.
|
13
|
+
class EnumeratedValue
|
14
|
+
|
15
|
+
class << self
|
16
|
+
include Enumerable
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
def_delegators :values, :first, :last, :each
|
20
|
+
|
21
|
+
# Returns an array of values in the order they're declared.
|
22
|
+
def values
|
23
|
+
@values ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
alias all values
|
27
|
+
|
28
|
+
# This class encapsulates an enum field (аctually a method with arity == 0).
|
29
|
+
class Field < Struct.new('Field', :name, :options, :block)
|
30
|
+
# Returns true if the :default option was given.
|
31
|
+
def default?
|
32
|
+
options.key?(:default)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the value of the :default option.
|
36
|
+
def default
|
37
|
+
options[:default]
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if a block was given.
|
41
|
+
def block?
|
42
|
+
!!block
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determine the default value for the enum value +obj+ if +options+ is
|
46
|
+
# the options hash given to the init method.
|
47
|
+
def default_value(obj, options)
|
48
|
+
field_value = options[name]
|
49
|
+
if field_value.nil?
|
50
|
+
if default?
|
51
|
+
default_value = default
|
52
|
+
elsif block?
|
53
|
+
default_value = block[obj]
|
54
|
+
end
|
55
|
+
else
|
56
|
+
field_value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the name as a string.
|
61
|
+
def to_s
|
62
|
+
name.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a detailed string representation of this field.
|
66
|
+
def inspect
|
67
|
+
"#<#{self.class}: #{self} #{options.inspect}>"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns an array of all fields defined on this enum.
|
72
|
+
def fields
|
73
|
+
@fields ||= []
|
74
|
+
end
|
75
|
+
|
76
|
+
# Defines a field with the name +name+, the options +options+ and the
|
77
|
+
# block +block+. The only valid option at the moment is :default which is
|
78
|
+
# the default value the field is initialized with.
|
79
|
+
def field(name, options = {}, &block)
|
80
|
+
name = name.to_sym
|
81
|
+
fields.delete_if { |f| f.name == name }
|
82
|
+
fields << field = Field.new(name, options, block)
|
83
|
+
instance_eval { attr_reader field.name }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the value with the name +name+ and returns it.
|
87
|
+
def with_name name
|
88
|
+
values_by_name[name.to_s]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns a hash that maps names to their respective values values.
|
92
|
+
def values_by_name
|
93
|
+
@values_by_name ||= values.inject({}) do |memo, value|
|
94
|
+
memo[value.name] = value
|
95
|
+
memo
|
96
|
+
end.freeze
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the enum value for +index+. If +index+ is an Integer the
|
100
|
+
# index-th enum value is returned. Otherwise +index+ is converted into a
|
101
|
+
# String. For strings that start with a capital letter the with_name
|
102
|
+
# method is used to determine the enum value with the name +index+. If
|
103
|
+
# the string starts with a lowercase letter it is converted into
|
104
|
+
# camelcase first, that is foo_bar will be converted into FooBar, before
|
105
|
+
# with_name is called with this new value.
|
106
|
+
def [](index)
|
107
|
+
case index
|
108
|
+
when Integer
|
109
|
+
values[index]
|
110
|
+
when self
|
111
|
+
values[index.index]
|
112
|
+
else
|
113
|
+
name = index.to_s
|
114
|
+
case name
|
115
|
+
when /\A(\d+)\Z/
|
116
|
+
return values[$1.to_i]
|
117
|
+
when /\A[a-z]/
|
118
|
+
name = name.gsub(/(?:\A|_)(.)/) { $1.upcase }
|
119
|
+
end
|
120
|
+
with_name(name)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns the enum instance stored in the marshalled string +string+.
|
125
|
+
def _load(string)
|
126
|
+
with_name Marshal.load(string)
|
127
|
+
end
|
128
|
+
|
129
|
+
if defined?(::JSON)
|
130
|
+
# Fetches the correct enum determined by the deserialized JSON
|
131
|
+
# document.
|
132
|
+
def json_create(data)
|
133
|
+
JSON.deep_const_get(data[JSON.create_id])[data['name']]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
include Comparable
|
139
|
+
|
140
|
+
# Name of this enumerated value as a string.
|
141
|
+
attr_reader :name
|
142
|
+
|
143
|
+
# Index of this enumerated value as an integer.
|
144
|
+
attr_reader :index
|
145
|
+
|
146
|
+
alias_method :id, :index
|
147
|
+
|
148
|
+
# Creates an enumerated value named +name+ with a unique autoincrementing
|
149
|
+
# index number.
|
150
|
+
def initialize name
|
151
|
+
@name = name.to_s.freeze
|
152
|
+
@index = self.class.values.size
|
153
|
+
self.class.values << self
|
154
|
+
end
|
155
|
+
|
156
|
+
# This is the standard init method method which has an arbitrary number of
|
157
|
+
# arguments. If the last argument is a Hash and its keys are defined fields
|
158
|
+
# their respective values will be used to initialize the fields. If you
|
159
|
+
# want to use this method from an enum and define your own custom init
|
160
|
+
# method there, don't forget to call super from your method.
|
161
|
+
def init(*args)
|
162
|
+
if Hash === options = args.last
|
163
|
+
for field in self.class.fields
|
164
|
+
instance_variable_set "@#{field}", field.default_value(self, options)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the fully qualified name of the constant referring to this value.
|
170
|
+
# Don't override this if you're using Renum with the constantize_attribute
|
171
|
+
# plugin, which relies on this behavior.
|
172
|
+
def to_s
|
173
|
+
"#{self.class}::#{name}"
|
174
|
+
end
|
175
|
+
|
176
|
+
# Sorts enumerated values into the order in which they're declared.
|
177
|
+
def <=> other
|
178
|
+
index <=> other.index
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns a marshalled string for this enum instance.
|
182
|
+
def _dump(limit = -1)
|
183
|
+
Marshal.dump(name, limit)
|
184
|
+
end
|
185
|
+
|
186
|
+
if defined?(::JSON)
|
187
|
+
# Set the given fields in the +obj+ hash
|
188
|
+
def set_fields(obj, fields)
|
189
|
+
fields.each do |f|
|
190
|
+
name = f.name
|
191
|
+
value = instance_variable_get("@#{name}")
|
192
|
+
value.nil? and next
|
193
|
+
obj[name] = value
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns an enum (actually more a reference to an enum) serialized as a
|
198
|
+
# JSON document.
|
199
|
+
def as_json(opts = {}, *a)
|
200
|
+
opts ||= {}
|
201
|
+
obj = {
|
202
|
+
JSON.create_id => self.class.name,
|
203
|
+
:name => name,
|
204
|
+
}
|
205
|
+
case fields_opt = opts[:fields]
|
206
|
+
when nil, false
|
207
|
+
when true
|
208
|
+
set_fields obj, self.class.fields
|
209
|
+
when Array
|
210
|
+
fields_opt = fields_opt.map(&:to_sym)
|
211
|
+
set_fields obj, self.class.fields.select { |field| fields_opt.include?(field.name) }
|
212
|
+
else
|
213
|
+
raise ArgumentError, "unexpected fields option #{fields_opt.inspect}"
|
214
|
+
end
|
215
|
+
obj.as_json(opts)
|
216
|
+
end
|
217
|
+
|
218
|
+
def to_json(opts, *a)
|
219
|
+
obj = as_json(opts)
|
220
|
+
opts.respond_to?(:fields) and opts.delete(:fields)
|
221
|
+
obj.to_json(opts, *a)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|