pakyow-core 0.11.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/{pakyow-core/README.md → README.md} +3 -4
  5. data/commands/pakyow +13 -0
  6. data/lib/pakyow/app/connection/behavior/session.rb +33 -0
  7. data/lib/pakyow/app/connection/behavior/values.rb +42 -0
  8. data/lib/pakyow/app/connection/behavior/verifier.rb +34 -0
  9. data/lib/pakyow/app/connection/session/abstract.rb +24 -0
  10. data/lib/pakyow/app/connection/session/cookie.rb +56 -0
  11. data/lib/pakyow/app/connection.rb +57 -0
  12. data/lib/pakyow/app.rb +219 -0
  13. data/lib/pakyow/behavior/aspects.rb +45 -0
  14. data/lib/pakyow/behavior/config.rb +72 -0
  15. data/lib/pakyow/behavior/endpoints.rb +41 -0
  16. data/lib/pakyow/behavior/frameworks.rb +45 -0
  17. data/lib/pakyow/behavior/helpers.rb +107 -0
  18. data/lib/pakyow/behavior/initializers.rb +19 -0
  19. data/lib/pakyow/behavior/isolating.rb +69 -0
  20. data/lib/pakyow/behavior/operations.rb +46 -0
  21. data/lib/pakyow/behavior/pipeline.rb +58 -0
  22. data/lib/pakyow/behavior/plugins.rb +148 -0
  23. data/lib/pakyow/behavior/rescuing.rb +51 -0
  24. data/lib/pakyow/behavior/restarting.rb +68 -0
  25. data/lib/pakyow/behavior/sessions.rb +34 -0
  26. data/lib/pakyow/behavior/verification.rb +47 -0
  27. data/lib/pakyow/cli.rb +199 -0
  28. data/lib/pakyow/connection/multipart_input.rb +24 -0
  29. data/lib/pakyow/connection/multipart_parser.rb +112 -0
  30. data/lib/pakyow/connection/params.rb +29 -0
  31. data/lib/pakyow/connection/query_parser.rb +174 -0
  32. data/lib/pakyow/connection/statuses.rb +119 -0
  33. data/lib/pakyow/connection.rb +568 -0
  34. data/lib/pakyow/endpoints.rb +94 -0
  35. data/lib/pakyow/environment/actions/dispatch.rb +37 -0
  36. data/lib/pakyow/environment/actions/input_parser.rb +21 -0
  37. data/lib/pakyow/environment/actions/logger.rb +33 -0
  38. data/lib/pakyow/environment/actions/normalizer.rb +73 -0
  39. data/lib/pakyow/environment/behavior/config.rb +166 -0
  40. data/lib/pakyow/environment/behavior/initializers.rb +21 -0
  41. data/lib/pakyow/environment/behavior/input_parsing.rb +54 -0
  42. data/lib/pakyow/environment/behavior/plugins.rb +23 -0
  43. data/lib/pakyow/environment/behavior/restarting.rb +54 -0
  44. data/lib/pakyow/environment/behavior/running.rb +129 -0
  45. data/lib/pakyow/environment/behavior/silencing.rb +23 -0
  46. data/lib/pakyow/environment/behavior/timezone.rb +21 -0
  47. data/lib/pakyow/environment/behavior/watching.rb +75 -0
  48. data/lib/pakyow/environment.rb +381 -0
  49. data/lib/pakyow/error.rb +273 -0
  50. data/lib/pakyow/errors.rb +97 -0
  51. data/lib/pakyow/framework.rb +39 -0
  52. data/lib/pakyow/generator.rb +138 -0
  53. data/lib/pakyow/generators/project/default/%dot%env.erb +1 -0
  54. data/lib/pakyow/generators/project/default/%dot%gitignore +11 -0
  55. data/lib/pakyow/generators/project/default/%dot%ruby-version.erb +1 -0
  56. data/lib/pakyow/generators/project/default/Gemfile.erb +28 -0
  57. data/lib/pakyow/generators/project/default/README.md +14 -0
  58. data/lib/pakyow/generators/project/default/config/application.rb.erb +17 -0
  59. data/lib/pakyow/generators/project/default/config/environment.rb +11 -0
  60. data/lib/pakyow/generators/project/default/config/initializers/application/keep +0 -0
  61. data/lib/pakyow/generators/project/default/config/initializers/environment/keep +0 -0
  62. data/lib/pakyow/generators/project/default/database/keep +0 -0
  63. data/lib/pakyow/generators/project/default/frontend/assets/images/keep +0 -0
  64. data/lib/pakyow/generators/project/default/frontend/assets/packs/keep +0 -0
  65. data/lib/pakyow/generators/project/default/frontend/assets/styles/keep +0 -0
  66. data/lib/pakyow/generators/project/default/frontend/includes/keep +0 -0
  67. data/lib/pakyow/generators/project/default/frontend/layouts/default.html.erb +19 -0
  68. data/lib/pakyow/generators/project/default/frontend/pages/keep +0 -0
  69. data/lib/pakyow/generators/project/default/public/favicon.ico +0 -0
  70. data/lib/pakyow/generators/project/default/public/robots.txt +1 -0
  71. data/lib/pakyow/generators/project.rb +29 -0
  72. data/lib/pakyow/helpers/app.rb +28 -0
  73. data/lib/pakyow/helpers/connection.rb +45 -0
  74. data/lib/pakyow/info.rb +26 -0
  75. data/lib/pakyow/integrations/bootsnap.rb +18 -0
  76. data/lib/pakyow/integrations/bundler/require.rb +7 -0
  77. data/lib/pakyow/integrations/bundler/setup.rb +7 -0
  78. data/lib/pakyow/integrations/dotenv.rb +9 -0
  79. data/lib/pakyow/loader.rb +31 -0
  80. data/lib/pakyow/logger/colorizer.rb +38 -0
  81. data/lib/pakyow/logger/destination.rb +17 -0
  82. data/lib/pakyow/logger/formatter.rb +22 -0
  83. data/lib/pakyow/logger/formatters/human.rb +101 -0
  84. data/lib/pakyow/logger/formatters/json.rb +99 -0
  85. data/lib/pakyow/logger/formatters/logfmt.rb +50 -0
  86. data/lib/pakyow/logger/multiplexed.rb +19 -0
  87. data/lib/pakyow/logger/thread_local.rb +29 -0
  88. data/lib/pakyow/logger/timekeeper.rb +64 -0
  89. data/lib/pakyow/logger.rb +125 -0
  90. data/lib/pakyow/operation.rb +71 -0
  91. data/lib/pakyow/plugin/helper_caller.rb +20 -0
  92. data/lib/pakyow/plugin/lookup.rb +33 -0
  93. data/lib/pakyow/plugin/state.rb +68 -0
  94. data/lib/pakyow/plugin.rb +367 -0
  95. data/lib/pakyow/process_manager.rb +53 -0
  96. data/lib/pakyow/processes/proxy.rb +67 -0
  97. data/lib/pakyow/processes/server.rb +32 -0
  98. data/lib/pakyow/rack/compatibility.rb +68 -0
  99. data/lib/pakyow/task.rb +315 -0
  100. data/lib/pakyow/tasks/boot.rake +21 -0
  101. data/lib/pakyow/tasks/create.rake +32 -0
  102. data/lib/pakyow/tasks/help.rake +17 -0
  103. data/lib/pakyow/tasks/info/endpoints.rake +72 -0
  104. data/lib/pakyow/tasks/info.rake +31 -0
  105. data/lib/pakyow/tasks/irb.rake +11 -0
  106. data/lib/pakyow/tasks/prelaunch.rake +37 -0
  107. data/lib/pakyow/types.rb +32 -0
  108. data/lib/pakyow/validations/acceptance.rb +37 -0
  109. data/lib/pakyow/validations/email.rb +28 -0
  110. data/lib/pakyow/validations/inline.rb +32 -0
  111. data/lib/pakyow/validations/length.rb +44 -0
  112. data/lib/pakyow/validations/presence.rb +41 -0
  113. data/lib/pakyow/validations.rb +8 -0
  114. data/lib/pakyow/validator.rb +81 -0
  115. data/lib/pakyow/verifier.rb +177 -0
  116. data/lib/pakyow.rb +5 -0
  117. metadata +267 -65
  118. data/pakyow-core/CHANGELOG.md +0 -128
  119. data/pakyow-core/LICENSE +0 -20
  120. data/pakyow-core/lib/pakyow/core/app.rb +0 -82
  121. data/pakyow-core/lib/pakyow/core/app_context.rb +0 -10
  122. data/pakyow-core/lib/pakyow/core/base.rb +0 -61
  123. data/pakyow-core/lib/pakyow/core/call_context.rb +0 -171
  124. data/pakyow-core/lib/pakyow/core/config/app.rb +0 -49
  125. data/pakyow-core/lib/pakyow/core/config/cookies.rb +0 -4
  126. data/pakyow-core/lib/pakyow/core/config/logger.rb +0 -34
  127. data/pakyow-core/lib/pakyow/core/config/reloader.rb +0 -10
  128. data/pakyow-core/lib/pakyow/core/config/server.rb +0 -10
  129. data/pakyow-core/lib/pakyow/core/config/session.rb +0 -41
  130. data/pakyow-core/lib/pakyow/core/config.rb +0 -95
  131. data/pakyow-core/lib/pakyow/core/errors.rb +0 -16
  132. data/pakyow-core/lib/pakyow/core/helpers/configuring.rb +0 -142
  133. data/pakyow-core/lib/pakyow/core/helpers/hooks.rb +0 -106
  134. data/pakyow-core/lib/pakyow/core/helpers/running.rb +0 -124
  135. data/pakyow-core/lib/pakyow/core/helpers.rb +0 -61
  136. data/pakyow-core/lib/pakyow/core/loader.rb +0 -32
  137. data/pakyow-core/lib/pakyow/core/middleware/logger.rb +0 -146
  138. data/pakyow-core/lib/pakyow/core/middleware/override.rb +0 -3
  139. data/pakyow-core/lib/pakyow/core/middleware/reloader.rb +0 -23
  140. data/pakyow-core/lib/pakyow/core/middleware/req_path_normalizer.rb +0 -49
  141. data/pakyow-core/lib/pakyow/core/middleware/session.rb +0 -5
  142. data/pakyow-core/lib/pakyow/core/middleware/static.rb +0 -76
  143. data/pakyow-core/lib/pakyow/core/multilog.rb +0 -19
  144. data/pakyow-core/lib/pakyow/core/request.rb +0 -119
  145. data/pakyow-core/lib/pakyow/core/response.rb +0 -135
  146. data/pakyow-core/lib/pakyow/core/route_eval.rb +0 -254
  147. data/pakyow-core/lib/pakyow/core/route_expansion_eval.rb +0 -115
  148. data/pakyow-core/lib/pakyow/core/route_lookup.rb +0 -41
  149. data/pakyow-core/lib/pakyow/core/route_merger.rb +0 -76
  150. data/pakyow-core/lib/pakyow/core/route_module.rb +0 -21
  151. data/pakyow-core/lib/pakyow/core/route_set.rb +0 -74
  152. data/pakyow-core/lib/pakyow/core/route_template_defaults.rb +0 -43
  153. data/pakyow-core/lib/pakyow/core/route_template_eval.rb +0 -72
  154. data/pakyow-core/lib/pakyow/core/router.rb +0 -181
  155. data/pakyow-core/lib/pakyow/core.rb +0 -8
  156. data/pakyow-core/lib/pakyow-core.rb +0 -1
