rico 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -14,28 +14,6 @@ Add rico to your Gemfile and `bundle install`:
14
14
  gem "rico"
15
15
  ```
16
16
 
17
- ## Usage
18
-
19
- Instantiate a Rico object with a **bucket** and a **key** then perform operations.
20
-
21
- Here's an example of how to use a set to manage a list of followed users:
22
-
23
- ```ruby
24
- follows = Rico::Set.new "follows", @user.id
25
-
26
- follows.member? @other_user.id # => false
27
-
28
- follows.add @other_user.id
29
-
30
- follows.member? @other_user.id # => true
31
- follows.length # => 1
32
-
33
- follows.remove @other_user.id
34
-
35
- follows.member? @other_user.id # => false
36
- follows.length # => 0
37
- ```
38
-
39
17
  ## Configuration
40
18
 
41
19
  By default, Rico uses a generic Riak::Client instance for operations. You can specify your own options for the Riak client (perhaps inside of a rails initializer) like so:
@@ -55,15 +33,18 @@ Rico.configure do |c|
55
33
  end
56
34
  ```
57
35
 
58
- ## Data Types
36
+ ## Supported Data Types
59
37
 
60
38
  **Arrays** - sequence of values
61
39
 
62
40
  ```ruby
63
41
  a = Rico::Array.new "bucket", "key"
64
- a.add [3, 1, 1, 4, 2]
65
- a.members # => [3, 1, 1, 4, 2]
66
- a.length # => 5
42
+ a.add [3, 1, 1, 4, 2] # writes to riak
43
+ a.members # => [3, 1, 1, 4, 2]
44
+ a.length # => 5
45
+ a.remove [2, 4] # writes to riak
46
+ a.members # => [3, 1, 1]
47
+ a.raw_data # => '{"_type":"array","_values":[3,1,1],"_deletes":[2,4]}'
67
48
  ```
68
49
 
69
50
  **Lists** - sorted sequence of values
@@ -71,8 +52,7 @@ a.length # => 5
71
52
  ```ruby
72
53
  l = Rico::List.new "bucket", "key"
73
54
  l.add [3, 1, 1, 4, 2]
74
- l.members # => [1, 1, 2, 3, 4]
75
- l.length # => 5
55
+ l.members # => [1, 1, 2, 3, 4]
76
56
  ```
77
57
 
78
58
  **Sets** - unique sequence of values
@@ -80,8 +60,7 @@ l.length # => 5
80
60
  ```ruby
81
61
  s = Rico::Set.new "bucket", "key"
82
62
  s.add [3, 1, 1, 4, 2]
83
- s.members # => [3, 1, 4, 2]
84
- s.length # => 4
63
+ s.members # => [3, 1, 4, 2]
85
64
  ```
86
65
 
87
66
  **Sorted Sets** - unique, sorted sequence of values
@@ -89,8 +68,8 @@ s.length # => 4
89
68
  ```ruby
90
69
  s = Rico::SortedSet.new "bucket", "key"
91
70
  s.add [3, 1, 1, 4, 2]
92
- s.members # => [1, 2, 3, 4]
93
- s.length # => 4
71
+ s.members # => [1, 2, 3, 4]
72
+ s.reduce(&:+)
94
73
  ```
95
74
 
96
75
  **Maps** - key-value mappings
@@ -99,8 +78,7 @@ s.length # => 4
99
78
  m = Rico::Map.new "bucket", "key"
100
79
  m.add({"a" => 1})
101
80
  m.add({"b" => 2, "c" => 3})
102
- m.members # => {"a" => 1, "b" => 2, "c" => 3}
103
- m.length # => 3
81
+ m.members # => {"a"=>1, "b"=>2, "c"=>3}
104
82
  ```
105
83
 
106
84
  **Sorted Maps** - key-value mappings sorted by key
@@ -109,8 +87,8 @@ m.length # => 3
109
87
  m = Rico::SortedMap.new "bucket", "key"
110
88
  m.add({"b" => 2, "c" => 3})
111
89
  m.add({"a" => 1})
112
- m.members # => {"a" => 1, "b" => 2, "c" => 3}
113
- m.length # => 3
90
+ m.members # => {"a"=>1, "b"=>2, "c"=>3}
91
+ m.raw_data # => '{"_type":"smap","_values":{"a":1,"b":2,"c":3}}'
114
92
  ```
