torque-postgresql 2.2.4 → 2.4.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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'auxiliary_statement/settings'
4
+ require_relative 'auxiliary_statement/recursive'
4
5
 
5
6
  module Torque
6
7
  module PostgreSQL
@@ -8,17 +9,20 @@ module Torque
8
9
  TABLE_COLUMN_AS_STRING = /\A(?:"?(\w+)"?\.)?"?(\w+)"?\z/.freeze
9
10
 
10
11
  class << self
11
- attr_reader :config
12
+ attr_reader :config, :table_name
12
13
 
13
14
  # Find or create the class that will handle statement
14
15
  def lookup(name, base)
15
16
  const = name.to_s.camelize << '_' << self.name.demodulize
16
17
  return base.const_get(const, false) if base.const_defined?(const, false)
17
- base.const_set(const, Class.new(AuxiliaryStatement))
18
+
19
+ base.const_set(const, Class.new(self)).tap do |klass|
20
+ klass.instance_variable_set(:@table_name, name.to_s)
21
+ end
18
22
  end
19
23
 
20
24
  # Create a new instance of an auxiliary statement
21
- def instantiate(statement, base, options = nil)
25
+ def instantiate(statement, base, **options)
22
26
  klass = while base < ActiveRecord::Base
23
27
  list = base.auxiliary_statements_list
24
28
  break list[statement] if list.present? && list.key?(statement)
@@ -26,15 +30,15 @@ module Torque
26
30
  base = base.superclass
27
31
  end
28
32
 
29
- return klass.new(options) unless klass.nil?
33
+ return klass.new(**options) unless klass.nil?
30
34
  raise ArgumentError, <<-MSG.squish
31
35
  There's no '#{statement}' auxiliary statement defined for #{base.class.name}.
32
36
  MSG
33
37
  end
34
38
 
35
39
  # Fast access to statement build
36
- def build(statement, base, options = nil, bound_attributes = [], join_sources = [])
37
- klass = instantiate(statement, base, options)
40
+ def build(statement, base, bound_attributes = [], join_sources = [], **options)
41
+ klass = instantiate(statement, base, **options)
38
42
  result = klass.build(base)
39
43
 
40
44
  bound_attributes.concat(klass.bound_attributes)
@@ -56,7 +60,7 @@ module Torque
56
60
  # A way to create auxiliary statements outside of models configurations,
57
61
  # being able to use on extensions
58
62
  def create(table_or_settings, &block)
59
- klass = Class.new(AuxiliaryStatement)
63
+ klass = Class.new(self)
60
64
 
61
65
  if block_given?
62
66
  klass.instance_variable_set(:@table_name, table_or_settings)
@@ -89,7 +93,8 @@ module Torque
89
93
  def configure(base, instance)
90
94
  return @config unless @config.respond_to?(:call)
91
95
 
92
- settings = Settings.new(base, instance)
96
+ recursive = self < AuxiliaryStatement::Recursive
97
+ settings = Settings.new(base, instance, recursive)
93
98
  settings.instance_exec(settings, &@config)
94
99
  settings
95
100
  end
@@ -98,11 +103,6 @@ module Torque
98
103
  def table
99
104
  @table ||= ::Arel::Table.new(table_name)
100
105
  end
101
-
102
- # Get the name of the table of the configurated statement
103
- def table_name
104
- @table_name ||= self.name.demodulize.split('_').first.underscore
105
- end
106
106
  end
107
107
 
108
108
  delegate :config, :table, :table_name, :relation, :configure, :relation_query?,
@@ -111,15 +111,14 @@ module Torque
111
111
  attr_reader :bound_attributes, :join_sources
112
112
 
113
113
  # Start a new auxiliary statement giving extra options
114
- def initialize(*args)
115
- options = args.extract_options!
114
+ def initialize(*, **options)
116
115
  args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
117
116
 
118
117
  @join = options.fetch(:join, {})
119
118
  @args = options.fetch(args_key, {})
120
119
  @where = options.fetch(:where, {})
121
120
  @select = options.fetch(:select, {})
