couchrest_model-radiant 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +19 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +21 -0
  5. data/history.txt +207 -0
  6. data/lib/couchrest/model.rb +10 -0
  7. data/lib/couchrest/model/associations.rb +223 -0
  8. data/lib/couchrest/model/base.rb +111 -0
  9. data/lib/couchrest/model/callbacks.rb +27 -0
  10. data/lib/couchrest/model/casted_array.rb +39 -0
  11. data/lib/couchrest/model/casted_model.rb +68 -0
  12. data/lib/couchrest/model/class_proxy.rb +122 -0
  13. data/lib/couchrest/model/collection.rb +263 -0
  14. data/lib/couchrest/model/configuration.rb +51 -0
  15. data/lib/couchrest/model/design_doc.rb +123 -0
  16. data/lib/couchrest/model/document_queries.rb +83 -0
  17. data/lib/couchrest/model/errors.rb +23 -0
  18. data/lib/couchrest/model/extended_attachments.rb +77 -0
  19. data/lib/couchrest/model/persistence.rb +155 -0
  20. data/lib/couchrest/model/properties.rb +208 -0
  21. data/lib/couchrest/model/property.rb +97 -0
  22. data/lib/couchrest/model/property_protection.rb +71 -0
  23. data/lib/couchrest/model/support/couchrest.rb +19 -0
  24. data/lib/couchrest/model/support/hash.rb +9 -0
  25. data/lib/couchrest/model/typecast.rb +175 -0
  26. data/lib/couchrest/model/validations.rb +68 -0
  27. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  28. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  29. data/lib/couchrest/model/validations/uniqueness.rb +44 -0
  30. data/lib/couchrest/model/views.rb +160 -0
  31. data/lib/couchrest/railtie.rb +12 -0
  32. data/lib/couchrest_model.rb +62 -0
  33. data/lib/rails/generators/couchrest_model.rb +16 -0
  34. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  35. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  36. data/spec/couchrest/assocations_spec.rb +196 -0
  37. data/spec/couchrest/attachment_spec.rb +176 -0
  38. data/spec/couchrest/base_spec.rb +463 -0
  39. data/spec/couchrest/casted_model_spec.rb +438 -0
  40. data/spec/couchrest/casted_spec.rb +75 -0
  41. data/spec/couchrest/class_proxy_spec.rb +132 -0
  42. data/spec/couchrest/configuration_spec.rb +78 -0
  43. data/spec/couchrest/inherited_spec.rb +40 -0
  44. data/spec/couchrest/persistence_spec.rb +415 -0
  45. data/spec/couchrest/property_protection_spec.rb +192 -0
  46. data/spec/couchrest/property_spec.rb +871 -0
  47. data/spec/couchrest/subclass_spec.rb +99 -0
  48. data/spec/couchrest/validations.rb +85 -0
  49. data/spec/couchrest/view_spec.rb +463 -0
  50. data/spec/fixtures/attachments/README +3 -0
  51. data/spec/fixtures/attachments/couchdb.png +0 -0
  52. data/spec/fixtures/attachments/test.html +11 -0
  53. data/spec/fixtures/base.rb +139 -0
  54. data/spec/fixtures/more/article.rb +35 -0
  55. data/spec/fixtures/more/card.rb +17 -0
  56. data/spec/fixtures/more/cat.rb +19 -0
  57. data/spec/fixtures/more/client.rb +6 -0
  58. data/spec/fixtures/more/course.rb +25 -0
  59. data/spec/fixtures/more/event.rb +8 -0
  60. data/spec/fixtures/more/invoice.rb +14 -0
  61. data/spec/fixtures/more/person.rb +9 -0
  62. data/spec/fixtures/more/question.rb +7 -0
  63. data/spec/fixtures/more/sale_entry.rb +9 -0
  64. data/spec/fixtures/more/sale_invoice.rb +13 -0
  65. data/spec/fixtures/more/service.rb +10 -0
  66. data/spec/fixtures/more/user.rb +22 -0
  67. data/spec/fixtures/views/lib.js +3 -0
  68. data/spec/fixtures/views/test_view/lib.js +3 -0
  69. data/spec/fixtures/views/test_view/only-map.js +4 -0
  70. data/spec/fixtures/views/test_view/test-map.js +3 -0
  71. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  72. data/spec/spec_helper.rb +48 -0
  73. 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