serviette 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42024b016108d33f64f36d93032fb8fc2faf682186df0ff30c536ede45d36ea8
4
- data.tar.gz: 62c6a9f3a2cfe4f2265970a92ee6167c8f82462e2bcfb25c1ea235a56e183c0f
3
+ metadata.gz: d8e89dda09e6f6644e9b10a9baad4bb3d733590a9e81d712ff333b41754f3657
4
+ data.tar.gz: d62cfe51417367f85e4beb8046128b9d9bd6baecbbe72ea1838a031ba17bb342
5
5
  SHA512:
6
- metadata.gz: 695d10512138c4ca2aa182d6487b05bbda8b40142aff10f5db6186c33c3c9b1f13aaf337c6d3d8815141f8050abfe086c3c26c611343003d7e12e3e9a7beecec
7
- data.tar.gz: 22901d6b3c5163e55d91ed26742f6e42a8f50ed4f52bdd3726c1f5cc4b6beec15344bb89ec2e96deeb86fa50bf6ad421eca12b3f03f8520daebc7a8d8d555ccc
6
+ metadata.gz: 57f81d5a322638c78fd04356be1de562975d6be41011f9137e361d292f0757d712722e537b6fad595d4c68a96b6d4526a1fcef63431a3dee8644fe6b1d2d7634
7
+ data.tar.gz: f7e20fe4dc96596c632dc7f034c38d97d4c3d9145748e60fcbafcdb58539682e1a50b9d72656a457bad7db9994c740b8d15eba8c99917f31990af60268feacdd
data/CODE_OF_CONDUCT.md CHANGED
@@ -1,74 +1,10 @@
1
- # Contributor Covenant Code of Conduct
1
+ # Code of Conduct
2
2
 
3
- ## Our Pledge
3
+ Serviette follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
4
 
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
11
9
 
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at john@hawthorn.email. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/
10
+ If you have any concerns about behaviour within this project, please contact us at [john@hawthorn.email](mailto:john@hawthorn.email).
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 John Hawthorn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,39 +1,62 @@
1
1
  # Serviette
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/serviette`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ > [!WARNING]
4
+ > Serviette is experimental and under active development. APIs may change without notice.
4
5
 
5
- TODO: Delete this and the text above, and describe your gem
6
+ A minimal, Ractor-safe web framework for Ruby 4.0.
6
7
 
7
- ## Installation
8
+ Inspired by Sinatra, Serviette provides a simple DSL for declaring route
9
+ handlers.
8
10
 
9
- Add this line to your application's Gemfile:
11
+ ## Usage
10
12
 
11
13
  ```ruby
12
- gem 'serviette'
13
- ```
14
-
15
- And then execute:
14
+ require 'serviette'
16
15
 
17
- $ bundle
16
+ class MyApp < Serviette::Application
17
+ get '/' do
18
+ redirect '/hello/world'
19
+ end
18
20
 
19
- Or install it yourself as:
20
-
21
- $ gem install serviette
22
-
23
- ## Usage
21
+ get '/hello/:name' do |name|
22
+ "Hello, #{name}!"
23
+ end
24
24
 
25
- TODO: Write usage instructions here
25
+ get '/users/:id' do
26
+ content_type 'application/json'
27
+ { id: params["id"] }.to_json
28
+ end
29
+ end
26
30
 
27
- ## Development
31
+ # config.ru
32
+ run MyApp
33
+ ```
28
34
 
29
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
35
+ Route handlers can return a string (automatically wrapped as a 200 response),
36
+ an integer status, or a Rack `[status, headers, body]` tuple with either the
37
+ headers or the body left out. Helpers like `status`, `headers`, `content_type`,
38
+ `redirect`, and `not_found` are available.
30
39
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
40
+ To customize 404s, register a handler with `not_found { |message| ... }`. It
41
+ renders unmatched paths and `not_found(message)` calls, and can use `erb`:
32
42
 
33
- ## Contributing
43
+ ```ruby
44
+ not_found { |message| erb :not_found, props: { message: message } }
45
+ ```
34
46
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/jhawthorn/serviette. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
47
+ ## Views
36
48
 
37
- ## Code of Conduct
49
+ ERB templates are precompiled at boot and made Ractor-shareable. A
50
+ `layout.erb` is automatically used if present (override with `layout:` or
51
+ disable with `layout: false`).
38
52
 
