rom 0.5.0 → 0.6.0.beta1

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -15
  3. data/.rubocop_todo.yml +28 -0
  4. data/.travis.yml +8 -1
  5. data/CHANGELOG.md +40 -0
  6. data/Gemfile +10 -2
  7. data/Guardfile +12 -10
  8. data/README.md +42 -43
  9. data/Rakefile +13 -23
  10. data/lib/rom.rb +19 -27
  11. data/lib/rom/command.rb +118 -0
  12. data/lib/rom/command_registry.rb +13 -27
  13. data/lib/rom/commands.rb +1 -59
  14. data/lib/rom/commands/abstract.rb +147 -0
  15. data/lib/rom/commands/composite.rb +47 -0
  16. data/lib/rom/commands/create.rb +2 -17
  17. data/lib/rom/commands/delete.rb +5 -25
  18. data/lib/rom/commands/result.rb +5 -5
  19. data/lib/rom/commands/update.rb +3 -27
  20. data/lib/rom/constants.rb +19 -0
  21. data/lib/rom/env.rb +85 -35
  22. data/lib/rom/global.rb +173 -42
  23. data/lib/rom/header.rb +5 -5
  24. data/lib/rom/header/attribute.rb +2 -2
  25. data/lib/rom/lint/enumerable_dataset.rb +52 -0
  26. data/lib/rom/lint/linter.rb +64 -0
  27. data/lib/rom/lint/repository.rb +78 -0
  28. data/lib/rom/lint/spec.rb +20 -0
  29. data/lib/rom/lint/test.rb +98 -0
  30. data/lib/rom/mapper.rb +32 -5
  31. data/lib/rom/mapper/attribute_dsl.rb +240 -0
  32. data/lib/rom/mapper/dsl.rb +100 -0
  33. data/lib/rom/mapper/model_dsl.rb +55 -0
  34. data/lib/rom/mapper_registry.rb +8 -1
  35. data/lib/rom/memory.rb +4 -0
  36. data/lib/rom/memory/commands.rb +46 -0
  37. data/lib/rom/memory/dataset.rb +72 -0
  38. data/lib/rom/memory/relation.rb +44 -0
  39. data/lib/rom/memory/repository.rb +62 -0
  40. data/lib/rom/memory/storage.rb +57 -0
  41. data/lib/rom/model_builder.rb +44 -5
  42. data/lib/rom/processor.rb +1 -1
  43. data/lib/rom/processor/transproc.rb +109 -16
  44. data/lib/rom/reader.rb +91 -39
  45. data/lib/rom/relation.rb +165 -26
  46. data/lib/rom/relation/composite.rb +132 -0
  47. data/lib/rom/relation/curried.rb +48 -0
  48. data/lib/rom/relation/lazy.rb +173 -0
  49. data/lib/rom/relation/loaded.rb +75 -0
  50. data/lib/rom/relation/registry_reader.rb +23 -0
  51. data/lib/rom/repository.rb +93 -34
  52. data/lib/rom/setup.rb +54 -98
  53. data/lib/rom/setup/finalize.rb +85 -76
  54. data/lib/rom/setup_dsl/command.rb +36 -0
  55. data/lib/rom/setup_dsl/command_dsl.rb +34 -0
  56. data/lib/rom/setup_dsl/mapper.rb +32 -0
  57. data/lib/rom/setup_dsl/mapper_dsl.rb +30 -0
  58. data/lib/rom/setup_dsl/relation.rb +21 -0
  59. data/lib/rom/setup_dsl/setup.rb +75 -0
  60. data/lib/rom/support/array_dataset.rb +38 -0
  61. data/lib/rom/support/class_builder.rb +44 -0
  62. data/lib/rom/support/class_macros.rb +56 -0
  63. data/lib/rom/support/data_proxy.rb +102 -0
  64. data/lib/rom/support/enumerable_dataset.rb +58 -0
  65. data/lib/rom/support/inflector.rb +73 -0
  66. data/lib/rom/support/options.rb +188 -0
  67. data/lib/rom/support/registry.rb +4 -8
  68. data/lib/rom/version.rb +1 -1
  69. data/rakelib/benchmark.rake +13 -0
  70. data/rakelib/mutant.rake +16 -0
  71. data/rakelib/rubocop.rake +18 -0
  72. data/rom.gemspec +4 -7
  73. data/spec/integration/commands/create_spec.rb +32 -24
  74. data/spec/integration/commands/delete_spec.rb +15 -7
  75. data/spec/integration/commands/update_spec.rb +13 -11
  76. data/spec/integration/mappers/deep_embedded_spec.rb +4 -11
  77. data/spec/integration/mappers/definition_dsl_spec.rb +31 -44
  78. data/spec/integration/mappers/embedded_spec.rb +9 -24
  79. data/spec/integration/mappers/group_spec.rb +22 -30
  80. data/spec/integration/mappers/prefixing_attributes_spec.rb +18 -23
  81. data/spec/integration/mappers/renaming_attributes_spec.rb +23 -38
  82. data/spec/integration/mappers/symbolizing_attributes_spec.rb +18 -24
  83. data/spec/integration/mappers/wrap_spec.rb +22 -30
  84. data/spec/integration/multi_repo_spec.rb +15 -37
  85. data/spec/integration/relations/reading_spec.rb +82 -14
  86. data/spec/integration/repositories/extending_relations_spec.rb +50 -0
  87. data/spec/integration/{adapters → repositories}/setting_logger_spec.rb +6 -5
  88. data/spec/integration/setup_spec.rb +59 -62
  89. data/spec/shared/enumerable_dataset.rb +49 -0
  90. data/spec/shared/one_behavior.rb +26 -0
  91. data/spec/shared/users_and_tasks.rb +11 -23
  92. data/spec/spec_helper.rb +16 -7
  93. data/spec/support/constant_leak_finder.rb +14 -0
  94. data/spec/test/memory_repository_lint_test.rb +27 -0
  95. data/spec/unit/rom/command_registry_spec.rb +44 -0
  96. data/spec/unit/rom/commands/result_spec.rb +14 -0
  97. data/spec/unit/rom/commands_spec.rb +174 -0
  98. data/spec/unit/rom/env_spec.rb +40 -7
  99. data/spec/unit/rom/global_spec.rb +14 -0
  100. data/spec/unit/rom/{mapper_builder_spec.rb → mapper/dsl_spec.rb} +52 -38
  101. data/spec/unit/rom/mapper_spec.rb +51 -10
  102. data/spec/unit/rom/{adapter/memory → memory}/dataset_spec.rb +6 -4
  103. data/spec/unit/rom/memory/repository_spec.rb +12 -0
  104. data/spec/unit/rom/memory/storage_spec.rb +45 -0
  105. data/spec/unit/rom/model_builder_spec.rb +4 -3
  106. data/spec/unit/rom/processor/transproc_spec.rb +1 -0
  107. data/spec/unit/rom/reader_spec.rb +97 -24
  108. data/spec/unit/rom/relation/composite_spec.rb +65 -0
  109. data/spec/unit/rom/relation/lazy_spec.rb +145 -0
  110. data/spec/unit/rom/relation/loaded_spec.rb +28 -0
  111. data/spec/unit/rom/relation_spec.rb +111 -6
  112. data/spec/unit/rom/repository_spec.rb +59 -9
  113. data/spec/unit/rom/setup_spec.rb +99 -11
  114. data/spec/unit/rom/support/array_dataset_spec.rb +59 -0
  115. data/spec/unit/rom/support/class_builder_spec.rb +42 -0
  116. data/spec/unit/rom/support/enumerable_dataset_spec.rb +17 -0
  117. data/spec/unit/rom/support/inflector_spec.rb +89 -0
  118. data/spec/unit/rom/support/options_spec.rb +119 -0
  119. metadata +74 -112
  120. data/lib/rom/adapter.rb +0 -191
  121. data/lib/rom/adapter/memory.rb +0 -32
  122. data/lib/rom/adapter/memory/commands.rb +0 -31
  123. data/lib/rom/adapter/memory/dataset.rb +0 -67
  124. data/lib/rom/adapter/memory/storage.rb +0 -26
  125. data/lib/rom/commands/with_options.rb +0 -18
  126. data/lib/rom/config.rb +0 -70
  127. data/lib/rom/mapper_builder.rb +0 -52
  128. data/lib/rom/mapper_builder/mapper_dsl.rb +0 -114
  129. data/lib/rom/mapper_builder/model_dsl.rb +0 -29
  130. data/lib/rom/reader_builder.rb +0 -48
  131. data/lib/rom/relation_builder.rb +0 -62
  132. data/lib/rom/setup/base_relation_dsl.rb +0 -46
  133. data/lib/rom/setup/command_dsl.rb +0 -46
  134. data/lib/rom/setup/mapper_dsl.rb +0 -19
  135. data/lib/rom/setup/relation_dsl.rb +0 -20
  136. data/lib/rom/setup/schema_dsl.rb +0 -33
  137. data/spec/integration/adapters/extending_relations_spec.rb +0 -41
  138. data/spec/integration/commands/try_spec.rb +0 -27
  139. data/spec/integration/schema_spec.rb +0 -77
  140. data/spec/unit/config_spec.rb +0 -60
  141. data/spec/unit/rom/adapter_spec.rb +0 -79
  142. data/spec/unit/rom_spec.rb +0 -14
