brianmario-couchrest 0.23
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/LICENSE +176 -0
- data/README.md +95 -0
- data/Rakefile +75 -0
- data/THANKS.md +18 -0
- data/examples/model/example.rb +144 -0
- data/examples/word_count/markov +38 -0
- data/examples/word_count/views/books/chunked-map.js +3 -0
- data/examples/word_count/views/books/united-map.js +1 -0
- data/examples/word_count/views/markov/chain-map.js +6 -0
- data/examples/word_count/views/markov/chain-reduce.js +7 -0
- data/examples/word_count/views/word_count/count-map.js +6 -0
- data/examples/word_count/views/word_count/count-reduce.js +3 -0
- data/examples/word_count/word_count.rb +46 -0
- data/examples/word_count/word_count_query.rb +40 -0
- data/examples/word_count/word_count_views.rb +26 -0
- data/lib/couchrest.rb +198 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/core/database.rb +303 -0
- data/lib/couchrest/core/design.rb +79 -0
- data/lib/couchrest/core/document.rb +87 -0
- data/lib/couchrest/core/response.rb +16 -0
- data/lib/couchrest/core/server.rb +88 -0
- data/lib/couchrest/core/view.rb +4 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +44 -0
- data/lib/couchrest/helper/upgrade.rb +51 -0
- data/lib/couchrest/mixins.rb +4 -0
- data/lib/couchrest/mixins/attachments.rb +31 -0
- data/lib/couchrest/mixins/callbacks.rb +483 -0
- data/lib/couchrest/mixins/class_proxy.rb +108 -0
- data/lib/couchrest/mixins/design_doc.rb +90 -0
- data/lib/couchrest/mixins/document_queries.rb +44 -0
- data/lib/couchrest/mixins/extended_attachments.rb +68 -0
- data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
- data/lib/couchrest/mixins/properties.rb +129 -0
- data/lib/couchrest/mixins/validation.rb +242 -0
- data/lib/couchrest/mixins/views.rb +169 -0
- data/lib/couchrest/monkeypatches.rb +113 -0
- data/lib/couchrest/more/casted_model.rb +28 -0
- data/lib/couchrest/more/extended_document.rb +215 -0
- data/lib/couchrest/more/property.rb +40 -0
- data/lib/couchrest/support/blank.rb +42 -0
- data/lib/couchrest/support/class.rb +176 -0
- data/lib/couchrest/validation/auto_validate.rb +163 -0
- data/lib/couchrest/validation/contextual_validators.rb +78 -0
- data/lib/couchrest/validation/validation_errors.rb +118 -0
- data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
- data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
- data/lib/couchrest/validation/validators/format_validator.rb +117 -0
- data/lib/couchrest/validation/validators/formats/email.rb +66 -0
- data/lib/couchrest/validation/validators/formats/url.rb +43 -0
- data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
- data/lib/couchrest/validation/validators/length_validator.rb +134 -0
- data/lib/couchrest/validation/validators/method_validator.rb +89 -0
- data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
- data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
- data/spec/couchrest/core/couchrest_spec.rb +201 -0
- data/spec/couchrest/core/database_spec.rb +699 -0
- data/spec/couchrest/core/design_spec.rb +138 -0
- data/spec/couchrest/core/document_spec.rb +267 -0
- data/spec/couchrest/core/server_spec.rb +35 -0
- data/spec/couchrest/helpers/pager_spec.rb +122 -0
- data/spec/couchrest/helpers/streamer_spec.rb +23 -0
- data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
- data/spec/couchrest/more/casted_model_spec.rb +98 -0
- data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
- data/spec/couchrest/more/extended_doc_spec.rb +509 -0
- data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
- data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
- data/spec/couchrest/more/property_spec.rb +136 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/more/article.rb +34 -0
- data/spec/fixtures/more/card.rb +20 -0
- data/spec/fixtures/more/course.rb +14 -0
- data/spec/fixtures/more/event.rb +6 -0
- data/spec/fixtures/more/invoice.rb +17 -0
- data/spec/fixtures/more/person.rb +8 -0
- data/spec/fixtures/more/question.rb +6 -0
- data/spec/fixtures/more/service.rb +12 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +26 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +200 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module ClassProxy
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Return a proxy object which represents a model class on a
|
12
|
+
# chosen database instance. This allows you to DRY operations
|
13
|
+
# where a database is chosen dynamically.
|
14
|
+
#
|
15
|
+
# ==== Example:
|
16
|
+
#
|
17
|
+
# db = CouchRest::Database.new(...)
|
18
|
+
# articles = Article.on(db)
|
19
|
+
#
|
20
|
+
# articles.all { ... }
|
21
|
+
# articles.by_title { ... }
|
22
|
+
#
|
23
|
+
# u = articles.get("someid")
|
24
|
+
#
|
25
|
+
# u = articles.new(:title => "I like plankton")
|
26
|
+
# u.save # saved on the correct database
|
27
|
+
|
28
|
+
def on(database)
|
29
|
+
Proxy.new(self, database)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Proxy #:nodoc:
|
34
|
+
def initialize(klass, database)
|
35
|
+
@klass = klass
|
36
|
+
@database = database
|
37
|
+
end
|
38
|
+
|
39
|
+
# ExtendedDocument
|
40
|
+
|
41
|
+
def new(*args)
|
42
|
+
doc = @klass.new(*args)
|
43
|
+
doc.database = @database
|
44
|
+
doc
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_missing(m, *args, &block)
|
48
|
+
if has_view?(m)
|
49
|
+
query = args.shift || {}
|
50
|
+
view(m, query, *args, &block)
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Mixins::DocumentQueries
|
57
|
+
|
58
|
+
def all(opts = {}, &block)
|
59
|
+
@klass.all({:database => @database}.merge(opts), &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def first(opts = {})
|
63
|
+
@klass.first({:database => @database}.merge(opts))
|
64
|
+
end
|
65
|
+
|
66
|
+
def get(id)
|
67
|
+
@klass.get(id, @database)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Mixins::Views
|
71
|
+
|
72
|
+
def has_view?(view)
|
73
|
+
@klass.has_view?(view)
|
74
|
+
end
|
75
|
+
|
76
|
+
def view(name, query={}, &block)
|
77
|
+
@klass.view(name, {:database => @database}.merge(query), &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def all_design_doc_versions
|
81
|
+
@klass.all_design_doc_versions(@database)
|
82
|
+
end
|
83
|
+
|
84
|
+
def cleanup_design_docs!
|
85
|
+
@klass.cleanup_design_docs!(@database)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Mixins::DesignDoc
|
89
|
+
|
90
|
+
def design_doc
|
91
|
+
@klass.design_doc
|
92
|
+
end
|
93
|
+
|
94
|
+
def design_doc_fresh
|
95
|
+
@klass.design_doc_fresh
|
96
|
+
end
|
97
|
+
|
98
|
+
def refresh_design_doc
|
99
|
+
@klass.refresh_design_doc
|
100
|
+
end
|
101
|
+
|
102
|
+
def save_design_doc
|
103
|
+
@klass.save_design_doc_on(@database)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
module Mixins
|
5
|
+
module DesignDoc
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
attr_accessor :design_doc, :design_doc_slug_cache, :design_doc_fresh
|
13
|
+
|
14
|
+
def design_doc
|
15
|
+
@design_doc ||= Design.new(default_design_doc)
|
16
|
+
end
|
17
|
+
|
18
|
+
def design_doc_id
|
19
|
+
"_design/#{design_doc_slug}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def design_doc_slug
|
23
|
+
return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
|
24
|
+
funcs = []
|
25
|
+
design_doc['views'].each do |name, view|
|
26
|
+
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
27
|
+
end
|
28
|
+
md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
|
29
|
+
self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def default_design_doc
|
33
|
+
{
|
34
|
+
"language" => "javascript",
|
35
|
+
"views" => {
|
36
|
+
'all' => {
|
37
|
+
'map' => "function(doc) {
|
38
|
+
if (doc['couchrest-type'] == '#{self.to_s}') {
|
39
|
+
emit(null,null);
|
40
|
+
}
|
41
|
+
}"
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def refresh_design_doc
|
48
|
+
design_doc['_id'] = design_doc_id
|
49
|
+
design_doc.delete('_rev')
|
50
|
+
#design_doc.database = nil
|
51
|
+
self.design_doc_fresh = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Save the design doc onto the default database, and update the
|
55
|
+
# design_doc attribute
|
56
|
+
def save_design_doc
|
57
|
+
refresh_design_doc unless design_doc_fresh
|
58
|
+
self.design_doc = update_design_doc(design_doc)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Save the design doc onto a target database in a thread-safe way,
|
62
|
+
# not modifying the model's design_doc
|
63
|
+
def save_design_doc_on(db)
|
64
|
+
update_design_doc(Design.new(design_doc), db)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Writes out a design_doc to a given database, returning the
|
70
|
+
# updated design doc
|
71
|
+
def update_design_doc(design_doc, db = database)
|
72
|
+
saved = db.get(design_doc['_id']) rescue nil
|
73
|
+
if saved
|
74
|
+
design_doc['views'].each do |name, view|
|
75
|
+
saved['views'][name] = view
|
76
|
+
end
|
77
|
+
db.save_doc(saved)
|
78
|
+
saved
|
79
|
+
else
|
80
|
+
design_doc.database = db
|
81
|
+
design_doc.save
|
82
|
+
design_doc
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end # module ClassMethods
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module DocumentQueries
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Load all documents that have the "couchrest-type" field equal to the
|
12
|
+
# name of the current class. Take the standard set of
|
13
|
+
# CouchRest::Database#view options.
|
14
|
+
def all(opts = {}, &block)
|
15
|
+
view(:all, opts, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Load the first document that have the "couchrest-type" field equal to
|
19
|
+
# the name of the current class.
|
20
|
+
#
|
21
|
+
# ==== Returns
|
22
|
+
# Object:: The first object instance available
|
23
|
+
# or
|
24
|
+
# Nil:: if no instances available
|
25
|
+
#
|
26
|
+
# ==== Parameters
|
27
|
+
# opts<Hash>::
|
28
|
+
# View options, see <tt>CouchRest::Database#view</tt> options for more info.
|
29
|
+
def first(opts = {})
|
30
|
+
first_instance = self.all(opts.merge!(:limit => 1))
|
31
|
+
first_instance.empty? ? nil : first_instance.first
|
32
|
+
end
|
33
|
+
|
34
|
+
# Load a document from the database by id
|
35
|
+
def get(id, db = database)
|
36
|
+
doc = db.get id
|
37
|
+
new(doc)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module ExtendedAttachments
|
4
|
+
|
5
|
+
# creates a file attachment to the current doc
|
6
|
+
def create_attachment(args={})
|
7
|
+
raise ArgumentError unless args[:file] && args[:name]
|
8
|
+
return if has_attachment?(args[:name])
|
9
|
+
self['_attachments'] ||= {}
|
10
|
+
set_attachment_attr(args)
|
11
|
+
rescue ArgumentError => e
|
12
|
+
raise ArgumentError, 'You must specify :file and :name'
|
13
|
+
end
|
14
|
+
|
15
|
+
# reads the data from an attachment
|
16
|
+
def read_attachment(attachment_name)
|
17
|
+
Base64.decode64(database.fetch_attachment(self, attachment_name))
|
18
|
+
end
|
19
|
+
|
20
|
+
# modifies a file attachment on the current doc
|
21
|
+
def update_attachment(args={})
|
22
|
+
raise ArgumentError unless args[:file] && args[:name]
|
23
|
+
return unless has_attachment?(args[:name])
|
24
|
+
delete_attachment(args[:name])
|
25
|
+
set_attachment_attr(args)
|
26
|
+
rescue ArgumentError => e
|
27
|
+
raise ArgumentError, 'You must specify :file and :name'
|
28
|
+
end
|
29
|
+
|
30
|
+
# deletes a file attachment from the current doc
|
31
|
+
def delete_attachment(attachment_name)
|
32
|
+
return unless self['_attachments']
|
33
|
+
self['_attachments'].delete attachment_name
|
34
|
+
end
|
35
|
+
|
36
|
+
# returns true if attachment_name exists
|
37
|
+
def has_attachment?(attachment_name)
|
38
|
+
!!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
|
39
|
+
end
|
40
|
+
|
41
|
+
# returns URL to fetch the attachment from
|
42
|
+
def attachment_url(attachment_name)
|
43
|
+
return unless has_attachment?(attachment_name)
|
44
|
+
"#{database.root}/#{self.id}/#{attachment_name}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def encode_attachment(data)
|
50
|
+
::Base64.encode64(data).gsub(/\r|\n/,'')
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_mime_type(file)
|
54
|
+
::MIME::Types.type_for(file.path).empty? ?
|
55
|
+
'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_attachment_attr(args)
|
59
|
+
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
|
60
|
+
self['_attachments'][args[:name]] = {
|
61
|
+
'content-type' => content_type,
|
62
|
+
'data' => encode_attachment(args[:file].read)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
end # module ExtendedAttachments
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'properties')
|
2
|
+
require File.join(File.dirname(__FILE__), 'document_queries')
|
3
|
+
require File.join(File.dirname(__FILE__), 'views')
|
4
|
+
require File.join(File.dirname(__FILE__), 'design_doc')
|
5
|
+
require File.join(File.dirname(__FILE__), 'validation')
|
6
|
+
require File.join(File.dirname(__FILE__), 'extended_attachments')
|
7
|
+
require File.join(File.dirname(__FILE__), 'class_proxy')
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'time'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'more', 'property')
|
3
|
+
|
4
|
+
module CouchRest
|
5
|
+
module Mixins
|
6
|
+
module Properties
|
7
|
+
|
8
|
+
class IncludeError < StandardError; end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval <<-EOS, __FILE__, __LINE__
|
12
|
+
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
13
|
+
self.properties ||= []
|
14
|
+
EOS
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_defaults
|
20
|
+
return unless self.respond_to?(:new_document?) && new_document?
|
21
|
+
return unless self.class.respond_to?(:properties)
|
22
|
+
return if self.class.properties.empty?
|
23
|
+
# TODO: cache the default object
|
24
|
+
self.class.properties.each do |property|
|
25
|
+
key = property.name.to_s
|
26
|
+
# let's make sure we have a default and we can assign the value
|
27
|
+
if property.default && (self.respond_to?("#{key}=") || self.key?(key))
|
28
|
+
if property.default.class == Proc
|
29
|
+
self[key] = property.default.call
|
30
|
+
else
|
31
|
+
self[key] = Marshal.load(Marshal.dump(property.default))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def cast_keys
|
38
|
+
return unless self.class.properties
|
39
|
+
self.class.properties.each do |property|
|
40
|
+
next unless property.casted
|
41
|
+
key = self.has_key?(property.name) ? property.name : property.name.to_sym
|
42
|
+
target = property.type
|
43
|
+
if target.is_a?(Array)
|
44
|
+
next unless self[key]
|
45
|
+
klass = ::CouchRest.constantize(target[0])
|
46
|
+
self[property.name] = self[key].collect do |value|
|
47
|
+
# Auto parse Time objects
|
48
|
+
obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)
|
49
|
+
obj.casted_by = self if obj.respond_to?(:casted_by)
|
50
|
+
obj
|
51
|
+
end
|
52
|
+
else
|
53
|
+
# Auto parse Time objects
|
54
|
+
self[property.name] = if ((property.init_method == 'new') && target == 'Time')
|
55
|
+
self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key]
|
56
|
+
else
|
57
|
+
# Let people use :send as a Time parse arg
|
58
|
+
klass = ::CouchRest.constantize(target)
|
59
|
+
# I'm not convince we should or should not create a new instance if we are casting a doc/extended doc without default value and nothing was passed
|
60
|
+
# unless (property.casted &&
|
61
|
+
# (klass.superclass == CouchRest::ExtendedDocument || klass.superclass == CouchRest::Document) &&
|
62
|
+
# (self[key].nil? || property.default.nil?))
|
63
|
+
klass.send(property.init_method, self[key])
|
64
|
+
#end
|
65
|
+
end
|
66
|
+
self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
|
73
|
+
def property(name, options={})
|
74
|
+
existing_property = self.properties.find{|p| p.name == name.to_s}
|
75
|
+
if existing_property.nil? || (existing_property.default != options[:default])
|
76
|
+
define_property(name, options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
# This is not a thread safe operation, if you have to set new properties at runtime
|
83
|
+
# make sure to use a mutex.
|
84
|
+
def define_property(name, options={})
|
85
|
+
# check if this property is going to casted
|
86
|
+
options[:casted] = options[:cast_as] ? options[:cast_as] : false
|
87
|
+
property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options)
|
88
|
+
create_property_getter(property)
|
89
|
+
create_property_setter(property) unless property.read_only == true
|
90
|
+
properties << property
|
91
|
+
end
|
92
|
+
|
93
|
+
# defines the getter for the property (and optional aliases)
|
94
|
+
def create_property_getter(property)
|
95
|
+
# meth = property.name
|
96
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
97
|
+
def #{property.name}
|
98
|
+
self['#{property.name}']
|
99
|
+
end
|
100
|
+
EOS
|
101
|
+
|
102
|
+
if property.alias
|
103
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
104
|
+
alias #{property.alias.to_sym} #{property.name.to_sym}
|
105
|
+
EOS
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# defines the setter for the property (and optional aliases)
|
110
|
+
def create_property_setter(property)
|
111
|
+
meth = property.name
|
112
|
+
class_eval <<-EOS
|
113
|
+
def #{meth}=(value)
|
114
|
+
self['#{meth}'] = value
|
115
|
+
end
|
116
|
+
EOS
|
117
|
+
|
118
|
+
if property.alias
|
119
|
+
class_eval <<-EOS
|
120
|
+
alias #{property.alias.to_sym}= #{meth.to_sym}=
|
121
|
+
EOS
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end # module ClassMethods
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|