39
- Everyone interacting in the Serviette project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jhawthorn/serviette/blob/master/CODE_OF_CONDUCT.md).
53
+ ```ruby
54
+ class MyApp < Serviette::Application
55
+ self.views = File.join(__dir__, "views")
56
+
57
+ get '/' do
58
+ @title = "Home"
59
+ erb :index
60
+ end
61
+ end
62
+ ```
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
- task :default => :spec
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ end
9
+
10
+ task default: :test
data/app.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'serviette'
4
+ require 'json'
5
+
6
+ class MyApp < Serviette::Application
7
+ START_TIME = Time.now.freeze
8
+ FORTUNES = ["good luck", "bad luck"].freeze
9
+
10
+ get '/' do
11
+ [200, {'Content-Type' => 'text/plain'}, ["Hello, World!\n"]]
12
+ end
13
+
14
+ get '/hello' do
15
+ [200, {'Content-Type' => 'text/html'}, ["<h1>Hello from Serviette!</h1>\n"]]
16
+ end
17
+
18
+ get '/hello/:name' do |name|
19
+ [200, {'Content-Type' => 'text/plain'}, ["Hello, #{name}!\n"]]
20
+ end
21
+
22
+ get '/fortune' do
23
+ fortune = FORTUNES.sample
24
+ [200, {'Content-Type' => 'text/plain'}, [fortune]]
25
+ end
26
+
27
+ get '/json' do
28
+ data = {
29
+ message: 'Hello from Serviette',
30
+ timestamp: Time.now.to_s,
31
+ version: '1.0.0'
32
+ }
33
+ [200, {'Content-Type' => 'application/json'}, [data.to_json]]
34
+ end
35
+
36
+ get '/api/status' do
37
+ status = {
38
+ status: 'ok',
39
+ server: 'Serviette',
40
+ uptime: Time.now - START_TIME,
41
+ requests_handled: rand(1000)
42
+ }
43
+ [200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(status)]]
44
+ end
45
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Freeze Rack constants so that they are shareable between Ractors.
4
+ # This file can be required after Rack is loaded to make it Ractor-safe
5
+ # without needing changes to Rack's source.
6
+
7
+ # Rack::Auth
8
+ Rack::Auth::AbstractRequest::AUTHORIZATION_KEYS.freeze
9
+
10
+ # Rack::Files
11
+ Rack::Files::ALLOWED_VERBS.freeze
12
+ Rack::Files::ALLOW_HEADER.freeze
13
+ Rack::Files::MULTIPART_BOUNDARY.freeze
14
+
15
+ class Rack::Files
16
+ class GetDispatcher
17
+ def initialize(files) = @files = files
18
+ def call(env) = @files.get(env)
19
+ end
20
+
21
+ remove_method :initialize
22
+ def initialize(root, headers = {}, default_mime = 'text/plain')
23
+ @root = (::File.expand_path(root) if root)
24
+ @headers = headers
25
+ @default_mime = default_mime
26
+ @head = Rack::Head.new(GetDispatcher.new(self))
27
+ Ractor.make_shareable(self)
28
+ end
29
+ end
30
+
31
+ # Rack::Headers
32
+ Rack::Headers::KNOWN_HEADERS.freeze
33
+
34
+ # Rack::Lint
35
+ Rack::Lint::Wrapper::BODY_METHODS.freeze
36
+ Rack::Lint::Wrapper::StreamWrapper::REQUIRED_METHODS.freeze
37
+
38
+ # Rack::Mime
39
+ Rack::Mime::MIME_TYPES.freeze
40
+
41
+ # Rack::ShowExceptions
42
+ Ractor.make_shareable(Rack::ShowExceptions::TEMPLATE)
43
+
44
+ # Rack::Multipart::Parser
45
+ if Rack::Multipart::Parser::TEMPFILE_FACTORY.is_a?(Proc) && !Ractor.shareable?(Rack::Multipart::Parser::TEMPFILE_FACTORY)
46
+ old_tempfile_factory = Rack::Multipart::Parser::TEMPFILE_FACTORY
47
+ Rack::Multipart::Parser.send(:remove_const, :TEMPFILE_FACTORY)
48
+ Rack::Multipart::Parser::TEMPFILE_FACTORY = Ractor.shareable_proc(&old_tempfile_factory)
49
+ end
50
+ Rack::Multipart::Parser::EMPTY.tmp_files.freeze
51
+ Rack::Multipart::Parser::EMPTY.freeze
52
+
53
+ # Rack::QueryParser
54
+ Rack::QueryParser::COMMON_SEP.freeze
55
+ Rack::Utils.default_query_parser.freeze
56
+
57
+ # Rack::Request
58
+ if Rack::Request.ip_filter.is_a?(Proc) && !Ractor.shareable?(Rack::Request.ip_filter)
59
+ # Rack's ip_filter lambda captures a local `trusted_proxies` regex that
60
+ # isn't shareable, so shareable_proc can't convert it. Rebuild it with
61
+ # a shareable copy of the same pattern.
62
+ valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
63
+ trusted_proxies = Ractor.make_shareable(Regexp.union(
64
+ /\A127#{valid_ipv4_octet}{3}\z/,
65
+ /\A::1\z/,
66
+ /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i,
67
+ /\A10#{valid_ipv4_octet}{3}\z/,
68
+ /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/,
69
+ /\A192\.168#{valid_ipv4_octet}{2}\z/,
70
+ /\Alocalhost\z|\Aunix(\z|:)/i,
71
+ ))
72
+ Rack::Request.ip_filter = Ractor.shareable_proc { |ip| trusted_proxies.match?(ip) }
73
+ end
74
+ Rack::Request::Helpers::DEFAULT_PORTS.freeze
75
+ Rack::Request::Helpers::FORM_DATA_MEDIA_TYPES.freeze
76
+ Rack::Request::Helpers::PARSEABLE_DATA_MEDIA_TYPES.freeze
77
+ Rack::Request.forwarded_priority.freeze
78
+ Rack::Request.x_forwarded_proto_priority.freeze
79
+
80
+ # Rack::Utils
81
+ Rack::Utils::HTTP_STATUS_CODES.freeze
82
+ Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.freeze
83
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.freeze
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Serviette
2
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
3
5
  end
