Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7507d70
Add protocol HTTP application boundary.
samuel-williams-shopify Aug 2, 2026
77ed7fd
Fix localized failure response handling.
samuel-williams-shopify Aug 2, 2026
b611241
Use protocol HTTP status descriptions.
samuel-williams-shopify Aug 2, 2026
e8b106e
Expand conditional branches.
samuel-williams-shopify Aug 2, 2026
dfba2e0
Decode request form arguments.
samuel-williams-shopify Aug 2, 2026
aa46c6b
Integrate protocol multipart form data.
samuel-williams-shopify Aug 3, 2026
edbbd12
Use released form data parsers.
samuel-williams-shopify Aug 3, 2026
4ad2a91
Update form data parser dependencies.
samuel-williams-shopify Aug 5, 2026
410a5d3
Parse request bodies in controllers.
samuel-williams-shopify Aug 5, 2026
857b80a
Add end-to-end parameter parsing tests.
samuel-williams-shopify Aug 8, 2026
b514d04
Delegate middleware shutdown.
samuel-williams-shopify Aug 8, 2026
eca99e1
Fix request parsing guide example.
samuel-williams-shopify Aug 8, 2026
f6152f6
Show complete application configuration.
samuel-williams-shopify Aug 8, 2026
7a01a41
Fix default application options.
samuel-williams-shopify Aug 8, 2026
820295f
Generate Falcon serve configuration.
samuel-williams-shopify Aug 9, 2026
7ea9ae4
Use protocol middleware for static generation.
samuel-williams-shopify Aug 9, 2026
c6d3b42
Use application terminology consistently.
samuel-williams-shopify Aug 9, 2026
7723649
Document protocol middleware application contracts.
samuel-williams-shopify Aug 9, 2026
dd94eba
Fix static conditional requests.
samuel-williams-shopify Aug 9, 2026
d1a22eb
Use weak static resource validators.
samuel-williams-shopify Aug 9, 2026
cc9bb1e
Revise localization resolution.
samuel-williams-shopify Aug 9, 2026
26858f5
Close replaced error responses.
samuel-williams-shopify Aug 9, 2026
cb4c135
Use protocol middleware terminals in tests.
samuel-williams-shopify Aug 9, 2026
d689e15
Normalize controller middleware responses.
samuel-williams-shopify Aug 9, 2026
65a229b
Simplify controller response negotiation.
samuel-williams-shopify Aug 9, 2026
18781a5
Make session cookie options explicit.
samuel-williams-shopify Aug 9, 2026
8e4caea
Use protocol HTTP test fixtures.
samuel-williams-shopify Aug 9, 2026
2fdde80
Generalize static site generation.
samuel-williams-shopify Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.body -text
2 changes: 1 addition & 1 deletion bake/utopia/site.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def initialize(...)
SETUP_ROOT = File.expand_path("../../setup", __dir__)

# Configuration files which should be installed/updated:
CONFIGURATION_FILES = [".gitignore", "config.ru", "config/environment.rb", "falcon.rb", "gems.rb", "bake.rb", "test/website.rb", "fixtures/website.rb"]
CONFIGURATION_FILES = [".gitignore", "config/application.rb", "config/environment.rb", "config/serve.rb", "falcon.rb", "gems.rb", "bake.rb", "test/website.rb", "fixtures/website.rb"]

# Directories that should exist:
DIRECTORIES = ["config", "lib", "pages", "public", "bake", "fixtures", "test"]
Expand Down
39 changes: 26 additions & 13 deletions bake/utopia/static.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.

def generate(output_path: "static")
# Generate a static copy of the application.
# @parameter output_path [String] The output path for the generated site.
# @parameter application_path [String] The application configuration path.
# @parameter public_path [String] The public assets path.
# @parameter force [Boolean] Remove the output directory before generating the site.
def generate(output_path: "static", application_path: "config/application.rb", public_path: "public", force: true)
require "falcon/server"
require "async/io"
require "async/http/endpoint"
require "async/container"
require "fileutils"
require "utopia/application"

