nobrainer 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +7 -0
  3. data/README.md +6 -0
  4. data/lib/no_brainer/autoload.rb +14 -0
  5. data/lib/no_brainer/config.rb +61 -0
  6. data/lib/no_brainer/connection.rb +53 -0
  7. data/lib/no_brainer/criteria.rb +20 -0
  8. data/lib/no_brainer/criteria/chainable/core.rb +67 -0
  9. data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
  10. data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
  11. data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
  12. data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
  13. data/lib/no_brainer/criteria/chainable/where.rb +198 -0
  14. data/lib/no_brainer/criteria/termination/cache.rb +71 -0
  15. data/lib/no_brainer/criteria/termination/count.rb +19 -0
  16. data/lib/no_brainer/criteria/termination/delete.rb +11 -0
  17. data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
  18. data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
  19. data/lib/no_brainer/criteria/termination/first.rb +25 -0
  20. data/lib/no_brainer/criteria/termination/inc.rb +14 -0
  21. data/lib/no_brainer/criteria/termination/update.rb +13 -0
  22. data/lib/no_brainer/database.rb +41 -0
  23. data/lib/no_brainer/decorated_symbol.rb +15 -0
  24. data/lib/no_brainer/document.rb +18 -0
  25. data/lib/no_brainer/document/association.rb +41 -0
  26. data/lib/no_brainer/document/association/belongs_to.rb +64 -0
  27. data/lib/no_brainer/document/association/core.rb +64 -0
  28. data/lib/no_brainer/document/association/has_many.rb +68 -0
  29. data/lib/no_brainer/document/attributes.rb +124 -0
  30. data/lib/no_brainer/document/core.rb +20 -0
  31. data/lib/no_brainer/document/criteria.rb +62 -0
  32. data/lib/no_brainer/document/dirty.rb +88 -0
  33. data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
  34. data/lib/no_brainer/document/id.rb +49 -0
  35. data/lib/no_brainer/document/index.rb +102 -0
  36. data/lib/no_brainer/document/injection_layer.rb +12 -0
  37. data/lib/no_brainer/document/persistance.rb +124 -0
  38. data/lib/no_brainer/document/polymorphic.rb +43 -0
  39. data/lib/no_brainer/document/serialization.rb +9 -0
  40. data/lib/no_brainer/document/store_in.rb +33 -0
  41. data/lib/no_brainer/document/timestamps.rb +18 -0
  42. data/lib/no_brainer/document/validation.rb +35 -0
  43. data/lib/no_brainer/error.rb +10 -0
  44. data/lib/no_brainer/fork.rb +14 -0
  45. data/lib/no_brainer/index_manager.rb +6 -0
  46. data/lib/no_brainer/loader.rb +5 -0
  47. data/lib/no_brainer/locale/en.yml +4 -0
  48. data/lib/no_brainer/query_runner.rb +37 -0
  49. data/lib/no_brainer/query_runner/connection.rb +17 -0
  50. data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
  51. data/lib/no_brainer/query_runner/driver.rb +8 -0
  52. data/lib/no_brainer/query_runner/logger.rb +29 -0
  53. data/lib/no_brainer/query_runner/run_options.rb +34 -0
  54. data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
  55. data/lib/no_brainer/query_runner/write_error.rb +28 -0
  56. data/lib/no_brainer/railtie.rb +36 -0
  57. data/lib/no_brainer/railtie/database.rake +34 -0
  58. data/lib/no_brainer/util.rb +23 -0
  59. data/lib/no_brainer/version.rb +3 -0
  60. data/lib/nobrainer.rb +59 -0
  61. metadata +152 -0
