dsl_compose 2.15.3 → 2.15.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fd1fd8c1e9fa1f20c3a46fa9d27bd9aca1cf5f4529e8faa2e86ba5b972ef211
4
- data.tar.gz: ca41bb7b1ef3f699b0d96baf77dee95fb27a7fcf0d26b7dcc37838b3a575dbff
3
+ metadata.gz: e0750c158e424591df6b08e2ac3786b689116b5850152b6f6fe51fc39092895a
4
+ data.tar.gz: 7e818d88dd0dde898b80faa4eb24f18eaa6ca8bd64a4c83fc974621ccee71e3e
5
5
  SHA512:
6
- metadata.gz: 53328f870487ad66a45bf7d179a5085d39049a0eb02eefacaa7e38a386bdd2fa043cd9fb7a887888ed53156d6b6cdf9fe828e19c53fd0c768f8e81d3b666ec8d
7
- data.tar.gz: d904bb0b9c0188755031e678099fa8644f5712433eea0471b0f581ea1247d6f29725766f638383d412bfb0fa7a187d1bcb5bba51ec61fd725ab171446cc87e7b
6
+ metadata.gz: a374a44e85a232cc6681c98034a1196cc7ccc077689a3f9bb75354aa54e0e02be8758a527de68944af95c81f04157b07df4bcaccab20877cfee48f2164eddad2
7
+ data.tar.gz: 73fd1c43e1677b42c2389a4919e0ced10feda5e35ca564d1d025376fbb357282f5d8bf7411980d3d9d744c46b6d50cbc2fa370314ad0c4de58093eec569fc53d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.15.5](https://github.com/craigulliott/dsl_compose/compare/v2.15.4...v2.15.5) (2023-10-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * added a first_use_only option to the parser so that only the most recent execution of a DSL will be used ([717293d](https://github.com/craigulliott/dsl_compose/commit/717293d906a183d8a13ffa1c4ae47e901de428a0))
9
+ * specs and fixes for `first_use_only` option on parser ([a7dc087](https://github.com/craigulliott/dsl_compose/commit/a7dc0878ffef9a104c25d4f43d5796f939cb7567))
10
+
11
+ ## [2.15.4](https://github.com/craigulliott/dsl_compose/compare/v2.15.3...v2.15.4) (2023-10-04)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * passing context along with where DSL's were defined, and then including this context in error messages ([5238a3d](https://github.com/craigulliott/dsl_compose/commit/5238a3daa8c1a32e77783d56c7c429b6335995bd))
17
+
3
18
  ## [2.15.3](https://github.com/craigulliott/dsl_compose/compare/v2.15.2...v2.15.3) (2023-10-03)
4
19
 
5
20
 
@@ -48,9 +48,11 @@ module DSLCompose
48
48
 
49
49
  # add a singleton method with the name of this new DSL onto our class, this is how our new DSL will be accessed
50
50
  define_singleton_method name do |*args, &block|
51
+ called_from = caller(1..1).first
52
+
51
53
  # when it is called, we process this new dynamic DSL with the interpreter
52
54
  # `self` here is the class in which the dsl is being used, not the class in which the DSL was defined
53
- interpreter.execute_dsl self, dsl, *args, &block
55
+ interpreter.execute_dsl self, dsl, called_from, *args, &block
54
56
  end
55
57
 
56
58
  end
@@ -127,7 +127,7 @@ module DSLCompose
127
127
  Interpreter.new(self).instance_eval(&block)
128
128
  end
129
129
  rescue => e
130
- raise e, "Error defining argument #{name}: #{e.message}", e.backtrace
130
+ raise e, "Error while defining argument #{name}\n#{e.message}", e.backtrace
131
131
  end
132
132
 
133
133
  # Set the description for this Argument to the provided value.
@@ -60,7 +60,7 @@ module DSLCompose
60
60
  Interpreter.new(self).instance_eval(&block)
61
61
  end
62
62
  rescue => e
63
- raise e, "Error defining method #{name}: #{e.message}", e.backtrace
63
+ raise e, "Error while defining method #{name}\n#{e.message}", e.backtrace
64
64
  end
65
65
 
66
66
  # Set the description for this DSLMethod to the provided value.
@@ -83,8 +83,6 @@ module DSLCompose
83
83
  else
84
84
  raise NoBlockProvidedError, "No block was provided for this DSL"
85
85
  end
86
- rescue => e
87
- raise e, "Error defining DSL #{@name} on #{@klass}: #{e.message}", e.backtrace
88
86
  end
89
87
 
90
88
  # Set the description for this DSL to the provided value.
@@ -4,25 +4,27 @@ module DSLCompose
4
4
  class Interpreter
5
5
  class Execution
6
6
  class Arguments
7
- class MissingRequiredArgumentsError < StandardError
7
+ class MissingRequiredArgumentsError < InterpreterError
8
8
  end
9
9
 
10
- class TooManyArgumentsError < StandardError
10
+ class TooManyArgumentsError < InterpreterError
11
11
  end
12
12
 
13
- class OptionalArgumentsShouldBeHashError < StandardError
13
+ class OptionalArgumentsShouldBeHashError < InterpreterError
14
14
  end
15
15
 
16
- class InvalidArgumentTypeError < StandardError
16
+ class InvalidArgumentTypeError < InterpreterError
17
17
  end
18
18
 
19
- class ArrayNotValidError < StandardError
19
+ class ArrayNotValidError < InterpreterError
20
20
  end
21
21
 
22
22
  attr_reader :arguments
23
+ attr_reader :called_from
23
24
 
24
- def initialize arguments, *args
25
+ def initialize arguments, called_from, *args
25
26
  @arguments = {}
27
+ @called_from = called_from
26
28
 
27
29
  required_argument_count = arguments.required_arguments.count
28
30
  required_non_kwarg_argument_count = arguments.required_arguments.count { |a| !a.kwarg }
@@ -40,7 +42,7 @@ module DSLCompose
40
42
  required_kwargs = arguments.required_arguments.filter { |a| a.kwarg }
41
43
 
42
44
  if required_kwargs.any? && !all_kwargs.is_a?(Hash)
43
- raise MissingRequiredArgumentsError, "This has required keyword arguments, but no keyword arguments were provided"
45
+ raise MissingRequiredArgumentsError.new("This has required keyword arguments, but no keyword arguments were provided", called_from)
44
46
  end
45
47
 
46
48
  required_kwargs.each do |required_kwarg|
@@ -51,7 +53,7 @@ module DSLCompose
51
53
  # left with only the optional args
52
54
  all_kwargs.delete required_kwarg.name
53
55
  else
54
- raise MissingRequiredArgumentsError, "The required kwarg `#{required_kwarg.name}` was not provided"
56
+ raise MissingRequiredArgumentsError.new("The required kwarg `#{required_kwarg.name}` was not provided", called_from)
55
57
  end
56
58
  end
57
59
 
@@ -61,12 +63,12 @@ module DSLCompose
61
63
 
62
64
  # assert that a value is provided for every required argument
63
65
  unless required_argument_count == required_args.count
64
- raise MissingRequiredArgumentsError, "This requires #{required_non_kwarg_argument_count} arguments, but only #{required_args.count} were provided"
66
+ raise MissingRequiredArgumentsError.new("This requires #{required_non_kwarg_argument_count} arguments, but only #{required_args.count} were provided", called_from)
65
67
  end
66
68
 
67
69
  # assert that too many arguments have not been provided
68
70
  if args.count > required_argument_count + (has_optional_arguments ? 1 : 0)
69
- raise TooManyArgumentsError, "Too many arguments provided"
71
+ raise TooManyArgumentsError.new("Too many arguments provided", called_from)
70
72
  end
71
73
 
72
74
  # Assume all optonal arguments are their defaults (except booleans, which default to false).
@@ -88,7 +90,7 @@ module DSLCompose
88
90
  # asset that, if provided, then the optional argument (always the last one) is a Hash
89
91
  if has_optional_arguments && !optional_arg.nil?
90
92
  unless optional_arg.is_a? Hash
91
- raise OptionalArgumentsShouldBeHashError, "If provided, then the optional arguments must be last, and be represented as a Hash"
93
+ raise OptionalArgumentsShouldBeHashError.new("If provided, then the optional arguments must be last, and be represented as a Hash", called_from)
92
94
  end
93
95
 
94
96
  # assert the each provided optional argument is valid
@@ -110,7 +112,7 @@ module DSLCompose
110
112
  end
111
113
 
112
114
  if optional_arg_value.is_a?(Array) && !optional_argument.array
113
- raise ArrayNotValidError, "An array was provided to an argument which does not accept an array of values"
115
+ raise ArrayNotValidError.new("An array was provided to an argument which does not accept an array of values", called_from)
114
116
  end
115
117
 
116
118
  # to simplify the code, we always process the reset of the validations as an array, even
@@ -121,38 +123,38 @@ module DSLCompose
121
123
  case optional_argument.type
122
124
  when :integer
123
125
  unless value.is_a? Integer
124
- raise InvalidArgumentTypeError, "`#{value}` is not an Integer"
126
+ raise InvalidArgumentTypeError.new("`#{value}` is not an Integer", called_from)
125
127
  end
126
128
  optional_argument.validate_integer! value
127
129
 
128
130
  when :float
129
131
  # float allows either floats or integers
130
132
  unless value.is_a?(Float) || value.is_a?(Integer)
131
- raise InvalidArgumentTypeError, "`#{value}` is not an Float or Integer"
133
+ raise InvalidArgumentTypeError.new("`#{value}` is not an Float or Integer", called_from)
132
134
  end
133
135
  optional_argument.validate_float! value
134
136
 
135
137
  when :symbol
136
138
  unless value.is_a? Symbol
137
- raise InvalidArgumentTypeError, "`#{value}` is not a Symbol"
139
+ raise InvalidArgumentTypeError.new("`#{value}` is not a Symbol", called_from)
138
140
  end
139
141
  optional_argument.validate_symbol! value
140
142
 
141
143
  when :string
142
144
  unless value.is_a? String
143
- raise InvalidArgumentTypeError, "`#{value}` is not a String"
145
+ raise InvalidArgumentTypeError.new("`#{value}` is not a String", called_from)
144
146
  end
145
147
  optional_argument.validate_string! value
146
148
 
147
149
  when :boolean
148
150
  unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
149
- raise InvalidArgumentTypeError, "`#{value}` is not a boolean"
151
+ raise InvalidArgumentTypeError.new("`#{value}` is not a boolean", called_from)
150
152
  end
151
153
  optional_argument.validate_boolean! value
152
154
 
153
155
  when :class
154
156
  unless value.is_a?(ClassCoerce)
155
- raise InvalidArgumentTypeError, "`#{value}` is not a class coerce (String)"
157
+ raise InvalidArgumentTypeError.new("`#{value}` is not a class coerce (String)", called_from)
156
158
  end
157
159
  optional_argument.validate_class! value
158
160
 
@@ -160,7 +162,7 @@ module DSLCompose
160
162
  optional_argument.validate_object! value
161
163
 
162
164
  else
163
- raise InvalidArgumentTypeError, "The argument value `#{value}` is not a supported type"
165
+ raise InvalidArgumentTypeError.new("The argument value `#{value}` is not a supported type", called_from)
164
166
  end
165
167
  end
166
168
 
@@ -174,7 +176,7 @@ module DSLCompose
174
176
  optional_arg_value
175
177
  end
176
178
  rescue => e
177
- raise e, "Error processing optional argument #{optional_argument_name}: #{e.message}", e.backtrace
179
+ raise e, "Error processing optional argument #{optional_argument_name}\n#{e.message}", e.backtrace
178
180
  end
179
181
 
180
182
  end
@@ -200,7 +202,7 @@ module DSLCompose
200
202
  end
201
203
 
202
204
  if required_arg_value.is_a?(Array) && !required_argument.array
203
- raise ArrayNotValidError, "An array was provided to an argument which does not accept an array of values"
205
+ raise ArrayNotValidError.new("An array was provided to an argument which does not accept an array of values", called_from)
204
206
  end
205
207
 
206
208
  # to simplify the code, we always process the reset of the validations as an array, even
@@ -211,38 +213,38 @@ module DSLCompose
211
213
  case required_argument.type
212
214
  when :integer
213
215
  unless value.is_a? Integer
214
- raise InvalidArgumentTypeError, "`#{value}` is not an Integer"
216
+ raise InvalidArgumentTypeError.new("`#{value}` is not an Integer", called_from)
215
217
  end
216
218
  required_argument.validate_integer! value
217
219
 
218
220
  when :float
219
221
  # float allows either floats or integers
220
222
  unless value.is_a?(Float) || value.is_a?(Integer)
221
- raise InvalidArgumentTypeError, "`#{value}` is not an Float or Integer"
223
+ raise InvalidArgumentTypeError.new("`#{value}` is not an Float or Integer", called_from)
222
224
  end
223
225
  required_argument.validate_float! value
224
226
 
225
227
  when :symbol
226
228
  unless value.is_a? Symbol
227
- raise InvalidArgumentTypeError, "`#{value}` is not a Symbol"
229
+ raise InvalidArgumentTypeError.new("`#{value}` is not a Symbol", called_from)
228
230
  end
229
231
  required_argument.validate_symbol! value
230
232
 
231
233
  when :string
232
234
  unless value.is_a? String
233
- raise InvalidArgumentTypeError, "`#{value}` is not a String"
235
+ raise InvalidArgumentTypeError.new("`#{value}` is not a String", called_from)
234
236
  end
235
237
  required_argument.validate_string! value
236
238
 
237
239
  when :boolean
238
240
  unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
239
- raise InvalidArgumentTypeError, "`#{value}` is not a boolean"
241
+ raise InvalidArgumentTypeError.new("`#{value}` is not a boolean", called_from)
240
242
  end
241
243
  required_argument.validate_boolean! value
242
244
 
243
245
  when :class
244
246
  unless value.is_a?(ClassCoerce)
245
- raise InvalidArgumentTypeError, "`#{value}` is not a class coerce (String)"
247
+ raise InvalidArgumentTypeError.new("`#{value}` is not a class coerce (String)", called_from)
246
248
  end
247
249
  required_argument.validate_class! value
248
250
 
@@ -250,7 +252,7 @@ module DSLCompose
250
252
  required_argument.validate_object! value
251
253
 
252
254
  else
253
- raise InvalidArgumentTypeError, "The argument `#{value}` is not a supported type"
255
+ raise InvalidArgumentTypeError.new("The argument `#{value}` is not a supported type", called_from)
254
256
  end
255
257
  end
256
258
 
@@ -264,7 +266,7 @@ module DSLCompose
264
266
  required_arg_value
265
267
  end
266
268
  rescue => e
267
- raise e, "Error processing required argument #{argument_name}: #{e.message}", e.backtrace
269
+ raise e, "Error processing required argument #{argument_name}\n#{e.message}", e.backtrace
268
270
  end
269
271
  end
270
272
 
@@ -5,15 +5,17 @@ module DSLCompose
5
5
  class Execution
6
6
  class MethodCalls
7
7
  class MethodCall
8
- class InvalidDescriptionError < StandardError
8
+ class InvalidDescriptionError < InterpreterError
9
9
  end
10
10
 
11
11
  attr_reader :dsl_method
12
+ attr_reader :called_from
12
13
  attr_reader :arguments
13
14
 
14
- def initialize dsl_method, *args, &block
15
+ def initialize dsl_method, called_from, *args, &block
15
16
  @dsl_method = dsl_method
16
- @arguments = Arguments.new(dsl_method.arguments, *args)
17
+ @called_from = called_from
18
+ @arguments = Arguments.new(dsl_method.arguments, called_from, *args)
17
19
  end
18
20
 
19
21
  def method_name
@@ -30,7 +32,7 @@ module DSLCompose
30
32
  # generate documentation
31
33
  def add_parser_usage_note note
32
34
  unless note.is_a?(String) && note.strip.length > 0
33
- raise InvalidDescriptionError, "The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0"
35
+ raise InvalidDescriptionError.new("The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0", @called_from)
34
36
  end
35
37
 
36
38
  @parser_usage_notes ||= []
@@ -14,16 +14,18 @@ module DSLCompose
14
14
  @method_calls.filter { |mc| mc.method_name == method_name }.any?
15
15
  end
16
16
 
17
- def add_method_call(dsl_method, ...)
17
+ def add_method_call(dsl_method, called_from, ...)
18
18
  # make sure we always have a variable which can be used in the exception message
19
+ # set to nil first, so that if we get an exception while setting them
20
+ # it wont break the error message generation
19
21
  dsl_method_name = nil
20
22
  dsl_method_name = dsl_method.name
21
23
 
22
- method_call = MethodCall.new(dsl_method, ...)
24
+ method_call = MethodCall.new(dsl_method, called_from, ...)
23
25
  @method_calls << method_call
24
26
  method_call
25
27
  rescue => e
26
- raise e, "Error while executing method #{dsl_method_name}: #{e.message}", e.backtrace
28
+ raise e, "Error while executing method #{dsl_method_name}\n#{e.message}", e.backtrace
27
29
  end
28
30
 
29
31
  def method_calls_by_name method_name
@@ -3,26 +3,28 @@
3
3
  module DSLCompose
4
4
  class Interpreter
5
5
  class Execution
6
- class MethodIsUniqueError < StandardError
6
+ class MethodIsUniqueError < InterpreterError
7
7
  end
8
8
 
9
- class RequiredMethodNotCalledError < StandardError
9
+ class RequiredMethodNotCalledError < InterpreterError
10
10
  end
11
11
 
12
- class InvalidDescriptionError < StandardError
12
+ class InvalidDescriptionError < InterpreterError
13
13
  end
14
14
 
15
15
  attr_reader :dsl
16
+ attr_reader :called_from
16
17
  attr_reader :klass
17
18
  attr_reader :method_calls
18
19
  attr_reader :arguments
19
20
 
20
21
  # execute/process a dynamically defined DSL
21
- def initialize klass, dsl, *args, &block
22
+ def initialize klass, dsl, called_from, *args, &block
22
23
  @klass = klass
23
24
  @dsl = dsl
25
+ @called_from = called_from
24
26
  @method_calls = MethodCalls.new
25
- @arguments = Arguments.new(dsl.arguments, *args)
27
+ @arguments = Arguments.new(dsl.arguments, called_from, *args)
26
28
 
27
29
  # dynamically process the DSL by calling the provided block
28
30
  # all methods executions will be caught and processed by the method_missing method below
@@ -34,7 +36,7 @@ module DSLCompose
34
36
  dsl.required_dsl_methods.each do |dsl_method|
35
37
  unless @method_calls.method_called? dsl_method.name
36
38
  dsl_method_name = dsl_method&.name
37
- raise RequiredMethodNotCalledError, "The method #{dsl_method_name} is required, but was not called within this DSL"
39
+ raise RequiredMethodNotCalledError.new("The method #{dsl_method_name} is required, but was not called within this DSL", called_from)
38
40
  end
39
41
  end
40
42
  end
@@ -43,7 +45,7 @@ module DSLCompose
43
45
  # generate documentation
44
46
  def add_parser_usage_note note
45
47
  unless note.is_a?(String) && note.strip.length > 0
46
- raise InvalidDescriptionError, "The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0"
48
+ raise InvalidDescriptionError.new("The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0", called_from)
47
49
  end
48
50
 
49
51
  @parser_usage_notes ||= []
@@ -60,15 +62,17 @@ module DSLCompose
60
62
 
61
63
  # catch and process any method calls within the DSL block
62
64
  def method_missing(method_name, ...)
65
+ called_from = caller(1..1).first
66
+
63
67
  # if the method does not exist, then this will raise a MethodDoesNotExistError
64
68
  dsl_method = @dsl.dsl_method method_name
65
69
 
66
70
  # if the method is unique, then it can only be called once per DSL
67
71
  if dsl_method.unique? && @method_calls.method_called?(method_name)
68
- raise MethodIsUniqueError, "This method `#{method_name}` is unique and can only be called once within this DSL"
72
+ raise MethodIsUniqueError.new("This method `#{method_name}` is unique and can only be called once within this DSL", called_from)
69
73
  end
70
74
 
71
- @method_calls.add_method_call(dsl_method, ...)
75
+ @method_calls.add_method_call(dsl_method, called_from, ...)
72
76
  end
73
77
 
74
78
  def respond_to_missing?(method_name, *args)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Interpreter
5
+ class InterpreterError < StandardError
6
+ attr_reader :original_context
7
+
8
+ def initialize(message, original_context)
9
+ super(message)
10
+ @original_context = original_context
11
+ end
12
+
13
+ def to_s
14
+ "#{super}\ndsl source: #{@original_context}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,10 +4,7 @@ module DSLCompose
4
4
  # The class is reponsible for parsing and executing a dynamic DSL (dynamic DSLs are
5
5
  # created using the DSLCompose::DSL class).
6
6
  class Interpreter
7
- class DSLExecutionNotFoundError < StandardError
8
- end
9
-
10
- class InvalidDescriptionError < StandardError
7
+ class InvalidDescriptionError < InterpreterError
11
8
  end
12
9
 
13
10
  # A dynamic DSL can be used multiple times on the same class, each time the DSL is used
@@ -23,7 +20,7 @@ module DSLCompose
23
20
  # generate documentation
24
21
  def add_parser_usage_note child_class, note
25
22
  unless note.is_a?(String) && note.strip.length > 0
26
- raise InvalidDescriptionError, "The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0"
23
+ raise InvalidDescriptionError.new("The parser usage description `#{note}` is invalid, it must be of type string and have length greater than 0", called_from)
27
24
  end
28
25
  @parser_usage_notes ||= {}
29
26
  @parser_usage_notes[child_class] ||= []
@@ -40,20 +37,23 @@ module DSLCompose
40
37
  # Execute/process a dynamically defined DSL on a class.
41
38
  # `klass` is the class in which the DSL is being used, not
42
39
  # the class in which the DSL was defined.
43
- def execute_dsl(klass, dsl, ...)
40
+ def execute_dsl(klass, dsl, called_from, ...)
44
41
  # make sure we have these variables for the exception message below
42
+ # set to nil first, so that if we get an exception while setting them
43
+ # it wont break the error message generation
45
44
  class_name = nil
46
- class_name = klass.name
47
45
  dsl_name = nil
46
+ class_name = klass.name
48
47
  dsl_name = dsl.name
49
48
 
50
- execution = Execution.new(klass, dsl, ...)
49
+ execution = Execution.new(klass, dsl, called_from, ...)
51
50
  @executions << execution
52
51
  execution
53
52
  rescue => e
54
- raise e, "Error processing dsl #{dsl_name} for class #{class_name}: #{e.message}", e.backtrace
53
+ raise e, "Error while defining DSL #{dsl_name} for class #{class_name}:\n#{e.message}", e.backtrace
55
54
  end
56
55
 
56
+ #
57
57
  # Returns an array of all executions for a given class.
58
58
  def class_executions klass
59
59
  @executions.filter { |e| e.klass == klass }
@@ -66,8 +66,15 @@ module DSLCompose
66
66
 
67
67
  # Returns an array of all executions for a given name and class. This includes
68
68
  # any ancestors of the provided class
69
- def class_dsl_executions klass, dsl_name, on_current_class, on_ancestor_class
70
- @executions.filter { |e| e.dsl.name == dsl_name && ((on_current_class && e.klass == klass) || (on_ancestor_class && klass < e.klass)) }
69
+ def class_dsl_executions klass, dsl_name, on_current_class, on_ancestor_class, first_use_only
70
+ filtered_executions = @executions.filter { |e| e.dsl.name == dsl_name && ((on_current_class && e.klass == klass) || (on_ancestor_class && klass < e.klass)) }
71
+ # Because the classes were evaluated in order, we can just return the
72
+ # last execution
73
+ if first_use_only && filtered_executions.length > 0
74
+ [filtered_executions.last]
75
+ else
76
+ filtered_executions
77
+ end
71
78
  end
72
79
 
73
80
  # returns the most recent, closest single execution of a dsl with the
@@ -83,7 +90,7 @@ module DSLCompose
83
90
  # note that this method does not need to do any special sorting, the required
84
91
  # order for getting the most recent execution is already guaranteed because
85
92
  # parent classes in ruby always have to be evaluated before their descendants
86
- class_dsl_executions(klass, dsl_name, true, true).last
93
+ class_dsl_executions(klass, dsl_name, true, true, false).last
87
94
  end
88
95
 
89
96
  # removes all executions from the interpreter, and any parser_usage_notes
@@ -28,6 +28,8 @@ module DSLCompose
28
28
  @dsl_execution = dsl_execution
29
29
  @method_names = method_names
30
30
 
31
+ dsl_name = dsl_execution.dsl.name
32
+
31
33
  # assert that a block was provided
32
34
  unless block
33
35
  raise NoBlockProvided
@@ -100,6 +102,14 @@ module DSLCompose
100
102
 
101
103
  # yeild the block in the context of this class
102
104
  instance_exec(**args, &block)
105
+ rescue => e
106
+ # if this is an InterpreterError, then it already has the called_from metadata
107
+ # just continue raising the original error
108
+ if e.is_a? Interpreter::InterpreterError
109
+ raise
110
+ end
111
+ # otherwise, decorate the error with where the DSL was defined
112
+ raise e, "#{e.message}\nparsing class: #{child_class.name}\ndsl name: #{dsl_name}\ndsl method name: #{method_name}\ndsl source: #{method_call.called_from}", e.backtrace
103
113
  end
104
114
  end
105
115
  end
@@ -20,7 +20,7 @@ module DSLCompose
20
20
  # of the provided name is used by the child class.
21
21
  #
22
22
  # base_class and child_class are set from the ForChildrenOfParser
23
- def initialize base_class, child_class, dsl_names, on_current_class, on_ancestor_class, &block
23
+ def initialize base_class, child_class, dsl_names, on_current_class, on_ancestor_class, first_use_only, &block
24
24
  @base_class = base_class
25
25
  @child_class = child_class
26
26
  @dsl_names = dsl_names
@@ -62,7 +62,7 @@ module DSLCompose
62
62
  dsl_names.each do |dsl_name|
63
63
  # a dsl can be execued multiple times on a class, so we find all of the executions
64
64
  # here and then yield the block once for each execution
65
- base_class.dsls.class_dsl_executions(child_class, dsl_name, on_current_class, on_ancestor_class).each do |dsl_execution|
65
+ base_class.dsls.class_dsl_executions(child_class, dsl_name, on_current_class, on_ancestor_class, first_use_only).each do |dsl_execution|
66
66
  # we only provide the requested arguments to the block, this allows
67
67
  # us to use keyword arguments to force a naming convention on these arguments
68
68
  # and to validate their use
@@ -108,6 +108,14 @@ module DSLCompose
108
108
  @dsl_execution = dsl_execution
109
109
  # yield the block in the context of this class
110
110
  instance_exec(**args, &block)
111
+ rescue => e
112
+ # if this is an InterpreterError, then it already has the called_from metadata
113
+ # just continue raising the original error
114
+ if e.is_a? Interpreter::InterpreterError
115
+ raise
116
+ end
117
+ # otherwise, decorate the error with where the DSL was defined
118
+ raise e, "#{e.message}\nparsing class: #{child_class.name}\ndsl name: #{dsl_name}\ndsl source: #{dsl_execution.called_from}", e.backtrace
111
119
  end
112
120
  end
113
121
  end
@@ -90,36 +90,42 @@ module DSLCompose
90
90
  # end
91
91
  #
92
92
  # If `on_current_class` is true, then the block will be yielded to for each DSL
93
- # which was used directly on the current class. If `oncurrent_class` is false,
94
- # then the block will not be yielded to for any DSL which was used directly on.
93
+ # which was used directly on the current class. If `on_current_class` is false,
94
+ # then the block will not be yielded to for any DSL which was used directly on it.
95
95
  # If `on_ancestor_class` is true, then the block will be yielded to for each DSL
96
96
  # which was used on any class in the current classes ancestry. If `on_ancestor_class`
97
97
  # is false, then the block will not be yielded to for any DSL which was used on
98
98
  # any class in the current classes ancestry.
99
- def for_dsl dsl_names, on_current_class: true, on_ancestor_class: false, &block
99
+ #
100
+ # If `first_use_only` is true, then this block will only yeild once for each subject class
101
+ # which directly uses or inherits use of the provided DSL. The DSL execution which occurs
102
+ # first in the class will be selected, and if the class does not use the DSL then each of
103
+ # the classes ancestors will be tested until an execution is found (only the current class
104
+ # will be tested if skip_inherited_dsls has been set to true).
105
+ def for_dsl dsl_names, on_current_class: true, on_ancestor_class: false, first_use_only: false, &block
100
106
  child_class = @child_class
101
107
 
102
108
  unless child_class
103
109
  raise NoChildClassError, "No child_class was found, please call this method from within a `for_children_of` block"
104
110
  end
105
111
 
106
- ForDSLParser.new(@base_class, child_class, dsl_names, on_current_class, on_ancestor_class, &block)
112
+ ForDSLParser.new(@base_class, child_class, dsl_names, on_current_class, on_ancestor_class, first_use_only, &block)
107
113
  end
108
114
 
109
115
  # this is a wrapper for the `for_dsl` method, but it provides a value of true
110
116
  # for the `on_ancestor_class` argument and a value of false for the `on_current_class`
111
117
  # argument. This will cause the parser to only yeild for dsls which were used on
112
118
  # a class which is in the current classes ancestry, but not on the current class
113
- def for_inherited_dsl dsl_names, &block
114
- for_dsl dsl_names, on_current_class: false, on_ancestor_class: true, &block
119
+ def for_inherited_dsl dsl_names, first_use_only: false, &block
120
+ for_dsl dsl_names, on_current_class: false, on_ancestor_class: true, first_use_only: first_use_only, &block
115
121
  end
116
122
 
117
123
  # this is a wrapper for the `for_dsl` method, but it provides a value of true
118
124
  # for the `on_ancestor_class` argument and a value of true for the `on_current_class`
119
125
  # argument. This will cause the parser to yeild for dsls which were used on either
120
126
  # the current class or any class in its ancestry
121
- def for_dsl_or_inherited_dsl dsl_names, &block
122
- for_dsl dsl_names, on_current_class: true, on_ancestor_class: true, &block
127
+ def for_dsl_or_inherited_dsl dsl_names, first_use_only: false, &block
128
+ for_dsl dsl_names, on_current_class: true, on_ancestor_class: true, first_use_only: first_use_only, &block
123
129
  end
124
130
 
125
131
  # takes a description of what this parser does and stores it against the DSL definition
@@ -89,7 +89,7 @@ module DSLCompose
89
89
  # Returns an array of ExecutionReaders to represent each time the DSL was used
90
90
  # on the provided class.
91
91
  def executions
92
- @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, true, false).map do |execution|
92
+ @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, true, false, false).map do |execution|
93
93
  ExecutionReader.new execution
94
94
  end
95
95
  end
@@ -100,7 +100,7 @@ module DSLCompose
100
100
  # earliest ancestor first and if the DSL was used more than once on a class then
101
101
  # the order they were used.
102
102
  def ancestor_executions
103
- @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, false, true).map do |execution|
103
+ @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, false, true, false).map do |execution|
104
104
  ExecutionReader.new execution
