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