castle-her 1.0.1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +110 -0
  12. data/castle-her.gemspec +30 -0
  13. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  16. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  17. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  18. data/lib/castle-her.rb +20 -0
  19. data/lib/castle-her/api.rb +113 -0
  20. data/lib/castle-her/collection.rb +12 -0
  21. data/lib/castle-her/errors.rb +27 -0
  22. data/lib/castle-her/json_api/model.rb +46 -0
  23. data/lib/castle-her/middleware.rb +12 -0
  24. data/lib/castle-her/middleware/accept_json.rb +17 -0
  25. data/lib/castle-her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/castle-her/middleware/json_api_parser.rb +36 -0
  27. data/lib/castle-her/middleware/parse_json.rb +21 -0
  28. data/lib/castle-her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/castle-her/model.rb +75 -0
  30. data/lib/castle-her/model/associations.rb +141 -0
  31. data/lib/castle-her/model/associations/association.rb +103 -0
  32. data/lib/castle-her/model/associations/association_proxy.rb +45 -0
  33. data/lib/castle-her/model/associations/belongs_to_association.rb +96 -0
  34. data/lib/castle-her/model/associations/has_many_association.rb +100 -0
  35. data/lib/castle-her/model/associations/has_one_association.rb +79 -0
  36. data/lib/castle-her/model/attributes.rb +284 -0
  37. data/lib/castle-her/model/base.rb +33 -0
  38. data/lib/castle-her/model/deprecated_methods.rb +61 -0
  39. data/lib/castle-her/model/http.rb +114 -0
  40. data/lib/castle-her/model/introspection.rb +65 -0
  41. data/lib/castle-her/model/nested_attributes.rb +45 -0
  42. data/lib/castle-her/model/orm.rb +207 -0
  43. data/lib/castle-her/model/parse.rb +216 -0
  44. data/lib/castle-her/model/paths.rb +126 -0
  45. data/lib/castle-her/model/relation.rb +164 -0
  46. data/lib/castle-her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +166 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +290 -0