@@ -1,8 +1,31 @@
1
1
  module ROM
2
- # @api private
2
+ # Model builders can be used to build model classes for mappers
3
+ #
4
+ # This is used when you define a mapper and setup a model using :name option.
5
+ #
6
+ # @example
7
+ # # this will define User model for you
8
+ # class UserMapper < ROM::Mapper
9
+ # model name: 'User'
10
+ # attribute :id
11
+ # attribute :name
12
+ # end
13
+ #
14
+ # @private
3
15
  class ModelBuilder
4
- attr_reader :options, :const_name, :namespace, :klass
16
+ include Options
5
17
 
18
+ option :name, reader: true
19
+
20
+ attr_reader :const_name, :namespace, :klass
21
+
22
+ # Return model builder subclass based on type
23
+ #
24
+ # @param [Symbol] type
25
+ #
26
+ # @return [Class]
27
+ #
28
+ # @api private
6
29
  def self.[](type)
7
30
  case type
8
31
  when :poro then PORO
@@ -11,14 +34,19 @@ module ROM
11
34
  end
12
35
  end
13
36
 
37
+ # Build a model class
38
+ #
39
+ # @return [Class]
40
+ #
41
+ # @api private
14
42
  def self.call(*args)
15
43
  new(*args).call
16
44
  end