115
93
 
116
94
  **Capped Sorted Maps** - key-value mappings sorted by key and bound by size
@@ -119,19 +97,17 @@ m.length # => 3
119
97
  m = Rico::CappedSortedMap.new "bucket", "key", limit: 2
120
98
  m.add({"b" => 2, "c" => 3})
121
99
  m.add({"a" => 1})
122
- m.members # => {"b" => 2, "c" => 3}
123
- m.length # => 2
100
+ m.members # => {"b"=>2, "c"=>3}
101
+ m.length # => 2
124
102
  ```
125
103
 
126
104
  **Values** - generic serialized values
127
105
 
128
106
  ```ruby
129
107
  v = Rico::Value.new "bucket", "key"
130
- v.exists? # => false
131
- v.get # => nil
108
+ v.get # => nil
132
109
  v.set "bob"
133
- v.get # => "bob"
134
- v.exists? # => true
110
+ v.get # => "bob"
135
111
  ```
136
112
 
137
113
  ## Content Types
@@ -144,10 +120,43 @@ v.exists? # => true
144
120
  s = Rico::Set.new "bucket", "key"
145
121
  s.content_type = "application/x-gzip"
146
122
  s.add [1,2,3]
147
- s.get # => [1, 2, 3]
148
- s.raw_data # => "\u001F\x8B\b\u0000G...."
123
+ s.members # => [1, 2, 3]
124
+ s.data # => {"_type"=>"set", "_values"=>[1, 2, 3]}
125
+ s.raw_data # => "\u001F\x8B\b\u0000G...."
149
126
  ```
150
127
 
128
+ ## Under The Hood
129
+
130
+ Objects are stored in a simple map encoded to JSON.
131
+
132
+ **Array, List, Set and SortedSet** types look like this:
133
+
134
+ ```json
135
+ { "_type": "sset", "_values": [1,2,3], "_deletes": [4] }
136
+ ```
137
+
138
+ The *_deletes* field acts as a temporary tombstone for preserve deletes during conflict resolution. The field will normally contain the deletes processed during the last write, or the cumulative deletes processed during the last sibling merge. The value of _deletes is intentionally forgotten after a successful read and will not taint future operations.
139
+
140
+ Conflict resolution works by adding the difference between value arrays, removing deleted values and returning the result. This implementation is susceptible to sticky deletes - if different clients delete and add the same value simultenously the delete will ultimately win and the value is left out.
141
+
142
+ **Map, SortedMap and CappedSortedMap** types look like this:
143
+
144
+ ```json
145
+ { "_type": "csmap", "_values": {"a": 1, "b": 2, "c": 3}, "_deletes": ["d"] }
146
+ ```
147
+
148
+ The *_deletes* field for map objects contains only the keys deleted.
149
+
150
+ Conflict resolution works by merging value hashes, removing deleted keys and returning the result. This implementation is susceptible to sticky deletes - if different clients delete and add the same key simultenously the delete will ultimately win and the value is left out.
151
+
152
+ **Value** type looks like this:
153
+
154
+ ```json
155
+ { "_type": "value", "_value": "Hipsters love modern folk rock" }
156
+ ```
157
+
158
+ Conflict resolution is handled by last-write-wins.
159
+
151
160
  ## Notes
152
161
 
153
162
  ### Enumerable
@@ -158,10 +167,6 @@ Enumerable-looking types are indeed Enumerable
158
167
 
159
168
  Data is persisted at operation time. For example, List#add(5) will immediately update the record in Riak. It'd generally be wise to compute a list of values to be added or removed and then issue a single operation.
160
169
 