122
- @join_type = options.fetch(:join_type, nil)
121
+ @join_type = options[:join_type]
123
122
 
124
123
  @bound_attributes = []
125
124
  @join_sources = []
@@ -131,7 +130,7 @@ module Torque
131
130
  @join_sources.clear
132
131
 
133
132
  # Prepare all the data for the statement
134
- prepare(base)
133
+ prepare(base, configure(base, self))
135
134
 
136
135
  # Add the join condition to the list
137
136
  @join_sources << build_join(base)
@@ -142,8 +141,7 @@ module Torque
142
141
 
143
142
  private
144
143
  # Setup the statement using the class configuration
145
- def prepare(base)
146
- settings = configure(base, self)
144
+ def prepare(base, settings)
147
145
  requires = Array.wrap(settings.requires).flatten.compact
148
146
  @dependencies = ensure_dependencies(requires, base).flatten.compact
149
147
 
@@ -151,14 +149,13 @@ module Torque
151
149
  @query = settings.query
152
150
 
153
151
  # Call a proc to get the real query
154
- if @query.methods.include?(:call)
152
+ if @query.respond_to?(:call)
155
153
  call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
156
154
  @query = @query.call(*call_args)
157
155
  @args = []
158
156
  end
159
157
 
160
- # Manually set the query table when it's not an relation query
161
- @query_table = settings.query_table unless relation_query?(@query)
158
+ # Merge select attributes provided on the instance creation
162
159
  @select = settings.attributes.merge(@select) if settings.attributes.present?
163
160
 
164
161
  # Merge join settings
@@ -168,7 +165,7 @@ module Torque
168
165
  @association = settings.through.to_s
169
166
  elsif relation_query?(@query)
170
167
  @association = base.reflections.find do |name, reflection|
171
- break name if @query.klass.eql? reflection.klass
168
+ break name if @query.klass.eql?(reflection.klass)
172
169
  end
173
170
  end
174
171
  end
@@ -234,15 +231,6 @@ module Torque
234
231
  as a query object on #{self.class.name}.
235
232
  MSG
236
233
 
237
- # Expose join columns
238
- if relation_query?(@query)
239
- query_table = @query.arel_table
240
- conditions.children.each do |item|
241
- @query.select_values += [query_table[item.left.name]] \
242
- if item.left.relation.eql?(table)
243
- end
244
- end
245
-
246
234
  # Build the join based on the join type
247
235
  arel_join.new(table, table.create_on(conditions))
248
236
  end
@@ -263,21 +251,31 @@ module Torque
263
251
 
264
252
  # Mount the list of selected attributes
265
253
  def expose_columns(base, query_table = nil)
266
- # Add select columns to the query and get exposed columns
267
- @select.map do |left, right|
268
- base.select_extra_values += [table[right.to_s]]
269
- project(left, query_table).as(right.to_s) if query_table
254
+ # Add the columns necessary for the join
255
+ list = @join_sources.each_with_object(@select) do |join, hash|
256
+ join.right.expr.children.each do |item|
257
+ hash[item.left.name] = nil if item.left.relation.eql?(table)
258
+ end
270
259
  end
260
+
261
+ # Add select columns to the query and get exposed columns
262
+ list.map do |left, right|
263
+ base.select_extra_values += [table[right.to_s]] unless right.nil?
264
+ next unless query_table
265
+
266
+ col = project(left, query_table)
267
+ right.nil? ? col : col.as(right.to_s)
268
+ end.compact
271
269
  end
272
270
 
273
271
  # Ensure that all the dependencies are loaded in the base relation
274
272
  def ensure_dependencies(list, base)
275
273
  with_options = list.extract_options!.to_a
276
- (list + with_options).map do |dependent, options|
277
- dependent_klass = base.model.auxiliary_statements_list[dependent]
274
+ (list + with_options).map do |name, options|
275
+ dependent_klass = base.model.auxiliary_statements_list[name]
278
276
 
279
277
  raise ArgumentError, <<-MSG.squish if dependent_klass.nil?
280
- The '#{dependent}' auxiliary statement dependency can't found on
278
+ The '#{name}' auxiliary statement dependency can't found on
281
279
  #{self.class.name}.
