lazy_graph 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +282 -47
- data/Rakefile +12 -1
- data/examples/converter.rb +41 -0
- data/examples/performance_tests.rb +4 -4
- data/examples/shopping_cart_totals.rb +56 -0
- data/lib/lazy_graph/builder/dsl.rb +67 -29
- data/lib/lazy_graph/builder.rb +43 -23
- data/lib/lazy_graph/builder_group.rb +29 -18
- data/lib/lazy_graph/cli.rb +47 -0
- data/lib/lazy_graph/context.rb +10 -6
- data/lib/lazy_graph/environment.rb +6 -0
- data/lib/lazy_graph/graph.rb +5 -3
- data/lib/lazy_graph/hash_utils.rb +4 -1
- data/lib/lazy_graph/logger.rb +87 -0
- data/lib/lazy_graph/missing_value.rb +8 -0
- data/lib/lazy_graph/node/array_node.rb +3 -2
- data/lib/lazy_graph/node/derived_rules.rb +60 -18
- data/lib/lazy_graph/node/node_properties.rb +16 -3
- data/lib/lazy_graph/node/object_node.rb +9 -5
- data/lib/lazy_graph/node/symbol_hash.rb +15 -2
- data/lib/lazy_graph/node.rb +97 -39
- data/lib/lazy_graph/rack_app.rb +138 -0
- data/lib/lazy_graph/rack_server.rb +199 -0
- data/lib/lazy_graph/version.rb +1 -1
- data/lib/lazy_graph.rb +6 -8
- metadata +115 -11
- data/lib/lazy_graph/server.rb +0 -96
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
|
5
|
+
module LazyGraph
|
6
|
+
class RackApp
|
7
|
+
ALLOWED_VALUES_VALIDATE = [true, false, nil, 'input', 'context'].to_set.freeze
|
8
|
+
ALLOWED_VALUES_DEBUG = [true, false, nil].to_set.freeze
|
9
|
+
|
10
|
+
attr_reader :routes
|
11
|
+
|
12
|
+
def initialize(routes: {})
|
13
|
+
@routes = routes.transform_keys(&:to_sym).compare_by_identity
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
env[:X_REQUEST_TIME_START] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
18
|
+
request = Rack::Request.new(env)
|
19
|
+
|
20
|
+
return routes!(request) if (request.path == '/routes' || request.path == '/') && request.get?
|
21
|
+
return health!(request) if request.path == '/health' && request.get?
|
22
|
+
return not_found!(request) unless (graph_module = @routes[request.path.to_sym])
|
23
|
+
return success!(request, graph_module.usage) if request.get?
|
24
|
+
return not_found!("#{request.request_method} #{request.path}") unless request.post?
|
25
|
+
|
26
|
+
query_lazy_graph!(request, graph_module)
|
27
|
+
end
|
28
|
+
|
29
|
+
def query_lazy_graph!(request, graph_module)
|
30
|
+
body = begin
|
31
|
+
JSON.parse(request.body.read, symbolize_names: true)
|
32
|
+
rescue JSON::ParserError => e
|
33
|
+
return not_acceptable!(request, 'Invalid JSON', e.message)
|
34
|
+
end
|
35
|
+
context, modules, validate, debug, query = body.values_at(:context, :modules, :validate, :debug, :query)
|
36
|
+
unless context.is_a?(Hash) && !context.empty?
|
37
|
+
return not_acceptable!(request, "Invalid 'context' Parameter", 'Should be a non-empty object.')
|
38
|
+
end
|
39
|
+
|
40
|
+
unless (modules.is_a?(Hash) && !modules.empty?) || modules.is_a?(String) || (modules.is_a?(Array) && modules.all? do |m|
|
41
|
+
m.is_a?(String)
|
42
|
+
end)
|
43
|
+
return not_acceptable!(request, "Invalid 'modules' Parameter",
|
44
|
+
'Should be a string, string-array or non-empty object.')
|
45
|
+
end
|
46
|
+
|
47
|
+
modules = Array(modules).map { |m| [m, {}] }.to_h unless modules.is_a?(Hash)
|
48
|
+
|
49
|
+
unless ALLOWED_VALUES_VALIDATE.include?(validate)
|
50
|
+
return not_acceptable!(
|
51
|
+
request, "Invalid 'validate' Parameter", "Should be nil, bool, or one of 'input', 'context'"
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
unless ALLOWED_VALUES_DEBUG.include?(debug) || debug.is_a?(String)
|
56
|
+
return not_acceptable!(request, "Invalid 'debug' Parameter", 'Should be nil or bool')
|
57
|
+
end
|
58
|
+
|
59
|
+
debug = Regexp.new(Regexp.escape(debug)) if debug.is_a?(String) && debug != 'exceptions'
|
60
|
+
|
61
|
+
unless query.nil? || query.is_a?(String) || (query.is_a?(Array) && query.all? do |q|
|
62
|
+
q.is_a?(String)
|
63
|
+
end)
|
64
|
+
return not_acceptable!(request, "Invalid 'query' Parameter", 'Should be nil, array or string array')
|
65
|
+
end
|
66
|
+
|
67
|
+
begin
|
68
|
+
result = graph_module.eval!(
|
69
|
+
modules: modules,
|
70
|
+
context: context,
|
71
|
+
validate: validate.nil? ? true : validate,
|
72
|
+
debug: if !debug.nil?
|
73
|
+
debug
|
74
|
+
else
|
75
|
+
LazyGraph::Environment.development? ? 'exceptions' : debug
|
76
|
+
end,
|
77
|
+
query: query
|
78
|
+
)
|
79
|
+
return not_acceptable!(request, result[:message], result[:detail]) if result[:type] == :error
|
80
|
+
|
81
|
+
success!(request, result)
|
82
|
+
rescue AbortError, ValidationError => e
|
83
|
+
LazyGraph.logger.error(e.message)
|
84
|
+
LazyGraph.logger.error(e.backtrace.join("\n"))
|
85
|
+
error!(request, 400, 'Bad Request', e.message)
|
86
|
+
rescue StandardError => e
|
87
|
+
LazyGraph.logger.error(e.message)
|
88
|
+
LazyGraph.logger.error(e.backtrace.join("\n"))
|
89
|
+
|
90
|
+
error!(request, 500, 'Internal Server Error', e.message)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def routes!(request)
|
95
|
+
success!(request, @routes.keys.map { |k, _v| { route: k.to_s, methods: %i[GET POST] } })
|
96
|
+
end
|
97
|
+
|
98
|
+
def health!(request)
|
99
|
+
success!(request, { status: 'ok' })
|
100
|
+
end
|
101
|
+
|
102
|
+
def not_acceptable!(request, message, details = '')
|
103
|
+
error!(request, 406, message, details)
|
104
|
+
end
|
105
|
+
|
106
|
+
def not_found!(request, details = '')
|
107
|
+
error!(request, 404, 'Not Found', details)
|
108
|
+
end
|
109
|
+
|
110
|
+
def request_ms(request)
|
111
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - request.env[:X_REQUEST_TIME_START]) * 1000.0).round(3)
|
112
|
+
end
|
113
|
+
|
114
|
+
def success!(request, result, status: 200)
|
115
|
+
req_ms = request_ms(request)
|
116
|
+
|
117
|
+
LazyGraph.logger.info(
|
118
|
+
Logger.build_color_string do
|
119
|
+
"#{bold(request.request_method)}: #{dim(request.path)} => #{green(status)} #{light_gray("#{req_ms}ms")}"
|
120
|
+
end
|
121
|
+
)
|
122
|
+
|
123
|
+
[status, { 'content-type' => 'application/json' }, [JSON.fast_generate(result)]]
|
124
|
+
end
|
125
|
+
|
126
|
+
def error!(request, status, message, details = '')
|
127
|
+
req_ms = request_ms(request)
|
128
|
+
|
129
|
+
LazyGraph.logger.error(
|
130
|
+
Logger.build_color_string do
|
131
|
+
"#{bold(request.request_method)}: #{dim(request.path)} => #{status.to_i >= 500 ? red(status) : orange(status)} #{light_gray("#{req_ms}ms")}"
|
132
|
+
end
|
133
|
+
)
|
134
|
+
|
135
|
+
[status, { 'content-type' => 'application/json' }, [JSON.fast_generate({ 'error': message, 'details': details })]]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module LazyGraph
|
6
|
+
class RackServer
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
@running = false
|
10
|
+
@threads = [] #
|
11
|
+
@threads_m = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def start(port: 9292, workers: 4)
|
15
|
+
trap_signals
|
16
|
+
|
17
|
+
@port = port
|
18
|
+
@workers = workers
|
19
|
+
|
20
|
+
if workers > 1
|
21
|
+
puts "Starting Raxx server with #{workers} processes on port #{port}..."
|
22
|
+
@server = TCPServer.new(@port)
|
23
|
+
@server.listen(1024)
|
24
|
+
|
25
|
+
workers.times do
|
26
|
+
fork(&method(:run_accept_loop))
|
27
|
+
end
|
28
|
+
Process.waitall
|
29
|
+
else
|
30
|
+
puts "Starting single-process server on port #{port}..."
|
31
|
+
@server = TCPServer.new(@port)
|
32
|
+
@server.listen(1024)
|
33
|
+
run_accept_loop
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
#
|
40
|
+
# Main accept loop
|
41
|
+
#
|
42
|
+
def run_accept_loop
|
43
|
+
enable_reuse(@server)
|
44
|
+
@running = true
|
45
|
+
puts "[PID #{Process.pid}] Listening on port #{@port}..."
|
46
|
+
|
47
|
+
while @running
|
48
|
+
begin
|
49
|
+
client = @server.accept_nonblock if @running
|
50
|
+
rescue IO::WaitReadable, Errno::EINTR
|
51
|
+
IO.select([@server], nil, nil, 0.1) if @running
|
52
|
+
retry
|
53
|
+
end
|
54
|
+
|
55
|
+
# Handle connection in a new thread
|
56
|
+
next unless client
|
57
|
+
|
58
|
+
thr = Thread.start(client) do |socket|
|
59
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
60
|
+
handle_request(socket)
|
61
|
+
socket.close
|
62
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, IOError, Errno::EINVAL
|
63
|
+
# Connection reset by peer - ignore
|
64
|
+
ensure
|
65
|
+
remove_thread(Thread.current)
|
66
|
+
end
|
67
|
+
|
68
|
+
add_thread(thr)
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
if @server
|
72
|
+
puts "[PID #{Process.pid}] Shutting down server socket..."
|
73
|
+
@server.close
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Actually handle requests in a keep-alive loop
|
79
|
+
#
|
80
|
+
def handle_request(socket)
|
81
|
+
while @running
|
82
|
+
|
83
|
+
begin
|
84
|
+
request_line = socket.gets if @running
|
85
|
+
rescue IO::WaitReadable, Errno::EINTR, Errno::EPIPE
|
86
|
+
IO.select([socket], nil, nil, 0.5) if @running
|
87
|
+
retry
|
88
|
+
end
|
89
|
+
|
90
|
+
break if request_line.nil? || request_line.strip.empty?
|
91
|
+
|
92
|
+
method, path, http_version = request_line.split
|
93
|
+
|
94
|
+
# Parse headers
|
95
|
+
headers = {}
|
96
|
+
content_length = 0
|
97
|
+
while (line = socket.gets) && line != "\r\n"
|
98
|
+
key, value = line.split(': ', 2)
|
99
|
+
headers[key.downcase] = value.strip
|
100
|
+
content_length = value.to_i if key == 'Content-Length'
|
101
|
+
end
|
102
|
+
|
103
|
+
body = content_length.positive? ? socket.read(content_length) : (+'').force_encoding('ASCII-8BIT')
|
104
|
+
|
105
|
+
# Build Rack environment
|
106
|
+
env = {
|
107
|
+
'REQUEST_METHOD' => method,
|
108
|
+
'PATH_INFO' => path,
|
109
|
+
'HTTP_VERSION' => http_version,
|
110
|
+
'QUERY_STRING' => path[/\?.*/].to_s,
|
111
|
+
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
112
|
+
'SERVER_NAME' => 'localhost',
|
113
|
+
'rack.url_scheme' => 'http',
|
114
|
+
'rack.request.headers' => headers,
|
115
|
+
'rack.input' => StringIO.new(body),
|
116
|
+
'rack.errors' => StringIO.new('')
|
117
|
+
}
|
118
|
+
|
119
|
+
# Call the Rack app
|
120
|
+
status, response_headers, body_enum = @app.call(env)
|
121
|
+
|
122
|
+
content = body_enum.to_enum(:each).map(&:to_s).join
|
123
|
+
|
124
|
+
response_headers['Connection'] = 'keep-alive'
|
125
|
+
response_headers['Content-Length'] = content.bytesize.to_s
|
126
|
+
|
127
|
+
# Write the response
|
128
|
+
socket.print "HTTP/1.1 #{status} #{http_status_message(status)}\r\n"
|
129
|
+
response_headers.each { |k, v| socket.print "#{k}: #{v}\r\n" }
|
130
|
+
socket.print "\r\n"
|
131
|
+
socket.print content
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Thread management helpers
|
137
|
+
#
|
138
|
+
def add_thread(thr)
|
139
|
+
@threads_m.synchronize { @threads << thr }
|
140
|
+
end
|
141
|
+
|
142
|
+
def remove_thread(thr)
|
143
|
+
@threads_m.synchronize { @threads.delete(thr) }
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# Trap signals, set @running = false, then force-kill leftover threads
|
148
|
+
#
|
149
|
+
def trap_signals
|
150
|
+
%w[INT TERM].each do |signal|
|
151
|
+
trap(signal) do
|
152
|
+
@running = false
|
153
|
+
# Give threads a grace period to finish
|
154
|
+
graceful_shutdown_threads(timeout: 5)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
# Gracefully wait for each thread to finish; then force-kill if still alive
|
161
|
+
#
|
162
|
+
def graceful_shutdown_threads(timeout:)
|
163
|
+
@threads.each do |thr|
|
164
|
+
next unless thr.alive?
|
165
|
+
|
166
|
+
# Try a graceful join
|
167
|
+
thr.join(timeout)
|
168
|
+
# If still running after timeout, forcibly kill
|
169
|
+
if thr.alive?
|
170
|
+
puts "[PID #{Process.pid}] Force-killing thread #{thr.object_id}"
|
171
|
+
thr.kill
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
#
|
177
|
+
# Reuse port if supported
|
178
|
+
#
|
179
|
+
def enable_reuse(server)
|
180
|
+
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
181
|
+
begin
|
182
|
+
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
183
|
+
rescue StandardError
|
184
|
+
# Not supported on all systems
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# Utility: Convert a status code to reason phrase
|
191
|
+
#
|
192
|
+
def http_status_message(status)
|
193
|
+
{
|
194
|
+
200 => 'OK',
|
195
|
+
404 => 'Not Found'
|
196
|
+
}[status] || 'Unknown'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/lazy_graph/version.rb
CHANGED
data/lib/lazy_graph.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'logger'
|
4
|
-
|
5
3
|
# LazyGraph is a library which allows you to define a strictly typed hierarchical Graph structure.
|
6
4
|
# Within this graph you can annotate certain nodes as derived nodes, which compute their outputs based
|
7
5
|
# on input dependencies on other nodes in the graph structure (can be relative and absolute references).
|
@@ -12,11 +10,7 @@ require 'logger'
|
|
12
10
|
#
|
13
11
|
module LazyGraph
|
14
12
|
class AbortError < StandardError; end
|
15
|
-
class
|
16
|
-
attr_accessor :logger
|
17
|
-
end
|
18
|
-
|
19
|
-
self.logger = Logger.new($stdout)
|
13
|
+
class ValidationError < StandardError; end
|
20
14
|
end
|
21
15
|
|
22
16
|
require_relative 'lazy_graph/context'
|
@@ -29,4 +23,8 @@ require_relative 'lazy_graph/hash_utils'
|
|
29
23
|
require_relative 'lazy_graph/builder'
|
30
24
|
require_relative 'lazy_graph/builder_group'
|
31
25
|
require_relative 'lazy_graph/stack_pointer'
|
32
|
-
require_relative 'lazy_graph/
|
26
|
+
require_relative 'lazy_graph/cli'
|
27
|
+
require_relative 'lazy_graph/rack_server'
|
28
|
+
require_relative 'lazy_graph/rack_app'
|
29
|
+
require_relative 'lazy_graph/logger'
|
30
|
+
require_relative 'lazy_graph/environment'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lazy_graph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wouter Coppieters
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-01-07 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: json-schema
|
@@ -52,13 +52,13 @@ dependencies:
|
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
|
-
name:
|
55
|
+
name: benchmark-ips
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
|
-
type: :
|
61
|
+
type: :development
|
62
62
|
prerelease: false
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
@@ -66,7 +66,21 @@ dependencies:
|
|
66
66
|
- !ruby/object:Gem::Version
|
67
67
|
version: '0'
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
|
-
name:
|
69
|
+
name: debug
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: fiddle
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
71
85
|
requirements:
|
72
86
|
- - ">="
|
@@ -80,7 +94,7 @@ dependencies:
|
|
80
94
|
- !ruby/object:Gem::Version
|
81
95
|
version: '0'
|
82
96
|
- !ruby/object:Gem::Dependency
|
83
|
-
name:
|
97
|
+
name: listen
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
85
99
|
requirements:
|
86
100
|
- - ">="
|
@@ -94,7 +108,7 @@ dependencies:
|
|
94
108
|
- !ruby/object:Gem::Version
|
95
109
|
version: '0'
|
96
110
|
- !ruby/object:Gem::Dependency
|
97
|
-
name:
|
111
|
+
name: memory_profiler
|
98
112
|
requirement: !ruby/object:Gem::Requirement
|
99
113
|
requirements:
|
100
114
|
- - ">="
|
@@ -108,19 +122,61 @@ dependencies:
|
|
108
122
|
- !ruby/object:Gem::Version
|
109
123
|
version: '0'
|
110
124
|
- !ruby/object:Gem::Dependency
|
111
|
-
name:
|
125
|
+
name: observer
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: ostruct
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: rack
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: rake
|
112
168
|
requirement: !ruby/object:Gem::Requirement
|
113
169
|
requirements:
|
114
170
|
- - "~>"
|
115
171
|
- !ruby/object:Gem::Version
|
116
|
-
version: '
|
172
|
+
version: '13.0'
|
117
173
|
type: :development
|
118
174
|
prerelease: false
|
119
175
|
version_requirements: !ruby/object:Gem::Requirement
|
120
176
|
requirements:
|
121
177
|
- - "~>"
|
122
178
|
- !ruby/object:Gem::Version
|
123
|
-
version: '
|
179
|
+
version: '13.0'
|
124
180
|
- !ruby/object:Gem::Dependency
|
125
181
|
name: rdoc
|
126
182
|
requirement: !ruby/object:Gem::Requirement
|
@@ -135,6 +191,48 @@ dependencies:
|
|
135
191
|
- - ">="
|
136
192
|
- !ruby/object:Gem::Version
|
137
193
|
version: '0'
|
194
|
+
- !ruby/object:Gem::Dependency
|
195
|
+
name: rubocop
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
type: :development
|
202
|
+
prerelease: false
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
- !ruby/object:Gem::Dependency
|
209
|
+
name: solargraph
|
210
|
+
requirement: !ruby/object:Gem::Requirement
|
211
|
+
requirements:
|
212
|
+
- - ">="
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: '0'
|
215
|
+
type: :development
|
216
|
+
prerelease: false
|
217
|
+
version_requirements: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '0'
|
222
|
+
- !ruby/object:Gem::Dependency
|
223
|
+
name: vernier
|
224
|
+
requirement: !ruby/object:Gem::Requirement
|
225
|
+
requirements:
|
226
|
+
- - ">="
|
227
|
+
- !ruby/object:Gem::Version
|
228
|
+
version: '0'
|
229
|
+
type: :development
|
230
|
+
prerelease: false
|
231
|
+
version_requirements: !ruby/object:Gem::Requirement
|
232
|
+
requirements:
|
233
|
+
- - ">="
|
234
|
+
- !ruby/object:Gem::Version
|
235
|
+
version: '0'
|
138
236
|
description: JSON Driven, Stateless Rules Engine for JIT and efficient evaluation
|
139
237
|
of complex rules and computation graphs.
|
140
238
|
email:
|
@@ -148,15 +246,20 @@ files:
|
|
148
246
|
- LICENSE.txt
|
149
247
|
- README.md
|
150
248
|
- Rakefile
|
249
|
+
- examples/converter.rb
|
151
250
|
- examples/performance_tests.rb
|
251
|
+
- examples/shopping_cart_totals.rb
|
152
252
|
- lib/lazy_graph.rb
|
153
253
|
- lib/lazy_graph/builder.rb
|
154
254
|
- lib/lazy_graph/builder/dsl.rb
|
155
255
|
- lib/lazy_graph/builder_group.rb
|
256
|
+
- lib/lazy_graph/cli.rb
|
156
257
|
- lib/lazy_graph/context.rb
|
258
|
+
- lib/lazy_graph/environment.rb
|
157
259
|
- lib/lazy_graph/graph.rb
|
158
260
|
- lib/lazy_graph/hash_utils.rb
|
159
261
|
- lib/lazy_graph/lazy-graph.json
|
262
|
+
- lib/lazy_graph/logger.rb
|
160
263
|
- lib/lazy_graph/missing_value.rb
|
161
264
|
- lib/lazy_graph/node.rb
|
162
265
|
- lib/lazy_graph/node/array_node.rb
|
@@ -168,7 +271,8 @@ files:
|
|
168
271
|
- lib/lazy_graph/path_parser/path.rb
|
169
272
|
- lib/lazy_graph/path_parser/path_group.rb
|
170
273
|
- lib/lazy_graph/path_parser/path_part.rb
|
171
|
-
- lib/lazy_graph/
|
274
|
+
- lib/lazy_graph/rack_app.rb
|
275
|
+
- lib/lazy_graph/rack_server.rb
|
172
276
|
- lib/lazy_graph/stack_pointer.rb
|
173
277
|
- lib/lazy_graph/version.rb
|
174
278
|
- logo.png
|
data/lib/lazy_graph/server.rb
DELETED
@@ -1,96 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack'
|
4
|
-
|
5
|
-
module LazyGraph
|
6
|
-
class Server
|
7
|
-
ALLOWED_VALUES_VALIDATE = [true, false, nil, 'input', 'context'].to_set.freeze
|
8
|
-
ALLOWED_VALUES_DEBUG = [true, false, nil].to_set.freeze
|
9
|
-
|
10
|
-
attr_reader :routes
|
11
|
-
|
12
|
-
def initialize(routes: {})
|
13
|
-
@routes = routes.transform_keys(&:to_sym).compare_by_identity
|
14
|
-
end
|
15
|
-
|
16
|
-
def call(env)
|
17
|
-
# Rack environment contains request details
|
18
|
-
env[:X_REQUEST_TIME_START] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
19
|
-
request = Rack::Request.new(env)
|
20
|
-
|
21
|
-
unless (graph_module = @routes[request.path.to_sym])
|
22
|
-
return not_found!(request.path)
|
23
|
-
end
|
24
|
-
|
25
|
-
return success!(request, graph_module.usage) if request.get?
|
26
|
-
|
27
|
-
if request.post?
|
28
|
-
body = JSON.parse(request.body.read)
|
29
|
-
context, modules, validate, debug, query = body.values_at('context', 'modules', 'validate', 'debug', 'query')
|
30
|
-
unless context.is_a?(Hash) && !context.empty?
|
31
|
-
return not_acceptable!(request, "Invalid 'context' Parameter", 'Should be a non-empty object.')
|
32
|
-
end
|
33
|
-
|
34
|
-
unless modules.is_a?(Hash) && !modules.empty?
|
35
|
-
return not_acceptable!(request, "Invalid 'modules' Parameter", 'Should be a non-empty object.')
|
36
|
-
end
|
37
|
-
|
38
|
-
unless ALLOWED_VALUES_VALIDATE.include?(validate)
|
39
|
-
return not_acceptable!(
|
40
|
-
request, "Invalid 'validate' Parameter", "Should be nil, bool, or one of 'input', 'context'"
|
41
|
-
)
|
42
|
-
end
|
43
|
-
|
44
|
-
unless ALLOWED_VALUES_DEBUG.include?(debug)
|
45
|
-
return not_acceptable!(request, "Invalid 'debug' Parameter", 'Should be nil or bool')
|
46
|
-
end
|
47
|
-
|
48
|
-
unless query.nil? || query.is_a?(String) || (query.is_a?(Array) && query.all? do |q|
|
49
|
-
q.is_a?(String)
|
50
|
-
end)
|
51
|
-
return not_acceptable!(request, "Invalid 'query' Parameter", 'Should be nil, array or string array')
|
52
|
-
end
|
53
|
-
|
54
|
-
begin
|
55
|
-
result = graph_module.eval!(
|
56
|
-
modules: modules, context: context,
|
57
|
-
validate: validate, debug: debug,
|
58
|
-
query: query
|
59
|
-
)
|
60
|
-
return not_acceptable!(request, result[:message], result[:detail]) if result[:type] == :error
|
61
|
-
|
62
|
-
return success!(request, result)
|
63
|
-
rescue StandardError => e
|
64
|
-
LazyGraph.logger.error(e.message)
|
65
|
-
LazyGraph.logger.error(e.backtrace.join("\n"))
|
66
|
-
|
67
|
-
return error!(request, 500, 'Internal Server Error', e.message)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
not_found!("#{request.request_method} #{request.path}")
|
72
|
-
end
|
73
|
-
|
74
|
-
def not_acceptable!(request, message, details = '')
|
75
|
-
error!(request, 406, message, details)
|
76
|
-
end
|
77
|
-
|
78
|
-
def not_found!(request, details = '')
|
79
|
-
error!(request, 404, 'Not Found', details)
|
80
|
-
end
|
81
|
-
|
82
|
-
def request_ms(request)
|
83
|
-
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - request.env[:X_REQUEST_TIME_START]) * 1000.0).round(3)
|
84
|
-
end
|
85
|
-
|
86
|
-
def success!(request, result, status: 200)
|
87
|
-
LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status} #{request_ms(request)}ms")
|
88
|
-
[status, { 'Content-Type' => 'text/json' }, [result.to_json]]
|
89
|
-
end
|
90
|
-
|
91
|
-
def error!(request, status, message, details = '')
|
92
|
-
LazyGraph.logger.info("#{request.request_method}: #{request.path} => #{status} #{request_ms(request)}ms")
|
93
|
-
[status, { 'Content-Type' => 'text/json' }, [{ 'error': message, 'details': details }.to_json]]
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|