data/lib/serviette.rb CHANGED
@@ -1,6 +1,216 @@
1
- require "serviette/version"
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'rack'
5
+ require_relative 'serviette/rack_ractor_support'
2
6
 
3
7
  module Serviette
4
- class Error < StandardError; end
5
- # Your code goes here...
8
+ class Template
9
+ def initialize(name, method)
10
+ @method = method
11
+ @partial = File.basename(name).start_with?("_")
12
+ freeze
13
+ end
14
+
15
+ def partial? = @partial
16
+
17
+ def render(instance, props = {}, &block)
18
+ @method.bind_call(instance, props, &block)
19
+ end
20
+ end
21
+
22
+ class Application
23
+ class << self
24
+ attr_reader :templates
25
+
26
+ def views=(path)
27
+ mod = Module.new
28
+ @templates = {}
29
+ Dir.glob(File.join(path, "**/*.erb")).each do |file|
30
+ name = file.delete_prefix("#{path}/").delete_suffix(".erb")
31
+ method_name = template_method_name(name)
32
+ ERB.new(File.read(file)).def_method(mod, "#{method_name}(props = {})", file)
33
+ @templates[name.to_sym] = Template.new(name, mod.instance_method(method_name))
34
+ end
35
+ Ractor.make_shareable(@templates)
36
+ end
37
+
38
+ def routes
39
+ @routes ||= {}
40
+ end
41
+
42
+ def route(verb, path, &block)
43
+ handler = Ractor.shareable_proc(&block)
44
+ pattern = compile_route(path)
45
+ (routes[verb] ||= []) << [pattern, handler].freeze
46
+ end
47
+
48
+ # Registers the block that renders every 404: unmatched paths and
49
+ # `not_found` calls in a route. It runs on an instance, like a route,
50
+ # and is given the message passed to `not_found` (nil if none).
51
+ def not_found(&block)
52
+ @not_found_handler = Ractor.shareable_proc(&block)
53
+ end
54
+
55
+ attr_reader :not_found_handler
56
+
57
+ def get(path, &block) = route(:GET, path, &block)
58
+ def post(path, &block) = route(:POST, path, &block)
59
+ def put(path, &block) = route(:PUT, path, &block)
60
+ def delete(path, &block) = route(:DELETE, path, &block)
61
+
62
+ def call(env)
63
+ request_method = env['REQUEST_METHOD'].to_sym
64
+ response = dispatch(env, request_method)
65
+ # HEAD falls back to the GET routes, after any explicit HEAD route.
66
+ # Dropping the response body is left to Rack::Head.
67
+ response ||= dispatch(env, :GET) if request_method == :HEAD
68
+
69
+ response || unmatched(env)
70
+ end
71
+
72
+ def unmatched(env)
73
+ instance = new(env)
74
+ body = instance.not_found
75
+ instance.response.body = [body] if String === body
76
+ instance.response.finish
77
+ end
78
+ private :unmatched
79
+
80
+ def dispatch(env, request_method)
81
+ path_info = env['PATH_INFO']
82
+
83
+ if (verb_routes = routes[request_method])
84
+ verb_routes.each do |pattern, handler|
85
+ next unless (match = pattern.match(path_info))
86
+
87
+ instance = new(env)
88
+ instance.params = match.named_captures.freeze
89
+ body = instance.instance_exec(*match.captures, &handler)
90
+ response = instance.response
91
+
92
+ if Array === body && Integer === body.first
93
+ raise ArgumentError, "invalid response tuple: #{body.inspect}" if body.size > 3
94
+ status, headers, rest = body
95
+ response.status = status
96
+ if Hash === headers
97
+ response.headers.merge!(headers)
98
+ body = rest
99
+ else
100
+ body = headers
101
+ end
102
+ elsif Integer === body
103
+ response.status = body
104
+ body = nil
105
+ end
106
+
107
+ if String === body
108
+ response.body = [body]
109
+ elsif body.respond_to?(:each)
110
+ response.body = body
111
+ end
112
+
113
+ return response.finish
114
+ end
115
+ end
116
+
117
+ nil
118
+ end
119
+ private :dispatch
120
+
121
+ def freeze
122
+ routes.each_value(&:freeze)
123
+ routes.freeze
124
+ super
125
+ end
126
+
127
+ private
128
+
129
+ # An overly complex mapping, that gives each filename a unique and
130
+ # recognizable method name:
131
+ # - "/" becomes "__"
132
+ # - "." becomes "_2"
133
+ # - anything else illegal becomes "_0" plus its hex bytes
134
+ # - a literal "_" becomes "_1", but only where it could otherwise be
135
+ # read as one of the escapes above
136
+ # So "users/_row" becomes "template_users___row".
137
+ def template_method_name(name)
138
+ "template_" + name.gsub(/[^a-zA-Z0-9_]|_(?![a-zA-Z]|\z)/) do |char|
139
+ case char
140
+ when "/" then "__"
141
+ when "." then "_2"
142
+ when "_" then "_1"
143
+ else char.bytes.map { |byte| "_0%02x" % byte }.join
144
+ end
145
+ end
146
+ end
147
+
148
+ def compile_route(path)
149
+ return path if path.is_a?(Regexp)
150
+
151
+ parts = path.split(/:(\w+)/)
152
+ regex = parts.each_slice(2).map do |literal, name|
153
+ Regexp.escape(literal) + (name ? "(?<#{name}>[^/]+)" : "")
154
+ end.join
155
+ /\A#{regex}\z/.freeze
156
+ end
157
+ end
158
+
159
+ attr_accessor :params
160
+ attr_reader :request, :response
161
+
162
+ def initialize(env = {})
163
+ @request = Rack::Request.new(env)
164
+ @response = Rack::Response.new
165
+ end
166
+
167
+ def status(value = nil)
168
+ response.status = value if value
169
+ response.status
170
+ end
171
+
172
+ def headers(hash = nil)
173
+ response.headers.merge!(hash) if hash
174
+ response.headers
175
+ end
176
+
177
+ def content_type(type)
178
+ response['content-type'] = type.to_s
179
+ end
180
+
181
+ def redirect(uri, status = 302)
182
+ response.status = status
183
+ response['location'] = uri.to_s
184
+ end
185
+
186
+ def not_found(body = nil)
187
+ response.status = 404
188
+ handler = self.class.not_found_handler
189
+ body = instance_exec(body, &handler) if handler
190
+ body = "Not Found\n" if body.nil?
191
+ response['content-type'] ||= 'text/plain; charset=utf-8' if String === body
192
+ body
193
+ end
194
+
195
+ def erb(template_name, layout: nil, props: {})
196
+ templates = self.class.templates
197
+ raise "views directory not configured. Add `self.views = File.join(__dir__, \"views\")` to #{self.class}" unless templates
198
+ template = templates[template_name.to_sym]
199
+ raise "unknown template :#{template_name}, available: #{templates.keys.inspect}" unless template
200
+ content_type "text/html; charset=utf-8"
201
+ output = template.render(self, props)
202
+
203
+ if layout.nil?
204
+ layout = :layout if templates.key?(:layout) && !template.partial?
205
+ end
206
+
207
+ if layout
208
+ layout_template = templates[layout.to_sym]
209
+ raise "unknown layout template :#{layout}, available: #{templates.keys.inspect}" unless layout_template
210
+ output = layout_template.render(self) { output }
211
+ end
212
+
213
+ output
214
+ end
215
+ end
6
216
  end
