riaktor 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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