ninja-model 0.8.1 → 0.9.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.
- data/.gitignore +1 -1
- data/Gemfile +1 -0
- data/Rakefile +6 -0
- data/lib/ninja_model.rb +23 -7
- data/lib/ninja_model/adapters.rb +25 -20
- data/lib/ninja_model/adapters/adapter_manager.rb +2 -0
- data/lib/ninja_model/adapters/adapter_pool.rb +2 -0
- data/lib/ninja_model/associations.rb +63 -101
- data/lib/ninja_model/associations/association.rb +146 -0
- data/lib/ninja_model/associations/association_scope.rb +39 -0
- data/lib/ninja_model/associations/belongs_to_association.rb +33 -21
- data/lib/ninja_model/associations/builder/association.rb +57 -0
- data/lib/ninja_model/associations/builder/belongs_to.rb +33 -0
- data/lib/ninja_model/associations/builder/collection_association.rb +60 -0
- data/lib/ninja_model/associations/builder/has_many.rb +11 -0
- data/lib/ninja_model/associations/builder/has_one.rb +20 -0
- data/lib/ninja_model/associations/builder/singular_association.rb +49 -0
- data/lib/ninja_model/associations/collection_association.rb +103 -0
- data/lib/ninja_model/associations/collection_proxy.rb +45 -0
- data/lib/ninja_model/associations/has_many_association.rb +19 -43
- data/lib/ninja_model/associations/has_one_association.rb +52 -6
- data/lib/ninja_model/associations/singular_association.rb +61 -0
- data/lib/ninja_model/attribute.rb +5 -2
- data/lib/ninja_model/attribute_methods.rb +35 -40
- data/lib/ninja_model/base.rb +31 -39
- data/lib/ninja_model/identity.rb +8 -6
- data/lib/ninja_model/rails_ext/active_record.rb +69 -225
- data/lib/ninja_model/railtie.rb +0 -9
- data/lib/ninja_model/reflection.rb +103 -20
- data/lib/ninja_model/relation.rb +2 -2
- data/lib/ninja_model/relation/query_methods.rb +16 -0
- data/lib/ninja_model/version.rb +1 -1
- data/ninja-model.gemspec +2 -1
- data/spec/db/schema.rb +15 -0
- data/spec/{ninja_model → lib/ninja_model}/adapters/abstract_adapter_spec.rb +0 -0
- data/spec/lib/ninja_model/adapters/adapter_manager_spec.rb +72 -0
- data/spec/lib/ninja_model/adapters/adapter_pool_spec.rb +230 -0
- data/spec/lib/ninja_model/adapters_spec.rb +120 -0
- data/spec/lib/ninja_model/associations/belongs_to_association_spec.rb +76 -0
- data/spec/lib/ninja_model/associations/has_many_association_spec.rb +118 -0
- data/spec/lib/ninja_model/associations/has_one_association_spec.rb +80 -0
- data/spec/{ninja_model → lib/ninja_model}/attribute_methods_spec.rb +2 -2
- data/spec/{ninja_model → lib/ninja_model}/attribute_spec.rb +4 -0
- data/spec/{ninja_model → lib/ninja_model}/base_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/identity_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/persistence_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/predicate_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/query_methods_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/reflection_spec.rb +7 -9
- data/spec/{ninja_model → lib/ninja_model}/relation_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/symbol_spec.rb +0 -0
- data/spec/{ninja_model → lib/ninja_model}/validation_spec.rb +0 -0
- data/spec/{ninja_model_spec.rb → lib/ninja_model_spec.rb} +0 -0
- data/spec/models/bio.rb +8 -0
- data/spec/models/body.rb +7 -0
- data/spec/models/category.rb +7 -0
- data/spec/models/email_address.rb +3 -0
- data/spec/models/post.rb +13 -0
- data/spec/models/tag.rb +3 -0
- data/spec/models/user.rb +4 -0
- data/spec/spec_helper.rb +38 -5
- data/spec/support/dummy_adapter/adapter.rb +48 -0
- data/spec/support/factories/bio.rb +11 -0
- data/spec/support/factories/body.rb +12 -0
- data/spec/support/factories/email_address.rb +5 -0
- data/spec/support/factories/post.rb +12 -0
- data/spec/support/factories/tag.rb +5 -0
- data/spec/support/factories/user.rb +5 -0
- metadata +121 -63
- data/spec/ninja_model/adapters/adapter_manager_spec.rb +0 -69
- data/spec/ninja_model/adapters/adapter_pool_spec.rb +0 -230
- data/spec/ninja_model/adapters_spec.rb +0 -85
@@ -0,0 +1,45 @@
|
|
1
|
+
module NinjaModel
|
2
|
+
module Associations
|
3
|
+
class CollectionProxy
|
4
|
+
|
5
|
+
delegate :order, :limit, :where, :to => :scoped
|
6
|
+
|
7
|
+
delegate :target, :load_target, :loaded?, :scoped, :to => :@association
|
8
|
+
|
9
|
+
delegate :select, :find, :first, :last, :build, :create, :create,
|
10
|
+
:count, :size, :length, :empty?, :any?, :to => :@association
|
11
|
+
|
12
|
+
def initialize(association)
|
13
|
+
@association = association
|
14
|
+
end
|
15
|
+
|
16
|
+
def proxy_association
|
17
|
+
@association
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to?(name, include_private = false)
|
21
|
+
super ||
|
22
|
+
(load_target && target.respond_to?(name, include_private)) ||
|
23
|
+
proxy_association.klass.respond_to?(name, include_private)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
|
28
|
+
if load_target
|
29
|
+
if target.respond_to?(method)
|
30
|
+
target.send(method, *args, &block)
|
31
|
+
else
|
32
|
+
begin
|
33
|
+
super
|
34
|
+
rescue NoMethodError => e
|
35
|
+
raise e, e.message.sub(/ for #<.*$/, "via proxy for #{target}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
else
|
40
|
+
scoped.send(method, *args, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,48 +1,24 @@
|
|
1
1
|
module NinjaModel
|
2
2
|
module Associations
|
3
|
-
class HasManyAssociation
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@relation
|
23
|
-
end
|
24
|
-
|
25
|
-
def inspect
|
26
|
-
@relation.to_a.inspect
|
27
|
-
end
|
28
|
-
|
29
|
-
def replace(other_array)
|
30
|
-
@current = other_array
|
31
|
-
end
|
32
|
-
|
33
|
-
def blank?
|
34
|
-
@relation.blank?
|
35
|
-
end
|
36
|
-
|
37
|
-
def to_ary
|
38
|
-
@relation.to_a
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def apply_default_scope(scoping)
|
44
|
-
scoping.where(@reflection.primary_key_name.to_sym.eq(@owner.id))
|
45
|
-
end
|
3
|
+
class HasManyAssociation < CollectionAssociation
|
4
|
+
|
5
|
+
#def insert_record(record, validate = true, raise = false)
|
6
|
+
# set_owner_attributes(record)
|
7
|
+
|
8
|
+
# if raise
|
9
|
+
# record.save!(:validate => validate)
|
10
|
+
# else
|
11
|
+
# record.save(:validate => validate)
|
12
|
+
# end
|
13
|
+
#end
|
14
|
+
|
15
|
+
#def association_scope
|
16
|
+
# scope = super
|
17
|
+
# puts "primary_key: #{reflection.primary_key}"
|
18
|
+
# puts "foreign_key: #{reflection.foreign_key}"
|
19
|
+
# scope = scope.where(reflection.foreign_key => owner.send(reflection.primary_key))
|
20
|
+
# scope
|
21
|
+
#end
|
46
22
|
end
|
47
23
|
end
|
48
24
|
end
|
@@ -1,18 +1,64 @@
|
|
1
1
|
module NinjaModel
|
2
2
|
module Associations
|
3
|
-
class HasOneAssociation <
|
3
|
+
class HasOneAssociation < SingularAssociation
|
4
|
+
|
5
|
+
def replace(record, save = true)
|
6
|
+
raise_on_type_mismatch(record) if record
|
7
|
+
load_target
|
8
|
+
|
9
|
+
if target && target != record
|
10
|
+
remove_target!(options[:dependent]) unless target.destroyed?
|
11
|
+
end
|
12
|
+
|
13
|
+
if record
|
14
|
+
set_owner_attributes(record)
|
15
|
+
set_inverse_instance(record)
|
16
|
+
|
17
|
+
if owner.persisted? && save && !record.save
|
18
|
+
nullify_owner_attributes(record)
|
19
|
+
set_owner_attributes(target) if target
|
20
|
+
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
self.target = record
|
4
25
|
|
5
|
-
|
6
|
-
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(method = options[:dependent])
|
29
|
+
if load_target
|
30
|
+
case method
|
31
|
+
when :delete
|
32
|
+
target.delete
|
33
|
+
when :destroy
|
34
|
+
target.destroy
|
35
|
+
when :nullify
|
36
|
+
target.update_attribute(reflection.foreign_key, nil)
|
37
|
+
end
|
38
|
+
end
|
7
39
|
end
|
8
40
|
|
9
41
|
private
|
10
42
|
|
11
|
-
def
|
12
|
-
|
43
|
+
def set_new_record(record)
|
44
|
+
replace(record, false)
|
45
|
+
end
|
46
|
+
|
47
|
+
def remove_target!(method)
|
48
|
+
if method.in?([:delete, :destroy])
|
49
|
+
target.send(method)
|
50
|
+
else
|
51
|
+
nullify_owner_attributes(target)
|
52
|
+
|
53
|
+
if target.persisted? && owner.persisted? && !target.save
|
54
|
+
set_owner_attributes(target)
|
55
|
+
raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. The record failed to save when after its foreign key was set to nil."
|
56
|
+
end
|
57
|
+
end
|
13
58
|
end
|
14
59
|
|
15
|
-
def
|
60
|
+
def nullify_owner_attributes(record)
|
61
|
+
record[reflection.foreign_key] = nil
|
16
62
|
end
|
17
63
|
end
|
18
64
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module NinjaModel
|
2
|
+
module Associations
|
3
|
+
class SingularAssociation < Association
|
4
|
+
def reader(force_reload = false)
|
5
|
+
if force_reload
|
6
|
+
klass.uncached { reload }
|
7
|
+
elsif !loaded? || stale_target?
|
8
|
+
reload
|
9
|
+
end
|
10
|
+
|
11
|
+
target
|
12
|
+
end
|
13
|
+
|
14
|
+
def writer(record)
|
15
|
+
replace(record)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(attributes = {}, options = {}, &block)
|
19
|
+
create_record(attributes, options, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create!(attributes = {}, options = {}, &block)
|
23
|
+
create_record(attributes, options, true, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build(attributes = {}, options = {})
|
27
|
+
record = build_record(attributes, options)
|
28
|
+
yield(record) if block_given?
|
29
|
+
set_new_record(record)
|
30
|
+
record
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def create_scope
|
36
|
+
scoped.scope_for_create.stringify_keys.except(klass.primary_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_target
|
40
|
+
scoped.first.tap { |record| set_inverse_instance(record) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def replace(record)
|
44
|
+
raise NotImplementedError, "Subclasses must implement a replace(record) method"
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_new_record(record)
|
48
|
+
replace(record)
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_record(attributes, options, raise_error = false)
|
52
|
+
record = build_record(attributes, options)
|
53
|
+
yield(record) if block_given?
|
54
|
+
saved = record.save
|
55
|
+
set_new_record(record)
|
56
|
+
raise RecordInvalid.new(record) if !saved && raise_error
|
57
|
+
record
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -130,8 +130,11 @@ module NinjaModel
|
|
130
130
|
end
|
131
131
|
|
132
132
|
def fallback_string_to_date(string)
|
133
|
-
|
134
|
-
|
133
|
+
begin
|
134
|
+
::Date.strptime(string, I18n.translate('date.formats.default'))
|
135
|
+
rescue ArgumentError
|
136
|
+
nil
|
137
|
+
end
|
135
138
|
end
|
136
139
|
|
137
140
|
def fallback_string_to_time(string)
|
@@ -1,15 +1,35 @@
|
|
1
1
|
module NinjaModel
|
2
|
-
class Base
|
3
|
-
class UnknownAttributeError < NinjaModelError; end
|
4
|
-
include ActiveModel::AttributeMethods
|
5
|
-
include ActiveModel::Dirty
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
class UnknownAttributeError < NinjaModelError; end
|
4
|
+
class AttributeAssignmentError < NinjaModelError
|
5
|
+
attr_reader :exception, :attribute
|
6
|
+
attr_reader :mymessage
|
7
|
+
def initialize(message, exception, attribute)
|
8
|
+
@exception = exception
|
9
|
+
@attribute = attribute
|
10
|
+
@message = message
|
11
|
+
@mymessage = message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class MultiparameterAssignmentErrors < NinjaModelError
|
15
|
+
attr_reader :errors
|
16
|
+
def initialize(errors)
|
17
|
+
@errors = errors
|
18
|
+
end
|
19
|
+
end
|
10
20
|
|
11
|
-
|
21
|
+
module AttributeMethods
|
22
|
+
extend ActiveSupport::Concern
|
12
23
|
|
24
|
+
included do
|
25
|
+
include ActiveModel::AttributeMethods
|
26
|
+
include ActiveModel::Dirty
|
27
|
+
class_attribute :model_attributes
|
28
|
+
self.model_attributes = []
|
29
|
+
attribute_method_suffix('', '=', '_before_type_cast')
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
13
33
|
def attribute(name, data_type, *args)
|
14
34
|
name = name.to_s
|
15
35
|
opts = args.extract_options!
|
@@ -17,14 +37,8 @@ module NinjaModel
|
|
17
37
|
self.primary_key = name if primary.eql?(true)
|
18
38
|
default = args.first unless args.blank?
|
19
39
|
new_attr = Attribute.new(name, data_type, opts)
|
20
|
-
self.model_attributes
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def define_attribute_methods(force = false)
|
25
|
-
return unless self.model_attributes
|
26
|
-
undefine_attribute_methods if force
|
27
|
-
super(self.model_attributes.map { |attr| attr.name })
|
40
|
+
self.model_attributes = model_attributes + [new_attr]
|
41
|
+
define_attribute_method name
|
28
42
|
end
|
29
43
|
|
30
44
|
def columns
|
@@ -43,25 +57,6 @@ module NinjaModel
|
|
43
57
|
|
44
58
|
alias :column_names :attribute_names
|
45
59
|
end
|
46
|
-
end
|
47
|
-
|
48
|
-
module AttributeMethods
|
49
|
-
class AttributeAssignmentError < NinjaModelError
|
50
|
-
attr_reader :exception, :attribute
|
51
|
-
attr_reader :mymessage
|
52
|
-
def initialize(message, exception, attribute)
|
53
|
-
@exception = exception
|
54
|
-
@attribute = attribute
|
55
|
-
@message = message
|
56
|
-
@mymessage = message
|
57
|
-
end
|
58
|
-
end
|
59
|
-
class MultiparameterAssignmentErrors < NinjaModelError
|
60
|
-
attr_reader :errors
|
61
|
-
def initialize(errors)
|
62
|
-
@errors = errors
|
63
|
-
end
|
64
|
-
end
|
65
60
|
|
66
61
|
def attributes_from_model_attributes
|
67
62
|
self.class.model_attributes.inject({}) do |result, attr|
|
@@ -106,17 +101,17 @@ module NinjaModel
|
|
106
101
|
if k.include?('(')
|
107
102
|
multi_parameter_attributes << [k, v]
|
108
103
|
else
|
109
|
-
respond_to?("#{k}=".to_sym) ? send("#{k}=".to_sym, v) : raise(NinjaModel::
|
104
|
+
respond_to?("#{k}=".to_sym) ? send("#{k}=".to_sym, v) : raise(NinjaModel::UnknownAttributeError, "unknown attribute: #{k}")
|
110
105
|
end
|
111
106
|
end
|
112
107
|
|
113
108
|
assign_multiparameter_attributes(multi_parameter_attributes)
|
114
109
|
end
|
115
110
|
|
116
|
-
def attribute_method?(name)
|
117
|
-
|
118
|
-
|
119
|
-
end
|
111
|
+
#def attribute_method?(name)
|
112
|
+
# name = name.to_s
|
113
|
+
# self.class.model_attributes_hash.key?(name)
|
114
|
+
#end
|
120
115
|
|
121
116
|
private
|
122
117
|
|
data/lib/ninja_model/base.rb
CHANGED
@@ -1,14 +1,4 @@
|
|
1
|
-
require 'ninja_model/
|
2
|
-
require 'ninja_model/associations'
|
3
|
-
require 'ninja_model/adapters'
|
4
|
-
require 'ninja_model/callbacks'
|
5
|
-
require 'ninja_model/identity'
|
6
|
-
require 'ninja_model/persistence'
|
7
|
-
require 'ninja_model/predicate'
|
8
|
-
require 'ninja_model/reflection'
|
9
|
-
require 'ninja_model/relation'
|
10
|
-
require 'ninja_model/validation'
|
11
|
-
require 'ninja_model/attribute'
|
1
|
+
require 'ninja_model/core_ext/symbol'
|
12
2
|
require 'active_record/named_scope'
|
13
3
|
require 'active_record/aggregations'
|
14
4
|
|
@@ -25,29 +15,19 @@ module NinjaModel
|
|
25
15
|
extend ActiveModel::Translation
|
26
16
|
extend ActiveModel::Naming
|
27
17
|
include ActiveModel::Observing
|
28
|
-
include ActiveModel::Dirty
|
29
18
|
include ActiveRecord::Aggregations
|
30
19
|
include ActiveRecord::NamedScope
|
31
20
|
include ActiveModel::Serializers::JSON
|
32
21
|
include ActiveModel::Serializers::Xml
|
33
22
|
|
34
23
|
define_model_callbacks :initialize, :find, :touch, :only => :after
|
24
|
+
class_attribute :pluralize_table_names, :instance_writer => false
|
25
|
+
self.pluralize_table_names = true
|
26
|
+
|
27
|
+
class_attribute :default_scopes
|
28
|
+
self.default_scopes = []
|
35
29
|
|
36
30
|
class << self
|
37
|
-
def inherited(subclass)
|
38
|
-
subclass.class_attribute :model_attributes
|
39
|
-
if self.respond_to?(:model_attributes)
|
40
|
-
subclass.model_attributes = self.model_attributes.dup
|
41
|
-
else
|
42
|
-
subclass.model_attributes = []
|
43
|
-
end
|
44
|
-
subclass.class_attribute :default_scoping
|
45
|
-
if self.respond_to?(:default_scoping)
|
46
|
-
subclass.default_scoping = self.default_scoping.dup
|
47
|
-
else
|
48
|
-
subclass.default_scoping = []
|
49
|
-
end
|
50
|
-
end
|
51
31
|
|
52
32
|
delegate :find, :first, :last, :all, :exists?, :to => :scoped
|
53
33
|
delegate :where, :order, :limit, :to => :scoped
|
@@ -66,12 +46,12 @@ module NinjaModel
|
|
66
46
|
|
67
47
|
def scoped_methods
|
68
48
|
key = "#{self}_scoped_methods".to_sym
|
69
|
-
Thread.current[key] = Thread.current[key].presence || self.
|
49
|
+
Thread.current[key] = Thread.current[key].presence || self.default_scopes.dup
|
70
50
|
end
|
71
51
|
|
72
|
-
def default_scope(
|
73
|
-
|
74
|
-
self.
|
52
|
+
def default_scope(scope = {})
|
53
|
+
scope = Proc.new if block_given?
|
54
|
+
self.default_scopes = default_scopes + [scope]
|
75
55
|
end
|
76
56
|
|
77
57
|
def current_scope
|
@@ -87,7 +67,6 @@ module NinjaModel
|
|
87
67
|
Thread.current["#{self}_scoped_methods".to_sym] = nil
|
88
68
|
end
|
89
69
|
|
90
|
-
private
|
91
70
|
|
92
71
|
def build_finder_relation(options = {}, scope = nil)
|
93
72
|
relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
|
@@ -121,15 +100,27 @@ module NinjaModel
|
|
121
100
|
end
|
122
101
|
end
|
123
102
|
|
103
|
+
def assign_attributes(new_attributes, options = {})
|
104
|
+
return unless new_attributes
|
105
|
+
|
106
|
+
attributes = new_attributes.stringify_keys
|
107
|
+
|
108
|
+
attributes.each do |k, v|
|
109
|
+
if respond_to?("#{k}=")
|
110
|
+
send("#{k}=", v)
|
111
|
+
else
|
112
|
+
raise(StandardError, "unknown attribute: #{k}")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
124
117
|
def attributes
|
125
|
-
|
126
|
-
|
127
|
-
attrs[name] = read_attribute(name)
|
118
|
+
self.class.attribute_names.inject({}) { |h, v|
|
119
|
+
h[v] = read_attribute(v); h
|
128
120
|
}
|
129
|
-
attrs
|
130
121
|
end
|
131
122
|
|
132
|
-
def initialize(attributes = nil)
|
123
|
+
def initialize(attributes = nil, options = {})
|
133
124
|
@attributes = attributes_from_model_attributes
|
134
125
|
@association_cache = {}
|
135
126
|
@aggregation_cache = {}
|
@@ -141,9 +132,8 @@ module NinjaModel
|
|
141
132
|
|
142
133
|
self.attributes = attributes unless attributes.nil?
|
143
134
|
|
144
|
-
|
145
|
-
|
146
|
-
result
|
135
|
+
yield self if block_given?
|
136
|
+
run_callbacks :initialize
|
147
137
|
end
|
148
138
|
|
149
139
|
def instantiate(record)
|
@@ -175,3 +165,5 @@ module NinjaModel
|
|
175
165
|
end
|
176
166
|
end
|
177
167
|
end
|
168
|
+
|
169
|
+
ActiveSupport.run_load_hooks(:ninja_model, NinjaModel::Base)
|