105
105
  end
106
106
  end
@@ -110,7 +110,7 @@ module DSLCompose
110
110
  # be returned in the order they were executed, which is the earliest ancestor first
111
111
  # and if the DSL was used more than once on a class then the order they were used.
112
112
  def all_executions
113
- @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, true, true).map do |execution|
113
+ @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, true, true, false).map do |execution|
114
114
  ExecutionReader.new execution
115
115
  end
116
116
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSLCompose
4
- VERSION = "2.15.3"
4
+ VERSION = "2.15.5"
5
5
  end
data/lib/dsl_compose.rb CHANGED
@@ -34,6 +34,7 @@ require "dsl_compose/reader/execution_reader/arguments_reader"
34
34
 
35
35
  require "dsl_compose/reader_base"
36
36
 
37
+ require "dsl_compose/interpreter/interpreter_error"
37
38
  require "dsl_compose/interpreter/execution/method_calls/method_call"
38
39
  require "dsl_compose/interpreter/execution/method_calls"
39
40
  require "dsl_compose/interpreter/execution/arguments"
@@ -4,23 +4,24 @@ module DSLCompose
4
4
  class Execution
5
5
  class Arguments
6
6
  attr_reader arguments: Hash[untyped, untyped]
7
- def initialize: (untyped arguments, *untyped args) -> void
7
+ attr_reader called_from: String
8
+
9
+ def initialize: (untyped arguments, String called_from, *untyped args) -> void
8
10
  def to_h: -> Hash[untyped, untyped]
