openlogic-couchrest_model 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,273 @@
1
+ module CouchRest
2
+ module Model
3
+ # Warning! The Collection module is seriously depricated.
4
+ # Use the new Design Views instead, as this code copies many other parts
5
+ # of CouchRest Model.
6
+ #
7
+ # Expect this to be removed soon.
8
+ #
9
+ module Collection
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ # Creates a new class method, find_all_<collection_name>, that will
18
+ # execute the view specified with the design_doc and view_name
19
+ # parameters, along with the specified view_options. This method will
20
+ # return the results of the view as an Array of objects which are
21
+ # instances of the class.
22
+ #
23
+ # This method is handy for objects that do not use the view_by method
24
+ # to declare their views.
25
+ def provides_collection(collection_name, design_doc, view_name, view_options)
26
+ class_eval <<-END, __FILE__, __LINE__ + 1
27
+ def self.find_all_#{collection_name}(options = {})
28
+ view_options = #{view_options.inspect} || {}
29
+ CollectionProxy.new(options[:database] || database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
30
+ end
31
+ END
32
+ end
33
+
34
+ # Fetch a group of objects from CouchDB. Options can include:
35
+ # :page - Specifies the page to load (starting at 1)
36
+ # :per_page - Specifies the number of objects to load per page
37
+ #
38
+ # Defaults are used if these options are not specified.
39
+ def paginate(options)
40
+ proxy = create_collection_proxy(options)
41
+ proxy.paginate(options)
42
+ end
43
+
44
+ # Iterate over the objects in a collection, fetching them from CouchDB
45
+ # in groups. Options can include:
46
+ # :page - Specifies the page to load
47
+ # :per_page - Specifies the number of objects to load per page
48
+ #
49
+ # Defaults are used if these options are not specified.
50
+ def paginated_each(options, &block)
51
+ search = options.delete(:search)
52
+ unless search == true
53
+ proxy = create_collection_proxy(options)
54
+ else
55
+ proxy = create_search_collection_proxy(options)
56
+ end
57
+ proxy.paginated_each(options, &block)
58
+ end
59
+
60
+ # Create a CollectionProxy for the specified view and options.
61
+ # CollectionProxy behaves just like an Array, but offers support for
62
+ # pagination.
63
+ def collection_proxy_for(design_doc, view_name, view_options = {})
64
+ options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
65
+ create_collection_proxy(options)
66
+ end
67
+
68
+ private
69
+
70
+ def create_collection_proxy(options)
71
+ design_doc, view_name, view_options = parse_view_options(options)
72
+ CollectionProxy.new(options[:database] || database, design_doc, view_name, view_options, self)
73
+ end
74
+
75
+ def create_search_collection_proxy(options)
76
+ design_doc, search_name, search_options = parse_search_options(options)
77
+ CollectionProxy.new(options[:database] || database, design_doc, search_name, search_options, self, :search)
78
+ end
79
+
80
+ def parse_view_options(options)
81
+ design_doc = options.delete(:design_doc)
82
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
83
+
84
+ view_name = options.delete(:view_name)
85
+ raise ArgumentError, 'view_name is required' if view_name.nil?
86
+
87
+ default_view_options = (design_doc.class == Design &&
88
+ design_doc['views'][view_name.to_s] &&
89
+ design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
90
+ view_options = default_view_options.merge(options)
91
+ view_options.delete(:database)
92
+
93
+ [design_doc, view_name, view_options]
94
+ end
95
+
96
+ def parse_search_options(options)
97
+ design_doc = options.delete(:design_doc)
98
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
99
+
100
+ search_name = options.delete(:view_name)
101
+ raise ArgumentError, 'search_name is required' if search_name.nil?
102
+
103
+ search_options = options.clone
104
+ search_options.delete(:database)
105
+
106
+ [design_doc, search_name, search_options]
107
+ end
108
+
109
+ end
110
+
111
+ class CollectionProxy
112
+ alias_method :proxy_respond_to?, :respond_to?
113
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
114
+
115
+ DEFAULT_PAGE = 1
116
+ DEFAULT_PER_PAGE = 30
117
+
118
+ # Create a new CollectionProxy to represent the specified view. If a
119
+ # container class is specified, the proxy will create an object of the
120
+ # given type for each row that comes back from the view. If no
121
+ # container class is specified, the raw results are returned.
122
+ #
123
+ # The CollectionProxy provides support for paginating over a collection
124
+ # via the paginate, and paginated_each methods.
125
+ def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
126
+ raise ArgumentError, "database is a required parameter" if database.nil?
127
+
128
+ @database = database
129
+ @container_class = container_class
130
+ @query_type = query_type
131
+
132
+ strip_pagination_options(view_options)
133
+ @view_options = view_options
134
+
135
+ if design_doc.class == Design
136
+ @view_name = "#{design_doc.name}/#{view_name}"
137
+ else
138
+ @view_name = "#{design_doc}/#{view_name}"
139
+ end
140
+
141
+ # Save the design doc, ready for use
142
+ @container_class.save_design_doc(@database)
143
+ end
144
+
145
+ # See Collection.paginate
146
+ def paginate(options = {})
147
+ page, per_page = parse_options(options)
148
+ results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
149
+ remember_where_we_left_off(results, page)
150
+ instances = convert_to_container_array(results)
151
+
152
+ begin
153
+ if Kernel.const_get('WillPaginate')
154
+ total_rows = results['total_rows'].to_i
155
+ paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager|
156
+ pager.replace(instances)
157
+ end
158
+ return paginated
159
+ end
160
+ rescue NameError
161
+ # When not using will_paginate, not much we could do about this. :x
162
+ end
163
+ return instances
164
+ end
165
+
166
+ # See Collection.paginated_each
167
+ def paginated_each(options = {}, &block)
168
+ page, per_page = parse_options(options)
169
+
170
+ begin
171
+ collection = paginate({:page => page, :per_page => per_page})
172
+ collection.each(&block)
173
+ page += 1
174
+ end until collection.size < per_page
175
+ end
176
+
177
+ def respond_to?(*args)
178
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
179
+ end
180
+
181
+ # Explicitly proxy === because the instance method removal above
182
+ # doesn't catch it.
183
+ def ===(other)
184
+ load_target
185
+ other === @target
186
+ end
187
+
188
+ private
189
+
190
+ def method_missing(method, *args)
191
+ if load_target
192
+ if block_given?
193
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
194
+ else
195
+ @target.send(method, *args)
196
+ end
197
+ end
198
+ end
199
+
200
+ def load_target
201
+ unless loaded?
202
+ @view_options.merge!({:include_docs => true}) if @query_type == :search
203
+ results = @database.send(@query_type, @view_name, @view_options)
204
+ @target = convert_to_container_array(results)
205
+ end
206
+ @loaded = true
207
+ @target
208
+ end
209
+
210
+ def loaded?
211
+ @loaded
212
+ end
213
+
214
+ def reload
215
+ reset
216
+ load_target
217
+ self unless @target.nil?
218
+ end
219
+
220
+ def reset
221
+ @loaded = false
222
+ @target = nil
223
+ end
224
+
225
+ def inspect
226
+ load_target
227
+ @target.inspect
228
+ end
229
+
230
+ def convert_to_container_array(results)
231
+ if @container_class.nil?
232
+ results
233
+ else
234
+ results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
235
+ end
236
+ end
237
+
238
+ def pagination_options(page, per_page)
239
+ view_options = @view_options.clone
240
+ if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
241
+ key = view_options.delete(:key)
242
+ end_key = view_options[:endkey] || key
243
+ options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
244
+ else
245
+ options = { :limit => per_page, :skip => per_page * (page - 1) }
246
+ end
247
+ options[:include_docs] = true
248
+ view_options.merge(options)
249
+ end
250
+
251
+ def parse_options(options)
252
+ page = options.delete(:page) || DEFAULT_PAGE
253
+ per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
254
+ [page.to_i, per_page.to_i]
255
+ end
256
+
257
+ def strip_pagination_options(options)
258
+ parse_options(options)
259
+ end
260
+
261
+ def remember_where_we_left_off(results, page)
262
+ last_row = results['rows'].last
263
+ if last_row
264
+ @last_key = last_row['key']
265
+ @last_docid = last_row['id']
266
+ end
267
+ @last_page = page
268
+ end
269
+ end
270
+
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,67 @@
1
+ module CouchRest
2
+
3
+ # CouchRest Model Configuration support, stolen from Carrierwave by jnicklas
4
+ # http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb
5
+
6
+ module Model
7
+ module Configuration
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ add_config :model_type_key
12
+ add_config :mass_assign_any_attribute
13
+ add_config :auto_update_design_doc
14
+ add_config :environment
15
+ add_config :connection
16
+ add_config :connection_config_file
17
+
18
+ configure do |config|
19
+ config.model_type_key = 'type' # was 'couchrest-type'
20
+ config.mass_assign_any_attribute = false
21
+ config.auto_update_design_doc = true
22
+
23
+ config.environment = :development
24
+ config.connection_config_file = File.join(Dir.pwd, 'config', 'couchdb.yml')
25
+ config.connection = {
26
+ :protocol => 'http',
27
+ :host => 'localhost',
28
+ :port => '5984',
29
+ :prefix => 'couchrest',
30
+ :suffix => nil,
31
+ :join => '_',
32
+ :username => nil,
33
+ :password => nil
34
+ }
35
+ end
36
+ end
37
+
38
+ module ClassMethods
39
+
40
+ def add_config(name)
41
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
42
+ def self.#{name}(value=nil)
43
+ @#{name} = value if value
44
+ return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name})
45
+ name = superclass.#{name}
46
+ return nil if name.nil? && !instance_variable_defined?("@#{name}")
47
+ @#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name
48
+ end
49
+
50
+ def self.#{name}=(value)
51
+ @#{name} = value
52
+ end
53
+
54
+ def #{name}
55
+ self.class.#{name}
56
+ end
57
+ RUBY
58
+ end
59
+
60
+ def configure
61
+ yield self
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,70 @@
1
+ module CouchRest
2
+ module Model
3
+ module Connection
4
+ extend ActiveSupport::Concern
5
+
6
+ def server
7
+ self.class.server
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Overwrite the normal use_database method so that a database
13
+ # name can be provided instead of a full connection.
14
+ def use_database(db)
15
+ @database = prepare_database(db)
16
+ end
17
+
18
+ # Overwrite the default database method so that it always
19
+ # provides something from the configuration
20
+ def database
21
+ super || (@database ||= prepare_database)
22
+ end
23
+
24
+ def server
25
+ @server ||= CouchRest::Server.new(prepare_server_uri)
26
+ end
27
+
28
+ def prepare_database(db = nil)
29
+ unless db.is_a?(CouchRest::Database)
30
+ conf = connection_configuration
31
+ db = [conf[:prefix], db.to_s, conf[:suffix]].reject{|s| s.to_s.empty?}.join(conf[:join])
32
+ self.server.database!(db)
33
+ else
34
+ db
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def prepare_server_uri
41
+ conf = connection_configuration
42
+ userinfo = [conf[:username], conf[:password]].compact.join(':')
43
+ userinfo += '@' unless userinfo.empty?
44
+ "#{conf[:protocol]}://#{userinfo}#{conf[:host]}:#{conf[:port]}"
45
+ end
46
+
47
+ def connection_configuration
48
+ @connection_configuration ||=
49
+ self.connection.update(
50
+ (load_connection_config_file[environment.to_sym] || {}).symbolize_keys
51
+ )
52
+ end
53
+
54
+ def load_connection_config_file
55
+ file = connection_config_file
56
+ connection_config_cache[file] ||=
57
+ (File.exists?(file) ?
58
+ YAML::load(ERB.new(IO.read(file)).result) :
59
+ { }).symbolize_keys
60
+ end
61
+
62
+ def connection_config_cache
63
+ Thread.current[:connection_config_cache] ||= {}
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
70
+ 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,66 @@
1
+ module CouchRest
2
+ module Model
3
+ module CoreExtensions
4
+ module TimeParsing
5
+
6
+ if RUBY_VERSION < "1.9.0"
7
+ # Overrwrite Ruby's standard new method to provide compatible support
8
+ # of 1.9.2's Time.new method.
9
+ #
10
+ # Only supports syntax like:
11
+ #
12
+ # Time.new(2011, 4, 1, 18, 50, 32, "+02:00")
13
+ # # or
14
+ # Time.new(2011, 4, 1, 18, 50, 32)
15
+ #
16
+ def new(*args)
17
+ return super() if (args.empty?)
18
+ zone = args.delete_at(6)
19
+ time = mktime(*args)
20
+ if zone =~ /([\+|\-]?)(\d{2}):?(\d{2})/
21
+ tz_difference = ("#{$1 == '-' ? '+' : '-'}#{$2}".to_i * 3600) + ($3.to_i * 60)
22
+ time + tz_difference + zone_offset(time.zone)
23
+ else
24
+ time
25
+ end
26
+ end
27
+ end
28
+
29
+ # Attemtps to parse a time string in ISO8601 format.
30
+ # If no match is found, the standard time parse will be used.
31
+ #
32
+ # Times, unless provided with a time zone, are assumed to be in
33
+ # UTC.
34
+ #
35
+ def parse_iso8601(string)
36
+ if (string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(Z| ?([\+|\s|\-])?(\d{2}):?(\d{2}))?/)
37
+ # $1 = year
38
+ # $2 = month
39
+ # $3 = day
40
+ # $4 = hours
41
+ # $5 = minutes
42
+ # $6 = seconds
43
+ # $7 = UTC or Timezone
44
+ # $8 = time zone direction
45
+ # $9 = tz difference hours
46
+ # $10 = tz difference minutes
47
+
48
+ if (!$7.to_s.empty? && $7 != 'Z')
49
+ new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, "#{$8 == '-' ? '-' : '+'}#{$9}:#{$10}")
50
+ else
51
+ utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i)
52
+ end
53
+ else
54
+ parse(string)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ Time.class_eval do
64
+ extend CouchRest::Model::CoreExtensions::TimeParsing
65
+ end
66
+