dsl_compose 2.0.1 → 2.1.1

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: 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: []