161
- ## TODO
162
-
163
- - Ability to provide custom sibling resolution callbacks
164
-
165
170
  ## Contributing
166
171
 
167
172
  1. Fork it
@@ -23,10 +23,11 @@ module Rico
23
23
  TYPES = {
24
24
  "Array" => "array",
25
25
  "List" => "list",
26
- "Map" => "map",
27
- "Set" => "set",
28
26
  "SortedSet" => "sset",
27
+ "Set" => "set",
28
+ "Map" => "map",
29
29
  "SortedMap" => "smap",
30
+ "CappedSortedMap" => "csmap",
30
31
  "Value" => "value"
31
32
  }
32
33
 
@@ -7,7 +7,9 @@ module Rico
7
7
  #
8
8
  # Returns the data in the object as an array
9
9
  def members
10
- Array((data || {})["_values"])
10
+ assert_type(::Array) do
11
+ data["_values"] || []
12
+ end
11
13
  end
12
14
 
13
15
  # Resolve conflict between one or more RObject siblings
@@ -35,18 +37,38 @@ module Rico
35
37
 
36
38
  protected
37
39
 
40
+ # Constructs a document map for the given operation
41
+ #
42
+ # items - Items to add to members
43
+ #
44
+ # Returns a Hash representing the document map
38
45
  def build_map_add(items)
39
46
  { "_type" => type_key, "_values" => compute_add(items) }
40
47
  end
41
48
 
49
+ # Constructs a document map for the given operation
50
+ #
51
+ # items - Items to remove to members
52
+ #
53
+ # Returns a Hash representing the document map
42
54
  def build_map_remove(items)
43
55
  { "_type" => type_key, "_values" => compute_remove(items), "_deletes" => items }
44
56
  end
45
57
 
58
+ # Add items to our member array
59
+ #
60
+ # items - Items to add
61
+ #
62
+ # Returns an Array of the resulting addition
46
63
  def compute_add(items)
47
64
  members + Array(items)
48
65
  end
49
66
 
67
+ # Remove items from our member array
68
+ #
69
+ # items - Items to remove. Can be an array of values or a single object
70
+ #
71
+ # Returns an Array of the new object
50
72
  def compute_remove(items)
51
73
  members - Array(items)
52
74
  end
@@ -7,7 +7,9 @@ module Rico
7
7
  #
8
8
  # Returns the data in the object as an array
9
9
  def members
10
- ((data || {})["_values"] || {})
10
+ assert_type(::Hash) do
11
+ data["_values"] || {}
12
+ end
11
13
  end
12
14
 
13
15
  # Resolve conflict between one or more RObject siblings
@@ -29,19 +31,39 @@ module Rico
29
31
 
30
32
  protected
31
33
 
34
+ # Constructs a document map for the given operation
35
+ #
36
+ # items - Items to add to members
37
+ #
38
+ # Returns a Hash representing the document map
32
39
  def build_map_add(items)
33
40
  { "_type" => type_key, "_values" => compute_add(items) }
34
41
  end
35
42
 
43
+ # Constructs a document map for the given operation
44
+ #
45
+ # items - Items to remove to members
46
+ #
47
+ # Returns a Hash representing the document map
36
48
  def build_map_remove(items)
37
49
  keys = extract_keys(items)
38
50
  { "_type" => type_key, "_values" => compute_remove(items), "_deletes" => keys }
39
51
  end
40
52
 
53
+ # Add items to our member hash
54
+ #
55
+ # items - Items to add
56
+ #
57
+ # Returns a Hash of the resulting merge
41
58
  def compute_add(items)
42
59
  members.merge(items)
43
60
  end
44
61
 
62
+ # Remove items from our member hash
63
+ #
64
+ # items - Items to remove. Can be a hash, array of keys or single key
65
+ #
66
+ # Returns a Hash of the new object
45
67
  def compute_remove(items)
46
68
  keys = extract_keys(items)
47
69
  members.delete_if {|k,v| keys.include? k.to_s }
