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.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +42 -0
- data/.rspec +4 -0
- data/.travis.yml +20 -0
- data/.yardopts +7 -0
- data/Gemfile +12 -0
- data/LICENSE +340 -0
- data/README.md +321 -0
- data/Rakefile +28 -0
- data/dsl_maker.gemspec +38 -0
- data/lib/dsl/maker.rb +207 -0
- data/lib/dsl/maker/version.rb +6 -0
- data/on_what.rb +15 -0
- data/spec/args_spec.rb +163 -0
- data/spec/error_spec.rb +47 -0
- data/spec/multi_level_spec.rb +158 -0
- data/spec/multiple_invocation_spec.rb +56 -0
- data/spec/single_level_spec.rb +131 -0
- data/spec/spec_helper.rb +31 -0
- metadata +172 -0
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
|
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
|