17
45
 
46
+ # @api private
18
47
  def initialize(options = {})
19
- @options = options
48
+ super
20
49
 
21
- name = options[:name]
22
50
  if name
23
51
  parts = name.split('::')
24
52
 
@@ -26,23 +54,34 @@ module ROM
26
54
 
27
55
  @namespace =
28
56
  if parts.any?
29
- Inflecto.constantize(parts.join('::'))
57
+ Inflector.constantize(parts.join('::'))
30
58
  else
31
59
  Object
32
60
  end
33
61
  end
34
62
  end
35
63
 
64
+ # Define a model class constant
65
+ #
66
+ # @api private
36
67
  def define_const
37
68
  namespace.const_set(const_name, klass)
38
69
  end
39
70
 
71
+ # Build a model class supporting specific attributes
72
+ #
73
+ # @return [Class]
74
+ #
75
+ # @api private
40
76
  def call(attrs)
41
77
  define_class(attrs)
42
78
  define_const if const_name
43
79
  @klass
44
80
  end
45
81
 
82
+ # PORO model class builder
83
+ #
84
+ # @private
46
85
  class PORO < ModelBuilder
47
86
  def define_class(attrs)
48
87
  @klass = Class.new
data/lib/rom/processor.rb CHANGED
@@ -5,7 +5,7 @@ module ROM
5
5
  #
6
6
  # Every ROM processor should inherit from this class
7
7
  #
8
- # @public
8
+ # @api public
9
9
  class Processor
10
10
  # Hook used to auto-register a processor class
11
11
  #
@@ -4,66 +4,146 @@ require 'rom/processor'
4
4
 
5
5
  module ROM
6
6
  class Processor
7
+ # Data mapping transformer builder using Transproc
8
+ #
9
+ # This builds a transproc function that is used to map a whole relation
10
+ #
11
+ # @see https://github.com/solnic/transproc too
12
+ #
13
+ # @private
7
14
  class Transproc < Processor