@@ -0,0 +1,45 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class AssociationProxy < (ActiveSupport.const_defined?('ProxyObject') ? ActiveSupport::ProxyObject : ActiveSupport::BasicObject)
5
+
6
+ # @private
7
+ def self.install_proxy_methods(target_name, *names)
8
+ names.each do |name|
9
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
10
+ def #{name}(*args, &block)
11
+ #{target_name}.send(#{name.inspect}, *args, &block)
12
+ end
13
+ RUBY
14
+ end
15
+ end
16
+
17
+ install_proxy_methods :association,
18
+ :build, :create, :where, :find, :all, :assign_nested_attributes
19
+
20
+ # @private
21
+ def initialize(association)
22
+ @_her_association = association
23
+ end
24
+
25
+ def association
26
+ @_her_association
27
+ end
28
+
29
+ # @private
30
+ def method_missing(name, *args, &block)
31
+ if :object_id == name # avoid redefining object_id
32
+ return association.fetch.object_id
33
+ end
34
+
35
+ # create a proxy to the fetched object's method
36
+ AssociationProxy.install_proxy_methods 'association.fetch', name
37
+
38
+ # resend message to fetched object
39
+ __send__(name, *args, &block)
40
+ end
41
+
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,96 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class BelongsToAssociation < Association
5
+
6
+ # @private
7
+ def self.attach(klass, name, opts)
8
+ opts = {
9
+ :class_name => name.to_s.classify,
10
+ :name => name,
11
+ :data_key => name,
12
+ :default => nil,
13
+ :foreign_key => "#{name}_id",
14
+ :path => "/#{name.to_s.pluralize}/:id"
15
+ }.merge(opts)
16
+ klass.associations[:belongs_to] << opts
17
+
18
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def #{name}
20
+ cached_name = :"@_her_association_#{name}"
21
+
22
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
23
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.proxy(self, #{opts.inspect}))
24
+ end
25
+ RUBY
26
+ end
27
+
28
+ # @private
29
+ def self.parse(*args)
30
+ parse_single(*args)
31
+ end
32
+
33
+ # Initialize a new object
34
+ #
35
+ # @example
36
+ # class User
37
+ # include Her::Model
38
+ # belongs_to :organization
39
+ # end
40
+ #
41
+ # class Organization
42
+ # include Her::Model
43
+ # end
44
+ #
45
+ # user = User.find(1)
46
+ # new_organization = user.organization.build(:name => "Foo Inc.")
47
+ # new_organization # => #<Organization name="Foo Inc.">
48
+ def build(attributes = {})
49
+ @klass.build(attributes)
50
+ end
51
+
52
+ # Create a new object, save it and associate it to the parent
53
+ #
54
+ # @example
55
+ # class User
56
+ # include Her::Model
57
+ # belongs_to :organization
58
+ # end
59
+ #
60
+ # class Organization
61
+ # include Her::Model
62
+ # end
63
+ #
64
+ # user = User.find(1)
65
+ # user.organization.create(:name => "Foo Inc.")
66
+ # user.organization # => #<Organization id=2 name="Foo Inc.">
67
+ def create(attributes = {})
68
+ resource = build(attributes)
69
+ @parent.attributes[@name] = resource if resource.save
70
+ resource
71
+ end
72
+
73
+ # @private
74
+ def fetch
75
+ foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
76
+ data_key_value = @parent.attributes[@opts[:data_key].to_sym]
77
+ return @opts[:default].try(:dup) if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @params.empty?) || (foreign_key_value.blank? && data_key_value.blank?)
78
+
79
+ return @cached_result unless @params.any? || @cached_result.nil?
80
+ return @parent.attributes[@name] unless @params.any? || @parent.attributes[@name].blank?
81
+
82
+ path_params = @parent.attributes.merge(@params.merge(@klass.primary_key => foreign_key_value))
83
+ path = build_association_path lambda { @klass.build_request_path(path_params) }
84
+ @klass.get_resource(path, @params).tap do |result|
85
+ @cached_result = result if @params.blank?
86
+ end
87
+ end
88
+
89
+ # @private
90
+ def assign_nested_attributes(attributes)
91
+ assign_single_nested_attributes(attributes)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,100 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasManyAssociation < Association
5
+
6
+ # @private
7
+ def self.attach(klass, name, opts)
8
+ opts = {
9
+ :class_name => name.to_s.classify,
10
+ :name => name,
11
+ :data_key => name,
12
+ :default => Her::Collection.new,
13
+ :path => "/#{name}",
14
+ :inverse_of => nil
15
+ }.merge(opts)
16
+ klass.associations[:has_many] << opts
17
+
18
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def #{name}
20
+ cached_name = :"@_her_association_#{name}"
21
+
22
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
23
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, #{opts.inspect}))
24
+ end
25
+ RUBY
26
+ end
27
+
28
+ # @private
29
+ def self.parse(association, klass, data)
30
+ data_key = association[:data_key]
31
+ return {} unless data[data_key]
32
+
33
+ klass = klass.her_nearby_class(association[:class_name])
34
+ { association[:name] => Her::Model::Attributes.initialize_collection(klass, :data => data[data_key]) }
35
+ end
36
+
37
+ # Initialize a new object with a foreign key to the parent
38
+ #
39
+ # @example
40
+ # class User
41
+ # include Her::Model
42
+ # has_many :comments
43
+ # end
44
+ #
45
+ # class Comment
46
+ # include Her::Model
47
+ # end
48
+ #
49
+ # user = User.find(1)
50
+ # new_comment = user.comments.build(:body => "Hello!")
51
+ # new_comment # => #<Comment user_id=1 body="Hello!">
52
+ # TODO: This only merges the id of the parents, handle the case
53
+ # where this is more deeply nested
54
+ def build(attributes = {})
55
+ @klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
56
+ end
57
+
58
+ # Create a new object, save it and add it to the associated collection
59
+ #
60
+ # @example
61
+ # class User
62
+ # include Her::Model
63
+ # has_many :comments
64
+ # end
65
+ #
66
+ # class Comment
67
+ # include Her::Model
68
+ # end
69
+ #
70
+ # user = User.find(1)
71
+ # user.comments.create(:body => "Hello!")
72
+ # user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
73
+ def create(attributes = {})
74
+ resource = build(attributes)
75
+
76
+ if resource.save
77
+ @parent.attributes[@name] ||= Her::Collection.new
78
+ @parent.attributes[@name] << resource
79
+ end
80
+
81
+ resource
82
+ end
83
+
84
+ # @private
85
+ def fetch
86
+ super.tap do |o|
87
+ inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
88
+ o.each { |entry| entry.send("#{inverse_of}=", @parent) }
89
+ end
90
+ end
91
+
92
+ # @private
93
+ def assign_nested_attributes(attributes)
94
+ data = attributes.is_a?(Hash) ? attributes.values : attributes
95
+ @parent.attributes[@name] = Her::Model::Attributes.initialize_collection(@klass, :data => data)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,79 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasOneAssociation < Association
5
+
6
+ # @private
7
+ def self.attach(klass, name, opts)
8
+ opts = {
9
+ :class_name => name.to_s.classify,
10
+ :name => name,
11
+ :data_key => name,
12
+ :default => nil,
13
+ :path => "/#{name}"
14
+ }.merge(opts)
15
+ klass.associations[:has_one] << opts
16
+
17
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
18
+ def #{name}
19
+ cached_name = :"@_her_association_#{name}"
20
+
21
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
22
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.proxy(self, #{opts.inspect}))
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ # @private
28
+ def self.parse(*args)
29
+ parse_single(*args)
30
+ end
31
+
32
+ # Initialize a new object with a foreign key to the parent
33
+ #
34
+ # @example
35
+ # class User
36
+ # include Her::Model
37
+ # has_one :role
38
+ # end
39
+ #
40
+ # class Role
41
+ # include Her::Model
42
+ # end
43
+ #
44
+ # user = User.find(1)
45
+ # new_role = user.role.build(:title => "moderator")
46
+ # new_role # => #<Role user_id=1 title="moderator">
47
+ def build(attributes = {})
48
+ @klass.build(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
49
+ end
50
+
51
+ # Create a new object, save it and associate it to the parent
52
+ #
53
+ # @example
54
+ # class User
55
+ # include Her::Model
56
+ # has_one :role
57
+ # end
58
+ #
59
+ # class Role
60
+ # include Her::Model
61
+ # end
62
+ #
63
+ # user = User.find(1)
64
+ # user.role.create(:title => "moderator")
65
+ # user.role # => #<Role id=2 user_id=1 title="moderator">
66
+ def create(attributes = {})
67
+ resource = build(attributes)
68
+ @parent.attributes[@name] = resource if resource.save
69
+ resource
70
+ end
71
+
72
+ # @private
73
+ def assign_nested_attributes(attributes)
74
+ assign_single_nested_attributes(attributes)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,284 @@
1
+ module Her
2
+ module Model
3
+ # This module handles all methods related to model attributes
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ # Initialize a new object with data
8
+ #
9
+ # @param [Hash] attributes The attributes to initialize the object with
10
+ # @option attributes [Hash,Array] :_metadata
11
+ # @option attributes [Hash,Array] :_errors
12
+ # @option attributes [Boolean] :_destroyed
13
+ #
14
+ # @example
15
+ # class User
16
+ # include Her::Model
17
+ # end
18
+ #
19
+ # User.new(name: "Tobias") # => #<User name="Tobias">
20
+ def initialize(attributes={})
21
+ attributes ||= {}
22
+ @metadata = attributes.delete(:_metadata) || {}
23
+ @response_errors = attributes.delete(:_errors) || {}
24
+ @destroyed = attributes.delete(:_destroyed) || false
25
+
26
+ attributes = self.class.default_scope.apply_to(attributes)
27
+ assign_attributes(attributes)
28
+ run_callbacks :initialize
29
+ end
30
+
31
+ # Initialize a collection of resources
32
+ #
33
+ # @private
34
+ def self.initialize_collection(klass, parsed_data={})
35
+ collection_data = klass.extract_array(parsed_data).map do |item_data|
36
+ if item_data.kind_of?(klass)
37
+ resource = item_data
38
+ else
39
+ resource = klass.new(klass.parse(item_data))
40
+ resource.run_callbacks :find
41
+ end
42
+ resource
43
+ end
44
+ Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
45
+ end
46
+
47
+ # Use setter methods of model for each key / value pair in params
48
+ # Return key / value pairs for which no setter method was defined on the model
49
+ #
50
+ # @private
51
+ def self.use_setter_methods(model, params)
52
+ params ||= {}
53
+
54
+ reserved_keys = [:id, model.class.primary_key] + model.class.association_keys
55
+ model.class.attributes *params.keys.reject { |k| reserved_keys.include?(k) || reserved_keys.map(&:to_s).include?(k) }
56
+
57
+ setter_method_names = model.class.setter_method_names
58
+ params.inject({}) do |memo, (key, value)|
59
+ setter_method = key.to_s + '='
60
+ if setter_method_names.include?(setter_method)
61
+ model.send(setter_method, value)
62
+ else
63
+ key = key.to_sym if key.is_a?(String)
64
+ memo[key] = value
65
+ end
66
+ memo
67
+ end
68
+ end
69
+
70
+ # Handles missing methods
71
+ #
72
+ # @private
73
+ def method_missing(method, *args, &blk)
74
+ if method.to_s =~ /[?=]$/ || @attributes.include?(method)
75
+ # Extract the attribute
76
+ attribute = method.to_s.sub(/[?=]$/, '')
77
+
78
+ # Create a new `attribute` methods set
79
+ self.class.attributes(*attribute)
80
+
81
+ # Resend the method!
82
+ send(method, *args, &blk)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ # @private
89
+ def respond_to_missing?(method, include_private = false)
90
+ method.to_s.end_with?('=') || method.to_s.end_with?('?') || @attributes.include?(method) || super
91
+ end
92
+
93
+ # Assign new attributes to a resource
94
+ #
95
+ # @example
96
+ # class User
97
+ # include Her::Model
98
+ # end
99
+ #
100
+ # user = User.find(1) # => #<User id=1 name="Tobias">
101
+ # user.assign_attributes(name: "Lindsay")
102
+ # user.changes # => { :name => ["Tobias", "Lindsay"] }
103
+ def assign_attributes(new_attributes)
104
+ @attributes ||= attributes
105
+ # Use setter methods first
106
+ unset_attributes = Her::Model::Attributes.use_setter_methods(self, new_attributes)
107
+
108
+ # Then translate attributes of associations into association instances
109
+ parsed_attributes = self.class.parse_associations(unset_attributes)
110
+
111
+ # Then merge the parsed_data into @attributes.
112
+ @attributes.merge!(parsed_attributes)
113
+ end
114
+ alias attributes= assign_attributes
115
+
116
+ def attributes
117
+ @attributes ||= HashWithIndifferentAccess.new
118
+ end
119
+
120
+ # Handles returning true for the accessible attributes
121
+ #
122
+ # @private
123
+ def has_attribute?(attribute_name)
124
+ @attributes.include?(attribute_name)
125
+ end
126
+
127
+ # Handles returning data for a specific attribute
128
+ #
129
+ # @private
130
+ def get_attribute(attribute_name)
131
+ @attributes[attribute_name]
132
+ end
133
+ alias attribute get_attribute
134
+
135
+ # Return the value of the model `primary_key` attribute
136
+ def id
137
+ @attributes[self.class.primary_key]
138
+ end
139
+
140
+ # Return `true` if the other object is also a Her::Model and has matching data
141
+ #
142
+ # @private
143
+ def ==(other)
144
+ other.is_a?(Her::Model) && @attributes == other.attributes
145
+ end
146
+
147
+ # Delegate to the == method
148
+ #
149
+ # @private
150
+ def eql?(other)
151
+ self == other
152
+ end
153
+
154
+ # Delegate to @attributes, allowing models to act correctly in code like:
155
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
156
+ # @private
157
+ def hash
158
+ @attributes.hash
159
+ end
160
+
161
+ # Assign attribute value (ActiveModel convention method).
162
+ #
163
+ # @private
164
+ def attribute=(attribute, value)
165
+ @attributes[attribute] = nil unless @attributes.include?(attribute)
166
+ self.send(:"#{attribute}_will_change!") if @attributes[attribute] != value
167
+ @attributes[attribute] = value
168
+ end
169
+
170
+ # Check attribute value to be present (ActiveModel convention method).
171
+ #
172
+ # @private
173
+ def attribute?(attribute)
174
+ @attributes.include?(attribute) && @attributes[attribute].present?
175
+ end
176
+
177
+ module ClassMethods
178
+ # Initialize a collection of resources with raw data from an HTTP request
179
+ #
180
+ # @param [Array] parsed_data
181
+ # @private
182
+ def new_collection(parsed_data)
183
+ Her::Model::Attributes.initialize_collection(self, parsed_data)
184
+ end
185
+
186
+ # Initialize a new object with the "raw" parsed_data from the parsing middleware
187
+ #
188
+ # @private
189
+ def new_from_parsed_data(parsed_data)
190
+ parsed_data = parsed_data.with_indifferent_access
191
+ new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
192
+ end
193
+
194
+ # Define attribute method matchers to automatically define them using ActiveModel's define_attribute_methods.
195
+ #
196
+ # @private
197
+ def define_attribute_method_matchers
198
+ attribute_method_suffix '='
199
+ attribute_method_suffix '?'
200
+ end
201
+
202
+ # Create a mutex for dynamically generated attribute methods or use one defined by ActiveModel.
203
+ #
204
+ # @private
205
+ def attribute_methods_mutex
206
+ @attribute_methods_mutex ||= if generated_attribute_methods.respond_to? :mu_synchronize
207
+ generated_attribute_methods
208
+ else
209
+ Mutex.new
210
+ end
211
+ end
212
+
213
+ # Define the attributes that will be used to track dirty attributes and validations
214
+ #
215
+ # @param [Array] attributes
216
+ # @example
217
+ # class User
218
+ # include Her::Model
219
+ # attributes :name, :email
220
+ # end
221
+ def attributes(*attributes)
222
+ attribute_methods_mutex.synchronize do
223
+ define_attribute_methods attributes
224
+ end
225
+ end
226
+
227
+ # Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
228
+ #
229
+ # @param [Symbol] store_response_errors
230
+ #
231
+ # @example
232
+ # class User
233
+ # include Her::Model
234
+ # store_response_errors :server_errors
235
+ # end
236
+ def store_response_errors(value = nil)
237
+ store_her_data(:response_errors, value)
238
+ end
239
+
240
+ # Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
241
+ #
242
+ # @param [Symbol] store_metadata
243
+ #
244
+ # @example
245
+ # class User
246
+ # include Her::Model
247
+ # store_metadata :server_data
248
+ # end
249
+ def store_metadata(value = nil)
250
+ store_her_data(:metadata, value)
251
+ end
252
+
253
+ # @private
254
+ def setter_method_names
255
+ @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
256
+ memo << method_name.to_s if method_name.to_s.end_with?('=')
257
+ memo
258
+ end
259
+ end
260
+
261
+ private
262
+ # @private
263
+ def store_her_data(name, value)
264
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
265
+ if @_her_store_#{name} && value.present?
266
+ remove_method @_her_store_#{name}.to_sym
267
+ remove_method @_her_store_#{name}.to_s + '='
268
+ end
269
+
270
+ @_her_store_#{name} ||= begin
271
+ superclass.store_#{name} if superclass.respond_to?(:store_#{name})
272
+ end
273
+
274
+ return @_her_store_#{name} unless value
275
+ @_her_store_#{name} = value
276
+
277
+ define_method(value) { @#{name} }
278
+ define_method(value.to_s+'=') { |value| @#{name} = value }
279
+ RUBY
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end