dsl_maker 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b6149b9dfdb94cd22dec949a542d68eaf6ab8a58
4
- data.tar.gz: 688c84bd9102359e839b2884967c610f6435a726
3
+ metadata.gz: 0a86f0dfab03af6d1b25904ef7c7dd0ef4090b33
4
+ data.tar.gz: f63cd7dd2beaa1850851dba11a42d19ac67300a0
5
5
  SHA512:
6
- metadata.gz: edd7f3990066e812e461e42981cc920517f434d34ceab0801f16dcd8f9ab385828b6faea8732cd7f9f638fa11a3e2e596075253d135b9063c9a1b5431bc08ec3
7
- data.tar.gz: cf857c182b8145d4b47dcd8b08e12c3697f60f2ea9a50aed770607112178f40185ce23d0a9086c5147999341b18cbceb10512fa94cc2c0fe22b94435815b8036
6
+ metadata.gz: bb2d7b7e998e65c35b595904fe49fb0b42c549d8796d303a5ac9b5faa366f7b6fb12e4ba3a74fef8517c961eb5a9086b44fde8a1080481a4028e2eb6a9deeffb
7
+ data.tar.gz: 8a9aafadbc5c3c4a99e071609e49abf2b95f115b8106431e272500b96fad7afa26e913bd465fc762ff5525b1718dec33edcf1c8e83283c89cdf84a3cca43429a
data/Changes ADDED
@@ -0,0 +1,10 @@
1
+ Revision history for DSL::Maker (ordered by revision number).
2
+
3
+ 0.0.2 Jul 22 2015
4
+ - Allow add_entrypoint() to take a DSL class instead of args
5
+ - Added execute_dsl() that takes a block
6
+ - Several refactorings to improve maintainability
7
+ - Recursive DSLs are now simpler to define and maintain
8
+
9
+ 0.0.1 Jul 21 2015
10
+ - Initial release
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # DSL::Maker
2
2
 