8
15
  include ::Transproc::Composer
9
16
 
10
- attr_reader :header, :model, :mapping, :tuple_proc
17
+ # @return [Header] header from a mapper
18
+ #
19
+ # @api private
20
+ attr_reader :header
11
21
 
22
+ # @return [Class] model class from a mapper
23
+ #
24
+ # @api private
25
+ attr_reader :model
26
+
27
+ # @return [Hash] header's attribute mapping
28
+ #
29
+ # @api private
30
+ attr_reader :mapping
31
+
32
+ # @return [Proc] row-processing proc
33
+ #
34
+ # @api private
35
+ attr_reader :row_proc
36
+
37
+ # Default no-op row_proc
12
38
  EMPTY_FN = -> tuple { tuple }.freeze
13
39
 
40
+ # Build a transproc function from the header
41
+ #
42
+ # @param [ROM::Header] header
43
+ #
44
+ # @return [Transproc::Function]
45
+ #
46
+ # @api private
14
47
  def self.build(header)
15
48
  new(header).to_transproc
16
49
  end
17
50
 
51
+ # @api private
18
52
  def initialize(header)
19
53
  @header = header
20
54
  @model = header.model
21
55
  @mapping = header.mapping
22
- initialize_tuple_proc
56
+ initialize_row_proc
23
57
  end
24
58
 
59
+ # Coerce mapper header to a transproc data mapping function
60
+ #
61
+ # @return [Transproc::Function]
62
+ #
63
+ # @api private
25
64
  def to_transproc
26
65
  compose(EMPTY_FN) do |ops|
27
66
  ops << header.groups.map { |attr| visit_group(attr, true) }
28
- ops << t(:map_array!, tuple_proc) if tuple_proc
67
+ ops << t(:map_array, row_proc) if row_proc
29
68
  end
30
69
  end
31
70
 
32
71
  private
33
72
 
73
+ # Visit an attribute from the header
74
+ #
75
+ # This forwards to a specialized visitor based on the attribute type
76
+ #
77
+ # @param [Header::Attribute] attribute
78
+ #
79
+ # @api private
34
80
  def visit(attribute)
35
81
  type = attribute.class.name.split('::').last.downcase
36
82
  send("visit_#{type}", attribute)
37
83
  end
38
84
 
85
+ # Visit plain attribute
86
+ #
87
+ # If it's a typed attribute a coercion transformation is added
88
+ #
89
+ # @param [Header::Attribute] attribute
90
+ #
91
+ # @api private
39
92
  def visit_attribute(attribute)
40
93
  if attribute.typed?
41
- t(:map_key!, attribute.name, t(:"to_#{attribute.type}"))
94
+ t(:map_key, attribute.name, t(:"to_#{attribute.type}"))
42
95
  end
43
96
  end
44
97
 
98
+ # Visit hash attribute
99
+ #
100
+ # @param [Header::Attribute::Hash] attribute
101
+ #
102
+ # @api private
45
103
  def visit_hash(attribute)
46
- with_tuple_proc(attribute) do |tuple_proc|
47
- t(:map_key!, attribute.name, tuple_proc)
104
+ with_row_proc(attribute) do |row_proc|
105
+ t(:map_key, attribute.name, row_proc)
48
106
  end
49
107
  end
50
108
 
109
+ # Visit array attribute
110
+ #
111
+ # @param [Header::Attribute::Array] attribute
112
+ #
113
+ # @api private
51
114
  def visit_array(attribute)
52
- with_tuple_proc(attribute) do |tuple_proc|
53
- t(:map_key!, attribute.name, t(:map_array!, tuple_proc))
115
+ with_row_proc(attribute) do |row_proc|
116
+ t(:map_key, attribute.name, t(:map_array, row_proc))
54
117
  end
55
118
  end
56
119
 
120
+ # Visit wrapped hash attribute
121
+ #
122
+ # :nest transformation is added to handle wrapping
123
+ #
124
+ # @param [Header::Attribute::Wrap] attribute
125
+ #
126
+ # @api private
57
127
  def visit_wrap(attribute)
58
128
  name = attribute.name
59
129
  keys = attribute.tuple_keys
