dsl_compose 2.0.1 → 2.1.1

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: ca7d4076508adb7aa4c534ffe69f276d4da8c50abcb65dbc898a2a40a241b2be
4
- data.tar.gz: f0efae218b8a5ed32212077de0b42d3a345e5578c29ebf0dc4e00356ae0d00cf
3
+ metadata.gz: 826d148f5440924eeea1a56699ec7f6a421dcf5fe273012e7e9cf9010ab4cb66
4
+ data.tar.gz: 0fa06d0fcf99bea843da3b3b5cb66f196be88a923287ba4df64459e6289f7054
5
5
  SHA512:
6
- metadata.gz: 1f1302ffba75b9614fbecf1c3d84159360dcbc67f760e7474fed1e9ba321abeb7200c424ba0b9c6d7a6eb26891649012474037a896387eeec058fc3e58cba651
7
- data.tar.gz: b20711b1f065090f9507e32029640d3766f8fd4ceb0a5d1d9372c3ddbdb03eef311982531284d0b213b4a559d614929f3531747b659db88d5eda8ebffd22cc6c
6
+ metadata.gz: c4bcce09e0c5b3028c97ac09dbd16a6481ed8514857e7bcaecbbe7b40aebec22b8d7b4220f299380363dd9b87de9c0585fa456d8c70c031e967d3e6048711b60
7
+ data.tar.gz: 20eb3288cafb32aafddc8efe78e2e8fba5cf088c1896b5a9e82eb42e21c63c84d6e2536888d5e938f9f4b9bc80d064bfb5e95ca2fdb410316707f0362e0bf6cf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.1](https://github.com/craigulliott/dsl_compose/compare/v2.1.0...v2.1.1) (2023-07-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * fixed bug where Execution classes were being returned from within the Reader but ExecutionReader classes were expected ([#56](https://github.com/craigulliott/dsl_compose/issues/56)) ([546cdaf](https://github.com/craigulliott/dsl_compose/commit/546cdaf323f3fc3be6ad09a47e5f99c3a59a50e2))
9
+
10
+ ## [2.1.0](https://github.com/craigulliott/dsl_compose/compare/v2.0.1...v2.1.0) (2023-07-24)
11
+
12
+
13
+ ### Features
14
+
15
+ * 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))
16
+
3
17
  ## [2.0.1](https://github.com/craigulliott/dsl_compose/compare/v2.0.0...v2.0.1) (2023-07-19)
4
18
 
5
19
 
data/README.md CHANGED
@@ -168,7 +168,7 @@ You can use `import_shared` anywhere within your DSL definition, you can even us
168
168
  Child classes can then use your new DSL
169
169
 
170
170
  ```ruby
171
- class Bar << Foo
171
+ class Bar < Foo
172
172
 
173
173
  your_dsl :first_dsl_argument, do
174
174
  your_method "first_method_argument", optional_argument: 123
@@ -239,7 +239,7 @@ A parser class can be used to process complicated DSLs. In the example below, a
239
239
 
240
240
  ```ruby
241
241
  # create your own parser by creating a new class which extends DSLCompose::Parser
242
- MyParser < DSLCompose::Parser
242
+ class MyParser < DSLCompose::Parser
243
243
  # `for_children_of` will process SomeBaseClass and yield the provided
244
244
  # block once for every class which extends SomeBaseClass.
245
245
  #
