rrod 0.0.1 → 1.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/README.md +33 -18
  4. data/lib/rrod/all.rb +24 -1
  5. data/lib/rrod/caster/nested_model.rb +16 -0
  6. data/lib/rrod/caster.rb +34 -0
  7. data/lib/rrod/cli.rb +9 -2
  8. data/lib/rrod/configuration.rb +40 -0
  9. data/lib/rrod/model/attribute.rb +80 -0
  10. data/lib/rrod/model/attribute_methods.rb +85 -0
  11. data/lib/rrod/model/collection.rb +37 -0
  12. data/lib/rrod/model/finders.rb +59 -0
  13. data/lib/rrod/model/persistence.rb +35 -0
  14. data/lib/rrod/model/schema.rb +30 -0
  15. data/lib/rrod/model/serialization.rb +12 -0
  16. data/lib/rrod/model.rb +38 -0
  17. data/lib/rrod/query.rb +30 -0
  18. data/lib/rrod/test_server/rspec.rb +50 -0
  19. data/lib/rrod/test_server/runner.rb +56 -0
  20. data/lib/rrod/test_server.rb +77 -0
  21. data/lib/rrod/version.rb +2 -3
  22. data/rrod.gemspec +7 -1
  23. data/spec/rrod/caster_spec.rb +117 -0
  24. data/spec/rrod/configuration_spec.rb +55 -0
  25. data/spec/rrod/model/attribute_methods_spec.rb +64 -0
  26. data/spec/rrod/model/attribute_spec.rb +102 -0
  27. data/spec/rrod/model/collection_spec.rb +39 -0
  28. data/spec/rrod/model/finders_spec.rb +78 -0
  29. data/spec/rrod/model/persistence_spec.rb +43 -0
  30. data/spec/rrod/model/schema_spec.rb +59 -0
  31. data/spec/rrod/model/serialization_spec.rb +17 -0
  32. data/spec/rrod/model/validations_spec.rb +50 -0
  33. data/spec/rrod/model_spec.rb +50 -0
  34. data/spec/rrod/query_spec.rb +34 -0
  35. data/spec/rrod_spec.rb +4 -0
  36. data/spec/spec_helper.rb +4 -0
  37. data/spec/support/models/car.rb +3 -0
  38. data/spec/support/models/person.rb +41 -0
  39. data/spec/support/test_server.yml.example +17 -0
  40. metadata +106 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 483487fdba1ca64ce64a80010c2c9381ae9e471a
4
- data.tar.gz: b6be998e8cbbcd06e65ed0b89b7114465d00727c
3
+ metadata.gz: 54fca12a0dd242e700196f2689c7e0c10b20f238
4
+ data.tar.gz: a218efee402307b755b0e681d1872d231d619771
5
5
  SHA512:
6
- metadata.gz: f2a3a7e0d98d410a876392c8121f9b5a3ec5537b2a6a265ed159ebb88a438275f22b44296f5ec6c076b309683672641568461baf6ca07ff44a6de3adae3cb6b7
7
- data.tar.gz: c29f1826f9aaf76b048cbe286e2c3434c48f38b955f6ae3030735a7b2422c06d178a1145547be4f62cce952940c4efd8b4acfdc8a59d36ce08e3021557bfad92
6
+ metadata.gz: e4d07847ef76659034a92ca8dcc2711dd47af332dd8e525a411a226885df9fc6de90542fb155d289a941fd89236d6d14bf7466a852db9f5d347d0c8096584abc
7
+ data.tar.gz: 48e8aca8812277c91321f8120785d399a2c9d3a8c4cbca7732c24ad8a0cff4abab4a9a75af0f395b1414af69f813c7ebf2f1d88faf9ad46663e9fef84223b9e4
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  .config
5
5
  .ruby-gemset
6
6
  .ruby-version
7
+ .tmp
7
8
  .yardoc
8
9
  Gemfile.lock
9
10
  InstalledFiles
@@ -14,6 +15,7 @@ lib/bundler/man
14
15
  pkg
15
16
  rdoc
16
17
  spec/reports
18
+ spec/support/test_server.yml
17
19
  test/tmp
18
20
  test/version_tmp
19
21
  tmp
data/README.md CHANGED
@@ -1,36 +1,51 @@
1
+ <!--- TODO use consistent capitalization with Rrod and Riak as well as backticks-->
1
2
  # Rrod
2
3
  ### Riak ruby object database.
3
4
  Rrod lets you use the super awesome database, riak, to model and persist your
4
5
  ruby objects.
5
6
 
