couchrest_model 1.0.0.beta7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +320 -0
  3. data/Rakefile +71 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +180 -0
  7. data/lib/couchrest/model.rb +10 -0
  8. data/lib/couchrest/model/associations.rb +207 -0
  9. data/lib/couchrest/model/attribute_protection.rb +74 -0
  10. data/lib/couchrest/model/attributes.rb +75 -0
  11. data/lib/couchrest/model/base.rb +111 -0
  12. data/lib/couchrest/model/callbacks.rb +27 -0
  13. data/lib/couchrest/model/casted_array.rb +39 -0
  14. data/lib/couchrest/model/casted_model.rb +68 -0
  15. data/lib/couchrest/model/class_proxy.rb +122 -0
  16. data/lib/couchrest/model/collection.rb +260 -0
  17. data/lib/couchrest/model/design_doc.rb +126 -0
  18. data/lib/couchrest/model/document_queries.rb +82 -0
  19. data/lib/couchrest/model/errors.rb +23 -0
  20. data/lib/couchrest/model/extended_attachments.rb +73 -0
  21. data/lib/couchrest/model/persistence.rb +141 -0
  22. data/lib/couchrest/model/properties.rb +144 -0
  23. data/lib/couchrest/model/property.rb +96 -0
  24. data/lib/couchrest/model/support/couchrest.rb +19 -0
  25. data/lib/couchrest/model/support/hash.rb +9 -0
  26. data/lib/couchrest/model/typecast.rb +170 -0
  27. data/lib/couchrest/model/validations.rb +68 -0
  28. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  29. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  30. data/lib/couchrest/model/validations/uniqueness.rb +45 -0
  31. data/lib/couchrest/model/views.rb +167 -0
  32. data/lib/couchrest_model.rb +56 -0
  33. data/spec/couchrest/assocations_spec.rb +213 -0
  34. data/spec/couchrest/attachment_spec.rb +148 -0
  35. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  36. data/spec/couchrest/base_spec.rb +463 -0
  37. data/spec/couchrest/casted_model_spec.rb +424 -0
  38. data/spec/couchrest/casted_spec.rb +75 -0
  39. data/spec/couchrest/class_proxy_spec.rb +132 -0
  40. data/spec/couchrest/inherited_spec.rb +40 -0
  41. data/spec/couchrest/persistence_spec.rb +409 -0
  42. data/spec/couchrest/property_spec.rb +804 -0
  43. data/spec/couchrest/subclass_spec.rb +99 -0
  44. data/spec/couchrest/validations.rb +73 -0
  45. data/spec/couchrest/view_spec.rb +463 -0
  46. data/spec/fixtures/attachments/README +3 -0
  47. data/spec/fixtures/attachments/couchdb.png +0 -0
  48. data/spec/fixtures/attachments/test.html +11 -0
  49. data/spec/fixtures/base.rb +139 -0
  50. data/spec/fixtures/more/article.rb +35 -0
  51. data/spec/fixtures/more/card.rb +17 -0
  52. data/spec/fixtures/more/cat.rb +19 -0
  53. data/spec/fixtures/more/course.rb +25 -0
  54. data/spec/fixtures/more/event.rb +8 -0
  55. data/spec/fixtures/more/invoice.rb +14 -0
  56. data/spec/fixtures/more/person.rb +9 -0
  57. data/spec/fixtures/more/question.rb +7 -0
  58. data/spec/fixtures/more/service.rb +10 -0
  59. data/spec/fixtures/more/user.rb +22 -0
  60. data/spec/fixtures/views/lib.js +3 -0
  61. data/spec/fixtures/views/test_view/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/only-map.js +4 -0
  63. data/spec/fixtures/views/test_view/test-map.js +3 -0
  64. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  65. data/spec/spec.opts +5 -0
  66. data/spec/spec_helper.rb +48 -0
  67. data/utils/remap.rb +27 -0
  68. data/utils/subset.rb +30 -0
  69. metadata +232 -0