@@ -0,0 +1,568 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "securerandom"
5
+
6
+ require "async/http"
7
+ require "async/http/protocol/response"
8
+
9
+ require "mini_mime"
10
+
11
+ require "pakyow/support/deep_dup"
12
+ require "pakyow/support/hookable"
13
+ require "pakyow/support/indifferentize"
14
+ require "pakyow/support/inspectable"
15
+ require "pakyow/support/pipeline/object"
16
+
17
+ require "pakyow/support/core_refinements/array/ensurable"
18
+
19
+ require "pakyow/logger"
20
+
21
+ module Pakyow
22
+ # Represents the connection throughout a request/response lifecycle.
23
+ #
24
+ class Connection
25
+ require "pakyow/connection/params"
26
+ require "pakyow/connection/query_parser"
27
+ require "pakyow/connection/statuses"
28
+
29
+ using Support::DeepDup
30
+ using Support::Indifferentize
31
+ using Support::Refinements::Array::Ensurable
32
+
33
+ include Support::Hookable
34
+ events :finalize
35
+
36
+ include Pakyow::Support::Inspectable
37
+ inspectable :method, :params, :cookies, :@status, :@body
38
+
39
+ include Support::Pipeline::Object
40
+
41
+ attr_reader :id, :timestamp, :logger, :status, :headers, :body
42
+
43
+ # Contains the error object when the connection is in a failed state.
44
+ #
45
+ attr_reader :error
46
+
47
+ # @api private
48
+ attr_writer :error, :input_parser
49
+ # @api private
50
+ attr_reader :request
51
+
52
+ Endpoint = Struct.new(:path, :params)
53
+
54
+ def initialize(request)
55
+ @id = SecureRandom.hex(4)
56
+ @timestamp = Time.now
57
+ @status = 200
58
+ @headers = {}
59
+ @request = request
60
+ @body = Async::HTTP::Body::Buffered.wrap(StringIO.new)
61
+ @params = Pakyow::Connection::Params.new
62
+ @logger = Logger.new(:http, started_at: @timestamp, id: @id, output: Pakyow.global_logger, level: Pakyow.config.logger.level)
63
+ @streams = []
64
+ end
65
+
66
+ def request_header?(key)
67
+ @request.headers.include?(normalize_header(key))
68
+ end
69
+
70
+ def request_header(key)
71
+ @request.headers[normalize_header(key)]
72
+ end
73
+
74
+ def header?(key)
75
+ @headers.key?(normalize_header(key))
76
+ end
77
+
78
+ def header(key)
79
+ @headers[normalize_header(key)]
80
+ end
81
+
82
+ def set_header(key, value)
83
+ @headers[normalize_header(key)] = value
84
+ end
85
+
86
+ def set_headers(headers)
87
+ headers.each do |key, value|
88
+ set_header(normalize_header(key), value)
89
+ end
90
+ end
91
+
92
+ def delete_header(key)
93
+ @headers.delete(normalize_header(key))
94
+ end
95
+
96
+ def input
97
+ @request.body
98
+ end
99
+
100
+ def parsed_input
101
+ unless instance_variable_defined?(:@parsed_input)
102
+ @parsed_input = nil; @parsed_input = parse_input
103
+ end
104
+
105
+ @parsed_input
106
+ end
107
+
108
+ # Returns the request method (e.g. `:get`).
109
+ #
110
+ def method
111
+ unless instance_variable_defined?(:@method)
112
+ @method = if request_method == "POST" && params.include?(:_method)
113
+ params[:_method].downcase.to_sym
114
+ else
115
+ request_method.downcase.to_sym
116
+ end
117
+ end
118
+
119
+ @method
120
+ end
121
+
122
+ def scheme
123
+ if request_header("https").to_s == "on" || request_header("x-forwarded-ssl").to_s == "on"
124
+ "https"
125
+ elsif value = request_header("x-forwarded-scheme")
126
+ value[0]
127
+ elsif value = request_header("x-forwarded-proto")
128
+ value[0]
129
+ else
130
+ @request.scheme
131
+ end
132
+ end
133
+
134
+ def authority
135
+ @request.authority
136
+ end
137
+
138
+ def host
139
+ unless instance_variable_defined?(:@host)
140
+ parse_authority
141
+ end
142
+
143
+ @host
144
+ end
145
+
146
+ def port
147
+ unless instance_variable_defined?(:@port)
148
+ parse_authority
149
+ end
150
+
151
+ @port
152
+ end
153
+
154
+ def subdomain
155
+ unless instance_variable_defined?(:@subdomain)
156
+ parse_subdomain
157
+ end
158
+
159
+ @subdomain
160
+ end
161
+
162
+ def path
163
+ unless instance_variable_defined?(:@path)
164
+ parse_path
165
+ end
166
+
167
+ @path
168
+ end
169
+
170
+ def query
171
+ unless instance_variable_defined?(:@query)
172
+ parse_path
173
+ end
174
+
175
+ @query
176
+ end
177
+
178
+ def fullpath
179
+ @request.path
180
+ end
181
+
182
+ def ip
183
+ request_header("x-forwarded-for").to_a.first || @request.remote_address.ip_address
184
+ end
185
+
186
+ def endpoint
187
+ unless instance_variable_defined?(:@endpoint)
188
+ @endpoint = Endpoint.new(path, params).freeze
189
+ end
190
+
191
+ @endpoint
192
+ end
193
+
194
+ # Returns the symbolized format of the request.
195
+ #
196
+ # @example
197
+ # request.format
198
+ # => :html
199
+ #
200
+ def format
201
+ unless instance_variable_defined?(:@format)
202
+ parse_format
203
+ end
204
+
205
+ @format
206
+ end
207
+
208
+ def type
209
+ unless instance_variable_defined?(:@type)
210
+ parse_content_type
211
+ end
212
+
213
+ @type
214
+ end
215
+ alias media_type type
216
+
217
+ def type_params
218
+ unless instance_variable_defined?(:@parsed_type_params)
219
+ @parsed_type_params = build_type_params
220
+ end
221
+
222
+ @parsed_type_params
223
+ end
224
+ alias media_type_params type_params
225
+
226
+ def secure?
227
+ scheme == "https"
228
+ end
229
+
230
+ # Returns an indifferentized params hash.
231
+ #
232
+ def params
233
+ unless instance_variable_defined?(:@built_params)
234
+ build_params
235
+ end
236
+
237
+ @params
238
+ end
239
+
240
+ def cookies
241
+ unless instance_variable_defined?(:@cookies)
242
+ parse_cookies
243
+ end
244
+
245
+ @cookies
246
+ end
247
+
248
+ # Sets the Content-Type header based on the format.
249
+ #
250
+ # @example
251
+ # request.format = :json
252
+ # request.content_type
253
+ # => "application/json"
254
+ #
255
+ def format=(format)
256
+ if mime = MiniMime.lookup_by_extension(format.to_s)
257
+ set_header("content-type", mime.content_type)
258
+ end
259
+
260
+ @format = format
261
+ end
262
+
263
+ def status=(status)
264
+ @status = Statuses.code(status)
265
+ end
266
+
267
+ def body=(body)
268
+ @body = if body.is_a?(Async::HTTP::Body)
269
+ body
270
+ else
271
+ Async::HTTP::Body::Buffered.wrap(body)
272
+ end
273
+ end
274
+
275
+ def write(content)
276
+ @body.write(content)
277
+ end
278
+ alias << write
279
+
280
+ def close
281
+ @body.close
282
+ end
283
+
284
+ def stream(length = nil)
285
+ unless streaming?
286
+ @body = Async::HTTP::Body::Writable.new(length)
287
+ end
288
+
289
+ @streams << Async::Task.current.async { |task|
290
+ Thread.current[:pakyow_logger] = @logger
291
+
292
+ begin
293
+ yield self
294
+ rescue => error
295
+ @logger.error(error: error)
296
+ end
297
+
298
+ @streams.delete(task)
299
+ }
300
+ end
301
+
302
+ def streaming?
303
+ @streams.any?
304
+ end
305
+
306
+ def sleep(seconds)
307
+ Async::Task.current.sleep(seconds)
308
+ end
309
+
310
+ def hijack?
311
+ @request.hijack?
312
+ end
313
+
314
+ def hijack!
315
+ @request.hijack!
316
+ end
317
+
318
+ def finalize
319
+ performing :finalize do
320
+ if request_method == "HEAD"
321
+ if streaming?
322
+ @streams.each(&:stop); @streams = []
323
+ end
324
+
325
+ close; @body = Async::HTTP::Body::Buffered.wrap(StringIO.new)
326
+ end
327
+
328
+ set_cookies
329
+
330
+ if streaming?
331
+ Async::Reactor.run do
332
+ while stream = @streams.shift
333
+ stream.wait
334
+ end
335
+
336
+ close
337
+ end
338
+ end
339
+
340
+ if instance_variable_defined?(:@response)
341
+ @response
342
+ else
343
+ Async::HTTP::Protocol::Response.new(nil, @status, finalize_headers, @body)
344
+ end
345
+ end
346
+ end
347
+
348
+ # @api private
349
+ def request_method
350
+ @request.method
351
+ end
352
+
353
+ # @api private
354
+ def request_path
355
+ @request.path
356
+ end
357
+
358
+ # @api private
359
+ def update_request_cookie(key, value)
360
+ if @request_cookies.key?(key)
361
+ @request_cookies[key] = value
362
+ end
363
+ end
364
+
365
+ private
366
+
367
+ def normalize_header(key)
368
+ key.to_s.downcase.gsub("_", "-")
369
+ end
370
+
371
+ DELETE_COOKIE = {
372
+ value: nil, path: nil, domain: nil, max_age: 0, expires: Time.at(0)
373
+ }.freeze
374
+
375
+ def set_cookies
376
+ response_cookies = {}
377
+
378
+ # Delete cookies with nil/empty values.
379
+ #
380
+ cookies.delete_if do |_, value|
381
+ value.nil? || value.empty?
382
+ end
383
+
384
+ # Normalize cookies.
385
+ #
386
+ cookies.keys.each do |key|
387
+ cookies[key] = normalize_cookie(cookies.delete(key))
388
+ end
389
+
390
+ # Set cookies that have new values.
391
+ #
392
+ cookies.reject { |key, cookie|
393
+ cookie[:value] == @request_cookies[key]
394
+ }.each do |key, cookie|
395
+ response_cookies[key] = cookie_config.merge(cookie)
396
+ end
397
+
398
+ # Remove cookies.
399
+ #
400
+ (@request_cookies.keys - cookies.keys).each do |key|
401
+ response_cookies[key] = DELETE_COOKIE
402
+ end
403
+
404
+ # Build the header value.
405
+ #
406
+ # TODO: protect against cookie values being larger than 4096 bytes
407
+ set_header(
408
+ "set-cookie",
409
+ response_cookies.map { |key, cookie|
410
+ String.new("#{escape(key.to_s)}=#{escape(cookie[:value].to_s)}") << cookie_options(cookie)
411
+ }
412
+ )
413
+ end
414
+
415
+ def normalize_cookie(cookie)
416
+ case cookie
417
+ when Hash, Support::IndifferentHash
418
+ cookie
419
+ else
420
+ { value: cookie }
421
+ end
422
+ end
423
+
424
+ def cookie_options(cookie)
425
+ String.new.tap do |options|
426
+ options << "; domain=#{cookie[:domain]}" if cookie[:domain]
427
+ options << "; path=#{cookie[:path]}" if cookie[:path]
428
+ options << "; max-age=#{cookie[:max_age]}" if cookie[:max_age]
429
+
430
+ if expires = cookie[:expires]
431
+ expires = case expires
432
+ when Integer
433
+ Time.now + expires
434
+ when Date, DateTime, Time
435
+ expires
436
+ else
437
+ nil
438
+ end
439
+
440
+ options << "; expires=#{expires.httpdate}" if expires
441
+ end
442
+
443
+ options << "; secure" if cookie[:secure]
444
+ options << "; HttpOnly" if cookie[:http_only]
445
+
446
+ if same_site = cookie[:same_site]
447
+ same_site = case same_site
448
+ when :lax
449
+ "Lax"
450
+ when :strict
451
+ "Strict"
452
+ else
453
+ nil
454
+ end
455
+
456
+ options << "; SameSite=#{same_site}" if same_site
457
+ end
458
+ end
459
+ end
460
+
461
+ def cookie_config
462
+ unless instance_variable_defined?(:@cookie_config)
463
+ config = {}
464
+ add_cookie_config(Pakyow.config.cookies, config)
465
+ @cookie_config = config
466
+ end
467
+
468
+ @cookie_config
469
+ end
470
+
471
+ def add_cookie_config(new_options, config)
472
+ new_options.to_h.each do |key, value|
473
+ if value
474
+ config[key] = value
475
+ end
476
+ end
477
+ end
478
+
479
+ def build_params
480
+ @built_params = true
481
+ @params.parse(query.to_s)
482
+ parsed_input
483
+ end
484
+
485
+ def build_type_params
486
+ unless instance_variable_defined?(:@type_params)
487
+ parse_content_type
488
+ end
489
+
490
+ QueryParser.new.tap { |parser|
491
+ parser.parse(@type_params.to_s)
492
+ }.params.deep_indifferentize
493
+ end
494
+
495
+ def parse_input
496
+ if instance_variable_defined?(:@input_parser) && input
497
+ @input_parser.call(input, self).tap do
498
+ input.rewind if input.respond_to?(:rewind)
499
+ end
500
+ else
501
+ nil
502
+ end
503
+ end
504
+
505
+ def parse_path
506
+ @path, @query = @request.path.to_s.split("?", 2)
507
+ end
508
+
509
+ def parse_authority
510
+ @host, @port = authority.to_s.split(":", 2)
511
+ end
512
+
513
+ def parse_subdomain
514
+ @subdomain = if authority.include?(".")
515
+ authority.split(".", 2)[0]
516
+ else
517
+ nil
518
+ end
519
+ end
520
+
521
+ def parse_format
522
+ @format = if path.include?(".")
523
+ path.split(".").last.to_sym
524
+ elsif accept = request_header("accept")
525
+ if accept[0] == "*/*"
526
+ :any
527
+ elsif mime_type = MiniMime.lookup_by_content_type(accept[0])&.extension
528
+ mime_type.to_sym
529
+ else
530
+ nil
531
+ end
532
+ else
533
+ :html
534
+ end
535
+ end
536
+
537
+ def parse_content_type
538
+ @type, @type_params = request_header("content-type").to_s.split(";", 2).map(&:strip)
539
+ end
540
+
541
+ def parse_cookies
542
+ @cookies = parse_cookie_header
543
+ @request_cookies = @cookies.dup
544
+ end
545
+
546
+ def parse_cookie_header
547
+ request_header("cookie").to_a.each_with_object(QueryParser.new) { |line, cookies|
548
+ cookies.parse(line)
549
+ }.params.deep_indifferentize
550
+ end
551
+
552
+ def unescape(string)
553
+ CGI.unescape(string)
554
+ end
555
+
556
+ def escape(string)
557
+ CGI.escape(string)
558
+ end
559
+
560
+ def finalize_headers
561
+ @headers.each_with_object([]) { |(key, value), headers|
562
+ Array.ensure(value).each do |single_value|
563
+ headers << [key, single_value]
564
+ end
565
+ }
566
+ end
567
+ end
568
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pakyow
6
+ # Lookup for endpoints.
7
+ #
8
+ class Endpoints
9
+ include Enumerable
10
+
11
+ extend Forwardable
12
+ def_delegator :@endpoints, :each
13
+
14
+ def initialize
15
+ @endpoints = []
16
+ end
17
+
18
+ def find(name:)
19
+ @endpoints.find { |endpoint|
20
+ endpoint.name == name
21
+ }
22
+ end
23
+
24
+ # Adds an endpoint.
25
+ #
26
+ def <<(endpoint)
27
+ @endpoints << endpoint
28
+ end
29
+
30
+ def load(object_with_endpoints)
31
+ if object_with_endpoints.respond_to?(:endpoints)
32
+ object_with_endpoints.endpoints.each do |endpoint|
33
+ self << endpoint
34
+ end
35
+ end
36
+ end
37
+
38
+ # Builds the path to a named route.
39
+ #
40
+ # @example Build the path to the +new+ route within the +post+ group:
41
+ # path(:post_new)
42
+ # # => "/posts/new"
43
+ #
44
+ # @example Build the path providing a value for +post_id+:
45
+ # path(:post_edit, post_id: 1)
46
+ # # => "/posts/1/edit"
47
+ #
48
+ def path(name, hashlike_object = nil, **params)
49
+ endpoint_with_name(name)&.path(hashlike_object, **params)
50
+ end
51
+
52
+ def method(name)
53
+ endpoint_with_name(name)&.method
54
+ end
55
+
56
+ # Builds the path to a route, following a trail of names.
57
+ #
58
+ # @example Build the path to the +new+ route within the +post+ group:
59
+ # path_to(:post, :new)
60
+ # # => "/posts/new"
61
+ #
62
+ # @example Build the path providing a value for +post_id+:
63
+ # path_to(:post, :edit, post_id: 1)
64
+ # # => "/posts/1/edit"
65
+ #
66
+ def path_to(*names, **params)
67
+ path(names.join("_").to_sym, **params)
68
+ end
69
+
70
+ private
71
+
72
+ def endpoint_with_name(name)
73
+ name = name.to_sym
74
+ @endpoints.find { |endpoint|
75
+ endpoint.name == name
76
+ }
77
+ end
78
+ end
79
+
80
+ class Endpoint
81
+ extend Forwardable
82
+ def_delegators :@builder, :params, :source_location
83
+
84
+ attr_reader :name, :method, :builder
85
+
86
+ def initialize(name:, method:, builder:)
87
+ @name, @method, @builder = name.to_sym, method.to_sym, builder
88
+ end
89
+
90
+ def path(hashlike_object = nil, **params)
91
+ @builder.call(**(hashlike_object || params).to_h)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Actions
5
+ class Dispatch
6
+ def call(connection)
7
+ catch :halt do
8
+ Pakyow.apps.each do |app|
9
+ if connection.path.start_with?(app.mount_path)
10
+ app.call(connection)
11
+ end
12
+ end
13
+ end
14
+
15
+ unless connection.halted?
16
+ error_404(connection)
17
+ end
18
+ rescue StandardError => error
19
+ connection.error = error
20
+ connection.logger.houston(error)
21
+ error_500(connection)
22
+ end
23
+
24
+ private
25
+
26
+ def error_404(connection, message = "404 Not Found")
27
+ connection.status = 404
28
+ connection.body = StringIO.new(message)
29
+ end
30
+
31
+ def error_500(connection, message = "500 Server Error")
32
+ connection.status = 500
33
+ connection.body = StringIO.new(message)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Actions
5
+ # Parses the request body with registered parsers.
6
+ #
7
+ class InputParser
8
+ def call(connection)
9
+ if (parser = parser(connection)) && (input = connection.input) && !input.empty?
10
+ connection.input_parser = parser
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def parser(connection)
17
+ Pakyow.input_parsers[connection.type]
18
+ end
19
+ end
20
+ end
21
+ end