syntropy 0.3 → 0.4

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -2
  3. data/CHANGELOG.md +8 -0
  4. data/README.md +30 -11
  5. data/TODO.md +89 -0
  6. data/bin/syntropy +2 -2
  7. data/cmd/setup/template/site/.gitignore +57 -0
  8. data/cmd/setup/template/site/Dockerfile +32 -0
  9. data/cmd/setup/template/site/Gemfile +3 -0
  10. data/cmd/setup/template/site/README.md +0 -0
  11. data/cmd/setup/template/site/bin/console +0 -0
  12. data/cmd/setup/template/site/bin/restart +0 -0
  13. data/cmd/setup/template/site/bin/server +0 -0
  14. data/cmd/setup/template/site/bin/start +0 -0
  15. data/cmd/setup/template/site/bin/stop +0 -0
  16. data/cmd/setup/template/site/docker-compose.yml +51 -0
  17. data/cmd/setup/template/site/proxy/Dockerfile +5 -0
  18. data/cmd/setup/template/site/proxy/etc/Caddyfile +7 -0
  19. data/cmd/setup/template/site/proxy/etc/tls_auto +2 -0
  20. data/cmd/setup/template/site/proxy/etc/tls_cloudflare +4 -0
  21. data/cmd/setup/template/site/proxy/etc/tls_custom +1 -0
  22. data/cmd/setup/template/site/proxy/etc/tls_selfsigned +1 -0
  23. data/cmd/setup/template/site/site/_layout/default.rb +11 -0
  24. data/cmd/setup/template/site/site/about.md +6 -0
  25. data/cmd/setup/template/site/site/articles/cage.rb +29 -0
  26. data/cmd/setup/template/site/site/articles/index.rb +3 -0
  27. data/cmd/setup/template/site/site/assets/css/style.css +40 -0
  28. data/cmd/setup/template/site/site/assets/img/syntropy.png +0 -0
  29. data/cmd/setup/template/site/site/index.rb +15 -0
  30. data/docker-compose.yml +51 -0
  31. data/lib/syntropy/app.rb +93 -24
  32. data/lib/syntropy/errors.rb +16 -2
  33. data/lib/syntropy/module.rb +26 -5
  34. data/lib/syntropy/request_extensions.rb +96 -0
  35. data/lib/syntropy/rpc_api.rb +26 -9
  36. data/lib/syntropy/side_run.rb +46 -0
  37. data/lib/syntropy/version.rb +1 -1
  38. data/lib/syntropy.rb +14 -49
  39. data/syntropy.gemspec +1 -1
  40. data/test/app/baz.rb +3 -0
  41. data/test/test_app.rb +57 -7
  42. data/test/test_side_run.rb +43 -0
  43. data/test/test_validation.rb +1 -1
  44. metadata +31 -3
data/lib/syntropy/app.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'qeweney'
4
3
  require 'json'
4
+ require 'yaml'
5
+
6
+ require 'qeweney'
5
7
  require 'papercraft'
6
8
 
7
9
  require 'syntropy/errors'
@@ -23,10 +25,12 @@ module Syntropy
23
25
  @machine.spin do
24
26
  # we do startup stuff asynchronously, in order to first let TP2 do its
25
27
  # setup tasks
26
- @machine.sleep 0.25
28
+ @machine.sleep 0.15
27
29
  @opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
28
30
  start_file_watcher if opts[:watch_files]
29
31
  end
32
+
33
+ @module_loader ||= Syntropy::ModuleLoader.new(@src_path, @opts)
30
34
  end
31
35
 
32
36
  def find_route(path, cache: true)
@@ -34,15 +38,16 @@ module Syntropy
34
38
  return cached if cached
35
39
 
36
40
  entry = calculate_route(path)
37
- if entry[:kind] != :not_found
38
- @route_cache[path] = entry if cache
39
- end
41
+ @route_cache[path] = entry if entry[:kind] != :not_found && cache
40
42
  entry
41
43
  end
42
44
 
43
45
  def call(req)
44
46
  entry = find_route(req.path)
45
47
  render_entry(req, entry)
48
+ rescue Syntropy::Error => e
49
+ msg = e.message
50
+ req.respond(msg.empty? ? nil : msg, ':status' => e.http_status)
46
51
  rescue StandardError => e
