couchrest_model 1.0.0.beta7

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 (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,19 @@
1
+
2
+ module CouchRest
3
+
4
+ class Database
5
+
6
+ alias :delete_orig! :delete!
7
+ def delete!
8
+ clear_model_fresh_cache
9
+ delete_orig!
10
+ end
11
+
12
+ # If the database is deleted, ensure that the design docs will be refreshed.
13
+ def clear_model_fresh_cache
14
+ ::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,9 @@
1
+ # This file contains various hacks for Rails compatibility.
2
+ class Hash
3
+ # Hack so that CouchRest::Document, which descends from Hash,
4
+ # doesn't appear to Rails routing as a Hash of options
5
+ def self.===(other)
6
+ return false if self == Hash && other.is_a?(CouchRest::Document)
7
+ super
8
+ end
9
+ end
@@ -0,0 +1,170 @@
1
+ class Time
2
+ # returns a local time value much faster than Time.parse
3
+ def self.mktime_with_offset(string)
4
+ string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
5
+ # $1 = year
6
+ # $2 = month
7
+ # $3 = day
8
+ # $4 = hours
9
+ # $5 = minutes
10
+ # $6 = seconds
11
+ # $7 = time zone direction
12
+ # $8 = tz difference
13
+ # utc time with wrong TZ info:
14
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
15
+ tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
16
+ time + tz_difference + zone_offset(time.zone)
17
+ end
18
+ end
19
+
20
+ module CouchRest
21
+ module Model
22
+ module Typecast
23
+
24
+ def typecast_value(value, property) # klass, init_method)
25
+ return nil if value.nil?
26
+ klass = property.type_class
27
+ if value.instance_of?(klass) || klass == Object
28
+ value
29
+ elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
30
+ send('typecast_to_'+klass.to_s.downcase, value)
31
+ else
32
+ # Allow the init_method to be defined as a Proc for advanced conversion
33
+ property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # Typecast a value to an Integer
40
+ def typecast_to_integer(value)
41
+ typecast_to_numeric(value, :to_i)
42
+ end
43
+
44
+ # Typecast a value to a String
45
+ def typecast_to_string(value)
46
+ value.to_s
47
+ end
48
+
49
+ # Typecast a value to a true or false
50
+ def typecast_to_trueclass(value)
51
+ if value.kind_of?(Integer)
52
+ return true if value == 1
53
+ return false if value == 0
54
+ elsif value.respond_to?(:to_s)
55
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
56
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
57
+ end
58
+ value
59
+ end
60
+
61
+ # Typecast a value to a BigDecimal
62
+ def typecast_to_bigdecimal(value)
63
+ if value.kind_of?(Integer)
64
+ value.to_s.to_d
65
+ else
66
+ typecast_to_numeric(value, :to_d)
67
+ end
68
+ end
69
+
70
+ # Typecast a value to a Float
71
+ def typecast_to_float(value)
72
+ typecast_to_numeric(value, :to_f)
73
+ end
74
+
75
+ # Match numeric string
76
+ def typecast_to_numeric(value, method)
77
+ if value.respond_to?(:to_str)
78
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
79
+ $1.send(method)
80
+ else
81
+ value
82
+ end
83
+ elsif value.respond_to?(method)
84
+ value.send(method)
85
+ else
86
+ value
87
+ end
88
+ end
89
+
90
+ # Typecasts an arbitrary value to a DateTime.
91
+ # Handles both Hashes and DateTime instances.
92
+ # This is slow!! Use Time instead.
93
+ def typecast_to_datetime(value)
94
+ if value.is_a?(Hash)
95
+ typecast_hash_to_datetime(value)
96
+ else
97
+ DateTime.parse(value.to_s)
98
+ end
99
+ rescue ArgumentError
100
+ value
101
+ end
102
+
103
+ # Typecasts an arbitrary value to a Date
104
+ # Handles both Hashes and Date instances.
105
+ def typecast_to_date(value)
106
+ if value.is_a?(Hash)
107
+ typecast_hash_to_date(value)
108
+ elsif value.is_a?(Time) # sometimes people think date is time!
109
+ value.to_date
110
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
111
+ # Faster than parsing the date
112
+ Date.new($1.to_i, $2.to_i, $3.to_i)
113
+ else
114
+ Date.parse(value)
115
+ end
116
+ rescue ArgumentError
117
+ value
118
+ end
119
+
120
+ # Typecasts an arbitrary value to a Time
121
+ # Handles both Hashes and Time instances.
122
+ def typecast_to_time(value)
123
+ if value.is_a?(Hash)
124
+ typecast_hash_to_time(value)
125
+ else
126
+ Time.mktime_with_offset(value.to_s)
127
+ end
128
+ rescue ArgumentError
129
+ value
130
+ rescue TypeError
131
+ value
132
+ end
133
+
134
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
135
+ # :hour, :min, :sec
136
+ def typecast_hash_to_datetime(value)
137
+ DateTime.new(*extract_time(value))
138
+ end
139
+
140
+ # Creates a Date instance from a Hash with keys :year, :month, :day
141
+ def typecast_hash_to_date(value)
142
+ Date.new(*extract_time(value)[0, 3])
143
+ end
144
+
145
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
146
+ # :hour, :min, :sec
147
+ def typecast_hash_to_time(value)
148
+ Time.local(*extract_time(value))
149
+ end
150
+
151
+ # Extracts the given args from the hash. If a value does not exist, it
152
+ # uses the value of Time.now.
153
+ def extract_time(value)
154
+ now = Time.now
155
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
156
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
157
+ end
158
+ end
159
+
160
+ # Typecast a value to a Class
161
+ def typecast_to_class(value)
162
+ value.to_s.constantize
163
+ rescue NameError
164
+ value
165
+ end
166
+
167
+ end
168
+ end
169
+ end
170
+
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ require "couchrest/model/validations/casted_model"
4
+ require "couchrest/model/validations/uniqueness"
5
+
6
+ I18n.load_path << File.join(
7
+ File.dirname(__FILE__), "validations", "locale", "en.yml"
8
+ )
9
+
10
+ module CouchRest
11
+ module Model
12
+
13
+ # Validations may be applied to both Model::Base and Model::CastedModel
14
+ module Validations
15
+ extend ActiveSupport::Concern
16
+ included do
17
+ include ActiveModel::Validations
18
+ end
19
+
20
+
21
+ module ClassMethods
22
+
23
+ # Validates the associated casted model. This method should not be
24
+ # used within your code as it is automatically included when a CastedModel
25
+ # is used inside the model.
26
+ #
27
+ def validates_casted_model(*args)
28
+ validates_with(CastedModelValidator, _merge_attributes(args))
29
+ end
30
+
31
+ # Validates if the field is unique for this type of document. Automatically creates
32
+ # a view if one does not already exist and performs a search for all matching
33
+ # documents.
34
+ #
35
+ # Example:
36
+ #
37
+ # class Person < CouchRest::Model::Base
38
+ # property :title, String
39
+ #
40
+ # validates_uniqueness_of :title
41
+ # end
42
+ #
43
+ # Asside from the standard options, you can specify the name of the view you'd like
44
+ # to use for the search inside the +:view+ option. The following example would search
45
+ # for the code in side the +all+ view, useful for when +unique_id+ is used and you'd
46
+ # like to check before receiving a RestClient Conflict error:
47
+ #
48
+ # validates_uniqueness_of :code, :view => 'all'
49
+ #
50
+ # A +:proxy+ parameter is also accepted if you would
51
+ # like to call a method on the document on which the view should be performed.
52
+ #
53
+ # For Example:
54
+ #
55
+ # # Same as not including proxy:
56
+ # validates_uniqueness_of :title, :proxy => 'class'
57
+ #
58
+ # # Person#company.people provides a proxy object for people
59
+ # validates_uniqueness_of :title, :proxy => 'company.people'
60
+ #
61
+ def validates_uniqueness_of(*args)
62
+ validates_with(UniquenessValidator, _merge_attributes(args))
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,14 @@
1
+ module CouchRest
2
+ module Model
3
+ module Validations
4
+ class CastedModelValidator < ActiveModel::EachValidator
5
+
6
+ def validate_each(document, attribute, value)
7
+ values = value.is_a?(Array) ? value : [value]
8
+ return if values.collect {|doc| doc.nil? || doc.valid? }.all?
9
+ document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: "is already taken"
5
+
@@ -0,0 +1,45 @@
1
+ # encoding: urf-8
2
+
3
+ module CouchRest
4
+ module Model
5
+ module Validations
6
+
7
+ # Validates if a field is unique
8
+ class UniquenessValidator < ActiveModel::EachValidator
9
+
10
+ # Ensure we have a class available so we can check for a usable view
11
+ # or add one if necessary.
12
+ def setup(klass)
13
+ @klass = klass
14
+ end
15
+
16
+
17
+ def validate_each(document, attribute, value)
18
+ view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
19
+
20
+ unless @klass.has_view?(view_name)
21
+ raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
22
+ @klass.view_by attribute
23
+ end
24
+
25
+ # Determine the base of the search
26
+ base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
27
+
28
+ docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
29
+ return if docs.empty?
30
+
31
+ unless document.new?
32
+ return if docs.find{|doc| doc['id'] == document.id}
33
+ end
34
+
35
+ if docs.length > 0
36
+ document.errors.add(attribute, :taken, :default => options[:message], :value => value)
37
+ end
38
+ end
39
+
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,167 @@
1
+ module CouchRest
2
+ module Model
3
+ module Views
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Define a CouchDB view. The name of the view will be the concatenation
8
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
9
+ #
10
+ # ==== Example views:
11
+ #
12
+ # class Post
13
+ # # view with default options
14
+ # # query with Post.by_date
15
+ # view_by :date, :descending => true
16
+ #
17
+ # # view with compound sort-keys
18
+ # # query with Post.by_user_id_and_date
19
+ # view_by :user_id, :date
20
+ #
21
+ # # view with custom map/reduce functions
22
+ # # query with Post.by_tags :reduce => true
23
+ # view_by :tags,
24
+ # :map =>
25
+ # "function(doc) {
26
+ # if (doc['couchrest-type'] == 'Post' && doc.tags) {
27
+ # doc.tags.forEach(function(tag){
28
+ # emit(doc.tag, 1);
29
+ # });
30
+ # }
31
+ # }",
32
+ # :reduce =>
33
+ # "function(keys, values, rereduce) {
34
+ # return sum(values);
35
+ # }"
36
+ # end
37
+ #
38
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
39
+ # function:
40
+ #
41
+ # function(doc) {
42
+ # if (doc['couchrest-type'] == 'Post' && doc.date) {
43
+ # emit(doc.date, null);
44
+ # }
45
+ # }
46
+ #
47
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
48
+ # valid options for CouchRest::Database#view. In addition, calling with
49
+ # the <tt>:raw => true</tt> option will return the view rows
50
+ # themselves. By default <tt>Post.by_date</tt> will return the
51
+ # documents included in the generated view.
52
+ #
53
+ # Calling with :database => [instance of CouchRest::Database] will
54
+ # send the query to a specific database, otherwise it will go to
55
+ # the model's default database (use_database)
56
+ #
57
+ # CouchRest::Database#view options can be applied at view definition
58
+ # time as defaults, and they will be curried and used at view query
59
+ # time. Or they can be overridden at query time.
60
+ #
61
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
62
+ # reduce results. The default for custom views is to query with
63
+ # <tt>:reduce => false</tt>.
64
+ #
65
+ # Views are generated (on a per-model basis) lazily on first-access.
66
+ # This means that if you are deploying changes to a view, the views for
67
+ # that model won't be available until generation is complete. This can
68
+ # take some time with large databases. Strategies are in the works.
69
+ #
70
+ # To understand the capabilities of this view system more completely,
71
+ # it is recommended that you read the RSpec file at
72
+ # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
73
+
74
+ def view_by(*keys)
75
+ opts = keys.pop if keys.last.is_a?(Hash)
76
+ opts ||= {}
77
+ ducktype = opts.delete(:ducktype)
78
+ unless ducktype || opts[:map]
79
+ opts[:guards] ||= []
80
+ opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
81
+ end
82
+ keys.push opts
83
+ design_doc.view_by(*keys)
84
+ req_design_doc_refresh
85
+ end
86
+
87
+ # returns stored defaults if there is a view named this in the design doc
88
+ def has_view?(view)
89
+ view = view.to_s
90
+ design_doc && design_doc['views'] && design_doc['views'][view]
91
+ end
92
+
93
+ # Dispatches to any named view.
94
+ def view(name, query={}, &block)
95
+ query = query.dup # Modifications made on copy!
96
+ db = query.delete(:database) || database
97
+ refresh_design_doc(db)
98
+ query[:raw] = true if query[:reduce]
99
+ raw = query.delete(:raw)
100
+ fetch_view_with_docs(db, name, query, raw, &block)
101
+ end
102
+
103
+ # Find the first entry in the view. If the second parameter is a string
104
+ # it will be used as the key for the request, for example:
105
+ #
106
+ # Course.first_from_view('by_teacher', 'Fred')
107
+ #
108
+ # More advanced requests can be performed by providing a hash:
109
+ #
110
+ # Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
111
+ #
112
+ def first_from_view(name, *args)
113
+ query = {:limit => 1}
114
+ case args.first
115
+ when String, Array
116
+ query.update(args[1]) unless args[1].nil?
117
+ query[:key] = args.first
118
+ when Hash
119
+ query.update(args.first)
120
+ end
121
+ view(name, query).first
122
+ end
123
+
124
+ private
125
+
126
+ def fetch_view_with_docs(db, name, opts, raw=false, &block)
127
+ if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
128
+ fetch_view(db, name, opts, &block)
129
+ else
130
+ begin
131
+ if block.nil?
132
+ collection_proxy_for(design_doc, name, opts.merge({:include_docs => true}))
133
+ else
134
+ view = fetch_view db, name, opts.merge({:include_docs => true}), &block
135
+ view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
136
+ end
137
+ rescue
138
+ # fallback for old versions of couchdb that don't
139
+ # have include_docs support
140
+ view = fetch_view(db, name, opts, &block)
141
+ view['rows'].collect{|r|create_from_database(db.get(r['id']))} if view['rows']
142
+ end
143
+ end
144
+ end
145
+
146
+ def fetch_view(db, view_name, opts, &block)
147
+ raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
148
+ retryable = true
149
+ begin
150
+ design_doc.view_on(db, view_name, opts, &block)
151
+ # the design doc may not have been saved yet on this database
152
+ rescue RestClient::ResourceNotFound => e
153
+ if retryable
154
+ save_design_doc(db)
155
+ retryable = false
156
+ retry
157
+ else
158
+ raise e
159
+ end
160
+ end
161
+ end
162
+
163
+ end # module ClassMethods
164
+
165
+ end
166
+ end
167
+ end