davidlee-state-fu 0.0.2 → 0.2.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.
Files changed (42) hide show
  1. data/README.textile +13 -1
  2. data/lib/state-fu.rb +4 -3
  3. data/lib/state_fu/active_support_lite/array/access.rb +53 -0
  4. data/lib/state_fu/active_support_lite/array/conversions.rb +196 -0
  5. data/lib/state_fu/active_support_lite/array/extract_options.rb +20 -0
  6. data/lib/state_fu/active_support_lite/array/grouping.rb +106 -0
  7. data/lib/state_fu/active_support_lite/array/random_access.rb +12 -0
  8. data/lib/state_fu/active_support_lite/array/wrapper.rb +24 -0
  9. data/lib/state_fu/active_support_lite/array.rb +7 -0
  10. data/lib/state_fu/active_support_lite/blank.rb +58 -0
  11. data/lib/state_fu/active_support_lite/cattr_reader.rb +54 -0
  12. data/lib/state_fu/active_support_lite/inheritable_attributes.rb +1 -0
  13. data/lib/state_fu/active_support_lite/keys.rb +52 -0
  14. data/lib/state_fu/active_support_lite/object.rb +6 -0
  15. data/lib/state_fu/active_support_lite/string.rb +33 -0
  16. data/lib/state_fu/active_support_lite/symbol.rb +15 -0
  17. data/lib/state_fu/binding.rb +113 -58
  18. data/lib/state_fu/core_ext.rb +12 -13
  19. data/lib/state_fu/event.rb +4 -4
  20. data/lib/state_fu/exceptions.rb +12 -0
  21. data/lib/state_fu/helper.rb +74 -12
  22. data/lib/state_fu/lathe.rb +15 -0
  23. data/lib/state_fu/machine.rb +17 -25
  24. data/lib/state_fu/mock_transition.rb +38 -0
  25. data/lib/state_fu/persistence/active_record.rb +2 -2
  26. data/lib/state_fu/persistence/attribute.rb +4 -4
  27. data/lib/state_fu/persistence/base.rb +1 -1
  28. data/lib/state_fu/sprocket.rb +4 -0
  29. data/lib/state_fu/state.rb +4 -4
  30. data/lib/state_fu/transition.rb +24 -22
  31. data/spec/helper.rb +5 -3
  32. data/spec/integration/binding_extension_spec.rb +41 -0
  33. data/spec/integration/dynamic_requirement_spec.rb +160 -0
  34. data/spec/integration/example_01_document_spec.rb +2 -1
  35. data/spec/integration/lathe_extension_spec.rb +67 -0
  36. data/spec/integration/requirement_reflection_spec.rb +71 -11
  37. data/spec/integration/temp_spec.rb +17 -0
  38. data/spec/integration/transition_spec.rb +31 -19
  39. data/spec/units/binding_spec.rb +6 -0
  40. data/spec/units/event_spec.rb +6 -5
  41. data/spec/units/lathe_spec.rb +4 -2
  42. metadata +40 -17
data/README.textile CHANGED
@@ -170,7 +170,7 @@ To install the dependencies for running specs:
170
170
 
171
171
  <pre>
172
172
  <code>
173
- sudo gem install rspec rr activesupport
173
+ sudo gem install rspec rr
174
174
  rake # run the specs
175
175
  rake spec:doc # generate specdocs
176
176
  rake doc # generate rdocs
@@ -185,4 +185,16 @@ The spec folder is currently the best source of documentation.
185
185
 
186
186
  If you have questions, feature request or ideas, please join the "google group":http://groups.google.com/group/state-fu
187
187
 
188
+ A note about ActiveSupport:
189
+
190
+ StateFu will use ActiveSupport if it is already loaded. If not, it
191
+ will load its own (very heavily cut down) 'lite' version. This means
192
+ that if you require StateFu *before* other libraries which require
193
+ ActiveSupport (e.g. ActiveRecord), you may have to explicitly
194
+ <code>require 'activesupport'</code> before loading the dependent
195
+ libraries.
196
+
197
+
188
198
  Also see the "issue tracker":http://github.com/davidlee/state-fu/issues
