degu 0.0.4
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/.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
|