openlogic-couchrest_model 1.0.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.
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
+