couchrest_model-radiant 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +176 -0
- data/README.md +19 -0
- data/Rakefile +74 -0
- data/THANKS.md +21 -0
- data/history.txt +207 -0
- data/lib/couchrest/model.rb +10 -0
- data/lib/couchrest/model/associations.rb +223 -0
- data/lib/couchrest/model/base.rb +111 -0
- data/lib/couchrest/model/callbacks.rb +27 -0
- data/lib/couchrest/model/casted_array.rb +39 -0
- data/lib/couchrest/model/casted_model.rb +68 -0
- data/lib/couchrest/model/class_proxy.rb +122 -0
- data/lib/couchrest/model/collection.rb +263 -0
- data/lib/couchrest/model/configuration.rb +51 -0
- data/lib/couchrest/model/design_doc.rb +123 -0
- data/lib/couchrest/model/document_queries.rb +83 -0
- data/lib/couchrest/model/errors.rb +23 -0
- data/lib/couchrest/model/extended_attachments.rb +77 -0
- data/lib/couchrest/model/persistence.rb +155 -0
- data/lib/couchrest/model/properties.rb +208 -0
- data/lib/couchrest/model/property.rb +97 -0
- data/lib/couchrest/model/property_protection.rb +71 -0
- data/lib/couchrest/model/support/couchrest.rb +19 -0
- data/lib/couchrest/model/support/hash.rb +9 -0
- data/lib/couchrest/model/typecast.rb +175 -0
- data/lib/couchrest/model/validations.rb +68 -0
- data/lib/couchrest/model/validations/casted_model.rb +14 -0
- data/lib/couchrest/model/validations/locale/en.yml +5 -0
- data/lib/couchrest/model/validations/uniqueness.rb +44 -0
- data/lib/couchrest/model/views.rb +160 -0
- data/lib/couchrest/railtie.rb +12 -0
- data/lib/couchrest_model.rb +62 -0
- data/lib/rails/generators/couchrest_model.rb +16 -0
- data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
- data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
- data/spec/couchrest/assocations_spec.rb +196 -0
- data/spec/couchrest/attachment_spec.rb +176 -0
- data/spec/couchrest/base_spec.rb +463 -0
- data/spec/couchrest/casted_model_spec.rb +438 -0
- data/spec/couchrest/casted_spec.rb +75 -0
- data/spec/couchrest/class_proxy_spec.rb +132 -0
- data/spec/couchrest/configuration_spec.rb +78 -0
- data/spec/couchrest/inherited_spec.rb +40 -0
- data/spec/couchrest/persistence_spec.rb +415 -0
- data/spec/couchrest/property_protection_spec.rb +192 -0
- data/spec/couchrest/property_spec.rb +871 -0
- data/spec/couchrest/subclass_spec.rb +99 -0
- data/spec/couchrest/validations.rb +85 -0
- data/spec/couchrest/view_spec.rb +463 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/base.rb +139 -0
- data/spec/fixtures/more/article.rb +35 -0
- data/spec/fixtures/more/card.rb +17 -0
- data/spec/fixtures/more/cat.rb +19 -0
- data/spec/fixtures/more/client.rb +6 -0
- data/spec/fixtures/more/course.rb +25 -0
- data/spec/fixtures/more/event.rb +8 -0
- data/spec/fixtures/more/invoice.rb +14 -0
- data/spec/fixtures/more/person.rb +9 -0
- data/spec/fixtures/more/question.rb +7 -0
- data/spec/fixtures/more/sale_entry.rb +9 -0
- data/spec/fixtures/more/sale_invoice.rb +13 -0
- data/spec/fixtures/more/service.rb +10 -0
- data/spec/fixtures/more/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec_helper.rb +48 -0
- metadata +263 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module CouchRest::Model
|
3
|
+
class Property
|
4
|
+
|
5
|
+
include ::CouchRest::Model::Typecast
|
6
|
+
|
7
|
+
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
|
8
|
+
|
9
|
+
# Attribute to define.
|
10
|
+
# All Properties are assumed casted unless the type is nil.
|
11
|
+
def initialize(name, type = nil, options = {})
|
12
|
+
@name = name.to_s
|
13
|
+
@casted = true
|
14
|
+
parse_type(type)
|
15
|
+
parse_options(options)
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
name
|
21
|
+
end
|
22
|
+
|
23
|
+
# Cast the provided value using the properties details.
|
24
|
+
def cast(parent, value)
|
25
|
+
return value unless casted
|
26
|
+
if type.is_a?(Array)
|
27
|
+
if value.nil?
|
28
|
+
value = []
|
29
|
+
elsif [Hash, HashWithIndifferentAccess].include?(value.class)
|
30
|
+
# Assume provided as a Hash where key is index!
|
31
|
+
data = value
|
32
|
+
value = [ ]
|
33
|
+
data.keys.sort.each do |k|
|
34
|
+
value << data[k]
|
35
|
+
end
|
36
|
+
elsif !value.is_a?(Array)
|
37
|
+
raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
|
38
|
+
end
|
39
|
+
arr = value.collect { |data| cast_value(parent, data) }
|
40
|
+
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
41
|
+
value = type_class != String ? CastedArray.new(arr, self) : arr
|
42
|
+
value.casted_by = parent if value.respond_to?(:casted_by)
|
43
|
+
elsif !value.nil?
|
44
|
+
value = cast_value(parent, value)
|
45
|
+
end
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cast an individual value, not an array
|
50
|
+
def cast_value(parent, value)
|
51
|
+
raise "An array inside an array cannot be casted, use CastedModel" if value.is_a?(Array)
|
52
|
+
value = typecast_value(value, self)
|
53
|
+
associate_casted_value_to_parent(parent, value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def default_value
|
57
|
+
return if default.nil?
|
58
|
+
if default.class == Proc
|
59
|
+
default.call
|
60
|
+
else
|
61
|
+
# Marshal.load(Marshal.dump(default)) # Removed as there are no failing tests and caused mutex errors
|
62
|
+
default
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def associate_casted_value_to_parent(parent, value)
|
69
|
+
value.casted_by = parent if value.respond_to?(:casted_by)
|
70
|
+
value
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_type(type)
|
74
|
+
if type.nil?
|
75
|
+
@casted = false
|
76
|
+
@type = nil
|
77
|
+
@type_class = nil
|
78
|
+
else
|
79
|
+
base = type.is_a?(Array) ? type.first : type
|
80
|
+
base = Object if base.nil?
|
81
|
+
raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
|
82
|
+
@type_class = base
|
83
|
+
@type = type
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_options(options)
|
88
|
+
@validation_format = options.delete(:format) if options[:format]
|
89
|
+
@read_only = options.delete(:read_only) if options[:read_only]
|
90
|
+
@alias = options.delete(:alias) if options[:alias]
|
91
|
+
@default = options.delete(:default) unless options[:default].nil?
|
92
|
+
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
|
93
|
+
@options = options
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module PropertyProtection
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Property protection from mass assignment to CouchRest::Model properties
|
7
|
+
#
|
8
|
+
# Protected methods will be removed from
|
9
|
+
# * new
|
10
|
+
# * update_attributes
|
11
|
+
# * upate_attributes_without_saving
|
12
|
+
# * attributes=
|
13
|
+
#
|
14
|
+
# There are two modes of protection
|
15
|
+
# 1) Declare accessible poperties, and assume all unspecified properties are protected
|
16
|
+
# property :name, :accessible => true
|
17
|
+
# property :admin # this will be automatically protected
|
18
|
+
#
|
19
|
+
# 2) Declare protected properties, and assume all unspecified properties are accessible
|
20
|
+
# property :name # this will not be protected
|
21
|
+
# property :admin, :protected => true
|
22
|
+
#
|
23
|
+
# 3) Mix and match, and assume all unspecified properties are protected.
|
24
|
+
# property :name, :accessible => true
|
25
|
+
# property :admin, :protected => true # ignored
|
26
|
+
# property :phone # this will be automatically protected
|
27
|
+
#
|
28
|
+
# Note: the timestamps! method protectes the created_at and updated_at properties
|
29
|
+
|
30
|
+
|
31
|
+
def self.included(base)
|
32
|
+
base.extend(ClassMethods)
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def accessible_properties
|
37
|
+
props = properties.select { |prop| prop.options[:accessible] }
|
38
|
+
if props.empty?
|
39
|
+
props = properties.select { |prop| !prop.options[:protected] }
|
40
|
+
end
|
41
|
+
props
|
42
|
+
end
|
43
|
+
|
44
|
+
def protected_properties
|
45
|
+
accessibles = accessible_properties
|
46
|
+
properties.reject { |prop| accessibles.include?(prop) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def accessible_properties
|
51
|
+
self.class.accessible_properties
|
52
|
+
end
|
53
|
+
|
54
|
+
def protected_properties
|
55
|
+
self.class.protected_properties
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return a new copy of the attributes hash with protected attributes
|
59
|
+
# removed.
|
60
|
+
def remove_protected_attributes(attributes)
|
61
|
+
protected_names = protected_properties.map { |prop| prop.name }
|
62
|
+
return attributes if protected_names.empty? or attributes.nil?
|
63
|
+
|
64
|
+
attributes.reject do |property_name, property_value|
|
65
|
+
protected_names.include?(property_name.to_s)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module CouchRest
|
3
|
+
|
4
|
+
class Database
|
5
|
+
|
6
|
+
alias :delete_orig! :delete!
|
7
|
+
def delete!
|
8
|
+
clear_model_fresh_cache
|
9
|
+
delete_orig!
|
10
|
+
end
|
11
|
+
|
12
|
+
# If the database is deleted, ensure that the design docs will be refreshed.
|
13
|
+
def clear_model_fresh_cache
|
14
|
+
::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# This file contains various hacks for Rails compatibility.
|
2
|
+
class Hash
|
3
|
+
# Hack so that CouchRest::Document, which descends from Hash,
|
4
|
+
# doesn't appear to Rails routing as a Hash of options
|
5
|
+
def self.===(other)
|
6
|
+
return false if self == Hash && other.is_a?(CouchRest::Document)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
class Time
|
2
|
+
# returns a local time value much faster than Time.parse
|
3
|
+
def self.mktime_with_offset(string)
|
4
|
+
string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(([\+|\s|\-])*(\d{2}):?(\d{2}))?/
|
5
|
+
# $1 = year
|
6
|
+
# $2 = month
|
7
|
+
# $3 = day
|
8
|
+
# $4 = hours
|
9
|
+
# $5 = minutes
|
10
|
+
# $6 = seconds
|
11
|
+
# $8 = time zone direction
|
12
|
+
# $9 = tz difference
|
13
|
+
# utc time with wrong TZ info:
|
14
|
+
time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6)
|
15
|
+
if ($7)
|
16
|
+
tz_difference = ("#{$8 == '-' ? '+' : '-'}#{$9}".to_i * 3600)
|
17
|
+
time + tz_difference + zone_offset(time.zone)
|
18
|
+
else
|
19
|
+
time
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module CouchRest
|
25
|
+
module Model
|
26
|
+
module Typecast
|
27
|
+
|
28
|
+
def typecast_value(value, property) # klass, init_method)
|
29
|
+
return nil if value.nil?
|
30
|
+
klass = property.type_class
|
31
|
+
if value.instance_of?(klass) || klass == Object
|
32
|
+
value
|
33
|
+
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
34
|
+
send('typecast_to_'+klass.to_s.downcase, value)
|
35
|
+
else
|
36
|
+
# Allow the init_method to be defined as a Proc for advanced conversion
|
37
|
+
property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Typecast a value to an Integer
|
44
|
+
def typecast_to_integer(value)
|
45
|
+
typecast_to_numeric(value, :to_i)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Typecast a value to a String
|
49
|
+
def typecast_to_string(value)
|
50
|
+
value.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
# Typecast a value to a true or false
|
54
|
+
def typecast_to_trueclass(value)
|
55
|
+
if value.kind_of?(Integer)
|
56
|
+
return true if value == 1
|
57
|
+
return false if value == 0
|
58
|
+
elsif value.respond_to?(:to_s)
|
59
|
+
return true if %w[ true 1 t ].include?(value.to_s.downcase)
|
60
|
+
return false if %w[ false 0 f ].include?(value.to_s.downcase)
|
61
|
+
end
|
62
|
+
value
|
63
|
+
end
|
64
|
+
|
65
|
+
# Typecast a value to a BigDecimal
|
66
|
+
def typecast_to_bigdecimal(value)
|
67
|
+
if value.kind_of?(Integer)
|
68
|
+
value.to_s.to_d
|
69
|
+
else
|
70
|
+
typecast_to_numeric(value, :to_d)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Typecast a value to a Float
|
75
|
+
def typecast_to_float(value)
|
76
|
+
typecast_to_numeric(value, :to_f)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Match numeric string
|
80
|
+
def typecast_to_numeric(value, method)
|
81
|
+
if value.respond_to?(:to_str)
|
82
|
+
if value.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
|
83
|
+
$1.send(method)
|
84
|
+
else
|
85
|
+
value
|
86
|
+
end
|
87
|
+
elsif value.respond_to?(method)
|
88
|
+
value.send(method)
|
89
|
+
else
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Typecasts an arbitrary value to a DateTime.
|
95
|
+
# Handles both Hashes and DateTime instances.
|
96
|
+
# This is slow!! Use Time instead.
|
97
|
+
def typecast_to_datetime(value)
|
98
|
+
if value.is_a?(Hash)
|
99
|
+
typecast_hash_to_datetime(value)
|
100
|
+
else
|
101
|
+
DateTime.parse(value.to_s)
|
102
|
+
end
|
103
|
+
rescue ArgumentError
|
104
|
+
value
|
105
|
+
end
|
106
|
+
|
107
|
+
# Typecasts an arbitrary value to a Date
|
108
|
+
# Handles both Hashes and Date instances.
|
109
|
+
def typecast_to_date(value)
|
110
|
+
if value.is_a?(Hash)
|
111
|
+
typecast_hash_to_date(value)
|
112
|
+
elsif value.is_a?(Time) # sometimes people think date is time!
|
113
|
+
value.to_date
|
114
|
+
elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
|
115
|
+
# Faster than parsing the date
|
116
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
117
|
+
else
|
118
|
+
Date.parse(value)
|
119
|
+
end
|
120
|
+
rescue ArgumentError
|
121
|
+
value
|
122
|
+
end
|
123
|
+
|
124
|
+
# Typecasts an arbitrary value to a Time
|
125
|
+
# Handles both Hashes and Time instances.
|
126
|
+
def typecast_to_time(value)
|
127
|
+
if value.is_a?(Hash)
|
128
|
+
typecast_hash_to_time(value)
|
129
|
+
else
|
130
|
+
Time.mktime_with_offset(value.to_s)
|
131
|
+
end
|
132
|
+
rescue ArgumentError
|
133
|
+
value
|
134
|
+
rescue TypeError
|
135
|
+
# After failures, resort to normal time parse
|
136
|
+
value
|
137
|
+
end
|
138
|
+
|
139
|
+
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
|
140
|
+
# :hour, :min, :sec
|
141
|
+
def typecast_hash_to_datetime(value)
|
142
|
+
DateTime.new(*extract_time(value))
|
143
|
+
end
|
144
|
+
|
145
|
+
# Creates a Date instance from a Hash with keys :year, :month, :day
|
146
|
+
def typecast_hash_to_date(value)
|
147
|
+
Date.new(*extract_time(value)[0, 3])
|
148
|
+
end
|
149
|
+
|
150
|
+
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
151
|
+
# :hour, :min, :sec
|
152
|
+
def typecast_hash_to_time(value)
|
153
|
+
Time.local(*extract_time(value))
|
154
|
+
end
|
155
|
+
|
156
|
+
# Extracts the given args from the hash. If a value does not exist, it
|
157
|
+
# uses the value of Time.now.
|
158
|
+
def extract_time(value)
|
159
|
+
now = Time.now
|
160
|
+
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
161
|
+
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Typecast a value to a Class
|
166
|
+
def typecast_to_class(value)
|
167
|
+
value.to_s.constantize
|
168
|
+
rescue NameError
|
169
|
+
value
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "couchrest/model/validations/casted_model"
|
4
|
+
require "couchrest/model/validations/uniqueness"
|
5
|
+
|
6
|
+
I18n.load_path << File.join(
|
7
|
+
File.dirname(__FILE__), "validations", "locale", "en.yml"
|
8
|
+
)
|
9
|
+
|
10
|
+
module CouchRest
|
11
|
+
module Model
|
12
|
+
|
13
|
+
# Validations may be applied to both Model::Base and Model::CastedModel
|
14
|
+
module Validations
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
included do
|
17
|
+
include ActiveModel::Validations
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# Validates the associated casted model. This method should not be
|
24
|
+
# used within your code as it is automatically included when a CastedModel
|
25
|
+
# is used inside the model.
|
26
|
+
#
|
27
|
+
def validates_casted_model(*args)
|
28
|
+
validates_with(CastedModelValidator, _merge_attributes(args))
|
29
|
+
end
|
30
|
+
|
31
|
+
# Validates if the field is unique for this type of document. Automatically creates
|
32
|
+
# a view if one does not already exist and performs a search for all matching
|
33
|
+
# documents.
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# class Person < CouchRest::Model::Base
|
38
|
+
# property :title, String
|
39
|
+
#
|
40
|
+
# validates_uniqueness_of :title
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# Asside from the standard options, you can specify the name of the view you'd like
|
44
|
+
# to use for the search inside the +:view+ option. The following example would search
|
45
|
+
# for the code in side the +all+ view, useful for when +unique_id+ is used and you'd
|
46
|
+
# like to check before receiving a RestClient Conflict error:
|
47
|
+
#
|
48
|
+
# validates_uniqueness_of :code, :view => 'all'
|
49
|
+
#
|
50
|
+
# A +:proxy+ parameter is also accepted if you would
|
51
|
+
# like to call a method on the document on which the view should be performed.
|
52
|
+
#
|
53
|
+
# For Example:
|
54
|
+
#
|
55
|
+
# # Same as not including proxy:
|
56
|
+
# validates_uniqueness_of :title, :proxy => 'class'
|
57
|
+
#
|
58
|
+
# # Person#company.people provides a proxy object for people
|
59
|
+
# validates_uniqueness_of :title, :proxy => 'company.people'
|
60
|
+
#
|
61
|
+
def validates_uniqueness_of(*args)
|
62
|
+
validates_with(UniquenessValidator, _merge_attributes(args))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Validations
|
4
|
+
class CastedModelValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(document, attribute, value)
|
7
|
+
values = value.is_a?(Array) ? value : [value]
|
8
|
+
return if values.collect {|doc| doc.nil? || doc.valid? }.all?
|
9
|
+
document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|