dsl_compose 1.0.0 → 1.2.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +300 -3
  4. data/lib/dsl_compose/composer.rb +74 -0
  5. data/lib/dsl_compose/dsl/arguments/argument/equal_to_validation.rb +25 -0
  6. data/lib/dsl_compose/dsl/arguments/argument/format_validation.rb +25 -0
  7. data/lib/dsl_compose/dsl/arguments/argument/greater_than_or_equal_to_validation.rb +35 -0
  8. data/lib/dsl_compose/dsl/arguments/argument/greater_than_validation.rb +35 -0
  9. data/lib/dsl_compose/dsl/arguments/argument/in_validation.rb +35 -0
  10. data/lib/dsl_compose/dsl/arguments/argument/interpreter.rb +86 -0
  11. data/lib/dsl_compose/dsl/arguments/argument/length_validation.rb +42 -0
  12. data/lib/dsl_compose/dsl/arguments/argument/less_than_or_equal_to_validation.rb +35 -0
  13. data/lib/dsl_compose/dsl/arguments/argument/less_than_validation.rb +35 -0
  14. data/lib/dsl_compose/dsl/arguments/argument/not_in_validation.rb +35 -0
  15. data/lib/dsl_compose/dsl/arguments/argument.rb +299 -0
  16. data/lib/dsl_compose/dsl/arguments.rb +113 -0
  17. data/lib/dsl_compose/dsl/dsl_method/interpreter.rb +57 -0
  18. data/lib/dsl_compose/dsl/dsl_method.rb +143 -0
  19. data/lib/dsl_compose/dsl/interpreter.rb +72 -0
  20. data/lib/dsl_compose/dsl.rb +152 -0
  21. data/lib/dsl_compose/dsls.rb +80 -0
  22. data/lib/dsl_compose/interpreter/execution/arguments.rb +145 -0
  23. data/lib/dsl_compose/interpreter/execution/method_calls/method_call.rb +53 -0
  24. data/lib/dsl_compose/interpreter/execution/method_calls.rb +25 -0
  25. data/lib/dsl_compose/interpreter/execution.rb +64 -0
  26. data/lib/dsl_compose/interpreter.rb +50 -0
  27. data/lib/dsl_compose/version.rb +2 -2
  28. data/lib/dsl_compose.rb +35 -4
  29. metadata +26 -3
