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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +15 -0
- data/LICENSE.md +23 -0
- data/README.md +113 -0
- data/Rakefile +6 -0
- data/dsel.gemspec +22 -0
- data/lib/dsel/api/generator.rb +285 -0
- data/lib/dsel/api/node.rb +241 -0
- data/lib/dsel/api.rb +8 -0
- data/lib/dsel/dsl/mixins/environment/ivar_explorer.rb +34 -0
- data/lib/dsel/dsl/nodes/api/environment.rb +49 -0
- data/lib/dsel/dsl/nodes/api.rb +18 -0
- data/lib/dsel/dsl/nodes/api_builder/environment.rb +56 -0
- data/lib/dsel/dsl/nodes/api_builder.rb +71 -0
- data/lib/dsel/dsl/nodes/base/environment.rb +50 -0
- data/lib/dsel/dsl/nodes/base.rb +110 -0
- data/lib/dsel/dsl/nodes/direct/environment.rb +14 -0
- data/lib/dsel/dsl/nodes/direct.rb +75 -0
- data/lib/dsel/dsl/nodes/proxy/environment.rb +41 -0
- data/lib/dsel/dsl/nodes/proxy.rb +20 -0
- data/lib/dsel/dsl.rb +10 -0
- data/lib/dsel/node.rb +42 -0
- data/lib/dsel/ruby/object.rb +53 -0
- data/lib/dsel/version.rb +3 -0
- data/lib/dsel.rb +10 -0
- data/spec/dsel/api/generator_spec.rb +402 -0
- data/spec/dsel/api/node_spec.rb +328 -0
- data/spec/dsel/dsel_spec.rb +63 -0
- data/spec/dsel/dsl/nodes/api/environment.rb +208 -0
- data/spec/dsel/dsl/nodes/api_builder/environment_spec.rb +91 -0
- data/spec/dsel/dsl/nodes/api_builder_spec.rb +148 -0
- data/spec/dsel/dsl/nodes/api_spec.rb +15 -0
- data/spec/dsel/dsl/nodes/direct/environment_spec.rb +14 -0
- data/spec/dsel/dsl/nodes/direct_spec.rb +43 -0
- data/spec/dsel/dsl/nodes/proxy/environment_spec.rb +56 -0
- data/spec/dsel/dsl/nodes/proxy_spec.rb +11 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/factories/clean_api_spec.rb +6 -0
- data/spec/support/fixtures/mock_api.rb +4 -0
- data/spec/support/helpers/paths.rb +19 -0
- data/spec/support/lib/factory.rb +107 -0
- data/spec/support/shared/dsl/nodes/base/environment.rb +104 -0
- data/spec/support/shared/dsl/nodes/base.rb +171 -0
- data/spec/support/shared/node.rb +70 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 07cd134eb19c94b82a1bb7330afe5a0c8a58767e53f42314eb782af6a189d8af
|
4
|
+
data.tar.gz: f368f83eb54221b038a5a3a25f5ba6e4f4791439c7ebe939c10942c142b76f91
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 76d12f31e125b3b643fd2218cbda3b45810ceee941462ae4081cd147f2b316bb1971d2e69e047b73753a3fa43e92cb8ac48b4fc4cdd3acf6cf08f8c4bcbca99c
|
7
|
+
data.tar.gz: 39ca85a0140581511aaa7ae093f2c47a852278dff6e022593b4e2ae715c2daef2c44be21f2bbffdef055743bd5e3611cc7a9dd7e3574b3bd269b27693def400f
|
data/CHANGELOG.md
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#License
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Copyright (c) 2018-2021 Tasos Laskos \<tasos.laskos@gmail.com\>.
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# DSeL
|
2
|
+
|
3
|
+
You can use DSeL (pronounced _Diesel_ -- best I could do) to:
|
4
|
+
|
5
|
+
* Safely interact with any object as if you were operating within it.
|
6
|
+
* No need to keep typing `obj.*` to work on `obj`.
|
7
|
+
* Unsafely re-open (almost) any object like you re-open a `Class` and mess about it there.
|
8
|
+
* Define clean APIs for 3rd party functionality via a DSL.
|
9
|
+
* Documentation can be generated dynamically using API tree data.
|
10
|
+
* Access APIs via a DSL.
|
11
|
+
* Allows for scripting etc.
|
12
|
+
* Create custom DSLs.
|
13
|
+
|
14
|
+
**Currently an unstable work in progress, touch at own risk.**
|
15
|
+
|
16
|
+
## Description
|
17
|
+
|
18
|
+
It is:
|
19
|
+
|
20
|
+
* A generic DSL (`DSeL::DSL::Nodes::Base`) supporting:
|
21
|
+
* Proxy (`DSeL::DSL::Nodes::Proxy`) environments for safe interaction with any given object.
|
22
|
+
* Method calls are forwarded.
|
23
|
+
* Internal state not affected nor accessible.
|
24
|
+
* Direct (`DSeL::DSL::Nodes::Direct`) environments,
|
25
|
+
where the DSL context is the actual object instance.
|
26
|
+
* Preserved contexts:
|
27
|
+
* Re-entrant.
|
28
|
+
* Re-usable.
|
29
|
+
* Per object instance.
|
30
|
+
* Shared variables across environment nodes.
|
31
|
+
* Tree environment structure, with traversal helpers:
|
32
|
+
* `Root`
|
33
|
+
* `Parent`
|
34
|
+
* An API specification framework (`DSeL::API::Node`).
|
35
|
+
* Specify call:
|
36
|
+
* Description
|
37
|
+
* Type
|
38
|
+
* Object/catch-call
|
39
|
+
* Arguments and &block.
|
40
|
+
* Handler
|
41
|
+
* Tree structure for sections.
|
42
|
+
* An API specification language (`DSeL::DSL::APIBuilder`).
|
43
|
+
* `import` external API spec files.
|
44
|
+
* Define and describe sections (children).
|
45
|
+
* An API runner.
|
46
|
+
* Providing a specialised DSL (`DSeL::DSL::Nodes::API`) for API nodes (`DSeL::API::Node`).
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
Add this line to your application's Gemfile:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
gem 'dsel', github: 'qadron/dsel'
|
54
|
+
```
|
55
|
+
|
56
|
+
And then execute:
|
57
|
+
|
58
|
+
$ bundle
|
59
|
+
|
60
|
+
## Examples
|
61
|
+
|
62
|
+
See: [examples/](https://github.com/qadron/dsel/tree/master/examples/)
|
63
|
+
|
64
|
+
### API
|
65
|
+
|
66
|
+
Include the API specification:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
require_relative 'examples/api/my_api'
|
70
|
+
```
|
71
|
+
|
72
|
+
Use via a DSL script:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
MyAPI.run 'examples/api/my_api_dsl.rb'
|
76
|
+
```
|
77
|
+
|
78
|
+
Use via a Ruby script:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'examples/api/my_api_ruby'
|
82
|
+
````
|
83
|
+
|
84
|
+
See: [examples/api/](https://github.com/qadron/dsel/tree/master/examples/api/)
|
85
|
+
|
86
|
+
### DSL
|
87
|
+
|
88
|
+
See: [examples/dsl/object.rb](https://github.com/qadron/dsel/tree/master/examples/dsl/object.rb)
|
89
|
+
|
90
|
+
#### Proxy
|
91
|
+
|
92
|
+
The safe way to get a DSL is to run the object inside a _Proxy_ context and
|
93
|
+
just proxy methods, thus allowing the user to interact with the object as if
|
94
|
+
operating within it semantically but without access to non-public methods or its
|
95
|
+
state.
|
96
|
+
|
97
|
+
See: [examples/dsl/proxy.rb](https://github.com/qadron/dsel/tree/master/examples/dsl/proxy.rb)
|
98
|
+
|
99
|
+
#### Direct
|
100
|
+
|
101
|
+
The direct way means not having the object inside the environment, but the
|
102
|
+
environment inside the object, thus allowing you to truly operate within it and
|
103
|
+
make a general mess of things or do some pretty cool stuff.
|
104
|
+
|
105
|
+
See: [examples/dsl/direct.rb](https://github.com/qadron/dsel/tree/master/examples/dsl/direct.rb)
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/qadron/dsel.
|
110
|
+
|
111
|
+
## License
|
112
|
+
|
113
|
+
Please see the `LICENSE.md` file.
|
data/Rakefile
ADDED
data/dsel.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dsel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'dsel'
|
8
|
+
s.version = DSeL::VERSION
|
9
|
+
s.email = 'tasos.laskos@gmail.com'
|
10
|
+
s.authors = [ 'Tasos Laskos' ]
|
11
|
+
s.licenses = ['MIT']
|
12
|
+
|
13
|
+
s.summary = %q{DSL/API generator and runner.}
|
14
|
+
s.homepage = 'https://github.com/qadron/dsel'
|
15
|
+
|
16
|
+
s.require_paths = ['lib']
|
17
|
+
s.files += Dir.glob( 'lib/**/**' )
|
18
|
+
s.files += %w(Gemfile Rakefile dsel.gemspec)
|
19
|
+
s.test_files = Dir.glob( 'spec/**/**' )
|
20
|
+
|
21
|
+
s.extra_rdoc_files = %w(README.md LICENSE.md CHANGELOG.md)
|
22
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module DSeL
|
5
|
+
module API
|
6
|
+
|
7
|
+
class Generator
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
# @return [Hash]
|
11
|
+
# Data on the last call made.
|
12
|
+
attr_reader :last_call
|
13
|
+
|
14
|
+
def last_call_with_caller?
|
15
|
+
@last_call_with_caller
|
16
|
+
end
|
17
|
+
|
18
|
+
def last_call_without_caller!
|
19
|
+
@last_call_with_caller = false
|
20
|
+
end
|
21
|
+
|
22
|
+
def last_call_with_caller!
|
23
|
+
@last_call_with_caller = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_call( &block )
|
27
|
+
fail ArgumentError, 'Missing &block' if !block
|
28
|
+
|
29
|
+
synchronize do
|
30
|
+
@on_call << block
|
31
|
+
end
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# @note Defaults to `Object#hash` and assumes case-insensitive strings.
|
37
|
+
#
|
38
|
+
# Sets a way to calculate unique routing hashes for call objects.
|
39
|
+
#
|
40
|
+
# @param [Symbol, #call] hasher
|
41
|
+
# * `Symbol`: Object method.
|
42
|
+
# * `#call`: To be passed each object and return an Integer hash.
|
43
|
+
def call_object_hasher=( hasher )
|
44
|
+
if hasher && !(hasher.is_a?( Symbol ) || hasher.respond_to?( :call ))
|
45
|
+
fail ArgumentError,
|
46
|
+
"Expected Symbol or #call-able hasher, got: #{hasher.inspect}"
|
47
|
+
end
|
48
|
+
|
49
|
+
@call_object_hasher = hasher
|
50
|
+
end
|
51
|
+
|
52
|
+
# @private
|
53
|
+
def initialize
|
54
|
+
reset
|
55
|
+
end
|
56
|
+
|
57
|
+
# @private
|
58
|
+
def reset
|
59
|
+
@mutex = Monitor.new
|
60
|
+
@on_call = []
|
61
|
+
|
62
|
+
@last_call_with_caller = false
|
63
|
+
@call_object_hasher = false
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
def define_definers( node, *types )
|
70
|
+
synchronize do
|
71
|
+
types.each do |type|
|
72
|
+
define_definer( node, type )
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
# @private
|
80
|
+
def define_call_handler( node, type, *possible_object, &block )
|
81
|
+
node.instance_eval do
|
82
|
+
handler = Generator.call_handler_name( type, *possible_object )
|
83
|
+
|
84
|
+
if method_defined?( handler )
|
85
|
+
fail ArgumentError,
|
86
|
+
'Call handler already exists: ' <<
|
87
|
+
Generator.handler_to_s( self, type, *possible_object )
|
88
|
+
end
|
89
|
+
|
90
|
+
# We can get the options from here and do stuff...I don't know.
|
91
|
+
push_call_handler( type, handler, *possible_object )
|
92
|
+
define_method handler, &block
|
93
|
+
end
|
94
|
+
|
95
|
+
define_call_router( node, type )
|
96
|
+
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# @private
|
101
|
+
def calling( node, type, handler, args, *possible_object, &block )
|
102
|
+
synchronize do
|
103
|
+
@last_call = {
|
104
|
+
node: node,
|
105
|
+
type: type
|
106
|
+
}
|
107
|
+
|
108
|
+
if !possible_object.empty?
|
109
|
+
@last_call.merge!( object: possible_object.first )
|
110
|
+
end
|
111
|
+
|
112
|
+
@last_call.merge!(
|
113
|
+
handler: handler,
|
114
|
+
args: args
|
115
|
+
)
|
116
|
+
|
117
|
+
if last_call_with_caller?
|
118
|
+
@last_call[:caller] = caller
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
t = Time.now
|
123
|
+
r = block.call
|
124
|
+
spent = Time.now - t
|
125
|
+
|
126
|
+
synchronize do
|
127
|
+
@last_call[:time] = spent
|
128
|
+
call_on_call( @last_call )
|
129
|
+
end
|
130
|
+
|
131
|
+
r
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
def definer_name( type )
|
136
|
+
"def_#{type}".to_sym
|
137
|
+
end
|
138
|
+
|
139
|
+
# @private
|
140
|
+
def call_handler_name( type, *possible_object )
|
141
|
+
possible_object.empty? ?
|
142
|
+
call_handler_catch_all_name( type ) :
|
143
|
+
call_handler_with_object_name( type, possible_object.first )
|
144
|
+
end
|
145
|
+
|
146
|
+
# @private
|
147
|
+
def call_handler_with_object_name( type, object )
|
148
|
+
"_#{type}_#{call_object_hash_for( object )}_#{token}".to_sym
|
149
|
+
end
|
150
|
+
|
151
|
+
# @private
|
152
|
+
def call_handler_catch_all_name( type )
|
153
|
+
"_#{type}_catch_all_#{token}".to_sym
|
154
|
+
end
|
155
|
+
|
156
|
+
# @private
|
157
|
+
def handler_to_s( node, type, *possible_object )
|
158
|
+
r = "#{node} #{type}"
|
159
|
+
|
160
|
+
if !possible_object.empty?
|
161
|
+
object = possible_object.first
|
162
|
+
r << ' '
|
163
|
+
r << (object.nil? ? 'nil' : object.to_s)
|
164
|
+
end
|
165
|
+
|
166
|
+
r
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def call_on_call( *args )
|
172
|
+
@on_call.each do |b|
|
173
|
+
b.call *args
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def call_object_hash_for( object )
|
178
|
+
if !@call_object_hasher
|
179
|
+
default_call_object_hash_for( object )
|
180
|
+
elsif @call_object_hasher.is_a?( Symbol )
|
181
|
+
object.send( @call_object_hasher )
|
182
|
+
else
|
183
|
+
@call_object_hasher.call( object )
|
184
|
+
end.tap do |h|
|
185
|
+
next if h.is_a? Integer
|
186
|
+
|
187
|
+
fail ArgumentError,
|
188
|
+
"Hasher #{@call_object_hasher.inspect} returned non-Integer" <<
|
189
|
+
" hash #{h.inspect} for object #{object.inspect}."
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def default_call_object_hash_for( object )
|
194
|
+
object = object.downcase if object.is_a?( String )
|
195
|
+
object.hash
|
196
|
+
end
|
197
|
+
|
198
|
+
def token
|
199
|
+
@token ||= SecureRandom.hex
|
200
|
+
end
|
201
|
+
|
202
|
+
def define_definer( node, type )
|
203
|
+
definer = Generator.definer_name( type )
|
204
|
+
|
205
|
+
# We can get the options from here and do stuff...I don't know.
|
206
|
+
node.push_definer( type, definer )
|
207
|
+
|
208
|
+
node.class_eval( "undef :#{definer} if defined? #{definer}" )
|
209
|
+
node.define_singleton_method definer do |*possible_object, &block|
|
210
|
+
if possible_object.size > 1
|
211
|
+
fail ArgumentError, 'No more than 1 objects are allowed.'
|
212
|
+
end
|
213
|
+
|
214
|
+
Generator.define_call_handler( self, type, *possible_object, &block )
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def define_call_router( node, type )
|
219
|
+
node.instance_eval do
|
220
|
+
return if method_defined?( type )
|
221
|
+
|
222
|
+
# Basically a router, object-based and catch-all.
|
223
|
+
define_method type do |*args, &block|
|
224
|
+
if !args.empty?
|
225
|
+
object = args.shift
|
226
|
+
with_object = Generator.call_handler_with_object_name( __method__, object )
|
227
|
+
|
228
|
+
if respond_to?( with_object )
|
229
|
+
r = nil
|
230
|
+
Generator.calling( self.class, type, with_object, args, object ) do
|
231
|
+
r = send( with_object, *args, &block )
|
232
|
+
end
|
233
|
+
|
234
|
+
return r.nil? ? self : (r == :nil ? nil :r)
|
235
|
+
end
|
236
|
+
|
237
|
+
args.unshift object
|
238
|
+
end
|
239
|
+
|
240
|
+
catch_all = Generator.call_handler_catch_all_name( __method__ )
|
241
|
+
if respond_to?( catch_all )
|
242
|
+
|
243
|
+
r = nil
|
244
|
+
Generator.calling( self.class, type, catch_all, args ) do
|
245
|
+
r = send( catch_all, *args, &block )
|
246
|
+
end
|
247
|
+
|
248
|
+
return r.nil? ? self : r
|
249
|
+
end
|
250
|
+
|
251
|
+
fail NoMethodError,
|
252
|
+
"No handler for: #{Generator.handler_to_s( self.class, type, *args )}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def synchronize( &block )
|
259
|
+
@mutex.synchronize( &block )
|
260
|
+
end
|
261
|
+
|
262
|
+
class <<self
|
263
|
+
|
264
|
+
def method_missing( sym, *args, &block )
|
265
|
+
if instance.respond_to?( sym )
|
266
|
+
instance.send( sym, *args, &block )
|
267
|
+
else
|
268
|
+
super( sym, *args, &block )
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def respond_to?( *args )
|
273
|
+
super || instance.respond_to?( *args )
|
274
|
+
end
|
275
|
+
|
276
|
+
# Ruby 2.0 doesn't like my class-level method_missing for some reason.
|
277
|
+
# @private
|
278
|
+
public :allocate
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
end
|