wabur 0.1.0d2 → 0.2.0d1
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 +4 -4
- data/README.md +19 -16
- data/lib/wab/controller.rb +155 -106
- data/lib/wab/data.rb +8 -0
- data/lib/wab/errors.rb +13 -0
- data/lib/wab/impl/data.rb +49 -18
- data/lib/wab/impl/shell.rb +1 -1
- data/lib/wab/io/call.rb +23 -0
- data/lib/wab/io/engine.rb +118 -0
- data/lib/wab/io/shell.rb +141 -0
- data/lib/wab/io.rb +13 -0
- data/lib/wab/shell.rb +93 -16
- data/lib/wab/version.rb +1 -1
- data/lib/wab.rb +2 -1
- data/test/data_test.rb +6 -7
- data/test/impl_test.rb +1 -4
- data/test/ioshell_test.rb +461 -0
- data/test/tests.rb +12 -0
- metadata +13 -4
@@ -0,0 +1,118 @@
|
|
1
|
+
|
2
|
+
require 'wab'
|
3
|
+
|
4
|
+
module WAB
|
5
|
+
|
6
|
+
module IO
|
7
|
+
|
8
|
+
class Engine
|
9
|
+
|
10
|
+
# Starts the engine by creating a listener on STDIN. Processing threads
|
11
|
+
# are also created to handle the processing of requests.
|
12
|
+
#
|
13
|
+
# tcnt:: processing thread count
|
14
|
+
def initialize(shell, tcnt)
|
15
|
+
@shell = shell
|
16
|
+
@last_rid = 0
|
17
|
+
@pending = {}
|
18
|
+
@lock = Thread::Mutex.new()
|
19
|
+
@queue = Queue.new()
|
20
|
+
tcnt = 1 if 0 >= tcnt
|
21
|
+
@tcnt = tcnt
|
22
|
+
end
|
23
|
+
|
24
|
+
def start()
|
25
|
+
@tcnt.times {
|
26
|
+
Thread.new {
|
27
|
+
process_msg(@queue.pop)
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
# TBD create timeout thread, sync on lock to check timeout in pending, sleep for .5
|
32
|
+
|
33
|
+
Oj.strict_load($stdin, symbol_keys: true) { |msg|
|
34
|
+
api = msg[:api]
|
35
|
+
if 1 == api
|
36
|
+
@queue.push(msg)
|
37
|
+
elsif 4 == api
|
38
|
+
rid = msg[:rid]
|
39
|
+
call = nil
|
40
|
+
@lock.synchronize {
|
41
|
+
call = @pending.delete(rid)
|
42
|
+
}
|
43
|
+
unless call.nil?
|
44
|
+
call.result = msg[:body]
|
45
|
+
call.thread.run
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# TBD handle error
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Send request to the model portion of the system.
|
55
|
+
#
|
56
|
+
# tql:: the body of the message which should be JSON-TQL as a native Hash
|
57
|
+
def request(tql)
|
58
|
+
call = Call.new() # TBD make timeout seconds a parameter
|
59
|
+
@lock.synchronize {
|
60
|
+
@last_rid += 1
|
61
|
+
call.rid = @last_rid.to_s
|
62
|
+
@pending[call.rid] = call
|
63
|
+
}
|
64
|
+
data = @shell.data({ rid: call.rid, api: 3, body: tql }, true)
|
65
|
+
# Send the message. Make sure to flush to assure it gets sent.
|
66
|
+
$stdout.puts(data.json())
|
67
|
+
$stdout.flush()
|
68
|
+
|
69
|
+
# Wait for either the response to arrive or for a timeout. In both
|
70
|
+
# cases #run should be called on the thread.
|
71
|
+
Thread.stop
|
72
|
+
call.result
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_msg(native)
|
76
|
+
rid = native[:rid]
|
77
|
+
api = native[:api]
|
78
|
+
body = native[:body]
|
79
|
+
reply = @shell.data({rid: rid, api: 2})
|
80
|
+
if body.nil?
|
81
|
+
reply.set('body.code', -1)
|
82
|
+
reply.set('body.error', 'No body in request.')
|
83
|
+
else
|
84
|
+
data = @shell.data(body, false)
|
85
|
+
data.detect()
|
86
|
+
controller = @shell.controller(data)
|
87
|
+
if controller.nil?
|
88
|
+
reply.set('body.code', -1)
|
89
|
+
reply.set('body.error', 'No handler found.')
|
90
|
+
else
|
91
|
+
op = body[:op]
|
92
|
+
begin
|
93
|
+
if 'NEW' == op && controller.respond_to?(:create)
|
94
|
+
reply.set('body', controller.create(body[:path], body[:query], data.get(:content)))
|
95
|
+
elsif 'GET' == op && controller.respond_to?(:read)
|
96
|
+
reply.set('body', controller.read(body[:path], body[:query]))
|
97
|
+
elsif 'DEL' == op && controller.respond_to?(:delete)
|
98
|
+
reply.set('body', controller.delete(body[:path], body[:query]))
|
99
|
+
elsif 'MOD' == op && controller.respond_to?(:update)
|
100
|
+
# Also used for TQL queries
|
101
|
+
reply.set('body', controller.update(body[:path], body[:query], data.get(:content)))
|
102
|
+
else
|
103
|
+
reply.set('body', controller.handle(data))
|
104
|
+
end
|
105
|
+
rescue Exception => e
|
106
|
+
reply.set('body.code', -1)
|
107
|
+
reply.set('body.error', e.message)
|
108
|
+
reply.set('body.backtrace', e.backtrace)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
$stdout.puts(reply.json)
|
113
|
+
$stdout.flush
|
114
|
+
end
|
115
|
+
|
116
|
+
end # Engine
|
117
|
+
end # IO
|
118
|
+
end # WAB
|
data/lib/wab/io/shell.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
|
2
|
+
require 'time'
|
3
|
+
require 'wab'
|
4
|
+
|
5
|
+
module WAB
|
6
|
+
|
7
|
+
module IO
|
8
|
+
|
9
|
+
# A Shell that uses STDIN and STDOUT for all interactions with the View
|
10
|
+
# and Model. Since the View and Model APIs are asynchronous and Controller
|
11
|
+
# calls are synchronous for simplicity some effort is required to block
|
12
|
+
# where needed to achieve the difference in behavior.
|
13
|
+
class Shell < ::WAB::Shell
|
14
|
+
|
15
|
+
attr_reader :path_pos
|
16
|
+
attr_reader :type_key
|
17
|
+
|
18
|
+
# Sets up the shell with the designated number of processing threads and
|
19
|
+
# the type_key.
|
20
|
+
#
|
21
|
+
# tcnt:: processing thread count
|
22
|
+
# type_key:: key to use for the record type
|
23
|
+
def initialize(tcnt, type_key='kind', path_pos=0)
|
24
|
+
super(type_key, path_pos)
|
25
|
+
@engine = Engine.new(self, tcnt)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Starts listening and processing.
|
29
|
+
def start()
|
30
|
+
@engine.start()
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create and return a new data instance with the provided initial value.
|
34
|
+
# The value must be a Hash or Array. The members of the Hash or Array
|
35
|
+
# must be nil, boolean, String, Integer, Float, BigDecimal, Array, Hash,
|
36
|
+
# Time, URI::HTTP, or WAB::UUID. Keys to Hashes must be Symbols.
|
37
|
+
#
|
38
|
+
# If the repair flag is true then an attempt will be made to fix the
|
39
|
+
# value by replacing String keys with Symbols and calling to_h or to_s
|
40
|
+
# on unsupported Objects.
|
41
|
+
#
|
42
|
+
# value:: initial value
|
43
|
+
# repair:: flag indicating invalid value should be repaired if possible
|
44
|
+
def data(value={}, repair=false)
|
45
|
+
::WAB::Impl::Data.new(value, repair)
|
46
|
+
end
|
47
|
+
|
48
|
+
### View related methods.
|
49
|
+
|
50
|
+
# Push changed data to the view if it matches one of the subscription
|
51
|
+
# filters.
|
52
|
+
#
|
53
|
+
# data: Wab::Data to push to the view if subscribed
|
54
|
+
def changed(data)
|
55
|
+
raise NotImplementedError.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reply asynchronously to a view request.
|
59
|
+
#
|
60
|
+
# rid:: request identifier the reply is associated with
|
61
|
+
# data:: content of the reply to be sent to the view
|
62
|
+
def reply(rid, data)
|
63
|
+
raise NotImplementedError.new
|
64
|
+
end
|
65
|
+
|
66
|
+
### Model related methods.
|
67
|
+
|
68
|
+
# Returns a WAB::Data that matches the object reference or nil if there
|
69
|
+
# is no match.
|
70
|
+
#
|
71
|
+
# ref:: object reference
|
72
|
+
def get(ref)
|
73
|
+
tql = { where: ref.to_i, select: '$' }
|
74
|
+
result = @engine.request(tql)
|
75
|
+
if result.nil? || 0 != result[:code]
|
76
|
+
if result.nil?
|
77
|
+
raise ::WAB::Error.new("nil result get of #{ref}.")
|
78
|
+
else
|
79
|
+
raise ::WAB::Error.new("error on get of #{ref}. #{result[:error]}")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
result[:results]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Evaluates the JSON TQL query. The TQL should be native Ruby objects
|
86
|
+
# that correspond to the TQL JSON format but using Symbol keys instead
|
87
|
+
# of strings.
|
88
|
+
#
|
89
|
+
# If a +handler+ is provided the call is evaluated asynchronously and
|
90
|
+
# the handler is called with the result of the query. If a +handler+ is
|
91
|
+
# supplied the +tql+ must contain an +:rid+ element that is unique
|
92
|
+
# across all handlers.
|
93
|
+
#
|
94
|
+
# tql:: query to evaluate
|
95
|
+
# handler:: callback handler that implements the #on_result() method
|
96
|
+
def query(tql, handler=nil)
|
97
|
+
|
98
|
+
# TBD handle async, maybe just send and leave it at that
|
99
|
+
|
100
|
+
@engine.request(tql)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Subscribe to changes in stored data and push changes to the controller
|
104
|
+
# if it passes the supplied filter.
|
105
|
+
#
|
106
|
+
# The +controller#changed+ method is called when changes in data cause
|
107
|
+
# the associated object to pass the provided filter.
|
108
|
+
#
|
109
|
+
# controller:: the controller to notify of changed
|
110
|
+
# filter:: the filter to apply to the data. Syntax is that TQL uses for the FILTER clause.
|
111
|
+
def subscribe(controller, filter)
|
112
|
+
raise NotImplementedError.new
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def form_where_eq(key, value)
|
118
|
+
value_class = value.class
|
119
|
+
x = ['EQ', key.to_s]
|
120
|
+
if value.is_a?(String)
|
121
|
+
x << "'" + value
|
122
|
+
elsif Time == value_class
|
123
|
+
x << value.utc.iso8601(9)
|
124
|
+
elsif value.nil? ||
|
125
|
+
TrueClass == value_class ||
|
126
|
+
FalseClass == value_class ||
|
127
|
+
Integer == value_class ||
|
128
|
+
Float == value_class ||
|
129
|
+
String == value_class
|
130
|
+
x << value
|
131
|
+
elsif 2 == RbConfig::CONFIG['MAJOR'] && 4 > RbConfig::CONFIG['MINOR'] && Fixnum == value_class
|
132
|
+
x << value
|
133
|
+
else
|
134
|
+
x << value.to_s
|
135
|
+
end
|
136
|
+
x
|
137
|
+
end
|
138
|
+
|
139
|
+
end # Shell
|
140
|
+
end # IO
|
141
|
+
end # WAB
|
data/lib/wab/io.rb
ADDED
data/lib/wab/shell.rb
CHANGED
@@ -1,28 +1,34 @@
|
|
1
1
|
module WAB
|
2
2
|
|
3
3
|
# The Shell is a duck-typed class. Any shell alternative should implement
|
4
|
-
# all the methods defined in the class except the +initialize+ method.
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# all the methods defined in the class except the +initialize+ method. The
|
5
|
+
# Shell acts as the conduit between the View and Model portions of the MVC
|
6
|
+
# design pattern.
|
7
|
+
#
|
8
|
+
# As the View conduit the Shell usually makes calls to the controller. The
|
9
|
+
# exception to this control flow direction is when data changes and is
|
10
|
+
# pushed out to the view.
|
11
|
+
#
|
12
|
+
# As the Model, the Shell must respond to request to update the store using
|
13
|
+
# either the CRUD type operations that match the controller.
|
14
|
+
#
|
15
|
+
# Note that this class implementes some basic features related to controller
|
16
|
+
# management but those features can be implemented in other ways as long as
|
17
|
+
# the methods remain the same.
|
7
18
|
class Shell
|
8
19
|
|
9
|
-
# Sets up the shell with a
|
10
|
-
|
11
|
-
|
12
|
-
|
20
|
+
# Sets up the shell with a type_key and path position.
|
21
|
+
#
|
22
|
+
# type_key:: key for the type associated with a record
|
23
|
+
# path_pos:: position in a URL path that is the class or type
|
24
|
+
def initialize(type_key='kind', path_pos=0)
|
13
25
|
@controllers = {}
|
14
26
|
@type_key = type_key
|
27
|
+
@path_pos = path_pos
|
15
28
|
end
|
16
29
|
|
17
|
-
#
|
18
|
-
def
|
19
|
-
@view
|
20
|
-
end
|
21
|
-
|
22
|
-
# Returns the model instance that can be used to get and modify data in
|
23
|
-
# the data store.
|
24
|
-
def model()
|
25
|
-
@model
|
30
|
+
# Starts the shell.
|
31
|
+
def start()
|
26
32
|
end
|
27
33
|
|
28
34
|
# Returns the path where a data type is located. The default is 'kind'.
|
@@ -42,6 +48,22 @@ module WAB
|
|
42
48
|
@controllers[type] = controller
|
43
49
|
end
|
44
50
|
|
51
|
+
# Returns the controller associated with the type key found in the
|
52
|
+
# data. If a controller has not be registered under that key the default
|
53
|
+
# controller is returned if there is one.
|
54
|
+
#
|
55
|
+
# data:: data to extract the type from for lookup in the controllers
|
56
|
+
def controller(data)
|
57
|
+
path = data.get(:path)
|
58
|
+
if path.nil? || @path_pos <= path.length
|
59
|
+
content = data.get(:content)
|
60
|
+
if content.is_a?(::WAB::Data)
|
61
|
+
@controllers[content.get(@type_key)] || @controllers[nil]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@controllers[nil]
|
65
|
+
end
|
66
|
+
|
45
67
|
# Create and return a new data instance with the provided initial value.
|
46
68
|
# The value must be a Hash or Array. The members of the Hash or Array must
|
47
69
|
# be nil, boolean, String, Integer, Float, BigDecimal, Array, Hash, Time,
|
@@ -57,5 +79,60 @@ module WAB
|
|
57
79
|
raise NotImplementedError.new
|
58
80
|
end
|
59
81
|
|
82
|
+
### View related methods.
|
83
|
+
|
84
|
+
# Push changed data to the view if it matches one of the subscription
|
85
|
+
# filters.
|
86
|
+
#
|
87
|
+
# data: Wab::Data to push to the view if subscribed
|
88
|
+
def changed(data)
|
89
|
+
raise NotImplementedError.new
|
90
|
+
end
|
91
|
+
|
92
|
+
# Reply asynchronously to a view request.
|
93
|
+
#
|
94
|
+
# rid:: request identifier the reply is associated with
|
95
|
+
# data:: content of the reply to be sent to the view
|
96
|
+
def reply(rid, data)
|
97
|
+
raise NotImplementedError.new
|
98
|
+
end
|
99
|
+
|
100
|
+
### Model related methods.
|
101
|
+
|
102
|
+
# Returns a WAB::Data that matches the object reference or nil if there
|
103
|
+
# is no match.
|
104
|
+
#
|
105
|
+
# ref:: object reference
|
106
|
+
def get(ref)
|
107
|
+
raise NotImplementedError.new
|
108
|
+
end
|
109
|
+
|
110
|
+
# Evaluates the JSON TQL query. The TQL should be native Ruby objects
|
111
|
+
# that correspond to the TQL JSON format but using Symbol keys instead
|
112
|
+
# of strings.
|
113
|
+
#
|
114
|
+
# If a +handler+ is provided the call is evaluated asynchronously and
|
115
|
+
# the handler is called with the result of the query. If a +handler+ is
|
116
|
+
# supplied the +tql+ must contain an +:rid+ element that is unique
|
117
|
+
# across all handlers.
|
118
|
+
#
|
119
|
+
# tql:: query to evaluate
|
120
|
+
# handler:: callback handler that implements the #on_result() method
|
121
|
+
def query(tql, handler=nil)
|
122
|
+
raise NotImplementedError.new
|
123
|
+
end
|
124
|
+
|
125
|
+
# Subscribe to changes in stored data and push changes to the controller
|
126
|
+
# if it passes the supplied filter.
|
127
|
+
#
|
128
|
+
# The +controller#changed+ method is called when changes in data cause
|
129
|
+
# the associated object to pass the provided filter.
|
130
|
+
#
|
131
|
+
# controller:: the controller to notify of changed
|
132
|
+
# filter:: the filter to apply to the data. Syntax is that TQL uses for the FILTER clause.
|
133
|
+
def subscribe(controller, filter)
|
134
|
+
raise NotImplementedError.new
|
135
|
+
end
|
136
|
+
|
60
137
|
end # Shell
|
61
138
|
end # WAB
|
data/lib/wab/version.rb
CHANGED
data/lib/wab.rb
CHANGED
data/test/data_test.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
|
-
$: <<
|
5
|
-
$: << File.join(File.dirname(File.
|
6
|
-
$: << File.join(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__)))), 'oj', 'ext')
|
7
|
-
$: << File.join(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__)))), 'oj', 'lib')
|
4
|
+
$: << __dir__
|
5
|
+
$: << File.join(File.dirname(File.expand_path(__dir__)), 'lib')
|
8
6
|
|
9
7
|
require 'minitest'
|
10
8
|
require 'minitest/autorun'
|
@@ -12,7 +10,7 @@ require 'minitest/autorun'
|
|
12
10
|
require 'wab'
|
13
11
|
require 'wab/impl'
|
14
12
|
|
15
|
-
$shell = ::WAB::Impl::Shell.new(
|
13
|
+
$shell = ::WAB::Impl::Shell.new() if $shell.nil?
|
16
14
|
|
17
15
|
class DataTest < Minitest::Test
|
18
16
|
|
@@ -39,20 +37,21 @@ class DataTest < Minitest::Test
|
|
39
37
|
a: [],
|
40
38
|
h: {},
|
41
39
|
})
|
40
|
+
# downcase to match Ruby 2.3 and 2.4 which encode BigDecimal differently.
|
42
41
|
assert_equal(%|{
|
43
42
|
"boo":true,
|
44
43
|
"n":null,
|
45
44
|
"num":7,
|
46
45
|
"float":7.654,
|
47
46
|
"str":"a string",
|
48
|
-
"t":"2017-01-
|
47
|
+
"t":"2017-01-05t15:04:33.123456789z",
|
49
48
|
"big":0.6321e2,
|
50
49
|
"uri":"http://opo.technology/sample",
|
51
50
|
"uuid":"b0ca922d-372e-41f4-8fea-47d880188ba3",
|
52
51
|
"a":[],
|
53
52
|
"h":{}
|
54
53
|
}
|
55
|
-
|, d.json(2))
|
54
|
+
|, d.json(2).downcase)
|
56
55
|
end
|
57
56
|
|
58
57
|
def test_validate_keys
|
data/test/impl_test.rb
CHANGED
@@ -1,14 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# encoding: UTF-8
|
3
3
|
|
4
|
-
$: <<
|
5
|
-
$: << File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'lib')
|
4
|
+
$: << __dir__
|
6
5
|
|
7
6
|
require 'minitest'
|
8
7
|
require 'minitest/autorun'
|
9
8
|
|
10
9
|
require 'wab/impl'
|
11
10
|
|
12
|
-
$shell = ::WAB::Impl::Shell.new(nil, nil)
|
13
|
-
|
14
11
|
require 'data_test'
|