dsl_compose 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +300 -3
  4. data/lib/dsl_compose/composer.rb +74 -0
  5. data/lib/dsl_compose/dsl/dsl_method/argument/equal_to_validation.rb +25 -0
  6. data/lib/dsl_compose/dsl/dsl_method/argument/format_validation.rb +25 -0
  7. data/lib/dsl_compose/dsl/dsl_method/argument/greater_than_or_equal_to_validation.rb +35 -0
  8. data/lib/dsl_compose/dsl/dsl_method/argument/greater_than_validation.rb +35 -0
  9. data/lib/dsl_compose/dsl/dsl_method/argument/in_validation.rb +35 -0
  10. data/lib/dsl_compose/dsl/dsl_method/argument/interpreter.rb +86 -0
  11. data/lib/dsl_compose/dsl/dsl_method/argument/length_validation.rb +42 -0
  12. data/lib/dsl_compose/dsl/dsl_method/argument/less_than_or_equal_to_validation.rb +35 -0
  13. data/lib/dsl_compose/dsl/dsl_method/argument/less_than_validation.rb +35 -0
  14. data/lib/dsl_compose/dsl/dsl_method/argument/not_in_validation.rb +35 -0
  15. data/lib/dsl_compose/dsl/dsl_method/argument.rb +299 -0
  16. data/lib/dsl_compose/dsl/dsl_method/interpreter.rb +57 -0
  17. data/lib/dsl_compose/dsl/dsl_method.rb +213 -0
  18. data/lib/dsl_compose/dsl/interpreter.rb +52 -0
  19. data/lib/dsl_compose/dsl.rb +148 -0
  20. data/lib/dsl_compose/dsls.rb +80 -0
  21. data/lib/dsl_compose/interpreter/execution/method_calls/method_call.rb +155 -0
  22. data/lib/dsl_compose/interpreter/execution/method_calls.rb +25 -0
  23. data/lib/dsl_compose/interpreter/execution.rb +60 -0
  24. data/lib/dsl_compose/interpreter.rb +43 -0
  25. data/lib/dsl_compose/version.rb +2 -2
  26. data/lib/dsl_compose.rb +32 -4
  27. metadata +24 -3
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class DSL
5
+ class DSLMethod
6
+ class ArgumentDoesNotExistError < StandardError
7
+ def message
8
+ "This argument does not exist for this DSLMethod"
9
+ end
10
+ end
11
+
12
+ class InvalidNameError < StandardError
13
+ def message
14
+ "The method #{method_name} is invalid, it must be of type symbol"
15
+ end
16
+ end
17
+
18
+ class MethodNameIsReservedError < StandardError
19
+ def message
20
+ "This method already would override an existing internal method"
21
+ end
22
+ end
23
+
24
+ class InvalidDescriptionError < StandardError
25
+ def message
26
+ "The DSL method description is invalid, it must be of type string and have length greater than 0"
27
+ end
28
+ end
29
+
30
+ class DescriptionAlreadyExistsError < StandardError
31
+ def message
32
+ "The description has already been set"
33
+ end
34
+ end
35
+
36
+ class ArgumentOrderingError < StandardError
37
+ def message
38
+ "Required arguments can not be added after optional ones"
39
+ end
40
+ end
41
+
42
+ class ArgumentAlreadyExistsError < StandardError
43
+ def message
44
+ "An argument with this name already exists for this DSL method"
45
+ end
46
+ end
47
+
48
+ class RequestedOptionalArgumentIsRequiredError < StandardError
49
+ def message
50
+ "A specific argument which was expected to be optional was requested, but the argument found was flagged as required"
51
+ end
52
+ end
53
+
54
+ class RequestedRequiredArgumentIsOptionalError < StandardError
55
+ def message
56
+ "A specific argument which was expected to be required was requested, but the argument found was flagged as optional"
57
+ end
58
+ end
59
+
60
+ # The name of this DSLMethod.
61
+ attr_reader :name
62
+ # if unique, then this DSLMethod can only be called once within the DSL.
63
+ attr_reader :unique
64
+ # if required, then this DSLMethod must be called at least once within the DSL.
65
+ attr_reader :required
66
+ # An otional description of this DSLMethod, if provided then it must be a string.
67
+ # The description accepts markdown and is used when generating documentation.
68
+ attr_reader :description
69
+
70
+ # Create a new DSLMethod object with the provided name and class.
71
+ #
72
+ # `name` must be a symbol.
73
+ # `unique` is a boolean which determines if this DSLMethod can only be called once witihn the DSL.
74
+ # `required` is a boolean which determines if this DSLMethod must be called at least once within the DSL.
75
+ # `block` contains the instructions to further configure this DSLMethod
76
+ def initialize name, unique, required, &block
77
+ @arguments = {}
78
+
79
+ if name.is_a? Symbol
80
+
81
+ # don't allow methods to override existing internal methods
82
+ if Class.respond_to? name
83
+ raise MethodNameIsReservedError
84
+ end
85
+
86
+ @name = name
87
+ else
88
+ raise InvalidNameError
89
+ end
90
+
91
+ @unique = unique ? true : false
92
+ @required = required ? true : false
93
+
94
+ # If a block was provided, then we evaluate it using a seperate
95
+ # interpreter class. We do this because the interpreter class contains
96
+ # no other methods or variables, if it was evaluated in the context of
97
+ # this class then the block would have access to all of the methods defined
98
+ # in here.
99
+ if block
100
+ Interpreter.new(self).instance_eval(&block)
101
+ end
102
+ end
103
+
104
+ # Set the description for this DSLMethod to the provided value.
105
+ #
106
+ # `description` must be a string with a length greater than 0.
107
+ # The `description` can only be set once per DSLMethod
108
+ def set_description description
109
+ unless description.is_a?(String) && description.length > 0
110
+ raise InvalidDescriptionError
111
+ end
112
+
113
+ if has_description?
114
+ raise DescriptionAlreadyExistsError
115
+ end
116
+
117
+ @description = description
118
+ end
119
+
120
+ # Returns `true` if this DSL has a description, else false.
121
+ def has_description?
122
+ @description.nil? == false
123
+ end
124
+
125
+ # Returns an array of all this DSLMethods Argument objects.
126
+ def arguments
127
+ @arguments.values
128
+ end
129
+
130
+ # Returns an array of only the optional Argument objects on this DSLMethod.
131
+ def optional_arguments
132
+ arguments.filter(&:optional?)
133
+ end
134
+
135
+ # Returns an array of only the required Argument objects on this DSLMethod.
136
+ def required_arguments
137
+ arguments.filter(&:required?)
138
+ end
139
+
140
+ # returns a specific Argument by it's name, if the Argument does not
141
+ # exist, then an error is raised
142
+ def argument name
143
+ if has_argument? name
144
+ @arguments[name]
145
+ else
146
+ raise ArgumentDoesNotExistError
147
+ end
148
+ end
149
+
150
+ # returns a specific optional Argument by it's name, if the Argument does not
151
+ # exist, or if it is required, then an error is raised
152
+ def optional_argument name
153
+ arg = argument name
154
+ if arg.optional?
155
+ @arguments[name]
156
+ else
157
+ raise RequestedOptionalArgumentIsRequiredError
158
+ end
159
+ end
160
+
161
+ # returns a specific required Argument by it's name, if the Argument does not
162
+ # exist, or if it is optional, then an error is raised
163
+ def required_argument name
164
+ arg = argument name
165
+ if arg.required?
166
+ @arguments[name]
167
+ else
168
+ raise RequestedRequiredArgumentIsOptionalError
169
+ end
170
+ end
171
+
172
+ # Returns `true` if an Argument with the provided name exists in this
173
+ # DSLMethod, otherwise it returns `false`.
174
+ def has_argument? name
175
+ @arguments.key? name
176
+ end
177
+
178
+ # returns true if this DSLMethod is flagged as unique, otherwise returns false.
179
+ def unique?
180
+ @unique == true
181
+ end
182
+
183
+ # returns true if this DSLMethod is flagged as required, otherwise returns false.
184
+ def required?
185
+ @required == true
186
+ end
187
+
188
+ # returns true if this DSLMethod is flagged as optional, otherwise returns false.
189
+ def optional?
190
+ @required == false
191
+ end
192
+
193
+ # Takes a method name, unique flag, required flag, and a block and creates
194
+ # a new Argument object.
195
+ #
196
+ # Argument `name` must be unique within the DSLMethod.
197
+ # `required` must be a boolean, and determines if this argument will be required
198
+ # or optional on the method which is exposed in our DSL.
199
+ def add_argument name, required, type, &block
200
+ if @arguments.key? name
201
+ raise ArgumentAlreadyExistsError
202
+ end
203
+
204
+ # required arguments may not come after optional ones
205
+ if required && optional_arguments.any?
206
+ raise ArgumentOrderingError
207
+ end
208
+
209
+ @arguments[name] = Argument.new(name, required, type, &block)
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class DSL
5
+ # The class is reponsible for parsing and executing our internal DSL which is used to define
6
+ # a new dynamic DSL. This class is instantaited by the DSLCompose::DSL class and our internal
7
+ # DSL is evaluated by passing a block to `instance_eval` on this class.
8
+ #
9
+ # An example of our internal DSL:
10
+ # define_dsl :my_dsl do
11
+ # description "This is my DSL"
12
+ # add_method :my_method do
13
+ # # ...
14
+ # end
15
+ # add_unique_method :my_uniq_method do
16
+ # # ...
17
+ # end
18
+ # end
19
+ class Interpreter
20
+ def initialize dsl
21
+ @dsl = dsl
22
+ end
23
+
24
+ private
25
+
26
+ # sets the description of the DSL
27
+ def description description
28
+ @dsl.set_description description
29
+ end
30
+
31
+ # adds a new method to the DSL
32
+ #
33
+ # methods flagged as `required` will cause your DSLs to raise an error
34
+ # if they are not used at least once within your new DSLs
35
+ # `block` contains the method definition and will be evaluated seperately
36
+ # by the DSLMethod::Interpreter
37
+ def add_method name, required: nil, &block
38
+ @dsl.add_method name, false, required ? true : false, &block
39
+ end
40
+
41
+ # adds a new unique method to the DSL
42
+ #
43
+ # methods flagged as `required` will cause your DSLs to raise an error
44
+ # if they are not used at least once within your new DSLs
45
+ # `block` contains the method definition and will be evaluated seperately
46
+ # by the DSLMethod::Interpreter
47
+ def add_unique_method name, required: nil, &block
48
+ @dsl.add_method name, true, required ? true : false, &block
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ # The class is reponsible for creating and representing a dynamic DSL
5
+ #
6
+ # These new dynamic DSL's are created using our own internal DSL, which is accessed
7
+ # by calling `define_dsl` in a class and passing it a block which contains the DSL definition
8
+ class DSL
9
+ class MethodDoesNotExistError < StandardError
10
+ def message
11
+ "This method does not exist for this DSL"
12
+ end
13
+ end
14
+
15
+ class MethodAlreadyExistsError < StandardError
16
+ def message
17
+ "This method already exists for this DSL"
18
+ end
19
+ end
20
+
21
+ class InvalidNameError < StandardError
22
+ def message
23
+ "This DSL name is invalid, it must be of type symbol"
24
+ end
25
+ end
26
+
27
+ class InvalidDescriptionError < StandardError
28
+ def message
29
+ "This DSL description is invalid, it must be of type string and have length greater than 0"
30
+ end
31
+ end
32
+
33
+ class DescriptionAlreadyExistsError < StandardError
34
+ def message
35
+ "The DSL description has already been set"
36
+ end
37
+ end
38
+
39
+ class NoBlockProvidedError < StandardError
40
+ def message
41
+ "No block was provided for this DSL"
42
+ end
43
+ end
44
+
45
+ # The name of this DSL.
46
+ attr_reader :name
47
+ # klass will be the class where `define_dsl` was called.
48
+ attr_reader :klass
49
+ # An otional description of this DSL, if provided then it must be a string.
50
+ # The description accepts markdown and is used when generating documentation.
51
+ attr_reader :description
52
+
53
+ # Create a new DSL object with the provided name and class.
54
+ #
55
+ # `name` must be a symbol.
56
+ # `klass` should be the class in which `define_dsl` is being called.
57
+ def initialize name, klass
58
+ @dsl_methods = {}
59
+
60
+ if name.is_a? Symbol
61
+ @name = name
62
+ else
63
+ raise InvalidNameError
64
+ end
65
+
66
+ @klass = klass
67
+ end
68
+
69
+ # Evaluate the configuration block which defines our new DSL
70
+ # `block` contains the DSL definition and will be evaluated to create
71
+ # the rest of the DSL.
72
+ def evaluate_configuration_block &block
73
+ if block
74
+ # We evaluate the internal DSL configuration blocks using a seperate interpreter
75
+ # class. We do this because the interpreter class contains no other methods or
76
+ # variables, if it was evaluated in the context of this class then the block
77
+ # would have access to all of the methods defined in here.
78
+ Interpreter.new(self).instance_eval(&block)
79
+ else
80
+ raise NoBlockProvidedError
81
+ end
82
+ end
83
+
84
+ # Set the description for this DSL to the provided value.
85
+ #
86
+ # `description` must be a string with a length greater than 0.
87
+ # The `description` can only be set once per DSL
88
+ def set_description description
89
+ unless description.is_a?(String) && description.length > 0
90
+ raise InvalidDescriptionError
91
+ end
92
+
93
+ if has_description?
94
+ raise DescriptionAlreadyExistsError
95
+ end
96
+
97
+ @description = description
98
+ end
99
+
100
+ # Returns `true` if this DSL has a description, else false.
101
+ def has_description?
102
+ @description.nil? == false
103
+ end
104
+
105
+ # Takes a method name, unique flag, required flag, and a block and creates
106
+ # a new DSLMethod object.
107
+ #
108
+ # Method `name` must be unique within the DSL.
109
+ def add_method name, unique, required, &block
110
+ if has_dsl_method? name
111
+ raise MethodAlreadyExistsError
112
+ end
113
+
114
+ @dsl_methods[name] = DSLMethod.new(name, unique, required, &block)
115
+ end
116
+
117
+ # Returns an array of all this DSLs DSLMethods.
118
+ def dsl_methods
119
+ @dsl_methods.values
120
+ end
121
+
122
+ # Returns an array of only the required DSLMethods in this DSL.
123
+ def required_dsl_methods
124
+ dsl_methods.filter(&:required?)
125
+ end
126
+
127
+ # Returns an array of only the optional DSLMethods in this DSL.
128
+ def optional_dsl_methods
129
+ dsl_methods.filter(&:optional?)
130
+ end
131
+
132
+ # returns a specific DSLMethod by it's name, if the DSLMethod does not
133
+ # exist, then an error is raised
134
+ def dsl_method name
135
+ if has_dsl_method? name
136
+ @dsl_methods[name]
137
+ else
138
+ raise MethodDoesNotExistError
139
+ end
140
+ end
141
+
142
+ # Returns `true` if a DSLMethod with the provided name exists in this
143
+ # DSL, otherwise it returns `false`.
144
+ def has_dsl_method? name
145
+ @dsl_methods.key? name
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ module DSLs
5
+ class ClassDSLDefinitionDoesNotExistError < StandardError
6
+ def message
7
+ "The requested DSL does not exist on this class"
8
+ end
9
+ end
10
+
11
+ class DSLAlreadyExistsError < StandardError
12
+ def message
13
+ "A DSL with this name already exists"
14
+ end
15
+ end
16
+
17
+ class NoDSLDefinitionsForClassError < StandardError
18
+ def message
19
+ "No DSLs have been defined for this class"
20
+ end
21
+ end
22
+
23
+ # an object to hold all of the defined DSLs in our application, the DSLs are
24
+ # organized by the class in which they were defined
25
+ @dsls = {}
26
+
27
+ # create a new DSL definition for a class
28
+ # `klass` here is the class in which `define_dsl` was called
29
+ def self.create_dsl klass, name
30
+ @dsls[klass] ||= {}
31
+
32
+ if @dsls[klass].key? name
33
+ raise DSLAlreadyExistsError
34
+ end
35
+
36
+ @dsls[klass][name] = DSLCompose::DSL.new(name, klass)
37
+ end
38
+
39
+ # return all of the DSL definitions
40
+ def self.dsls
41
+ @dsls
42
+ end
43
+
44
+ # return a DSL with a provided name for the provided class, if the DSL doesn't
45
+ # exist then it will be automatically created
46
+ def self.class_dsl_exists? klass, name
47
+ @dsls.key?(klass) && @dsls[klass].key?(name)
48
+ end
49
+
50
+ # return an array of DSL definitions for a provided class, if no DSLs
51
+ # exist for the provided class, then an error is raised
52
+ def self.class_dsls klass
53
+ if @dsls.key? klass
54
+ @dsls[klass].values
55
+ else
56
+ raise NoDSLDefinitionsForClassError
57
+ end
58
+ end
59
+
60
+ # return a specific DSL definition for a provided class
61
+ # if it does not exist, then an error is raised
62
+ def self.class_dsl klass, name
63
+ if @dsls.key? klass
64
+ if @dsls[klass].key? name
65
+ @dsls[klass][name]
66
+ else
67
+ raise ClassDSLDefinitionDoesNotExistError
68
+ end
69
+ else
70
+ raise NoDSLDefinitionsForClassError
71
+ end
72
+ end
73
+
74
+ # removes all DSL deinitions, this method is typically used by the test
75
+ # suite for resetting state inbetween each test
76
+ def self.reset
77
+ @dsls = {}
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Interpreter
5
+ class Execution
6
+ class MethodCalls
7
+ class MethodCall
8
+ attr_reader :dsl_method
9
+ attr_reader :arguments
10
+
11
+ class MissingRequiredArgumentsError < StandardError
12
+ def initialize required_count, provided_count
13
+ super "This method requires #{required_count} arguments, but only #{required_count} were provided"
14
+ end
15
+ end
16
+
17
+ class TooManyArgumentsError < StandardError
18
+ def message
19
+ "Too many arguments provided to this method"
20
+ end
21
+ end
22
+
23
+ class OptionalArgsShouldBeHashError < StandardError
24
+ def message
25
+ "If provided, then the optional arguments must be last, and be represented as a Hash"
26
+ end
27
+ end
28
+
29
+ class InvalidArgumentTypeError < StandardError
30
+ def message
31
+ "The provided argument is the wrong type"
32
+ end
33
+ end
34
+
35
+ def initialize dsl_method, *args, &block
36
+ @dsl_method = dsl_method
37
+ @arguments = {}
38
+
39
+ required_argument_count = dsl_method.required_arguments.count
40
+ has_optional_arguments = dsl_method.optional_arguments.any?
41
+
42
+ # the first N args, where N = required_argument_count, are the
43
+ # provided required arguments
44
+ required_args = args.slice(0, required_argument_count)
45
+ # the optional arg which comes next is the hash which represents
46
+ # all the optional arguments
47
+ optional_arg = args[required_argument_count]
48
+
49
+ # assert that a value is provided for every required argument
50
+ unless required_argument_count == required_args.count
51
+ raise MissingRequiredArgumentsError.new required_argument_count, required_args.count
52
+ end
53
+
54
+ # assert that too many arguments have not been provided
55
+ if args.count > required_argument_count + (has_optional_arguments ? 1 : 0)
56
+ raise TooManyArgumentsError
57
+ end
58
+
59
+ # asset that, if provided, then the optional argument (always the last one) is a Hash
60
+ if has_optional_arguments && optional_arg.nil? === false
61
+ unless optional_arg.is_a? Hash
62
+ raise OptionalArgsShouldBeHashError
63
+ end
64
+
65
+ # assert the each provided optional argument is valid
66
+ optional_arg.keys.each do |optional_argument_name|
67
+ optional_arg_value = optional_arg[optional_argument_name]
68
+ optional_argument = dsl_method.optional_argument optional_argument_name
69
+
70
+ case optional_argument.type
71
+ when :integer
72
+ unless optional_arg_value.is_a? Integer
73
+ raise InvalidArgumentTypeError
74
+ end
75
+ optional_argument.validate_integer! optional_arg_value
76
+
77
+ when :symbol
78
+ unless optional_arg_value.is_a? Symbol
79
+ raise InvalidArgumentTypeError
80
+ end
81
+ optional_argument.validate_symbol! optional_arg_value
82
+
83
+ when :string
84
+ unless optional_arg_value.is_a? String
85
+ raise InvalidArgumentTypeError
86
+ end
87
+ optional_argument.validate_string! optional_arg_value
88
+
89
+ when :boolean
90
+ unless optional_arg_value.is_a?(TrueClass) || optional_arg_value.is_a?(FalseClass)
91
+ raise InvalidArgumentTypeError
92
+ end
93
+ optional_argument.validate_boolean! optional_arg_value
94
+
95
+ else
96
+ raise InvalidArgumentTypeError
97
+ end
98
+
99
+ # the provided value appears valid for this argument, save the value
100
+ @arguments[optional_argument_name] = optional_arg_value
101
+ end
102
+
103
+ end
104
+
105
+ # validate the value provided to each required argument
106
+ dsl_method.required_arguments.each_with_index do |required_argument, i|
107
+ arg = args[i]
108
+ case required_argument.type
109
+ when :integer
110
+ unless arg.is_a? Integer
111
+ raise InvalidArgumentTypeError
112
+ end
113
+ required_argument.validate_integer! arg
114
+
115
+ when :symbol
116
+ unless arg.is_a? Symbol
117
+ raise InvalidArgumentTypeError
118
+ end
119
+ required_argument.validate_symbol! arg
120
+
121
+ when :string
122
+ unless arg.is_a? String
123
+ raise InvalidArgumentTypeError
124
+ end
125
+ required_argument.validate_string! arg
126
+
127
+ when :boolean
128
+ unless arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
129
+ raise InvalidArgumentTypeError
130
+ end
131
+ required_argument.validate_boolean! arg
132
+
133
+ else
134
+ raise InvalidArgumentTypeError
135
+ end
136
+
137
+ # the provided value appears valid for this argument, save the value
138
+ @arguments[required_argument.name] = arg
139
+ end
140
+ end
141
+
142
+ def method_name
143
+ @dsl_method.name
144
+ end
145
+
146
+ def to_h
147
+ {
148
+ arguments: @arguments
149
+ }
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Interpreter
5
+ class Execution
6
+ class MethodCalls
7
+ attr_reader :method_calls
8
+
9
+ def initialize
10
+ @method_calls = []
11
+ end
12
+
13
+ def method_called? method_name
14
+ @method_calls.filter { |mc| mc.method_name == method_name }.any?
15
+ end
16
+
17
+ def add_method_call dsl_method, *args, &block
18
+ method_call = MethodCall.new(dsl_method, *args, &block)
19
+ @method_calls << method_call
20
+ method_call
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end