9
11
 
10
- class MissingRequiredArgumentsError < StandardError
11
- def initialize: (untyped required_count, untyped provided_count) -> void
12
+ class MissingRequiredArgumentsError < InterpreterError
12
13
  end
13
14
 
14
- class TooManyArgumentsError < StandardError
15
+ class TooManyArgumentsError < InterpreterError
15
16
  end
16
17
 
17
- class OptionalArgumentsShouldBeHashError < StandardError
18
+ class OptionalArgumentsShouldBeHashError < InterpreterError
18
19
  end
19
20
 
20
- class InvalidArgumentTypeError < StandardError
21
+ class InvalidArgumentTypeError < InterpreterError
21
22
  end
22
23
 
23
- class ArrayNotValidError < StandardError
24
+ class ArrayNotValidError < InterpreterError
24
25
  end
25
26
  end
26
27
  end
@@ -4,21 +4,22 @@ module DSLCompose
4
4
  class Execution
5
5
  class MethodCalls
6
6
  class MethodCall
7
- class InvalidDescriptionError < StandardError
8
- end
9
-
10
7
  @method_call: MethodCall
11
8
  @parser_usage_notes: Array[String]
12
9
 
13
10
  attr_reader dsl_method: DSL::DSLMethod
11
+ attr_reader called_from: String
14
12
  attr_reader arguments: Arguments
