ripple 0.7.1 → 0.8.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/Rakefile +5 -4
  2. data/lib/rails/generators/ripple/model/model_generator.rb +20 -0
  3. data/lib/rails/generators/ripple/model/templates/model.rb +10 -0
  4. data/lib/rails/generators/ripple_generator.rb +60 -0
  5. data/lib/ripple.rb +24 -6
  6. data/lib/ripple/associations.rb +70 -3
  7. data/lib/ripple/associations/linked.rb +17 -5
  8. data/lib/ripple/associations/many.rb +5 -4
  9. data/lib/ripple/associations/many_embedded_proxy.rb +2 -0
  10. data/lib/ripple/associations/many_linked_proxy.rb +35 -0
  11. data/lib/ripple/associations/one.rb +4 -0
  12. data/lib/ripple/associations/one_embedded_proxy.rb +1 -1
  13. data/lib/ripple/associations/one_linked_proxy.rb +29 -0
  14. data/lib/ripple/attribute_methods/dirty.rb +2 -2
  15. data/lib/ripple/core_ext.rb +1 -0
  16. data/lib/ripple/core_ext/casting.rb +6 -8
  17. data/lib/ripple/document.rb +1 -0
  18. data/lib/ripple/document/bucket_access.rb +1 -1
  19. data/lib/ripple/document/finders.rb +1 -1
  20. data/lib/ripple/document/persistence.rb +28 -8
  21. data/lib/ripple/embedded_document.rb +1 -0
  22. data/lib/ripple/embedded_document/persistence.rb +11 -1
  23. data/lib/ripple/inspection.rb +26 -0
  24. data/lib/ripple/locale/en.yml +8 -5
  25. data/lib/ripple/railtie.rb +2 -12
  26. data/lib/ripple/validations.rb +5 -0
  27. data/lib/ripple/validations/associated_validator.rb +1 -2
  28. data/spec/fixtures/config.yml +6 -1
  29. data/spec/integration/ripple/associations_spec.rb +21 -0
  30. data/spec/ripple/associations/many_embedded_proxy_spec.rb +6 -0
  31. data/spec/ripple/associations/many_linked_proxy_spec.rb +103 -0
  32. data/spec/ripple/associations/one_embedded_proxy_spec.rb +5 -0
  33. data/spec/ripple/associations/one_linked_proxy_spec.rb +76 -0
  34. data/spec/ripple/associations/proxy_spec.rb +1 -1
  35. data/spec/ripple/bucket_access_spec.rb +3 -4
  36. data/spec/ripple/core_ext_spec.rb +11 -0
  37. data/spec/ripple/embedded_document/persistence_spec.rb +12 -0
  38. data/spec/ripple/finders_spec.rb +1 -1
  39. data/spec/ripple/inspection_spec.rb +48 -0
  40. data/spec/ripple/persistence_spec.rb +52 -6
  41. data/spec/ripple/properties_spec.rb +6 -0
  42. data/spec/ripple/ripple_spec.rb +12 -1
  43. data/spec/support/models/tasks.rb +13 -0
  44. data/spec/support/models/widget.rb +5 -1
  45. metadata +43 -18
data/Rakefile CHANGED
@@ -11,10 +11,10 @@ gemspec = Gem::Specification.new do |gem|
11
11
  gem.email = "seancribbs@gmail.com"
12
12
  gem.homepage = "http://seancribbs.github.com/ripple"
13
13
  gem.authors = ["Sean Cribbs"]
14
- gem.add_development_dependency "rspec", "~>2.0.0.beta.6"
14
+ gem.add_development_dependency "rspec", "~>2.0.0.beta.18"
15
15
  gem.add_dependency "riak-client", version
16
- gem.add_dependency "activesupport", "3.0.0.beta3"
17
- gem.add_dependency "activemodel", "3.0.0.beta3"
16
+ gem.add_dependency "activesupport", "3.0.0.rc2"
17
+ gem.add_dependency "activemodel", "3.0.0.rc2"
18
18
 
19
19
  files = FileList["**/*"]
20
20
  files.exclude /\.DS_Store/
@@ -26,6 +26,7 @@ gemspec = Gem::Specification.new do |gem|
26
26
  files.exclude '**/*.rej'
27
27
  files.exclude /^pkg/
28
28
  files.exclude 'ripple.gemspec'
29
+ files.exclude 'Gemfile'
29
30
 
30
31
  gem.files = files.to_a
31
32
 
@@ -48,7 +49,7 @@ end
48
49
 
49
50
  desc %{Release the gem to RubyGems.org}
50
51
  task :release => :gem do
51
- "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
52
+ system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
52
53
  end
53
54
 