@@ -2,7 +2,7 @@ module Rico
2
2
  module Object
3
3
  extend Forwardable
4
4
 
5
- def_delegators :riak_object, :conflict?, :content_type, :content_type=, :data, :delete, :store, :raw_data
5
+ def_delegators :riak_object, :conflict?, :content_type, :content_type=, :delete, :store, :raw_data
6
6
 
7
7
  attr_accessor :bucket, :key
8
8
 
@@ -17,6 +17,19 @@ module Rico
17
17
  options.each {|k,v| send("#{k}=", v)}
18
18
  end
19
19
 
20
+ # Retrieves data from Riak
21
+ #
22
+ # Raises an error on type mismatch
23
+ def data
24
+ result = riak_object.data || {}
25
+
26
+ if result["_type"] && (result["_type"] != type_key)
27
+ raise TypeError, "#{@bucket}:#{@key} expected type to be #{type_key}, got #{result["_type"]}"
28
+ end
29
+
30
+ result
31
+ end
32
+
20
33
  # Sets a new value on the object and stores it
21
34
  #
22
35
  # value - new value to set
@@ -36,11 +49,33 @@ module Rico
36
49
 
37
50
  protected
38
51
 
52
+ # Determines an appropriate type key for the object
53
+ #
54
+ # Returns a String
39
55
  def type_key
40
56
  name = self.class.name.split("::").last
41
57
  Rico::TYPES[name]
42
58
  end
43
59
 
60
+ # Verifies the result of a given block is of a given type.
61
+ #
62
+ # klass - Class to test type
63
+ # block - Block to call that produces value for test
64
+ #
65
+ # Returns the result of the block on success, raises a TypeError on failure
66
+ def assert_type(klass, &block)
67
+ value = block.call
68
+
69
+ unless value.class == klass
70
+ raise TypeError, "#{@bucket}:#{@key} expected value to be #{klass.name}, got #{value.class}"
71
+ end
72
+
73
+ value
74
+ end
75
+
76
+ # Instantiates and memoizes a Riak::RObject instance for our bucket and key
77
+ #
78
+ # Returns the Riak::RInstance object
44
79
  def riak_object
45
80
  @riak_object ||= Rico.bucket(@bucket).get_or_new @key
46
81
  end
@@ -5,14 +5,18 @@ module Rico
5
5
  # Gets the value of the object
6
6
  #
7
7
  # Returns the deserialized value
8
- alias_method :get, :data
8
+ def get
9
+ data["_value"]
10
+ end
9
11
 
10
12
  # Sets and stores the new value for the object
11
13
  #
12
14
  # value - the new value to store
13
15
  #
14
16
  # Returns the result of the store operation
15
- alias_method :set, :mutate
17
+ def set(value)
18
+ mutate build_map(value)
19
+ end
16
20
 
17
21
  # Sets the value if it does not exist
18
22
  #
@@ -36,9 +40,22 @@ module Rico
36
40
  #
37
41
  # Returns a single RObject result or nil
38
42
  def self.resolve(robject)
39
- obj = Riak::RObject.new(robject.bucket, robject.key)
40
- obj.data = robject.siblings.first.data
43
+ winner = robject.siblings.sort {|a,b| b.last_modified <=> a.last_modified }.first
44
+
45
+ obj = robject.dup
46
+ obj.siblings = [winner]
41
47
  obj
42
48
  end
49
+
50
+ protected
51
+
52
+ # Constructs a document map for the new value
53
+ #
54
+ # value - Value to include in map
55
+ #
56
+ # Returns a Hash representing the document map
57
+ def build_map(value)
58
+ { "_type" => type_key, "_value" => value }
59
+ end
43
60
  end
44
61
  end
@@ -1,3 +1,3 @@
1
1
  module Rico
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -97,6 +97,13 @@ describe Rico::Array do
97
97
  end
98
98
 
99
99
  describe "#members" do
