riaktor 0.0.1 → 0.0.2

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/History.txt CHANGED
@@ -1,4 +1,10 @@
1
- === 0.0.1 2010-03-15
1
+ === 0.0.1 2010-03-18
2
2
 
3
3
  * 1 major enhancement:
4
4
  * Initial release
5
+
6
+ === 0.0.2 2010-03-18
7
+
8
+ * 1 major enhancement:
9
+ * Fixed the Manfiest file to include everything
10
+ * Escape slashes in bucket and key names
data/Manifest.txt CHANGED
@@ -1,9 +1,21 @@
1
1
  History.txt
2
+ lib/riaktor/client.rb
3
+ lib/riaktor/document/association.rb
4
+ lib/riaktor/document/associations.rb
5
+ lib/riaktor/document/attribute.rb
6
+ lib/riaktor/document/attributes.rb
7
+ lib/riaktor/document/base.rb
8
+ lib/riaktor/document/index.rb
9
+ lib/riaktor/document/indexes.rb
10
+ lib/riaktor/document.rb
11
+ lib/riaktor/r_object/link.rb
12
+ lib/riaktor/r_object/persistence.rb
13
+ lib/riaktor/r_object.rb
14
+ lib/riaktor.rb
2
15
  Manifest.txt
3
16
  PostInstall.txt
4
- README.rdoc
5
17
  Rakefile
6
- lib/riaktor.rb
18
+ README.rdoc
7
19
  script/console
8
20
  script/destroy
9
21
  script/generate