54
55
  require 'rspec/core'
@@ -0,0 +1,20 @@
1
+ require 'rails/generators/ripple_generator'
2
+
3
+ module Ripple
4
+ module Generators
5
+ class ModelGenerator < Base
6
+ desc 'Creates a ripple model'
7
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
8
+ class_option :parent, :type => :string, :desc => "The parent class for the generated model"
9
+ class_option :embedded, :type => :boolean, :desc => "Make an embedded document model.", :default => false
10
+ class_option :embedded_in, :type => :string, :desc => "Specify the enclosing model for the embedded document. Implies --embedded."
11
+ check_class_collision
12
+
13
+ def create_model_file
14
+ template 'model.rb', "app/models/#{file_path}.rb"
15
+ end
16
+
17
+ hook_for :test_framework
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ class <%= class_name %><%= " < #{options[:parent].classify}" if options[:parent] %>
2
+ <% unless options[:parent] -%>
3
+ include Ripple::<%= "Embedded" if options[:embedded] || options[:embedded_in] %>Document
4
+ <% if options[:embedded_in] -%>embedded_in :<%= options[:embedded_in].underscore %><% end -%>
5
+ <% end -%>
6
+
7
+ <% attributes.reject{|attr| attr.reference?}.each do |attribute| -%>
8
+ property :<%= attribute.name %>, <%= attribute.type_class %>
9
+ <% end -%>
10
+ end
@@ -0,0 +1,60 @@
1
+ require "rails/generators/named_base"
2
+ require "rails/generators/active_model"
3
+
4
+ module Ripple
5
+ module Generators
6
+ class Base < ::Rails::Generators::NamedBase
7
+
8
+ def self.source_root
9
+ @_ripple_source_root ||=
10
+ File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__)
11
+ end
12
+ end
13
+
14
+ class ActiveModel < ::Rails::Generators::ActiveModel
15
+ def self.all(klass)
16
+ "#{klass}.all"
17
+ end
18
+
19
+ def self.find(klass, params=nil)
20
+ "#{klass}.find(#{params})"
21
+ end
22
+
23
+ def self.build(klass, params=nil)
24
+ if params
25
+ "#{klass}.new(#{params})"
26
+ else
27
+ "#{klass}.new"
28
+ end
29
+ end
30
+
31
+ def save
32
+ "#{name}.save"
33
+ end
34
+
35
+ def update_attributes(params=nil)
36
+ "#{name}.update_attributes(#{params})"
37
+ end
38
+
39
+ def errors
40
+ "#{name}.errors"
41
+ end
42
+
43
+ def destroy
44
+ "#{name}.destroy"
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ module Rails
51
+ module Generators
52
+ class GeneratedAttribute #:nodoc:
53
+ def type_class
54
+ return "Time" if type.to_s == "datetime"
55
+ return "String" if type.to_s == "text"
56
+ return type.to_s.camelcase
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/ripple.rb CHANGED
@@ -16,12 +16,12 @@ require 'riak'
16
16
  require 'active_support/all'
17
17
  require 'active_model'
18
18
  require 'ripple/i18n'
19
+ require 'ripple/core_ext'
19
20
 
20
21
  # Contains the classes and modules related to the ODM built on top of
21
22
  # the basic Riak client.
22
23
  module Ripple
23
24
  extend ActiveSupport::Autoload
24
- include ActiveSupport::Configurable
25
25
 
26
26
  # Primary models
27
27
  autoload :EmbeddedDocument
@@ -42,10 +42,9 @@ module Ripple
42
42
  autoload :PropertyTypeMismatch
43
43
 
44
44
  # Utilities
45
+ autoload :Inspection
45
46
  autoload :Translation
46
47
 
47
- DEFAULT_CONFIG = {}
48
-
49
48
  class << self
50
49
  # @return [Riak::Client] The client for the current thread.
51
50
  def client
@@ -60,11 +59,30 @@ module Ripple
60
59
 
61
60
  def config=(hash)
62
61
  self.client = nil
63
- super
62
+ @config = hash.symbolize_keys
63
+ end
64
+
65
+ def config
66
+ @config ||= {}
64
67
  end
65
68
 
66
- def load_config(config_file)
67
- self.config = YAML.load_file(File.expand_path config_file).with_indifferent_access[:ripple]
69
+ def load_configuration(config_file, config_keys = [:ripple])
70
+ config_file = File.expand_path(config_file)
71
+ config_hash = YAML.load(ERB.new(File.read(config_file)).result).with_indifferent_access
72
+ config_keys.each {|k| config_hash = config_hash[k]}
73
+ self.config = config_hash || {}
74
+ rescue Errno::ENOENT
75
+ raise Ripple::MissingConfiguration.new(config_file)
76
+ end
77
+ alias_method :load_config, :load_configuration
78
+ end
79
+
80
+ class MissingConfiguration < StandardError
81
+ include Translation
82
+ def initialize(file_path)
83
+ super(t("missing_configuration", :file => file_path))
68
84
  end