data/test_ractor.rb ADDED
@@ -0,0 +1,44 @@
1
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
2
+ require 'serviette'
3
+ require_relative 'app'
4
+
5
+ # Freeze the app class and its routes for Ractor safety
6
+ MyApp.freeze
7
+
8
+ # Test in a Ractor
9
+ r = Ractor.new(MyApp) do |app_class|
10
+ env = {
11
+ 'REQUEST_METHOD' => 'GET',
12
+ 'PATH_INFO' => '/'
13
+ }
14
+
15
+ response = app_class.call(env)
16
+ response
17
+ end
18
+
19
+ puts "Response from Ractor: #{r.value.inspect}"
20
+
21
+ # Test multiple routes in parallel
22
+ ractors = [
23
+ ['/', 'GET'],
24
+ ['/hello', 'GET'],
25
+ ['/hello/world', 'GET'],
26
+ ['/fortune', 'GET'],
27
+ ['/json', 'GET'],
28
+ ['/api/status', 'GET']
29
+ ].map do |path, method|
30
+ Ractor.new(MyApp, path, method) do |app_class, path_info, request_method|
31
+ env = {
32
+ 'REQUEST_METHOD' => request_method,
33
+ 'PATH_INFO' => path_info
34
+ }
35
+
36
+ [path_info, app_class.call(env)]
37
+ end
38
+ end
39
+
40
+ ractors.each do |r|
41
+ path, response = r.value
42
+ status, headers, body = response
43
+ puts "#{path}: #{status} - #{body.first.inspect}"
44
+ end
metadata CHANGED
@@ -1,64 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serviette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
8
- autorequire:
9
- bindir: exe
8
+ bindir: bin
10
9
  cert_chain: []
