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.
@@ -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'