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 +7 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +16 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/Rakefile +40 -0
- data/lib/stir_fry/app.rb +290 -0
- data/lib/stir_fry/component.rb +80 -0
- data/lib/stir_fry/middleware/logger.rb +88 -0
- data/lib/stir_fry/middleware.rb +8 -0
- data/lib/stir_fry/pages/error_page.rb +68 -0
- data/lib/stir_fry/pages/layout.rb +15 -0
- data/lib/stir_fry/pages/not_found_page.rb +31 -0
- data/lib/stir_fry/pages/routes_page.rb +21 -0
- data/lib/stir_fry/pages.rb +10 -0
- data/lib/stir_fry/request.rb +43 -0
- data/lib/stir_fry/response.rb +351 -0
- data/lib/stir_fry/static/stir_fry.css +64 -0
- data/lib/stir_fry/version.rb +6 -0
- data/lib/stir_fry.rb +180 -0
- data/stir_fry.gemspec +45 -0
- metadata +180 -0
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
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
data/lib/stir_fry/app.rb
ADDED
|
@@ -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
|