lazy_graph 0.1.6 → 0.2.0
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 +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
|