config_path = File.join(Dir.pwd, "config.ru")
application_path = File.expand_path(application_path, Dir.pwd)
public_path = File.expand_path(public_path, Dir.pwd)
container_class = Async::Container::Threaded
server_port = 9090

app, options = Rack::Builder.parse_file(config_path)
app = Utopia::Application.load(application_path)

container = container_class.run(count: 2) do
container = container_class.run(count: 1) do
Async do
server = Falcon::Server.new(
Falcon::Server.middleware(app),
Falcon::Server.protocol_middleware(app),
Async::HTTP::Endpoint.parse("http://localhost:#{server_port}")
)

Expand All @@ -28,17 +35,23 @@ def generate(output_path: "static")

output_path = File.expand_path(output_path, Dir.pwd)

# Delete any existing stuff:
FileUtils.rm_rf(output_path)
# Delete existing output when explicitly requested:
if force
FileUtils.rm_rf(output_path)
end

# Copy all public assets:
FileUtils::Verbose.mkpath(output_path)
Dir.glob(File.join(Dir.pwd, "public/*")) do |path|
Dir.glob(File.join(public_path, "*")) do |path|
FileUtils::Verbose.cp_r(path, output_path)
end

# Generate HTML pages:
system("wget", "--mirror", "--recursive", "--continue", "--convert-links", "--adjust-extension", "--no-host-directories", "--directory-prefix", output_path.to_s, "http://localhost:#{server_port}")

container.stop
begin
# Generate HTML pages:
unless system("wget", "--mirror", "--recursive", "--continue", "--convert-links", "--adjust-extension", "--no-host-directories", "--directory-prefix", output_path.to_s, "http://localhost:#{server_port}")
raise "Static site generation failed!"
end
ensure
container.stop
end
end
4 changes: 1 addition & 3 deletions config/external.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
utopia-project:
url: https://github.com/socketry/utopia-project.git
command: bundle exec bake test
www.codeotaku.com:
url: https://github.com/ioquatix/www.codeotaku.com.git
branch: v3-protocol-application
command: bundle exec bake test
12 changes: 7 additions & 5 deletions context/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This guide explains how to set up a `utopia` website for local development and d

## Installation

Utopia is built on Ruby and Rack. Therefore, Ruby (suggested 2.0+) should be installed and working. Then, to install `utopia` and all required dependencies, run:
Utopia is built on Ruby. Therefore, Ruby should be installed and working. Then, to install `utopia` and all required dependencies, run:

~~~ bash
$ gem install utopia
Expand Down Expand Up @@ -32,10 +32,12 @@ You will now have a basic template site running on `https://localhost:9292`.
Utopia includes a redirection middleware to redirect all root-level requests to a given URI. The default being `/welcome/index`:

```ruby
# in config.ru
# in config/application.rb

use Utopia::Redirection::Rewrite,
"/" => "/welcome/index"
Application = Utopia::Application.build do
use Utopia::Redirection::Rewrite,
"/" => "/welcome/index"
end
```

The content for this page is stored in `pages/welcome/index.xnode`. The format of this page is a subset of HTML5 - open and close tags are strictly enforced.
Expand Down Expand Up @@ -84,7 +86,7 @@ website

Least Coverage:
pages/_page.xnode: 6 lines not executed!
config.ru: 4 lines not executed!
config/application.rb: 4 lines not executed!
pages/welcome/index.xnode: 2 lines not executed!
pages/_heading.xnode: 1 lines not executed!

Expand Down
2 changes: 1 addition & 1 deletion context/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ files:
and deployment.
- path: middleware.md
title: Middleware
description: This guide gives an overview of the different Rack middleware used
description: This guide gives an overview of the different middleware used
by Utopia.
- path: server-setup.md
title: Server Setup
Expand Down
18 changes: 12 additions & 6 deletions context/middleware.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Middleware

This guide gives an overview of the different Rack middleware used by Utopia.
This guide gives an overview of the different middleware used by Utopia.

## Static

