dsel 0.1.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/Gemfile +15 -0
  4. data/LICENSE.md +23 -0
  5. data/README.md +113 -0
  6. data/Rakefile +6 -0
  7. data/dsel.gemspec +22 -0
  8. data/lib/dsel/api/generator.rb +285 -0
  9. data/lib/dsel/api/node.rb +241 -0
  10. data/lib/dsel/api.rb +8 -0
  11. data/lib/dsel/dsl/mixins/environment/ivar_explorer.rb +34 -0
  12. data/lib/dsel/dsl/nodes/api/environment.rb +49 -0
  13. data/lib/dsel/dsl/nodes/api.rb +18 -0
  14. data/lib/dsel/dsl/nodes/api_builder/environment.rb +56 -0
  15. data/lib/dsel/dsl/nodes/api_builder.rb +71 -0
  16. data/lib/dsel/dsl/nodes/base/environment.rb +50 -0
  17. data/lib/dsel/dsl/nodes/base.rb +110 -0
  18. data/lib/dsel/dsl/nodes/direct/environment.rb +14 -0
  19. data/lib/dsel/dsl/nodes/direct.rb +75 -0
  20. data/lib/dsel/dsl/nodes/proxy/environment.rb +41 -0
  21. data/lib/dsel/dsl/nodes/proxy.rb +20 -0
  22. data/lib/dsel/dsl.rb +10 -0
  23. data/lib/dsel/node.rb +42 -0
  24. data/lib/dsel/ruby/object.rb +53 -0
  25. data/lib/dsel/version.rb +3 -0
  26. data/lib/dsel.rb +10 -0
  27. data/spec/dsel/api/generator_spec.rb +402 -0
  28. data/spec/dsel/api/node_spec.rb +328 -0
  29. data/spec/dsel/dsel_spec.rb +63 -0
  30. data/spec/dsel/dsl/nodes/api/environment.rb +208 -0
  31. data/spec/dsel/dsl/nodes/api_builder/environment_spec.rb +91 -0
  32. data/spec/dsel/dsl/nodes/api_builder_spec.rb +148 -0
  33. data/spec/dsel/dsl/nodes/api_spec.rb +15 -0
  34. data/spec/dsel/dsl/nodes/direct/environment_spec.rb +14 -0
  35. data/spec/dsel/dsl/nodes/direct_spec.rb +43 -0
  36. data/spec/dsel/dsl/nodes/proxy/environment_spec.rb +56 -0
  37. data/spec/dsel/dsl/nodes/proxy_spec.rb +11 -0
  38. data/spec/spec_helper.rb +22 -0
  39. data/spec/support/factories/clean_api_spec.rb +6 -0
  40. data/spec/support/fixtures/mock_api.rb +4 -0
  41. data/spec/support/helpers/paths.rb +19 -0
  42. data/spec/support/lib/factory.rb +107 -0
  43. data/spec/support/shared/dsl/nodes/base/environment.rb +104 -0
  44. data/spec/support/shared/dsl/nodes/base.rb +171 -0
  45. data/spec/support/shared/node.rb +70 -0
  46. metadata +108 -0
