dsl_maker 0.0.1

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.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'rake/clean'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ # This is used by the Yardoc stuff in docile's Rakefile. We're not there yet.
6
+ #require File.expand_path('on_what', File.dirname(__FILE__))
7
+
8
+ # Default task for `rake` is to run rspec
9
+ task :default => [:spec]
10
+
11
+ # Use default rspec rake task
12
+ RSpec::Core::RakeTask.new
13
+
14
+ # Configure `rake clobber` to delete all generated files
15
+ CLOBBER.include('pkg', 'doc', 'coverage')
16
+
17
+ if !on_travis? && !on_jruby? && !on_1_8?
18
+ require 'github/markup'
19
+ require 'redcarpet'
20
+ require 'yard'
21
+ require 'yard/rake/yardoc_task'
22
+
23
+ YARD::Rake::YardocTask.new do |t|
24
+ OTHER_PATHS = %w()
25
+ t.files = ['lib/**/*.rb', OTHER_PATHS]
26
+ t.options = %w(--markup-provider=redcarpet --markup=markdown --main=README.md)
27
+ end
28
+ end
data/dsl_maker.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ require File.expand_path('on_what', File.dirname(__FILE__))
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'dsl/maker/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'dsl_maker'
7
+ s.version = DSL::Maker::VERSION
8
+ s.author = 'Rob Kinyon'
9
+ s.email = 'rob.kinyon@gmail.com'
10
+ s.summary = 'Easy multi-level DSLs'
11
+ s.description = 'Easy multi-level DSLs, built on top of Docile'
12
+ s.license = 'GPL2'
13
+ s.homepage = 'https://github.com/robkinyon/ruby-dsl-maker'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = []
18
+ s.require_paths = %w(lib)
19
+
20
+ s.required_ruby_version = '>= 1.9.3'
21
+
22
+ s.add_dependency 'docile', '~> 1.1', '>= 1.1.0'
23
+
24
+ # Run rspec tests from rake
25
+ s.add_development_dependency 'rake', '~> 10'
26
+ s.add_development_dependency 'rspec', '~> 3.0.0', '>= 3.0.0'
27
+ s.add_development_dependency 'simplecov', '~> 0'
28
+
29
+ # To limit needed compatibility with versions of dependencies, only configure
30
+ # yard doc generation when *not* on Travis, JRuby, or 1.8
31
+ if !on_travis? && !on_jruby? && !on_1_8?
32
+ # Github flavored markdown in YARD documentation
33
+ # http://blog.nikosd.com/2011/11/github-flavored-markdown-in-yard.html
34
+ s.add_development_dependency 'yard', '~> 0.8'
35
+ s.add_development_dependency 'redcarpet', '~> 3'
36
+ s.add_development_dependency 'github-markup', '~> 1.3'
37
+ end
38
+ end
data/lib/dsl/maker.rb ADDED
@@ -0,0 +1,207 @@
1
+ require 'dsl/maker/version'
2
+
3
+ require 'docile'
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
17
+
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
37
+ end
38
+ 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
47
+ end
48
+
49
+ # TODO: Is this safe if the invoker doesn't use parse_dsl()?
50
+ @@accumulator = []
51
+
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
+ end
70
+
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
79
+ 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
89
+ 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)
124
+ end
125
+ end
126
+ else
127
+ raise "Unrecognized element type '#{type}'"
128
+ end
129
+
130
+ return
131
+ end
132
+
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
159
+
160
+ args.each do |name, type|
161
+ if klass.new.respond_to? name.to_sym
162
+ raise "Illegal attribute name '#{name}'"
163
+ end
164
+
165
+ build_dsl_element(klass, name, type)
166
+ end
167
+
168
+ return klass
169
+ end
170
+
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
192
+
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) {}
196
+
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
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,6 @@
1
+ module DSL
2
+ class Maker
3
+ # The current version of this library
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
data/on_what.rb ADDED
@@ -0,0 +1,15 @@
1
+ # NOTE: Very simple tests for what system we are on, extracted for sharing
2
+ # between Rakefile, gemspec, and spec_helper. Not for use in actual library.
3
+ # This is copied from https://github.com/ms-ati/docile/blob/master/on_what.rb
4
+
5
+ def on_travis?
6
+ ENV['CI'] == 'true'
7
+ end
8
+
9
+ def on_jruby?
10
+ defined?(RUBY_ENGINE) && 'jruby' == RUBY_ENGINE
11
+ end
12
+
13
+ def on_1_8?
14
+ RUBY_VERSION.start_with? '1.8'
15
+ end
data/spec/args_spec.rb ADDED
@@ -0,0 +1,163 @@
1
+ # This will use a DSL that defines fruit
2
+
3
+ describe "A DSL with argument handling describing fruit" do
4
+ Color = Struct.new(:name)
5
+ Fruit = Struct.new(:name, :color)
6
+
7
+ describe "with one argument in add_entrypoint" do
8
+ dsl_class = Class.new(DSL::Maker) do
9
+ add_entrypoint(:fruit, {
10
+ :name => String,
11
+ }) do |*args|
12
+ default(:name, args, 0)
13
+ Fruit.new(name, nil)
14
+ end
15
+ end
16
+
17
+ it "can handle nil" do
18
+ fruit = dsl_class.parse_dsl("
19
+ fruit
20
+ ")
21
+ expect(fruit).to be_instance_of(Fruit)
22
+ expect(fruit.name).to be_nil
23
+ end
24
+
25
+ it "can handle the name in the attribute" do
26
+ fruit = dsl_class.parse_dsl("
27
+ fruit { name 'banana' }
28
+ ")
29
+ expect(fruit).to be_instance_of(Fruit)
30
+ expect(fruit.name).to eq('banana')
31
+ end
32
+
33
+ it "can handle the name in the args" do
34
+ fruit = dsl_class.parse_dsl("
35
+ fruit 'banana'
36
+ ")
37
+ expect(fruit).to be_instance_of(Fruit)
38
+ expect(fruit.name).to eq('banana')
39
+ end
40
+
41
+ it "can handle setting the name in both" do
42
+ fruit = dsl_class.parse_dsl("
43
+ # Minions don't get to name fruit
44
+ fruit 'buh-nana!' do
45
+ name 'banana'
46
+ end
47
+ ")
48
+ expect(fruit).to be_instance_of(Fruit)
49
+ expect(fruit.name).to eq('banana')
50
+ end
51
+ end
52
+
53
+ describe "with two arguments in add_entrypoint" do
54
+ dsl_class = Class.new(DSL::Maker) do
55
+ add_entrypoint(:fruit, {
56
+ :name => String,
57
+ :color => String,
58
+ }) do |*args|
59
+ default('name', args)
60
+ default('color', args, 1)
61
+
62
+ Fruit.new(name, color)
63
+ end
64
+ end
65
+
66
+ it "can handle no arguments" do
67
+ fruit = dsl_class.parse_dsl("
68
+ fruit
69
+ ")
70
+ expect(fruit).to be_instance_of(Fruit)
71
+ expect(fruit.name).to be_nil
72
+ expect(fruit.color).to be_nil
73
+ end
74
+
75
+ it "can handle the name in args, color in attributes" do
76
+ # Must use parentheses if you want to curly-braces
77
+ fruit = dsl_class.parse_dsl("
78
+ fruit('banana') {
79
+ color 'yellow'
80
+ }
81
+ ")
82
+ expect(fruit).to be_instance_of(Fruit)
83
+ expect(fruit.name).to eq('banana')
84
+ expect(fruit.color).to eq('yellow')
85
+
86
+ # Must use do..end syntax if you want to avoid parentheses
87
+ fruit = dsl_class.parse_dsl("
88
+ fruit 'plantain' do
89
+ color 'green'
90
+ end
91
+ ")
92
+ expect(fruit).to be_instance_of(Fruit)
93
+ expect(fruit.name).to eq('plantain')
94
+ expect(fruit.color).to eq('green')
95
+ end
96
+
97
+ it "can handle everything in the args" do
98
+ fruit = dsl_class.parse_dsl("
99
+ fruit 'banana', 'yellow'
100
+ ")
101
+ expect(fruit).to be_instance_of(Fruit)
102
+ expect(fruit.name).to eq('banana')
103
+ expect(fruit.color).to eq('yellow')
104
+ end
105
+ end
106
+
107
+ describe "with one argument in generate_dsl" do
108
+ dsl_class = Class.new(DSL::Maker) do
109
+ add_entrypoint(:fruit, {
110
+ :name => String,
111
+ :color => generate_dsl({
112
+ :name => String,
113
+ }) { |*args|
114
+ default('name', args, 0)
115
+ Color.new(name)
116
+ }
117
+ }) do |*args|
118
+ default('name', args, 0)
119
+ Fruit.new(name, color)
120
+ end
121
+ end
122
+
123
+ it "can handle arguments for fruit, but attribute for color" do
124
+ fruit = dsl_class.parse_dsl("
125
+ fruit 'banana' do
126
+ color {
127
+ name 'yellow'
128
+ }
129
+ end
130
+ ")
131
+ expect(fruit).to be_instance_of(Fruit)
132
+ expect(fruit.name).to eq('banana')
133
+ expect(fruit.color).to be_instance_of(Color)
134
+ expect(fruit.color.name).to eq('yellow')
135
+ end
136
+
137
+ it "can handle arguments for both fruit and color" do
138
+ fruit = dsl_class.parse_dsl("
139
+ fruit 'banana' do
140
+ color 'yellow'
141
+ end
142
+ ")
143
+ expect(fruit).to be_instance_of(Fruit)
144
+ expect(fruit.name).to eq('banana')
145
+ expect(fruit.color).to be_instance_of(Color)
146
+ expect(fruit.color.name).to eq('yellow')
147
+ end
148
+
149
+ it "can handle arguments for both fruit and color" do
150
+ fruit = dsl_class.parse_dsl("
151
+ fruit 'banana' do
152
+ color 'yellow' do
153
+ name 'green'
154
+ end
155
+ end
156
+ ")
157
+ expect(fruit).to be_instance_of(Fruit)
158
+ expect(fruit.name).to eq('banana')
159
+ expect(fruit.color).to be_instance_of(Color)
160
+ expect(fruit.color.name).to eq('green')
161
+ end
162
+ end
163
+ end