199
+
200
+ And the "build monitor":http://runcoderun.com/davidlee/state-fu/
data/lib/state-fu.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  #!/usr/bin/env ruby
3
2
  #
4
3
  # State-Fu
@@ -44,7 +43,7 @@
44
43
  # my_doc = Document.new
45
44
  #
46
45
  # my_doc.status # returns a StateFu::Binding, which lets us access the 'Fu
47
- # my_doc.status_state => 'draft' # if this wasn't already a database column or attribute, an
46
+ # my_doc.status.state => 'draft' # if this wasn't already a database column or attribute, an
48
47
  # # attribute has been created to keep track of the state
49
48
  # my_doc.status.name => :draft # the name of the current_state (defaults to the first defined)
50
49
  # my_doc.status.publish! # raised => StateFu::RequirementError: [:author]
@@ -55,7 +54,8 @@
55
54
  # "new feed!" # aha - our event hook fires!
56
55
  # my_doc.status.name => :published # and the state has been updated.
57
56
 
58
- require 'activesupport'
57
+ require 'rubygems'
58
+ # require 'activesupport'
59
59
 
60
60
  require 'state_fu/core_ext'
61
61
  require 'state_fu/logger'
@@ -76,6 +76,7 @@ require 'state_fu/event'
76
76
  require 'state_fu/hooks'
77
77
  require 'state_fu/interface'
78
78
  require 'state_fu/transition'
79
+ require 'state_fu/mock_transition'
79
80
 
80
81
  module StateFu
81
82
  DEFAULT_MACHINE = :state_fu