69
85
  end
70
86
  end
87
+
88
+ require 'ripple/railtie' if defined? Rails::Railtie
@@ -22,9 +22,12 @@ module Ripple
22
22
  autoload :One
23
23
  autoload :Many
24
24
  autoload :Embedded
25
+ autoload :Linked
25
26
  autoload :Instantiators
26
27
  autoload :OneEmbeddedProxy
27
28
  autoload :ManyEmbeddedProxy
29
+ autoload :OneLinkedProxy
30
+ autoload :ManyLinkedProxy
28
31
 
29
32
  module ClassMethods
30
33
  # @private
@@ -66,7 +69,7 @@ module Ripple
66
69
  value
67
70
  end
68
71
 
69
- if association.one?
72
+ unless association.many?
70
73
  define_method("#{name}?") do
71
74
  get_proxy(association).present?
72
75
  end
@@ -98,6 +101,7 @@ module Ripple
98
101
  end
99
102
 
100
103
  class Association
104
+ include Ripple::Translation
101
105
  attr_reader :type, :name, :options
102
106
 
103
107
  # association options :using, :class_name, :class, :extend,
@@ -107,6 +111,7 @@ module Ripple
107
111
  @type, @name, @options = type, name, options.to_options
108
112
  end
109
113
 
114
+ # @return String The class name of the associated object(s)
110
115
  def class_name
111
116
  @class_name ||= case
112
117
  when @options[:class_name]
@@ -120,38 +125,100 @@ module Ripple
120
125
  end
121
126
  end
122
127
 
128
+ # @return [Class] The class of the associated object(s)
123
129
  def klass
124
130
  @klass ||= options[:class] || class_name.constantize
125
131
  end
126
132
 
133
+ # @return [true,false] Is the cardinality of the association > 1
127
134
  def many?
128
135
  @type == :many
129
136
  end
130
137
 
138
+ # @return [true,false] Is the cardinality of the association == 1
131
139
  def one?
132
140
  @type == :one
133
141
  end
134
142
 
143
+ # @return [true,false] Is the associated class an EmbeddedDocument
135
144
  def embeddable?
136
145
  klass.embeddable?
137
146
  end
138
147
 
148
+ # TODO: Polymorphic not supported
149
+ # @return [true,false] Does the association support more than one associated class
139
150
  def polymorphic?
140
151
  false
141
152
  end
142
153
 
154
+ # @return [true,false] Does the association use links
155
+ def linked?
156
+ using == :linked
157
+ end
158
+
159
+ # @return [String] the instance variable in the owner where the association will be stored
143
160
  def ivar
144
161
  "@_#{name}"
145
162
  end
146
163
 
164
+ # @return [Class] the association proxy class
147
165
  def proxy_class
148
166
  @proxy_class ||= proxy_class_name.constantize
149
167
  end
150
168
 
169
+ # @return [String] the class name of the association proxy
151
170
  def proxy_class_name
152
- @using ||= options[:using] || (embeddable? ? :embedded : :link)
153
- klass_name = (many? ? 'Many' : 'One') + @using.to_s.camelize + ('Polymorphic' if polymorphic?).to_s + 'Proxy'
171
+ klass_name = (many? ? 'Many' : 'One') + using.to_s.camelize + ('Polymorphic' if polymorphic?).to_s + 'Proxy'
154
172
  "Ripple::Associations::#{klass_name}"
155
173
  end