282
280
  MSG
283
281
 
@@ -285,7 +283,8 @@ module Torque
285
283
  cte.is_a?(dependent_klass)
286
284
  end
287
285
 
288
- AuxiliaryStatement.build(dependent, base, options, bound_attributes, join_sources)
286
+ options ||= {}
287
+ AuxiliaryStatement.build(name, base, bound_attributes, join_sources, **options)
289
288
  end
290
289
  end
291
290
 
@@ -5,15 +5,27 @@ module Torque
5
5
  module Base
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ ##
9
+ # :singleton-method: schema
10
+ # :call-seq: schema
11
+ #
12
+ # The schema to which the table belongs to.
13
+
8
14
  included do
9
15
  mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
16
+ class_attribute :schema, instance_writer: false
10
17
  end
11
18
 
12
19
  module ClassMethods
13
20
  delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
14
21
 
15
- # Wenever it's inherited, add a new list of auxiliary statements
16
- # It also adds an auxiliary statement to load inherited records' relname
22
+ # Make sure that table name is an instance of TableName class
23
+ def reset_table_name
24
+ self.table_name = TableName.new(self, super)
25
+ end
26
+
27
+ # Whenever the base model is inherited, add a list of auxiliary
28
+ # statements like the one that loads inherited records' relname
17
29
  def inherited(subclass)
18
30
  super
19
31
 
@@ -24,6 +36,11 @@ module Torque
24
36
 
25
37
  # Define helper methods to return the class of the given records
26
38
  subclass.auxiliary_statement record_class do |cte|
39
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
40
+ Inheritance does not use this auxiliary statement and it can be removed.
41
+ You can replace it with `model.select_extra_values << 'tableoid::regclass'`.
42
+ MSG
43
+
27
44
  pg_class = ::Arel::Table.new('pg_class')
28
45
  arel_query = ::Arel::SelectManager.new(pg_class)
29
46
  arel_query.project(pg_class['oid'], pg_class['relname'].as(record_class.to_s))
@@ -36,18 +53,11 @@ module Torque
36
53
  # Define the dynamic attribute that returns the same information as
37
54
  # the one provided by the auxiliary statement
38
55
  subclass.dynamic_attribute(record_class) do
39
- next self.class.table_name unless self.class.physically_inheritances?
40
-
41
- pg_class = ::Arel::Table.new('pg_class')
42
- source = ::Arel::Table.new(subclass.table_name, as: 'source')
43
- quoted_id = ::Arel::Nodes::Quoted.new(id)
56
+ klass = self.class
57
+ next klass.table_name unless klass.physically_inheritances?
44
58
 
45
- query = ::Arel::SelectManager.new(pg_class)
46
- query.join(source).on(pg_class['oid'].eq(source['tableoid']))
47
- query.where(source[subclass.primary_key].eq(quoted_id))
48
- query.project(pg_class['relname'])
49
-
50
- self.class.connection.select_value(query)
59
+ query = klass.unscoped.where(subclass.primary_key => id)
60
+ query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
51
61
  end
52
62
  end
53
63
 
@@ -299,6 +309,16 @@ module Torque
299
309
  klass.configurator(block)
300
310
  end
301
311
  alias cte auxiliary_statement
312
+
313
+ # Creates a new recursive auxiliary statement (CTE) under the base
314
+ # Very similar to the regular auxiliary statement, but with two-part
315
+ # query where one is executed first and the second recursively
316
+ def recursive_auxiliary_statement(table, &block)
317
+ klass = AuxiliaryStatement::Recursive.lookup(table, self)
318
+ auxiliary_statements_list[table.to_sym] = klass
319
+ klass.configurator(block)
320
+ end
321
+ alias recursive_cte recursive_auxiliary_statement
302
322
  end
303
323
  end
304
324
 
@@ -40,6 +40,19 @@ module Torque
40
40
  end.to_h
41
41
  end
42
42
 