The {ruby Utopia::Static} middleware services static files efficiently. By default, it works with `Rack::Sendfile` and supports `ETag` based caching. Normally, you'd prefer to put static files into `public/_static` but it's also acceptable to put static content into `pages/` if it makes sense.
The {ruby Utopia::Static} middleware services static files efficiently and supports `ETag` based caching. Normally, you'd prefer to put static files into `public/_static` but it's also acceptable to put static content into `pages/` if it makes sense.

~~~ ruby
use Utopia::Static,
Expand Down Expand Up @@ -36,7 +36,7 @@ use Utopia::Redirection::Errors,

## Localization

The {ruby Utopia::Localization} middleware provides non-intrusive localization on top of the controller and view layers. The middleware uses the `accept-language` header to guess the preferred locale out of the given options. If a request path maps to a resource, that resource is returned. Otherwise, a non-localized request is made.
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.

~~~ ruby
use Utopia::Localization,
Expand All @@ -53,7 +53,7 @@ pages/index.ja.xnode
pages/index.zh.xnode
~~~

You can also access the current locale in the view via {ruby Utopia::Content::Node::Context#localization}.
You can access the selected locale in a view using `localization.locale`. Controllers can inspect the request preferences using `request.localization`.

## Controller

Expand Down Expand Up @@ -90,7 +90,7 @@ def passthrough(request, path)

# Succeed the request and immediately respond.
# def succeed!(status: 200, headers: {}, **options)
# options may include content: string or body: Enumerable (as per Rack specifications
# options may include content: String or body: Enumerable.

suceed!
end
Expand All @@ -108,7 +108,7 @@ end

on "edit" do |request, path|
if request.post?
@user.update_attributes(request[:user])
@user.update_attributes(parse_body(request)["user"])
end
end

Expand Down Expand Up @@ -155,3 +155,9 @@ use Utopia::Session,
```

All session data is stored on the client, but it's encrypted with a salt and the secret key. It is impossible for the client to decrypt the data without the secret stored on the server.

When the middleware is installed, the session is available on the request:

```ruby
request.session[:user_id] = user.id
```
14 changes: 0 additions & 14 deletions fixtures/a_rack_application.rb

This file was deleted.

6 changes: 2 additions & 4 deletions gems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

group :development do
gem "json"
gem "rackula"
end

group :test do
Expand All @@ -32,14 +31,13 @@
gem "rubocop-md"
gem "rubocop-socketry"

gem "falcon"
gem "falcon", "~> 0.57"
gem "async-websocket"
gem "sus-fixtures-async-http"
gem "sus-fixtures-protocol-http", "~> 0.1"

gem "bake-test"
gem "bake-test-external"

gem "benchmark-ips"

gem "rack-test"
end
12 changes: 7 additions & 5 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This guide explains how to set up a `utopia` website for local development and d

## Installation

Utopia is built on Ruby and Rack. Therefore, Ruby (suggested 2.0+) should be installed and working. Then, to install `utopia` and all required dependencies, run:
Utopia is built on Ruby. Therefore, Ruby should be installed and working. Then, to install `utopia` and all required dependencies, run:

~~~ bash
$ gem install utopia
Expand Down Expand Up @@ -32,10 +32,12 @@ You will now have a basic template site running on `https://localhost:9292`.
Utopia includes a redirection middleware to redirect all root-level requests to a given URI. The default being `/welcome/index`:

```ruby
# in config.ru
# in config/application.rb

use Utopia::Redirection::Rewrite,
"/" => "/welcome/index"
Application = Utopia::Application.build do
use Utopia::Redirection::Rewrite,
"/" => "/welcome/index"
end
```

The content for this page is stored in `pages/welcome/index.xnode`. The format of this page is a subset of HTML5 - open and close tags are strictly enforced.
Expand Down Expand Up @@ -84,7 +86,7 @@ website

Least Coverage:
pages/_page.xnode: 6 lines not executed!
config.ru: 4 lines not executed!
config/application.rb: 4 lines not executed!
pages/welcome/index.xnode: 2 lines not executed!
pages/_heading.xnode: 1 lines not executed!

Expand Down
47 changes: 36 additions & 11 deletions guides/middleware/readme.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Middleware

This guide gives an overview of the different Rack middleware used by Utopia.
This guide gives an overview of the different middleware used by Utopia.

## Static

The {ruby Utopia::Static} middleware services static files efficiently. By default, it works with `Rack::Sendfile` and supports `ETag` based caching. Normally, you'd prefer to put static files into `public/_static` but it's also acceptable to put static content into `pages/` if it makes sense.
The {ruby Utopia::Static} middleware services static files efficiently and supports `ETag` based caching. Normally, you'd prefer to put static files into `public/_static` but it's also acceptable to put static content into `pages/` if it makes sense.

~~~ ruby
use Utopia::Static,
Expand Down Expand Up @@ -36,7 +36,7 @@ use Utopia::Redirection::Errors,

## Localization

The {ruby Utopia::Localization} middleware provides non-intrusive localization on top of the controller and view layers. The middleware uses the `accept-language` header to guess the preferred locale out of the given options. If a request path maps to a resource, that resource is returned. Otherwise, a non-localized request is made.
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.

~~~ ruby
use Utopia::Localization,
Expand All @@ -53,7 +53,7 @@ pages/index.ja.xnode
pages/index.zh.xnode
~~~

You can also access the current locale in the view via {ruby Utopia::Content::Node::Context#localization}.
You can access the selected locale in a view using `localization.locale`. Controllers can inspect the request preferences using `request.localization`.

## Controller

Expand All @@ -73,7 +73,7 @@ A controller is a file within the specified root directory (typically `pages`) w
def passthrough(request, path)
# Call one of:

# This will cause the middleware to generate a response.
# Respond immediately with a complete Protocol::HTTP::Response.
# def respond!(response)

# This will cause the controller to skip the request.
Expand All @@ -85,17 +85,36 @@ def passthrough(request, path)
# Controller relative redirect.
# def goto!(target, status = 302)

# Respond with an error which indiciates some kind of failure.
# Respond with an error which indicates some kind of failure.
# def fail!(error = 400, message = nil)

# Succeed the request and immediately respond.
# def succeed!(status: 200, headers: {}, **options)
# options may include content: string or body: Enumerable (as per Rack specifications
# Succeed with a semantic value which the Respond layer serializes.
# def succeed!(value = nil, status: 200, headers: {})

suceed!
succeed!
end
```

Controllers which return semantic values should prepend {ruby Utopia::Controller::Respond} and configure serializers. Serializer blocks return wire-ready response bodies, so they can return streaming {ruby Protocol::HTTP::Body::Readable} objects without buffering them.

```ruby
prepend Utopia::Controller::Respond, Utopia::Controller::Actions

responds.with("application/json") do |media_range, value|
JSON.dump(value)
end

on "show" do
succeed!({"name" => "Samuel"})
end
```

Use `respond!` when a controller has already constructed a complete response and no content negotiation is required:

```ruby
respond! Utopia::Response[200, {"content-type" => "text/plain"}, ["Hello World"]]
```

The controller layer can do more complex operations by prepending modules into it.

```ruby
Expand All @@ -108,7 +127,7 @@ end

on "edit" do |request, path|
if request.post?
@user.update_attributes(request[:user])
@user.update_attributes(parse_body(request)["user"])
end
end

Expand Down Expand Up @@ -155,3 +174,9 @@ use Utopia::Session,
```

All session data is stored on the client, but it's encrypted with a salt and the secret key. It is impossible for the client to decrypt the data without the secret stored on the server.

When the middleware is installed, the session is available on the request:

```ruby
request.session[:user_id] = user.id
```
1 change: 1 addition & 0 deletions lib/utopia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require_relative "utopia/version"

require_relative "utopia/application"
require_relative "utopia/import_map"
require_relative "utopia/content"
require_relative "utopia/controller"
Expand Down
Loading
Loading