174
+
175
+ # @return [Proc] a filter proc to be used with Enumerable#select for collecting links that belong to this association (only when #linked? is true)
176
+ def link_filter
177
+ linked? ? lambda {|link| link.tag == link_tag } : lambda {|_| false }
178
+ end
179
+
180
+ # @return [String,nil] when #linked? is true, the tag for outgoing links
181
+ def link_tag
182
+ linked? ? Array(link_spec).first.tag : nil
183
+ end
184
+
185
+ # @return [Riak::WalkSpec] when #linked? is true, a specification for which links to follow to retrieve the associated documents
186
+ def link_spec
187
+ # TODO: support transitive linked associations
188
+ if linked?
189
+ tag = name.to_s
190
+ bucket = polymorphic? ? '_' : klass.bucket_name
191
+ Riak::WalkSpec.new(:tag => tag, :bucket => bucket)
192
+ else
193
+ nil
194
+ end
195
+ end
196
+
197
+ # @return [Symbol] which method is used for representing the association - currently only supports :embedded and :linked
198
+ def using
199
+ @using ||= options[:using] || (embeddable? ? :embedded : :linked)
200
+ end
201
+
202
+ # @raise [ArgumentError] if the value does not match the class of the association
203
+ def verify_type!(value, owner)
204
+ unless type_matches?(value)
205
+ raise ArgumentError.new(t('invalid_association_value',
206
+ :name => name,
207
+ :owner => owner.inspect,
208
+ :klass => polymorphic? ? "<polymorphic>" : klass.name,
209
+ :value => value.inspect))
210
+ end
211
+ end
212
+
213
+ def type_matches?(value)
214
+ case
215
+ when polymorphic?
216
+ one? || Array === value
217
+ when many?
218
+ Array === value && value.all? {|d| (embeddable? && Hash === d) || klass === d }
219
+ when one?
220
+ value.nil? || (embeddable? && Hash === value) || klass === value
221
+ end
222
+ end
156
223
  end
157
224
  end
@@ -16,15 +16,27 @@ require 'ripple'
16
16
  module Ripple
17
17
  module Associations
18
18
  module Linked
19
-
20
- def create(attrs={})
21
- instantiate_target(:create, attrs)
19
+ def replace(value)
20
+ @reflection.verify_type!(value, @owner)
21
+ @owner.robject.links -= links
22
+ Array(value).compact.each do |doc|
23
+ doc.save if doc.new?
24
+ @owner.robject.links << doc.robject.to_link(@reflection.link_tag)
25
+ end
26
+ loaded
27
+ @target = value
22
28
  end
23
29
 
24
- def create!(attrs={})
25
- instantiate_target(:create!, attrs)
30
+ protected
31
+ def links
32
+ @owner.robject.links.select(&@reflection.link_filter)
26
33
  end
27
34
 
35
+ def robjects
36
+ @owner.robject.walk(*Array(@reflection.link_spec)).first || []
37
+ rescue
38
+ []
39
+ end
28
40
  end
29
41
  end
30
42
  end
@@ -33,13 +33,14 @@ module Ripple
33
33
  @target = []
34
34
  end
35
35
 
36
- def <<
37
- raise NotImplementedError
36
+ def <<(value)
37
+ raise NotImplementedError
38
38
  end
39
+
39
40
  alias_method :push, :<<
40
- alias_method :concat, :<<
41
+ alias_method :concat, :<<
41
42
 
42
- protected
43
+ protected
43
44
  def instantiate_target(instantiator, attrs={})
44
45
  doc = klass.send(instantiator, attrs)
45
46
  self << doc
@@ -21,12 +21,14 @@ module Ripple
21
21
 
22
22
  def <<(docs)
23
23
  load_target
24
+ @reflection.verify_type!(Array(docs), @owner)
24
25
  assign_references(docs)
25
26
  @target += Array(docs)
26
27
  self
27
28
  end
28
29
 
29
30
  def replace(docs)
31
+ @reflection.verify_type!(docs, @owner)
30
32
  @_docs = docs.map { |doc| attrs = doc.respond_to?(:attributes_for_persistence) ? doc.attributes_for_persistence : doc }
31
33
  assign_references(docs)
32
34
  reset
@@ -0,0 +1,35 @@
1
+ # Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ require 'ripple'
15
+
16
+ module Ripple
17
+ module Associations
18
+ class ManyLinkedProxy < Proxy
19
+ include Many
20
+ include Linked
21
+
22
+ def <<(value)
23
+ load_target
24
+ new_target = @target.concat(Array(value))
25
+ replace new_target
26
+ self
27
+ end
28
+
29
+ protected
30
+ def find_target
31
+ robjects.map {|robj| klass.send(:instantiate, robj) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -18,6 +18,10 @@ module Ripple
18
18
  module One
19
19
  include Instantiators
20
20
 
21
+ def to_a
22
+ [self]
23
+ end
24
+
21
25
  protected
22
26
  def instantiate_target(instantiator, attrs={})
23
27
  @target = klass.send(instantiator, attrs)
@@ -20,6 +20,7 @@ module Ripple
20
20
  include Embedded
21
21
 
22
22
  def replace(doc)
23
+ @reflection.verify_type!(doc, @owner)
23
24
  @_doc = doc.respond_to?(:attributes_for_persistence) ? doc.attributes_for_persistence : doc
24
25
  assign_references(doc)
25
26
  reset
@@ -33,7 +34,6 @@ module Ripple
33
34
  assign_references(doc)
34
35
  end
35
36
  end
36
-
37
37
  end
38
38
  end
39
39
  end