dsl_compose 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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