15
13
 
16
- def initialize: (DSL::DSLMethod dsl_method, *untyped args) -> void
14
+ def initialize: (DSL::DSLMethod dsl_method, String called_from, *untyped args) -> void
17
15
  def method_name: -> Symbol
18
16
  def add_parser_usage_note: (String note) -> void
19
17
  def parser_usage_notes: () -> Array[String]
20
18
 
21
19
  def to_h: -> Hash[untyped, untyped]
20
+
21
+ class InvalidDescriptionError < InterpreterError
22
+ end
22
23
  end
23
24
  end
24
25
  end
@@ -9,7 +9,7 @@ module DSLCompose
9
9
  def method_calls: () -> Array[MethodCall]
10
10
  def method_called?: (Symbol method_name) -> bool
11
11
  def method_calls_by_name: (Symbol method_name) -> Array[MethodCall]
12
- def add_method_call: (DSL::DSLMethod dsl_method, *untyped args) -> MethodCall
12
+ def add_method_call: (DSL::DSLMethod dsl_method, String called_from, *untyped args) -> MethodCall
13
13
  end
14
14
  end
15
15
  end
@@ -5,11 +5,12 @@ module DSLCompose
5
5
  @parser_usage_notes: Array[String]
6
6
 
7
7
  attr_reader dsl: DSL