43
+ # Configure multiple schemas
44
+ config.nested(:schemas) do |schemas|
45
+
46
+ # Defines a list of LIKE-based schemas to not consider for a multiple
47
+ # schema database
48
+ schemas.blacklist = %w[information_schema pg_%]
49
+
50
+ # Defines a list of LIKE-based schemas to consider for a multiple schema
51
+ # database
52
+ schemas.whitelist = %w[public]
53
+
54
+ end
55
+
43
56
  # Configure associations features
44
57
  config.nested(:associations) do |assoc|
45
58
 
@@ -56,10 +69,14 @@ module Torque
56
69
  # arguments to format string or send on a proc
57
70
  cte.send_arguments_key = :args
58
71
 
59
- # Estipulate a class name (which may contain namespace) that expose the
72
+ # Estipulate a class name (which may contain namespace) that exposes the
60
73
  # auxiliary statement in order to perform detached CTEs
61
74
  cte.exposed_class = 'TorqueCTE'
62
75
 
76
+ # Estipulate a class name (which may contain namespace) that exposes the
77
+ # recursive auxiliary statement in order to perform detached CTEs
78
+ cte.exposed_recursive_class = 'TorqueRecursiveCTE'
79
+
63
80
  end
64
81
 
65
82
  # Configure ENUM features
@@ -55,7 +55,9 @@ module Torque
55
55
 
56
56
  # Check if the model's table depends on any inheritance
57
57
  def physically_inherited?
58
- @physically_inherited ||= connection.schema_cache.dependencies(
58
+ return @physically_inherited if defined?(@physically_inherited)
59
+
60
+ @physically_inherited = connection.schema_cache.dependencies(
59
61
  defined?(@table_name) ? @table_name : decorated_table_name,
60
62
  ).present?
61
63
  rescue ActiveRecord::ConnectionNotEstablished
@@ -5,16 +5,26 @@ module Torque
5
5
  module Migration
6
6
  module CommandRecorder
7
7
 
8
- # Records the rename operation for types.
8
+ # Records the rename operation for types
9
9
  def rename_type(*args, &block)
10
10
  record(:rename_type, args, &block)
11
11
  end
12
12
 
13
- # Inverts the type name.
13
+ # Inverts the type rename operation
14
14
  def invert_rename_type(args)
15
15
  [:rename_type, args.reverse]
16
16
  end
17
17
 
18
+ # Records the creation of a schema
19
+ def create_schema(*args, &block)
20
+ record(:create_schema, args, &block)
21
+ end
22
+
23
+ # Inverts the creation of a schema
24
+ def invert_create_schema(args)
25
+ [:drop_schema, [args.first]]
26
+ end
27
+
18
28
  # Records the creation of the enum to be reverted.
19
29
  def create_enum(*args, &block)
20
30
  record(:create_enum, args, &block)
@@ -30,11 +30,15 @@ module Torque
30
30
  Torque::PostgreSQL::Attributes::Enum.lookup(name).sample
31
31
  end
32
32
 
33
- # Define the exposed constant for auxiliary statements
33
+ # Define the exposed constant for both types of auxiliary statements
34
34
  if torque_config.auxiliary_statement.exposed_class.present?
35
35
  *ns, name = torque_config.auxiliary_statement.exposed_class.split('::')
36
36
  base = ns.present? ? Object.const_get(ns.join('::')) : Object
37
37
  base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement)
38
+
39
+ *ns, name = torque_config.auxiliary_statement.exposed_recursive_class.split('::')
40
+ base = ns.present? ? Object.const_get(ns.join('::')) : Object
41
+ base.const_set(name, Torque::PostgreSQL::AuxiliaryStatement::Recursive)
38
42
  end
39
43
  end
40
44
  end
@@ -10,22 +10,14 @@ module Torque
10
10
  # :nodoc:
11
11
  def auxiliary_statements_values=(value); set_value(:auxiliary_statements, value); end
12
12
 
13
- # Set use of an auxiliary statement already configurated on the model
14
- def with(*args)
15
- spawn.with!(*args)
13
+ # Set use of an auxiliary statement
14
+ def with(*args, **settings)
15
+ spawn.with!(*args, **settings)
16
16
  end
17
17
 