@@ -0,0 +1,241 @@
1
+ module DSeL
2
+ module API
3
+
4
+ class Node < Node
5
+
6
+ def self.inherited( base )
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def run( *args, &block )
12
+ DSL::Nodes::API.new( new ).run( *args, &block )
13
+ end
14
+
15
+ def define( *types )
16
+ if types.size > 1
17
+ if has_options?
18
+ fail ArgumentError,
19
+ "Cannot set options for multiple types: #{types}"
20
+ end
21
+
22
+ if has_description?
23
+ fail ArgumentError,
24
+ "Cannot set description for multiple types: #{types}"
25
+ end
26
+ end
27
+
28
+ Generator.define_definers( self, *types )
29
+ end
30
+
31
+ def has_options?
32
+ !!@last_options
33
+ end
34
+
35
+ def has_description?
36
+ !!@last_description
37
+ end
38
+
39
+ def describe( description )
40
+ @last_description = description
41
+ end
42
+
43
+ def configure( options, &block )
44
+ @last_options = [options, block].compact
45
+ end
46
+
47
+ def definers
48
+ @definers ||= []
49
+ end
50
+
51
+ def has_call_handler?( type, *possible_object )
52
+ method_defined? Generator.call_handler_name( type, *possible_object )
53
+ end
54
+
55
+ def call_handlers
56
+ @call_handlers ||= []
57
+ end
58
+
59
+ def root?
60
+ !parent
61
+ end
62
+
63
+ def child?
64
+ !root?
65
+ end
66
+
67
+ def root
68
+ return self if root?
69
+
70
+ @root ||= begin
71
+ p = @parent
72
+ while p.parent
73
+ p = p.parent
74
+ end
75
+ p
76
+ end
77
+ end
78
+
79
+ def parent
80
+ @parent
81
+ end
82
+
83
+ def push_children( c )
84
+ c.each do |name, (klass, *args)|
85
+ push_child( name, klass, *args )
86
+ end
87
+
88
+ nil
89
+ end
90
+
91
+ def push_child( name, node, s = nil )
92
+ if s && !s.is_a?( Symbol ) && !s.respond_to?( :call )
93
+ fail ArgumentError, 'Subject not Symbol nor responds to #call.'
94
+ end
95
+
96
+ node.set_parent( self )
97
+
98
+ child = {
99
+ name: name.to_sym,
100
+ node: node
101
+ }
102
+
103
+ if (options = self.flush_options)
104
+ child.merge!( options: options )
105
+ end
106
+
107
+ if (description = self.flush_description)
108
+ child.merge!( description: description )
109
+ end
110
+
111
+ children[name] = child
112
+
113
+ define_method name do
114
+ ivar = "@#{name}"
115
+
116
+ v = instance_variable_get( ivar )
117
+ return v if v
118
+
119
+ sub = @subject
120
+ if s.is_a?( Symbol )
121
+ sub = sub.send( s )
122
+ end
123
+
124
+ if s.respond_to?( :call )
125
+ sub = s.call( sub )
126
+ end
127
+
128
+ instance_variable_set( ivar, node.new( sub, parent: self ) )
129
+ end
130
+
131
+ child
132
+ end
133
+
134
+ def children
135
+ @children ||= {}
136
+ end
137
+
138
+ def tree
139
+ root.branch
140
+ end
141
+
142
+ def branch
143
+ t = {
144
+ definers: self.definers,
145
+ call_handlers: self.call_handlers.map { |h| c = h.dup; c.delete( :method ); c },
146
+ children: {}
147
+ }
148
+
149
+ self.children.each do |name, child|
150
+ t[:children][name] = child.merge( child[:node].branch )
151
+ end
152
+
153
+ t
154
+ end
155
+
156
+ # @private
157
+ def flush_description
158
+ d = @last_description
159
+ @last_description = nil
160
+ d
161
+ end
162
+
163
+ # @private
164
+ def flush_options
165
+ o = @last_options
166
+ @last_options = nil
167
+ o
168
+ end
169
+
170
+ # @private
171
+ def push_call_handler( type, method, *possible_object )
172
+ handler = {
173
+ type: type.to_sym
174
+ }
175
+
176
+ if !possible_object.empty?
177
+ handler.merge!( object: possible_object.first )
178
+ end
179
+
180
+ if (options = self.flush_options)
181
+ handler.merge!( options: options )
182
+ end
183
+
184
+ if (description = self.flush_description)
185
+ handler.merge!( description: description )
186
+ end
187
+
188
+ handler.merge!(
189
+ method: method.to_sym
190
+ )
191
+
192
+ call_handlers << handler
193
+ handler
194
+ end
195
+
196
+ # @private
197
+ def set_parent( node )
198
+ fail if @parent
199
+ @parent = node
200
+ end
201
+
202
+ # @private
203
+ def push_definer( type, method )
204
+ definer = {
205
+ type: type.to_sym
206
+ }
207
+
208
+ if (options = self.flush_options)
209
+ definer.merge!( options: options )
210
+ end
211
+
212
+ if (description = self.flush_description)
213
+ definer.merge!( description: description )
214
+ end
215
+
216
+ definer.merge!(
217
+ method: method.to_sym
218
+ )
219
+
220
+ definers << definer
221
+ definer
222
+ end
223
+
224
+ def method_missing( m, *args, &block )
225
+ ms = m.to_s
226
+ if ms.start_with? 'def_'
227
+ to_define = ms.split( 'def_', 2 ).last
228
+ define to_define
229
+
230
+ send m, *args, &block
231
+ else
232
+ super( m, *args, &block )
233
+ end
234
+ end
235
+
236
+ end
237
+
238
+ end
239
+
240
+ end
241
+ end
data/lib/dsel/api.rb ADDED
@@ -0,0 +1,8 @@
1
+ module DSeL
2
+ module API
3
+
4
+ require_relative 'api/generator'
5
+ require_relative 'api/node'
6
+
7
+ end
8
+ end
@@ -0,0 +1,34 @@
1
+ module DSeL
2
+ module DSL
3
+ module Mixins
4
+ module Environment
5
+
6
+ module IvarExplorer
7
+
8
+ def method_missing( name, *args, &block )
9
+ first_letter = name[0...1]
10
+
11
+ if block && first_letter == first_letter.capitalize
12
+ ivar = "@#{name}".to_sym
13
+ return _dsel_node_for_ivar( ivar, &block )
14
+ end
15
+
16
+ super( name, *args, &block )
17
+ end
18
+
19
+ # @private
20
+ def _dsel_node_for_ivar( ivar, &block )
21
+ ivar = ivar.downcase
22
+ if !_dsel_node.subject.instance_variable_defined?( ivar )
23
+ fail ArgumentError, "Instance variable not defined: #{ivar}"
24
+ end
25
+
26
+ _dsel_node.node_for( _dsel_node.subject.instance_variable_get( ivar ) ).run( &block )
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ require_relative '../proxy/environment'
2
+
3
+ module DSeL
4
+ module DSL
5
+ module Nodes
6
+ class API
7
+
8
+ class Environment < Proxy::Environment
9
+
10
+ define_method "#{DSEL_NODE_ACCESSOR}=" do |node|
11
+ super( node )
12
+
13
+ if node
14
+ node.subject.class.children.keys.each do |name|
15
+ define_singleton_method name.capitalize do |&b|
16
+ node.node_for( send( name ) ).run( &b )
17
+ end
18
+ end
19
+ end
20
+
21
+ node
22
+ end
23
+
24
+ def also( *args, &block )
25
+ # TODO: Store #last_call on Node at the instance level,
26
+ # this global state can be interfered with by other DSLs.
27
+ last_call = DSeL::API::Generator.last_call
28
+ type = last_call[:type]
29
+
30
+ # Check to see if there is a handler that matches our possible object.
31
+ # If so, treat it as object.
32
+ # If not, use the last object and assume arguments.
33
+ if last_call.include?( :object ) &&
34
+ !_dsel_self.class.has_call_handler?( type, args.first )
35
+
36
+ args.unshift last_call[:object]
37
+ end
38
+
39
+ send( type, *args, &block )
40
+
41
+ self
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
1
+ module DSeL
2
+ module DSL
3
+ module Nodes
4
+
5
+ class API < Proxy
6
+ require_relative 'api/environment'
7
+
8
+ private
9
+
10
+ def prepare_environment
11
+ @environment ||= Environment.new
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ module DSeL
2
+ module DSL
3
+ module Nodes
4
+ class APIBuilder
5
+
6
+ module Environment
7
+ include Base::Environment
8
+
9
+ def import( file )
10
+ f = file.dup
11
+ f << '.rb' if !file.end_with?( '.rb' )
12
+
13
+ _dsel_import f
14
+ end
15
+
16
+ def import_many( glob )
17
+ Dir["#{glob}.rb"].each { |file| _dsel_import( file ) }
18
+ end
19
+
20
+ def import_relative( file )
21
+ f = _dsel_caller_dir
22
+ f << file
23
+ f << '.rb' if !file.end_with?( '.rb' )
24
+
25
+ _dsel_import f
26
+ end
27
+
28
+ def import_relative_many( glob )
29
+ Dir["#{_dsel_caller_dir}#{glob}.rb"].each { |file| _dsel_import( file ) }
30
+ end
31
+
32
+ def child( method_name, class_name, *args, &block )
33
+ node = _dsel_node.node_for( class_name )
34
+ node.run( &block )
35
+
36
+ _dsel_node.subject.push_child(
37
+ method_name,
38
+ node.subject,
39
+ *args
40
+ )
41
+ end
42
+
43
+ def _dsel_import( file )
44
+ _dsel_node.subject.instance_eval( IO.read( file ) )
45
+ end
46
+
47
+ def _dsel_caller_dir( offset = 1 )
48
+ File.dirname( caller[offset].split( ':', 2 ).first ) << '/'
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'direct'
2
+
3
+ module DSeL
4
+ module DSL
5
+ module Nodes
6
+
7
+ class APIBuilder < Nodes::Direct
8
+ require_relative 'api_builder/environment'
9
+
10
+ def self.build( *args, &block )
11
+ fail ArgumentError, 'Missing block.' if !block
12
+
13
+ node = new( *args )
14
+ node.run( &block )
15
+ node.subject
16
+ end
17
+
18
+ API_NODE = DSeL::API::Node
19
+
20
+ def initialize( node, options = {} )
21
+ @superclass = options[:superclass] || API_NODE
22
+ if !(@superclass <= API_NODE)
23
+ fail ArgumentError, "Superclass not subclass of #{API_NODE}."
24
+ end
25
+
26
+ if node.is_a?( Symbol )
27
+ namespace = options[:namespace] || Object
28
+
29
+ if namespace.constants.include?( node )
30
+ fail ArgumentError, "Node name taken: #{c.inspect}"
31
+ end
32
+
33
+ subject = namespace.const_set( node, Class.new( @superclass ) )
34
+
35
+ elsif node.is_a?( Class ) && node < DSeL::API::Node
36
+ subject = node
37
+
38
+ else
39
+ fail ArgumentError,
40
+ "Expected #{Symbol} or #{DSeL::API::Node}, got: #{node.inspect}"
41
+ end
42
+
43
+ super( subject, options )
44
+ end
45
+
46
+ # @private
47
+ def node_for( subject, options = {} )
48
+ super( subject, options.merge(
49
+ namespace: @subject,
50
+ superclass: @superclass
51
+ ))
52
+ end
53
+
54
+ def reset_methods
55
+ [
56
+ :instance_variables,
57
+ :method_missing
58
+ ]
59
+ end
60
+
61
+ def extend_env
62
+ [
63
+ Environment
64
+ ]
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module DSeL
2
+ module DSL
3
+ module Nodes
4
+ class Base
5
+
6
+ module Environment
7
+
8
+ DSEL_NODE_ACCESSOR = :_dsel_node
9
+ DSEL_NODE_IVAR = "@#{DSEL_NODE_ACCESSOR}".to_sym
10
+
11
+ # @private
12
+ attr_accessor DSEL_NODE_ACCESSOR
13
+
14
+ def instance_variables
15
+ super.tap { |ivars| ivars.delete DSEL_NODE_IVAR }
16
+ end
17
+
18
+ def _dsel_shared_variables
19
+ _dsel_node.shared_variables
20
+ end
21
+
22
+ def _dsel_self
23
+ _dsel_node.subject
24
+ end
25
+
26
+ def _dsel_variables
27
+ s = {}
28
+ instance_variables.each do |ivar|
29
+ s[ivar.to_s.sub( '@', '' ).to_sym] = instance_variable_get( ivar )
30
+ end
31
+ s.freeze
32
+ end
33
+
34
+ def Parent( &block )
35
+ fail 'Already root.' if _dsel_node.root?
36
+
37
+ _dsel_node.parent.run( &block )
38
+ end
39
+
40
+ def Root( &block )
41
+ fail 'Already root.' if _dsel_node.root?
42
+ _dsel_node.root.run( &block )
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,110 @@
1
+ module DSeL
2
+ module DSL
3
+ module Nodes
4
+
5
+ class Base < Node
6
+ require_relative 'base/environment'
7
+
8
+ # @return [Environment]
9
+ attr_reader :environment
10
+
11
+ def initialize(*)
12
+ super
13
+
14
+ @shared_variables = {}
15
+ @nodes = {}
16
+
17
+ cache_node( self )
18
+ end
19
+
20
+ # @private
21
+ def nodes
22
+ root? ? @nodes : @root.nodes
23
+ end
24
+
25
+ def shared_variables
26
+ root? ? @shared_variables : @root.shared_variables
27
+ end
28
+
29
+ # @private
30
+ def cache_node( node )
31
+ nodes[node.hash] ||= node
32
+ end
33
+
34
+ # @private
35
+ def node_for( subject, options = {} )
36
+ nodes[calc_node_hash( subject )] ||=
37
+ self.class.new( subject, options.merge( parent: self ) )
38
+ end
39
+
40
+ def run( script = nil, &block )
41
+ if script && block
42
+ fail ArgumentError, 'Cannot use both script and &block.'
43
+ end
44
+
45
+ begin
46
+ prepare
47
+
48
+ calling do
49
+ if block
50
+ return @environment.instance_eval( &block )
51
+ end
52
+
53
+ if script
54
+ @environment.instance_eval do
55
+ return eval( IO.read( script ) )
56
+ end
57
+ end
58
+ end
59
+ ensure
60
+ # Re-entry, don't touch anything.
61
+ return if calling?
62
+
63
+ # May not have been prepared yet.
64
+ return if !@environment.respond_to?( Environment::DSEL_NODE_ACCESSOR )
65
+
66
+ cleanup
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def prepare
73
+ prepare_environment
74
+ @environment._dsel_node = self
75
+ end
76
+
77
+ def cleanup
78
+ @environment._dsel_node = nil
79
+ cleanup_environment
80
+ end
81
+
82
+ # @abstract
83
+ def cleanup_environment
84
+ end
85
+
86
+ # @abstract
87
+ def prepare_environment
88
+ fail 'Not implemented.'
89
+ end
90
+
91
+ def calling( &block )
92
+ return block.call if @calling
93
+
94
+ @calling = true
95
+ begin
96
+ block.call
97
+ ensure
98
+ @calling = false
99
+ end
100
+ end
101
+
102
+ def calling?
103
+ @calling
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,14 @@
1
+ module DSeL
2
+ module DSL
3
+ module Nodes
4
+ class Direct
5
+
6
+ module Environment
7
+ include Base::Environment
8
+
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end