syntropy 0.33.0 → 0.34.1

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/cmd/console.rb +18 -7
  4. data/cmd/serve.rb +26 -18
  5. data/cmd/test.rb +37 -24
  6. data/examples/blog/.gitignore +1 -0
  7. data/examples/blog/app/_layout/default.rb +3 -0
  8. data/examples/blog/app/_lib/database.rb +13 -0
  9. data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
  10. data/examples/blog/app/assets/style.css +20 -0
  11. data/examples/blog/app/index.rb +12 -2
  12. data/examples/blog/app/posts/[id]/edit.rb +2 -2
  13. data/examples/blog/app/posts/[id]/index.rb +4 -4
  14. data/examples/blog/app/posts/index.rb +4 -4
  15. data/examples/blog/app/posts/new.rb +1 -1
  16. data/examples/blog/app/test.rb +7 -0
  17. data/examples/blog/config/development.rb +5 -0
  18. data/examples/blog/config/production.rb +4 -0
  19. data/examples/blog/config/test.rb +5 -0
  20. data/examples/blog/test/test_posts.rb +65 -0
  21. data/examples/mcp-oauth/app/oauth/token.rb +1 -1
  22. data/examples/template/.gitignore +2 -0
  23. data/examples/template/Gemfile +3 -0
  24. data/examples/template/app/_layout/default.rb +14 -0
  25. data/examples/template/app/_lib/database.rb +13 -0
  26. data/examples/template/app/_schema/2026-01-01-initial.rb +9 -0
  27. data/examples/template/app/assets/style.css +25 -0
  28. data/examples/template/app/index.rb +27 -0
  29. data/examples/template/app/test.rb +7 -0
  30. data/examples/template/config/development.rb +5 -0
  31. data/examples/template/config/production.rb +4 -0
  32. data/examples/template/config/test.rb +5 -0
  33. data/examples/template/test/test_app.rb +14 -0
  34. data/lib/syntropy/app.rb +48 -40
  35. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
  36. data/lib/syntropy/applets/builtin/default_error_handler/style.css +4 -8
  37. data/lib/syntropy/applets/builtin/default_error_handler.rb +18 -9
  38. data/lib/syntropy/db/schema.rb +1 -1
  39. data/lib/syntropy/db/store.rb +2 -0
  40. data/lib/syntropy/errors.rb +6 -2
  41. data/lib/syntropy/http/client.rb +1 -0
  42. data/lib/syntropy/http/server_connection.rb +0 -4
  43. data/lib/syntropy/json_api.rb +27 -1
  44. data/lib/syntropy/logger.rb +81 -27
  45. data/lib/syntropy/markdown.rb +61 -32
  46. data/lib/syntropy/mime_types.rb +9 -5
  47. data/lib/syntropy/module_loader.rb +31 -9
  48. data/lib/syntropy/papercraft_extensions.rb +2 -2
  49. data/lib/syntropy/request/mock_adapter.rb +10 -8
  50. data/lib/syntropy/request/request_info.rb +91 -0
  51. data/lib/syntropy/request/response.rb +1 -12
  52. data/lib/syntropy/request/validation.rb +1 -0
  53. data/lib/syntropy/request.rb +51 -19
  54. data/lib/syntropy/routing_tree.rb +27 -28
  55. data/lib/syntropy/session.rb +198 -0
  56. data/lib/syntropy/side_run.rb +25 -2
  57. data/lib/syntropy/test.rb +105 -10
  58. data/lib/syntropy/utils.rb +53 -18
  59. data/lib/syntropy/version.rb +1 -1
  60. data/lib/syntropy.rb +44 -10
  61. data/test/bm_router_proc.rb +4 -4
  62. data/test/fixtures/app/class_instance.rb +5 -0
  63. data/test/fixtures/app/http.rb +5 -0
  64. data/test/fixtures/app/post_ct.rb +5 -0
  65. data/test/fixtures/app/singleton.rb +3 -0
  66. data/test/test_app.rb +13 -52
  67. data/test/test_caching.rb +2 -2
  68. data/test/test_db_schema.rb +1 -1
  69. data/test/test_http_server_connection.rb +3 -3
  70. data/test/test_module_loader.rb +5 -2
  71. data/test/test_response.rb +0 -19
  72. data/test/test_routing_tree.rb +69 -69
  73. data/test/test_server.rb +5 -9
  74. data/test/test_test.rb +70 -0
  75. metadata +66 -42
  76. data/examples/blog/app/_setup.rb +0 -4
  77. data/lib/syntropy/request/session.rb +0 -113
  78. /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
  79. /data/test/{app → fixtures/app}/_hook.rb +0 -0
  80. /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
  81. /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
  82. /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
  83. /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
  84. /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
  85. /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
  86. /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
  87. /data/test/{app → fixtures/app}/about/_error.rb +0 -0
  88. /data/test/{app → fixtures/app}/about/foo.md +0 -0
  89. /data/test/{app → fixtures/app}/about/index.rb +0 -0
  90. /data/test/{app → fixtures/app}/about/raise.rb +0 -0
  91. /data/test/{app → fixtures/app}/api+.rb +0 -0
  92. /data/test/{app → fixtures/app}/assets/style.css +0 -0
  93. /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
  94. /data/test/{app → fixtures/app}/bar.rb +0 -0
  95. /data/test/{app → fixtures/app}/baz.rb +0 -0
  96. /data/test/{app → fixtures/app}/by_method.rb +0 -0
  97. /data/test/{app → fixtures/app}/deps.rb +0 -0
  98. /data/test/{app → fixtures/app}/index.html +0 -0
  99. /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
  100. /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
  101. /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
  102. /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
  103. /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
  104. /data/test/{app → fixtures/app}/rss.rb +0 -0
  105. /data/test/{app → fixtures/app}/tmp.rb +0 -0
  106. /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
  107. /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
  108. /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
  109. /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
  110. /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
  111. /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
  112. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
  113. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
  114. /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
  115. /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