@@ -0,0 +1,53 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Array #:nodoc:
4
+ # Makes it easier to access parts of an array.
5
+ module Access
6
+ # Returns the tail of the array from +position+.
7
+ #
8
+ # %w( a b c d ).from(0) # => %w( a b c d )
9
+ # %w( a b c d ).from(2) # => %w( c d )
10
+ # %w( a b c d ).from(10) # => nil
11
+ # %w().from(0) # => nil
12
+ def from(position)
13
+ self[position..-1]
14
+ end
15
+
16
+ # Returns the beginning of the array up to +position+.
17
+ #
18
+ # %w( a b c d ).to(0) # => %w( a )
19
+ # %w( a b c d ).to(2) # => %w( a b c )
20
+ # %w( a b c d ).to(10) # => %w( a b c d )
21
+ # %w().to(0) # => %w()
22
+ def to(position)
23
+ self[0..position]
24
+ end
25
+
26
+ # Equal to <tt>self[1]</tt>.
27
+ def second
28
+ self[1]
29
+ end
30
+
31
+ # Equal to <tt>self[2]</tt>.
32
+ def third
33
+ self[2]
34
+ end
35
+
36
+ # Equal to <tt>self[3]</tt>.
37
+ def fourth
38
+ self[3]
39
+ end
40
+
41
+ # Equal to <tt>self[4]</tt>.
42
+ def fifth
43
+ self[4]
44
+ end
45
+
46
+ # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
47
+ def forty_two
48
+ self[41]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,196 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Array #:nodoc:
4
+ module Conversions
5
+ # Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
6
+ # * <tt>:words_connector</tt> - The sign or word used to join the elements in arrays with two or more elements (default: ", ")
7
+ # * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ")
8
+ # * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ")
9
+ def to_sentence(options = {})
10
+ default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
11
+ default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale])
12
+ default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
13
+
14
+ # Try to emulate to_senteces previous to 2.3
15
+ if options.has_key?(:connector) || options.has_key?(:skip_last_comma)
16
+ ::ActiveSupport::Deprecation.warn(":connector has been deprecated. Use :words_connector instead", caller) if options.has_key? :connector
17
+ ::ActiveSupport::Deprecation.warn(":skip_last_comma has been deprecated. Use :last_word_connector instead", caller) if options.has_key? :skip_last_comma
18
+
19
+ skip_last_comma = options.delete :skip_last_comma
20
+ if connector = options.delete(:connector)
21
+ options[:last_word_connector] ||= skip_last_comma ? connector : ", #{connector}"
22
+ else
23
+ options[:last_word_connector] ||= skip_last_comma ? default_two_words_connector : default_last_word_connector
24
+ end
25
+ end
26
+
27
+ options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
28
+ options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector
29
+
30
+ case length
31
+ when 0
32
+ ""
33
+ when 1
34
+ self[0].to_s
35
+ when 2
36
+ "#{self[0]}#{options[:two_words_connector]}#{self[1]}"
37
+ else
38
+ "#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
39
+ end
40
+ end
41
+
42
+
43
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
44
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
45
+ def to_param
46
+ collect { |e| e.to_param }.join '/'
47
+ end
48
+
49
+ # Converts an array into a string suitable for use as a URL query string,
50
+ # using the given +key+ as the param name.
51
+ #
52
+ # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
53
+ def to_query(key)
54
+ prefix = "#{key}[]"
55
+ collect { |value| value.to_query(prefix) }.join '&'
56
+ end
57
+
58
+ def self.included(base) #:nodoc:
59
+ base.class_eval do
60
+ alias_method :to_default_s, :to_s
61
+ alias_method :to_s, :to_formatted_s
62
+ end
63
+ end
64
+
65
+ # Converts a collection of elements into a formatted string by calling
66
+ # <tt>to_s</tt> on all elements and joining them:
67
+ #
68
+ # Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
69
+ #
70
+ # Adding in the <tt>:db</tt> argument as the format yields a prettier
71
+ # output:
72
+ #
73
+ # Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
74
+ def to_formatted_s(format = :default)
75
+ case format
76
+ when :db
77
+ if respond_to?(:empty?) && self.empty?
78
+ "null"
79
+ else
80
+ collect { |element| element.id }.join(",")
81
+ end
82
+ else
83
+ to_default_s
84
+ end
85
+ end
86
+
87
+ # Returns a string that represents this array in XML by sending +to_xml+
88
+ # to each element. Active Record collections delegate their representation
89
+ # in XML to this method.
90
+ #
91
+ # All elements are expected to respond to +to_xml+, if any of them does
92
+ # not an exception is raised.
93
+ #
94
+ # The root node reflects the class name of the first element in plural
95
+ # if all elements belong to the same type and that's not Hash:
96
+ #
97
+ # customer.projects.to_xml
98
+ #
99
+ # <?xml version="1.0" encoding="UTF-8"?>
100
+ # <projects type="array">
101
+ # <project>
102
+ # <amount type="decimal">20000.0</amount>
103
+ # <customer-id type="integer">1567</customer-id>
104
+ # <deal-date type="date">2008-04-09</deal-date>
105
+ # ...
106
+ # </project>
107
+ # <project>
108
+ # <amount type="decimal">57230.0</amount>
109
+ # <customer-id type="integer">1567</customer-id>
110
+ # <deal-date type="date">2008-04-15</deal-date>
111
+ # ...
112
+ # </project>
113
+ # </projects>
114
+ #
115
+ # Otherwise the root element is "records":
116
+ #
117
+ # [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
118
+ #
119
+ # <?xml version="1.0" encoding="UTF-8"?>
120
+ # <records type="array">
121
+ # <record>
122
+ # <bar type="integer">2</bar>
123
+ # <foo type="integer">1</foo>
124
+ # </record>
125
+ # <record>
126
+ # <baz type="integer">3</baz>
127
+ # </record>
128
+ # </records>
129
+ #
130
+ # If the collection is empty the root element is "nil-classes" by default:
131
+ #
132
+ # [].to_xml
133
+ #
134
+ # <?xml version="1.0" encoding="UTF-8"?>
135
+ # <nil-classes type="array"/>
136
+ #
137
+ # To ensure a meaningful root element use the <tt>:root</tt> option:
138
+ #
139
+ # customer_with_no_projects.projects.to_xml(:root => "projects")
140
+ #
141
+ # <?xml version="1.0" encoding="UTF-8"?>
142
+ # <projects type="array"/>
143
+ #
144
+ # By default root children have as node name the one of the root
145
+ # singularized. You can change it with the <tt>:children</tt> option.
146
+ #
147
+ # The +options+ hash is passed downwards:
148
+ #
149
+ # Message.all.to_xml(:skip_types => true)
150
+ #
151
+ # <?xml version="1.0" encoding="UTF-8"?>
152
+ # <messages>
153
+ # <message>
154
+ # <created-at>2008-03-07T09:58:18+01:00</created-at>
155
+ # <id>1</id>
156
+ # <name>1</name>
157
+ # <updated-at>2008-03-07T09:58:18+01:00</updated-at>
158
+ # <user-id>1</user-id>
159
+ # </message>
160
+ # </messages>
161
+ #
162
+ def to_xml(options = {})
163
+ raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
164
+ require 'builder' unless defined?(Builder)
165
+
166
+ options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize : "records"
167
+ options[:children] ||= options[:root].singularize
168
+ options[:indent] ||= 2
169
+ options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
170
+
171
+ root = options.delete(:root).to_s
172
+ children = options.delete(:children)
173
+
174
+ if !options.has_key?(:dasherize) || options[:dasherize]
175
+ root = root.dasherize
176
+ end
177
+
178
+ options[:builder].instruct! unless options.delete(:skip_instruct)
179
+
180
+ opts = options.merge({ :root => children })
181
+
182
+ xml = options[:builder]
183
+ if empty?
184
+ xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
185
+ else
186
+ xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
187
+ yield xml if block_given?
188
+ each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) }
189
+ }
190
+ end
191
+ end
192
+
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Array #:nodoc:
4
+ module ExtractOptions
5
+ # Extracts options from a set of arguments. Removes and returns the last
6
+ # element in the array if it's a hash, otherwise returns a blank hash.
7
+ #
8
+ # def options(*args)
9
+ # args.extract_options!
10
+ # end
11
+ #
12
+ # options(1, 2) # => {}
13
+ # options(1, 2, :a => :b) # => {:a=>:b}
14
+ def extract_options!
15
+ last.is_a?(::Hash) ? pop : {}
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,106 @@
1
+ require 'enumerator'
2
+
3
+ module ActiveSupport #:nodoc:
4
+ module CoreExtensions #:nodoc:
5
+ module Array #:nodoc:
6
+ module Grouping
7
+ # Splits or iterates over the array in groups of size +number+,
8
+ # padding any remaining slots with +fill_with+ unless it is +false+.
9
+ #
10
+ # %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
11
+ # ["1", "2", "3"]
12
+ # ["4", "5", "6"]
13
+ # ["7", nil, nil]
14
+ #
15
+ # %w(1 2 3).in_groups_of(2, '&nbsp;') {|group| p group}
16
+ # ["1", "2"]
17
+ # ["3", "&nbsp;"]
18
+ #
19
+ # %w(1 2 3).in_groups_of(2, false) {|group| p group}
20
+ # ["1", "2"]
21
+ # ["3"]
22
+ def in_groups_of(number, fill_with = nil)
23
+ if fill_with == false
24
+ collection = self
25
+ else
26
+ # size % number gives how many extra we have;
27
+ # subtracting from number gives how many to add;
28
+ # modulo number ensures we don't add group of just fill.
29
+ padding = (number - size % number) % number
30
+ collection = dup.concat([fill_with] * padding)
31
+ end
32
+
33
+ if block_given?
34
+ collection.each_slice(number) { |slice| yield(slice) }
35
+ else
36
+ returning [] do |groups|
37
+ collection.each_slice(number) { |group| groups << group }
38
+ end
39
+ end
40
+ end
41
+
42
+ # Splits or iterates over the array in +number+ of groups, padding any
43
+ # remaining slots with +fill_with+ unless it is +false+.
44
+ #
45
+ # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
46
+ # ["1", "2", "3", "4"]
47
+ # ["5", "6", "7", nil]
48
+ # ["8", "9", "10", nil]
49
+ #
50
+ # %w(1 2 3 4 5 6 7).in_groups(3, '&nbsp;') {|group| p group}
51
+ # ["1", "2", "3"]
52
+ # ["4", "5", "&nbsp;"]
53
+ # ["6", "7", "&nbsp;"]
54
+ #
55
+ # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
56
+ # ["1", "2", "3"]
57
+ # ["4", "5"]
58
+ # ["6", "7"]
59
+ def in_groups(number, fill_with = nil)
60
+ # size / number gives minor group size;
61
+ # size % number gives how many objects need extra accomodation;
62
+ # each group hold either division or division + 1 items.
63
+ division = size / number
64
+ modulo = size % number
65
+
66
+ # create a new array avoiding dup
67
+ groups = []
68
+ start = 0
69
+
70
+ number.times do |index|
71
+ length = division + (modulo > 0 && modulo > index ? 1 : 0)
72
+ padding = fill_with != false &&
73
+ modulo > 0 && length == division ? 1 : 0
74
+ groups << slice(start, length).concat([fill_with] * padding)
75
+ start += length
76
+ end
77
+
78
+ if block_given?
79
+ groups.each{|g| yield(g) }
80
+ else
81
+ groups
82
+ end
83
+ end
84
+
85
+ # Divides the array into one or more subarrays based on a delimiting +value+
86
+ # or the result of an optional block.
87
+ #
88
+ # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
89
+ # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
90
+ def split(value = nil)
91
+ using_block = block_given?
92
+
93
+ inject([[]]) do |results, element|
94
+ if (using_block && yield(element)) || (value == element)
95
+ results << []
96
+ else
97
+ results.last << element
98
+ end
99
+
100
+ results
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Array #:nodoc:
4
+ module RandomAccess
5
+ # Returns a random element from the array.
6
+ def rand
7
+ self[Kernel.rand(length)]
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Array #:nodoc:
4
+ module Wrapper
5
+ # Wraps the object in an Array unless it's an Array. Converts the
6
+ # object to an Array using #to_ary if it implements that.
7
+ def wrap(object)
8
+ case object
9
+ when nil
10
+ []
11
+ when self
12
+ object
13
+ else
14
+ if object.respond_to?(:to_ary)
15
+ object.to_ary
16
+ else
17
+ [object]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ require File.join( File.dirname( __FILE__),'array/extract_options')
2
+ require File.join( File.dirname( __FILE__),'array/grouping')
3
+
4
+ class Array #:nodoc:
5
+ include ActiveSupport::CoreExtensions::Array::ExtractOptions
6
+ include ActiveSupport::CoreExtensions::Array::Grouping
7
+ end
@@ -0,0 +1,58 @@
1
+ class Object
2
+ # An object is blank if it's false, empty, or a whitespace string.
3
+ # For example, "", " ", +nil+, [], and {} are blank.
4
+ #
5
+ # This simplifies
6
+ #
7
+ # if !address.nil? && !address.empty?
8
+ #
9
+ # to
10
+ #
11
+ # if !address.blank?
12
+ def blank?
13
+ respond_to?(:empty?) ? empty? : !self
14
+ end
15
+
16
+ # An object is present if it's not blank.
17
+ def present?
18
+ !blank?
19
+ end
20
+ end
21
+
22
+ class NilClass #:nodoc:
23
+ def blank?
24
+ true
25
+ end
26
+ end
27
+
28
+ class FalseClass #:nodoc:
29
+ def blank?
30
+ true
31
+ end
32
+ end
33
+
34
+ class TrueClass #:nodoc:
35
+ def blank?
36
+ false
37
+ end
38
+ end
39
+
40
+ class Array #:nodoc:
41
+ alias_method :blank?, :empty?
42
+ end
43
+
44
+ class Hash #:nodoc:
45
+ alias_method :blank?, :empty?
46
+ end
47
+
48
+ class String #:nodoc:
49
+ def blank?
50
+ self !~ /\S/
51
+ end
52
+ end
53
+
54
+ class Numeric #:nodoc:
55
+ def blank?
56
+ false
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # Extends the class object with class and instance accessors for class attributes,
2
+ # just like the native attr* accessors for instance attributes.
3
+ #
4
+ # class Person
5
+ # cattr_accessor :hair_colors
6
+ # end
7
+ #
8
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
9
+ class Class
10
+ def cattr_reader(*syms)
11
+ syms.flatten.each do |sym|
12
+ next if sym.is_a?(Hash)
13
+ class_eval(<<-EOS, __FILE__, __LINE__)
14
+ unless defined? @@#{sym} # unless defined? @@hair_colors
15
+ @@#{sym} = nil # @@hair_colors = nil
16
+ end # end
17
+ #
18
+ def self.#{sym} # def self.hair_colors
19
+ @@#{sym} # @@hair_colors
20
+ end # end
21
+ #
22
+ def #{sym} # def hair_colors
23
+ @@#{sym} # @@hair_colors
24
+ end # end
25
+ EOS
26
+ end
27
+ end
28
+
29
+ def cattr_writer(*syms)
30
+ options = syms.extract_options!
31
+ syms.flatten.each do |sym|
32
+ class_eval(<<-EOS, __FILE__, __LINE__)
33
+ unless defined? @@#{sym} # unless defined? @@hair_colors
34
+ @@#{sym} = nil # @@hair_colors = nil
35
+ end # end
36
+ #
37
+ def self.#{sym}=(obj) # def self.hair_colors=(obj)
38
+ @@#{sym} = obj # @@hair_colors = obj
39
+ end # end
40
+ #
41
+ #{" #
42
+ def #{sym}=(obj) # def hair_colors=(obj)
43
+ @@#{sym} = obj # @@hair_colors = obj
44
+ end # end
45
+ " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
46
+ EOS
47
+ end
48
+ end
49
+
50
+ def cattr_accessor(*syms)
51
+ cattr_reader(*syms)
52
+ cattr_writer(*syms)
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveSupport #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Hash #:nodoc:
4
+ module Keys
5
+ # Return a new hash with all keys converted to strings.
6
+ def stringify_keys
7
+ inject({}) do |options, (key, value)|
8
+ options[key.to_s] = value
9
+ options
10
+ end
11
+ end
12
+
13
+ # Destructively convert all keys to strings.
14
+ def stringify_keys!
15
+ keys.each do |key|
16
+ self[key.to_s] = delete(key)
17
+ end
18
+ self
19
+ end
20
+
21
+ # Return a new hash with all keys converted to symbols.
22
+ def symbolize_keys
23
+ inject({}) do |options, (key, value)|
24
+ options[(key.to_sym rescue key) || key] = value
25
+ options
26
+ end
27
+ end
28
+
29
+ # Destructively convert all keys to symbols.
30
+ def symbolize_keys!
31
+ self.replace(self.symbolize_keys)
32
+ end
33
+
34
+ alias_method :to_options, :symbolize_keys
35
+ alias_method :to_options!, :symbolize_keys!
36
+
37
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
38
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
39
+ # as keys, this will fail.
40
+ #
41
+ # ==== Examples
42
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
43
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
44
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
45
+ def assert_valid_keys(*valid_keys)
46
+ unknown_keys = keys - [valid_keys].flatten
47
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ class Object
2
+ def extended_by #:nodoc:
3
+ ancestors = class << self; ancestors end
4
+ ancestors.select { |mod| mod.class == Module } - [ Object, Kernel ]
5
+ end
6
+ end