11
- date: 2019-03-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: bundler
13
+ name: rack
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.17'
20
- type: :development
18
+ version: '0'
19
+ type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '1.17'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '10.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '10.0'
41
- description: Saving for later
25
+ version: '0'
26
+ description: A minimal, Ractor-safe web framework for Ruby 4.0
42
27
  email:
43
28
  - john@hawthorn.email
44
29
  executables: []
45
30
  extensions: []
46
31
  extra_rdoc_files: []
47
32
  files:
48
- - ".gitignore"
49
33
  - CODE_OF_CONDUCT.md
50
- - Gemfile
34
+ - LICENSE.txt
51
35
  - README.md
52
36
  - Rakefile
53
- - bin/console
54
- - bin/setup
37
+ - app.rb
55
38
  - lib/serviette.rb
39
+ - lib/serviette/rack_ractor_support.rb
56
40
  - lib/serviette/version.rb
57
- - serviette.gemspec
58
- homepage: https://johnhawthorn.com
59
- licenses: []
41
+ - test_ractor.rb
42
+ homepage: https://github.com/jhawthorn/serviette
43
+ licenses:
44
+ - MIT
60
45
  metadata: {}
61
- post_install_message:
62
46
  rdoc_options: []
63
47
  require_paths:
64
48
  - lib
@@ -66,16 +50,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
50
  requirements:
67
51
  - - ">="
68
52
  - !ruby/object:Gem::Version
69
- version: '0'
53
+ version: '4.0'
70
54
  required_rubygems_version: !ruby/object:Gem::Requirement
71
55
  requirements:
72
56
  - - ">="
73
57
  - !ruby/object:Gem::Version
74
58
  version: '0'
75
59
  requirements: []
76
- rubyforge_project:
77
- rubygems_version: 2.7.6
78
- signing_key:
60
+ rubygems_version: 4.0.20
79
61
  specification_version: 4
80
- summary: Really good gem name
62
+ summary: A minimal, Ractor-safe web framework for Ruby 4.0
81
63
  test_files: []
data/.gitignore DELETED
@@ -1,8 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
- # Specify your gem's dependencies in serviette.gemspec
6
- gemspec
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "serviette"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/serviette.gemspec DELETED
@@ -1,27 +0,0 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "serviette/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "serviette"
8
- spec.version = Serviette::VERSION
9
- spec.authors = ["John Hawthorn"]
10
- spec.email = ["john@hawthorn.email"]
11
-
12
- spec.summary = %q{Really good gem name}
13
- spec.description = %q{Saving for later}
14
- spec.homepage = "https://johnhawthorn.com"
15
-
16
- # Specify which files should be added to the gem when it is released.
17
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
- end
21
- spec.bindir = "exe"
22
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
24
-
25
- spec.add_development_dependency "bundler", "~> 1.17"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- end