6
- # Riak
7
- First, you must make sure you have Riak installed. See:
8
- http://docs.basho.com/riak/latest/quickstart/#Install-Riak.
9
- These guys have extremely good docs
10
- and will usually answer your questions in IRC #riak.
7
+ ## Riak Installation
8
+ First, you must make sure you
9
+ [have Riak installed](http://docs.basho.com/riak/latest/quickstart/#Install-Riak).
10
+ These guys have extremely good docs and will usually answer your questions
11
+ in freenode IRC #riak. You must also make sure you have
12
+ [enabled search](http://docs.basho.com/riak/latest/ops/advanced/configs/search/)
13
+ for your Riak installation.
11
14
 
12
- ## Installation
15
+ ## Rrod Installation
13
16
  Add `rrod` to your Gemfile and run `bundle` or execute `gem install rrod`.
14
17
 
15
- # Usage
16
- Once you have riak and rrod installed, make sure you know the port of one of your
17
- riak nodes. Usually `8098`.
18
+ ## Usage
19
+ Once you have Riak and Rrod installed you can begin to play. `Rrod` will
20
+ connect to Riak on localhost using protocol buffers on port 8087. What you
21
+ say? Don't worry about it.
18
22
 
19
- In terminal, run:
23
+ Next, if you installed Rrod by adding it to your Gemfile, run
20
24
 
21
- $ rrod irb
25
+ ```
26
+ $ bundle exec rrod pry
27
+ ```
22
28
 
23
- This will set you up with an irb environment with rrod loaded. Then run the
24
- following commands.
29
+ Otherwise, if you installed using `gem install rrod`, run:
30
+
31
+ ```
32
+ $ rrod pry
33
+ ```
34
+
35
+ This will set you up with an pry environment with `Rrod` loaded.
36
+ Next you will wave your magic wand and enter the following incantations:
25
37
 
26
38
  ```ruby
27
- Car = Class.new(Rrod::Document)
28
- car = {wheels: 4, color: :black, make: 'Jeep'}
29
- Car.persist(car)
30
- @car = Car.first
31
- @car.color
39
+ Car = Class.new { include Rrod::Model }
40
+ car = Car.new(wheels: 5, color: 'black', make: 'Jeep')
41
+ car.save
42
+ @car = Car.find_first_by make: 'Jeep'
43
+ @car.wheels # => 5
32
44
  ```
33
45
 
46
+ `Rrod` lets you have arbitrary attributes on your ruby objects and persist and
47
+ retreive them from Riak.
48
+
34
49
  ## Contributing
35
50
 
36
51
  1. Fork it
data/lib/rrod/all.rb CHANGED
@@ -1 +1,24 @@
1
- require 'rrod/version'
1
+ require 'active_model'
2
+ require 'active_support/concern'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+ require 'active_support/core_ext/hash/keys'
5
+ require 'active_support/core_ext/string/conversions'
6
+ require 'active_support/core_ext/string/inflections'
7
+ require 'active_support/core_ext/string/starts_ends_with'
8
+ require 'american_date'
9
+ require 'bigdecimal'
10
+ require 'riak'
11
+
12
+ require 'rrod/version' # first
13
+ require 'rrod/configuration'
14
+ require 'rrod/caster'
15
+ require 'rrod/caster/nested_model'
16
+ require 'rrod/model/attribute'
17
+ require 'rrod/model/attribute_methods'
18
+ require 'rrod/model/collection'
19
+ require 'rrod/model/finders'
20
+ require 'rrod/model/persistence'
21
+ require 'rrod/model/schema'
22
+ require 'rrod/model/serialization'
23
+ require 'rrod/model'
24
+ require 'rrod/query'
@@ -0,0 +1,16 @@
1
+ module Rrod
2
+ class Caster
3
+ module NestedModel
4
+
5
+ def rrod_cast(values)
6
+ return if values.nil?
7
+ Rrod::Model::Collection.new(values.map { |value| model.rrod_cast(value) })
8
+ end
9
+
10
+ def model
11
+ first
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ module Rrod
2
+ class Caster
3
+
4
+ def initialize(&casting)
5
+ @casting = casting
6
+ end
7
+
8
+ def rrod_cast(value)
9
+ return if value.nil?
10
+ @casting.(value)
11
+ end
12
+
13
+ module Boolean
14
+ extend self
15
+ def rrod_cast(value)
16
+ [nil, false, 'false', 0, '0'].include?(value) ? false : true
17
+ end
18
+ end
19
+
20
+ BigDecimal = new { |value| ::BigDecimal.new(value.to_s) }
21
+ Date = new { |value| value.to_date }
22
+ DateTime = new { |value| value.to_datetime }
23
+ Float = new { |value| value.to_f }
24
+ Integer = new { |value| value.to_i }
25
+ Numeric = new { |value|
26
+ float_value = value.to_f
27
+ int_value = value.to_i
28
+ float_value == int_value ? int_value : float_value
29
+ }
30
+ String = new { |value| value.to_s }
31
+ Symbol = new { |value| value.to_sym }
32
+ Time = new { |value| value.to_time }
33
+ end
34
+ end
data/lib/rrod/cli.rb CHANGED
@@ -1,16 +1,23 @@
1
1
  require 'thor'
2
+ require 'rrod'
2
3
 
3
4
  module Rrod
4
5
  class Cli < Thor
5
6
  package_name "Rrod"
6
7
 
7
8
  map "-i" => :pry
9
+ map "-t" => :test
8
10
 
9
11
  desc "pry", "start an interactive ruby session with Rrod loaded"
10
12
  def pry
11
13
  require 'pry'
12
- require 'rrod'
13
- Class.new { pry }
14
+ TOPLEVEL_BINDING.pry
15
+ end
16
+
17
+ desc "test", "start the Rrod::TestServer"
18
+ def test
19
+ require 'rrod/test_server/runner'
20
+ Rrod::TestServer::Runner.run
14
21
  end
15
22
  end
16
23
  end
@@ -0,0 +1,40 @@
1
+ module Rrod
2
+ module Config
3
+ def configure
4
+ yield configuration if block_given?
5
+ end
6
+
7
+ def configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+ end
11
+
12
+ extend Config
13
+
14
+ class Configuration
15
+ attr_accessor :client, :http_port, :pb_port, :protocol,
16
+ :test_server_yml, :test_server_search_startup_timeout, :nodes
17
+
18
+ def initialize
19
+ @protocol = 'pbc'
20
+ @test_server_yml = File.expand_path('spec/support/test_server.yml', '.')
21
+ @test_server_search_startup_timeout = 40 # because it's a test
22
+ end
23
+
24
+ def client
25
+ @client ||= Riak::Client.new(client_options)
26
+ end
27
+
28
+ private
29
+
30
+ def client_options
31
+ attributes = %w[http_port pb_port protocol nodes]
32
+ attributes.inject({}) do |acc, method|
33
+ acc.tap { |hash|
34
+ value = public_send(method)
35
+ hash[method.to_sym] = value unless value.nil?
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,80 @@
1
+ module Rrod
2
+ module Model
3
+ class Attribute
4
+
5
+ def self.reader_definition(name)
6
+ -> { read_attribute name }
7
+ end
8
+
9
+ def self.writer_definition(name)
10
+ ->(value) { write_attribute name, value }
11
+ end
12
+
13
+ attr_accessor :model, :name, :type, :options, :default
14
+
15
+ # @param [Class] The class that is declaring the attribute
16
+ # @param [Symbol] The name of the attribute
17
+ # @param [Class] The type of the attribute for marshalling
18
+ # @param [Hash] Options for the attribute (they are optional).
19
+ def initialize(model, name, type, options={})
20
+ self.model = model
21
+ self.name = name.to_sym
22
+ self.type = type
23
+ self.default = options.delete(:default)
24
+ self.options = options
25
+ end
26
+
27
+ # @return [Object] default value or result of call on default value
28
+ def default
29
+ @default.respond_to?(:call) ? @default.call : @default
30
+ end
31
+
32
+ def define
33
+ define_reader
34
+ define_writer
35
+ apply_validators
36
+ self
37
+ end
38
+
39
+ def cast(value)
40
+ caster = type.respond_to?(:rrod_cast) ? type : rrod_caster
41
+ caster ? caster.rrod_cast(value) : value
42
+ end
43
+
44
+ private
45
+
46
+ def name=(value)
47
+ @name = value.to_sym
48
+ end
49
+
50
+ def type=(value)
51
+ @type = value
52
+ @type.extend(Rrod::Caster::NestedModel) if nested_model?
53
+ end
54
+
55
+ def nested_model?
56
+ type.is_a?(Array) and type.first.ancestors.include?(Rrod::Model)
57
+ end
58
+
59
+ def define_reader
60
+ model.send :define_method, name, self.class.reader_definition(name)
61
+ end
62
+
63
+ def define_writer
64
+ model.send :define_method, "#{name}=", self.class.writer_definition(name)
65
+ end
66
+
67
+ def apply_validators
68
+ model.validates name, options if options.present?
69
+ end
70
+
71
+ def rrod_caster
72
+ "Rrod::Caster::#{type}".constantize
73
+ rescue NameError
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # stand-in for true/false attribute types
80
+ Boolean = Module.new unless defined? ::Boolean
@@ -0,0 +1,85 @@
1
+ module Rrod
2
+ module Model
3
+ module AttributeMethods
4
+
5
+ def initialize(attributes = {})
6
+ @attributes = {}
7
+ self.magic_methods = attributes.keys
8
+ self.attributes = attributes
9
+ end
10
+
11
+ def id
12
+ read_attribute :id
13
+ end
14
+
15
+ def id=(value)
16
+ write_attribute :id, value
17
+ end
18
+
19
+ # Returns a new hash with all of the object's attributes.
20
+ # @return [Hash] the object's attributes
21
+ def attributes
22
+ @attributes.keys.inject({}) { |acc, key|
23
+ acc.tap { |hash| hash[key] = respond_to?(key) ? public_send(key) : nil }
24
+ }
25
+ end
26
+
27
+ # Mass assign the attributes of the object.
28
+ # @param [Hash] the attributes to mass assign
29
+ def attributes=(attrs)
30
+ attrs.each do |key, value|
31
+ public_send "#{key}=", value
32
+ end
33
+ end
34
+
35
+ # Read a single attribute of the object.
36
+ # @param [Symbol, String] the key of the attribute to read
37
+ # @return the attribute at the given key
38
+ def read_attribute(key)
39
+ @attributes[key.to_s]
40
+ end
41
+ alias :[] :read_attribute
42
+
43
+ # Write a given value to the attributes for a given key.
44
+ # @param [Symbol, String] the key of the attribute to write
45
+ # @param the value to write to attributes
46
+ def write_attribute(key, value)
47
+ @attributes[key.to_s] = self.class.cast_attribute(key, value)
48
+ end
49
+ alias :[]= :write_attribute
50
+
51
+ private
52
+
53
+ def magic_methods=(keys)
54
+ return if self.class.schema? # classes with attributes don't get magic methods
55
+ @magic_methods = keys.inject([]) { |acc, k| acc << k.to_s << "#{k}=" }
56
+ end
57
+
58
+ def magic_methods
59
+ @magic_methods || []
60
+ end
61
+
62
+ def define_singleton_reader(attribute)
63
+ define_singleton_method attribute, Attribute.reader_definition(attribute)
64
+ end
65
+
66
+ def define_singleton_writer(attribute)
67
+ define_singleton_method "#{attribute}=", Attribute.writer_definition(attribute)
68
+ end
69
+
70
+ def method_missing(method_id, *args, &block)
71
+ method = method_id.to_s
72
+ return super unless magic_methods.include?(method)
73
+
74
+ accessor = method.ends_with?('=') ? :writer : :reader
75
+ send "define_singleton_#{accessor}", method.chomp('=')
76
+ send method_id, *args
77
+ end
78
+
79
+ def respond_to_missing?(*args)
80
+ magic_methods.include? args.first.to_s
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,37 @@
1
+ module Rrod
2
+ module Model
3
+ class Collection
4
+ include Enumerable
5
+
6
+ delegate(*%w[clear count each length size], to: :collection)
7
+
8
+ def initialize(collection=[])
9
+ self.collection = collection
10
+ end
11
+
12
+ def collection=(collection)
13
+ raise InvalidCollectionTypeError.new unless collection.respond_to?(:each)
14
+ collection.map { |member| push member }
15
+ rescue InvalidMemberTypeError => e
16
+ clear and raise e
17
+ end
18
+
19
+ def collection
20
+ @collection ||= []
21
+ end
22
+
23
+ def push(value)
24
+ raise InvalidMemberTypeError.new unless Rrod::Model === (value)
25
+ collection.push(value)
26
+ end
27
+ alias :<< :push
28
+
29
+ def serializable_hash(*)
30
+ collection.map(&:serializable_hash)
31
+ end
32
+
33
+ InvalidCollectionTypeError = Class.new(StandardError)
34
+ InvalidMemberTypeError = Class.new(StandardError)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ module Rrod
2
+ module Model
3
+ module Finders
4
+
5
+ def find(id)
6
+ find_one(id).tap { |found| raise NotFound.new if found.nil? }
7
+ end
8
+
9
+ def find_one(id)
10
+ robject = bucket.get(id)
11
+ found(id, robject.data, robject)
12
+ rescue Riak::FailedRequest => e
13
+ raise e unless e.not_found?
14
+ end
15
+
16
+ def find_first_by(attributes)
17
+ query = Query.new(attributes)
18
+
19
+ if query.using_id?
20
+ find_one(query.id)
21
+ else
22
+ docs = search(query)
23
+ docs.any? ? found(nil, docs.first) : nil
24
+ end
25
+ end
26
+
27
+ def find_first_by!(attributes)
28
+ find_first_by(attributes).tap { |model| raise NotFound.new if model.nil? }
29
+ end
30
+
31
+ def find_all_by(attributes)
32
+ query = Query.new(attributes)
33
+ raise ArgumentError.new('Cannot pass id to find_all_by') if query.using_id?
34
+ search(query).map { |doc| found(nil, doc) }
35
+ end
36
+
37
+ def find_all_by!(attributes)
38
+ find_all_by(attributes).tap { |models| raise NotFound.new if models.empty? }
39
+ end
40
+
41
+ private
42
+
43
+ def search(query)
44
+ client.search(bucket_name, query.to_s)['docs']
45
+ end
46
+
47
+ def found(key, data, robject=nil)
48
+ new(data).tap { |instance|
49
+ instance.id = key
50
+ instance.robject = robject
51
+ instance.instance_variable_set(:@persisted, true)
52
+ }
53
+ end
54
+
55
+ end
56
+
57
+ NotFound = Class.new(StandardError)
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ module Rrod
2
+ module Model
3
+ module Persistence
4
+ attr_accessor :robject
5
+
6
+ def persisted?
7
+ @persisted
8
+ end
9
+
10
+ def new?
11
+ !persisted?
12
+ end
13
+ alias :new_record? :new?
14
+
15
+ def save(options={})
16
+ options.fetch(:validate, true) ?
17
+ (valid? and persist) : persist
18
+ end
19
+
20
+ def persist
21
+ bucket.enable_index!
22
+ robject.raw_data = to_json
23
+ robject.key = id unless id.nil?
24
+ robject.store
25
+ self.id = robject.key
26
+ @persisted = true
27
+ end
28
+
29
+ def robject
30
+ @robject ||= bucket.new
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,30 @@
1
+ module Rrod
2
+ module Model
3
+ module Schema
4
+
5
+ def attributes
6
+ @attributes ||= {}
7
+ end
8
+
9
+ def attribute(name, type, options={})
10
+ attributes[name.to_sym] = Attribute.new(self, name, type, options).define
11
+ end
12
+
13
+ def cast_attribute(key, value)
14
+ attribute = attributes[key.to_sym]
15
+ attribute ? attribute.cast(value) : value
16
+ end
17
+
18
+ def schema?
19
+ attributes.any?
20
+ end
21
+
22
+ def rrod_cast(value)
23
+ return if value.nil?
24
+ return value if value.is_a?(Rrod::Model)
25
+ found(nil, value)
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module Rrod
2
+ module Model
3
+ module Serialization
4
+
5
+ def read_attribute_for_serialization(key)
6
+ value = read_attribute(key)
7
+ value.respond_to?(:serializable_hash) ? value.serializable_hash : value
8
+ end
9
+
10
+ end
11
+ end
12
+ end
data/lib/rrod/model.rb ADDED
@@ -0,0 +1,38 @@
1
+ module Rrod
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ include AttributeMethods
6
+ include Persistence
7
+ include Serialization
8
+ include ActiveModel::Serializers::JSON
9
+ include ActiveModel::Serializers::Xml
10
+ include ActiveModel::Validations
11
+
12
+ module ClassMethods
13
+ include Finders
14
+ include Schema
15
+
16
+ def client
17
+ Rrod.configuration.client
18
+ end
19
+
20
+ def bucket
21
+ @bucket ||= client[bucket_name]
22
+ end
23
+
24
+ # TODO make class attribute
25
+ def bucket_name
26
+ name.tableize
27
+ end
28
+ end
29
+
30
+ def client
31
+ self.class.client
32
+ end
33
+
34
+ def bucket
35
+ self.class.bucket
36
+ end
37
+ end
38
+ end
data/lib/rrod/query.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Rrod
2
+ class Query
3
+ attr_accessor :options
4
+
5
+ def initialize(options)
6
+ self.options = options
7
+ end
8
+
9
+ def id
10
+ options[:id]
11
+ end
12
+
13
+ def using_id?
14
+ id.present?
15
+ end
16
+
17
+ def to_s
18
+ options.map { |key, value| "#{key}:#{value}" }.join(" AND ")
19
+ end
20
+
21
+ def options=(options)
22
+ @options = options.with_indifferent_access
23
+ raise ArgumentError.new("no search options") if options.blank?
24
+ raise ArgumentError.new("cannot mix id with other options") if using_id? and options.keys.count > 1
25
+ rescue => e
26
+ @options.clear and raise e
27
+ end
28
+
29
+ end
30
+ end