3
+ [![Gem Version](https://img.shields.io/gem/v/dsl_maker.svg)](https://rubygems.org/gems/dsl_maker)
4
+ [![Gem Downloads](https://img.shields.io/gem/dt/dsl_maker.svg)](https://rubygems.org/gems/dsl_maker)
5
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/robkinyon/ruby-dsl-maker)
6
+
3
7
  [![Build Status](https://img.shields.io/travis/robkinyon/ruby-dsl-maker.svg)](https://travis-ci.org/robkinyon/ruby-dsl-maker)
4
8
  [![Code Climate](https://img.shields.io/codeclimate/github/robkinyon/ruby-dsl-maker.svg)](https://codeclimate.com/github/robkinyon/ruby-dsl-maker)
5
9
  [![Code Coverage](https://img.shields.io/codecov/c/github/robkinyon/ruby-dsl-maker.svg)](https://codecov.io/github/robkinyon/ruby-dsl-maker)
@@ -157,7 +161,7 @@ best to explain.
157
161
 
158
162
  ```ruby
159
163
  Person = Struct.new(:name, :age, :mother, :father)
160
- class FamilyTree < DSL::Maker
164
+ class FamilyTreeDSL < DSL::Maker
161
165
  parent = generate_dsl({
162
166
  :name => String,
163
167
  :age => String,
@@ -177,7 +181,7 @@ class FamilyTree < DSL::Maker
177
181
  end
178
182
  end
179
183
 
180
- john_smith = FamilyTree.parse_dsl("
184
+ john_smith = FamilyTreeDSL.parse_dsl("
181
185
  person 'John Smith' do
182
186
  age 20
183
187
  mother 'Mary Smith' do
@@ -193,6 +197,31 @@ john_smith = FamilyTree.parse_dsl("
193
197
 
194
198
  The result is exactly the same as before.
195
199
 
200
+ ### Recursive DSLs
201
+
202
+ We're making an artificial distinction between `person` and `parent` in the
203
+ `FamilyTreeDSL` example above. Really, we want to say "A person can have a mother
204
+ and a father and those are also persons." So, let's say that.
205
+
206
+ ```ruby
207
+ Person = Struct.new(:name, :age, :mother, :father)
208
+
209
+ class FamilyTreeDSL < DSL::Maker
210
+ person_dsl = add_entrypoint(:person, {
211
+ :name => String,
212
+ :age => String,
213
+ }) do |*args|
214
+ default(:name, args)
215
+ Person.new(name, age mother, father)
216
+ end
217
+
218
+ build_dsl_element(person_dsl, :mother, person_dsl)
219
+ build_dsl_element(person_dsl, :father, person_dsl)
220
+ end
221
+ ```
222
+
223
+ Now, we can handle an arbitrarily-deep family tree.
224
+
196
225
  ### Handling multiple items
197
226
 
198
227
  Chef's recipe files have many entries of different types in them. It doesn't do
@@ -239,7 +268,7 @@ you get back an `Array` with everything in the right order.
239
268
 
240
269
  ### Class Methods
241
270
 
242
- DSL::Maker provides three class methods - two for constructing your DSL and one
271
+ DSL::Maker provides five class methods - three for constructing your DSL and two
243
272
  for parsing your DSL.
244
273
 
245
274
  * `add_entrypoint(Symbol, Hash={}, Block)`
@@ -261,13 +290,13 @@ This is normally called by `generate_dsl()` to actually construct the DSL elemen
261
290
  It is provided for you so that you can create recursive DSL definitions. Look at
262
291
  the tests in `spec/multi_level_spec.rb` for an example of this.
263
292
 
264
- * `parse_dsl(String)`
293
+ * `parse_dsl(String)` / `execute_dsl(&block)`
265
294
 
266
295
  You call this on your DSL class when you're ready to invoke your DSL. It will
267
296
  return whatever the block provided `add_entrypoint()` returns.
268
297
 
269
298
  In the case of multiple DSL entrypoints (for example, a normal Chef recipe),
270
- `parse_dsl()` will return an array with all the return values in the order of
299
+ these methods will return an array with all the return values in the order of
271
300
  invocation.
272
301
 
273
302
  ### Coercions
@@ -1,6 +1,6 @@
1
1
  module DSL
2
2
  class Maker
3
3
  # The current version of this library
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.2'
5
5
  end
6
6
  end
data/lib/dsl/maker.rb CHANGED
@@ -2,206 +2,228 @@ require 'dsl/maker/version'
2
2
 
3
3
  require 'docile'
4
4
 
5
- # The DSL namespace
6
- module DSL
7
- # This is the base class we provide.
8
- class Maker
9
- # This is a useful class that contains all the Boolean handling we have.
10
- class Boolean
11
- {
12
- :yes => true, :no => false,
13
- :on => true, :off => false,
14
- }.each do |name, result|
15
- define_method(name) { result }
16
- end
5
+ # This is the base class we provide.
6
+ class DSL::Maker
7
+ # This is a useful class that contains all the Boolean handling we have.
8
+ class Boolean
9
+ {
10
+ :yes => true, :no => false,
11
+ :on => true, :off => false,
12
+ }.each do |name, result|
13
+ define_method(name) { result }
14
+ end
17
15
 
18
- # 21 character method names are obscene. Make it easier to read.
19
- alias :___set :instance_variable_set
20
-
21
- # 21 character method names are obscene. Make it easier to read.
22
- alias :___get :instance_variable_get
23
-
24
- # A helper method for handling defaults from args easily.
25
- #
26
- # @param method_name [String] The name of the attribute being defaulted.
27
- # @param args [Array] The arguments provided to the block.
28
- # @param position [Integer] The index in args to work with, default 0.
29
- #
30
- # @return nil
31
- def default(method_name, args, position=0)
32
- method = method_name.to_sym
33
- if args.length >= (position + 1) && !self.send(method)
34
- self.send(method, args[position])
35
- end
36
- return
16
+ # 21 character method names are obscene. Make it easier to read.
17
+ alias :___set :instance_variable_set
18
+
19
+ # 21 character method names are obscene. Make it easier to read.
20
+ alias :___get :instance_variable_get
21
+
22
+ # A helper method for handling defaults from args easily.
23
+ #
24
+ # @param method_name [String] The name of the attribute being defaulted.
25
+ # @param args [Array] The arguments provided to the block.
26
+ # @param position [Integer] The index in args to work with, default 0.
27
+ #
28
+ # @return nil
29
+ def default(method_name, args, position=0)
30
+ method = method_name.to_sym
31
+ if args.length >= (position + 1) && !self.send(method)
32
+ self.send(method, args[position])
37
33
  end
34
+ return
38
35
  end
39
- Yes = On = True = true
40
- No = Off = False = false
41
- $to_bool = lambda do |value|
42
- if value
43
- return false if %w(no off false nil).include? value.to_s.downcase
44
- end
45
- # The bang-bang boolean-izes the value. We want this to be lossy.
46
- !!value
36
+ end
37
+ Yes = On = True = true
38
+ No = Off = False = false
39
+ $to_bool = lambda do |value|
40
+ if value
41
+ return false if %w(no off false nil).include? value.to_s.downcase
47
42
  end
43
+ # The bang-bang boolean-izes the value. We want this to be lossy.
44
+ !!value
45
+ end
48
46
 
49
- # TODO: Is this safe if the invoker doesn't use parse_dsl()?
47
+ # TODO: Is this safe if the invoker doesn't use parse_dsl()?
48
+ @@accumulator = []
49
+
50
+ # Parse the DSL provided in the parameter.
51
+ #
52
+ # @note If the DSL contains multiple entrypoints, then this will return an
53
+ # Array. This is desirable.
54
+ #
55
+ # @param dsl [String] The DSL to be parsed by this class.
56
+ #
57
+ # @return [Object] Whatever is returned by the block defined in this class.
58
+ def self.parse_dsl(dsl)
59
+ # add_entrypoint() will use @@accumulator to handle multiple entrypoints.
60
+ # Reset it here so that we're only handling the values from this run.
50
61
  @@accumulator = []
62
+ eval dsl, self.get_binding
63
+ if @@accumulator.length <= 1
64
+ return @@accumulator[0]
65
+ end
66
+ return @@accumulator
67
+ end
51
68
 
52
- # Parse the DSL provided in the parameter
53
- #
54
- # @note If the DSL contains multiple entrypoints, then this will return an
55
- # Array. This is desirable.
56
- #
57
- # @param dsl [String] The DSL to be parsed by this class.
58
- #
59
- # @return [Object] Whatever is returned by the block defined in this class.
60
- def self.parse_dsl(dsl)
61
- # add_entrypoint() will use @@accumulator to handle multiple entrypoints.
62
- # Reset it here so that we're only handling the values from this run.
63
- @@accumulator = []
64
- eval dsl, self.get_binding
65
- if @@accumulator.length <= 1
66
- return @@accumulator[0]
67
- end
68
- return @@accumulator
69
+ # Execute the DSL provided in the block.
70
+ #
71
+ # @note If the DSL contains multiple entrypoints, then this will return an
72
+ # Array. This is desirable.
73
+ #
74
+ # @param &block [Block] The DSL to be executed by this class.
75
+ #
76
+ # @return [Object] Whatever is returned by &block
77
+ def self.execute_dsl(&block)
78
+ @@accumulator = []
79
+ instance_eval(&block)
80
+ if @@accumulator.length <= 1
81
+ return @@accumulator[0]
69
82
  end
83
+ return @@accumulator
84
+ end
70
85
 
71
- @@dsl_elements = {
72
- String => ->(klass, name, type) {
73
- as_attr = '@' + name.to_s
74
- klass.class_eval do
75
- define_method(name.to_sym) do |*args|
76
- ___set(as_attr, args[0].to_s) unless args.empty?
77
- ___get(as_attr)
78
- end
86
+ @@dsl_elements = {
87
+ String => ->(klass, name, type) {
88
+ as_attr = '@' + name.to_s
89
+ klass.class_eval do
90
+ define_method(name.to_sym) do |*args|
91
+ ___set(as_attr, args[0].to_s) unless args.empty?
92
+ ___get(as_attr)
79
93
  end
80
- },
81
- Boolean => ->(klass, name, type) {
82
- as_attr = '@' + name.to_s
83
- klass.class_eval do
84
- define_method(name.to_sym) do |*args|
85
- ___set(as_attr, $to_bool.call(args[0])) unless args.empty?
86
- # Ensure that the default nil returns as false.
87
- !!___get(as_attr)
88
- end
94
+ end
95
+ },
96
+ Boolean => ->(klass, name, type) {
97
+ as_attr = '@' + name.to_s
98
+ klass.class_eval do
99
+ define_method(name.to_sym) do |*args|
100
+ ___set(as_attr, $to_bool.call(args[0])) unless args.empty?
101
+ # Ensure that the default nil returns as false.
102
+ !!___get(as_attr)
89
103
  end
90
- },
91
- }
92
-
93
- # Add a single element of a DSL to a class representing a level in a DSL.
94
- #
95
- # Each of the types represents a coercion - a guarantee and check of the value
96
- # in that name. The standard coercions are:
97
- #
98
- # * String - whatever you give is returned.
99
- # * Boolean - the truthiness of whatever you give is returned.
100
- # * generate_dsl() - this represents a new level of the DSL.
101
- #
102
- # @param klass [Class] The class representing this level in the DSL.
103
- # @param name [String] The name of the element we're working on.
104
- # @param type [Class] The type of this element we're working on.
105
- # This is the coercion spoken above.
106
- #
107
- # @return nil
108
- def self.build_dsl_element(klass, name, type)
109
- if @@dsl_elements.has_key?(type)
110
- @@dsl_elements[type].call(klass, name, type)
111
- elsif type.is_a?(Class) && type.ancestors.include?(Boolean)
112
- as_attr = '@' + name.to_s
113
- klass.class_eval do
114
- define_method(name.to_sym) do |*args, &dsl_block|
115
- unless (args.empty? && !dsl_block)
116
- obj = type.new
117
- Docile.dsl_eval(obj, &dsl_block) if dsl_block
118
-
119
- # I don't know why this code doesn't work, but it's why __apply().
120
- #___set(as_attr, obj.instance_exec(*args, &defn_block))
121
- ___set(as_attr, obj.__apply(*args))
122
- end
123
- ___get(as_attr)
104
+ end
105
+ },
106
+ }
107
+
108
+ # Add a single element of a DSL to a class representing a level in a DSL.
109
+ #
110
+ # Each of the types represents a coercion - a guarantee and check of the value
111
+ # in that name. The standard coercions are:
112
+ #
113
+ # * String - whatever you give is returned.
114
+ # * Boolean - the truthiness of whatever you give is returned.
115
+ # * generate_dsl() - this represents a new level of the DSL.
116
+ #
117
+ # @param klass [Class] The class representing this level in the DSL.
118
+ # @param name [String] The name of the element we're working on.
119
+ # @param type [Class] The type of this element we're working on.
120
+ # This is the coercion spoken above.
121
+ #
122
+ # @return nil
123
+ def self.build_dsl_element(klass, name, type)
124
+ if @@dsl_elements.has_key?(type)
125
+ @@dsl_elements[type].call(klass, name, type)
126
+ elsif type.is_a?(Class) && type.ancestors.include?(Boolean)
127
+ as_attr = '@' + name.to_s
128
+ klass.class_eval do
129
+ define_method(name.to_sym) do |*args, &dsl_block|
130
+ unless (args.empty? && !dsl_block)
131
+ obj = type.new
132
+ Docile.dsl_eval(obj, &dsl_block) if dsl_block
133
+
134
+ # I don't know why this code doesn't work, but it's why __apply().
135
+ #___set(as_attr, obj.instance_exec(*args, &defn_block))
136
+ ___set(as_attr, obj.__apply(*args))
124
137
  end
138
+ ___get(as_attr)
125
139
  end
126
- else
127
- raise "Unrecognized element type '#{type}'"
128
140
  end
129
-
130
- return
141
+ else
142
+ raise "Unrecognized element type '#{type}'"
131
143
  end
132
144
 
133
- # Add the meat of a DSL block to some level of this class's DSL.
134
- #
135
- # In order for Docile to parse a DSL, each level must be represented by a
136
- # different class. This method creates anonymous classes that each represents
137
- # a different level in the DSL's structure.
138
- #
139
- # The creation of each DSL element is delegated to build_dsl_element.
140
- #
141
- # @param args [Hash] the elements of the DSL block (passed to generate_dsl)
142
- # @param defn_block [Proc] what is executed once the DSL block is parsed.
143
- #
144
- # @return [Class] The class that implements this level's DSL definition.
145
- def self.generate_dsl(args={}, &defn_block)
146
- raise 'Block required for generate_dsl' unless block_given?
147
-
148
- # Inherit from the Boolean class to gain access to the useful methods
149
- # TODO: Convert DSL::Maker::Boolean into a Role
150
- # TODO: Create a DSL::Maker::Base class to inherit from
151
- klass = Class.new(Boolean) do
152
- # This instance method exists because we cannot seem to inline its work
153
- # where we call it. Could it be a problem of incorrect binding?
154
- # It has to be defined here because it needs access to &defn_block
155
- define_method(:__apply) do |*args|
156
- instance_exec(*args, &defn_block)
157
- end
158
- end
145
+ return
146
+ end
159
147
 
160
- args.each do |name, type|
161
- if klass.new.respond_to? name.to_sym
162
- raise "Illegal attribute name '#{name}'"
163
- end
148
+ # Add the meat of a DSL block to some level of this class's DSL.
149
+ #
150
+ # In order for Docile to parse a DSL, each level must be represented by a
151
+ # different class. This method creates anonymous classes that each represents
152
+ # a different level in the DSL's structure.
153
+ #
154
+ # The creation of each DSL element is delegated to build_dsl_element.
155
+ #
156
+ # @param args [Hash] the elements of the DSL block (passed to generate_dsl)
157
+ # @param defn_block [Proc] what is executed once the DSL block is parsed.
158
+ #
159
+ # @return [Class] The class that implements this level's DSL definition.
160
+ def self.generate_dsl(args={}, &defn_block)
161
+ raise 'Block required for generate_dsl' unless block_given?
162
+
163
+ # Inherit from the Boolean class to gain access to the useful methods
164
+ # TODO: Convert DSL::Maker::Boolean into a Role
165
+ # TODO: Create a DSL::Maker::Base class to inherit from
166
+ dsl_class = Class.new(Boolean) do
167
+ # This instance method exists because we cannot seem to inline its work
168
+ # where we call it. Could it be a problem of incorrect binding?
169
+ # It has to be defined here because it needs access to &defn_block
170
+ define_method(:__apply) do |*args|
171
+ instance_exec(*args, &defn_block)
172
+ end
173
+ end
164
174
 
165
- build_dsl_element(klass, name, type)
175
+ args.each do |name, type|
176
+ if dsl_class.new.respond_to? name.to_sym
177
+ raise "Illegal attribute name '#{name}'"
166
178
  end
167
179
 
168
- return klass
180
+ build_dsl_element(dsl_class, name, type)
169
181
  end
170
182
 
171
- # Add an entrypoint (top-level DSL element) to this class's DSL.
172
- #
173
- # This delegates to generate_dsl() for the majority of the work.
174
- #
175
- # @param name [String] the name of the entrypoint
176
- # @param args [Hash] the elements of the DSL block (passed to generate_dsl)
177
- # @param defn_block [Proc] what is executed once the DSL block is parsed.
178
- #
179
- # @return nil
180
- def self.add_entrypoint(name, args={}, &defn_block)
181
- # Without defn_block, there's no way to give back the result of the
182
- # DSL parsing. So, raise an error if we don't get one.
183
- # TODO: Provide a default block that returns the datastructure as a HoH.
184
- raise "Block required for add_entrypoint" unless block_given?
185
-
186
- # Ensure that get_binding() exists in the child class. This is necessary to
187
- # provide parse_dsl() so that eval works as expected. We have to do it here
188
- # because this is the only place we know for certain will be called.
189
- unless self.respond_to? :get_binding
190
- define_singleton_method(:get_binding) { binding }
191
- end
183
+ return dsl_class
184
+ end
192
185
 
193
- # FIXME: This is a wart. Really, we should be pulling out name, then
194
- # yielding to generate_dsl() in some fashion.
195
- dsl_class = generate_dsl(args) {}
186
+ # Add an entrypoint (top-level DSL element) to this class's DSL.
187
+ #
188
+ # This delegates to generate_dsl() for the majority of the work.
189
+ #
190
+ # @note `args` could be a Hash (to be passed to generate_dsl()) or the result
191
+ # of a call to generate_dsl().
192
+ #
193
+ # @param name [String] the name of the entrypoint
194
+ # @param args [Hash] the elements of the DSL block (passed to generate_dsl)
195
+ # @param defn_block [Proc] what is executed once the DSL block is parsed.
196
+ #
197
+ # @return [Class] The class that implements this level's DSL definition.
198
+ def self.add_entrypoint(name, args={}, &defn_block)
199
+ # Without defn_block, there's no way to give back the result of the
200
+ # DSL parsing. So, raise an error if we don't get one.
201
+ # TODO: Provide a default block that returns the datastructure as a HoH.
202
+ raise "Block required for add_entrypoint" unless block_given?
203
+
204
+ # Ensure that get_binding() exists in the child class. This is necessary to
205
+ # provide parse_dsl() so that eval works as expected. We have to do it here
206
+ # because this is the only place we know for certain will be called.
207
+ unless self.respond_to? :get_binding
208
+ define_singleton_method(:get_binding) { binding }
209
+ end
196
210
 
197
- define_singleton_method(name.to_sym) do |*args, &dsl_block|
198
- obj = dsl_class.new
199
- Docile.dsl_eval(obj, &dsl_block) if dsl_block
200
- rv = obj.instance_exec(*args, &defn_block)
201
- @@accumulator.push(rv)
202
- return rv
203
- end
204
- return
211
+ # FIXME: This is a wart. Really, we should be pulling out name, then
212
+ # yielding to generate_dsl() in some fashion.
213
+ if args.is_a?(Class) && args.ancestors.include?(Boolean)
214
+ dsl_class = args
215
+ else
216
+ dsl_class = generate_dsl(args, &defn_block)
205
217
  end
218
+
219
+ define_singleton_method(name.to_sym) do |*args, &dsl_block|
220
+ obj = dsl_class.new
221
+ Docile.dsl_eval(obj, &dsl_block) if dsl_block
222
+ rv = obj.instance_exec(*args, &defn_block)
223
+ @@accumulator.push(rv)
224
+ return rv
225
+ end
226
+
227
+ return dsl_class
206
228
  end
207
229
  end
@@ -92,47 +92,43 @@ describe 'A multi-level DSL making family-trees' do
92
92
  expect(person.child.child.name).to eq('Judith')
93
93
  end
94
94
 
95
- it "can define things recursively" do
96
- dsl_class = Class.new(DSL::Maker) do
97
- person = generate_dsl({
98
- :name => String,
99
- }) {
100
- Person.new(name, child)
101
- }
102
- build_dsl_element(person, :child, person)
103
-
104
- add_entrypoint(:person, {
105
- :name => String,
106
- :child => person,
107
- }) do
108
- Person.new(name, child)
95
+ describe "with recursion" do
96
+ it "can handle a single axis of recursion" do
97
+ dsl_class = Class.new(DSL::Maker) do
98
+ person_dsl = add_entrypoint(:person, {
99
+ :name => String,
100
+ }) do
101
+ Person.new(name, child)
102
+ end
103
+ build_dsl_element(person_dsl, :child, person_dsl)
109
104
  end
110
- end
111
105
 
112
- # This is taken from https://en.wikipedia.org/wiki/Family_tree_of_the_Bible
113
- person = dsl_class.parse_dsl("
114
- person {
115
- name 'Adam'
116
- child {
117
- name 'Seth'
106
+ # This list of names is taken from
107
+ # https://en.wikipedia.org/wiki/Family_tree_of_the_Bible
108
+ person = dsl_class.parse_dsl("
109
+ person {
110
+ name 'Adam'
118
111
  child {
119
- name 'Enos'
112
+ name 'Seth'
120
113
  child {
121
- name 'Cainan'
114
+ name 'Enos'
122
115
  child {
123
- name 'Mahalaleel'
116
+ name 'Cainan'
124
117
  child {
125
- name 'Jared'
118
+ name 'Mahalaleel'
126
119
  child {
127
- name 'Enoch'
120
+ name 'Jared'
128
121
  child {
129
- name 'Methuselah'
122
+ name 'Enoch'
130
123
  child {
131
- name 'Lamech'
124
+ name 'Methuselah'
132
125
  child {
133
- name 'Noah'
126
+ name 'Lamech'
134
127
  child {
135
- name 'Shem'
128
+ name 'Noah'
129
+ child {
130
+ name 'Shem'
131
+ }
136
132
  }
137
133
  }
138
134
  }
@@ -143,16 +139,52 @@ describe 'A multi-level DSL making family-trees' do
143
139
  }
144
140
  }
145
141
  }
146
- }
147
- ")
142
+ ")
143
+
144
+ [
145
+ 'Adam', 'Seth', 'Enos', 'Cainan', 'Mahalaleel', 'Jared',
146
+ 'Enoch', 'Methuselah', 'Lamech', 'Noah', 'Shem',
147
+ ].each do |name|
148
+ expect(person).to be_instance_of(Person)
149
+ expect(person.name).to eq(name)
150
+ person = person.child
151
+ end
152
+ end
153
+
154
+ it "can handle two axes of recursion" do
155
+ OtherPerson = Struct.new(:name, :mother, :father)
156
+ dsl_class = Class.new(DSL::Maker) do
157
+ person_dsl = add_entrypoint(:person, {
158
+ :name => String,
159
+ }) do
160
+ OtherPerson.new(name, mother, father)
161
+ end
162
+ build_dsl_element(person_dsl, :mother, person_dsl)
163
+ build_dsl_element(person_dsl, :father, person_dsl)
164
+ end
165
+
166
+ person = dsl_class.parse_dsl("
167
+ person {
168
+ name 'John Smith'
169
+ mother {
170
+ name 'Mary Smith'
171
+ }
172
+ father {
173
+ name 'Tom Smith'
174
+ }
175
+ }
176
+ ")
177
+
178
+ expect(person).to be_instance_of(OtherPerson)
179
+ expect(person.name).to eq('John Smith')
180
+
181
+ mother = person.mother
182
+ expect(mother).to be_instance_of(OtherPerson)
183
+ expect(mother.name).to eq('Mary Smith')
148
184
 
149
- [
150
- 'Adam', 'Seth', 'Enos', 'Cainan', 'Mahalaleel', 'Jared',
151
- 'Enoch', 'Methuselah', 'Lamech', 'Noah', 'Shem',
152
- ].each do |name|
153
- expect(person).to be_instance_of(Person)
154
- expect(person.name).to eq(name)
155
- person = person.child
185
+ father = person.father
186
+ expect(father).to be_instance_of(OtherPerson)
187
+ expect(father.name).to eq('Tom Smith')
156
188
  end
157
189
  end
158
190
  end
@@ -53,4 +53,33 @@ describe "A DSL describing cars used with multiple invocations" do
53
53
  expect(vehicles[2]).to be_instance_of(Truck)
54
54
  expect(vehicles[2].maker).to eq('Toyota')
55
55
  end
56
+
57
+ it "does all the same things with execute_dsl" do
58
+ dsl_class = Class.new(DSL::Maker) do
59
+ add_entrypoint(:car, {
60
+ :maker => String,
61
+ }) do
62
+ Car.new(maker)
63
+ end
64
+ add_entrypoint(:truck, {
65
+ :maker => String,
66
+ }) do
67
+ Truck.new(maker)
68
+ end
69
+ end
70
+
71
+ vehicles = dsl_class.execute_dsl do
72
+ truck { maker 'Ford' }
73
+ car { maker 'Honda' }
74
+ truck { maker 'Toyota' }
75
+ end
76
+ expect(vehicles).to be_instance_of(Array)
77
+ expect(vehicles.length).to eq(3)
78
+ expect(vehicles[0]).to be_instance_of(Truck)
79
+ expect(vehicles[0].maker).to eq('Ford')
80
+ expect(vehicles[1]).to be_instance_of(Car)
81
+ expect(vehicles[1].maker).to eq('Honda')
82
+ expect(vehicles[2]).to be_instance_of(Truck)
83
+ expect(vehicles[2].maker).to eq('Toyota')
84
+ end
56
85
  end
@@ -103,12 +103,15 @@ describe 'A single-level DSL for pizza' do
103
103
 
104
104
  it 'makes a pizza with everything' do
105
105
  dsl_class = Class.new(DSL::Maker) do
106
- add_entrypoint(:pizza, {
106
+ toppings_dsl = generate_dsl({
107
107
  :cheese => DSL::Maker::Boolean,
108
108
  :bacon => DSL::Maker::Boolean,
109
109
  :pepperoni => DSL::Maker::Boolean,
110
110
  :sauce => String,
111
- }) do
111
+ }) {}
112
+
113
+ # This is a wart - this block should be against toppings_dsl, not here.
114
+ add_entrypoint(:pizza, toppings_dsl) do
112
115
  Pizza.new(cheese, pepperoni, bacon, sauce)
113
116
  end
114
117
  end
@@ -128,4 +131,35 @@ describe 'A single-level DSL for pizza' do
128
131
  :cheese => true,
129
132
  )
130
133
  end
134
+
135
+ it 'can execute the DSL directly' do
136
+ dsl_class = Class.new(DSL::Maker) do
137
+ toppings_dsl = generate_dsl({
138
+ :cheese => DSL::Maker::Boolean,
139
+ :bacon => DSL::Maker::Boolean,
140
+ :pepperoni => DSL::Maker::Boolean,
141
+ :sauce => String,
142
+ }) {}
143
+
144
+ # This is a wart - this block should be against toppings_dsl, not here.
145
+ add_entrypoint(:pizza, toppings_dsl) do
146
+ Pizza.new(cheese, pepperoni, bacon, sauce)
147
+ end
148
+ end
149
+
150
+ pizza = dsl_class.execute_dsl do
151
+ pizza {
152
+ cheese yes
153
+ pepperoni yes
154
+ bacon no
155
+ sauce :extra
156
+ }
157
+ end
158
+ verify_pizza(pizza,
159
+ :sauce => 'extra',
160
+ :pepperoni => true,
161
+ :bacon => false,
162
+ :cheese => true,
163
+ )
164
+ end
131
165
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsl_maker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Kinyon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-21 00:00:00.000000000 Z
11
+ date: 2015-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: docile
@@ -131,6 +131,7 @@ files:
131
131
  - ".rspec"
132
132
  - ".travis.yml"
133
133
  - ".yardopts"
134
+ - Changes
134
135
  - Gemfile
135
136
  - LICENSE
136
137
  - README.md