torque-postgresql 2.2.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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