18
18
  # Like #with, but modifies relation in place.
19
- def with!(*args)
20
- options = args.extract_options!
21
- args.each do |table|
22
- instance = table.is_a?(Class) && table < PostgreSQL::AuxiliaryStatement \
23
- ? table.new(options) \
24
- : PostgreSQL::AuxiliaryStatement.instantiate(table, self, options)
25
-
26
- self.auxiliary_statements_values += [instance]
27
- end
28
-
19
+ def with!(*args, **settings)
20
+ instantiate_auxiliary_statements(*args, **settings)
29
21
  self
30
22
  end
31
23
 
@@ -47,8 +39,23 @@ module Torque
47
39
  # Hook arel build to add the distinct on clause
48
40
  def build_arel(*)
49
41
  arel = super
50
- subqueries = build_auxiliary_statements(arel)
51
- subqueries.nil? ? arel : arel.with(subqueries)
42
+ type = auxiliary_statement_type
43
+ sub_queries = build_auxiliary_statements(arel)
44
+ sub_queries.nil? ? arel : arel.with(*type, *sub_queries)
45
+ end
46
+
47
+ # Instantiate one or more auxiliary statements for the given +klass+
48
+ def instantiate_auxiliary_statements(*args, **options)
49
+ klass = PostgreSQL::AuxiliaryStatement
50
+ klass = klass::Recursive if options.delete(:recursive).present?
51
+
52
+ self.auxiliary_statements_values += args.map do |table|
53
+ if table.is_a?(Class) && table < klass
54
+ table.new(**options)
55
+ else
56
+ klass.instantiate(table, self, **options)
57
+ end
58
+ end
52
59
  end
53
60
 
54
61
  # Build all necessary data for auxiliary statements
@@ -59,6 +66,12 @@ module Torque
59
66
  end
60
67
  end
61
68
 
69
+ # Return recursive if any auxiliary statement is recursive
70
+ def auxiliary_statement_type
71
+ klass = PostgreSQL::AuxiliaryStatement::Recursive
72
+ :recursive if auxiliary_statements_values.any?(klass)
73
+ end
74
+
62
75
  # Throw an error showing that an auxiliary statement of the given
63
76
  # table name isn't defined
64
77
  def auxiliary_statement_error(name)
@@ -117,6 +117,12 @@ module Torque
117
117
  scopes = scoped_class.scan(/(?:::)?[A-Z][a-z]+/)
118
118
  scopes.unshift('Object::')
119
119
 
120
+ # Check if the table name comes with a schema
121
+ if table_name.include?('.')
122
+ schema, table_name = table_name.split('.')
123
+ scopes.insert(1, schema.camelize) if schema != 'public'
124
+ end
125
+
120
126
  # Consider the maximum namespaced possible model name
121
127
  max_name = table_name.tr('_', '/').camelize.split(/(::)/)
122
128
  max_name[-1] = max_name[-1].singularize
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ class TableName < Delegator
6
+ def initialize(klass, table_name)
7
+ @klass = klass
8
+ @table_name = table_name
9
+ end
10
+
11
+ def schema
12
+ return @schema if defined?(@schema)
13
+
14
+ @schema = ([@klass] + @klass.module_parents[0..-2]).find do |klass|
15
+ next unless klass.respond_to?(:schema)
16
+ break klass.schema
17
+ end
18
+ end
19
+
20
+ def to_s
21
+ schema.nil? ? @table_name : "#{schema}.#{@table_name}"
22
+ end
23
+
24
+ alias __getobj__ to_s
25
+
26
+ def ==(other)
27
+ other.to_s =~ /("?#{schema | search_path_schemes.join('|')}"?\.)?"?#{@table_name}"?/
28
+ end
29
+
30
+ def __setobj__(value)
31
+ @table_name = value
32
+ end
33
+
34
+ private
35
+
36
+ def search_path_schemes
37
+ klass.connection.schemas_search_path_sanitized
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '2.2.4'
5
+ VERSION = '2.4.0'
6
6
  end
7
7
  end
@@ -20,12 +20,13 @@ require 'torque/postgresql/associations'
20
20
  require 'torque/postgresql/attributes'
