stir_fry 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d7a00e29a6a5328edd5213cd08821bd05627ac300c11bed5cc473a54059e63fc
4
+ data.tar.gz: d529099f09b0056ed4be196a62b0cfea2bcf99c2b0bb4c747116bcf92a8b16ba
5
+ SHA512:
6
+ metadata.gz: 59c837dd40c9dc13e80a8d467fd8ce1618cef3c8e4fc5e1dcffe25cc5faefcdb45bd22ffaaf5da531a9e9f6969e481ad8c1a7fcd0cf501c64194c3b516a5cbad
7
+ data.tar.gz: 97a6c2f514e52a7304f1900e802d4132ebeaac41ad623c1bcd00202fc1f476b286d6f2a2c825cf6077323d509c283e654f01546debd140885c611adf0044d05f
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-08-10
4
+
5
+ Initial release of the whole framework including:
6
+
7
+ - Rack compatible App to run the app server in a way that is easy to use.
8
+ - Components for reacting to routes.
9
+ - Request & Response helpers
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "irb"
8
+ gem "pstore"
9
+ gem "puma"
10
+ gem "rake", "~> 13.0"
11
+ gem "rdoc"
12
+ gem "rerun"
13
+ gem "rspec", "~> 3.13"
14
+ gem "rubocop", "~> 1.21"
15
+ gem "rubocop-rake", ">= 0.7.1"
16
+ gem "rubocop-rspec", "~> 3.10", require: false
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Tim Anema
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 ADDED
@@ -0,0 +1,99 @@
1
+ # StirFry
2
+ A small, simple web framework that lets you enjoy ruby with a JSX type flavour.
3
+
4
+ ## Installation
5
+
6
+ `gem install 'stir_fry'`
7
+
8
+ ## Usage
9
+ An example project could look like this. The templating leverages a weird feature
10
+ of ruby with the `__END__` data sections [ref](https://til.hashrocket.com/posts/17787bf181-rubys-end)
11
+ to inline templates in the same file and be able to parse them where they are.
12
+ I have not yet figured out how to add syntax highlighting for it but it is a start.
13
+
14
+ ### layout.rb
15
+
16
+ ```ruby
17
+ class Layout < StirFry::Component; end
18
+ __END__
19
+ <html>
20
+ <body>{ yield }</body>
21
+ </html>
22
+ ```
23
+
24
+ ### home.rb
25
+
26
+ ```ruby
27
+ class Home < StirFry::Component
28
+ def name = "Bobby"
29
+ end
30
+
31
+ __END__
32
+ <Layout>
33
+ <h1>Hello {name}</h1>
34
+ </Layout>
35
+ ```
36
+
37
+ ### app.rb
38
+
39
+ ```
40
+ require "layout"
41
+ require "home"
42
+
43
+ class App < StirFry::App
44
+ # Render the components
45
+ get "/", Home
46
+ # Raw API call
47
+ get "/hello/:name" do|request:, response:, name:|
48
+ response.text("Hello #{req.args["name"]}")
49
+ end
50
+ end
51
+
52
+ StirFry.run!(App)
53
+ ```
54
+
55
+ See the `/examples` directory for more in-depth examples.
56
+
57
+ ### Multiple Inline templates
58
+ Multiple templates can be defined in the same file as well with an addition of
59
+ an `@@` label. This idea was taken from [Sinatra](#acknowledgment).
60
+
61
+ ```ruby
62
+ class Layout < StirFry::Component; end
63
+ class Home < StirFry::Component; end
64
+
65
+ __END__
66
+ @@Layout
67
+ <html>
68
+ <body>{ yield }</body>
69
+ </html>
70
+
71
+ @@Home
72
+ <Layout><h1>Hello Bobby</h1></Layout>
73
+ ```
74
+
75
+ ## Development
76
+
77
+ After checking out the repo,
78
+
79
+ - run `bundle install` to install dependencies.
80
+ - run `./example/todos_app/app.rb` to run the example todo app.
81
+ - run `./example/chat_app/app.rb` to run the example chat app.
82
+ - run `rake` to run the specs, rubocop and rdoc.
83
+
84
+ ## Further Reading
85
+
86
+ - [Code of Conduct](CODE_OF_CONDUCT.md)
87
+ - [License](https://opensource.org/licenses/MIT)
88
+
89
+ ## Acknowledgment
90
+ These are the projects that I took both inspiration and code chunks from. Since I
91
+ wanted an interface a lot like Sinatra, I used their codebase heavily for routing.
92
+ Also since I wanted a reactjs markdown style, I used rbexy initially but then
93
+ ended up re-writing a lot of it to remove all rails integrations and I ended up
94
+ rewriting to more of a classical recursive descent parser since I find push-down
95
+ automata harder to parse personally and it would be more comfortable for me to work on.
96
+
97
+ - [Sinatra](https://github.com/sinatra/sinatra/)
98
+ - [Rbexy](https://github.com/patbenatar/rbexy)
99
+ - [Rack](https://github.com/rack/rack)
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+ require "rubocop/rake_task"
7
+ require "rdoc/task"
8
+ require "rerun"
9
+
10
+ task default: %i[spec rubocop rdoc:coverage]
11
+
12
+ RuboCop::RakeTask.new
13
+
14
+ RSpec::Core::RakeTask.new(:spec) do |t|
15
+ t.rspec_opts = ["--require spec_helper"]
16
+ t.verbose = ENV.fetch("VERBOSE", nil)
17
+ end
18
+
19
+ RDoc::Task.new do |rdoc|
20
+ rdoc.main = "README.md"
21
+ rdoc.generator = "aliki"
22
+ rdoc.rdoc_files.include(
23
+ "lib/**/*.rb",
24
+ "rbxrb/lib/**/*.rb",
25
+ "CHANGELOG.md",
26
+ "CODE_OF_CONDUCT.md",
27
+ "README.md",
28
+ "LICENSE"
29
+ )
30
+ rdoc.rdoc_dir = "docs"
31
+ rdoc.markup = "markdown"
32
+ end
33
+
34
+ desc "Run an irb console with the library loaded"
35
+ task :console do
36
+ require "stir_fry"
37
+ require "irb"
38
+ ARGV.clear
39
+ IRB.start
40
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "rack/session"
5
+ require "mustermann"
6
+
7
+ module StirFry
8
+ # App is the root of your StirFry app that makes it runnable and ready for use
9
+ # on any rack server
10
+ #
11
+ # Example:
12
+ #
13
+ # ```ruby
14
+ # class App < StirFry::App
15
+ # get "/greet/:name" do |response:, **|
16
+ # response.text("Hello #{name}")
17
+ # end
18
+ # get "/todo", Todos::List
19
+ # end
20
+ #
21
+ # StirFry.run!(App)
22
+ # ```
23
+ class App
24
+ DEFAULT_ERROR_HANDLERS = { # :nodoc: disable in production
25
+ OKAY => ->(**args) { pass_through(status: :ok, **args) },
26
+ Found => ->(**args) { pass_through(status: :found, **args) },
27
+ NotModified => ->(**args) { pass_through(status: :not_modified, **args) },
28
+ NotFound => Pages::NotFoundPage
29
+ }.freeze
30
+
31
+ REQUEST_ENV_KEY = "stir_fry.request" # :nodoc:
32
+ RESPONSE_ENV_KEY = "stir_fry.response" # :nodoc:
33
+
34
+ attr_accessor :request, :response, :env # :nodoc:
35
+
36
+ class << self
37
+ def routes = @routes ||= {} # :nodoc:
38
+ def all_routes = @all_routes ||= [] # :nodoc:
39
+ def error_handlers = @error_handlers ||= DEFAULT_ERROR_HANDLERS.dup # :nodoc:
40
+ def call(env) = root_app.call(env) # :nodoc:
41
+
42
+ # sugar for `use Rack::Session::Cookie, config`
43
+ # See https://github.com/rack/rack-session for more config.
44
+ # Params:
45
+ # - `domain` host for the cookie security (example: 'mywebsite.com')
46
+ # - `path` path for the cookie security (example: '/')
47
+ # - `expire_after` mark the cookie as expired after seconds (example: 3600*24)
48
+ # - `max_age` similar to expires_after.
49
+ # - `secret` *required* a random string 64 character long or longer. Used to encrypt cookie.
50
+ def use_session(**config)
51
+ raise "cannot enable sessions without a :secret set." if config[:secret].nil?
52
+
53
+ secret_len = config[:secret].length
54
+ if secret_len < 64
55
+ raise "session secret too short, secret is #{secret_len} but session secret is required to be >= 64"
56
+ end
57
+
58
+ use Rack::Session::Cookie, config
59
+ end
60
+
61
+ # Add a GET request route to the application
62
+ def get(pattern, klass = nil, &) = route("GET", pattern, klass, &)
63
+
64
+ # Add a HEAD request route to the application
65
+ def head(pattern, klass = nil, &) = route("HEAD", pattern, klass, &)
66
+
67
+ # Add a OPTIONS request route to the application
68
+ def options(pattern, klass = nil, &) = route("OPTIONS", pattern, klass, &)
69
+
70
+ # Add a POST request route to the application
71
+ def post(pattern, klass = nil, &) = route("POST", pattern, klass, &)
72
+
73
+ # Add a PUT request route to the application
74
+ def put(pattern, klass = nil, &) = route("PUT", pattern, klass, &)
75
+
76
+ # Add a PATCH request route to the application
77
+ def patch(pattern, klass = nil, &) = route("PATCH", pattern, klass, &)
78
+
79
+ # Add a DELETE request route to the application
80
+ def delete(pattern, klass = nil, &) = route("DELETE", pattern, klass, &)
81
+
82
+ # running environment for this app.
83
+ def app_env = StirFry.app_env
84
+
85
+ # is the app running in development.
86
+ def development? = app_env == :development
87
+
88
+ # is the app running in test.
89
+ def test? = app_env == :test
90
+
91
+ # is the app running in production
92
+ def production? = app_end == :production
93
+
94
+ # configured logger for the application for easy access in the app
95
+ def logger = StirFry.logger
96
+
97
+ def pass_through(response:, status:, **) # :nodoc:
98
+ response.status(status)
99
+ end
100
+
101
+ ##
102
+ # Add a handler for an error raised during runtime.
103
+ #
104
+ # Example:
105
+ #
106
+ # ```ruby
107
+ # class App < StirFry::App
108
+ # rescue_from StirFry::NotFound do |response:, **|
109
+ # response.text("Not Found", :not_found)
110
+ # end
111
+ # end
112
+ # ```
113
+ def rescue_from(klass, &block) = error_handlers[klass] = block
114
+
115
+ ##
116
+ # Adds a middleware class to the chain of middleware. The order of operations
117
+ # is significant! So if you create routes before you add a middleware, the
118
+ # routes will not include the later middleware. This can be nice to scope
119
+ # your middleware but it can also be an easy mistake.
120
+ def use(middleware_class, *args, &block) = context.last[1] << [middleware_class, args, block]
121
+
122
+ ##
123
+ # Hosts a directory of static files. Every file within the directory will
124
+ # be requestable and sym links are followed so be careful what you host.
125
+ # A prefix can be added to the file routes to allow them to be scoped to a
126
+ # certain endpoint.
127
+ #
128
+ # Example:
129
+ #
130
+ # ```ruby
131
+ # class App < StirFry::App
132
+ # static File.join(__dir__, "public")
133
+ # end
134
+ # ```
135
+ def static(dirname, path: "/")
136
+ files = Rack::Files.new(File.expand_path(dirname))
137
+ path = route_prefix + path
138
+ static_apps << (path == "/" ? files : Rack::URLMap.new(path => files))
139
+ end
140
+
141
+ ##
142
+ # Creates a sub-scope of routes that allow to prefix all the routes and add
143
+ # specific middleware that only applies to those routes.
144
+ #
145
+ # Example:
146
+ #
147
+ # ```ruby
148
+ # class App < StirFry::App
149
+ # scope "/todos" do
150
+ # use AuthenticationMiddleware
151
+ #
152
+ # get "/:id", Todo
153
+ # delete "/:id", Todo
154
+ # post "/:id", Todo
155
+ # end
156
+ # end
157
+ # ```
158
+ def scope(prefix = "", &)
159
+ context.push([prefix, []])
160
+ yield
161
+ ensure
162
+ context.pop
163
+ end
164
+
165
+ ##
166
+ # Raw route builder, used by the other helper models so `get("/")` becomes
167
+ # `route("GET", "/")`
168
+ def route(verb, pattern, klass, &block)
169
+ path_pattern = Mustermann.new(route_prefix + pattern)
170
+ all_routes << path_pattern.to_s unless path_pattern.to_s.start_with?("/stir_fry")
171
+ (routes[verb] ||= []) << [path_pattern, route_handler(klass || block)]
172
+ end
173
+
174
+ protected
175
+
176
+ def setup_default_middleware # :nodoc:
177
+ use Middleware::Logger, logger
178
+ use Rack::Head
179
+ use Rack::ShowStatus if development?
180
+ use Rack::ShowExceptions if development?
181
+ use Rack::ContentLength
182
+ end
183
+
184
+ def setup_framework_routes # :nodoc:
185
+ scope "/stir_fry" do
186
+ static File.join(__dir__, "static")
187
+ get "/routes", Pages::RoutesPage
188
+ end
189
+ end
190
+
191
+ private
192
+
193
+ def static_apps = @static_apps ||= []
194
+ def context = @context ||= [["", []]]
195
+ def route_prefix = context.map(&:first).join
196
+ def route_handler(handler) = build_rack_app(context[1..].flat_map(&:last), wrap_handler(handler))
197
+
198
+ def root_app
199
+ @root_app ||= begin
200
+ builder = Rack::Builder.new
201
+ context[0][1].each { |c, a, b| builder.use(c, *a, &b) }
202
+ builder.run(Rack::Cascade.new(static_apps + [->(env) { new(env).call }]))
203
+ builder.to_app
204
+ end
205
+ end
206
+
207
+ def wrap_handler(handler)
208
+ lambda do |env|
209
+ req = env.fetch(REQUEST_ENV_KEY)
210
+ resp = env.fetch(RESPONSE_ENV_KEY)
211
+ verb = req.request_method.downcase.to_sym
212
+ handler = handler.method(verb) if handler.respond_to?(verb)
213
+ call_params = req.args.to_h { |k, v| [k.to_sym, v] }
214
+ default_handler_params = { application: self, request: req, response: resp }
215
+ handler.call(**call_params, **default_handler_params)
216
+ resp.finish
217
+ end
218
+ end
219
+
220
+ def build_rack_app(middleware, handler)
221
+ builder = Rack::Builder.new
222
+ middleware.each { |c, a, b| builder.use(c, *a, &b) }
223
+ builder.run(handler)
224
+ builder.to_app
225
+ end
226
+ end
227
+
228
+ # Hook into the app to define development routes and default middleware
229
+ def self.inherited(subclass)
230
+ super
231
+ subclass.class_eval do
232
+ subclass.setup_default_middleware
233
+ subclass.setup_framework_routes if subclass.development?
234
+ end
235
+ end
236
+
237
+ def initialize(env) # :nodoc:
238
+ @env = env
239
+ @request = Request.new(env)
240
+ @response = Response.new(env)
241
+ env[REQUEST_ENV_KEY] = request
242
+ env[RESPONSE_ENV_KEY] = response
243
+ end
244
+
245
+ # running environment for this app.
246
+ def app_env = self.class.app_env
247
+ # is the app running in development.
248
+ def development? = self.class.development?
249
+ # is the app running in test.
250
+ def test? = self.class.test?
251
+ # is the app running in production
252
+ def production? = self.class.production?
253
+ # configured logger for the application for easy access in the app
254
+ def logger = self.class.logger
255
+
256
+ def call # :nodoc:
257
+ find_route.call(env)
258
+ rescue StandardError => e
259
+ handle_error(e)
260
+ end
261
+
262
+ def all_routes # :nodoc:
263
+ self.class.all_routes
264
+ end
265
+
266
+ private
267
+
268
+ def handle_error(err)
269
+ response.request_error = err
270
+ handler = error_handler_for(err.class)
271
+ handler.call(error: err, application: self, request: request, response: response)
272
+ response.finish
273
+ end
274
+
275
+ def error_handler_for(err_class)
276
+ error_handlers = self.class.error_handlers
277
+ err_class.ancestors.each { |klass| return error_handlers[klass] if error_handlers.key?(klass) }
278
+ Pages::ErrorPage
279
+ end
280
+
281
+ def find_route
282
+ self.class.routes[request.request_method]&.each do |route|
283
+ params = route[0].params(request.path_info)
284
+ request.args = params if params
285
+ return route[1] if params
286
+ end
287
+ raise NotFound
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StirFry
4
+ # Component is a request/response view handler. It can be a plain class with a
5
+ # template inlined in the same file but it can also have the request class methods
6
+ # such as `self.get`, `self.post`, `self.delete` ect. so that specific requests
7
+ # can have more rich handling and different views can be returned.
8
+ #
9
+ # Example:
10
+ #
11
+ #
12
+ # ```ruby
13
+ # module Todos
14
+ # class Todo < StirFry::Component
15
+ # attr_reader :todo
16
+ #
17
+ # class << self
18
+ # # PUT /todo/:id/toggle
19
+ # def put(request:, response:, id:)
20
+ # todo = ::App.todos.find { |t| t["id"] == id.to_i }
21
+ # todo["done"] = !todo["done"]
22
+ # response.ok(new(todo: todo)) # Return a Todo view
23
+ # end
24
+ #
25
+ # # DELETE /todo/:id
26
+ # def delete(request:, response:, id:)
27
+ # ::App.todos.delete_if { |t| t["id"] == id.to_i }
28
+ # response.render(List.new(todos: ::App.todos)) # Return a list view instead
29
+ # end
30
+ # end
31
+ #
32
+ # def initialize(todo:)
33
+ # super()
34
+ # @todo = todo
35
+ # end
36
+ # end
37
+ # end
38
+ # ```
39
+ class Component
40
+ include RBX::Component
41
+
42
+ # application is root application where all the routes are defined and maybe
43
+ # where you might keep config or database references.
44
+ # This is only defined on the root component that is defined on the route.
45
+ attr_accessor :application
46
+ # request is the request that called this component. This is only defined on the root
47
+ # component that is defined on the route.
48
+ attr_accessor :request
49
+ # response is the response that is used for responding in the component.
50
+ # This is only defined on the root component that is defined on the route.
51
+ attr_accessor :response
52
+
53
+ # Simple access to StirFry.logger
54
+ def self.logger = StirFry.logger
55
+
56
+ # call it the method called from routing. It is called with the request and response
57
+ # first, and the arguments to initialize the component will be appended to the
58
+ # end. It will set the request, and response on the component and call render
59
+ # on the component.
60
+ def self.call(application: nil, request: nil, response: nil, **args)
61
+ new(application: application, request: request, response: response, **args).call
62
+ end
63
+
64
+ # create a new component with a request and response
65
+ def initialize(application: nil, request: nil, response: nil, **_args)
66
+ self.application = application
67
+ self.request = request
68
+ self.response = response
69
+ end
70
+
71
+ # call is sugar for `response.html(render)` and should be overriden to change how
72
+ # a component responds.
73
+ def call
74
+ response.html(render)
75
+ end
76
+
77
+ # Simple access to StirFry.logger
78
+ def logger = StirFry.logger
79
+ end
80
+ end