@@ -3,39 +3,68 @@
3
3
  require 'yaml'
4
4
 
5
5
  module Syntropy
6
- DATE_REGEXP = /(\d{4}-\d{2}-\d{2})/
7
- FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
8
- YAML_OPTS = {
9
- permitted_classes: [Date],
10
- symbolize_names: true
11
- }
12
-
13
- # Parses the markdown file at the given path.
14
- #
15
- # @param path [String] file path
16
- # @return [Array] an tuple containing properties<Hash>, contents<String>
17
- def self.parse_markdown_file(path, env)
18
- content = IO.read(path) || ''
19
- atts = {}
20
-
21
- # Parse date from file name
22
- m = path.match(DATE_REGEXP)
23
- atts[:date] ||= Date.parse(m[1]) if m
24
-
25
- if (m = content.match(FRONT_MATTER_REGEXP))
26
- front_matter = m[1]
27
- content = m.post_match
28
-
29
- yaml = YAML.safe_load(front_matter, **YAML_OPTS)
30
- atts = atts.merge(yaml)
31
- end
6
+ # Markdown parsing.
7
+ module Markdown
8
+ FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
9
+ YAML_OPTS = {
10
+ permitted_classes: [Date],
11
+ symbolize_names: true
12
+ }.freeze
32
13
 
