arunthampi-supermodel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Arun Thampi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,23 @@
1
+ SuperModel aims to give sexy semantics to any model/library in Ruby that you might create. This includes has, has_many and has_one semantics which we generally associate with ActiveRecord.
2
+
3
+ This project draws its roots from the ActiveCouch project (http://www.github.com/arunthampi/activecouch), after which I saw myself re-using the same semantics in many other projects.
4
+
5
+ Hence, the SuperModel project was born!
6
+
7
+ The irony is that even though a SuperModel will be more bloated than a regular Ruby model, it will be better looking.
8
+
9
+ So with SuperModel, you can define a model such as this:
10
+
11
+ class Person < SuperModel::Base
12
+ has :name, :which_is => :text, :with_default_value => "McLovin"
13
+ end
14
+
15
+ Also supports JSON serialization, so you can do this:
16
+
17
+ p = Person.new(:name => 'McLovin').to_json # => {"name":"McLovin"}
18
+
19
+ Plans For Future
20
+ ----------------
21
+ 1. Serialization in any format: to_xml, to_yaml methods (and of course from_xml, from_yaml methods as well)
22
+ 2. Callbacks: Define any callback for any event (This is stolen from ActiveRecord)
23
+ 3. More Sexiness and Awesomeness
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.platform = Gem::Platform::RUBY
6
+ s.summary = "Sexy Semantics for Any Ruby Model"
7
+ s.name = 'supermodel'
8
+ s.author = 'Arun Thampi'
9
+ s.email = "arun.thampi@gmail.com"
10
+ s.homepage = "http://www.github.com/arunthampi/supermodel"
11
+ s.version = '0.1.0'
12
+ s.files = FileList[ '[A-Z]*', 'lib/**/*.rb', 'spec/**/*.rb' ],
13
+ s.has_rdoc = true
14
+ s.require_path = "lib"
15
+ s.extra_rdoc_files = ["README"]
16
+ s.add_dependency 'json', '>=1.1.2'
17
+ end
18
+
19
+ Rake::GemPackageTask.new(spec) do |pkg|
20
+ pkg.need_zip = true
21
+ pkg.need_tar = true
22
+ end
23
+
24
+ task :lines do
25
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
26
+
27
+ for file_name in FileList["lib/supermodel/**/*.rb"]
28
+ next if file_name =~ /vendor/
29
+ f = File.open(file_name)
30
+
31
+ while line = f.gets
32
+ lines += 1
33
+ next if line =~ /^\s*$/
34
+ next if line =~ /^\s*#/
35
+ codelines += 1
36
+ end
37
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
38
+
39
+ total_lines += lines
40
+ total_codelines += codelines
41
+
42
+ lines, codelines = 0, 0
43
+ end
44
+
45
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
46
+ end
47
+
48
+ task :default => [:package]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/supermodel.rb ADDED
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+ require 'json'
6
+
7
+ require 'supermodel/support'
8
+ require 'supermodel/errors'
9
+ require 'supermodel/base'
@@ -0,0 +1,284 @@
1
+ module SuperModel
2
+ class Base
3
+ SPECIAL_MEMBERS = %w(attributes has_many_associations has_one_associations)
4
+ TYPES = { :text => "", :number => 0, :decimal => 0.0, :boolean => true }
5
+ TYPES.default = ""
6
+
7
+ # Initializes an SuperModel::Base object. The constructor accepts both a hash, as well as
8
+ # a block to initialize attributes
9
+ #
10
+ # Examples:
11
+ # class Person < SuperModel::Base
12
+ # has :name
13
+ # end
14
+ #
15
+ # person1 = Person.new(:name => "McLovin")
16
+ # person1.name # => "McLovin"
17
+ #
18
+ # person2 = Person.new do |p|
19
+ # p.name = "Seth"
20
+ # end
21
+ # person2.name # => "Seth"
22
+ def initialize(params = {})
23
+ # Object instance variable
24
+ @attributes, @has_many_associations = {}, @has_one_associations = {}
25
+ # Initialize local variables from class instance variables
26
+ klass_atts = self.class.attributes
27
+ klass_has_many_assocs = self.class.has_many_associations
28
+ klass_has_one_assocs = self.class.has_one_associations
29
+ # Define getter methods for special members
30
+ SPECIAL_MEMBERS.each do |k|
31
+ self.instance_eval "def #{k}; @#{k}; end"
32
+ end
33
+ # First, initialize all the attributes
34
+ klass_atts.each_key do |property|
35
+ @attributes[property] = klass_atts[property]
36
+ self.instance_eval "def #{property}; attributes[:#{property}]; end"
37
+ self.instance_eval "def #{property}=(val); attributes[:#{property}] = val; end"
38
+ end
39
+ # Then, initialize all the has-many associations
40
+ klass_has_many_assocs.each do |k,v|
41
+ @has_many_associations[k] = v
42
+ self.instance_eval "def #{k}; @#{k} ||= []; end"
43
+ # If you have has_many :people, this will add a method called add_person
44
+ # to the object instantiated from the class
45
+ self.instance_eval "def add_#{k.singularize}(val); @#{k} = #{k} << val; end"
46
+ end
47
+
48
+ # Then, initialize all the has-many associations
49
+ klass_has_one_assocs.each do |k,v|
50
+ @has_one_associations[k] = v
51
+ self.instance_eval "def #{k}; @#{k}; end"
52
+ # If you have has_many :people, this will add a method called add_person
53
+ # to the object instantiated from the class
54
+ self.instance_eval "def #{k.singularize}=(val); @#{k} = val; end"
55
+ end
56
+
57
+ # Set any instance variables if any, which are present in the params hash
58
+ from_hash(params)
59
+ # Handle the block, which can also be used to initialize the object
60
+ yield self if block_given?
61
+ end
62
+
63
+ # Generates a JSON representation of an instance of a subclass of SuperModel::Base.
64
+ # Ignores attributes which have a nil value.
65
+ #
66
+ # Examples:
67
+ # class Person < SuperModel::Base
68
+ # has :name, :which_is => :text, :with_default_value => "McLovin"
69
+ # end
70
+ #
71
+ # person = Person.new
72
+ # person.to_json # {"name":"McLovin"}
73
+ #
74
+ # class AgedPerson < SuperModel::Base
75
+ # has :age, :which_is => :decimal, :with_default_value => 3.5
76
+ # end
77
+ #
78
+ # aged_person = AgedPerson.new
79
+ # aged_person.id = 'abc-def'
80
+ # aged_person.to_json # {"age":3.5, "_id":"abc-def"}
81
+ def to_json
82
+ hash = {}
83
+ # First merge the attributes...
84
+ hash.merge!(attributes.reject{ |k,v| v.nil? })
85
+ # ...and then the associations, first has_many...
86
+ has_many_associations.each_key { |name| hash.merge!({ name => self.__send__(name.to_s) }) }
87
+ # ...and then has_one
88
+ has_one_associations.each_key { |name| hash.merge!({ name => self.__send__(name.to_s) }) }
89
+ # and by the Power of Grayskull, convert the hash to json
90
+ hash.to_json
91
+ end
92
+
93
+ def marshal_dump # :nodoc:
94
+ # Deflate using Zlib
95
+ self.to_json
96
+ end
97
+
98
+ def marshal_load(str) # :nodoc:
99
+ self.instance_eval do
100
+ # Inflate first, and then parse the JSON
101
+ hash = JSON.parse(str)
102
+ initialize(hash)
103
+ end
104
+ self
105
+ end
106
+
107
+ class << self # Class methods
108
+ # Defines an attribute for a subclass of SuperModel::Base. The parameters
109
+ # for this method include name, which is the name of the attribute as well as
110
+ # an options hash.
111
+ #
112
+ # The options hash can contain the key 'which_is' which can
113
+ # have possible values :text, :decimal, :number. It can also contain the key
114
+ # 'with_default_value' which can set a default value for each attribute defined
115
+ # in the subclass of SuperModel::Base
116
+ #
117
+ # Examples:
118
+ # class Person < SuperModel::Base
119
+ # has :name
120
+ # end
121
+ #
122
+ # person = Person.new
123
+ # p.name.methods.include?(:name) # true
124
+ # p.name.methods.include?(:name=) # false
125
+ #
126
+ # class AgedPerson < SuperModel::Base
127
+ # has :age, :which_is => :number, :with_default_value = 18
128
+ # end
129
+ #
130
+ # person = AgedPerson.new
131
+ # person.age # 18
132
+ def has(name, options = {})
133
+ unless name.is_a?(String) || name.is_a?(Symbol)
134
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
135
+ end
136
+ # Set the attributes value to options[:with_default_value]
137
+ # In the constructor, this will be used to initialize the value of
138
+ # the 'name' instance variable to the value in the hash
139
+ @attributes[name] = options[:with_default_value] || TYPES[:which_is]
140
+ end
141
+
142
+ # Defines an array of objects which are 'children' of this class. The has_many
143
+ # function guesses the class of the child, based on the name of the association,
144
+ # but can be over-ridden by the :class key in the options hash.
145
+ #
146
+ # Examples:
147
+ #
148
+ # class Person < SuperModel::Base
149
+ # has :name
150
+ # end
151
+ #
152
+ # class GrandPerson < SuperModel::Base
153
+ # has_many :people # which will create an empty array which can contain
154
+ # # Person objects
155
+ # end
156
+ def has_many(name, options = {})
157
+ unless name.is_a?(String) || name.is_a?(Symbol)
158
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
159
+ end
160
+
161
+ @has_many_associations[name] = get_klass(name, options)
162
+ end
163
+
164
+ # Defines a single object which is a 'child' of this class. The has_one
165
+ # function guesses the class of the child, based on the name of the association,
166
+ # but can be over-ridden by the :class key in the options hash.
167
+ #
168
+ # Examples:
169
+ #
170
+ # class Child < SuperModel::Base
171
+ # has :name
172
+ # end
173
+ #
174
+ # class GrandParent < SuperModel::Base
175
+ # has_one :child
176
+ # end
177
+ def has_one(name, options = {})
178
+ unless name.is_a?(String) || name.is_a?(Symbol)
179
+ raise ArgumentError, "#{name} is neither a String nor a Symbol"
180
+ end
181
+
182
+ @has_one_associations[name] = get_klass(name, options)
183
+ end
184
+
185
+ # Initializes an object of a subclass of SuperModel::Base based on a JSON
186
+ # representation of the object.
187
+ #
188
+ # Example:
189
+ # class Person < SuperModel::Base
190
+ # has :name
191
+ # end
192
+ #
193
+ # person = Person.from_json('{"name":"McLovin"}')
194
+ # person.name # "McLovin"
195
+ def from_json(json)
196
+ hash = JSON.parse(json)
197
+ # Create new based on parsed
198
+ self.new(hash)
199
+ end
200
+
201
+ # Defines an "attribute" method. A new (class) method will be created with the
202
+ # given name. If a value is specified, the new method will
203
+ # return that value (as a string). Otherwise, the given block
204
+ # will be used to compute the value of the method.
205
+ #
206
+ # The original method, if it exists, will be aliased, with the
207
+ # new name being
208
+ # prefixed with "original_". This allows the new method to
209
+ # access the original value.
210
+ #
211
+ # This method is stolen from ActiveRecord.
212
+ #
213
+ # Example:
214
+ #
215
+ # class Foo < SuperModel::Base
216
+ # define_attr_method :database_name, 'foo'
217
+ # # OR
218
+ # define_attr_method(:database_name) do
219
+ # original_database_name + '_legacy'
220
+ # end
221
+ # end
222
+ def define_attr_method(name, value = nil, &block)
223
+ metaclass.send(:alias_method, "original_#{name}", name)
224
+ if block_given?
225
+ meta_def name, &block
226
+ else
227
+ metaclass.class_eval "def #{name}; #{value.to_s.inspect}; end"
228
+ end
229
+ end
230
+
231
+ def inherited(subklass)
232
+ subklass.instance_eval do
233
+ @attributes, @has_many_associations, @has_one_associations = {}, {}, {}
234
+ end
235
+
236
+ SPECIAL_MEMBERS.each do |k|
237
+ subklass.instance_eval "def #{k}; @#{k}; end"
238
+ end
239
+ end
240
+
241
+ def base_class
242
+ class_of_supermodel_descendant(self)
243
+ end
244
+
245
+ private
246
+ # Generate a class from a name
247
+ def get_klass(name, options)
248
+ klass = options[:class]
249
+ !klass.nil? && klass.is_a?(Class) ? klass : name.to_s.classify.constantize
250
+ end
251
+
252
+ # Returns the class descending directly from SuperModel in the inheritance hierarchy.
253
+ def class_of_supermodel_descendant(klass)
254
+ if klass.superclass == Base
255
+ klass
256
+ elsif klass.superclass.nil?
257
+ raise SuperModelError, "#{name} doesn't belong in a hierarchy descending from SuperModel"
258
+ else
259
+ class_of_supermodel_descendant(klass.superclass)
260
+ end
261
+ end
262
+ end # End class methods
263
+
264
+ private
265
+ def from_hash(hash)
266
+ hash.each do |property, value|
267
+ property = property.to_sym rescue property
268
+ # This means a has_many association
269
+ if value.is_a?(Array) && !(child_klass = @has_many_associations[property]).nil?
270
+ value.each do |child|
271
+ child.is_a?(Hash) ? child_obj = child_klass.new(child) : child_obj = child
272
+ self.send "add_#{property.to_s.singularize}", child_obj
273
+ end
274
+ # This means a has_one association
275
+ elsif value.is_a?(Hash) && !(child_klass = @has_one_associations[property]).nil?
276
+ self.send "#{property.to_s.singularize}=", child_klass.new(value)
277
+ # This means this is a normal attribute
278
+ else
279
+ self.send("#{property}=", value) if respond_to?("#{property}=")
280
+ end
281
+ end
282
+ end
283
+ end # End class Base
284
+ end # End module SuperModel
@@ -0,0 +1,6 @@
1
+ module SuperModel
2
+ # Base exception class for all SuperModel errors.
3
+ class SuperModelError < StandardError
4
+ end
5
+
6
+ end
@@ -0,0 +1,2 @@
1
+ require 'supermodel/support/inflector'
2
+ require 'supermodel/support/extensions'
@@ -0,0 +1,80 @@
1
+ module SuperModel
2
+
3
+ Symbol.class_eval do
4
+ def singularize; Inflector.singularize(self); end
5
+ end
6
+
7
+ String.class_eval do
8
+ require 'cgi'
9
+ def url_encode; CGI.escape("\"#{self.to_s}\""); end
10
+ # Delegate to Inflector
11
+ def singularize; Inflector.singularize(self); end
12
+ def demodulize; Inflector.demodulize(self); end
13
+ def pluralize; Inflector.pluralize(self); end
14
+ def underscore; Inflector.underscore(self); end
15
+ def classify; Inflector.classify(self); end
16
+ def constantize; Inflector.constantize(self); end
17
+ end
18
+
19
+ Hash.class_eval do
20
+ # Flatten on the array removes everything into *one* single array,
21
+ # so {}.to_a.flatten sometimes won't work nicely because a value might be an array
22
+ # So..introducing flatten for Hash, so that arrays which are values (to keys)
23
+ # are retained
24
+ def flatten
25
+ (0...self.size).inject([]) {|k,v| k << self.keys[v]; k << self.values[v]}
26
+ end
27
+ end
28
+
29
+ Object.class_eval do
30
+ def get_class(name)
31
+ # From 'The Ruby Way Second Edition' by Hal Fulton
32
+ # This is to get nested class for e.g. A::B::C
33
+ name.split("::").inject(Object) {|x,y| x.const_get(y)}
34
+ end
35
+
36
+ # The singleton class.
37
+ def metaclass; class << self; self; end; end
38
+ def meta_eval &blk; metaclass.instance_eval &blk; end
39
+
40
+ # Adds methods to a metaclass.
41
+ def meta_def name, &blk
42
+ meta_eval { define_method name, &blk }
43
+ end
44
+
45
+ # Defines an instance method within a class.
46
+ def class_def name, &blk
47
+ class_eval { define_method name, &blk }
48
+ end
49
+ end
50
+
51
+ Module.module_eval do
52
+ # Return the module which contains this one; if this is a root module, such as
53
+ # +::MyModule+, then Object is returned.
54
+ def parent
55
+ parent_name = name.split('::')[0..-2] * '::'
56
+ parent_name.empty? ? Object : Inflector.constantize(parent_name)
57
+ end
58
+
59
+ def alias_method_chain(target, feature)
60
+ # Strip out punctuation on predicates or bang methods since
61
+ # e.g. target?_without_feature is not a valid method name.
62
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
63
+ yield(aliased_target, punctuation) if block_given?
64
+
65
+ with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
66
+
67
+ alias_method without_method, target
68
+ alias_method target, with_method
69
+
70
+ case
71
+ when public_method_defined?(without_method)
72
+ public target
73
+ when protected_method_defined?(without_method)
74
+ protected target
75
+ when private_method_defined?(without_method)
76
+ private target
77
+ end
78
+ end
79
+ end
80
+ end