arunthampi-supermodel 0.1.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.
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