8
+ attr_reader called_from: String
8
9
  attr_reader klass: Object
9
10
  attr_reader method_calls: MethodCalls
10
11
  attr_reader arguments: Arguments
11
12
 
12
- def initialize: (Object klass, DSL dsl, *untyped args) -> void
13
+ def initialize: (Object klass, DSL dsl, String called_from, *untyped args) -> void
13
14
  def instance_eval: () -> void
14
15
  def add_parser_usage_note: (String note) -> void
15
16
  def parser_usage_notes: () -> Array[String]
@@ -18,11 +19,15 @@ module DSLCompose
18
19
  def method_missing: (Symbol method_name, *untyped args) -> untyped
19
20
  def respond_to_missing?: (Symbol method_name, *untyped args) -> untyped
20
21
 
21
- class MethodIsUniqueError < StandardError
22
+ class MethodIsUniqueError < InterpreterError
22
23
  end
23
24
 
24
- class RequiredMethodNotCalledError < StandardError
25
+ class RequiredMethodNotCalledError < InterpreterError
25
26
  end
27
+
28
+ class InvalidDescriptionError < InterpreterError
29
+ end
30
+
26
31
  end
27
32
  end
28
33
  end
@@ -0,0 +1,11 @@
1
+
2
+ # Classes
3
+ module DSLCompose
4
+ class Interpreter
5
+ class InterpreterError < StandardError
6
+ attr_reader original_context: String
7
+ def initialize: (String message, String original_context) -> void
8
+ def to_s: -> String
9
+ end
10
+ end
11
+ end
@@ -12,10 +12,7 @@ module DSLCompose
12
12
  def add_parser_usage_note: (singleton(Object) klass, String note) -> void