100
+ it "asserts value type as array" do
101
+ a = Rico::Map.new RiakHelpers.bucket, "array_members_assert_type"
102
+ a.add({"a" => 1})
103
+ b = Rico::Array.new RiakHelpers.bucket, "array_members_assert_type"
104
+ lambda { b.members }.should raise_error(TypeError)
105
+ end
106
+
100
107
  it "returns a list of members" do
101
108
  a = Rico::Array.new RiakHelpers.bucket, "array_members_lists"
102
109
  a.add [{"usd" => 123.41, "cad" => 61.89}, "Bears", "Beets", "Battlestar Galactica", 3.14159, 71]
@@ -85,6 +85,15 @@ describe Rico::Map do
85
85
  end
86
86
  end
87
87
 
88
+ describe "#members" do
89
+ it "asserts value type as hash" do
90
+ a = Rico::Array.new RiakHelpers.bucket, "map_members_assert_type"
91
+ a.add [1,2,3]
92
+ b = Rico::Map.new RiakHelpers.bucket, "map_members_assert_type"
93
+ lambda { b.members }.should raise_error(TypeError)
94
+ end
95
+ end
96
+
88
97
  describe "#length" do
89
98
  it "returns zero for an empty list" do
90
99
  a = Rico::Map.new RiakHelpers.bucket, "map_length_empty"
@@ -25,7 +25,7 @@ describe Rico::Value do
25
25
  a = Rico::Value.new RiakHelpers.bucket, "value_gzip_content_type"
26
26
  a.content_type = "application/x-gzip"
27
27
  a.set "JOHN DOE"
28
- gunzip(a.raw_data).should eql "\"JOHN DOE\""
28
+ gunzip(a.raw_data).should eql({"_type" => "value", "_value" => "JOHN DOE"}.to_json)
29
29
  a.content_type.should eql "application/x-gzip"
30
30
  end
31
31
 
@@ -108,23 +108,27 @@ describe Rico::Value do
108
108
  end
109
109
 
110
110
  describe ".resolve" do
111
- it "just returns the first sibling" do
112
- datas = ["Tom", "Jerry"]
113
- conflicted = RiakHelpers.build_conflicted_robject "value_resolve_simple", datas
114
- result = Rico::Value.resolve(conflicted)
115
- result.data.should eql "Tom"
116
- end
117
-
118
- it "properly deletes deleted values after resolve" do
111
+ it "just returns the last modified sibling" do
119
112
  datas = [
120
- { "_type" => "array", "_values" => [1,2,3,4] },
121
- { "_type" => "array", "_values" => [1,2,3], "_deletes" => [4] }
113
+ { "_type" => "value", "_value" => "Oldest" },
114
+ { "_type" => "value", "_value" => "Middle" },
115
+ { "_type" => "value", "_value" => "Newest" },
116
+ { "_type" => "value", "_value" => "Middle" },
117
+ { "_type" => "value", "_value" => "Middle" }
122
118
  ]
123
- conflicted = RiakHelpers.build_conflicted_robject "array_resolve_delete", datas
124
- result = Rico::Array.resolve(conflicted)
125
- result.data["_values"].should eql [1,2,3]
126
- result.data["_deletes"].should eql [4]
119
+ times = [
120
+ Time.utc(2012, 10, 10, 10, 10, 10),
121
+ Time.utc(2013, 10, 10, 10, 10, 10),
122
+ Time.utc(2014, 10, 10, 10, 10, 10),
123
+ Time.utc(2013, 10, 10, 10, 10, 10),
124
+ Time.utc(2013, 10, 10, 10, 10, 10)
125
+ ]
126
+ conflicted = RiakHelpers.build_conflicted_robject "value_resolve_simple", datas
127
+ conflicted.siblings.each_with_index do |s, i|
128
+ s.last_modified = times[i]
129
+ end
130
+ result = Rico::Value.resolve(conflicted)
131
+ result.data.should eql({ "_type" => "value", "_value" => "Newest" })
127
132
  end
128
133
  end
129
-
130
134
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rico
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: