dsel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
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
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'awesome_print', require: 'ap'
5
+
6
+ group :docs do
7
+ gem 'yard'
8
+ gem 'redcarpet'
9
+ end
10
+
11
+ group :spec do
12
+ gem 'rspec'
13
+ end
14
+
15
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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