dsl_compose 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca7d4076508adb7aa4c534ffe69f276d4da8c50abcb65dbc898a2a40a241b2be
4
- data.tar.gz: f0efae218b8a5ed32212077de0b42d3a345e5578c29ebf0dc4e00356ae0d00cf
3
+ metadata.gz: 535a5086604fa5366f7a9baf2cc1a1ef823d4f29f16d8fc273b78a680180c3e2
4
+ data.tar.gz: 12f9bfc80a59ad1cbf61ad03795fd6e29535618c41143dbb7d8afb1228c7f3ae
5
5
  SHA512:
6
- metadata.gz: 1f1302ffba75b9614fbecf1c3d84159360dcbc67f760e7474fed1e9ba321abeb7200c424ba0b9c6d7a6eb26891649012474037a896387eeec058fc3e58cba651
7
- data.tar.gz: b20711b1f065090f9507e32029640d3766f8fd4ceb0a5d1d9372c3ddbdb03eef311982531284d0b213b4a559d614929f3531747b659db88d5eda8ebffd22cc6c
6
+ metadata.gz: 768fbc012d89a3542f0c96934bfc9187098c790a4a7d975dff80ae63ba1e50c0eb19e8f8abddacf234410839fd1a5688eb76c30a51ed939485d219c67263c5f7
7
+ data.tar.gz: 3553a51fc949265b84866761ce8ee5ed2ae4de67d2db17da36b4d646a21a5feb34b91f0085a3fcdfa54c5707457aa3cc0adc9c63d3da5820e042e372d5224603
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.0](https://github.com/craigulliott/dsl_compose/compare/v2.0.1...v2.1.0) (2023-07-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * created a Reader class which can be used to access the resulting configuration from executing DSLs ([#53](https://github.com/craigulliott/dsl_compose/issues/53)) ([6e18eb7](https://github.com/craigulliott/dsl_compose/commit/6e18eb736611193b4fad8dac380cdd760095760f))
9
+
3
10
  ## [2.0.1](https://github.com/craigulliott/dsl_compose/compare/v2.0.0...v2.0.1) (2023-07-19)
4
11
 
5
12
 
data/README.md CHANGED
@@ -312,6 +312,82 @@ MyParser < DSLCompose::Parser
312
312
  end
313
313
  ```
314
314
 
315
+ In addition to parser classes (or as a useful tool within parser classes) you can access the results of DSL execution via the class (or via a descendent of the class) where the DSL was originally defined.
316
+
317
+ ```ruby
318
+ # Create a new Reader object.
319
+ #
320
+ # Reader objects build and return ExecutionReader classes which expose a
321
+ # simple API to access the arguments, methods and method arguments which
322
+ # were provided when using a DSL.
323
+ #
324
+ # In the example below, MyClass is a class, or descendent of a class which
325
+ # had a DSL defined on it with the name :my_dsl.
326
+ #
327
+ # An error will be raised if a DSL with the provided name was never defined
328
+ # on MyClass or any of its ancestors.
329
+ reader = DSLCompose::Reader.new MyClass, :my_dsl
330
+
331
+ # `reader.last_execution` will return an ExecutionReader which represents
332
+ # the last time the DSL was used.
333
+ #
334
+ # If the dsl has been executed once or more on the provided class, then
335
+ # the last (most recent) execution will be returned. If the DSL was not
336
+ # executed on the provided class, then we traverse up the classes ancestors
337
+ # and look for the last (most recent) time it was executed on each ancestor.
338
+ #
339
+ # If no execution of the DSL is found, then nil will be returned
340
+ execution = reader.last_execution
341
+
342
+ # `execution.arguments` returns an ArgumentsReader object which allows access
343
+ # via dot notation to to any argument values provided to the DSL
344
+ execution.arguments.my_dsl_argument # returns the value provided for the argument, or nil
345
+
346
+ # `execution.method_called?` will return true if the method with the provided
347
+ # name was called, if a method with this name does exist, but was not called
348
+ # then false will be returned. If a method with this name does not exist, then
349
+ # an error will be raised.
350
+ execution.method_called? :my_dsl_method # returns true/false
351
+
352
+ # You can directly access the argument values for methods by calling the
353
+ # ExecutionReader object with the same method name as the defined method on
354
+ # your DSL.
355
+ #
356
+ # If your method was defined as unique via `add_unique_method` then this will
357
+ # return a single ArgumentsReader which represents the arguments provided to
358
+ # your method.
359
+ #
360
+ # Note that `execution.my_dsl_method` will return nil if the method was
361
+ # not used in this DSL execution, so you should either check this first
362
+ # with `method_called?` or use ruby's safe navigation operator (`.&`). If
363
+ # you want to enforce use of this method, then it should be marked as
364
+ # required when your DSL was originally defined.
365
+ execution.my_dsl_method.my_dsl_method_argument
366
+
367
+ # If your method was not defined as unique, then this will return an array
368
+ # representing each time the method was used. If the method was never used
369
+ # then an empty array will be returned
370
+ execution.my_dsl_method.each do |arguments|
371
+ arguments.my_dsl_method_argument # returns the value provided for the argument, or nil
372
+ end
373
+
374
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
375
+ # on ancestors of the provided class.
376
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
377
+ # on the ancestors of the provided class, but not on the provided class itself.
378
+ # The executions will be returned in the order they were executed, which is the
379
+ # earliest ancestor first and if the DSL was used more than once on a class then
380
+ # the order they were used.
381
+ executions = reader.ancestor_executions
382
+
383
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
384
+ # on the provided class or ancestors of the provided class. The executions will
385
+ # be returned in the order they were executed, which is the earliest ancestor first
386
+ # and if the DSL was used more than once on a class then the order they were used.
387
+ executions = reader.all_executions
388
+
389
+ ```
390
+
315
391
 
316
392
  ## Argument validations
317
393
 
@@ -22,6 +22,11 @@ module DSLCompose
22
22
  @arguments = {}
23
23
  end
24
24
 
25
+ # returns true if there are any arguments, otherwise returns false
26
+ def any?
27
+ @arguments.values.any?
28
+ end
29
+
25
30
  # Returns an array of all this DSLMethods Argument objects.
26
31
  def arguments
27
32
  @arguments.values
@@ -107,6 +107,21 @@ module DSLCompose
107
107
  @dsl_methods.values
108
108
  end
109
109
 
110
+ # does this DSL have any methods
111
+ def has_methods?
112
+ dsl_methods.any?
113
+ end
114
+
115
+ # does this DSL have any required methods
116
+ def has_required_methods?
117
+ required_dsl_methods.any?
118
+ end
119
+
120
+ # does this DSL have any optional methods
121
+ def has_optional_methods?
122
+ optional_dsl_methods.any?
123
+ end
124
+
110
125
  # Returns an array of only the required DSLMethods in this DSL.
111
126
  def required_dsl_methods
112
127
  dsl_methods.filter(&:required?)
@@ -32,8 +32,8 @@ module DSLCompose
32
32
  @dsls
33
33
  end
34
34
 
35
- # return a DSL with a provided name for the provided class, if the DSL doesn't
36
- # exist then it will be automatically created
35
+ # if a DSL witg the provided name exists for the provided class, then return true
36
+ # else return false
37
37
  def self.class_dsl_exists? klass, name
38
38
  @dsls.key?(klass) && @dsls[klass].key?(name)
39
39
  end
@@ -2,9 +2,10 @@ module DSLCompose
2
2
  # A small helper to fix indentation and clean up whitespace in
3
3
  # strings which were created with rubys heredoc syntax.
4
4
  module FixHeredocIndentation
5
- # This method is used to fix the indentation of heredoc strings.
6
- # It will remove the leading whitespace from each line of the string
7
- # and remove the leading newline from the string.
5
+ # This method is used to trim empty lines from the start and end of
6
+ # a block of markdown, it will also fix the indentation of heredoc
7
+ # strings by removing the leading whitespace from the first line, and
8
+ # that same amount of white space from every other line
8
9
  def self.fix_heredoc_indentation string
9
10
  # replace all tabs with spaces
10
11
  string = string.gsub(/\t/, " ")
@@ -14,10 +15,7 @@ module DSLCompose
14
15
  string = string.gsub(/( *\n)+\Z/, "")
15
16
  # removes the number of leading spaces on the first line, from
16
17
  # all the other lines
17
- string = string.gsub(/^#{string[/\A */]}/, "")
18
- # collapse lines which are next to each other onto the same line
19
- # because they are really the same paragraph
20
- string.gsub(/([^ \n])\n([^ \n])/, '\1 \2')
18
+ string.gsub(/^#{string[/\A */]}/, "")
21
19
  end
22
20
  end
23
21
  end
@@ -4,6 +4,9 @@ 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
+
7
10
  # A dynamic DSL can be used multiple times on the same class, each time the DSL is used
8
11
  # a corresponding execution will be created. The execution contains the resulting configuration
9
12
  # from that particular use of the DSL.
@@ -61,6 +64,22 @@ module DSLCompose
61
64
  @executions.filter { |e| e.dsl.name == dsl_name && ((on_current_class && e.klass == klass) || (on_ancestor_class && klass < e.klass)) }
62
65
  end
63
66
 
67
+ # returns the most recent, closest single execution of a dsl with the
68
+ # provided name for the provided class
69
+ #
70
+ # If the dsl has been executed once or more on the provided class, then
71
+ # the last (most recent) execution will be returned. If the DSL was not
72
+ # executed on the provided class, then we traverse up the classes ancestors
73
+ # until we reach the ancestor where the DSL was originally defined and test
74
+ # each of them and return the first most recent execution of the DSL.
75
+ # If no execution of the DSL is found, then nil will be returned
76
+ def get_last_dsl_execution klass, dsl_name
77
+ # note that this method does not need to do any special sorting, the required
78
+ # order for getting the most recent execution is already guaranteed because
79
+ # parent classes in ruby always have to be evaluated before their descendents
80
+ class_dsl_executions(klass, dsl_name, true, true).last
81
+ end
82
+
64
83
  # removes all executions from the interpreter, and any parser_usage_notes
65
84
  # this is primarily used from within a test suite when dynamically creating
66
85
  # classes for tests and then wanting to clear the interpreter before the
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Reader
5
+ class ExecutionReader
6
+ # This class is part of a decorator for DSL executions.
7
+ class ArgumentsReader
8
+ # When instantiated, this class dynamically provides methods which corespiond to argument
9
+ # names and returns the argument value.
10
+ #
11
+ # `arguments` should be the DSL Arguments object, as this represents the possible arguments
12
+ # `argument_values` is a key/value object representing the actual values which were provided
13
+ # when the DSL was executed
14
+ #
15
+ # This class is used to represent both DSL arguments, and dsl_method arguments
16
+ def initialize arguments, argument_values
17
+ @arguments = arguments
18
+ @argument_values = argument_values
19
+ end
20
+
21
+ # catch and process any method calls, if arguments exist with the same name
22
+ # as the method call, then return the appropriate value, otherwise raise an error
23
+ def method_missing method_name
24
+ # fetch the argument to ensure it exists (this will raise an error if it does not)
25
+ argument = @arguments.argument method_name
26
+ # return the argument value, or nil if the argument was not used
27
+ @argument_values.arguments[argument.name]
28
+ end
29
+
30
+ def respond_to_missing? method_name
31
+ @arguments.has_argument? method_name
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Reader
5
+ # This class is a decorator for DSL executions.
6
+ #
7
+ # When a dynamically defined DSL is executed on a class, it creates an execution
8
+ # object which represents the argument values, and any methods and subsequent
9
+ # method argument values which were provided. This class provides a clean and simple
10
+ # API to access those vaues
11
+ class ExecutionReader
12
+ class InvalidExecution < StandardError
13
+ end
14
+
15
+ class MethodDoesNotExist < StandardError
16
+ end
17
+
18
+ def initialize execution
19
+ raise InvalidExecution unless execution.is_a? Interpreter::Execution
20
+ @execution = execution
21
+ end
22
+
23
+ # returns an object which represents the arguments available for this DSL and allows
24
+ # accessing their values via methods
25
+ def arguments
26
+ ArgumentsReader.new @execution.dsl.arguments, @execution.arguments
27
+ end
28
+
29
+ # was a method with the provided name called during the use of this DSL
30
+ def method_called? method_name
31
+ unless @execution.dsl.has_dsl_method? method_name
32
+ raise MethodDoesNotExist, "The method `#{method_name}` does not exist for DSL `#{@execution.dsl.name}`"
33
+ end
34
+ @execution.method_calls.method_called? method_name
35
+ end
36
+
37
+ # Catch and process any method calls, if a DSL method with the same name exists
38
+ # then return a representation of the arguments which were provided to the execution
39
+ # of that method within the DSL.
40
+ #
41
+ # If the method is marked as unique, then an arguments object will be returned, if the
42
+ # method is not unique, then an array of arguments objects will be returned (one for each
43
+ # time the method was used within this DSL execution).
44
+ def method_missing method_name
45
+ # Fetch the method from the DSL (this will raise an error if a method with this name
46
+ # was not defined for this exections DSL).
47
+ dsl_method = @execution.dsl.dsl_method method_name
48
+
49
+ # the Arguments object which represents the possible arguments which can be
50
+ # used with this method
51
+ arguments = dsl_method.arguments
52
+
53
+ # Fetch the array of method calls, this represents each use of a DSL method within
54
+ # a single use of the DSL
55
+ method_calls = @execution.method_calls.method_calls_by_name(method_name)
56
+
57
+ # If the use of this DSL method is only allowed once per DSL execution, then return
58
+ # an ArgumentsReader object which represents the argument values provided when the
59
+ # method was used.
60
+ #
61
+ # If the method was never used, then nil will be returned. We don't have to validate
62
+ # if the method use is required or not here, because that validation already happened
63
+ # when the DSL was used.
64
+ if dsl_method.unique?
65
+ method_call = method_calls.first
66
+ unless method_call.nil?
67
+ ArgumentsReader.new arguments, method_call.arguments
68
+ end
69
+
70
+ # If the method call is not unique, then return an array representing the argument
71
+ # values provided for each use of the DSL method
72
+ else
73
+ method_calls.map do |method_call|
74
+ ArgumentsReader.new arguments, method_call.arguments
75
+ end
76
+ end
77
+ end
78
+
79
+ def respond_to_missing? method_name
80
+ method_called? method_name
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ # This class is a decorator for DSL executions.
5
+ #
6
+ # When a dynamically defined DSL is executed on a class, it creates an execution
7
+ # object which represents the argument values, and any methods and subsequent
8
+ # method argument values which were provided. This class provides a clean and simple
9
+ # API to access those vaues
10
+ class Reader
11
+ class DSLNotFound < StandardError
12
+ end
13
+
14
+ # given a class and the DSL name, finds and creates a reference to the DSL
15
+ # and the ancestor class where the DSL was originally defined
16
+ def initialize klass, dsl_name
17
+ # a reference to the class and dsl_name which we want to read values for
18
+ @klass = klass
19
+ @dsl_name = dsl_name
20
+
21
+ # Move up through this classes ancestors until we find the class which defined
22
+ # the DSL with the provided name. When we reach the top of the ancestor chain we
23
+ # exit the loop.
24
+ while klass
25
+ # if we find a DSL with this name, then store a reference to the DSL and the
26
+ # ancestor class where it was defined
27
+ if DSLs.class_dsl_exists?(klass, dsl_name)
28
+ @dsl = DSLs.class_dsl(klass, dsl_name)
29
+ @dsl_defining_class = klass
30
+ # stop once we find the DSL
31
+ break
32
+ end
33
+
34
+ # the DSL was not found here, so traverse up the provided classes hierachy
35
+ # and keep looking for where this DSL was initially defined
36
+ klass = klass.superclass
37
+ end
38
+
39
+ # if no DSL was found, then raise an error
40
+ if @dsl.nil? && @dsl_defining_class.nil?
41
+ raise DSLNotFound, "No DSL named `#{dsl_name}` was found for class `#{klass}`"
42
+ end
43
+ end
44
+
45
+ # Returns an ExecutionReader class which exposes a simple API to access the
46
+ # arguments, methods and method arguments provided when using this DSL.
47
+ #
48
+ # If the dsl has been executed once or more on the provided class, then
49
+ # the last (most recent) execution will be returned. If the DSL was not
50
+ # executed on the provided class, then we traverse up the classes ancestors
51
+ # and look for the last time it was executed on each ancestor.
52
+ # If no execution of the DSL is found, then nil will be returned
53
+ def last_execution
54
+ @dsl_defining_class.dsls.get_last_dsl_execution @klass, @dsl_name
55
+ end
56
+
57
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
58
+ # on the provided class.
59
+ def executions
60
+ @dsl_defining_class.dsls.class_dsl_executions @klass, @dsl_name, true, false
61
+ end
62
+
63
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
64
+ # on the ancestors of the provided class, but not on the provided class itself.
65
+ # The executions will be returned in the order they were executed, which is the
66
+ # earliest ancestor first and if the DSL was used more than once on a class then
67
+ # the order they were used.
68
+ def ancestor_executions
69
+ @dsl_defining_class.dsls.class_dsl_executions @klass, @dsl_name, false, true
70
+ end
71
+
72
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
73
+ # on the provided class or ancestors of the provided class. The executions will
74
+ # be returned in the order they were executed, which is the earliest ancestor first
75
+ # and if the DSL was used more than once on a class then the order they were used.
76
+ def all_executions
77
+ @dsl_defining_class.dsls.class_dsl_executions @klass, @dsl_name, true, true
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DSLCompose
4
- VERSION = "2.0.1"
4
+ VERSION = "2.1.0"
5
5
  end
data/lib/dsl_compose.rb CHANGED
@@ -28,6 +28,10 @@ require "dsl_compose/dsl/interpreter"
28
28
 
29
29
  require "dsl_compose/dsl"
30
30
 
31
+ require "dsl_compose/reader"
32
+ require "dsl_compose/reader/execution_reader"
33
+ require "dsl_compose/reader/execution_reader/arguments_reader"
34
+
31
35
  require "dsl_compose/interpreter/execution/method_calls/method_call"
32
36
  require "dsl_compose/interpreter/execution/method_calls"
33
37
  require "dsl_compose/interpreter/execution/arguments"
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.0.1
4
+ version: 2.1.0
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-07-19 00:00:00.000000000 Z
11
+ date: 2023-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: class_spec_helper
@@ -74,6 +74,9 @@ files:
74
74
  - lib/dsl_compose/parser/for_children_of_parser/descendents.rb
75
75
  - lib/dsl_compose/parser/for_children_of_parser/for_dsl_parser.rb
76
76
  - lib/dsl_compose/parser/for_children_of_parser/for_dsl_parser/for_method_parser.rb
77
+ - lib/dsl_compose/reader.rb
78
+ - lib/dsl_compose/reader/execution_reader.rb
79
+ - lib/dsl_compose/reader/execution_reader/arguments_reader.rb
77
80
  - lib/dsl_compose/shared_configuration.rb
78
81
  - lib/dsl_compose/version.rb
79
82
  homepage: