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
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