rethinker 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +10 -0
  3. data/README.md +49 -0
  4. data/USAGE.rb.md +99 -0
  5. data/lib/rethinker.rb +38 -0
  6. data/lib/rethinker/autoload.rb +14 -0
  7. data/lib/rethinker/connection.rb +49 -0
  8. data/lib/rethinker/criterion.rb +32 -0
  9. data/lib/rethinker/database.rb +38 -0
  10. data/lib/rethinker/document.rb +8 -0
  11. data/lib/rethinker/document/attributes.rb +117 -0
  12. data/lib/rethinker/document/core.rb +37 -0
  13. data/lib/rethinker/document/dynamic_attributes.rb +12 -0
  14. data/lib/rethinker/document/id.rb +54 -0
  15. data/lib/rethinker/document/injection_layer.rb +11 -0
  16. data/lib/rethinker/document/persistence.rb +79 -0
  17. data/lib/rethinker/document/polymorphic.rb +37 -0
  18. data/lib/rethinker/document/relation.rb +34 -0
  19. data/lib/rethinker/document/selection.rb +49 -0
  20. data/lib/rethinker/document/serialization.rb +41 -0
  21. data/lib/rethinker/document/timestamps.rb +18 -0
  22. data/lib/rethinker/document/validation.rb +62 -0
  23. data/lib/rethinker/error.rb +6 -0
  24. data/lib/rethinker/query_runner.rb +32 -0
  25. data/lib/rethinker/query_runner/connection.rb +17 -0
  26. data/lib/rethinker/query_runner/database_on_demand.rb +16 -0
  27. data/lib/rethinker/query_runner/driver.rb +10 -0
  28. data/lib/rethinker/query_runner/selection.rb +8 -0
  29. data/lib/rethinker/query_runner/table_on_demand.rb +12 -0
  30. data/lib/rethinker/query_runner/write_error.rb +34 -0
  31. data/lib/rethinker/railtie.rb +16 -0
  32. data/lib/rethinker/railtie/database.rake +37 -0
  33. data/lib/rethinker/relation.rb +6 -0
  34. data/lib/rethinker/relation/belongs_to.rb +36 -0
  35. data/lib/rethinker/relation/has_many.rb +30 -0
  36. data/lib/rethinker/relation/has_many/selection.rb +27 -0
  37. data/lib/rethinker/selection.rb +6 -0
  38. data/lib/rethinker/selection/core.rb +39 -0
  39. data/lib/rethinker/selection/count.rb +13 -0
  40. data/lib/rethinker/selection/delete.rb +9 -0
  41. data/lib/rethinker/selection/enumerable.rb +22 -0
  42. data/lib/rethinker/selection/first.rb +20 -0
  43. data/lib/rethinker/selection/inc.rb +12 -0
  44. data/lib/rethinker/selection/limit.rb +15 -0
  45. data/lib/rethinker/selection/order_by.rb +41 -0
  46. data/lib/rethinker/selection/scope.rb +11 -0
  47. data/lib/rethinker/selection/update.rb +6 -0
  48. data/lib/rethinker/selection/where.rb +23 -0
  49. data/lib/rethinker/version.rb +3 -0
  50. metadata +140 -0