@@ -0,0 +1,12 @@
1
+ module NoBrainer::Document::InjectionLayer
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def inject_in_layer(name, code=nil, file=nil, line=nil, &block)
6
+ mod = class_eval "module NoBrainer; module #{name.to_s.camelize}; self; end; end"
7
+ mod.module_eval(code, file, line) if code
8
+ mod.module_exec(&block) if block
9
+ include mod
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,124 @@
1
+ module NoBrainer::Document::Persistance
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend ActiveModel::Callbacks
6
+ define_model_callbacks :create, :update, :save, :destroy, :terminator => 'false'
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 reload(options={})
28
+ unless options[:keep_ivars]
29
+ id = self.id
30
+ instance_variables.each { |ivar| remove_instance_variable(ivar) }
31
+ @attributes = {}
32
+ self.id = id
33
+ end
34
+ assign_attributes(selector.raw.first!, :pristine => true, :from_db => true)
35
+ self
36
+ end
37
+
38
+ def _create(options={})
39
+ run_callbacks :create do
40
+ if options[:validate] && !valid?
41
+ false
42
+ else
43
+ keys = self.class.insert_all(attributes)
44
+ self.id ||= keys.first
45
+ @new_record = false
46
+ true
47
+ end
48
+ end
49
+ end
50
+
51
+ def update(options={}, &block)
52
+ run_callbacks :update do
53
+ if options[:validate] && !valid?
54
+ false
55
+ else
56
+ selector.update_all(&block)
57
+ true
58
+ end
59
+ end
60
+ end
61
+
62
+ def replace(options={}, &block)
63
+ run_callbacks :update do
64
+ if options[:validate] && !valid?
65
+ false
66
+ else
67
+ selector.replace_all(&block)
68
+ true
69
+ end
70
+ end
71
+ end
72
+
73
+ def save(options={})
74
+ options = options.reverse_merge(:validate => true)
75
+ run_callbacks :save do
76
+ new_record? ? _create(options) : replace(options) { attributes }
77
+ end
78
+ end
79
+
80
+ def save!(*args)
81
+ save(*args) or raise NoBrainer::Error::DocumentInvalid, self
82
+ end
83
+
84
+ def update_attributes(attrs, options={})
85
+ assign_attributes(attrs, options)
86
+ save(options)
87
+ end
88
+
89
+ def update_attributes!(*args)
90
+ update_attributes(*args) or raise NoBrainer::Error::DocumentInvalid, self
91
+ end
92
+
93
+ def delete
94
+ unless @destroyed
95
+ selector.delete_all
96
+ @destroyed = true
97
+ end
98
+ # TODO freeze attributes
99
+ true
100
+ end
101
+
102
+ def destroy
103
+ run_callbacks(:destroy) { delete }
104
+ end
105
+
106
+ module ClassMethods
107
+ def create(attrs={}, options={})
108
+ new(attrs, options).tap { |doc| doc.save(options) }
109
+ end
110
+
111
+ def create!(attrs={}, options={})
112
+ new(attrs, options).tap { |doc| doc.save!(options) }
113
+ end
114
+
115
+ def insert_all(*attrs)
116
+ result = NoBrainer.run(rql_table.insert(*attrs))
117
+ result['generated_keys'].to_a
118
+ end
119
+
120
+ def sync
121
+ NoBrainer.run(rql_table.sync)['synced'] == 1
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,43 @@
1
+ module NoBrainer::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 assign_attributes(*args)
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
+
37
+ def all
38
+ criterion = super
39
+ criterion = criterion.where(:_type.in => descendants_type_values) unless is_root_class?
40
+ criterion
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module NoBrainer::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 = NoBrainer::Config.include_root_in_json }
9
+ end
@@ -0,0 +1,33 @@
1
+ module NoBrainer::Document::StoreIn
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ class_attribute :store_in_options
6
+ self.store_in_options = {}
7
+ end
8
+
9
+ module ClassMethods
10
+ def store_in(options)
11
+ raise "store_in() must be called on the parent class" unless root_class?
12
+ self.store_in_options.merge!(options)
13
+ end
14
+
15
+ def database_name
16
+ db = self.store_in_options[:database]
17
+ db.is_a?(Proc) ? db.call : db
18
+ end
19
+
20
+ def table_name
21
+ table = store_in_options[:table]
22
+ table_name = table.is_a?(Proc) ? table.call : table
23
+ table_name || root_class.name.underscore.gsub('/', '__').pluralize
24
+ end
25
+
26
+ def rql_table
27
+ db = self.database_name
28
+ rql = RethinkDB::RQL.new
29
+ rql = rql.db(db) if db
30
+ rql.table(table_name)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ module NoBrainer::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,35 @@
1
+ module NoBrainer::Document::Validation
2
+ extend ActiveSupport::Concern
3
+ include ActiveModel::Validations
4
+ include ActiveModel::Validations::Callbacks
5
+
6
+ # TODO Test that thing
7
+ def valid?(context=nil)
8
+ super(context || (new_record? ? :create : :update))
9
+ end
10
+
11
+ module ClassMethods
12
+ def validates_uniqueness_of(*attr_names)
13
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
14
+ end
15
+ end
16
+
17
+ class UniquenessValidator < ActiveModel::EachValidator
18
+ def validate_each(doc, attr, value)
19
+ criteria = doc.root_class.unscoped.where(attr => value)
20
+ criteria = apply_scopes(criteria, doc)
21
+ criteria = exclude_doc(criteria, doc) if doc.persisted?
22
+ is_unique = criteria.count == 0
23
+ doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless is_unique
24
+ is_unique
25
+ end
26
+
27
+ def apply_scopes(criteria, doc)
28
+ criteria.where([*options[:scope]].map { |k| {k => doc.read_attribute(k)} })
29
+ end
30
+
31
+ def exclude_doc(criteria, doc)
32
+ criteria.where(:id.ne => doc.id)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ module NoBrainer::Error
2
+ class Connection < StandardError; end
3
+ class DocumentNotFound < StandardError; end
4
+ class DocumentInvalid < StandardError; end
5
+ class DocumentNotSaved < StandardError; end
6
+ class ChildrenExist < StandardError; end
7
+ class CannotUseIndex < StandardError; end
8
+ class InvalidType < StandardError; end
9
+ class AssociationNotSaved < StandardError; end
10
+ end
@@ -0,0 +1,14 @@
1
+ module NoBrainer::Fork
2
+ def self.hook
3
+ Kernel.module_eval do
4
+ alias_method :fork_without_nobrainer, :fork
5
+
6
+ def fork(&block)
7
+ NoBrainer.disconnect
8
+ fork_without_nobrainer(&block)
9
+ end
10
+
11
+ module_function :fork
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ module NoBrainer::IndexManager
2
+ def self.update_indexes(options={})
3
+ Rails.application.eager_load! if defined?(Rails)
4
+ NoBrainer::Document.all.each { |model| model.perform_update_indexes(options) }
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module NoBrainer::Loader
2
+ def self.cleanup
3
+ NoBrainer::Document.all.clear
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: "is already taken"
@@ -0,0 +1,37 @@
1
+ require 'middleware'
2
+ require 'rethinkdb'
3
+
4
+ module NoBrainer::QueryRunner
5
+ extend NoBrainer::Autoload
6
+
7
+ class Middleware
8
+ def initialize(runner)
9
+ @runner = runner
10
+ end
11
+ end
12
+
13
+ autoload :Driver, :DatabaseOnDemand, :TableOnDemand, :WriteError,
14
+ :Connection, :Selection, :RunOptions, :Logger
15
+
16
+ class << self
17
+ attr_accessor :stack
18
+
19
+ def run(*args, &block)
20
+ options = args.extract_options!
21
+ raise ArgumentError unless args.size == 1 || block
22
+ query = args.first || block.call(RethinkDB::RQL.new)
23
+ stack.call(:query => query, :options => options)
24
+ end
25
+ end
26
+
27
+ # thread-safe, since require() is ran with a mutex.
28
+ self.stack = ::Middleware::Builder.new do
29
+ use RunOptions
30
+ use Connection
31
+ use WriteError
32
+ use DatabaseOnDemand
33
+ use TableOnDemand
34
+ use Logger
35
+ use Driver
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ class NoBrainer::QueryRunner::Connection < NoBrainer::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env)
4
+ rescue RuntimeError, NoBrainer::Error::DocumentNotSaved => e
5
+ if e.message =~ /cannot perform (read|write): lost contact with master/
6
+ env[:connection_retries] ||= 0
7
+ # TODO sleep in between? timing out should be time based?
8
+
9
+ # XXX Possibly dangerous, as we could reexecute a non idempotent operation
10
+ # Check the semantics of the db
11
+
12
+ # TODO Unit test
13
+ retry if (env[:connection_retries] += 1) < NoBrainer::Config.max_reconnection_tries
14
+ end
15
+ raise e
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ class NoBrainer::QueryRunner::DatabaseOnDemand < NoBrainer::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env)
4
+ rescue RuntimeError => e
5
+ if NoBrainer::Config.auto_create_databases &&
6
+ e.message =~ /^Database `(.+)` does not exist\.$/
7
+ auto_create_database(env, $1)
8
+ retry
9
+ end
10
+ raise
11
+ end
12
+
13
+ private
14
+
15
+ def auto_create_database(env, database_name)
16
+ if env[:auto_create_database] == database_name
17
+ raise "Auto database creation is not working with #{database_name}"
18
+ end
19
+ env[:auto_create_database] = database_name
20
+
21
+ NoBrainer.db_create(database_name)
22
+ rescue RuntimeError => e
23
+ # We might have raced with another db_create
24
+ raise unless e.message =~ /Database `#{database_name}` already exists/
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ class NoBrainer::QueryRunner::Driver < NoBrainer::QueryRunner::Middleware
2
+ def call(env)
3
+ env[:query].run(NoBrainer.connection, env[:options])
4
+ rescue NoMethodError => e
5
+ raise "NoBrainer is not connected to a RethinkDB instance" unless NoBrainer.connection
6
+ raise e
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
2
+ def call(env)
3
+ start_time = Time.now
4
+ res = @runner.call(env)
5
+ if NoBrainer.logger and NoBrainer.logger.debug?
6
+ duration = Time.now - start_time
7
+ msg = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
8
+
9
+ # Need to fix the rethinkdb gem.
10
+ if msg =~ /Erroneous_Portion_Constructed/
11
+ msg = "r.the_rethinkdb_gem_is_flipping_out_with_Erroneous_Portion_Constructed"
12
+ end
13
+
14
+ msg = "(#{env[:db_name]}) #{msg}" if env[:db_name]
15
+ msg = "[#{(duration * 1000.0).round(1)}ms] #{msg}"
16
+
17
+ if NoBrainer::Config.colorize_logger
18
+ case NoBrainer::Util.rql_type(env[:query])
19
+ when :write then msg = "\e[1;31m#{msg}\e[0m" # red
20
+ when :read then msg = "\e[1;32m#{msg}\e[0m" # green
21
+ when :management then msg = "\e[1;33m#{msg}\e[0m" # yellow
22
+ end
23
+ end
24
+
25
+ NoBrainer.logger.debug(msg)
26
+ end
27
+ res
28
+ end
29
+ end