@@ -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 with a Reader class.
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,86 @@
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
+ attr_reader :execution
19
+
20
+ def initialize execution
21
+ raise InvalidExecution unless execution.is_a? Interpreter::Execution
22
+ @execution = execution
23
+ end
24
+
25
+ # returns an object which represents the arguments available for this DSL and allows
26
+ # accessing their values via methods
27
+ def arguments
28
+ ArgumentsReader.new @execution.dsl.arguments, @execution.arguments
29
+ end
30
+
31
+ # was a method with the provided name called during the use of this DSL
32
+ def method_called? method_name
33
+ unless @execution.dsl.has_dsl_method? method_name
34
+ raise MethodDoesNotExist, "The method `#{method_name}` does not exist for DSL `#{@execution.dsl.name}`"
35
+ end
36
+ @execution.method_calls.method_called? method_name
37
+ end
38
+
39
+ # Catch and process any method calls, if a DSL method with the same name exists
40
+ # then return a representation of the arguments which were provided to the execution
41
+ # of that method within the DSL.
42
+ #
43
+ # If the method is marked as unique, then an arguments object will be returned, if the
44
+ # method is not unique, then an array of arguments objects will be returned (one for each
45
+ # time the method was used within this DSL execution).
46
+ def method_missing method_name
47
+ # Fetch the method from the DSL (this will raise an error if a method with this name
48
+ # was not defined for this exections DSL).
49
+ dsl_method = @execution.dsl.dsl_method method_name
50
+
51
+ # the Arguments object which represents the possible arguments which can be
52
+ # used with this method
53
+ arguments = dsl_method.arguments
54
+
55
+ # Fetch the array of method calls, this represents each use of a DSL method within
56
+ # a single use of the DSL
57
+ method_calls = @execution.method_calls.method_calls_by_name(method_name)
58
+
59
+ # If the use of this DSL method is only allowed once per DSL execution, then return
60
+ # an ArgumentsReader object which represents the argument values provided when the
61
+ # method was used.
62
+ #
63
+ # If the method was never used, then nil will be returned. We don't have to validate
64
+ # if the method use is required or not here, because that validation already happened
65
+ # when the DSL was used.
66
+ if dsl_method.unique?
67
+ method_call = method_calls.first
68
+ unless method_call.nil?
69
+ ArgumentsReader.new arguments, method_call.arguments
70
+ end
71
+
72
+ # If the method call is not unique, then return an array representing the argument
73
+ # values provided for each use of the DSL method
74
+ else
75
+ method_calls.map do |method_call|
76
+ ArgumentsReader.new arguments, method_call.arguments
77
+ end
78
+ end
79
+ end
80
+
81
+ def respond_to_missing? method_name
82
+ method_called? method_name
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,86 @@
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
+ ExecutionReader.new(@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).map do |execution|
61
+ ExecutionReader.new execution
62
+ end
63
+ end
64
+
65
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
66
+ # on the ancestors of the provided class, but not on the provided class itself.
67
+ # The executions will be returned in the order they were executed, which is the
68
+ # earliest ancestor first and if the DSL was used more than once on a class then
69
+ # the order they were used.
70
+ def ancestor_executions
71
+ @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, false, true).map do |execution|
72
+ ExecutionReader.new execution
73
+ end
74
+ end
75
+
76
+ # Returns an array of ExecutionReaders to represent each time the DSL was used
77
+ # on the provided class or ancestors of the provided class. The executions will
78
+ # be returned in the order they were executed, which is the earliest ancestor first
79
+ # and if the DSL was used more than once on a class then the order they were used.
80
+ def all_executions
81
+ @dsl_defining_class.dsls.class_dsl_executions(@klass, @dsl_name, true, true).map do |execution|
82
+ ExecutionReader.new execution
83
+ end
84
+ end
85
+ end
86
+ 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.1"
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Craig Ulliott
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-19 00:00:00.000000000 Z
11
+ date: 2023-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: class_spec_helper
@@ -74,15 +74,18 @@ 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
- homepage:
82
+ homepage:
80
83
  licenses:
81
84
  - MIT
82
85
  metadata:
83
86
  source_code_uri: https://github.com/craigulliott/dsl_compose/
84
87
  changelog_uri: https://github.com/craigulliott/dsl_compose/blob/main/CHANGELOG.md
85
- post_install_message:
88
+ post_install_message:
86
89
  rdoc_options: []
87
90
  require_paths:
88
91
  - lib
@@ -98,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
101
  version: '0'
99
102
  requirements: []
100
103
  rubygems_version: 3.3.26
101
- signing_key:
104
+ signing_key:
102
105
  specification_version: 4
103
106
  summary: Ruby gem to add dynamic DSLs to classes
104
107
  test_files: []