@@ -0,0 +1,137 @@
1
+ module Riaktor
2
+ class Client
3
+ class MissingOperationKey < RuntimeError; end
4
+
5
+ def self.config
6
+ @config ||= { :host => "127.0.0.1", :port => 8098, :prefix => 'riak' }
7
+ end
8
+
9
+ def self.config=(hash)
10
+ @config = hash
11
+ end
12
+
13
+ def self.client_id
14
+ Thread.current[:riaktor_client_id] ||=UUIDTools::UUID.random_create.to_s
15
+ end
16
+
17
+ def self.client_id=(val)
18
+ Thread.current[:riaktor_client_id] = val
19
+ end
20
+
21
+ def self.max_concurrency
22
+ @max_concurrency ||= 25
23
+ end
24
+
25
+ def self.max_concurrency=(val)
26
+ @max_concurrency = val
27
+ end
28
+
29
+ def self.build_url(bucket,key=nil)
30
+ url = []
31
+ url << "http://#{self.config[:host]}:#{self.config[:port]}"
32
+ url << self.config[:prefix]
33
+ url << URI.escape(bucket).gsub("/", "%2F")
34
+ url << URI.escape(key).gsub("/", "%2F") if key
35
+ url.join("/")
36
+ end
37
+
38
+ def self.bucket_props(bucket,params={})
39
+ params[:keys] ||= false
40
+ response = Typhoeus::Request.get(self.build_url(bucket), :params => params)
41
+ Yajl::Parser.parse(response.body)
42
+ end
43
+
44
+ def self.set_bucket_props(bucket, props, params={})
45
+ body = Yajl::Encoder.encode(props)
46
+ response = Typhoeus::Request.put(self.build_url(bucket),
47
+ :params => params, :headers => { "Content-Type" => "application/json" }, :body => body)
48
+ response.code == 204
49
+ end
50
+
51
+ def self.put(op)
52
+ self.put_many([op])[[op["bucket"],op["key"]]]
53
+ end
54
+
55
+ def self.put_many(operations)
56
+ operations.each do |operation|
57
+ operation['bucket'] || raise(MissingOperationKey, "bucket")
58
+ operation['key'] || raise(MissingOperationKey, "key")
59
+ operation['value'] ||= ""
60
+ operation['headers'] ||= {}
61
+ operation['params'] ||= {}
62
+ end
63
+ results = {}
64
+ hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
65
+ operations.each do |op|
66
+ op['params']["r"] ||= 2
67
+ op['params']["w"] ||= 2
68
+ op['params']["dw"] ||= 0
69
+ op['params']["returnbody"] ||= true
70
+ op['headers']["Content-Type"] ||= "text/plain"
71
+ op['headers']["X-Riak-ClientId"] ||= self.client_id
72
+ request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
73
+ :method => :put, :headers => op['headers'], :params => op['params'], :body => op['value'].to_s)
74
+ request.on_complete do |response|
75
+ results[[op["bucket"],op["key"]]] = { "headers" => response.headers_hash,
76
+ "body" => response.body, "code" => response.code }
77
+ end
78
+ hydra.queue request
79
+ end # operations.each
80
+ hydra.run; results
81
+ end # self.put_many
82
+
83
+ def self.get(op)
84
+ self.get_many([op])[[op["bucket"],op["key"]]]
85
+ end
86
+
87
+ def self.get_many(operations)
88
+ operations.each do |operation|
89
+ operation['bucket'] || raise(MissingOperationKey, "bucket")
90
+ operation['key'] || raise(MissingOperationKey, "key")
91
+ operation['headers'] ||= {}
92
+ operation['params'] ||= {}
93
+ end
94
+ results = {}
95
+ hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
96
+ operations.each do |op|
97
+ op['params']["r"] ||= 2
98
+ op['headers']["Content-Type"] ||= "text/plain"
99
+ op['headers']["X-Riak-ClientId"] ||= self.client_id
100
+ request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
101
+ :method => :get, :headers => op['headers'], :params => op['params'])
102
+ request.on_complete do |response|
103
+ results[[op["bucket"], op["key"]]] = { "headers" => response.headers_hash,
104
+ "body" => response.body, "code" => response.code }
105
+ end
106
+ hydra.queue request
107
+ end # operations.each
108
+ hydra.run; results
109
+ end # self.get_many
110
+
111
+ def self.delete(op)
112
+ self.delete_many([op])[[op["bucket"],op["key"]]]
113
+ end
114
+
115
+ def self.delete_many(operations)
116
+ operations.each do |operation|
117
+ operation['bucket'] || raise(MissingOperationKey, "bucket")
118
+ operation['key'] || raise(MissingOperationKey, "key")
119
+ operation['headers'] ||= {}
120
+ operation['params'] ||= {}
121
+ end
122
+ results = {}
123
+ hydra = Typhoeus::Hydra.new(:max_concurrency => self.max_concurrency)
124
+ operations.each do |op|
125
+ op['headers']["X-Riak-ClientId"] ||= self.client_id
126
+ request = Typhoeus::Request.new(self.build_url(op['bucket'],op['key']),
127
+ :method => :delete, :headers => op['headers'], :params => op['params'])
128
+ request.on_complete do |response|
129
+ results[[op["bucket"], op["key"]]] = { "headers" => response.headers_hash,
130
+ "body" => response.body, "code" => response.code }
131
+ end
132
+ hydra.queue request
133
+ end # operations.each
134
+ hydra.run; results
135
+ end # self.delete_many
136
+ end # Client
137
+ end
@@ -0,0 +1,123 @@
1
+ module Riaktor
2
+ module Document
3
+ class Association
4
+ attr_accessor :name, :opts, :parent
5
+
6
+ def initialize(name, opts, parent)
7
+ raise(ArgumentError, "missing or invalid name") if name.blank?
8
+ raise(ArgumentError, "missing or invalid option :class") if opts.blank? or opts[:class].blank?
9
+ raise(ArgumentError, "missing or invalid parent") if parent.blank?
10
+ @name = name
11
+ @opts = opts
12
+ @parent = parent
13
+ end
14
+
15
+ def klass
16
+ @klass ||= eval(opts[:class].to_s.camelize)
17
+ end
18
+
19
+ def reload
20
+ @links = nil
21
+ @members = nil
22
+ @sort_index = nil
23
+ @include = nil
24
+ @cached_doc = nil
25
+ end
26
+
27
+ def changed?
28
+ @changed == true
29
+ end
30
+
31
+ def links
32
+ @links ||= begin
33
+ (@parent.links || []).select { |l| l.tag.to_s == @name.to_s }
34
+ end
35
+ end
36
+
37
+ def include?(key_or_doc)
38
+ @include ||= {}
39
+ @include[key_or_doc] ||= begin
40
+ if key_or_doc.is_a?(String)
41
+ links.any? { |l| l.key == key_or_doc }
42
+ else
43
+ links.any? { |l| l.key == key_or_doc.key }
44
+ end
45
+ end
46
+ end
47
+
48
+ def members(from=0,to=-1)
49
+ @members ||= {}
50
+ @members[[from..to]] ||= begin
51
+ if links.empty?
52
+ []
53
+ else
54
+ if range = links[from..to] and !range.empty?
55
+ found = range.collect do |link|
56
+ cached_doc(link.key) || link.key
57
+ end
58
+ uncached = found.select { |f| f.is_a?(String) }
59
+ unless uncached.empty?
60
+ klass.find_many(uncached).each do |key,doc|
61
+ cache_doc(key,doc)
62
+ found[found.index(doc.key)] = doc
63
+ end
64
+ end
65
+ found.sort! { |x,y| sort_index.index(x.key) <=> sort_index.index(y.key) }
66
+ else
67
+ []
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def <<(doc)
74
+ @changed = true
75
+ unless doc.class.to_s == klass.to_s
76
+ raise(ArgumentError, "invalid object class: #{doc.class.to_s}")
77
+ end
78
+ return true if members.include?(doc)
79
+ link = Riaktor::RObject::Link.new(doc.bucket, doc.key, @name.to_s)
80
+ @parent.links << link
81
+ reload
82
+ true
83
+ end
84
+
85
+ def remove(doc)
86
+ @changed = true
87
+ @parent.links.reject! { |l| l.bucket == doc.bucket && l.key == doc.key }
88
+ reload
89
+ true
90
+ end
91
+
92
+ def save
93
+ if res = @parent.save
94
+ @changed = false
95
+ end; res
96
+ end
97
+
98
+ protected
99
+
100
+ def cached_doc(key)
101
+ (@cached_doc || {})[key]
102
+ end
103
+
104
+ def cache_doc(key,doc)
105
+ @cached_doc ||= {}
106
+ @cached_doc[key] = doc
107
+ end
108
+
109
+ def sort_index
110
+ @sort_index ||= links.collect { |l| l.key }
111
+ end
112
+
113
+ def add_to_sort_index(key)
114
+ sort_index
115
+ @sort_index << key unless @sort_index.include?(key)
116
+ end
117
+
118
+ def remove_from_sort_index(key)
119
+ sort_index && @sort_index.delete(key)
120
+ end
121
+ end # Attribute
122
+ end # Document
123
+ end # Riaktor
@@ -0,0 +1,34 @@
1
+ require "#{File.dirname(__FILE__)}/association"
2
+
3
+ module Riaktor
4
+ module Document
5
+ module Associations
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def defined_associations
13
+ @defined_associations || {}
14
+ end
15
+
16
+ def association(name,opts={})
17
+ raise(ArgumentError, "missing key :class") unless opts[:class]
18
+ @defined_associations ||= {}
19
+ @defined_associations[name] = opts
20
+ define_method("#{name}") do
21
+ @associations ||= {}
22
+ @associations[name] ||= Association.new(name,opts,self)
23
+ end # define_method
24
+ end # def association
25
+ end # ClassMethods
26
+
27
+ module InstanceMethods
28
+ def any_associations_changed?
29
+ (@associations || {}).any? { |k,v| v.changed? }
30
+ end
31
+ end
32
+ end # Attributes
33
+ end # Document
34
+ end # Riaktor
@@ -0,0 +1,13 @@
1
+ module Riaktor
2
+ module Document
3
+ class Attribute < RObject
4
+ attr_accessor :parent
5
+
6
+ def save(opts={})
7
+ if self.parent && self.parent.valid?
8
+ super(opts)
9
+ end
10
+ end
11
+ end # Attribute
12
+ end # Document
13
+ end # Riaktor
@@ -0,0 +1,79 @@
1
+ require "#{File.dirname(__FILE__)}/attribute"
2
+
3
+ module Riaktor
4
+ module Document
5
+ module Attributes
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def defined_attributes
13
+ @defined_attributes || {}
14
+ end
15
+
16
+ def attribute(name,opts={})
17
+ @defined_attributes ||= {}
18
+ @defined_attributes[name] = opts
19
+ define_method("#{name}") do
20
+ load_attributes && @attributes[name]
21
+ end # define_method
22
+ end # def attribute
23
+ end # ClassMethods
24
+
25
+ module InstanceMethods
26
+ def key_for_attribute(name)
27
+ [self.key, "attribute", name].join("--")
28
+ end
29
+
30
+ def attributes_key_to_name_map
31
+ @attributes_key_to_name_map ||= begin
32
+ map = {}
33
+ self.class.defined_attributes.each do |name, opts|
34
+ map[key_for_attribute(name)] = name
35
+ end; map
36
+ end
37
+ end
38
+
39
+ def any_attributes_in_conflict?
40
+ load_attributes
41
+ @attributes.each do |name, attrib|
42
+ return true if attrib.in_conflict?
43
+ end; false
44
+ end
45
+
46
+ def load_attributes(force=false)
47
+ return true if @attributes and !force
48
+ @attributes = {}
49
+ return true if attributes_key_to_name_map.keys.empty?
50
+ results = Riaktor::Document::Attribute.find_many(
51
+ attributes_key_to_name_map.keys, :bucket => self.bucket)
52
+ results.each do |att_key, doc|
53
+ doc ||= Riaktor::Document::Attribute.new(:key => att_key, :bucket => self.bucket, :new => true)
54
+ doc.parent = self
55
+ name = attributes_key_to_name_map[att_key]
56
+ @attributes[name] = doc
57
+ end
58
+ @attributes
59
+ end # load_attributes
60
+
61
+ def save_attributes
62
+ return true unless @attributes
63
+ attributes_key_to_name_map.each do |att_key, att_name|
64
+ if attrib = @attributes[att_name]
65
+ attrib.save
66
+ end
67
+ end; true
68
+ end # save_attributes
69
+
70
+ def destroy_attributes
71
+ load_attributes
72
+ @attributes.each do |name, attrib|
73
+ attrib.destroy unless attrib.new_record?
74
+ end; true
75
+ end # destroy_attributes
76
+ end # InstanceMethods
77
+ end # Attributes
78
+ end # Document
79
+ end # Riaktor
@@ -0,0 +1,124 @@
1
+ require "#{File.dirname(__FILE__)}/attributes"
2
+ require "#{File.dirname(__FILE__)}/associations"
3
+ require "#{File.dirname(__FILE__)}/indexes"
4
+
5
+ module Riaktor
6
+ module Document
7
+ class Base < RObject
8
+ include Attributes
9
+ include Indexes
10
+ include Associations
11
+ include ActiveModel::Validations
12
+ extend ActiveModel::Callbacks
13
+
14
+ attr_accessor :links
15
+
16
+ define_model_callbacks :save, :destroy
17
+
18
+ validates_presence_of :key
19
+
20
+ def self.bucket
21
+ self.to_s.underscore
22
+ end
23
+
24
+ def self.find(key,opts={})
25
+ super(key,opts.merge(:bucket => self.bucket))
26
+ end
27
+
28
+ def self.find_many(keys,opts={})
29
+ super(keys,opts.merge(:bucket => self.bucket))
30
+ end
31
+
32
+ def self.save_many(docs)
33
+ results = {}
34
+ docs.uniq.compact.in_groups_of(Riaktor::Client.max_concurrency) do |doc_group|
35
+ threads = []
36
+ doc_group.compact.each do |doc|
37
+ threads << Thread.new do
38
+ begin
39
+ results[doc] = doc.save
40
+ rescue Exception => e
41
+ results[doc] = e
42
+ end
43
+ end # Thread.new
44
+ end # doc_group.each
45
+ threads.each { |th| th.join }
46
+ end; results
47
+ end
48
+
49
+ def self.destroy_many(docs)
50
+ results = {}
51
+ docs.uniq.compact.in_groups_of(Riaktor::Client.max_concurrency) do |doc_group|
52
+ threads = []
53
+ doc_group.compact.each do |doc|
54
+ threads << Thread.new do
55
+ begin
56
+ results[doc] = doc.destroy
57
+ rescue Exception => e
58
+ results[doc] = e
59
+ end
60
+ end # Thread.new
61
+ end # doc_group.each
62
+ threads.each { |th| th.join }
63
+ end; results
64
+ end
65
+
66
+ def bucket
67
+ self.class.bucket
68
+ end
69
+
70
+ def in_conflict?
71
+ super || any_attributes_in_conflict?
72
+ end
73
+
74
+ def changed?
75
+ super || any_associations_changed?
76
+ end
77
+
78
+ alias :ancestor_save :save
79
+
80
+ def save(opts={})
81
+ return false unless valid?
82
+
83
+ if new_record?
84
+ if existing = self.class.find(self.key)
85
+ errors[:key] << "is not unique"
86
+ return false
87
+ end
88
+ end
89
+
90
+ _run_save_callbacks do
91
+ if self.links and !self.links.empty?
92
+ opts[:links] = self.links
93
+ end
94
+
95
+ if ancestor_save(opts)
96
+ save_attributes
97
+ save_indexes
98
+ end
99
+ end; true
100
+ end
101
+
102
+ alias :ancestor_destroy :destroy
103
+
104
+ def destroy(opts={})
105
+ return false if new_record?
106
+ _run_destroy_callbacks do
107
+ if ancestor_destroy(opts)
108
+ destroy_attributes
109
+ destroy_indexes
110
+ end
111
+ end; true
112
+ end
113
+
114
+ def read_attribute_for_validation(name)
115
+ case name.to_sym
116
+ when :key, :value
117
+ self.send(name.to_sym)
118
+ else
119
+ self.send(name.to_sym).value
120
+ end
121
+ end
122
+ end # Base
123
+ end # Document
124
+ end # Riaktor
@@ -0,0 +1,13 @@
1
+ module Riaktor
2
+ module Document
3
+ class Index < RObject
4
+ attr_accessor :parent
5
+
6
+ def save(opts={})
7
+ if self.parent && self.parent.valid?
8
+ super(opts)
9
+ end
10
+ end
11
+ end # Attribute
12
+ end # Document
13
+ end # Riaktor
@@ -0,0 +1,104 @@
1
+ require "#{File.dirname(__FILE__)}/index"
2
+
3
+ module Riaktor
4
+ module Document
5
+ module Indexes
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def defined_indexes
13
+ @defined_indexes || {}
14
+ end
15
+
16
+ def index(attrib_name, opts={})
17
+ @defined_indexes ||= {}
18
+ @defined_indexes[attrib_name] = opts
19
+ end
20
+
21
+ def key_for_index(name,value,opts={})
22
+ ["indexes",name,Digest::SHA1.hexdigest(self.encode_value(value))].join("--")
23
+ end
24
+
25
+ def indexes_bucket
26
+ [self.bucket, "indexes"].join("--")
27
+ end
28
+
29
+ def find_with_index(name, value, opts={})
30
+ key = key_for_index(name,value)
31
+ if index = Riaktor::Document::Index.find(key, {:bucket => indexes_bucket}.merge(opts))
32
+ self.find(index.value)
33
+ end
34
+ end
35
+ end # ClassMethods
36
+
37
+ module InstanceMethods
38
+ def indexes_key_to_name_map
39
+ @indexes_key_to_name_map ||= begin
40
+ map = {}
41
+ self.class.defined_indexes.each do |name, opts|
42
+ map[key_for_index(name,opts)] = name
43
+ end; map
44
+ end
45
+ end
46
+
47
+ def save_indexes
48
+ load_attributes
49
+ Array(self.class.defined_indexes).in_groups_of(Riaktor::Client.max_concurrency).each do |defined_indexes_group|
50
+ threads = []
51
+ defined_indexes_group.each do |name_and_opts|
52
+ next unless name_and_opts
53
+ name = name_and_opts[0]
54
+ opts = name_and_opts[1]
55
+ attrib = @attributes[name]
56
+ next unless attrib.changed?
57
+ threads << Thread.new do
58
+ if !attrib.new_record? and attrib.orig_value
59
+ # delete old index
60
+ index_key = self.class.key_for_index(name,attrib.orig_value)
61
+ if index = Riaktor::Document::Index.find(index_key, :bucket => self.class.indexes_bucket)
62
+ index.parent = self
63
+ index.destroy
64
+ end
65
+ end
66
+ if attrib.value
67
+ # save new index
68
+ index_key = self.class.key_for_index(name,attrib.value)
69
+ index = Riaktor::Document::Index.new(:key => index_key, :bucket => self.class.indexes_bucket, :value => self.key)
70
+ index.parent = self
71
+ index.save
72
+ end
73
+ end # Thread.new do
74
+ end # defined_indexes_group.each
75
+ threads.each { |th| th.join }
76
+ end; true
77
+ end # save_indexes
78
+
79
+ def destroy_indexes
80
+ load_attributes
81
+ Array(self.class.defined_indexes).in_groups_of(Riaktor::Client.max_concurrency).each do |defined_indexes_group|
82
+ threads = []
83
+ defined_indexes_group.each do |name_and_opts|
84
+ next unless name_and_opts
85
+ name = name_and_opts[0]
86
+ opts = name_and_opts[1]
87
+ attrib = @attributes[name]
88
+ next unless attrib.orig_value
89
+ threads << Thread.new do
90
+ index_key = self.class.key_for_index(name,attrib.orig_value)
91
+ if index = Riaktor::Document::Index.find(index_key, :bucket => self.class.indexes_bucket)
92
+ index.parent = self
93
+ index.destroy
94
+ end
95
+ end # Thread.new do
96
+ end # defined_indexes_group.each
97
+ threads.each { |th| th.join }
98
+ end; true
99
+ end # destroy_indexes
100
+
101
+ end # InstanceMethods
102
+ end # Attributes
103
+ end # Document
104
+ end # Riaktor
@@ -0,0 +1,6 @@
1
+ module Riaktor
2
+ module Document
3
+ end # Document
4
+ end # Riaktor
5
+
6
+ require "#{File.dirname(__FILE__)}/document/base"
@@ -0,0 +1,44 @@
1
+ module Riaktor
2
+ class RObject
3
+ class Link
4
+ attr_accessor :bucket, :key, :tag
5
+
6
+ def self.from_header(link_header)
7
+ return [] if link_header.nil?
8
+ links = []
9
+ link_header.split(",").each do |link_str|
10
+ url, tag = link_str.split(";")
11
+ url = url.gsub("<","").gsub(">","").strip.split("/")
12
+ bucket = url[-2]
13
+ key = url[-1]
14
+ tag = tag.split("=")[1].gsub("\"","").strip
15
+ next if tag == "up"
16
+ links << new(bucket,key,tag)
17
+ end; links
18
+ end
19
+
20
+ def self.to_header(links)
21
+ links.reject! { |l| l.tag == "up" }
22
+ (links.collect { |l| l.to_s }).uniq.join(",")
23
+ end
24
+
25
+ def initialize(bucket, key, tag)
26
+ @bucket = bucket
27
+ @key = key
28
+ @tag = tag
29
+ end
30
+
31
+ def ==(obj)
32
+ obj.is_a?(Link) && obj.bucket == self.bucket && obj.key == self.key && obj.tag == self.tag
33
+ end
34
+
35
+ def to_s
36
+ "</riak/#{bucket}/#{key}>; riaktag=\"#{tag}\""
37
+ end
38
+
39
+ def to_doc(klass)
40
+ klass.find(key, :bucket => bucket)
41
+ end
42
+ end # Link
43
+ end # RObject
44
+ end # Riaktor
@@ -0,0 +1,155 @@
1
+ require "#{File.dirname(__FILE__)}/link"
2
+
3
+ module Riaktor
4
+ class RObject
5
+ module Persistence
6
+ class SaveError < RuntimeError; end
7
+ class DestroyError < RuntimeError; end
8
+
9
+ def self.included(base)
10
+ base.send :extend, ClassMethods
11
+ base.send :include, InstanceMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ def find(key,opts={})
16
+ find_many([key],opts)[key]
17
+ end
18
+
19
+ def find_many(keys,opts={})
20
+ raise(ArgumentError, "missing :bucket") unless opts[:bucket]
21
+ raise(ArgumentError, "empty keys") if keys.nil? or keys.empty?
22
+
23
+ operations = []
24
+ keys.each do |key|
25
+ op = {}
26
+ op["bucket"] = opts[:bucket]
27
+ op["key"] = key
28
+ op["params"] = { "r" => opts["r"] || 2 }
29
+ operations << op
30
+ end # keys.each
31
+
32
+ found = {}
33
+ Riaktor::Client.get_many(operations).each do |key_bucket_pair, resp|
34
+ bucket = key_bucket_pair[0]
35
+ key = key_bucket_pair[1]
36
+ case resp["code"]
37
+ when 200 then
38
+ found[key] = new(:key => key,
39
+ :bucket => bucket,
40
+ :value => decode_value(resp["body"]),
41
+ :vector_clock => resp["headers"]["X-Riak-Vclock"],
42
+ :links => Link.from_header(resp["headers"]["Link"]),
43
+ :new => false)
44
+ when 300 then
45
+ found[key] = new(:key => key,
46
+ :bucket => bucket,
47
+ :value => nil,
48
+ :vector_clock => resp["headers"]["X-Riak-Vclock"],
49
+ :links => Link.from_header(resp["headers"]["Link"]),
50
+ :new => false)
51
+ vtags = resp["body"].split("\n")[1..-1]
52
+ found[key].load_siblings(vtags,{"r" => opts["r"] || 2})
53
+ else
54
+ found[key] = nil
55
+ end
56
+ end # Riaktor.get_many
57
+
58
+ found
59
+ end
60
+
61
+ def decode_value(val)
62
+ case self.encoder
63
+ when :json
64
+ Yajl::Parser.parse(val)
65
+ when :yaml
66
+ YAML.load(val)
67
+ end
68
+ end
69
+
70
+ def encode_value(val)
71
+ case self.encoder
72
+ when :json
73
+ Yajl::Encoder.encode(val)
74
+ when :yaml
75
+ YAML.dump(val)
76
+ end
77
+ end
78
+ end # ClassMethods
79
+
80
+ module InstanceMethods
81
+ def load_siblings(vtags,params={})
82
+ params["r"] ||= 2
83
+ @siblings = {}
84
+
85
+ vtags.each do |vtag|
86
+ resp = Riaktor::Client.get({ "bucket" => self.bucket, "key" => self.key,
87
+ "params" => { "vtag" => vtag }.merge(params) })
88
+ if resp["code"] == 200
89
+ @siblings[vtag] = self.class.decode_value(resp["body"])
90
+ end
91
+ end
92
+
93
+ @siblings
94
+ end
95
+
96
+ def resolved!
97
+ @siblings = nil; true
98
+ end
99
+
100
+ def in_conflict?
101
+ !@siblings.nil?
102
+ end
103
+
104
+ def save(opts={})
105
+ raise(SaveError, "cannot save with siblings present") if self.in_conflict?
106
+ if changed? or opts[:force] == true
107
+ op = {}
108
+ op["bucket"] = self.bucket
109
+ op["key"] = self.key
110
+ op["value"] = self.class.encode_value(self.value)
111
+ op["params"] = { "dw" => opts[:dw] || 0, "w" => opts[:w] || 2, "r" => opts[:r] || 2 }
112
+ op["headers"] = { "X-Riak-Vclock" => self.vector_clock } if self.vector_clock
113
+ op["headers"]["Link"] = Link.to_header(opts[:links]) if opts[:links]
114
+ resp = Riaktor::Client.put(op)
115
+ if [204,404].include?(resp["code"])
116
+ resp = Riaktor::Client.get(op)
117
+ end
118
+ case resp["code"]
119
+ when 200 then
120
+ @links = Link.from_header(resp["headers"]["Link"])
121
+ @vector_clock = resp["headers"]["X-Riak-Vclock"]
122
+ @new = false
123
+ when 300 then
124
+ @links = Link.from_header(resp["headers"]["Link"])
125
+ @vector_clock = resp["headers"]["X-Riak-Vclock"]
126
+ @new = false
127
+ vtags = resp["body"].split("\n")[1..-1]
128
+ load_siblings(vtags,{"r" => op["params"]["r"]})
129
+ else
130
+ raise(SaveError, resp["code"])
131
+ end # case
132
+ end
133
+ true
134
+ end
135
+
136
+ def destroy(opts={})
137
+ raise(DestroyError, "cannot destroy with siblings present") if self.in_conflict?
138
+ raise(DestroyError, "cannot destroy unsaved document") if self.new_record?
139
+ op = {}
140
+ op["bucket"] = self.bucket
141
+ op["key"] = self.key
142
+ op["params"] = { "dw" => opts[:dw] || 0, "w" => opts[:w] || 2, "r" => opts[:r] || 2 }
143
+ op["headers"] = { "X-Riak-Vclock" => self.vector_clock } if self.vector_clock
144
+ resp = Riaktor::Client.delete(op)
145
+ case resp["code"]
146
+ when 204 then
147
+ @deleted = true
148
+ else
149
+ raise(DestroyError, resp["code"])
150
+ end # case
151
+ end
152
+ end # InstanceMethods
153
+ end # Persistence
154
+ end # Document
155
+ end # Riaktor
@@ -0,0 +1,46 @@
1
+ require "#{File.dirname(__FILE__)}/r_object/persistence"
2
+
3
+ module Riaktor
4
+ class RObject
5
+ include Persistence
6
+
7
+ attr_accessor :key, :bucket, :value, :orig_value, :links, :siblings, :vector_clock, :deleted
8
+
9
+ def self.encoder
10
+ @encoder || :yaml
11
+ end
12
+
13
+ def self.encoder=(val)
14
+ @encoder = val
15
+ end
16
+
17
+ def initialize(opts={})
18
+ @key = opts[:key]
19
+ @value = opts[:value]
20
+ @bucket = opts[:bucket]
21
+ @siblings = opts[:siblings]
22
+ @links = opts[:links] || []
23
+ @new = opts[:new].nil? ? true : opts[:new]
24
+ @orig_value = @new == true ? nil : @value.dup rescue nil
25
+ @vector_clock = opts[:vector_clock]
26
+ end
27
+
28
+ alias :id :key
29
+
30
+ def ==(obj)
31
+ self.class.to_s == obj.class.to_s && self.key == obj.key
32
+ end
33
+
34
+ def changed?
35
+ @value != @orig_value
36
+ end
37
+
38
+ def new_record?
39
+ @new == true
40
+ end
41
+
42
+ def destroyed?
43
+ @deleted == true
44
+ end
45
+ end # RObject
46
+ end # Riaktor
data/lib/riaktor.rb CHANGED
@@ -17,7 +17,7 @@ gem "activemodel", '= 3.0.0.beta'
17
17
  require "active_model"