33
- if env[:root_dir]
34
- atts[:url] = path
35
- .gsub(/#{env[:root_dir]}/, '')
36
- .gsub(/\.md$/, '')
37
- end
14
+ class << self
15
+ # Parses the markdown file at the given path.
16
+ #
17
+ # @param path [String] file path
18
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
19
+ def parse(path, env)
20
+ content = IO.read(path) || ''
21
+ atts = {}
22
+
23
+ parse_date(path, atts)
24
+ content = parse_content(content, atts)
25
+ atts[:url] = path_to_url(path, env[:app_root]) if env[:app_root]
26
+
27
+ [atts, content]
28
+ end
29
+
30
+ private
38
31
 
39
- [atts, content]
32
+ # Parses date information from the given path.
33
+ #
34
+ # @param path [String] file path
35
+ # @param atts [Hash] file attributes
36
+ # @return [void]
37
+ def parse_date(path, atts)
38
+ # Parse date from file name
39
+ if (m = path.match(/(\d{4}-\d{2}-\d{2})/))
40
+ atts[:date] ||= Date.parse(m[1])
41
+ end
42
+ end
43
+
44
+ # Parses the markdown content and front matter attributes from the given content.
45
+ #
46
+ # @param content [String] file content
47
+ # @param atts [Hash] file attributes
48
+ # @return [String] parsed markdown content
49
+ def parse_content(content, atts)
50
+ if (m = content.match(FRONT_MATTER_REGEXP))
51
+ front_matter = m[1]
52
+ content = m.post_match
53
+
54
+ yaml = YAML.safe_load(front_matter, **YAML_OPTS)
55
+ atts.merge!(yaml)
56
+ end
57
+ content
58
+ end
59
+
60
+ # Converts the markdown file path to URL
61
+ #
62
+ # @param path [String] file path
63
+ # @param app_root [String] app root directory
64
+ # @return [String] url
65
+ def path_to_url(path, app_root)
66
+ path.gsub(/#{app_root}/, '').gsub(/\.md$/, '')
67
+ end
68
+ end
40
69
  end
41
70
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- # File extension to MIME type mapping
4
+ # The MimeTypes module maps file extensions to MIME types.
5
5
  module MimeTypes
6
6
  TYPES = {
7
7
  'html' => 'text/html',
@@ -21,16 +21,20 @@ module Syntropy
21
21
 
22
22
  EXT_REGEXP = /\.?([^\.]+)$/.freeze
23
23
 
24
- def self.[](ref)
25
- case ref
24
+ # Returns the mime type for the given file extension.
25
+ #
26
+ # @param ext [String, Symbol] file extension
27
+ # @return [String, nil] MIME type
28
+ def self.[](ext)
29
+ case ext
26
30
  when Symbol
27
- TYPES[ref.to_s]
31
+ TYPES[ext.to_s]
28
32
  when EXT_REGEXP
29
33
  TYPES[Regexp.last_match(1)]
30
34
  when ''
31
35
  nil
32
36
  else
33
- raise "Invalid argument #{ref.inspect}"
37
+ raise "Invalid argument #{ext.inspect}"
34
38
  end
35
39
  end
36
40
  end
@@ -28,7 +28,7 @@ module Syntropy
28
28
  # @return [void]
29
29
  def initialize(env)
30
30
  @env = env
31
- @root_dir = env[:root_dir]
31
+ @app_root = env[:app_root]
32
32
  @modules = {} # maps ref to module entry
33
33
  @fn_map = {} # maps filename to ref
34
34
  end
@@ -55,8 +55,8 @@ module Syntropy
55
55
  # @param dir [String] relative module directory
56
56
  # @return [Array<String>] list of modules
57
57
  def list(dir)
58
- fns = Dir[File.join(@root_dir, dir, '*.rb')]
59
- fns.map { it.match(/^#{@root_dir}\/(.+)\.rb$/)[1] }.sort
58
+ fns = Dir[File.join(@app_root, dir, '*.rb')]
59
+ fns.map { it.match(/^#{@app_root}\/(.+)\.rb$/)[1] }.sort
60
60
  end
61
61
 
62
62
  # Invalidates a module by its filename, normally following a change to the
@@ -89,10 +89,13 @@ module Syntropy
89
89
  entry[:reverse_deps].each { invalidate_ref(it) }
90
90
  end
91
91
 
92
+ # Invalidates a collection module.
93
+ #
94
+ # @return [void]
92
95
  def invalidate_collection_modules
93
96
  refs = []
94
97
  @modules.each do |ref, entry|
95
- refs << ref if entry[:module].is_collection_module?
98
+ refs << ref if entry[:module].collection_module?
96
99
  end
97
100
  refs.each { invalidate_ref(it) }
98
101
  end
@@ -118,7 +121,7 @@ module Syntropy
118
121
  # @return [Hash] module entry
119
122
  def load_module(ref, raise_on_missing: true)
120
123
  ref = "/#{ref}" if ref !~ /^\//
121
- fn = File.expand_path(File.join(@root_dir, "#{ref}.rb"))
124
+ fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
122
125
  if !File.file?(fn)
123
126
  raise Syntropy::Error, "File not found #{fn}" if raise_on_missing
124
127
 
@@ -142,6 +145,10 @@ module Syntropy
142
145
  }
143
146
  end
144
147
 
148
+ # Cleans up a module reference specifier, turning /index into /
149
+ #
150
+ # @param ref [String] input ref
151
+ # @return [String] clean ref
145
152
  def clean_ref(ref)
146
153
  return '/' if ref =~ /^index(\+)?$/
147
154
 
@@ -191,6 +198,17 @@ module Syntropy
191
198
  m.instance_eval(code, fn)
192
199
  env[:logger]&.info(message: "Loaded module at #{fn}")
193
200
  m
201
+ rescue SyntaxError => e
202
+ STDERR.puts("\n#{e.message}")
203
+
204
+ if (m = e.message.match(/^(.+)\: syntax/))
205
+ location = m[1]
206
+ e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
207
+ e2.set_backtrace([location] + e.backtrace)
208
+ raise e2
209
+ else
210
+ raise e
211
+ end
194
212
  end
195
213
 
196
214
  # Initializes a module with the given environment hash.
@@ -222,7 +240,7 @@ module Syntropy
222
240
  # #collection_module!
223
241
  #
224
242
  # @return [bool]
225
- def is_collection_module?
243
+ def collection_module?
226
244
  @collection_module_p
227
245
  end
228
246
 
@@ -258,6 +276,10 @@ module Syntropy
258
276
  self
259
277
  end
260
278
 
279
+ # Normalize an import reference, turning a relative path into an absolute one.
280
+ #
281
+ # @param ref [String] input ref
282
+ # @return [String] normalized ref
261
283
  def normalize_import_ref(ref)
262
284
  base = @ref == '' ? '/' : @ref
263
285
  if ref =~ /^\//
@@ -274,7 +296,7 @@ module Syntropy
274
296
  # @return [Papercraft::Template] template
275
297
  def template(proc = nil, &block)
276
298
  proc ||= block
277
- raise "No template block/proc given" if !proc
299
+ raise 'No template block/proc given' if !proc
278
300
 
279
301
  Papercraft::Template.new(proc)
280
302
  end
@@ -286,10 +308,10 @@ module Syntropy
286
308
  # @return [Papercraft::Template] template
287
309
  def template_xml(proc = nil, &block)
288
310
  proc ||= block
289
- raise "No template block/proc given" if !proc
311
+ raise 'No template block/proc given' if !proc
290
312
 
291
313
  Papercraft::Template.new(proc, mode: :xml)
292
- rescue => e
314
+ rescue StandardError => e
293
315
  p e
294
316
  p e.backtrace
295
317
  raise
@@ -4,12 +4,12 @@ require 'papercraft'
4
4
 
5
5
  Papercraft.extension(
6
6
  'auto_refresh!': ->(loc = '/.syntropy') {
7
- if $syntropy_dev_mode
7
+ if Syntropy.dev_mode
8
8
  script(src: File.join(loc, 'auto_refresh/watch.js'), type: 'module')
9
9
  end
10
10
  },
11
11
  'debug_template!': ->(loc = '/.syntropy') {
12
- if $syntropy_dev_mode
12
+ if Syntropy.dev_mode
13
13
  script(src: File.join(loc, 'debug/debug.js'), type: 'module')
14
14
  end
15
15
  }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
+ # Implements a mock adapter for testing
4
5
  class MockAdapter
5
6
  attr_reader :response_body, :response_headers, :calls
6
7
 
@@ -19,14 +20,13 @@ module Syntropy
19
20
  end
20
21
 
21
22
  def initialize(request_body)
22
- case request_body
23
- when Array
24
- @request_body_chunks = request_body
25
- when nil
26
- @request_body_chunks = []
27
- else
28
- @request_body_chunks = [request_body]
29
- end
23
+ @request_body_chunks =
24
+ case request_body
25
+ when Array then request_body
26
+ when nil then []
27
+ else [request_body]
28
+ end
29
+
30
30
  @calls = []
31
31
  end
32
32
 
@@ -47,6 +47,8 @@ module Syntropy
47
47
  response_headers[':status'] || HTTP::OK
48
48
  end
49
49
 
50
+ def respond_to_missing?(sym) = true
51
+
50
52
  def method_missing(sym, *args)
51
53
  calls << [sym, *args]
52
54
  end
@@ -3,36 +3,61 @@
3
3
  require 'uri'
4
4
 
5
5
  module Syntropy
6
+ # Request information extension methods.
6
7
  module RequestInfoMethods
8
+ # Returns the request host.
9
+ #
10
+ # @return [String, nil]
7
11
  def host
8
12
  @headers['host'] || @headers[':authority']
9
13
  end
10
14
  alias_method :authority, :host
11
15
 
16
+ # Returns the connection header value.
17
+ #
18
+ # @return [String, nil]
12
19
  def connection
13
20
  @headers['connection']
14
21
  end
15
22
 
23
+ # Returns the upgrade protocol.
24
+ #
25
+ # @return [String, nil]
16
26
  def upgrade_protocol
17
27
  connection == 'upgrade' && @headers['upgrade']&.downcase
18
28
  end
19
29
 
30
+ # Returns the websocket version.
31
+ #
32
+ # @return [String, nil]
20
33
  def websocket_version
21
34
  headers['sec-websocket-version'].to_i
22
35
  end
23
36
 
37
+ # Returns the protocol.
38
+ #
39
+ # @return [String, nil]
24
40
  def protocol
25
41
  @protocol ||= @adapter.protocol
26
42
  end
27
43
 
44
+ # Returns the HTTP method in lower case.
45
+ #
46
+ # @return [String]
28
47
  def method
29
48
  @method ||= @headers[':method'].downcase
30
49
  end
31
50
 
51
+ # Returns the request scheme.
52
+ #
53
+ # @return [String, nil]
32
54
  def scheme
33
55
  @scheme ||= @headers[':scheme']
34
56
  end
35
57
 
58
+ # Returns the request content type.
59
+ #
60
+ # @return [String, nil]
36
61
  def content_type
37
62
  ct = @headers['content-type']
38
63
  return nil if !ct
@@ -59,22 +84,37 @@ module Syntropy
59
84
  self
60
85
  end
61
86
 
87
+ # Returns the parsed request URI.
88
+ #
89
+ # @return [URI::Generic]
62
90
  def uri
63
91
  @uri ||= URI.parse(@headers[':path'] || '')
64
92
  end
65
93
 
94
+ # Returns the parsed full request URI.
95
+ #
96
+ # @return [URI::HTTP]
66
97
  def full_uri
67
98
  @full_uri = "#{scheme}://#{host}#{uri}"
68
99
  end
69
100
 
101
+ # Returns the request path.
102
+ #
103
+ # @return [String]
70
104
  def path
71
105
  @path ||= uri.path
72
106
  end
73
107
 
108
+ # Returns the request (unparsed) query string.
109
+ #
110
+ # @return [String, nil]
74
111
  def query_string
75
112
  @query_string ||= uri.query
76
113
  end
77
114
 
115
+ # Returns the parsed query hash.
116
+ #
117
+ # @return [Hash]
78
118
  def query
79
119
  return @query if @query
80
120
 
@@ -83,6 +123,10 @@ module Syntropy
83
123
 
84
124
  QUERY_KV_REGEXP = /([^=]+)(?:=(.*))?/
85
125
 
126
+ # Converts a query string into a query hash
127
+ #
128
+ # @param query [String]
129
+ # @return [Hash]
86
130
  def parse_query(query)
87
131
  query.split('&').each_with_object({}) do |kv, h|
88
132
  k, v = kv.match(QUERY_KV_REGEXP)[1..2]
@@ -90,10 +134,16 @@ module Syntropy
90
134
  end
91
135
  end
92
136
 
137
+ # Returns the request ID.
138
+ #
139
+ # @return [String, nil]
93
140
  def request_id
94
141
  @headers['x-request-id']
95
142
  end
96
143
 
144
+ # Returns the forwarded for value.
145
+ #
146
+ # @return [String, nil]
97
147
  def forwarded_for
98
148
  @headers['x-forwarded-for']
99
149
  end
@@ -107,6 +157,9 @@ module Syntropy
107
157
  encoding.split(',').map { |i| i.strip }
108
158
  end
109
159
 
160
+ # Returns the parsed cookie values.
161
+ #
162
+ # @return [String, nil]
110
163
  def cookies
111
164
  @cookies ||= parse_cookies(headers['cookie'])
112
165
  end
@@ -114,6 +167,10 @@ module Syntropy
114
167
  COOKIE_RE = /^([^=]+)=(.*)$/.freeze
115
168
  SEMICOLON = ';'
116
169
 
170
+ # Parses the cookie string.
171
+ #
172
+ # @param cookies [String]
173
+ # @return [Hash]
117
174
  def parse_cookies(cookies)
118
175
  return {} unless cookies
119
176
 
@@ -139,6 +196,9 @@ module Syntropy
139
196
  raise Syntropy::Error.new('Invalid form data', HTTP::BAD_REQUEST)
140
197
  end
141
198
 
199
+ # Returns true if the user-agent is a browser.
200
+ #
201
+ # @return [bool]
142
202
  def browser?
143
203
  user_agent = headers['user-agent']
144
204
  user_agent && user_agent =~ /^Mozilla\//
@@ -156,6 +216,9 @@ module Syntropy
156
216
  @accept_parts.include?(mime_type)
157
217
  end
158
218
 
219
+ # Returns the bearer token.
220
+ #
221
+ # @return [String, nil]
159
222
  def auth_bearer_token
160
223
  auth = headers['authorization']
161
224
  if auth && (m = auth.match(/Bearer\s+([^\w]+)/))
@@ -167,12 +230,22 @@ module Syntropy
167
230
 
168
231
  private
169
232
 
233
+ # Parses an accept string into an array of accepted MIME types.
234
+ #
235
+ # @param accept [string]
236
+ # @return [Array<String>]
170
237
  def parse_accept_parts(accept)
171
238
  accept.split(',').map { it.match(/^\s*([^\s;]+)/)[1] }
172
239
  end
173
240
  end
174
241
 
242
+ # Request info class methods
175
243
  module RequestInfoClassMethods
244
+ # Parses form data into a hash
245
+ #
246
+ # @param body [String]
247
+ # @param headers [Hash]
248
+ # @return [Hash]
176
249
  def parse_form_data(body, headers)
177
250
  case (content_type = headers['content-type'])
178
251
  when /^multipart\/form\-data; boundary=([^\s]+)/
@@ -185,6 +258,11 @@ module Syntropy
185
258
  end
186
259
  end
187
260
 
261
+ # Parses a multipart form body.
262
+ #
263
+ # @param body [String]
264
+ # @param boundary [String]
265
+ # @return [Hash]
188
266
  def parse_multipart_form_data(body, boundary)
189
267
  parts = body.split(boundary)
190
268
  raise BadRequestError, 'Invalid form data' if parts.size < 2
@@ -197,6 +275,11 @@ module Syntropy
197
275
  end
198
276
  end
199
277
 
278
+ # Parses a multipart form data part.
279
+ #
280
+ # @param body [String]
281
+ # @param hash [Hash] output hash
282
+ # @return [void]
200
283
  def parse_multipart_form_data_part(part, hash)
201
284
  body, headers = parse_multipart_form_data_part_headers(part)
202
285
  disposition = headers['content-disposition'] || ''
@@ -211,6 +294,10 @@ module Syntropy
211
294
  end
212
295
  end
213
296
 
297
+ # Parses a multipart form data part headers.
298
+ #
299
+ # @param part [String]
300
+ # @return [Hash]
214
301
  def parse_multipart_form_data_part_headers(part)
215
302
  headers = {}
216
303
  while true
@@ -234,6 +321,10 @@ module Syntropy
234
321
  MAX_PARAMETER_NAME_SIZE = 256
235
322
  MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB
236
323
 
324
+ # Parses a URL-encoded form.
325
+ #
326
+ # @param body [String]
327
+ # @return [Hash]
237
328
  def parse_urlencoded_form_data(body)
238
329
  return {} unless body
239
330
 
@@ -7,18 +7,7 @@ require_relative '../http/status'
7
7
  require_relative '../mime_types'
8
8
 
9
9
  module Syntropy
10
- module StaticFileCaching
11
- class << self
12
- def file_stat_to_etag(stat)
13
- "#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
14
- end
15
-
16
- def file_stat_to_last_modified(stat)
17
- stat.mtime.httpdate
18
- end
19
- end
20
- end
21
-
10
+ # Response methods.
22
11
  module ResponseMethods
23
12
  WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
24
13
 
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
 
5
5
  module Syntropy
6
+ # Request validation methods.
6
7
  module RequestValidationMethods
7
8
 
8
9
  # Checks the request's HTTP method against the given accepted values. If not