sander6-daijobu 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,6 +1,65 @@
1
1
  = Daijobu
2
2
 
3
- A library for easing serialization and deserialzation of data stuffed into Tokyo Cabinet, including flexible and fallback schemes. This will all make more sense once the documentation is written for real.
3
+ === "Oh no! Another key-value-store wrapper!"
4
+
5
+ Don't worry, Daijobu isn't about that. Rather, it wraps the serialization/deserialzation layer of accessing key-value stores, specifically Tokyo Cabinet, but I guess with some work it could work with other things.
6
+
7
+ === "So how's it work?"
8
+
9
+ Make a new, inappropriately-named Daijobu::Client object and give it your store (supported stores are MemCache, Rufus::Tokyo::Cabinet, and Rufus::Tokyo::Tyrant), and then the scheme or schemes you want to use for serialization.
10
+
11
+ rufus = Rufus::Tokyo::Cabinet.new('casket.tch')
12
+ daijobu = Daijobu::Client.new(rufus, :schemes => :json)
13
+
14
+ You can now use the regular hash getter and setter methods ([] and []=) without worrying about serialization.
15
+
16
+ The supported schemes are :marshal (using Marshal), :json (using JSON from the json gem), :yaml (using YAML), :eval (using Kernel::eval), and :raw (using nothing).
17
+
18
+ Because some schemes are very strict, and sometimes what gets dumped can't be parsed back (for example, unparsing an integer using JSON), you can specify a set of schemes. If one doesn't work, it'll try the next in the list until it succeeds in parsing.
19
+
20
+ Daijobu::Client.new(rufus, :schemes => [:json, :yaml, :eval])
21
+
22
+ Daijobu doesn't do any real format-checking; if, say, Marshal.load throws an error, for whatever reason, it'll just rescue and move on to the next scheme. The success of this gem all hinges on the fact that the supported serialization formats don't have much to do with one another (except for JSON and YAML, of course), and you shouldn't ever end up parsing Ruby code as JSON accidentally, or at least not in unintended ways.
23
+
24
+ You can set different read and write schemes, too. This would be useful, say, if you wanted to change the serialization format.
25
+
26
+ Daijobu::Client.new(rufus, :read => :json, :write => :raw)
27
+
28
+ One last cool thing that I threw in just because: the key names in a key-value store are often namespaced. You can call these namespaces as methods on a Client object to automatically prepend that namespace to any keys asked for.
29
+
30
+ daijobu = Daijobu::Client.new(rufus, :read => :json, :write => :raw)
31
+ daijobu.namespace['123'] # => looks for 'namespace:123'
32
+ daijobu.name.space['123'] # => looks for 'name:space:123'
33
+
34
+ You can set the separator (defaults to ':' because that's what I was using) on the Daijobu::NamespaceProxy class.
35
+
36
+ Daijobu::NamespaceProxy.default_separator = '/'
37
+ daijobu.name.space['123'] # => looks for 'name/space/123'
38
+
39
+ And a last bit of syntatical sugar: you can leave off the brackets when asking for namespaced keys.
40
+
41
+ daijobu.namespace '123' # => looks for 'namespace:123'
42
+ daijobu.name.space '123' # => looks for 'name:space:123'
43
+
44
+ === "Anything else I should know?"
45
+
46
+ Pretty much only the getting and setting APIs are implemented for the supported adapters, so fancier stuff like iterating over keys isn't there yet.
47
+
48
+ Multi-get is also kind of janky right now for MemCache, due to a flaw in the memcache-client gem that assumes Marshal.load'ing on get_multi. Keys are therefore fetched one at a time.
49
+
50
+ === "What does the future hold for Daijobu?"
51
+
52
+ I plan to eventually switch out my home-rolled adapters for a more complete and more-intelligently-written backend, like Moneta, but last time I checked it was broken and I needed this gem to do work right now.
53
+
54
+ Until then, I'm thinking of putting in support for user-defined adapters as a stopgap, basically any object that responds to #get(key) and #set(key, value).
55
+
56
+ Less of a priority is support for user-defined serialization schemes that respond to #parse(string) and #unparse(object).
57
+
58
+ I also plan to take a look at ActiveRecord::Base#serialize to see if I can do something with that, because reading and writing YAML is like dying every day.
59
+
60
+ === "What's with the name?"
61
+
62
+ It's my understanding that "daijobu" is Japanese for "I'm fine" or "It's okay". The hardest thing about making gems is deciding what to call them, and that's the first thing that popped into my head.
4
63
 
5
64
  == Copyright
6
65
 
@@ -1,6 +1,16 @@
1
1
  module Daijobu
2
+
3
+ # Daijobu::Adapter is the parent module of the various adapter classes.
2
4
  module Adapter
3
5
 
6
+ # Given an object, returns a new instance of the corresponding adapter based on the
7
+ # object's class.
8
+ #
9
+ # MemCache => Daijobu::Adapter::MemCacheAdapter
10
+ # Rufus::Tokyo::Cabinet => Daijobu::Adapter::TokyoCabinetAdapter
11
+ # Rufus::Tokyo::Tyrant => Daijobu::Adapter::TokyoTyrantAdapter
12
+ #
13
+ # Raises Daijobu::InvalidAdapter if given a object it doesn't know about.
4
14
  def self.get(casket)
5
15
  if defined?(MemCache) && casket.is_a?(MemCache)
6
16
  Daijobu::Adapter::MemCacheAdapter.new(casket)
@@ -2,12 +2,17 @@ module Daijobu
2
2
 
3
3
  module Adapter
4
4
 
5
+ # Daijobu::Adapter::MemCacheAdapter wraps getting and setting to a MemCache store.
6
+ # Note that you can use MemCache to talk to a Tokyo Tyrant server, since they
7
+ # speak the same language.
5
8
  class MemCacheAdapter
6
9
 
10
+ # Daijobu::Adapter::MemCacheAdapter.new takes a MemCache object.
7
11
  def initialize(store)
8
12
  @store = store
9
13
  end
10
14
 
15
+ # Gets the key or keys given. Multiple values will be returned in a hash.
11
16
  def get(*keys)
12
17
  if keys.size == 0
13
18
  nil
@@ -18,12 +23,14 @@ module Daijobu
18
23
  end
19
24
  end
20
25
 
26
+ # Sets the key to the given value (using MemCache#add).
21
27
  def set(key, value)
22
28
  @store.add(key, value, 0, true)
23
29
  end
24
30
 
25
31
  private
26
32
 
33
+ # Gets a single key. Used internally.
27
34
  def get_one(key)
28
35
  @store.get(key, true)
29
36
  end
@@ -2,12 +2,16 @@ module Daijobu
2
2
 
3
3
  module Adapter
4
4
 
5
+ # Daijobu::Adapter::TokyoCabinetAdapter wraps getting and setting to a Rufus::Tokyo::Cabinet store.
5
6
  class TokyoCabinetAdapter
6
7
 
8
+ # Daijobu::Adapter::TokyoCabinetAdapter.new takes a Rufus::Tokyo::Cabinet object.
7
9
  def initialize(store)
8
10
  @store = store
9
11
  end
10
12
 
13
+ # Gets the key or keys given, using Cabinet#[] or Cabinet#lget.
14
+ # Multiple values should be returned in a hash, but that's really up to the Cabinet object.
11
15
  def get(*keys)
12
16
  if keys.size == 0
13
17
  nil
@@ -18,6 +22,7 @@ module Daijobu
18
22
  end
19
23
  end
20
24
 
25
+ # Sets the key to the given value (using Cabinet#[]=).
21
26
  def set(key, value)
22
27
  @store[key] = value
23
28
  end
@@ -2,12 +2,16 @@ module Daijobu
2
2
 
3
3
  module Adapter
4
4
 
5
+ # Daijobu::Adapter::TokyoTyrantAdapter wraps getting and setting to a Rufus::Tokyo::Tyrant store.
5
6
  class TokyoTyrantAdapter
6
7
 
8
+ # Daijobu::Adapter::TokyoTyrantAdapter.new takes a Rufus::Tokyo::Tyrant object.
7
9
  def initialize(store)
8
10
  @store = store
9
11
  end
10
12
 
13
+ # Gets the key or keys given, using Tyrant#[] or Tyrant#lget.
14
+ # Multiple values should be returned in a hash, but that's really up to Tyrant object.
11
15
  def get(*keys)
12
16
  if keys.size == 0
13
17
  nil
@@ -18,6 +22,7 @@ module Daijobu
18
22
  end
19
23
  end
20
24
 
25
+ # Sets the key to the given value (using Tyrant#[]=).
21
26
  def set(key, value)
22
27
  @store[key] = value
23
28
  end
@@ -1,30 +1,57 @@
1
1
  module Daijobu
2
+
3
+ # The unfortunately-named Daijobu::Client is the serialization wrapper for key-value stores.
2
4
  class Client
3
5
 
6
+ # Client.new takes a key-value store as its first argument, and then a hash of serialization schemes.
7
+ #
8
+ # Options:
9
+ # :schemes => the scheme or schemes used to read and write, in order
10
+ # :read => the scheme or schemes used to read, in order. Trumped by :schemes
11
+ # :write => the scheme or schemes used to write, in order. Trumped by :schemes
4
12
  def initialize(casket, options = {})
5
13
  @adapter = Daijobu::Adapter.get(casket)
6
14
  @read_schemes = Daijobu::SchemeSet.new(options[:schemes] || options[:read])
7
15
  @write_schemes = Daijobu::SchemeSet.new(options[:schemes] || options[:write])
8
16
  end
9
17
 
18
+ # Getter for keys. The actual getting method is handled by the specific Adapter object.
19
+ # You can ask for multiple keys at once, in which case this returns a hash of key => value.
10
20
  def [](*keys)
11
21
  __parse__(@adapter.get(*keys.collect { |key| key.to_s }))
12
22
  end
13
23
 
24
+ # Setter for keys. The actual setting method is handled by the specific Adapter object.
14
25
  def []=(key, value)
15
26
  @adapter.set(key.to_s, __unparse__(value))
16
27
  end
17
28
 
29
+ # Any missing method is assumed to be a namespace for keys to get. The NamespaceProxy object
30
+ # returned also implements the same kind of method_missing, so namespaces can be chained together.
31
+ #
32
+ # client.namespace['key'] # => gets key 'namespace:key'
33
+ # client.name.space['key'] # => gets key 'name:space:key'
34
+ # client.namespace['key'] = 'value' # => sets key 'namespace:key'
35
+ #
36
+ # As an added bit of syntactic sugar, you can leave the brackets off when getting keys this way.
37
+ #
38
+ # client.namespace 'key' # => same as client.namespace['key']
39
+ #
40
+ # See NamespaceProxy for more details about setting the separator.
18
41
  def method_missing(name, *args)
19
42
  args.empty? ? Daijobu::NamespaceProxy.new(self, name) : Daijobu::NamespaceProxy.new(self, name)[*args]
20
43
  end
21
44
 
22
45
  private
23
46
 
47
+ # Tries to parse (load) the string using each of the reading schemes in order.
48
+ # The actual parsing is done by the specific Scheme object.
24
49
  def __parse__(str)
25
50
  @read_schemes.parse(str)
26
51
  end
27
52
 
53
+ # Tries to unparse (dump) the object using each of the writing schemes in order.
54
+ # The actual unparsing is done by the specific Scheme object.
28
55
  def __unparse__(obj)
29
56
  @write_schemes.unparse(obj)
30
57
  end
@@ -1,11 +1,17 @@
1
1
  module Daijobu
2
2
 
3
+ # The superclass of all the errors that Daijobu raises.
3
4
  class Error < StandardError; end
4
5
 
6
+ # Raised when asking for a scheme that doesn't exist.
7
+ # Supported schemes are :marshal, :json, :yaml, :eval, and :raw.
5
8
  class UnknownScheme < Daijobu::Error; end
6
9
 
10
+ # Raised when the key-value-store object doesn't have a appropriate adapter.
11
+ # Supported stores are MemCache, Rufus::Tokyo::Cabinet, and Rufus::Tokyo::Tyrant.
7
12
  class InvalidAdapter < Daijobu::Error; end
8
13
 
14
+ # Raised when all of the (un)parsing schemes fail.
9
15
  class NoFallbackScheme < Daijobu::Error; end
10
16
 
11
17
  end
@@ -2,31 +2,43 @@ module Daijobu
2
2
  class NamespaceProxy
3
3
 
4
4
  @@default_separator = ':'
5
+
6
+ # Getter for the default separator. Default is ':'
5
7
  def self.default_separator
6
8
  @@default_separator
7
9
  end
8
10
 
11
+ # Setter for the default separator. I use ':', but a lot of people like '/'.
9
12
  def self.default_separator=(separator)
10
13
  @@default_separator = separator
11
14
  end
12
-
15
+
16
+ # NamespaceProxy.new takes an owner (a Daijobu::Client), a namespace, and a separator (defaults
17
+ # to @@default_separator).
18
+ # NamespaceProxy objects are typically created using Daijobu::Client#method_missing, so you're
19
+ # rarely going to instantiate one of these on your own.
13
20
  def initialize(owner, namespace, separator = @@default_separator)
14
21
  @owner = owner
15
22
  @namespace = namespace.to_s
16
23
  @separator = separator
17
24
  end
18
-
25
+
26
+ # Sends #[] back to the owner, prepending the key given with the namespace and separator.
19
27
  def [](key)
20
28
  @owner["#{@namespace}#{@separator}#{key}"]
21
29
  end
22
-
30
+
31
+ # Sends #[]= back to the owner, prepending the key given with the namespace and separator.
23
32
  def []=(key, value)
24
33
  @owner["#{@namespace}#{@separator}#{key}"] = value
25
34
  end
26
-
35
+
36
+ # Any missing method is assumed to be yet another namespace.
27
37
  def method_missing(namespace, *args)
28
38
  separator = args.shift || @@default_separator
29
- Daijobu::NamespaceProxy.new(@owner, "#{@namespace}#{@separator}#{namespace}", separator)
39
+ @namespace = "#{@namespace}#{@separator}#{namespace}"
40
+ @separator = separator
41
+ self
30
42
  end
31
43
  end
32
44
  end
@@ -1,6 +1,17 @@
1
1
  module Daijobu
2
+
3
+ # The Scheme module is the parent of the various serialization schemes.
2
4
  module Scheme
3
5
 
6
+ # Given a name, returns a new instance of the corresponding scheme.
7
+ #
8
+ # :marshal => Daijobu::Scheme::Marshal
9
+ # :json => Daijobu::Scheme::JSON
10
+ # :yaml => Daijobu::Scheme::YAML
11
+ # :eval => Daijobu::Scheme::Eval
12
+ # :raw => Daijobu::Scheme::Raw
13
+ #
14
+ # Raises Daijobu::UnknownScheme if given a name it can't handle.
4
15
  def self.get(name)
5
16
  case name
6
17
  when :marshal
@@ -11,6 +22,8 @@ module Daijobu
11
22
  Daijobu::Scheme::YAML.new
12
23
  when :eval
13
24
  Daijobu::Scheme::Eval.new
25
+ when :raw
26
+ Daijobu::Scheme::Raw.new
14
27
  else
15
28
  raise Daijobu::UnknownScheme
16
29
  end
@@ -1,10 +1,14 @@
1
1
  module Daijobu
2
+
3
+ # A SchemeSet holds a bundle of schemes, and has logic for iterating over them.
2
4
  class SchemeSet
3
5
 
4
6
  DEFAULT = [ :marshal, :json, :yaml, :eval ]
5
7
 
6
8
  attr_reader :current
7
-
9
+
10
+ # SchemeSet.new takes a single symbol or array of symbols and initializes a new Scheme
11
+ # object of the appropriate type for each one.
8
12
  def initialize(schemes = nil)
9
13
  schemes = Array(schemes)
10
14
  schemes = DEFAULT if schemes.empty?
@@ -12,6 +16,11 @@ module Daijobu
12
16
  @current = 0
13
17
  end
14
18
 
19
+ # Returns the next scheme object in the stack. Raises Daijobu::NoFallbackScheme when there
20
+ # are no more schemes.
21
+ #
22
+ # And yes, I know it's kind of weird to call this method #next when the first invocation
23
+ # returns the first scheme, but it made sense at the time.
15
24
  def next
16
25
  scheme = @schemes[@current]
17
26
  raise NoFallbackScheme unless scheme
@@ -19,10 +28,15 @@ module Daijobu
19
28
  return scheme
20
29
  end
21
30
 
31
+ # Resets the stack of schemes, so that the next invocation of #next returns the first scheme
32
+ # (I know, I know).
22
33
  def reset
23
34
  @current = 0
24
35
  end
25
36
 
37
+ # Tries the parse (load) the string with each scheme in turn.
38
+ # Assumes (defensibly) that parsing failed if any non Daijobu::Error exceptions are raised
39
+ # and moves on to the next scheme.
26
40
  def parse(str)
27
41
  begin
28
42
  obj = self.next.parse(str)
@@ -37,6 +51,9 @@ module Daijobu
37
51
  obj
38
52
  end
39
53
 
54
+ # Tries the unparse (dump) the string with each scheme in turn.
55
+ # Assumes (defensibly) that unparsing failed if any non Daijobu::Error exceptions are raised
56
+ # and moves on to the next scheme.
40
57
  def unparse(obj)
41
58
  begin
42
59
  str = self.next.unparse(obj)
@@ -1,11 +1,17 @@
1
1
  module Daijobu
2
2
  module Scheme
3
+
4
+ # Daijobu::Scheme::Eval is the serialization for pure Ruby code.
5
+ # Theoretically, then, anything could be put into and taken out of a key-value-store,
6
+ # provided that they're always rehydrated into an appropriate binding.
3
7
  class Eval
4
8
 
9
+ # Parses by #eval'ing the string.
5
10
  def parse(str)
6
11
  str.nil? ? nil : eval(str)
7
12
  end
8
13
 
14
+ # Unparses by #inspect'ing the object.
9
15
  def unparse(obj)
10
16
  obj.inspect
11
17
  end
@@ -2,12 +2,21 @@ require 'json'
2
2
 
3
3
  module Daijobu
4
4
  module Scheme
5
+
6
+ # Daijobu::Scheme::JSON is the serialization for JSON.
7
+ # Uses the native (C) json gem, which is respectably fast.
5
8
  class JSON
6
9
 
10
+ # Parses the string using JSON.parse.
11
+ # JSON is pretty strict, and it dies whenever the object doesn't have an enclosing
12
+ # structure (i.e. an array or tuple). You might have problems parsing bare strings,
13
+ # integers, booleans, and nulls. It's weird, though, because JSON doesn't seem to have
14
+ # a problem unparsing these things. Just a heads-up.
7
15
  def parse(str)
8
16
  str.nil? ? nil : ::JSON.parse(str)
9
17
  end
10
18
 
19
+ # Unparses the object using JSON.unparse.
11
20
  def unparse(obj)
12
21
  ::JSON.unparse(obj)
13
22
  end
@@ -1,11 +1,15 @@
1
1
  module Daijobu
2
2
  module Scheme
3
+
4
+ # Daijobu::Scheme::Marshal is the serialization for Ruby binary code.
3
5
  class Marshal
4
6
 
7
+ # Parses the string using Marshal.load.
5
8
  def parse(str)
6
9
  str.nil? ? nil : ::Marshal.load(str)
7
10
  end
8
11
 
12
+ # Unparses the object using Marshal.dump.
9
13
  def unparse(obj)
10
14
  ::Marshal.dump(obj)
11
15
  end
@@ -1,11 +1,17 @@
1
1
  module Daijobu
2
2
  module Scheme
3
+
4
+ # Daijobu::Scheme::Raw is the serialization for nothing. Seriously, it doesn't do anything.
5
+ # It's rather dubious why you'd use this scheme for reading (if you're just going to be reading
6
+ # as raw, why use this gem at all?), but writing raw is useful sometimes.
3
7
  class Raw
4
-
8
+
9
+ # Doesn't parse anything. Just returns the string.
5
10
  def parse(str)
6
11
  str
7
12
  end
8
13
 
14
+ # Doesn't unparse anything. Just returns the object.
9
15
  def unparse(obj)
10
16
  obj
11
17
  end
@@ -2,12 +2,20 @@ require 'yaml'
2
2
 
3
3
  module Daijobu
4
4
  module Scheme
5
+
6
+ # Daijobu::Scheme::YAML is the serialization for YAML.
7
+ # Due to the strictness of the JSON module, you'll often have more luck parsing JSON containing
8
+ # bare objects (strings, integers, booleans, etc. without an enclosing structure) using YAML
9
+ # than with JSON, but YAML's a lot slower. That being said, it makes a good fallback scheme to use
10
+ # when JSON starts dying.
5
11
  class YAML
6
12
 
13
+ # Parses the string using YAML.load.
7
14
  def parse(str)
8
15
  str.nil? ? nil : ::YAML.load(str)
9
16
  end
10
17
 
18
+ # Unparses the object using YAML.dump.
11
19
  def unparse(obj)
12
20
  ::YAML.dump(obj)
13
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sander6-daijobu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sander Hartlage