simply_couch 0.1.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +182 -0
  3. data/LICENSE.txt +15 -0
  4. data/README.md +294 -0
  5. data/lib/core_ext/date.rb +15 -0
  6. data/lib/core_ext/time.rb +23 -0
  7. data/lib/simply_couch/class_methods_base.rb +72 -0
  8. data/lib/simply_couch/has_attachment.rb +225 -0
  9. data/lib/simply_couch/include_relation.rb +160 -0
  10. data/lib/simply_couch/instance_methods.rb +356 -0
  11. data/lib/simply_couch/locale/en.yml +5 -0
  12. data/lib/simply_couch/model/ancestry.rb +307 -0
  13. data/lib/simply_couch/model/association_property.rb +26 -0
  14. data/lib/simply_couch/model/attachments.rb +90 -0
  15. data/lib/simply_couch/model/belongs_to.rb +140 -0
  16. data/lib/simply_couch/model/database.rb +209 -0
  17. data/lib/simply_couch/model/embedded_in.rb +196 -0
  18. data/lib/simply_couch/model/find_by.rb +202 -0
  19. data/lib/simply_couch/model/finders.rb +77 -0
  20. data/lib/simply_couch/model/has_and_belongs_to_many.rb +223 -0
  21. data/lib/simply_couch/model/has_many.rb +177 -0
  22. data/lib/simply_couch/model/has_many_embedded.rb +187 -0
  23. data/lib/simply_couch/model/has_one.rb +75 -0
  24. data/lib/simply_couch/model/pagination.rb +25 -0
  25. data/lib/simply_couch/model/pagination_options.rb +55 -0
  26. data/lib/simply_couch/model/persistence.rb +411 -0
  27. data/lib/simply_couch/model/properties.rb +11 -0
  28. data/lib/simply_couch/model/validations.rb +28 -0
  29. data/lib/simply_couch/model/view/base_view_spec.rb +115 -0
  30. data/lib/simply_couch/model/view/custom_view_spec.rb +49 -0
  31. data/lib/simply_couch/model/view/custom_views.rb +50 -0
  32. data/lib/simply_couch/model/view/lists.rb +25 -0
  33. data/lib/simply_couch/model/view/model_view_spec.rb +106 -0
  34. data/lib/simply_couch/model/view/properties_view_spec.rb +53 -0
  35. data/lib/simply_couch/model/view/raw_view_spec.rb +30 -0
  36. data/lib/simply_couch/model/view/view_query.rb +98 -0
  37. data/lib/simply_couch/model/view.rb +8 -0
  38. data/lib/simply_couch/model/views/array_property_view_spec.rb +26 -0
  39. data/lib/simply_couch/model/views/deleted_model_view_spec.rb +43 -0
  40. data/lib/simply_couch/model/views.rb +2 -0
  41. data/lib/simply_couch/model.rb +195 -0
  42. data/lib/simply_couch/rake.rb +23 -0
  43. data/lib/simply_couch/storage.rb +147 -0
  44. data/lib/simply_couch.rb +26 -0
  45. metadata +144 -0