@@ -0,0 +1,74 @@
1
+ module CouchRest
2
+ module Model
3
+ module AttributeProtection
4
+ # Attribute protection from mass assignment to CouchRest properties
5
+ #
6
+ # Protected methods will be removed from
7
+ # * new
8
+ # * update_attributes
9
+ # * upate_attributes_without_saving
10
+ # * attributes=
11
+ #
12
+ # There are two modes of protection
13
+ # 1) Declare accessible poperties, assume all the rest are protected
14
+ # property :name, :accessible => true
15
+ # property :admin # this will be automatically protected
16
+ #
17
+ # 2) Declare protected properties, assume all the rest are accessible
18
+ # property :name # this will not be protected
19
+ # property :admin, :protected => true
20
+ #
21
+ # Note: you cannot set both flags in a single class
22
+
23
+ def self.included(base)
24
+ base.extend(ClassMethods)
25
+ end
26
+
27
+ module ClassMethods
28
+ def accessible_properties
29
+ properties.select { |prop| prop.options[:accessible] }
30
+ end
31
+
32
+ def protected_properties
33
+ properties.select { |prop| prop.options[:protected] }
34
+ end
35
+ end
36
+
37
+ def accessible_properties
38
+ self.class.accessible_properties
39
+ end
40
+
41
+ def protected_properties
42
+ self.class.protected_properties
43
+ end
44
+
45
+ def remove_protected_attributes(attributes)
46
+ protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
47
+ return attributes if protected_names.empty?
48
+
49
+ attributes.reject! do |property_name, property_value|
50
+ protected_names.include?(property_name.to_s)
51
+ end
52
+
53
+ attributes || {}
54
+ end
55
+
56
+ private
57
+
58
+ def properties_to_remove_from_mass_assignment
59
+ has_protected = !protected_properties.empty?
60
+ has_accessible = !accessible_properties.empty?
61
+
62
+ if !has_protected && !has_accessible
63
+ []
64
+ elsif has_protected && !has_accessible
65
+ protected_properties
66
+ elsif has_accessible && !has_protected
67
+ properties.reject { |prop| prop.options[:accessible] }
68
+ else
69
+ raise "Set either :accessible or :protected for #{self.class}, but not both"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,75 @@
1
+ module CouchRest
2
+ module Model
3
+ module Attributes
4
+
5
+ ## Support for handling attributes
6
+ #
7
+ # This would be better in the properties file, but due to scoping issues
8
+ # this is not yet possible.
9
+ #
10
+
11
+ def prepare_all_attributes(doc = {}, options = {})
12
+ apply_all_property_defaults
13
+ if options[:directly_set_attributes]
14
+ directly_set_read_only_attributes(doc)
15
+ else
16
+ remove_protected_attributes(doc)
17
+ end
18
+ directly_set_attributes(doc) unless doc.nil?
19
+ end
20
+
21
+ # Takes a hash as argument, and applies the values by using writer methods
22
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
23
+ # missing. In case of error, no attributes are changed.
24
+ def update_attributes_without_saving(hash)
25
+ # Remove any protected and update all the rest. Any attributes
26
+ # which do not have a property will simply be ignored.
27
+ attrs = remove_protected_attributes(hash)
28
+ directly_set_attributes(attrs)
29
+ end
30
+ alias :attributes= :update_attributes_without_saving
31
+
32
+ # Takes a hash as argument, and applies the values by using writer methods
33
+ # for each key. Raises a NoMethodError if the corresponding methods are
34
+ # missing. In case of error, no attributes are changed.
35
+ def update_attributes(hash)
36
+ update_attributes_without_saving hash
37
+ save
38
+ end
39
+
40
+ private
41
+
42
+ def directly_set_attributes(hash)
43
+ hash.each do |attribute_name, attribute_value|
44
+ if self.respond_to?("#{attribute_name}=")
45
+ self.send("#{attribute_name}=", hash.delete(attribute_name))
46
+ end
47
+ end
48
+ end
49
+
50
+ def directly_set_read_only_attributes(hash)
51
+ property_list = self.properties.map{|p| p.name}
52
+ hash.each do |attribute_name, attribute_value|
53
+ next if self.respond_to?("#{attribute_name}=")
54
+ if property_list.include?(attribute_name)
55
+ write_attribute(attribute_name, hash.delete(attribute_name))
56
+ end
57
+ end
58
+ end
59
+
60
+ def set_attributes(hash)
61
+ attrs = remove_protected_attributes(hash)
62
+ directly_set_attributes(attrs)
63
+ end
64
+
65
+ def check_properties_exist(attrs)
66
+ property_list = self.properties.map{|p| p.name}
67
+ attrs.each do |attribute_name, attribute_value|
68
+ raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,111 @@
1
+ module CouchRest
2
+ module Model
3
+ class Base < Document
4
+
5
+ extend ActiveModel::Naming
6
+
7
+ include CouchRest::Model::Persistence
8
+ include CouchRest::Model::Callbacks
9
+ include CouchRest::Model::DocumentQueries
10
+ include CouchRest::Model::Views
11
+ include CouchRest::Model::DesignDoc
12
+ include CouchRest::Model::ExtendedAttachments
13
+ include CouchRest::Model::ClassProxy
14
+ include CouchRest::Model::Collection
15
+ include CouchRest::Model::AttributeProtection
16
+ include CouchRest::Model::Attributes
17
+ include CouchRest::Model::Associations
18
+ include CouchRest::Model::Validations
19
+
20
+ def self.subclasses
21
+ @subclasses ||= []
22
+ end
23
+
24
+ def self.inherited(subklass)
25
+ super
26
+ subklass.send(:include, CouchRest::Model::Properties)
27
+ subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
28
+ def self.inherited(subklass)
29
+ super
30
+ subklass.properties = self.properties.dup
31
+ # This is nasty:
32
+ subklass._validators = self._validators.dup
33
+ end
34
+ EOS
35
+ subclasses << subklass
36
+ end
37
+
38
+ # Accessors
39
+ attr_accessor :casted_by
40
+
41
+
42
+ # Instantiate a new ExtendedDocument by preparing all properties
43
+ # using the provided document hash.
44
+ #
45
+ # Options supported:
46
+ #
47
+ # * :directly_set_attributes: true when data comes directly from database
48
+ #
49
+ def initialize(doc = {}, options = {})
50
+ prepare_all_attributes(doc, options)
51
+ super(doc)
52
+ unless self['_id'] && self['_rev']
53
+ self['couchrest-type'] = self.class.to_s
54
+ end
55
+ after_initialize if respond_to?(:after_initialize)
56
+ end
57
+
58
+
59
+ # Temp solution to make the view_by methods available
60
+ def self.method_missing(m, *args, &block)
61
+ if has_view?(m)
62
+ query = args.shift || {}
63
+ return view(m, query, *args, &block)
64
+ elsif m.to_s =~ /^find_(by_.+)/
65
+ view_name = $1
66
+ if has_view?(view_name)
67
+ return first_from_view(view_name, *args)
68
+ end
69
+ end
70
+ super
71
+ end
72
+
73
+ ### instance methods
74
+
75
+ # Gets a reference to the actual document in the DB
76
+ # Calls up to the next document if there is one,
77
+ # Otherwise we're at the top and we return self
78
+ def base_doc
79
+ return self if base_doc?
80
+ @casted_by.base_doc
81
+ end
82
+
83
+ # Checks if we're the top document
84
+ def base_doc?
85
+ !@casted_by
86
+ end
87
+
88
+ ## Compatibility with ActiveSupport and older frameworks
89
+
90
+ # Hack so that CouchRest::Document, which descends from Hash,
91
+ # doesn't appear to Rails routing as a Hash of options
92
+ def is_a?(klass)
93
+ return false if klass == Hash
94
+ super
95
+ end
96
+ alias :kind_of? :is_a?
97
+
98
+ def persisted?
99
+ !new?
100
+ end
101
+
102
+ def to_key
103
+ new? ? nil : [id]
104
+ end
105
+
106
+ alias :to_param :id
107
+ alias :new_record? :new?
108
+ alias :new_document? :new?
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ module CouchRest #:nodoc:
4
+ module Model #:nodoc:
5
+
6
+ module Callbacks
7
+ extend ActiveSupport::Concern
8
+ included do
9
+ extend ActiveModel::Callbacks
10
+
11
+ define_model_callbacks \
12
+ :create,
13
+ :destroy,
14
+ :save,
15
+ :update,
16
+ :validate
17
+
18
+ end
19
+
20
+ def valid?(*) #nodoc
21
+ _run_validation_callbacks { super }
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ #
2
+ # Wrapper around Array so that the casted_by attribute is set in all
3
+ # elements of the array.
4
+ #
5
+
6
+ module CouchRest::Model
7
+ class CastedArray < Array
8
+ attr_accessor :casted_by
9
+ attr_accessor :property
10
+
11
+ def initialize(array, property)
12
+ self.property = property
13
+ super(array)
14
+ end
15
+
16
+ def << obj
17
+ super(instantiate_and_cast(obj))
18
+ end
19
+
20
+ def push(obj)
21
+ super(instantiate_and_cast(obj))
22
+ end
23
+
24
+ def []= index, obj
25
+ super(index, instantiate_and_cast(obj))
26
+ end
27
+
28
+ protected
29
+
30
+ def instantiate_and_cast(obj)
31
+ if self.casted_by && self.property && obj.class != self.property.type_class
32
+ self.property.cast_value(self.casted_by, obj)
33
+ else
34
+ obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
35
+ obj
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ module CouchRest::Model
2
+ module CastedModel
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include CouchRest::Model::AttributeProtection
8
+ include CouchRest::Model::Attributes
9
+ include CouchRest::Model::Callbacks
10
+ include CouchRest::Model::Properties
11
+ include CouchRest::Model::Associations
12
+ include CouchRest::Model::Validations
13
+ attr_accessor :casted_by
14
+ end
15
+
16
+ def initialize(keys = {})
17
+ raise StandardError unless self.is_a? Hash
18
+ prepare_all_attributes(keys)
19
+ super()
20
+ end
21
+
22
+ def []= key, value
23
+ super(key.to_s, value)
24
+ end
25
+
26
+ def [] key
27
+ super(key.to_s)
28
+ end
29
+
30
+ # Gets a reference to the top level extended
31
+ # document that a model is saved inside of
32
+ def base_doc
33
+ return nil unless @casted_by
34
+ @casted_by.base_doc
35
+ end
36
+
37
+ # False if the casted model has already
38
+ # been saved in the containing document
39
+ def new?
40
+ @casted_by.nil? ? true : @casted_by.new?
41
+ end
42
+ alias :new_record? :new?
43
+
44
+ def persisted?
45
+ !new?
46
+ end
47
+
48
+ # The to_param method is needed for rails to generate resourceful routes.
49
+ # In your controller, remember that it's actually the id of the document.
50
+ def id
51
+ return nil if base_doc.nil?
52
+ base_doc.id
53
+ end
54
+ alias :to_key :id
55
+ alias :to_param :id
56
+
57
+ # Sets the attributes from a hash
58
+ def update_attributes_without_saving(hash)
59
+ hash.each do |k, v|
60
+ raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
61
+ end
62
+ hash.each do |k, v|
63
+ self.send("#{k}=",v)
64
+ end
65
+ end
66
+ alias :attributes= :update_attributes_without_saving
67
+ end
68
+ end
@@ -0,0 +1,122 @@
1
+ module CouchRest
2
+ module Model
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
+ # Base
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
+ return view(m, query, *args, &block)
51
+ elsif m.to_s =~ /^find_(by_.+)/
52
+ view_name = $1
53
+ if has_view?(view_name)
54
+ return first_from_view(view_name, *args)
55
+ end
56
+ end
57
+ super
58
+ end
59
+
60
+ # DocumentQueries
61
+
62
+ def all(opts = {}, &block)
63
+ docs = @klass.all({:database => @database}.merge(opts), &block)
64
+ docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
65
+ docs
66
+ end
67
+
68
+ def count(opts = {}, &block)
69
+ @klass.all({:database => @database, :raw => true, :limit => 0}.merge(opts), &block)['total_rows']
70
+ end
71
+
72
+ def first(opts = {})
73
+ doc = @klass.first({:database => @database}.merge(opts))
74
+ doc.database = @database if doc && doc.respond_to?(:database)
75
+ doc
76
+ end
77
+
78
+ def get(id)
79
+ doc = @klass.get(id, @database)
80
+ doc.database = @database if doc && doc.respond_to?(:database)
81
+ doc
82
+ end
83
+ alias :find :get
84
+
85
+ # Views
86
+
87
+ def has_view?(view)
88
+ @klass.has_view?(view)
89
+ end
90
+
91
+ def view(name, query={}, &block)
92
+ docs = @klass.view(name, {:database => @database}.merge(query), &block)
93
+ docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
94
+ docs
95
+ end
96
+
97
+ def first_from_view(name, *args)
98
+ # add to first hash available, or add to end
99
+ (args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
100
+ doc = @klass.first_from_view(name, *args)
101
+ doc.database = @database if doc && doc.respond_to?(:database)
102
+ doc
103
+ end
104
+
105
+ # DesignDoc
106
+
107
+ def design_doc
108
+ @klass.design_doc
109
+ end
110
+
111
+ def refresh_design_doc
112
+ @klass.refresh_design_doc(@database)
113
+ end
114
+
115
+ def save_design_doc
116
+ @klass.save_design_doc(@database)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+ end