21
21
  require 'torque/postgresql/autosave_association'
22
22
  require 'torque/postgresql/auxiliary_statement'
23
- require 'torque/postgresql/base'
24
23
  require 'torque/postgresql/inheritance'
24
+ require 'torque/postgresql/base'# Needs to be after inheritance
25
25
  require 'torque/postgresql/insert_all'
26
26
  require 'torque/postgresql/migration'
27
27
  require 'torque/postgresql/relation'
28
28
  require 'torque/postgresql/reflection'
29
29
  require 'torque/postgresql/schema_cache'
30
+ require 'torque/postgresql/table_name'
30
31
 
31
32
  require 'torque/postgresql/railtie' if defined?(Rails)
data/lib/torque/range.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Torque
2
2
  module Range
3
3
  def intersection(other)
4
+ ActiveSupport::Deprecation.warn('Range extensions will be removed in future versions')
4
5
  raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
5
6
 
6
7
  new_min = self.cover?(other.min) ? other.min : other.cover?(min) ? min : nil
@@ -11,6 +12,7 @@ module Torque
11
12
  alias_method :&, :intersection
12
13
 
13
14
  def union(other)
15
+ ActiveSupport::Deprecation.warn('Range extensions will be removed in future versions')
14
16
  raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
15
17
 
16
18
  ([min, other.min].min)..([max, other.max].max)
@@ -0,0 +1,2 @@
1
+ class Category < ActiveRecord::Base
2
+ end
@@ -0,0 +1,5 @@
1
+ module Internal
2
+ class User < ActiveRecord::Base
3
+ self.schema = 'internal'
4
+ end
5
+ end
data/spec/schema.rb CHANGED
@@ -20,6 +20,9 @@ ActiveRecord::Schema.define(version: version) do
20
20
  enable_extension "pgcrypto"
21
21
  enable_extension "plpgsql"
22
22
 
23
+ # Custom schemas used in this database.
24
+ create_schema "internal", force: :cascade
25
+
23
26
  # Custom types defined in this database.
24
27
  # Note that some types may not work with other database engines. Be careful if changing database.
25
28
  create_enum "content_status", ["created", "draft", "published", "archived"], force: :cascade
@@ -66,6 +69,11 @@ ActiveRecord::Schema.define(version: version) do
66
69
  t.enum "specialty", enum_type: :specialties
67
70
  end
68
71
 
72
+ create_table "categories", force: :cascade do |t|
73
+ t.integer "parent_id"
74
+ t.string "title"
75
+ end
76
+
69
77
  create_table "texts", force: :cascade do |t|
70
78
  t.integer "user_id"
71
79
  t.string "content"
@@ -83,6 +91,7 @@ ActiveRecord::Schema.define(version: version) do
83
91
  end
84
92
 
85
93
  create_table "courses", force: :cascade do |t|
94
+ t.integer "category_id"
86
95
  t.string "title", null: false
87
96
  t.interval "duration"
88
97
  t.enum "types", enum_type: :types, array: true
@@ -117,6 +126,13 @@ ActiveRecord::Schema.define(version: version) do
117
126
  t.datetime "updated_at", null: false
118
127
  end
119
128
 
129
+ create_table "users", schema: "internal", force: :cascade do |t|
130
+ t.string "email"
131
+ t.datetime "created_at", null: false
132
+ t.datetime "updated_at", null: false
133
+ t.index ["email"], name: "index_internal_users_on_email", unique: true
134
+ end
135
+
120
136
  create_table "activities", force: :cascade do |t|
121
137
  t.integer "author_id"
122
138
  t.string "title"
data/spec/spec_helper.rb CHANGED
@@ -22,7 +22,7 @@ cleaner = ->() do
22
22
  end
23
23
 
24
24
  load File.join('schema.rb')
25
- Dir.glob(File.join('spec', '{models,factories,mocks}', '*.rb')) do |file|
25
+ Dir.glob(File.join('spec', '{models,factories,mocks}', '**', '*.rb')) do |file|
26
26
  require file[5..-4]
27
27
  end
28
28