18
18
 
19
19
  module Riaktor
20
- VERSION = '0.0.1'
20
+ VERSION = '0.0.2'
21
21
  end
22
22
 
23
23
  require "#{File.dirname(__FILE__)}/riaktor/client"
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 1
9
- version: 0.0.1
8
+ - 2
9
+ version: 0.0.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Ben Myles
@@ -146,11 +146,23 @@ extra_rdoc_files:
146
146
  - PostInstall.txt
147
147
  files:
148
148
  - History.txt
149
+ - lib/riaktor/client.rb
150
+ - lib/riaktor/document/association.rb
151
+ - lib/riaktor/document/associations.rb
152
+ - lib/riaktor/document/attribute.rb
153
+ - lib/riaktor/document/attributes.rb
154
+ - lib/riaktor/document/base.rb
155
+ - lib/riaktor/document/index.rb
156
+ - lib/riaktor/document/indexes.rb
157
+ - lib/riaktor/document.rb
158
+ - lib/riaktor/r_object/link.rb
159
+ - lib/riaktor/r_object/persistence.rb
160
+ - lib/riaktor/r_object.rb
161
+ - lib/riaktor.rb
149
162
  - Manifest.txt
150
163
  - PostInstall.txt
151
- - README.rdoc
152
164
  - Rakefile
153
- - lib/riaktor.rb
165
+ - README.rdoc
154
166
  - script/console
155
167
  - script/destroy
156
168
  - script/generate