60
130
 
61
131
  compose do |ops|
62
- ops << t(:nest!, name, keys)
132
+ ops << t(:nest, name, keys)
63
133
  ops << visit_hash(attribute)
64
134
  end
65
135
  end
66
136
 
137
+ # Visit group hash attribute
138
+ #
139
+ # :group transformation is added to handle grouping during preprocessing.
140
+ # Otherwise we simply use array visitor for the attribute.
141
+ #
142
+ # @param [Header::Attribute::Group] attribute
143
+ # @param [Boolean] preprocess true if we are building a relation preprocessing
144
+ # function that is applied to the whole relation
145
+ #
146
+ # @api private
67
147
  def visit_group(attribute, preprocess = false)
68
148
  if preprocess
69
149
  name = attribute.name
@@ -76,7 +156,7 @@ module ROM
76
156
  ops << t(:group, name, keys)
77
157
 
78
158
  ops << other.map { |attr|
79
- t(:map_array!, t(:map_key!, name, visit_group(attr, true)))
159
+ t(:map_array, t(:map_key, name, visit_group(attr, true)))
80
160
  }
81
161
  end
82
162
  else
@@ -84,19 +164,32 @@ module ROM
84
164
  end
85
165
  end
86
166
 
87
- def initialize_tuple_proc
88
- @tuple_proc = compose do |ops|
89
- ops << t(:map_hash!, mapping) if header.aliased?
167
+ # Build row_proc
168
+ #
169
+ # This transproc function is applied to each row in a dataset
170
+ #
171
+ # @api private
172
+ def initialize_row_proc
173
+ @row_proc = compose do |ops|
174
+ ops << t(:map_hash, mapping) if header.aliased?
90
175
  ops << header.map { |attr| visit(attr) }
91
176
  ops << t(-> tuple { model.new(tuple) }) if model
92
177
  end
93
178
  end
94
179
 
95
- def with_tuple_proc(attribute)
96
- tuple_proc = new(attribute.header).tuple_proc
97
- yield(tuple_proc) if tuple_proc
180
+ # Yield row proc for a given attribute if any
181
+ #
182
+ # @param [Header::Attribute] attribute
183
+ #
184
+ # @api private
185
+ def with_row_proc(attribute)
186
+ row_proc = new(attribute.header).row_proc
187
+ yield(row_proc) if row_proc
98
188
  end
99
189
 
190
+ # Return a new instance of the processor
191
+ #
192
+ # @api private
100
193
  def new(*args)
101
194
  self.class.new(*args)
102
195
  end
data/lib/rom/reader.rb CHANGED
@@ -5,11 +5,16 @@ module ROM
5
5
  #
6
6
  # @api public
7
7
  class Reader
8
- MapperMissingError = Class.new(StandardError)
9
-
10
8
  include Enumerable
11
9
  include Equalizer.new(:path, :relation, :mapper)
12
10
 
11
+ # Map relation to an array using a mapper
12
+ #
13
+ # @return [Array]
14
+ #
15
+ # @api public
16
+ alias_method :to_ary, :to_a
17
+
13
18
  # @return [String] access path used to read a relation
14
19
  #
15
20
  # @api private
@@ -30,58 +35,55 @@ module ROM
30
35
  # @api private
31
36
  attr_reader :mapper
32
37
 
33
- # Build a reader subclass for the relation and instantiate it
34
- #
35
- # This method defines public methods on the class narrowing down data access
36
- # only to the methods exposed by a given relation
38
+ # Builds a reader instance for the provided relation
37
39
  #
38
40
  # @param [Symbol] name of the root relation
39
41
  # @param [Relation] relation that the reader will use
40
- # @param [MapperRegistry] registry of mappers
41
- # @param [Array<Symbol>] a list of method names exposed by the relation
42
+ # @param [MapperRegistry] mappers registry of mappers
43
+ # @param [Array<Symbol>] method_names a list of method names exposed by the relation
42
44
  #
43
45
  # @return [Reader]
44
46
  #
45
47
  # @api private
46
48
  def self.build(name, relation, mappers, method_names = [])
