wabur 0.1.0d2 → 0.2.0d1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+
2
+ require 'wab'
3
+
4
+ module WAB
5
+ module IO
6
+ end
7
+ end
8
+
9
+ require 'wab/io/shell'
10
+ require 'wab/io/engine'
11
+ require 'wab/io/call'
12
+
13
+
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. This
5
- # class is also the default Ruby version of the shell that can be used for
6
- # development and small systems.
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 view, model, and type_key.
10
- def initialize(view, model, type_key='kind')
11
- @view = view
12
- @model = model
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
- # Returns the view instance that can be used for pushing data to a view.
18
- def view()
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
@@ -1,5 +1,5 @@
1
1
 
2
2
  module WAB
3
3
  # Current version of the module.
4
- VERSION = '0.1.0d2'
4
+ VERSION = '0.2.0d1'
5
5
  end
data/lib/wab.rb CHANGED
@@ -3,8 +3,9 @@
3
3
  module WAB
4
4
  end
5
5
 
6
+ require 'wab/controller'
6
7
  require 'wab/data'
7
- require 'wab/model'
8
+ require 'wab/errors'
8
9
  require 'wab/shell'
9
10
  require 'wab/uuid'
10
11
  require 'wab/version'
data/test/data_test.rb CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
3
 
4
- $: << File.dirname(__FILE__)
5
- $: << File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), 'lib')
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(nil, nil) if $shell.nil?
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-05T15:04:33.123456789Z",
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
- $: << File.dirname(__FILE__)
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'