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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +182 -0
- data/LICENSE.txt +15 -0
- data/README.md +294 -0
- data/lib/core_ext/date.rb +15 -0
- data/lib/core_ext/time.rb +23 -0
- data/lib/simply_couch/class_methods_base.rb +72 -0
- data/lib/simply_couch/has_attachment.rb +225 -0
- data/lib/simply_couch/include_relation.rb +160 -0
- data/lib/simply_couch/instance_methods.rb +356 -0
- data/lib/simply_couch/locale/en.yml +5 -0
- data/lib/simply_couch/model/ancestry.rb +307 -0
- data/lib/simply_couch/model/association_property.rb +26 -0
- data/lib/simply_couch/model/attachments.rb +90 -0
- data/lib/simply_couch/model/belongs_to.rb +140 -0
- data/lib/simply_couch/model/database.rb +209 -0
- data/lib/simply_couch/model/embedded_in.rb +196 -0
- data/lib/simply_couch/model/find_by.rb +202 -0
- data/lib/simply_couch/model/finders.rb +77 -0
- data/lib/simply_couch/model/has_and_belongs_to_many.rb +223 -0
- data/lib/simply_couch/model/has_many.rb +177 -0
- data/lib/simply_couch/model/has_many_embedded.rb +187 -0
- data/lib/simply_couch/model/has_one.rb +75 -0
- data/lib/simply_couch/model/pagination.rb +25 -0
- data/lib/simply_couch/model/pagination_options.rb +55 -0
- data/lib/simply_couch/model/persistence.rb +411 -0
- data/lib/simply_couch/model/properties.rb +11 -0
- data/lib/simply_couch/model/validations.rb +28 -0
- data/lib/simply_couch/model/view/base_view_spec.rb +115 -0
- data/lib/simply_couch/model/view/custom_view_spec.rb +49 -0
- data/lib/simply_couch/model/view/custom_views.rb +50 -0
- data/lib/simply_couch/model/view/lists.rb +25 -0
- data/lib/simply_couch/model/view/model_view_spec.rb +106 -0
- data/lib/simply_couch/model/view/properties_view_spec.rb +53 -0
- data/lib/simply_couch/model/view/raw_view_spec.rb +30 -0
- data/lib/simply_couch/model/view/view_query.rb +98 -0
- data/lib/simply_couch/model/view.rb +8 -0
- data/lib/simply_couch/model/views/array_property_view_spec.rb +26 -0
- data/lib/simply_couch/model/views/deleted_model_view_spec.rb +43 -0
- data/lib/simply_couch/model/views.rb +2 -0
- data/lib/simply_couch/model.rb +195 -0
- data/lib/simply_couch/rake.rb +23 -0
- data/lib/simply_couch/storage.rb +147 -0
- data/lib/simply_couch.rb +26 -0
- 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, █ end
|
|
220
|
+
def self.after_#{cb}(*args, &block); set_callback :#{cb}, :after, *args, █ end
|
|
221
|
+
def self.around_#{cb}(*args, &block); set_callback :#{cb}, :around, *args, █ 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, █ end
|
|
227
|
+
def self.after_#{cb}(*args, &block); set_callback :#{cb}, :after, *args, █ 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
|