ninja-model 0.4.2

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 (54) hide show
  1. data/Gemfile +2 -0
  2. data/README.md +1 -0
  3. data/Rakefile +38 -0
  4. data/lib/generators/ninja_model/model/model_generator.rb +19 -0
  5. data/lib/generators/ninja_model/model/templates/model.rb +7 -0
  6. data/lib/generators/ninja_model/scaffold/scaffold_generator.rb +69 -0
  7. data/lib/generators/ninja_model.rb +12 -0
  8. data/lib/ninja-model.rb +1 -0
  9. data/lib/ninja_model/adapters/abstract_adapter.rb +63 -0
  10. data/lib/ninja_model/adapters/adapter_manager.rb +67 -0
  11. data/lib/ninja_model/adapters/adapter_pool.rb +128 -0
  12. data/lib/ninja_model/adapters/adapter_specification.rb +11 -0
  13. data/lib/ninja_model/adapters.rb +70 -0
  14. data/lib/ninja_model/associations/active_record_proxy.rb +53 -0
  15. data/lib/ninja_model/associations/association_proxy.rb +8 -0
  16. data/lib/ninja_model/associations/belongs_to_association.rb +31 -0
  17. data/lib/ninja_model/associations/has_many_association.rb +36 -0
  18. data/lib/ninja_model/associations/has_one_association.rb +19 -0
  19. data/lib/ninja_model/associations/ninja_model_proxy.rb +46 -0
  20. data/lib/ninja_model/associations.rb +198 -0
  21. data/lib/ninja_model/attributes.rb +134 -0
  22. data/lib/ninja_model/base.rb +106 -0
  23. data/lib/ninja_model/callbacks.rb +14 -0
  24. data/lib/ninja_model/configuration.rb +20 -0
  25. data/lib/ninja_model/core_ext/symbol.rb +7 -0
  26. data/lib/ninja_model/errors.rb +5 -0
  27. data/lib/ninja_model/identity.rb +24 -0
  28. data/lib/ninja_model/log_subscriber.rb +18 -0
  29. data/lib/ninja_model/persistence.rb +76 -0
  30. data/lib/ninja_model/predicate.rb +43 -0
  31. data/lib/ninja_model/railtie.rb +27 -0
  32. data/lib/ninja_model/reflection.rb +118 -0
  33. data/lib/ninja_model/relation/finder_methods.rb +74 -0
  34. data/lib/ninja_model/relation/query_methods.rb +62 -0
  35. data/lib/ninja_model/relation/spawn_methods.rb +59 -0
  36. data/lib/ninja_model/relation.rb +80 -0
  37. data/lib/ninja_model/scoping.rb +50 -0
  38. data/lib/ninja_model/validation.rb +38 -0
  39. data/lib/ninja_model/version.rb +3 -0
  40. data/lib/ninja_model.rb +38 -0
  41. data/spec/ninja_model/adapters/adapter_pool_spec.rb +72 -0
  42. data/spec/ninja_model/adapters_spec.rb +8 -0
  43. data/spec/ninja_model/attributes_spec.rb +85 -0
  44. data/spec/ninja_model/base_spec.rb +66 -0
  45. data/spec/ninja_model/identity_spec.rb +41 -0
  46. data/spec/ninja_model/persistence_spec.rb +9 -0
  47. data/spec/ninja_model/predicate_spec.rb +13 -0
  48. data/spec/ninja_model/query_methods_spec.rb +76 -0
  49. data/spec/ninja_model/relation_spec.rb +26 -0
  50. data/spec/ninja_model/scoping_spec.rb +40 -0
  51. data/spec/ninja_model_spec.rb +11 -0
  52. data/spec/spec_helper.rb +9 -0
  53. data/spec/support/active_model_lint.rb +17 -0
  54. metadata +214 -0
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # Ninja-model
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rake'
6
+ require 'rake/rdoctask'
7
+ require 'rspec/core/rake_task'
8
+ require 'cucumber'
9
+ require 'cucumber/rake/task'
10
+
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.verbose = false
13
+ end
14
+
15
+ desc 'Run specs with documentation format'
16
+ RSpec::Core::RakeTask.new(:specd) do |t|
17
+ t.verbose = false
18
+ t.rspec_opts = '--format d'
19
+ end
20
+
21
+ Rake::RDocTask.new do |r|
22
+ r.rdoc_dir = 'doc/html'
23
+ r.main = "README.md"
24
+ r.rdoc_files.include('README.md', 'lib/**/*.rb')
25
+ r.rdoc_files.exclude('lib/generators/**/*')
26
+ end
27
+
28
+ namespace :spec do
29
+ RSpec::Core::RakeTask.new(:rcov) do |t|
30
+ t.rcov = true
31
+ t.rcov_opts = %w{--text-report --sort coverage}
32
+ t.rcov_opts << %w{--exclude gems\/,spec\/}
33
+ end
34
+ end
35
+
36
+ Cucumber::Rake::Task.new(:features) do |t|
37
+ t.cucumber_opts = "features --format progress"
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'generators/ninja_model'
2
+
3
+ module NinjaModel
4
+ module Generators
5
+ class ModelGenerator < NamedBase
6
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
7
+
8
+ def initialize(*args, &block)
9
+ super
10
+ end
11
+
12
+ def create_model
13
+ template 'model.rb', "app/models/#{singular_name}.rb"
14
+ end
15
+
16
+ hook_for :test_framework, :as => :model
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ require 'ninja_model'
2
+
3
+ class <%= class_name %> < NinjaModel::Base
4
+ <%- attributes.each do |a| -%>
5
+ attribute :<%= a.name %>, :<%= a.type %>
6
+ <%- end -%>
7
+ end
@@ -0,0 +1,69 @@
1
+ require 'generators/ninja_model'
2
+
3
+ module NinjaModel
4
+ module Generators
5
+ class ScaffoldGenerator < ModelGenerator
6
+ argument :attributes, :type => :array, :default => [], :banner => 'field:type field:type'
7
+
8
+ def initialize(*args, &block)
9
+ super
10
+
11
+ @controller_actions = []
12
+ actions.each do |action|
13
+
14
+ end
15
+ end
16
+
17
+ def create_controller
18
+ template 'controller.rb', "app/controllers/#{plural_name}_controller.rb"
19
+ unless options.skip_helper?
20
+ template 'helper.rb', "app/helpers/#{plural_name}_helper.rb"File.join(plugin_path, 'app/helpers',
21
+ "#{plural_file_path}_helper.rb")
22
+ end
23
+
24
+ controller_actions.each do |action|
25
+ if %w[index show new edit].include?(action)
26
+ template "views/#{action}.html.erb", File.join(plugin_path,
27
+ "app/views/#{plural_name}/#{action}.html.erb")
28
+
29
+
30
+ end
31
+ end
32
+ template 'views/_form.html.erb', File.join(plugin_path, "app/views/#{plural_name}/_form.html.erb")
33
+
34
+ if class_path.length < 0
35
+ plugin_route("resources #{plural_file_name.to_sym.inspect},
36
+ :controller => '#{(class_path + [plural_name]).join('/')}")
37
+ else
38
+ plugin_route("resources #{plural_name.to_sym.inspect}")
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ attr_reader :controller_actions
45
+
46
+ def controller_methods(dir)
47
+ controller_actions.map do |action|
48
+ read_template("#{dir}/#{action}.rb")
49
+ end.join("\n").strip
50
+ end
51
+
52
+ def read_template(relative_path)
53
+ ERB.new(File.read(find_in_source_paths(relative_path)), nil, '-').result(binding)
54
+ end
55
+
56
+ def all_actions
57
+ %w[index show new create edit update destroy]
58
+ end
59
+
60
+ def action?(name)
61
+ controller_actions.include? name.to_s
62
+ end
63
+
64
+ def actions?(*names)
65
+ names.all? { |name| action? name }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/named_base'
3
+
4
+ module NinjaModel
5
+ module Generators
6
+ class NamedBase < Rails::Generators::NamedBase
7
+ def self.source_root
8
+ @ninja_model_source_root ||= File.join(File.dirname(__FILE__), 'ninja_model', generator_name, 'templates')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ require 'ninja_model'
@@ -0,0 +1,63 @@
1
+ module NinjaModel
2
+ module Adapters
3
+ class AbstractAdapter
4
+ class << self
5
+ def instance
6
+ @instance ||= new
7
+ end
8
+ end
9
+
10
+ def initialize(config, logger = nil)
11
+ @active = nil
12
+ @config, @logger = config, logger
13
+ end
14
+
15
+ def adapter_name
16
+ 'Abstract'
17
+ end
18
+
19
+ def persistent_connection?
20
+ true
21
+ end
22
+
23
+ def active?
24
+ @active != false
25
+ end
26
+
27
+ def reconnect!
28
+ @active = true
29
+ end
30
+
31
+ def disconnect!
32
+ @active = false
33
+ end
34
+
35
+ def reset!
36
+ end
37
+
38
+ def verify!
39
+ reconnect! unless active?
40
+ end
41
+
42
+ def create(model)
43
+ false
44
+ end
45
+
46
+ def read(query)
47
+ nil
48
+ end
49
+
50
+ def update(model)
51
+ false
52
+ end
53
+
54
+ def destroy(model)
55
+ false
56
+ end
57
+
58
+ def raw_connection
59
+ @connection
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,67 @@
1
+ module NinjaModel
2
+ module Adapters
3
+ class AdapterManager
4
+ attr_reader :adapter_pools
5
+
6
+ def initialize(pools = {})
7
+ @adapter_pools = pools
8
+ end
9
+
10
+ def create_adapter(name, spec)
11
+ @adapter_pools[name] = Adapters::AdapterPool.new(spec)
12
+ end
13
+
14
+ def release_active_adapters!
15
+ @adapter_pools.each_value do |pool|
16
+ pool.release_adapter
17
+ end
18
+ end
19
+
20
+ def release_reloadable_adapters!
21
+ @adapter_pools.each_value do |pool|
22
+ pool.shutdown_reloadable_adapters!
23
+ end
24
+ end
25
+
26
+ def release_all_adapters!
27
+ @adapter_pools.each_value do |pool|
28
+ pool.shutdown!
29
+ end
30
+ end
31
+
32
+ def retrieve_adapter(klass)
33
+ pool = retrieve_adapter_pool(klass)
34
+ (pool && pool.instance) or raise StandardError
35
+ end
36
+
37
+ def remove_adapter(klass)
38
+ pool = @adapter_pools[klass.name]
39
+ return nil unless pool
40
+ @adapter_pools.delete_if { |key, value| value == pool }
41
+ pool.shutdown!
42
+ pool.spec.config
43
+ end
44
+
45
+ def retrieve_adapter_pool(klass)
46
+ pool = @adapter_pools[klass.name]
47
+ return pool if pool
48
+ return nil if NinjaModel::Base == klass
49
+ retrieve_adapter_pool klass.superclass
50
+ end
51
+ end
52
+
53
+ class AdapterManagement
54
+ def initialize(app)
55
+ @app = app
56
+ end
57
+
58
+ def call(env)
59
+ @app.call(env)
60
+ ensure
61
+ unless env.key?('rake.test')
62
+ NinjaModel::Base.clear_active_instances!
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,128 @@
1
+ module NinjaModel
2
+ module Adapters
3
+ class AdapterPool
4
+ attr_reader :spec, :instances
5
+ def initialize(spec)
6
+ @spec = spec
7
+ @instances = []
8
+ @checked_out_instances = []
9
+ @assigned_instances = {}
10
+ @instance_mutex = Monitor.new
11
+ @instance_queue = @instance_mutex.new_cond
12
+ @max_size = 5
13
+ @timeout = 3
14
+ end
15
+
16
+ def instance
17
+ NinjaModel.logger.debug("instance called for #{current_instance_id}")
18
+ @assigned_instances[current_instance_id] ||= checkout
19
+ end
20
+
21
+ def release_instance(with_id = current_instance_id)
22
+ inst = @assigned_instances.delete(with_id)
23
+ checkin inst if inst
24
+ end
25
+
26
+ def with_instance
27
+ instance_id = current_instance_id
28
+ fresh_instance = true unless @assigned_instances[instance_id]
29
+ yield instance
30
+ ensure
31
+ release_instance(instance_id) if fresh_instance
32
+ end
33
+
34
+ def connected?
35
+ !@instances.empty?
36
+ end
37
+
38
+ private
39
+
40
+ def clear_stale_cached_instances!
41
+ NinjaModel.logger.debug("clearing stale instances: #{@assigned_instances.keys}")
42
+ keys = @assigned_instances.keys - Thread.list.find_all { |t|
43
+ t.alive?
44
+ }.map { |thread| thread.object_id }
45
+ keys.each do |key|
46
+ NinjaModel.logger.debug("Checking in stale connection for #{key}")
47
+ checkin @assigned_instances[key]
48
+ @assigned_instances.delete(key)
49
+ end
50
+ end
51
+
52
+ def checkout
53
+ NinjaModel.logger.debug("checking out a connection for #{current_instance_id}")
54
+ @instance_mutex.synchronize do
55
+ loop do
56
+ instance = if @checked_out_instances.size < @instances.size
57
+ checkout_existing_instance
58
+ elsif @instances.size < @max_size
59
+ checkout_new_instance
60
+ end
61
+ return instance if instance
62
+
63
+ # If we're here, we didn't get a valid instance
64
+ @instance_queue.wait(@timeout)
65
+ if (@checked_out_instances.size < @instances.size)
66
+ next
67
+ else
68
+ NinjaModel.logger.debug("Connection pool full. Clearing cached instances.")
69
+ clear_stale_cached_instances!
70
+ if @max_size == @checked_out_instances.size
71
+ NinjaModel.logger.warn("Connection pool full?")
72
+ NinjaModel.logger.warn("@checked_out_instances: #{@checked_out_instances.inspect}")
73
+ NinjaModel.logger.warn("@assigned_instances: #{@assigned_instances.inspect}")
74
+ raise ConnectionTimeoutError, "[ninja-model] *ERROR* Could not obtain an adapter instance within #{@timeout} seconds. The max adapter pool size is currently #{@max_size}...consider increasing it."
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def checkin(inst)
82
+ @instance_mutex.synchronize do
83
+ @checked_out_instances.delete inst
84
+ @instance_queue.signal
85
+ end
86
+ end
87
+
88
+ def new_instance
89
+ NinjaModel::Base.send(spec.adapter_method, spec.config)
90
+ end
91
+
92
+ def current_instance_id
93
+ Thread.current.object_id
94
+ end
95
+
96
+ def checkout_new_instance
97
+ i = new_instance
98
+ @instances << i
99
+ checkout_and_verify(i)
100
+ end
101
+
102
+ def checkout_existing_instance
103
+ i = (@instances - @checked_out_instances).first
104
+ checkout_and_verify(i)
105
+ end
106
+
107
+ def checkout_and_verify(inst)
108
+ inst.verify!
109
+ @checked_out_instances << inst
110
+ inst
111
+ end
112
+ end
113
+
114
+ class AdapterManagement
115
+ def initialize(app)
116
+ @app = app
117
+ end
118
+
119
+ def call(env)
120
+ @app.call(env)
121
+ ensure
122
+ unless env.key?('rask.test')
123
+ NinjaModel::Base.clear_active_instances!
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,11 @@
1
+ module NinjaModel
2
+ module Adapters
3
+ class AdapterSpecification
4
+ attr_accessor :config, :adapter_method
5
+
6
+ def initialize(config, adapter_method)
7
+ @config, @adapter_method = config, adapter_method
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support'
2
+
3
+ module NinjaModel
4
+
5
+ module Adapters
6
+ extend ActiveSupport::Concern
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :AdapterSpecification
10
+ autoload :AdapterManager
11
+ autoload :AdapterPool
12
+ autoload :AbstractAdapter
13
+
14
+ class << self
15
+ end
16
+ end
17
+
18
+ class Base
19
+ class_attribute :adapter_manager
20
+ self.adapter_manager = NinjaModel::Adapters::AdapterManager.new
21
+
22
+ def adapter
23
+ self.class.adapter
24
+ end
25
+
26
+ def self.set_adapter(spec = nil)
27
+ case spec
28
+ when nil
29
+ raise AdapterNotSpecified unless defined?(Rails.env)
30
+ set_adapter(Rails.env)
31
+ when AdapterSpecification
32
+ self.adapter_manager.create_adapter(name, spec)
33
+ when Symbol, String
34
+ if config = NinjaModel.configuration.specs[spec.to_s]
35
+ set_adapter(config)
36
+ else
37
+ raise AdapterNotSpecified, "#{spec} is not configured"
38
+ end
39
+ else
40
+ spec = spec.symbolize_keys
41
+ unless spec.key?(:adapter) then raise AdapterNotSpecified, "configuration does not specify adapter" end
42
+ begin
43
+ require File.join(NinjaModel.configuration.adapter_path, "#{spec[:adapter]}_adapter")
44
+ rescue LoadError => e
45
+ raise "Please install (or create) the #{spec[:adapter]} adapter. Search path is #{NinjaModel.configuration.adapter_path}"
46
+ end
47
+ adapter_method = "#{spec[:adapter]}_adapter"
48
+ if !respond_to?(adapter_method)
49
+ raise AdapterNotFound, "ninja configuration specifies nonexistent #{spec[:adapter]} adapter"
50
+ end
51
+ shutdown_adapter
52
+ set_adapter(AdapterSpecification.new(spec, adapter_method))
53
+ end
54
+ end
55
+
56
+ class << self
57
+ def adapter
58
+ retrieve_adapter
59
+ end
60
+
61
+ def retrieve_adapter
62
+ adapter_manager.retrieve_adapter(self)
63
+ end
64
+
65
+ def shutdown_adapter(klass = self)
66
+ adapter_manager.remove_adapter(klass)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,53 @@
1
+
2
+ module NinjaModel
3
+ module Associations
4
+ class ActiveRecordProxy
5
+ def initialize(ninja_model)
6
+ @klass = ninja_model
7
+ @klass.class_eval do
8
+ def proxy
9
+ @proxy ||= begin
10
+ self.class.proxy.instance(self)
11
+ end
12
+ @proxy.attributes = self.attributes.delete_if { |k,v| k.eql?('id') }
13
+ @proxy
14
+ end
15
+ end
16
+
17
+ @proxy_klass = ninja_model.parent.const_set("#{@klass.model_name}Proxy", Class.new(ActiveRecord::Base))
18
+ @proxy_klass.class_eval do
19
+ cattr_accessor :columns
20
+ self.columns = []
21
+ def self.column(name, sql_type = nil, default = nil)
22
+ self.columns << ActiveRecord::ConnectionAdapters::Column.new(name, nil, sql_type.to_s, default)
23
+ end
24
+ end
25
+
26
+ @klass.model_attributes.each do |attr|
27
+ @proxy_klass.send :column, attr.name, attr.type, attr.default
28
+ end
29
+ end
30
+
31
+ def instance(obj)
32
+ proxy = @proxy_klass.new
33
+ proxy.send :init_with, {'attributes' => obj.attributes}
34
+ proxy
35
+ end
36
+
37
+ def handle_association(macro, association_id, options)
38
+ unless macro.eql?(:belongs_to)
39
+ options = {:foreign_key => derive_foreign_key}.merge(options)
40
+ end
41
+
42
+ @proxy = nil
43
+ @proxy_klass.send macro, association_id, options
44
+ end
45
+
46
+ private
47
+
48
+ def derive_foreign_key
49
+ "#{@klass.name.underscore}_id".to_sym
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_record/associations/association_proxy'
2
+
3
+ module NinjaModel
4
+ module Associations
5
+ class AssociationProxy < ActiveRecord::Associations::AssociationProxy
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ module NinjaModel
2
+ module Associations
3
+ class BelongsToAssociation < AssociationProxy
4
+ def initialize(owner, reflection)
5
+ @owner, @reflection = owner, reflection
6
+ end
7
+
8
+ def replace(record)
9
+ if record.nil?
10
+ @target = @owner[@reflection.primary_key_name] = nil
11
+ else
12
+ @target = (AssociationProxy === record ? record.target : record)
13
+ @owner[@reflection.primary_key_name] = record_id(record) if record.persisted?
14
+ @updated = true
15
+ end
16
+ loaded
17
+ record
18
+ end
19
+
20
+ private
21
+
22
+ def find_target
23
+ @reflection.klass.scoped.where(@reflection.association_foreign_key => @owner.send(@reflection.primary_key_name)).first
24
+ end
25
+
26
+ def record_id(record)
27
+ record.send(@reflection.options[:primary_key] || :id)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ module NinjaModel
2
+ module Associations
3
+ class HasManyAssociation
4
+ def initialize(owner, reflection)
5
+ @owner, @reflection = owner, reflection
6
+ @relation = reflection.klass.scoped.where(reflection.primary_key_name.to_sym.eq(owner.id))
7
+ end
8
+
9
+ delegate :each, :collect, :map, :to_a, :size, :blank?, :empty?, :to => :relation
10
+
11
+ def method_missing(method, *args)
12
+ if @relation.respond_to?(method)
13
+ @relation.send(method, *args)
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ def relation
20
+ @relation
21
+ end
22
+
23
+ def inspect
24
+ @relation.to_a.inspect
25
+ end
26
+
27
+ def blank?
28
+ @relation.blank?
29
+ end
30
+
31
+ def to_ary
32
+ @relation.to_a
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ module NinjaModel
2
+ module Associations
3
+ class HasOneAssociation < AssociationProxy
4
+
5
+ def create(attrs = {}, replace_existing = true)
6
+ new_record
7
+ end
8
+
9
+ private
10
+
11
+ def find_target
12
+ @reflection.klass.scoped.where(@reflection.primary_key_name => @owner.send(:id)).first
13
+ end
14
+
15
+ def new_record(replace_existing)
16
+ end
17
+ end
18
+ end
19
+ end