wabur 0.2.0d1 → 0.4.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 +40 -14
- data/bin/wabur +103 -0
- data/lib/wab/controller.rb +133 -90
- data/lib/wab/data.rb +14 -27
- data/lib/wab/errors.rb +14 -8
- data/lib/wab/impl/bool_expr.rb +25 -0
- data/lib/wab/impl/configuration.rb +166 -0
- data/lib/wab/impl/data.rb +155 -232
- data/lib/wab/impl/expr.rb +26 -0
- data/lib/wab/impl/expr_parser.rb +55 -0
- data/lib/wab/impl/exprs/and.rb +29 -0
- data/lib/wab/impl/exprs/between.rb +41 -0
- data/lib/wab/impl/exprs/eq.rb +28 -0
- data/lib/wab/impl/exprs/gt.rb +30 -0
- data/lib/wab/impl/exprs/gte.rb +30 -0
- data/lib/wab/impl/exprs/has.rb +26 -0
- data/lib/wab/impl/exprs/in.rb +28 -0
- data/lib/wab/impl/exprs/lt.rb +30 -0
- data/lib/wab/impl/exprs/lte.rb +30 -0
- data/lib/wab/impl/exprs/not.rb +27 -0
- data/lib/wab/impl/exprs/or.rb +29 -0
- data/lib/wab/impl/exprs/regex.rb +28 -0
- data/lib/wab/impl/handler.rb +95 -0
- data/lib/wab/impl/model.rb +197 -0
- data/lib/wab/impl/path_expr.rb +14 -0
- data/lib/wab/impl/shell.rb +92 -7
- data/lib/wab/impl/utils.rb +110 -0
- data/lib/wab/impl.rb +24 -0
- data/lib/wab/io/call.rb +4 -7
- data/lib/wab/io/engine.rb +128 -51
- data/lib/wab/io/shell.rb +61 -64
- data/lib/wab/io.rb +0 -2
- data/lib/wab/open_controller.rb +43 -0
- data/lib/wab/shell.rb +46 -61
- data/lib/wab/shell_logger.rb +13 -0
- data/lib/wab/utils.rb +36 -0
- data/lib/wab/uuid.rb +3 -6
- data/lib/wab/version.rb +2 -2
- data/lib/wab.rb +3 -0
- data/pages/Plan.md +20 -14
- data/test/bench_io_shell.rb +49 -0
- data/test/{impl_test.rb → helper.rb} +2 -4
- data/test/mirror_controller.rb +16 -0
- data/test/test_configuration.rb +38 -0
- data/test/test_data.rb +207 -0
- data/test/test_expr.rb +35 -0
- data/test/test_expr_and.rb +24 -0
- data/test/test_expr_between.rb +43 -0
- data/test/test_expr_eq.rb +24 -0
- data/test/test_expr_gt.rb +24 -0
- data/test/test_expr_gte.rb +24 -0
- data/test/test_expr_has.rb +19 -0
- data/test/test_expr_in.rb +24 -0
- data/test/test_expr_lt.rb +24 -0
- data/test/test_expr_lte.rb +24 -0
- data/test/test_expr_not.rb +22 -0
- data/test/test_expr_or.rb +24 -0
- data/test/test_expr_regex.rb +30 -0
- data/test/test_impl.rb +38 -0
- data/test/test_io_shell.rb +189 -0
- data/test/test_model.rb +31 -0
- data/test/test_runner.rb +177 -0
- data/test/tests.rb +3 -8
- metadata +91 -18
- data/lib/wab/model.rb +0 -136
- data/lib/wab/view.rb +0 -21
- data/test/data_test.rb +0 -253
- data/test/ioshell_test.rb +0 -461
@@ -0,0 +1,197 @@
|
|
1
|
+
|
2
|
+
require 'fileutils'
|
3
|
+
require 'oj'
|
4
|
+
|
5
|
+
module WAB
|
6
|
+
module Impl
|
7
|
+
|
8
|
+
# The Model class is used to store data when using the
|
9
|
+
# WAB::Impl::Shell. It is no intended for any other use. The *get* and
|
10
|
+
# *query* methods are the primary means of interacting with the model.
|
11
|
+
#
|
12
|
+
# The Model is simple in that it stores data in a Hash references by *ref*
|
13
|
+
# numbers. Data is stores in a directory as separate JSON files that are
|
14
|
+
# named as the *ref* number as a 16 character hexidecimal.
|
15
|
+
class Model
|
16
|
+
|
17
|
+
# Create a new Model using the designated directory as the store.
|
18
|
+
#
|
19
|
+
# dir:: directory to store data in
|
20
|
+
def initialize(dir)
|
21
|
+
@dir = dir.nil? ? nil : File.expand_path(dir)
|
22
|
+
@cnt = 0
|
23
|
+
@map = {}
|
24
|
+
@lock = Thread::Mutex.new
|
25
|
+
FileUtils.mkdir_p(@dir) unless @dir.nil? || Dir.exist?(@dir)
|
26
|
+
load_files unless @dir.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get a single record in the database. A WAB::Impl::Data object is
|
30
|
+
# returned if not nil.
|
31
|
+
#
|
32
|
+
# ref:: references number of the object to retrieve.
|
33
|
+
def get(ref)
|
34
|
+
@map[ref]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Execute a TQL query.
|
38
|
+
#
|
39
|
+
# _Note that the current implementation does not support nested data
|
40
|
+
# retrieval.
|
41
|
+
#
|
42
|
+
# tql:: query to execute
|
43
|
+
def query(tql)
|
44
|
+
rid = tql[:rid]
|
45
|
+
where = nil
|
46
|
+
filter = nil
|
47
|
+
if tql.has_key?(:where)
|
48
|
+
w = tql[:where]
|
49
|
+
where = w.is_a?(Array) ? ExprParser.parse(w) : w
|
50
|
+
end
|
51
|
+
filter = ExprParser.parse(tql[:filter]) if tql.has_key?(:filter)
|
52
|
+
|
53
|
+
if tql.has_key?(:insert)
|
54
|
+
insert(tql[:insert], rid, where, filter)
|
55
|
+
elsif tql.has_key?(:update)
|
56
|
+
update(tql[:update], rid, where, filter)
|
57
|
+
elsif tql.has_key?(:delete)
|
58
|
+
delete(tql[:delete], rid, where, filter)
|
59
|
+
else
|
60
|
+
select(tql[:select], rid, where, filter)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def insert(obj, rid, where, filter)
|
67
|
+
ref = nil
|
68
|
+
@lock.synchronize {
|
69
|
+
unless where.nil?
|
70
|
+
@map.each_value { |v|
|
71
|
+
if where.eval(v) && (filter.nil? || filter.eval(v))
|
72
|
+
result = { code: -1, error: 'Already exists.' }
|
73
|
+
result[:rid] = rid unless rid.nil?
|
74
|
+
return result
|
75
|
+
end
|
76
|
+
}
|
77
|
+
end
|
78
|
+
@cnt += 1
|
79
|
+
ref = @cnt
|
80
|
+
@map[ref] = Data.new(obj, true)
|
81
|
+
write_to_file(ref, obj)
|
82
|
+
}
|
83
|
+
result = { code: 0, ref: ref }
|
84
|
+
result[:rid] = rid unless rid.nil?
|
85
|
+
result
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_matches(format, ref, rid, obj)
|
89
|
+
if format.nil?
|
90
|
+
{ id: ref, data: obj.native }
|
91
|
+
else
|
92
|
+
format_obj(format, ref, rid, obj)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def select(format, rid, where, filter)
|
97
|
+
matches = []
|
98
|
+
@lock.synchronize {
|
99
|
+
if where.nil? && filter.nil?
|
100
|
+
@map.each { |ref,obj|
|
101
|
+
matches << extract_matches(format, ref, rid, obj)
|
102
|
+
}
|
103
|
+
else
|
104
|
+
@map.each { |ref,obj|
|
105
|
+
if where.eval(obj) && (filter.nil? || filter.eval(obj))
|
106
|
+
matches << extract_matches(format, ref, rid, obj)
|
107
|
+
end
|
108
|
+
}
|
109
|
+
end
|
110
|
+
}
|
111
|
+
result = { code: 0, results: matches }
|
112
|
+
result[:rid] = rid unless rid.nil?
|
113
|
+
result
|
114
|
+
end
|
115
|
+
|
116
|
+
def update(obj, _rid, where, _filter)
|
117
|
+
updated = []
|
118
|
+
@lock.synchronize {
|
119
|
+
if where.is_a?(Expr)
|
120
|
+
# TBD must be able to update portions of an object
|
121
|
+
else
|
122
|
+
# A reference.
|
123
|
+
@map[where] = Data.new(obj, true)
|
124
|
+
updated << where
|
125
|
+
write_to_file(where, obj)
|
126
|
+
end
|
127
|
+
}
|
128
|
+
{ code: 0, updated: updated }
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete(_del_opt, _rid, where, filter)
|
132
|
+
deleted = []
|
133
|
+
@lock.synchronize {
|
134
|
+
if where.is_a?(Expr)
|
135
|
+
@map.each { |ref,obj|
|
136
|
+
if where.eval(obj) && (filter.nil? || filter.eval(obj))
|
137
|
+
deleted << ref
|
138
|
+
@map.delete(ref)
|
139
|
+
end
|
140
|
+
}
|
141
|
+
# A reference.
|
142
|
+
elsif !@map.delete(where).nil?
|
143
|
+
deleted << where
|
144
|
+
remove_file(where)
|
145
|
+
end
|
146
|
+
}
|
147
|
+
{ code: 0, deleted: deleted }
|
148
|
+
end
|
149
|
+
|
150
|
+
def format_obj(format, ref, rid, obj)
|
151
|
+
case format
|
152
|
+
when Hash
|
153
|
+
native = {}
|
154
|
+
format.each { |k,v| native[k] = format_obj(v, ref, rid, obj) }
|
155
|
+
native
|
156
|
+
when Array
|
157
|
+
format.map { |v| format_obj(v, ref, rid, obj) }
|
158
|
+
when String
|
159
|
+
if '$ref' == format
|
160
|
+
ref
|
161
|
+
elsif '$rid' == format
|
162
|
+
rid
|
163
|
+
elsif '$' == format || '$root' == format
|
164
|
+
obj.native
|
165
|
+
elsif !format.empty? && format.start_with?("'")
|
166
|
+
format[1..-1]
|
167
|
+
else
|
168
|
+
obj.get(format)
|
169
|
+
end
|
170
|
+
else
|
171
|
+
format
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def load_files()
|
176
|
+
Dir.foreach(@dir) { |fn|
|
177
|
+
next if '.' == fn[0]
|
178
|
+
ref = fn[0..-6]
|
179
|
+
iref = ref.to_i(16)
|
180
|
+
@cnt = iref if @cnt < iref
|
181
|
+
@map[iref] = Data.new(Oj.load_file(File.join(@dir, fn), mode: :wab), true)
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
def write_to_file(ref, obj)
|
186
|
+
return if @dir.nil?
|
187
|
+
obj.native if obj.is_a?(WAB::Data)
|
188
|
+
File.open(File.join(@dir, "%016x.json" % ref), 'wb') { |f| f.write(Oj.dump(obj, mode: :wab, indent: 0)) }
|
189
|
+
end
|
190
|
+
|
191
|
+
def remove_file(ref)
|
192
|
+
File.delete(File.join(@dir, "%016x.json" % ref)) unless @dir.nil?
|
193
|
+
end
|
194
|
+
|
195
|
+
end # Model
|
196
|
+
end # Impl
|
197
|
+
end # WAB
|
data/lib/wab/impl/shell.rb
CHANGED
@@ -1,16 +1,101 @@
|
|
1
1
|
|
2
|
-
require 'wab'
|
2
|
+
require 'wab/impl/handler'
|
3
|
+
require 'wab/impl/model'
|
3
4
|
|
4
5
|
module WAB
|
5
|
-
|
6
6
|
module Impl
|
7
7
|
|
8
8
|
# The shell for reference Ruby implementation.
|
9
|
-
class Shell
|
9
|
+
class Shell
|
10
|
+
include WAB::ShellLogger
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
# Returns the path where a data type is located. The default is 'kind'.
|
14
|
+
attr_reader :type_key
|
15
|
+
attr_reader :path_pos
|
16
|
+
|
17
|
+
# Call the Model instance with these methods.
|
18
|
+
def_delegators :@model, :get, :query
|
19
|
+
|
20
|
+
# Sets up the shell with the supplied configuration data.
|
21
|
+
#
|
22
|
+
# config:: Configuration object
|
23
|
+
def initialize(config)
|
24
|
+
@pre_path = config[:path_prefix] || '/v1'
|
25
|
+
@path_pos = @pre_path.split('/').length - 1
|
26
|
+
base = config[:base] || '.'
|
27
|
+
@model = Model.new((config['store.dir'] || File.join(base, 'data')).gsub('$BASE', base))
|
28
|
+
@type_key = config[:type_key] || 'kind'
|
29
|
+
@logger = config[:logger]
|
30
|
+
@logger.level = config[:verbosity] unless @logger.nil?
|
31
|
+
@http_dir = (config['http.dir'] || File.join(base, 'pages')).gsub('$BASE', base)
|
32
|
+
@http_port = (config['http.port'] || 6363).to_i
|
33
|
+
@controllers = {}
|
34
|
+
|
35
|
+
requires = config[:require]
|
36
|
+
case requires
|
37
|
+
when Array
|
38
|
+
requires.each { |r| require r.strip }
|
39
|
+
when String
|
40
|
+
requires.split(',').each { |r| require r.strip }
|
41
|
+
end
|
42
|
+
|
43
|
+
if config[:handler].is_a?(Array)
|
44
|
+
config[:handler].each { |hh| register_controller(hh[:type], hh[:handler]) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start listening. This should be called after registering Controllers
|
49
|
+
# with the Shell.
|
50
|
+
def start()
|
51
|
+
server = WEBrick::HTTPServer.new(Port: @http_port, DocumentRoot: @http_dir)
|
52
|
+
server.mount(@pre_path, WAB::Impl::Handler, self)
|
53
|
+
|
54
|
+
trap 'INT' do server.shutdown end
|
55
|
+
server.start
|
56
|
+
end
|
10
57
|
|
11
|
-
#
|
12
|
-
|
13
|
-
|
58
|
+
# Register a controller for a named type.
|
59
|
+
#
|
60
|
+
# If a request is received for an unregistered type the default controller
|
61
|
+
# will be used. The default controller is registered with a +nil+ key.
|
62
|
+
#
|
63
|
+
# type:: type name
|
64
|
+
# controller:: Controller instance for handling requests for the
|
65
|
+
# identified +type+. This can be a Controller, a Controller
|
66
|
+
# class, or a Controller class name.
|
67
|
+
def register_controller(type, controller)
|
68
|
+
case controller
|
69
|
+
when String
|
70
|
+
controller = Object.const_get(controller).new(self)
|
71
|
+
when Class
|
72
|
+
controller = controller.new(self)
|
73
|
+
end
|
74
|
+
controller.shell = self
|
75
|
+
@controllers[type] = controller
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the controller associated with the type key found in the
|
79
|
+
# data. If a controller has not be registered under that key the default
|
80
|
+
# controller is returned if there is one.
|
81
|
+
#
|
82
|
+
# data:: data to extract the type from for lookup in the controllers
|
83
|
+
def controller(data)
|
84
|
+
path = data.get(:path)
|
85
|
+
path = path.native if path.is_a?(WAB::Data)
|
86
|
+
return path_controller(path) unless path.nil? || (path.length <= @path_pos)
|
87
|
+
|
88
|
+
content = data.get(:content)
|
89
|
+
return @controllers[content.get(@type_key)] || @controllers[nil] unless content.nil?
|
90
|
+
|
91
|
+
@controllers[nil]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the controller according to the type in the path.
|
95
|
+
#
|
96
|
+
# path: path Array such as from a URL
|
97
|
+
def path_controller(path)
|
98
|
+
@controllers[path[@path_pos]] || @controllers[nil]
|
14
99
|
end
|
15
100
|
|
16
101
|
# Create and return a new data instance with the provided initial value.
|
@@ -27,7 +112,7 @@ module WAB
|
|
27
112
|
def data(value={}, repair=false)
|
28
113
|
Data.new(value, repair)
|
29
114
|
end
|
30
|
-
|
115
|
+
|
31
116
|
end # Shell
|
32
117
|
end # Impl
|
33
118
|
end # WAB
|
@@ -0,0 +1,110 @@
|
|
1
|
+
|
2
|
+
module WAB
|
3
|
+
module Impl
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
# Convert a key to an +Integer+ or raise.
|
9
|
+
def key_to_int(key)
|
10
|
+
return key if key.is_a?(Integer)
|
11
|
+
|
12
|
+
key = key.to_s if key.is_a?(Symbol)
|
13
|
+
if key.is_a?(String)
|
14
|
+
i = key.to_i
|
15
|
+
return i if i.to_s == key
|
16
|
+
end
|
17
|
+
return key if WAB::Utils.pre_24_fixnum?(key)
|
18
|
+
|
19
|
+
raise WAB::Error, 'path key must be an integer for an Array.'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns either an +Integer+ or +nil+.
|
23
|
+
def attempt_key_to_int(key)
|
24
|
+
return key if key.is_a?(Integer)
|
25
|
+
|
26
|
+
key = key.to_s if key.is_a?(Symbol)
|
27
|
+
if key.is_a?(String)
|
28
|
+
i = key.to_i
|
29
|
+
return i if i.to_s == key
|
30
|
+
end
|
31
|
+
return key if WAB::Utils.pre_24_fixnum?(key)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gets the Data element or value identified by the path where the path
|
36
|
+
# elements are separated by the '.' character.
|
37
|
+
def get_node(root, path)
|
38
|
+
return root[path] if path.is_a?(Symbol)
|
39
|
+
|
40
|
+
path = path.to_s.split('.') unless path.is_a?(Array)
|
41
|
+
node = root
|
42
|
+
|
43
|
+
path.each { |key|
|
44
|
+
if node.is_a?(Hash)
|
45
|
+
node = node[key.to_sym]
|
46
|
+
elsif node.is_a?(Array)
|
47
|
+
i = key.to_i
|
48
|
+
return nil if i.zero? && key != i && key != i.to_s
|
49
|
+
node = node[i]
|
50
|
+
else
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
}
|
54
|
+
|
55
|
+
node
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sets the node value identified by the path where the path elements are
|
59
|
+
# separated by the '.' character.
|
60
|
+
def set_value(node, path, value)
|
61
|
+
path = path.to_s.split('.') unless path.is_a?(Array)
|
62
|
+
|
63
|
+
path[0..-2].each_index { |i|
|
64
|
+
key = path[i]
|
65
|
+
if node.is_a?(Hash)
|
66
|
+
key = key.to_sym
|
67
|
+
unless node.has_key?(key)
|
68
|
+
node[key] = attempt_key_to_int(path[i + 1]).nil? ? {} : []
|
69
|
+
end
|
70
|
+
node = node[key]
|
71
|
+
elsif node.is_a?(Array)
|
72
|
+
key = key_to_int(key)
|
73
|
+
if key < node.length && -node.length < key
|
74
|
+
node = node[key]
|
75
|
+
else
|
76
|
+
entry = attempt_key_to_int(path[i + 1]).nil? ? {} : []
|
77
|
+
if key < -node.length
|
78
|
+
node.unshift(entry)
|
79
|
+
else
|
80
|
+
node[key] = entry
|
81
|
+
end
|
82
|
+
node = entry
|
83
|
+
end
|
84
|
+
else
|
85
|
+
raise WAB::TypeError, "Can not set a member of an #{node.class}."
|
86
|
+
end
|
87
|
+
}
|
88
|
+
|
89
|
+
key = path[-1]
|
90
|
+
|
91
|
+
if node.is_a?(Hash)
|
92
|
+
node[key.to_sym] = value
|
93
|
+
elsif node.is_a?(Array)
|
94
|
+
key = key_to_int(key)
|
95
|
+
if key < -node.length
|
96
|
+
node.unshift(value)
|
97
|
+
else
|
98
|
+
node[key] = value
|
99
|
+
end
|
100
|
+
else
|
101
|
+
raise WAB::TypeError, "Can not set a member of an #{node.class}."
|
102
|
+
end
|
103
|
+
value
|
104
|
+
end
|
105
|
+
|
106
|
+
end # Singleton class
|
107
|
+
|
108
|
+
end # Utils
|
109
|
+
end # Impl
|
110
|
+
end # WAB
|
data/lib/wab/impl.rb
CHANGED
@@ -7,5 +7,29 @@ module WAB
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
require 'wab/impl/configuration'
|
10
11
|
require 'wab/impl/data'
|
12
|
+
require 'wab/impl/expr'
|
13
|
+
require 'wab/impl/path_expr'
|
14
|
+
require 'wab/impl/bool_expr'
|
11
15
|
require 'wab/impl/shell'
|
16
|
+
require 'wab/impl/utils'
|
17
|
+
|
18
|
+
# Require the concrete Expr subclasses so a mapping table can be created for
|
19
|
+
# the parser.
|
20
|
+
|
21
|
+
require 'wab/impl/exprs/between'
|
22
|
+
require 'wab/impl/exprs/eq'
|
23
|
+
require 'wab/impl/exprs/gt'
|
24
|
+
require 'wab/impl/exprs/gte'
|
25
|
+
require 'wab/impl/exprs/has'
|
26
|
+
require 'wab/impl/exprs/in'
|
27
|
+
require 'wab/impl/exprs/lt'
|
28
|
+
require 'wab/impl/exprs/lte'
|
29
|
+
require 'wab/impl/exprs/regex'
|
30
|
+
|
31
|
+
require 'wab/impl/exprs/and'
|
32
|
+
require 'wab/impl/exprs/or'
|
33
|
+
require 'wab/impl/exprs/not'
|
34
|
+
|
35
|
+
require 'wab/impl/expr_parser'
|
data/lib/wab/io/call.rb
CHANGED
@@ -1,21 +1,18 @@
|
|
1
1
|
|
2
|
-
require 'wab'
|
3
|
-
|
4
2
|
module WAB
|
5
|
-
|
6
3
|
module IO
|
7
4
|
|
8
5
|
class Call
|
9
6
|
|
10
|
-
attr_accessor :rid
|
11
7
|
attr_accessor :result
|
12
8
|
attr_accessor :thread
|
9
|
+
attr_accessor :rid
|
10
|
+
attr_accessor :giveup
|
13
11
|
|
14
12
|
def initialize(timeout=2.0)
|
15
|
-
@rid =
|
16
|
-
@result = nil
|
17
|
-
@thread = Thread.current
|
13
|
+
@rid = rid
|
18
14
|
@giveup = Time.now + timeout
|
15
|
+
@thread = Thread.current
|
19
16
|
end
|
20
17
|
|
21
18
|
end # Call
|