13
13
  def parser_usage_notes: (singleton(Object) klass) -> Array[String]
14
14
 
15
- class InvalidDescriptionError < StandardError
16
- end
17
-
18
- class DSLExecutionNotFoundError < StandardError
15
+ class InvalidDescriptionError < InterpreterError
19
16
  end
20
17
  end
21
18
  end
@@ -8,7 +8,7 @@ module DSLCompose
8
8
  @dsl_names: Array[Symbol]
9
9
  @dsl_execution: Interpreter::Execution
10
10
 
11
- def initialize: (singleton(Object) base_class, singleton(Object) child_class, Array[Symbol] dsl_names, bool on_current_class, bool on_ancestor_class) -> void
11
+ def initialize: (singleton(Object) base_class, singleton(Object) child_class, Array[Symbol] dsl_names, bool on_current_class, bool on_ancestor_class, bool first_use_only) -> void
12
12
 
13
13
  # overriding this method because steep doesn't
14
14
  # correctly infer that a block is being passed to this method
@@ -14,10 +14,12 @@ module DSLCompose
14
14
  ) -> void
15
15
 
16
16
  private
17
- def for_dsl: (Array[Symbol] dsl_names, ?on_current_class: bool, ?on_ancestor_class: bool) -> untyped
17
+ def for_dsl: (Array[Symbol] dsl_names, ?on_current_class: bool, ?on_ancestor_class: bool, ?first_use_only: bool) -> untyped
18
+ def for_inherited_dsl: (Array[Symbol] dsl_names, ?first_use_only: bool) -> untyped
19
+ def for_dsl_or_inherited_dsl: (Array[Symbol] dsl_names, ?first_use_only: bool) -> untyped
18
20
 
