dsl_compose 1.0.0 → 1.2.0

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