@@ -0,0 +1,411 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Persistence — replaces CouchPotato::Persistence.
4
+ # Provides property macro, callbacks, JSON, dirty tracking, timestamps, validations.
5
+ #
6
+ module SimplyCouch
7
+ module Model
8
+ module Persistence
9
+ require 'active_support/time'
10
+
11
+ def self.included(base)
12
+ base.instance_variable_set(:@properties, nil) if base.instance_variable_defined?(:@properties)
13
+ base.send :include, Properties
14
+ base.send :include, Callbacks
15
+ base.send :include, Json
16
+ base.send :include, DirtyAttributes
17
+ base.send :include, MagicTimestamps
18
+ base.send :include, ActiveModelCompliance
19
+ base.send :include, ForbiddenAttributesProtection
20
+ base.send :include, Revisions
21
+ base.send :include, Validation
22
+ base.send :include, View::CustomViews
23
+ base.send :include, View::Lists
24
+
25
+ base.class_eval do
26
+ attr_accessor :_id, :_rev, :_deleted, :_attachments, :database
27
+ alias_method :id, :_id
28
+ alias_method :id=, :_id=
29
+ end
30
+ end
31
+
32
+ # ── initialize / attributes ────────────────────────────────────────
33
+
34
+ def initialize(attributes = {})
35
+ if attributes
36
+ @skip_dirty_tracking = true
37
+ self.attributes = attributes
38
+ @skip_dirty_tracking = false
39
+ end
40
+ yield self if block_given?
41
+ end
42
+
43
+ def attributes=(hash)
44
+ hash.each { |attribute, value| self.public_send "#{attribute}=", value }
45
+ end
46
+
47
+ def attributes
48
+ self.class.properties.inject(ActiveSupport::HashWithIndifferentAccess.new) do |res, property|
49
+ property.value(res, self)
50
+ res
51
+ end
52
+ end
53
+
54
+ def []=(attribute, value); public_send("#{attribute}=", value); end
55
+ def [](attribute); public_send(attribute); end
56
+ def has_key?(key); attributes.has_key?(key); end
57
+ def new?; _rev.nil?; end
58
+ alias_method :new_record?, :new?
59
+ def to_param; _id; end
60
+
61
+ def ==(other)
62
+ super || (self.class == other.class && self._id.present? && self._id == other._id)
63
+ end
64
+ def eql?(other); self == other; end
65
+ def hash; _id.hash * (_id.hash.to_s.size ** 10) + _rev.hash; end
66
+ def reload; database.load id; end
67
+
68
+ def inspect
69
+ attrs = attributes.map {|k,v| "#{k}: #{v.inspect}"}.join(", ")
70
+ %Q{#<#{self.class} _id: "#{_id}", _rev: "#{_rev}", #{attrs}>}
71
+ end
72
+
73
+ # ── Properties ─────────────────────────────────────────────────────
74
+
75
+ module Properties
76
+ class PropertyList
77
+ include Enumerable
78
+ attr_accessor :list
79
+
80
+ def initialize(clazz)
81
+ @clazz = clazz
82
+ @list = []
83
+ @hash = {}
84
+ end
85
+
86
+ def each(&block); (list + inherited_properties).each(&block); end
87
+ def <<(property); @hash[property.name] = property; @list << property; end
88
+ def find_property(name); @hash[name] || @clazz.superclass.properties.find_property(name); end
89
+ def inspect; list.map(&:name).inspect; end
90
+
91
+ def inherited_properties
92
+ superclazz = @clazz.superclass
93
+ properties = []
94
+ while superclazz && superclazz.respond_to?(:properties)
95
+ properties << superclazz.properties.list
96
+ superclazz = superclazz.superclass
97
+ end
98
+ properties.flatten
99
+ end
100
+ end
101
+
102
+ def self.included(base)
103
+ base.extend ClassMethods
104
+ base.class_eval do
105
+ def self.properties
106
+ @properties ||= {}
107
+ @properties[name] ||= PropertyList.new(self)
108
+ @properties[name]
109
+ end
110
+ end
111
+ end
112
+
113
+ def type_caster; @type_caster ||= TypeCaster.new; end
114
+
115
+ module ClassMethods
116
+ def property_names; properties.map(&:name); end
117
+
118
+ def property(name, options = {})
119
+ am = send(:generated_attribute_methods)
120
+ am.module_eval { undef_method(name) if instance_methods.include?(name) }
121
+ define_attribute_method name
122
+ properties << SimpleProperty.new(self, name, options)
123
+ am.send(:remove_method, name) if am.instance_methods.include?(name)
124
+ end
125
+
126
+ def check_existing_properties(name, type)
127
+ existing = properties.find{|p| name.to_sym == p.name.to_sym}
128
+ return if existing.nil? || existing.class == type
129
+ raise "Property #{name} already defined as #{existing.class}, cannot redefine as #{type}"
130
+ end
131
+ end
132
+ end
133
+
134
+ # ── SimpleProperty ──────────────────────────────────────────────────
135
+
136
+ module PropertyMethods
137
+ private
138
+ def load_attribute_from_document(name)
139
+ if _document.has_key?(name)
140
+ property = self.class.properties.find_property(name)
141
+ @skip_dirty_tracking = true
142
+ value = property.build(self, _document)
143
+ @skip_dirty_tracking = false
144
+ value
145
+ end
146
+ end
147
+ end
148
+
149
+ class SimpleProperty
150
+ attr_accessor :name, :type
151
+
152
+ def initialize(owner_clazz, name, options = {})
153
+ self.name = name
154
+ @setter_name = "#{name}="
155
+ self.type = options[:type]
156
+ @type_caster = TypeCaster.new
157
+ owner_clazz.send(:include, PropertyMethods) unless owner_clazz.ancestors.include?(PropertyMethods)
158
+ define_accessors(accessors_module_for(owner_clazz), name, options)
159
+ end
160
+
161
+ def build(object, json); object.public_send @setter_name, json[name]; end
162
+ def changed?(object); object.public_send("#{name}_changed?"); end
163
+ def serialize(json, object); json[name] = @type_caster.cast_back object.public_send(name); end
164
+ alias :value :serialize
165
+
166
+ private
167
+
168
+ def module_for(clazz, name)
169
+ suffix = "#{clazz.name.to_s.gsub('::', '__')}#{name}"
170
+ unless clazz.const_defined?(suffix)
171
+ clazz.const_set(suffix, Module.new).tap {|m| clazz.send(:include, m) }
172
+ end
173
+ clazz.const_get(suffix)
174
+ end
175
+
176
+ def accessors_module_for(clazz); module_for(clazz, "AccessorMethods"); end
177
+
178
+ def define_accessors(base, name, options)
179
+ ivar = "@#{name}".freeze
180
+ base.class_eval do
181
+ define_method(name) do
182
+ load_attribute_from_document(name) unless instance_variable_defined?(ivar)
183
+ value = instance_variable_get(ivar)
184
+ if value.nil? && !options[:default].nil?
185
+ default = if options[:default].respond_to?(:call)
186
+ options[:default].arity == 1 ? options[:default].call(self) : options[:default].call
187
+ else
188
+ clone_attribute(options[:default])
189
+ end
190
+ instance_variable_set(ivar, default)
191
+ else
192
+ value
193
+ end
194
+ end
195
+
196
+ define_method("#{name}=") do |value|
197
+ typecasted = type_caster.cast(value, options[:type])
198
+ public_send("#{name}_will_change!") unless @skip_dirty_tracking || typecasted == public_send(name)
199
+ instance_variable_set(ivar, typecasted)
200
+ end
201
+
202
+ define_method("#{name}?") { !send(name).nil? && !send(name).try(:blank?) }
203
+ end
204
+ end
205
+ end
206
+
207
+ # ── Callbacks ────────────────────────────────────────────────────────
208
+
209
+ module Callbacks
210
+ extend ActiveSupport::Concern
211
+ include ActiveSupport::Callbacks
212
+
213
+ included do
214
+ define_callbacks :validate, :validation,
215
+ :validation_on_save, :validation_on_create, :validation_on_update,
216
+ :save, :create, :update, :destroy
217
+ %w(validate validation save create update destroy).each do |cb|
218
+ class_eval <<-RUBY, __FILE__, __LINE__
219
+ def self.before_#{cb}(*args, &block); set_callback :#{cb}, :before, *args, &block; end
220
+ def self.after_#{cb}(*args, &block); set_callback :#{cb}, :after, *args, &block; end
221
+ def self.around_#{cb}(*args, &block); set_callback :#{cb}, :around, *args, &block; end
222
+ RUBY
223
+ end
224
+ %w(validation_on_create validation_on_update).each do |cb|
225
+ class_eval <<-RUBY, __FILE__, __LINE__
226
+ def self.before_#{cb}(*args, &block); set_callback :#{cb}, :before, *args, &block; end
227
+ def self.after_#{cb}(*args, &block); set_callback :#{cb}, :after, *args, &block; end
228
+ RUBY
229
+ end
230
+ end
231
+ end
232
+
233
+ # ── Dirty Attributes ─────────────────────────────────────────────────
234
+
235
+ module DirtyAttributes
236
+ def self.included(base)
237
+ base.send :include, ActiveModel::Dirty
238
+ base.send :alias_method, :dirty?, :changed?
239
+
240
+ base.class_eval { after_save :clear_changes_information }
241
+ end
242
+ private
243
+ def clone_attribute(value)
244
+ if [Integer, Symbol, TrueClass, FalseClass, NilClass, Float].any? {|k| value.is_a?(k)}
245
+ value
246
+ elsif [Hash, Array].include?(value.class)
247
+ Marshal.load(Marshal.dump(value))
248
+ else
249
+ value.clone
250
+ end
251
+ end
252
+ end
253
+
254
+ # ── JSON ─────────────────────────────────────────────────────────────
255
+
256
+ module Json
257
+ def to_json(*args); to_hash(*args).to_json(*args); end
258
+ def to_hash(options = nil)
259
+ doc = { 'ruby_class' => self.class.name, '_id' => _id, '_rev' => _rev }.reject { |_, v| v.nil? }.merge(@_document || {})
260
+ (self.class.properties || []).inject(doc) {|d, p| p.serialize(d, self); d }
261
+ end
262
+ alias :as_json :to_hash
263
+ def _document; @_document ||= {}; end
264
+
265
+ def self.included(base)
266
+ base.extend ClassMethods
267
+ end
268
+
269
+ module ClassMethods
270
+ # Called by JSON.parse to hydrate documents into model instances.
271
+ # Looks for 'ruby_class' key (mozo convention) or 'json_class' (standard).
272
+ def json_create(json)
273
+ return if json.nil?
274
+ doc = ActiveSupport::HashWithIndifferentAccess.new(json)
275
+ instance = new
276
+ instance.instance_variable_set(:@_document, doc)
277
+ instance._id = doc[:_id] || doc['_id']
278
+ instance._rev = doc[:_rev] || doc['_rev']
279
+ instance._attachments = doc[:_attachments] || doc['_attachments']
280
+ instance
281
+ end
282
+ end
283
+ end
284
+
285
+ # ── Magic Timestamps ─────────────────────────────────────────────────
286
+
287
+ module MagicTimestamps
288
+ def self.included(base)
289
+ base.instance_eval do
290
+ property :created_at, type: Time
291
+ property :updated_at, type: Time
292
+ before_create :set_created_at
293
+ before_save :set_updated_at
294
+ end
295
+ end
296
+ private
297
+ def set_created_at; self.created_at ||= Time.now; end
298
+ def set_updated_at; self.updated_at = Time.now; end
299
+ end
300
+
301
+ # ── ActiveModel Compliance ───────────────────────────────────────────
302
+
303
+ module ActiveModelCompliance
304
+ extend ActiveSupport::Concern
305
+ def persisted?; !new? && !destroyed?; end
306
+ def destroyed?; @destroyed || false; end
307
+ def to_key; persisted? ? [id] : nil; end
308
+ def to_model; self; end
309
+ end
310
+
311
+ # ── Forbidden Attributes Protection ──────────────────────────────────
312
+
313
+ module ForbiddenAttributesProtection
314
+ # Rails 5+ uses strong parameters at controller level — stub only
315
+ end
316
+
317
+ # ── Revisions ────────────────────────────────────────────────────────
318
+
319
+ module Revisions
320
+ def self.included(base)
321
+ base.class_eval do
322
+ def self.revisions(ids)
323
+ return [] if ids.empty?
324
+ database.couchrest_database.bulk_load(ids)
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ # ── Validation ───────────────────────────────────────────────────────
331
+
332
+ module Validation
333
+ extend ActiveSupport::Concern
334
+ include ActiveModel::Validations
335
+ end
336
+
337
+ # ── Type Caster ──────────────────────────────────────────────────────
338
+
339
+ class TypeCaster
340
+ # Cast a value to the given type.
341
+ # Supports:
342
+ # type: SomeClass — direct class reference
343
+ # type: :boolean, :integer — symbol (lazy, Rails autoloading safe)
344
+ # type: 'ClassName' — string (constantize in Rails)
345
+ def cast(value, type = nil)
346
+ return value unless type
347
+ resolved = resolve_type(type)
348
+ # If resolved is a Module (class), try to coerce
349
+ if resolved.is_a?(Module)
350
+ return value if value.is_a?(resolved)
351
+ return cast_to_builtin(value, resolved)
352
+ end
353
+ value
354
+ end
355
+
356
+ private
357
+
358
+ def cast_to_builtin(value, klass)
359
+ case klass.name
360
+ when 'Integer' then value.to_i
361
+ when 'Float' then value.to_f
362
+ when 'String' then value.to_s
363
+ when 'Symbol' then value.to_sym
364
+ when 'TrueClass', 'FalseClass' then !!value
365
+ when 'Array' then value.is_a?(Array) ? value : [value]
366
+ when 'Hash' then value.is_a?(Hash) ? value : { value: value }
367
+ when 'Time', 'DateTime', 'Date'
368
+ value.is_a?(String) ? Time.parse(value) : value
369
+ else value
370
+ end
371
+ end
372
+
373
+ public
374
+ def cast_back(value)
375
+ value.respond_to?(:iso8601) ? value.iso8601 : value
376
+ end
377
+
378
+ private
379
+
380
+ # Map symbols and strings to Ruby classes.
381
+ # Symbols are preferred — they work without Rails autoloading.
382
+ BUILTIN_TYPES = {
383
+ boolean: [TrueClass, FalseClass],
384
+ integer: Integer,
385
+ float: Float,
386
+ string: String,
387
+ symbol: Symbol,
388
+ time: Time,
389
+ datetime: DateTime,
390
+ date: Date,
391
+ array: Array,
392
+ hash: Hash,
393
+ }.freeze
394
+
395
+ def resolve_type(type)
396
+ case type
397
+ when Symbol
398
+ mapped = BUILTIN_TYPES[type]
399
+ mapped || (Object.const_get(type.to_s.classify) rescue type)
400
+ when String
401
+ Object.const_get(type) rescue type
402
+ when Module
403
+ type
404
+ else
405
+ type
406
+ end
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
@@ -0,0 +1,11 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module Properties
4
+ def check_existing_properties(name, type)
5
+ if properties.find{|property| name.to_sym == property.name.to_sym && property.class != type}
6
+ raise "Property with the name (#{name}) already defined"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ #require 'active_support/core_ext/hash/except'
2
+ module SimplyCouch
3
+ module Model
4
+ module Validations
5
+ class UniquenessValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ other_instance = record.class.send("find_by_#{attribute}", value)
8
+ if other_instance && other_instance != record && other_instance.send(attribute) == value
9
+ record.errors.add(attribute, :taken, **options.except(:case_sensitive, :scope).merge(:value => value))
10
+ end
11
+ end
12
+ end
13
+ def validates_uniqueness_of(*attr_names)
14
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
15
+ end
16
+ class ContainmentValidator < ActiveModel::EachValidator
17
+ def validate_each(record, attribute, value)
18
+ unless Array.wrap(value) - options[:in] == []
19
+ record.errors.add(attribute, :inclusion, **options.except(:in, :within).merge!(:value => value))
20
+ end
21
+ end
22
+ end
23
+ def validates_containment_of(*attr_names)
24
+ validates_with ContainmentValidator, _merge_attributes(attr_names)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,115 @@
1
+ require 'digest'
2
+ module SimplyCouch
3
+ module Model
4
+ module View
5
+ class BaseViewSpec
6
+ attr_reader :reduce_function, :lib, :list_name, :list_function, :design_document, :view_name, :klass, :options, :language
7
+ attr_accessor :view_parameters
8
+
9
+ private :klass, :options
10
+
11
+ DEFAULT_LANGUAGE = :javascript
12
+ DEFAULT_DIGEST_VIEW_NAMES = false
13
+
14
+ def initialize(klass, view_name, options, view_parameters)
15
+ normalized_view_parameters = normalize_view_parameters view_parameters
16
+
17
+ @list_name = normalized_view_parameters.delete(:list) || options[:list]
18
+ @language = options[:language] || DEFAULT_LANGUAGE
19
+
20
+ assert_valid_view_parameters normalized_view_parameters
21
+ assert_sorted_false_not_with_startkey_endkey normalized_view_parameters
22
+ @klass = klass
23
+ @options = options
24
+ @view_name = compute_view_name(view_name,
25
+ options.key?(:digest_view_name) ? options[:digest_view_name] : DEFAULT_DIGEST_VIEW_NAMES)
26
+ @design_document = translate_to_design_doc_name(klass.to_s, @view_name, @list_name)
27
+ @list_params = normalized_view_parameters.delete :list_params
28
+
29
+ @list_function = klass.lists(@list_name) if @list_name
30
+ @view_parameters = {}
31
+ [:group, :include_docs, :descending, :group_level, :limit].each do |key|
32
+ @view_parameters[key] = options[key] if options.include?(key)
33
+ end
34
+ @view_parameters.merge!(normalized_view_parameters)
35
+ @view_parameters.merge!(@list_params) if @list_params
36
+ end
37
+
38
+ def process_results(results)
39
+ if (filter = options[:results_filter])
40
+ filter.call results
41
+ else
42
+ results
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def compute_view_name(view_name, digest)
49
+ if digest
50
+ "#{view_name}-#{Digest::MD5.hexdigest(map_function + reduce_function.to_s)}"
51
+ else
52
+ view_name
53
+ end
54
+ end
55
+
56
+ def normalize_view_parameters(params)
57
+ hash = wrap_in_hash params
58
+ remove_nil_stale(replace_range_key(hash))
59
+ end
60
+
61
+ def remove_nil_stale(params)
62
+ params.reject{|name, value| name.to_s == 'stale' && value.nil?}
63
+ end
64
+
65
+ def wrap_in_hash(params)
66
+ if params.is_a?(Hash)
67
+ params
68
+ else
69
+ {:key => params}
70
+ end
71
+ end
72
+
73
+ def replace_range_key(params)
74
+ if((key = params[:key]).is_a?(Range))
75
+ params.delete :key
76
+ params[:startkey] = key.first
77
+ params[:endkey] = key.last
78
+ end
79
+ params
80
+ end
81
+
82
+ def assert_valid_view_parameters(params)
83
+ params.keys.each do |key|
84
+ fail ArgumentError, "invalid view parameter: #{key}" unless valid_view_parameters.include?(key.to_s)
85
+ end
86
+ end
87
+
88
+ def assert_sorted_false_not_with_startkey_endkey(params)
89
+ used_key_params = params.keys & [:startkey, :endkey, :startkey_docid, :endkey_docid]
90
+ if params[:sorted] == false && used_key_params.any?
91
+ fail ArgumentError, "view parameter: `sorted: false` can not be combined with #{used_key_params.join(', ')}"
92
+ end
93
+ end
94
+
95
+ def valid_view_parameters
96
+ %w(list_params key keys startkey startkey_docid endkey endkey_docid limit stale descending skip group group_level reduce include_docs inclusive_end sorted)
97
+ end
98
+
99
+ def translate_to_design_doc_name(klass_name, view_name, list_name)
100
+ klass_name = klass_name.dup
101
+ klass_name.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
102
+ klass_name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
103
+ klass_name.tr!('-', '_')
104
+ doc_name = klass_name.downcase
105
+
106
+ if options && options[:split_design_doc]
107
+ doc_name += "_view_#{view_name}" if view_name.present?
108
+ doc_name += "_list_#{list_name}" if list_name.present?
109
+ end
110
+ doc_name
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,49 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ # a view for custom map/reduce functions that still returns model instances
5
+ #
6
+ # example:
7
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :include_docs => true, :type => :custom, :reduce => nil
8
+ class CustomViewSpec < BaseViewSpec
9
+ def map_function
10
+ options[:map]
11
+ end
12
+
13
+ def reduce_function
14
+ options[:reduce]
15
+ end
16
+
17
+ def lib
18
+ options[:lib]
19
+ end
20
+
21
+ def view_parameters
22
+ {:include_docs => options[:include_docs] || false}.merge(super)
23
+ end
24
+
25
+ def process_results(results)
26
+ processed = if count?
27
+ results['rows'].first.try(:[], 'value') || 0
28
+ else
29
+ results['rows'].map do |row|
30
+ if row['doc'].kind_of?(klass)
31
+ row['doc']
32
+ else
33
+ result = row['doc'] || (row['value'].merge(:_id => row['id'] || row['key']) unless view_parameters[:include_docs])
34
+ klass.json_create result if result
35
+ end
36
+ end.compact
37
+ end
38
+ super processed
39
+ end
40
+
41
+ private
42
+
43
+ def count?
44
+ view_parameters[:reduce]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ module CustomViews
5
+ extend ActiveSupport::Concern
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def views(view_name = nil)
13
+ if view_name
14
+ _find_view(view_name)
15
+ else
16
+ @views ||= {}
17
+ end
18
+ end
19
+
20
+ def execute_view(view_name, view_parameters)
21
+ options = views(view_name).dup
22
+ options[:split_design_doc] = split_design_documents? if respond_to?(:split_design_documents?)
23
+ view_spec_class(options[:type]).new(self, view_name, options, view_parameters)
24
+ end
25
+
26
+ # Declare a CouchDB view, for examples on how to use see the *ViewSpec classes in SimplyCouch::Model::View
27
+ def view(view_name, options)
28
+ view_name = view_name.to_s
29
+ views[view_name] = options
30
+ method_str = "def #{view_name}(view_parameters = {}); execute_view(\"#{view_name}\", view_parameters); end"
31
+ self.instance_eval(method_str)
32
+ end
33
+
34
+ def view_spec_class(type)
35
+ if type && type.is_a?(Class)
36
+ type
37
+ else
38
+ name = type.nil? ? 'Model' : type.to_s.camelize
39
+ SimplyCouch::Model::View.const_get("#{name}ViewSpec")
40
+ end
41
+ end
42
+
43
+ def _find_view(view)
44
+ (@views && @views[view]) || (superclass._find_view(view) if superclass.respond_to?(:_find_view))
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,25 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ module Lists
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def list(name, list_function)
11
+ lists[name] = list_function
12
+ end
13
+
14
+ def lists(name = nil)
15
+ if name.nil?
16
+ @lists ||= {}
17
+ else
18
+ (@lists && @lists[name]) || (superclass.lists(name) if superclass.respond_to?(:lists))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end