wabur 0.1.0d2 → 0.2.0d1
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|