couchrest_model-radiant 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.
- 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
|