@@ -0,0 +1,37 @@
1
+ module Rethinker::Document::Core
2
+ extend ActiveSupport::Concern
3
+
4
+ # TODO This assume the primary key is id.
5
+ # RethinkDB can have a custom primary key. careful.
6
+ include ActiveModel::Conversion
7
+
8
+ included do
9
+ # TODO test these includes
10
+ extend ActiveModel::Naming
11
+ extend ActiveModel::Translation
12
+ end
13
+
14
+ def initialize(attrs={}, options={}); end
15
+
16
+ module ClassMethods
17
+ def table_name
18
+ root_class.name.underscore.gsub('/', '__').pluralize
19
+ end
20
+
21
+ # Even though we are using class variables, it's threads-safe.
22
+ # It's still racy, but the race is harmless.
23
+ def table
24
+ root_class.class_eval do
25
+ @table ||= RethinkDB::RQL.new.table(table_name).freeze
26
+ end
27
+ end
28
+
29
+ # Thread safe because the operation is idempotent
30
+ # (no error if we try to create the table twice)
31
+ def ensure_table!
32
+ root_class.class_eval do
33
+ @table_created ||= !!self.count
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ module Rethinker::Document::DynamicAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ def [](name)
5
+ attributes[name.to_s]
6
+ end
7
+
8
+ def []=(name, value)
9
+ attributes[name.to_s] = value
10
+ end
11
+
12
+ end
@@ -0,0 +1,54 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'digest/md5'
4
+
5
+ module Rethinker::Document::Id
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ self.field :id
10
+ end
11
+
12
+ def reset_attributes
13
+ super
14
+ self.id = Rethinker::Document::Id.generate
15
+ end
16
+
17
+ def ==(other)
18
+ return super unless self.class == other.class
19
+ !id.nil? && id == other.id
20
+ end
21
+ alias_method :eql?, :==
22
+
23
+ delegate :hash, :to => :id
24
+
25
+ # The following code is inspired by the mongo-ruby-driver
26
+
27
+ @machine_id = Digest::MD5.digest(Socket.gethostname)[0, 3]
28
+ @lock = Mutex.new
29
+ @index = 0
30
+
31
+ def self.get_inc
32
+ @lock.synchronize do
33
+ @index = (@index + 1) % 0xFFFFFF
34
+ end
35
+ end
36
+
37
+ # TODO Unit test that thing
38
+ def self.generate
39
+ oid = ''
40
+ # 4 bytes current time
41
+ oid += [Time.now.to_i].pack("N")
42
+
43
+ # 3 bytes machine
44
+ oid += @machine_id
45
+
46
+ # 2 bytes pid
47
+ oid += [Process.pid % 0xFFFF].pack("n")
48
+
49
+ # 3 bytes inc
50
+ oid += [get_inc].pack("N")[1, 3]
51
+
52
+ oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ module Rethinker::Document::InjectionLayer
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def inject_in_layer(name, code, file, line)
6
+ class_eval <<-RUBY, file, line
7
+ include module RethinkerLayer; module #{name.to_s.camelize}; #{code}; self; end; end
8
+ RUBY
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,79 @@
1
+ module Rethinker::Document::Persistence
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend ActiveModel::Callbacks
6
+ define_model_callbacks :create, :update, :save, :destroy
7
+ end
8
+
9
+ # TODO after_initialize, after_find callback
10
+ def initialize(attrs={}, options={})
11
+ super
12
+ @new_record = !options[:from_db]
13
+ end
14
+
15
+ def new_record?
16
+ @new_record
17
+ end
18
+
19
+ def destroyed?
20
+ !!@destroyed
21
+ end
22
+
23
+ def persisted?
24
+ !new_record? && !destroyed?
25
+ end
26
+
27
+ def _create
28
+ run_callbacks :create do
29
+ result = Rethinker.run { self.class.table.insert(attributes) }
30
+ self.id ||= result['generated_keys'].first
31
+ @new_record = false
32
+ true
33
+ end
34
+ end
35
+
36
+ def reload
37
+ assign_attributes(selector.run, :pristine => true)
38
+ self
39
+ end
40
+
41
+ def update(&block)
42
+ run_callbacks :update do
43
+ selector.update(&block)
44
+ true
45
+ end
46
+ end
47
+
48
+ def save(options={})
49
+ run_callbacks :save do
50
+ new_record? ? _create : update { attributes }
51
+ end
52
+ end
53
+
54
+ def update_attributes(attrs, options={})
55
+ assign_attributes(attrs, options)
56
+ save
57
+ end
58
+
59
+ def delete
60
+ selector.delete
61
+ @destroyed = true
62
+ # TODO freeze attributes
63
+ true
64
+ end
65
+
66
+ def destroy
67
+ run_callbacks(:destroy) { delete }
68
+ end
69
+
70
+ module ClassMethods
71
+ def create(*args)
72
+ new(*args).tap { |doc| doc.save }
73
+ end
74
+
75
+ def create!(*args)
76
+ new(*args).tap { |doc| doc.save! }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,37 @@
1
+ module Rethinker::Document::Polymorphic
2
+ extend ActiveSupport::Concern
3
+ include ActiveSupport::DescendantsTracker
4
+
5
+ included do
6
+ class_attribute :root_class
7
+ self.root_class = self
8
+ end
9
+
10
+ def reset_attributes
11
+ super
12
+ self._type = self.class.type_value unless self.class.is_root_class?
13
+ end
14
+
15
+ module ClassMethods
16
+ def inherited(subclass)
17
+ super
18
+ subclass.field :_type if is_root_class?
19
+ end
20
+
21
+ def type_value
22
+ name
23
+ end
24
+
25
+ def descendants_type_values
26
+ ([self] + descendants).map(&:type_value)
27
+ end
28
+
29
+ def is_root_class?
30
+ self == root_class
31
+ end
32
+
33
+ def klass_from_attrs(attrs)
34
+ attrs['_type'].try(:constantize) || root_class
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Rethinker::Document::Relation
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ class << self; attr_accessor :relations; end
6
+ self.relations = {}
7
+ end
8
+
9
+ def reset_attributes
10
+ super
11
+ @relations_cache = {}
12
+ end
13
+
14
+ module ClassMethods
15
+ def inherited(subclass)
16
+ super
17
+ subclass.relations = self.relations.dup
18
+ end
19
+
20
+ [:belongs_to, :has_many].each do |relation|
21
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
+ def #{relation}(target, options={})
23
+ target = target.to_sym
24
+ r = Rethinker::Relation::#{relation.to_s.camelize}.new(self, target, options)
25
+ r.hook
26
+
27
+ ([self] + descendants).each do |klass|
28
+ klass.relations[target] = r
29
+ end
30
+ end
31
+ RUBY
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ module Rethinker::Document::Selection
2
+ extend ActiveSupport::Concern
3
+
4
+ def selector
5
+ self.class.selector_for(id)
6
+ end
7
+
8
+ module ClassMethods
9
+ def all
10
+ sel = Rethinker::Selection.new(Rethinker::Criterion.new(:table, table_name), :klass => self)
11
+
12
+ unless is_root_class?
13
+ # TODO use this: sel = sel.where(:_type.in(descendants_type_values))
14
+ sel = sel.where do |doc|
15
+ doc.has_fields(:_type) &
16
+ descendants_type_values.map { |type| doc[:_type].eq(type) }
17
+ .reduce { |a,b| a | b }
18
+ end
19
+ end
20
+
21
+ sel
22
+ end
23
+
24
+ def scope(name, selection)
25
+ singleton_class.class_eval do
26
+ define_method(name) { |*args| selection.call(*args) }
27
+ end
28
+ end
29
+
30
+ delegate :count, :where, :order_by, :first, :last, :to => :all
31
+
32
+ def selector_for(id)
33
+ # TODO Pass primary key if not default
34
+ Rethinker::Selection.new([Rethinker::Criterion.new(:table, table_name), Rethinker::Criterion.new(:get, id)], :klass => self)
35
+ end
36
+
37
+ # XXX this doesn't have the same semantics as
38
+ # other ORMs. the equivalent is find!.
39
+ def find(id)
40
+ new_from_db(selector_for(id).run)
41
+ end
42
+
43
+ def find!(id)
44
+ find(id).tap do |doc|
45
+ doc or raise Rethinker::Error::DocumentNotFound, "#{self.class} id #{id} not found"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,41 @@
1
+ module Rethinker::Document::Serialization
2
+ extend ActiveSupport::Concern
3
+
4
+ include ActiveModel::Serialization
5
+ include ActiveModel::Serializers::JSON
6
+ include ActiveModel::Serializers::Xml
7
+
8
+ included { self.include_root_in_json = false }
9
+
10
+ # XXX This is a giant copy paste from lib/active_model/serialization.rb
11
+ # The diff is:
12
+ # - attribute_names = attributes.keys.sort
13
+ # + attribute_names = self.class.fields.keys.sort
14
+ # That's all.
15
+ def serializable_hash(options = nil)
16
+ options ||= {}
17
+
18
+ attribute_names = self.class.fields.keys.sort
19
+ if only = options[:only]
20
+ attribute_names &= Array.wrap(only).map(&:to_s)
21
+ elsif except = options[:except]
22
+ attribute_names -= Array.wrap(except).map(&:to_s)
23
+ end
24
+
25
+ hash = {}
26
+ attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
27
+
28
+ method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
29
+ method_names.each { |n| hash[n] = send(n) }
30
+
31
+ serializable_add_includes(options) do |association, records, opts|
32
+ hash[association] = if records.is_a?(Enumerable)
33
+ records.map { |a| a.serializable_hash(opts) }
34
+ else
35
+ records.serializable_hash(opts)
36
+ end
37
+ end
38
+
39
+ hash
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ module Rethinker::Document::Timestamps
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ self.field :created_at
6
+ self.field :updated_at
7
+
8
+ before_create { self.created_at = Time.now if self.respond_to?(:created_at=) }
9
+ before_save { self.updated_at = Time.now if self.respond_to?(:updated_at=) }
10
+ end
11
+
12
+ module ClassMethods
13
+ def disable_timestamps
14
+ self.remove_field :created_at
15
+ self.remove_field :updated_at
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,62 @@
1
+ module Rethinker::Document::Validation
2
+ extend ActiveSupport::Concern
3
+ include ActiveModel::Validations
4
+ include ActiveModel::Validations::Callbacks
5
+
6
+ def save(options={})
7
+ options = options.reverse_merge(:validate => true)
8
+
9
+ if options[:validate]
10
+ valid? ? super : false
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ # TODO Test that thing
17
+ def valid?(context=nil)
18
+ super(context || (new_record? ? :create : :update))
19
+ end
20
+
21
+ [:save, :update_attributes].each do |method|
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
+ def #{method}!(*args)
24
+ #{method}(*args) or raise Rethinker::Error::DocumentInvalid, errors
25
+ end
26
+ RUBY
27
+ end
28
+
29
+ class UniquenessValidator < ActiveModel::EachValidator
30
+ # Validate the document for uniqueness violations.
31
+ #
32
+ # @example Validate the document.
33
+ # validate_each(person, :title, "Sir")
34
+ #
35
+ # @param [ Document ] document The document to validate.
36
+ # @param [ Symbol ] attribute The field to validate on.
37
+ # @param [ Object ] value The value of the field.
38
+ #
39
+ # @return [ Boolean ] true if the attribute is unique.
40
+ def validate_each(document, attribute, value)
41
+ finder = document.class.where(attribute => value)
42
+ finder = apply_scopes(finder, document)
43
+ finder = exclude_document(finder, document) if document.persisted?
44
+ is_unique = finder.count == 0
45
+ unless is_unique
46
+ document.errors.add(attribute, 'is already taken')
47
+ end
48
+ is_unique
49
+ end
50
+
51
+ def apply_scopes(finder, document)
52
+ Array.wrap(options[:scope]).each do |scope_item|
53
+ finder = finder.where{|doc| doc[scope_item.to_s].eq(document.attributes[scope_item.to_s])}
54
+ end
55
+ finder
56
+ end
57
+
58
+ def exclude_document(finder, document)
59
+ finder.where{|doc| doc["id"].ne(document.attributes["id"])}
60
+ end
61
+ end
62
+ end