19
21
  class AllBlockParametersMustBeKeywordParametersError < StandardError
20
- end
22
+ end
21
23
 
22
24
  class ClassDoesNotUseDSLComposeError < StandardError
23
25
  end
@@ -26,7 +28,7 @@ module DSLCompose
26
28
  end
27
29
 
28
30
  class NoChildClassError < StandardError
29
- end
31
+ end
30
32
  end
31
33
  end
32
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsl_compose
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.3
4
+ version: 2.15.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Craig Ulliott
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-03 00:00:00.000000000 Z
11
+ date: 2023-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: class_spec_helper
@@ -67,6 +67,7 @@ files:
67
67
  - lib/dsl_compose/interpreter/execution/arguments.rb
68
68
  - lib/dsl_compose/interpreter/execution/method_calls.rb
69
69
  - lib/dsl_compose/interpreter/execution/method_calls/method_call.rb
70
+ - lib/dsl_compose/interpreter/interpreter_error.rb
70
71
  - lib/dsl_compose/parser.rb
71
72
  - lib/dsl_compose/parser/block_arguments.rb
72
73
  - lib/dsl_compose/parser/for_children_of_parser.rb
@@ -109,6 +110,7 @@ files:
109
110
  - sig/dsl_compose/interpreter/execution/arguments.rbs
110
111
  - sig/dsl_compose/interpreter/execution/method_calls.rbs
111
112
  - sig/dsl_compose/interpreter/execution/method_calls/method_call.rbs
113
+ - sig/dsl_compose/interpreter/interpreter_error.rbs
112
114
  - sig/dsl_compose/parser.rbs
113
115
  - sig/dsl_compose/parser/block_arguments.rbs
114
116
  - sig/dsl_compose/parser/for_children_of_parser.rbs