47
52
  p e
48
53
  p e.backtrace
@@ -68,6 +73,8 @@ module Syntropy
68
73
  end
69
74
 
70
75
  def invalidate_cache(fn)
76
+ @module_loader.unload(fn)
77
+
71
78
  invalidated_keys = []
72
79
  @route_cache.each do |k, v|
73
80
  @opts[:logger]&.call("Invalidate cache for #{k}", nil)
@@ -79,7 +86,7 @@ module Syntropy
79
86
 
80
87
  def calculate_relative_path_re(mount_path)
81
88
  mount_path = '' if mount_path == '/'
82
- /^#{mount_path}(?:\/(.*))?$/
89
+ %r{^#{mount_path}(?:/(.*))?$}
83
90
  end
84
91
 
85
92
  FILE_KINDS = {
@@ -89,7 +96,7 @@ module Syntropy
89
96
  NOT_FOUND = { kind: :not_found }
90
97
 
91
98
  # We don't allow access to path with /.., or entries that start with _
92
- FORBIDDEN_RE = /(\/_)|((\/\.\.)\/?)/
99
+ FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
93
100
 
94
101
  def calculate_route(path)
95
102
  return NOT_FOUND if path =~ FORBIDDEN_RE
@@ -141,7 +148,7 @@ module Syntropy
141
148
  entry[:kind] == :module ? entry : NOT_FOUND
142
149
  end
143
150
 
144
- UP_TREE_PATH_RE = /^(.+)?\/[^\/]+$/
151
+ UP_TREE_PATH_RE = %r{^(.+)?/[^/]+$}
145
152
 
146
153
  def parent_path(path)
147
154
  m = path.match(UP_TREE_PATH_RE)
@@ -151,25 +158,47 @@ module Syntropy
151
158
  def render_entry(req, entry)
152
159
  case entry[:kind]
153
160
  when :not_found
154
- req.respond('Not found', ':status' => Qeweney::Status::NOT_FOUND)
161
+ respond_not_found(req, entry)
155
162
  when :static
156
163
  respond_static(req, entry)
157
164
  when :markdown
158
- body = render_markdown(IO.read(entry[:fn]))
159
- req.respond(body, 'Content-Type' => 'text/html')
165
+ respond_markdown(req, entry)
160
166
  when :module
161
- call_module(req, entry)
167
+ respond_module(req, entry)
162
168
  else
163
169
  raise 'Invalid entry kind'
164
170
  end
165
171
  end
166
172
 
173
+ def respond_not_found(req, _entry)
174
+ headers = { ':status' => Qeweney::Status::NOT_FOUND }
175
+ case req.method
176
+ when 'head'
177
+ req.respond(nil, headers)
178
+ else
179
+ req.respond('Not found', headers)
180
+ end
181
+ end
182
+
167
183
  def respond_static(req, entry)
168
184
  entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
169
- req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
185
+ headers = { 'Content-Type' => entry[:mime_type] }
186
+ req.respond_by_http_method(
187
+ 'head' => [nil, headers],
188
+ 'get' => -> { [IO.read(entry[:fn]), headers] }
189
+ )
190
+ end
191
+
192
+ def respond_markdown(req, entry)
193
+ entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
194
+ headers = { 'Content-Type' => entry[:mime_type] }
195
+ req.respond_by_http_method(
196
+ 'head' => [nil, headers],
197
+ 'get' => -> { [render_markdown(entry[:fn]), headers] }
198
+ )
170
199
  end
171
200
 
172
- def call_module(req, entry)
201
+ def respond_module(req, entry)
173
202
  entry[:code] ||= load_module(entry)
174
203
  if entry[:code] == :invalid
175
204
  req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -177,6 +206,8 @@ module Syntropy
177
206
  end
178
207
 
179
208
  entry[:code].call(req)
209
+ rescue Syntropy::Error => e
210
+ req.respond(nil, ':status' => e.http_status)
180
211
  rescue StandardError => e
181
212
  p e
182
213
  p e.backtrace
@@ -184,24 +215,62 @@ module Syntropy
184
215
  end
185
216
 
186
217
  def load_module(entry)
187
- loader = Syntropy::ModuleLoader.new(@src_path, @opts)
188
- ref = entry[:fn].gsub(%r{^#{@src_path}\/}, '').gsub(/\.rb$/, '')
189
- o = loader.load(ref)
190
- # klass = Class.new
191
- # o = klass.instance_eval(body, entry[:fn], 1)
192
-
193
- o.is_a?(Papercraft::HTML) ? wrap_template(o) : o
218
+ ref = entry[:fn].gsub(%r{^#{@src_path}/}, '').gsub(/\.rb$/, '')
219
+ o = @module_loader.load(ref)
220
+ o.is_a?(Papercraft::Template) ? wrap_template(o) : o
221
+ rescue Exception => e
222
+ @opts[:logger]&.call("Error while loading module #{ref}: #{e.message}")
223
+ :invalid
194
224
  end
195
225
 
196
226
  def wrap_template(templ)
197
- ->(req) {
227
+ lambda { |req|
198
228
  body = templ.render
199
229
  req.respond(body, 'Content-Type' => 'text/html')
200
230
  }
201
231
  end
202
232
 
203
- def render_markdown(str)
204
- Papercraft.markdown(str)
233
+ def render_markdown(fn)
234
+ atts, md = parse_markdown_file(fn)
235
+
236
+ if atts[:layout]
237
+ layout = @module_loader.load("_layout/#{atts[:layout]}")
238
+ html = layout.apply { emit_markdown(md) }.render
239
+ else
240
+ html = Papercraft.markdown(md)
241
+ end
242
+ html
243
+ end
244
+
245
+ DATE_REGEXP = /(\d{4}\-\d{2}\-\d{2})/
246
+ FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
247
+ YAML_OPTS = {
248
+ permitted_classes: [Date],
249
+ symbolize_names: true
250
+ }
251
+
252
+ # Parses the markdown file at the given path.
253
+ #
254
+ # @param path [String] file path
255
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
256
+ def parse_markdown_file(path)
257
+ content = IO.read(path) || ''
258
+ atts = {}
259
+
260
+ # Parse date from file name
261
+ if (m = path.match(DATE_REGEXP))
262
+ atts[:date] ||= Date.parse(m[1])
263
+ end
264
+
265
+ if (m = content.match(FRONT_MATTER_REGEXP))
266
+ front_matter = m[1]
267
+ content = m.post_match
268
+
269
+ yaml = YAML.safe_load(front_matter, **YAML_OPTS)
270
+ atts = atts.merge(yaml)
271
+ end
272
+
273
+ [atts, content]
205
274
  end
206
275
  end
207
276
  end
@@ -4,17 +4,31 @@ require 'qeweney'
4
4
 
5
5
  module Syntropy
6
6
  class Error < StandardError
7
+ Status = Qeweney::Status
8
+
7
9
  attr_reader :http_status
8
10
 
9
11
  def initialize(status, msg = '')
10
- @http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
11
12
  super(msg)
13
+ @http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
14
+ end
15
+
16
+ class << self
17
+ # Create class methods for common errors
18
+ {
19
+ not_found: Status::NOT_FOUND,
20
+ method_not_allowed: Status::METHOD_NOT_ALLOWED,
21
+ teapot: Status::TEAPOT
22
+ }
23
+ .each { |k, v|
24
+ define_method(k) { |msg = ''| new(v, msg) }
25
+ }
12
26
  end
13
27
  end
14
28
 
15
29
  class ValidationError < Error
16
30
  def initialize(msg)
17
- @http_status = Qeweney::Status::BAD_REQUEST
31
+ super(Qeweney::Status::BAD_REQUEST, msg)
18
32
  end
19
33
  end
20
34
  end
@@ -1,37 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'papercraft'
4
+
3
5
  module Syntropy
4
6
  class ModuleLoader
5
7
  def initialize(root, env)
6
8
  @root = root
7
9
  @env = env
8
- @loaded = {}
10
+ @loaded = {} # maps ref to code
11
+ @fn_map = {} # maps filename to ref
9
12
  end
10
13
 
11
14
  def load(ref)
12
15
  @loaded[ref] ||= load_module(ref)
13
16
  end
14
17
 
18
+ def unload(fn)
19
+ ref = @fn_map[fn]
20
+ return if !ref
21
+
22
+ @loaded.delete(ref)
23
+ @fn_map.delete(fn)
24
+ end
25
+
15
26
  private
16
27
 
17
28
  def load_module(ref)
18
- fn = File.join(@root, "#{ref}.rb")
19
- raise RuntimeError, "File not found #{fn}" if !File.file?(fn)
29
+ fn = File.expand_path(File.join(@root, "#{ref}.rb"))
30
+ @fn_map[fn] = ref
31
+ raise "File not found #{fn}" if !File.file?(fn)
20
32
 
21
33
  mod_body = IO.read(fn)
22
34
  mod_ctx = Class.new(Syntropy::Module)
23
35
  mod_ctx.loader = self
24
- # mod_ctx = .new(self, @env)
25
36
  mod_ctx.module_eval(mod_body, fn, 1)
26
37
 
27
38
  export_value = mod_ctx.__export_value__
28
39
 
40
+ wrap_module(mod_ctx, export_value)
41
+ end
42
+
43
+ def wrap_module(mod_ctx, export_value)
29
44
  case export_value
30
45
  when nil
31
- raise RuntimeError, 'No export found'
46
+ raise 'No export found'
32
47
  when Symbol
33
48
  # TODO: verify export_value denotes a valid method
34
49
  mod_ctx.new(@env)
50
+ when String
51
+ ->(req) { req.respond(export_value) }
35
52
  when Proc
36
53
  export_value
37
54
  else
@@ -57,6 +74,10 @@ module Syntropy
57
74
  @__export_value__ = ref
58
75
  end
59
76
 
77
+ def self.templ(&block)
78
+ Papercraft.html(&block)
79
+ end
80
+
60
81
  def self.__export_value__
61
82
  @__export_value__
62
83
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney'
4
+
5
+ module Syntropy
6
+ module RequestExtensions
7
+ def ctx
8
+ @ctx ||= {}
9
+ end
10
+
11
+ def validate_http_method(*accepted)
12
+ raise Syntropy::Error.method_not_allowed if !accepted.include?(method)
13
+ end
14
+
15
+ def respond_by_http_method(map)
16
+ value = map[self.method]
17
+ raise Syntropy::Error.method_not_allowed if !value
18
+
19
+ value = value.() if value.is_a?(Proc)
20
+ (body, headers) = value
21
+ respond(body, headers)
22
+ end
23
+
24
+ def respond_on_get(body, headers = {})
25
+ case self.method
26
+ when 'head'
27
+ respond(nil, headers)
28
+ when 'get'
29
+ respond(body, headers)
30
+ else
31
+ raise Syntropy::Error.method_not_allowed
32
+ end
33
+ end
34
+
35
+ def respond_on_post(body, headers = {})
36
+ case self.method
37
+ when 'head'
38
+ respond(nil, headers)
39
+ when 'post'
40
+ respond(body, headers)
41
+ else
42
+ raise Syntropy::Error.method_not_allowed
43
+ end
44
+ end
45
+
46
+ def validate_param(name, *clauses)
47
+ value = query[name]
48
+ clauses.each do |c|
49
+ valid = param_is_valid?(value, c)
50
+ raise(Syntropy::ValidationError, 'Validation error') if !valid
51
+
52
+ value = param_convert(value, c)
53
+ end
54
+ value
55
+ end
56
+
57
+ private
58
+
59
+ BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
60
+ BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
61
+ INTEGER_REGEXP = /^[+-]?[0-9]+$/
62
+ FLOAT_REGEXP = /^[+-]?[0-9]+(\.[0-9]+)?$/
63
+
64
+ def param_is_valid?(value, cond)
65
+ return cond.any? { |c| param_is_valid?(value, c) } if cond.is_a?(Array)
66
+
67
+ if value
68
+ if cond == :bool
69
+ return value =~ BOOL_REGEXP
70
+ elsif cond == Integer
71
+ return value =~ INTEGER_REGEXP
72
+ elsif cond == Float
73
+ return value =~ FLOAT_REGEXP
74
+ end
75
+ end
76
+
77
+ cond === value
78
+ end
79
+
80
+ def param_convert(value, klass)
81
+ if klass == :bool
82
+ value =~ BOOL_TRUE_REGEXP ? true : false
83
+ elsif klass == Integer
84
+ value.to_i
85
+ elsif klass == Float
86
+ value.to_f
87
+ elsif klass == Symbol
88
+ value.to_sym
89
+ else
90
+ value
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ Qeweney::Request.include(Syntropy::RequestExtensions)
@@ -11,7 +11,7 @@ module Syntropy
11
11
  end
12
12
 
13
13
  def call(req)
14
- response, status = invoke(req)
14
+ response, status = __invoke__(req)
15
15
  req.respond(
16
16
  response.to_json,
17
17
  ':status' => status,
@@ -19,15 +19,17 @@ module Syntropy
19
19
  )
20
20
  end
21
21
 
22
- def invoke(req)
23
- q = req.validate_param(:q, String)
22
+ private
23
+
24
+ def __invoke__(req)
25
+ q = req.validate_param(:q, String).to_sym
24
26
  response = case req.method
25
27
  when 'get'
26
- send(q.to_sym, req)
28
+ __invoke_get__(q, req)
27
29
  when 'post'
28
- send(:"#{q}!", req)
30
+ __invoke_post__(q, req)
29
31
  else
30
- raise Syntropy::Error.new(Qeweney::Status::METHOD_NOT_ALLOWED)
32
+ raise Syntropy::Error.method_not_allowed
31
33
  end
32
34
  [{ status: 'OK', response: response }, Qeweney::Status::OK]
33
35
  rescue => e
@@ -35,14 +37,29 @@ module Syntropy
35
37
  p e
36
38
  p e.backtrace
37
39
  end
38
- error_response(e)
40
+ __error_response__(e)
41
+ end
42
+
43
+ def __invoke_get__(sym, req)
44
+ return send(sym, req) if respond_to?(sym)
45
+
46
+ err = respond_to?(:"#{sym}!") ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
47
+ raise err
48
+ end
49
+
50
+ def __invoke_post__(sym, req)
51
+ sym_post = :"#{sym}!"
52
+ return send(sym_post, req) if respond_to?(sym_post)
53
+
54
+ err = respond_to?(sym) ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
55
+ raise err
39
56
  end
40
57
 
41
58
  INTERNAL_SERVER_ERROR = Qeweney::Status::INTERNAL_SERVER_ERROR
42
59
 
43
- def error_response(err)
60
+ def __error_response__(err)
44
61
  http_status = err.respond_to?(:http_status) ? err.http_status : INTERNAL_SERVER_ERROR
45
- error_name = err.class.name
62
+ error_name = err.class.name.split('::').last
46
63
  [{ status: error_name, message: err.message }, http_status]
47
64
  end
48
65
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module Syntropy
6
+ module SideRun
7
+ class << self
8
+ def call(machine, &block)
9
+ setup if !@queue
10
+
11
+ # TODO: share mailboxes, acquire them with e.g. with_mailbox { |mbox| ... }
12
+ mailbox = Thread.current[:fiber_mailbox] ||= UM::Queue.new
13
+ machine.push(@queue, [mailbox, block])
14
+ result = machine.shift(mailbox)
15
+ result.is_a?(Exception) ? (raise result) : result
16
+ end
17
+
18
+ def setup
19
+ @queue = UM::Queue.new
20
+ count = (Etc.nprocessors - 1).clamp(2..6)
21
+ @workers = count.times.map {
22
+ Thread.new { side_run_worker(@queue) }
23
+ }
24
+ end
25
+
26
+ def side_run_worker(queue)
27
+ machine = UM.new
28
+ loop { handle_request(machine, queue) }
29
+ rescue UM::Terminate
30
+ # # We can also add a timeout here
31
+ # t0 = Time.now
32
+ # while !queue.empty? && (Time.now - t0) < 10
33
+ # handle_request(machine, queue)
34
+ # end
35
+ end
36
+
37
+ def handle_request(machine, queue)
38
+ response_mailbox, closure = machine.shift(queue)
39
+ result = closure.call
40
+ machine.push(response_mailbox, result)
41
+ rescue Exception => e
42
+ machine.push(response_mailbox, e)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.3'
4
+ VERSION = '0.4'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -8,64 +8,29 @@ require 'syntropy/errors'
8
8
  require 'syntropy/connection_pool'
9
9
  require 'syntropy/module'
10
10
  require 'syntropy/rpc_api'
11
+ require 'syntropy/side_run'
11
12
  require 'syntropy/app'
13
+ require 'syntropy/request_extensions'
12
14
 
13
- class Qeweney::Request
14
- def ctx
15
- @ctx ||= {}
16
- end
17
-
18
- def validate_param(name, *clauses)
19
- value = query[name]
20
- clauses.each do |c|
21
- valid = param_is_valid?(value, c)
22
- raise(Syntropy::ValidationError, 'Validation error') if !valid
23
- value = param_convert(value, c)
24
- end
25
- value
26
- end
27
-
28
- private
29
-
30
- BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
31
- BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
32
- INTEGER_REGEXP = /^[\+\-]?[0-9]+$/
33
- FLOAT_REGEXP = /^[\+\-]?[0-9]+(\.[0-9]+)?$/
15
+ module Syntropy
16
+ Status = Qeweney::Status
34
17
 
35
- def param_is_valid?(value, cond)
36
- if cond == :bool
37
- return (value && value =~ BOOL_REGEXP)
38
- elsif cond == Integer
39
- return (value && value =~ INTEGER_REGEXP)
40
- elsif cond == Float
41
- return (value && value =~ FLOAT_REGEXP)
42
- elsif cond.is_a?(Array)
43
- return cond.any? { |c| param_is_valid?(value, c) }
44
- end
18
+ class << self
19
+ attr_accessor :machine
45
20
 
46
- cond === value
47
- end
21
+ def side_run(&block)
22
+ raise 'Syntropy.machine not set' if !@machine
48
23
 
49
- def param_convert(value, klass)
50
- if klass == :bool
51
- value = value =~ BOOL_TRUE_REGEXP ? true : false
52
- elsif klass == Integer
53
- value = value.to_i
54
- elsif klass == Float
55
- value = value.to_f
56
- else
57
- value
24
+ SideRun.call(@machine, &block)
58
25
  end
59
26
  end
60
- end
61
27
 
62
- module Syntropy
63
28
  def colorize(color_code)
64
29
  "\e[#{color_code}m#{self}\e[0m"
65
30
  end
66
31
 
67
32
  GREEN = "\e[32m"
68
- WHITE = "\e[0m"
33
+ CLEAR = "\e[0m"
69
34
  YELLOW = "\e[33m"
70
35
 
71
36
  BANNER = (
@@ -73,10 +38,10 @@ module Syntropy
73
38
  " #{GREEN}\n"\
74
39
  " #{GREEN} ooo\n"\
75
40
  " #{GREEN}ooooo\n"\
76
- " #{GREEN} ooo vvv #{WHITE}Syntropy - a web framework for Ruby\n"\
77
- " #{GREEN} o vvvvv #{WHITE}--------------------------------------\n"\
78
- " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{WHITE}https://github.com/noteflakes/syntropy\n"\
41
+ " #{GREEN} ooo vvv #{CLEAR}Syntropy - a web framework for Ruby\n"\
42
+ " #{GREEN} o vvvvv #{CLEAR}--------------------------------------\n"\
43
+ " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/noteflakes/syntropy\n"\
79
44
  " #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
80
- " #{YELLOW}++++++++++++\e[0m\n\n"
45
+ "#{YELLOW}+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\e[0m\n\n"
81
46
  )
82
47
  end
data/syntropy.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.add_dependency 'json', '2.12.2'
26
26
  s.add_dependency 'papercraft', '1.4'
27
27
  s.add_dependency 'qeweney', '0.21'
28
- s.add_dependency 'tp2', '0.12.3.1'
28
+ s.add_dependency 'tp2', '0.13.2'
29
29
  s.add_dependency 'uringmachine', '0.15'
30
30
 
31
31
  s.add_dependency 'listen', '3.9.0'
data/test/app/baz.rb ADDED
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond_on_get('foobar')
3
+ }