@@ -0,0 +1,143 @@
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
+ # an object which represents the argument configuration
70
+ attr_reader :arguments
71
+
72
+ # Create a new DSLMethod object with the provided name and class.
73
+ #
74
+ # `name` must be a symbol.
75
+ # `unique` is a boolean which determines if this DSLMethod can only be called once witihn the DSL.
76
+ # `required` is a boolean which determines if this DSLMethod must be called at least once within the DSL.
77
+ # `block` contains the instructions to further configure this DSLMethod
78
+ def initialize name, unique, required, &block
79
+ @arguments = Arguments.new
80
+
81
+ if name.is_a? Symbol
82
+
83
+ # don't allow methods to override existing internal methods
84
+ if Class.respond_to? name
85
+ raise MethodNameIsReservedError
86
+ end
87
+
88
+ @name = name
89
+ else
90
+ raise InvalidNameError
91
+ end
92
+
93
+ @unique = unique ? true : false
94
+ @required = required ? true : false
95
+
96
+ # If a block was provided, then we evaluate it using a seperate
97
+ # interpreter class. We do this because the interpreter class contains
98
+ # no other methods or variables, if it was evaluated in the context of
99
+ # this class then the block would have access to all of the methods defined
100
+ # in here.
101
+ if block
102
+ Interpreter.new(self).instance_eval(&block)
103
+ end
104
+ end
105
+
106
+ # Set the description for this DSLMethod to the provided value.
107
+ #
108
+ # `description` must be a string with a length greater than 0.
109
+ # The `description` can only be set once per DSLMethod
110
+ def set_description description
111
+ unless description.is_a?(String) && description.length > 0
112
+ raise InvalidDescriptionError
113
+ end
114
+
115
+ if has_description?
116
+ raise DescriptionAlreadyExistsError
117
+ end
118
+
119
+ @description = description
120
+ end
121
+
122
+ # Returns `true` if this DSL has a description, else false.
123
+ def has_description?
124
+ @description.nil? == false
125
+ end
126
+
127
+ # returns true if this DSLMethod is flagged as unique, otherwise returns false.
128
+ def unique?
129
+ @unique == true
130
+ end
131
+
132
+ # returns true if this DSLMethod is flagged as required, otherwise returns false.
133
+ def required?
134
+ @required == true
135
+ end
136
+
137
+ # returns true if this DSLMethod is flagged as optional, otherwise returns false.
138
+ def optional?
139
+ @required == false
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,72 @@
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
+
51
+ # adds a new optional argument to the DSLMethod
52
+ #
53
+ # name must be a symbol
54
+ # `type` can be either :integer, :boolean, :float, :string or :symbol
55
+ # `block` contains the argument definition and will be evaluated seperately
56
+ # by the Argument::Interpreter
57
+ def optional name, type, &block
58
+ @dsl.arguments.add_argument name, false, type, &block
59
+ end
60
+
61
+ # adds a new required argument to the DSLMethod
62
+ #
63
+ # name must be a symbol
64
+ # `type` can be either :integer, :boolean, :float, :string or :symbol
65
+ # `block` contains the argument definition and will be evaluated seperately
66
+ # by the Argument::Interpreter
67
+ def requires name, type, &block
68
+ @dsl.arguments.add_argument name, true, type, &block
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,152 @@
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
+ # an object which represents the argument configuration
53
+ attr_reader :arguments
54
+
55
+ # Create a new DSL object with the provided name and class.
56
+ #
57
+ # `name` must be a symbol.
58
+ # `klass` should be the class in which `define_dsl` is being called.
59
+ def initialize name, klass
60
+ @dsl_methods = {}
61
+
62
+ @arguments = Arguments.new
63
+
64
+ if name.is_a? Symbol
65
+ @name = name
66
+ else
67
+ raise InvalidNameError
68
+ end
69
+
70
+ @klass = klass
71
+ end
72
+
73
+ # Evaluate the configuration block which defines our new DSL
74
+ # `block` contains the DSL definition and will be evaluated to create
75
+ # the rest of the DSL.
76
+ def evaluate_configuration_block &block
77
+ if block
78
+ # We evaluate the internal DSL configuration blocks using a seperate interpreter
79
+ # class. We do this because the interpreter class contains no other methods or
80
+ # variables, if it was evaluated in the context of this class then the block
81
+ # would have access to all of the methods defined in here.
82
+ Interpreter.new(self).instance_eval(&block)
83
+ else
84
+ raise NoBlockProvidedError
85
+ end
86
+ end
87
+
88
+ # Set the description for this DSL to the provided value.
89
+ #
90
+ # `description` must be a string with a length greater than 0.
91
+ # The `description` can only be set once per DSL
92
+ def set_description description
93
+ unless description.is_a?(String) && description.length > 0
94
+ raise InvalidDescriptionError
95
+ end
96
+
97
+ if has_description?
98
+ raise DescriptionAlreadyExistsError
99
+ end
100
+
101
+ @description = description
102
+ end
103
+
104
+ # Returns `true` if this DSL has a description, else false.
105
+ def has_description?
106
+ @description.nil? == false
107
+ end
108
+
109
+ # Takes a method name, unique flag, required flag, and a block and creates
110
+ # a new DSLMethod object.
111
+ #
112
+ # Method `name` must be unique within the DSL.
113
+ def add_method name, unique, required, &block
114
+ if has_dsl_method? name
115
+ raise MethodAlreadyExistsError
116
+ end
117
+
118
+ @dsl_methods[name] = DSLMethod.new(name, unique, required, &block)
119
+ end
120
+
121
+ # Returns an array of all this DSLs DSLMethods.
122
+ def dsl_methods
123
+ @dsl_methods.values
124
+ end
125
+
126
+ # Returns an array of only the required DSLMethods in this DSL.
127
+ def required_dsl_methods
128
+ dsl_methods.filter(&:required?)
129
+ end
130
+
131
+ # Returns an array of only the optional DSLMethods in this DSL.
132
+ def optional_dsl_methods
133
+ dsl_methods.filter(&:optional?)
134
+ end
135
+
136
+ # returns a specific DSLMethod by it's name, if the DSLMethod does not
137
+ # exist, then an error is raised
138
+ def dsl_method name
139
+ if has_dsl_method? name
140
+ @dsl_methods[name]
141
+ else
142
+ raise MethodDoesNotExistError
143
+ end
144
+ end
145
+
146
+ # Returns `true` if a DSLMethod with the provided name exists in this
147
+ # DSL, otherwise it returns `false`.
148
+ def has_dsl_method? name
149
+ @dsl_methods.key? name
150
+ end
151
+ end
152
+ 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,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DSLCompose
4
+ class Interpreter
5
+ class Execution
6
+ class Arguments
7
+ class MissingRequiredArgumentsError < StandardError
8
+ def initialize required_count, provided_count
9
+ super "This method requires #{required_count} arguments, but only #{required_count} were provided"
10
+ end
11
+ end
12
+
13
+ class TooManyArgumentsError < StandardError
14
+ def message
15
+ "Too many arguments provided to this method"
16
+ end
17
+ end
18
+
19
+ class OptionalArgsShouldBeHashError < StandardError
20
+ def message
21
+ "If provided, then the optional arguments must be last, and be represented as a Hash"
22
+ end
23
+ end
24
+
25
+ class InvalidArgumentTypeError < StandardError
26
+ def message
27
+ "The provided argument is the wrong type"
28
+ end
29
+ end
30
+
31
+ attr_reader :arguments
32
+
33
+ def initialize arguments, *args
34
+ @arguments = {}
35
+
36
+ required_argument_count = arguments.required_arguments.count
37
+ has_optional_arguments = arguments.optional_arguments.any?
38
+
39
+ # the first N args, where N = required_argument_count, are the
40
+ # provided required arguments
41
+ required_args = args.slice(0, required_argument_count)
42
+ # the optional arg which comes next is the hash which represents
43
+ # all the optional arguments
44
+ optional_arg = args[required_argument_count]
45
+
46
+ # assert that a value is provided for every required argument
47
+ unless required_argument_count == required_args.count
48
+ raise MissingRequiredArgumentsError.new required_argument_count, required_args.count
49
+ end
50
+
51
+ # assert that too many arguments have not been provided
52
+ if args.count > required_argument_count + (has_optional_arguments ? 1 : 0)
53
+ raise TooManyArgumentsError
54
+ end
55
+
56
+ # asset that, if provided, then the optional argument (always the last one) is a Hash
57
+ if has_optional_arguments && optional_arg.nil? === false
58
+ unless optional_arg.is_a? Hash
59
+ raise OptionalArgsShouldBeHashError
60
+ end
61
+
62
+ # assert the each provided optional argument is valid
63
+ optional_arg.keys.each do |optional_argument_name|
64
+ optional_arg_value = optional_arg[optional_argument_name]
65
+ optional_argument = arguments.optional_argument optional_argument_name
66
+
67
+ case optional_argument.type
68
+ when :integer
69
+ unless optional_arg_value.is_a? Integer
70
+ raise InvalidArgumentTypeError
71
+ end
72
+ optional_argument.validate_integer! optional_arg_value
73
+
74
+ when :symbol
75
+ unless optional_arg_value.is_a? Symbol
76
+ raise InvalidArgumentTypeError
77
+ end
78
+ optional_argument.validate_symbol! optional_arg_value
79
+
80
+ when :string
81
+ unless optional_arg_value.is_a? String
82
+ raise InvalidArgumentTypeError
83
+ end
84
+ optional_argument.validate_string! optional_arg_value
85
+
86
+ when :boolean
87
+ unless optional_arg_value.is_a?(TrueClass) || optional_arg_value.is_a?(FalseClass)
88
+ raise InvalidArgumentTypeError
89
+ end
90
+ optional_argument.validate_boolean! optional_arg_value
91
+
92
+ else
93
+ raise InvalidArgumentTypeError
94
+ end
95
+
96
+ # the provided value appears valid for this argument, save the value
97
+ @arguments[optional_argument_name] = optional_arg_value
98
+ end
99
+
100
+ end
101
+
102
+ # validate the value provided to each required argument
103
+ arguments.required_arguments.each_with_index do |required_argument, i|
104
+ arg = args[i]
105
+ case required_argument.type
106
+ when :integer
107
+ unless arg.is_a? Integer
108
+ raise InvalidArgumentTypeError
109
+ end
110
+ required_argument.validate_integer! arg
111
+
112
+ when :symbol
113
+ unless arg.is_a? Symbol
114
+ raise InvalidArgumentTypeError
115
+ end
116
+ required_argument.validate_symbol! arg
117
+
118
+ when :string
119
+ unless arg.is_a? String
120
+ raise InvalidArgumentTypeError
121
+ end
122
+ required_argument.validate_string! arg
123
+
124
+ when :boolean
125
+ unless arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
126
+ raise InvalidArgumentTypeError
127
+ end
128
+ required_argument.validate_boolean! arg
129
+
130
+ else
131
+ raise InvalidArgumentTypeError
132
+ end
133
+
134
+ # the provided value appears valid for this argument, save the value
135
+ @arguments[required_argument.name] = arg
136
+ end
137
+ end
138
+
139
+ def to_h
140
+ @arguments
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,53 @@
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 = Arguments.new(dsl_method.arguments, *args)
38
+ end
39
+
40
+ def method_name
41
+ @dsl_method.name
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ arguments: @arguments.to_h
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ 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