47
- klass = Class.new(self)
48
-
49
- klass_name = "#{self.name}[#{Inflecto.camelize(relation.name)}]"
50
-
51
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
52
- def self.name
53
- #{klass_name.inspect}
54
- end
55
-
56
- def self.inspect
57
- name
58
- end
49
+ klass = build_class(relation, method_names)
50
+ klass.new(name, relation, mappers)
51
+ end
59
52
 
60
- def self.to_s
61
- name
53
+ # Build a reader subclass for the relation
54
+ #
55
+ # This method defines public methods on the class narrowing down data access
56
+ # only to the methods exposed by a given relation
57
+ #
58
+ # @param [Relation] relation that the reader will use
59
+ # @param [Array<Symbol>] method_names a list of method names exposed by the relation
60
+ #
61
+ # @return [Class]
62
+ #
63
+ # @api private
64
+ def self.build_class(relation, method_names)
65
+ klass_name = "#{Reader.name}[#{Inflector.camelize(relation.name)}]"
66
+
67
+ ClassBuilder.new(name: klass_name, parent: Reader).call do |klass|
68
+ method_names.each do |method_name|
69
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
70
+ def #{method_name}(*args, &block)
71
+ new_relation = relation.send(#{method_name.inspect}, *args, &block)
72
+ self.class.new(
73
+ new_path(#{method_name.to_s.inspect}), new_relation, mappers
74
+ )
75
+ end
76
+ RUBY
62
77
  end
63
- RUBY
64
-
65
- method_names.each do |method_name|
66
- klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
67
- def #{method_name}(*args, &block)
68
- new_relation = relation.send(#{method_name.inspect}, *args, &block)
69
- self.class.new(
70
- new_path(#{method_name.to_s.inspect}), new_relation, mappers
71
- )
72
- end
73
- RUBY
74
78
  end
75
-
76
- klass.new(name, relation, mappers)
77
79
  end
78
80
 
79
81
  # @api private
80
- def initialize(path, relation, mappers = {})
82
+ def initialize(path, relation, mappers, mapper = nil)
81
83
  @path = path.to_s
82
84
  @relation = relation
83
85
  @mappers = mappers
84
- @mapper = mappers.by_path(@path) || raise(MapperMissingError, path)
86
+ @mapper = mapper || mappers.by_path(@path)
85
87
  end
86
88
 
87
89
  # @api private
@@ -101,13 +103,63 @@ module ROM
101
103
  #
102
104
  # @api public
103
105
  def each
104
- mapper.process(relation) { |tuple| yield(tuple) }
106
+ mapper.call(relation).each { |tuple| yield(tuple) }
107
+ end
108
+
109
+ # Returns a single tuple from the relation if there is one.
110
+ #
111
+ # @raise [ROM::TupleCountMismatchError] if the relation contains more than
112
+ # one tuple
113
+ #
114
+ # @api public
115
+ def one
116
+ if relation.count > 1
117
+ raise(
118
+ TupleCountMismatchError,
119
+ 'The relation consists of more than one tuple'
120
+ )
121
+ else
122
+ mapper.call(relation).first
123
+ end
124
+ end
125
+
126
+ # Like [one], but additionally raises an error if the relation is empty.
127
+ #
128
+ # @raise [ROM::TupleCountMismatchError] if the relation does not contain
129
+ # exactly one tuple
130
+ #
131
+ # @api public
132
+ def one!
133
+ one || raise(
134
+ TupleCountMismatchError,
135
+ 'The relation does not contain any tuples'
136
+ )
137
+ end
138
+
139
+ # Map tuples using a specific mapper if name is provided
140
+ #
141
+ # Defaults to Enumerable#map behavior
142
+ #
143
+ # @example
144
+ #
145
+ # rom.read(:users).map(:my_mapper_name)
146
+ # rom.read(:users).map { |user| ... }
147
+ #
148
+ # @return [Array,Reader]
149
+ #
150
+ # @api public
151
+ def map(*args)
152
+ if args.any?
153
+ mappers[args[0]].call(relation)
154
+ else
155
+ super
156
+ end
105
157
  end
106
158
 
107
159
  private
108
160
 
109
161
  # @api private
110
- def method_missing(name)
162
+ def method_missing(name, *)
111
163
  raise(
112
164
  NoRelationError,
113
165
  "undefined relation #{name.inspect} within #{path.inspect}"