Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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", "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
5 changes: 3 additions & 2 deletions bake/utopia/static.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ def generate(output_path: "static")
require "async/io"
require "async/http/endpoint"
require "async/container"
require "utopia/application"

config_path = File.join(Dir.pwd, "config.ru")
application_path = File.join(Dir.pwd, Utopia::Application::CONFIGURATION_PATH)
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
Async do
Expand Down
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
6 changes: 3 additions & 3 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,7 +32,7 @@ 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"
Expand Down Expand Up @@ -84,7 +84,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
4 changes: 2 additions & 2 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
3 changes: 0 additions & 3 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 @@ -40,6 +39,4 @@
gem "bake-test-external"

gem "benchmark-ips"

gem "rack-test"
end
6 changes: 3 additions & 3 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,7 +32,7 @@ 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"
Expand Down Expand Up @@ -84,7 +84,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
4 changes: 2 additions & 2 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
3 changes: 2 additions & 1 deletion lib/utopia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

require_relative "utopia/version"

require_relative "utopia/application"
require_relative "utopia/import_map"
require_relative "utopia/content"
require_relative "utopia/controller"
require_relative "utopia/exceptions"
require_relative "utopia/redirection"
require_relative "utopia/static"

# Utopia is a web application framework built on top of Rack.
# Utopia is a web application framework.
module Utopia
end
89 changes: 89 additions & 0 deletions lib/utopia/application.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "protocol/http/middleware"
require "protocol/http/middleware/builder"

require_relative "request"
require_relative "response"

module Utopia
# The protocol-facing entrypoint for a Utopia application.
#
# This object accepts {Protocol::HTTP::Request} instances, wraps them in a
# {Utopia::Request}, dispatches to the Utopia application stack, and normalizes
# the result back to a {Protocol::HTTP::Response}.
class Application < Protocol::HTTP::Middleware
CONFIGURATION_PATH = "config/application.rb".freeze

# Build a Utopia application stack using the protocol HTTP middleware builder.
# @parameter default_app [Interface(:call)] The terminal application used when the block does not call `run`.
# @parameter block [Proc] The middleware builder block.
# @returns [Application] The protocol-facing Utopia application.
def self.build(default_app = Response::NotFound, &block)
builder = Protocol::HTTP::Middleware::Builder.new(default_app)

if block
if block.arity.zero?
builder.instance_exec(&block)
else
block.call(builder)
end
end

return self.new(builder.to_app)
end

# Build the default Utopia application.
# @returns [Application] The default protocol-facing Utopia application.
def self.default
self.build
end

# Load a Utopia application from a conventional configuration file.
#
# If the file defines an `Application` constant, it will be returned
# directly. If the constant is a class, it will be instantiated.
# If the file does not exist, or does not define `Application`, the default
# application is returned.
#
# @parameter path [String] The application configuration path.
# @parameter options [Hash] Options passed to the application constructor.
# @returns [Interface(:call)] The loaded protocol-facing application.
def self.load(path = CONFIGURATION_PATH, **options)
if File.exist?(path)
top = Module.new
top.class_eval(File.read(path), path)

if top.const_defined?(:Application, false)
application = top.const_get(:Application)

if application.is_a?(Class)
return application.new(**options)
else
return application
end
end
end

return self.default
end

# Initialize the protocol-facing application boundary.
# @parameter delegate [Interface(:call)] The Utopia application stack.
def initialize(delegate)
super(delegate)
end

# Process a protocol HTTP request.
# @parameter http_request [Protocol::HTTP::Request] The incoming protocol request.
# @returns [Protocol::HTTP::Response] The normalized protocol response.
def call(http_request)
request = Request.new(http_request)

return Response.wrap(super(request))
end
end
end
4 changes: 2 additions & 2 deletions lib/utopia/content/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(request, attributes = {})

# @returns [Path] The original request path, if known.
def request_path
Path[request.env["REQUEST_PATH"]]
Path[request.attributes["REQUEST_PATH"]]
end

protected def current_base_uri_path
Expand Down Expand Up @@ -87,7 +87,7 @@ def parse_markup(markup)
MarkupParser.parse(markup, self)
end

# The Rack::Request for this document.
# The request for this document.
attr :request

# Per-document global attributes.
Expand Down
14 changes: 7 additions & 7 deletions lib/utopia/content/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require_relative "../middleware"
require_relative "../localization"
require_relative "../response"

require_relative "links"
require_relative "node"
Expand Down Expand Up @@ -92,16 +93,15 @@ def resolve_link(link)

def respond(link, request)
if node = resolve_link(link)
attributes = request.env.fetch(VARIABLES_KEY, {}).to_hash
attributes = request.fetch(VARIABLES_KEY, {}).to_hash

return node.process!(request, attributes)
elsif redirect_uri = link[:uri]
return [307, {HTTP::LOCATION => redirect_uri}, []]
return Utopia::Response[307, {HTTP::LOCATION => redirect_uri}, []]
end
end

def call(env)
request = Rack::Request.new(env)
def call(request)
path = Path.create(request.path_info)

# Check if the request is to a non-specific index. This only works for requests with a given name:
Expand All @@ -112,17 +112,17 @@ def call(env)
if File.directory? directory_path
index_path = [basename, INDEX]

return [307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
return Utopia::Response[307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
end

locale = env[Localization::CURRENT_LOCALE_KEY]
locale = request[Localization::CURRENT_LOCALE_KEY]
if link = @links.for(path, locale)
if response = self.respond(link, request)
return response
end
end

return @app.call(env)
return @app.call(request)
end

private
Expand Down
2 changes: 1 addition & 1 deletion lib/utopia/content/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def call(document, state)
end

def process!(request, attributes = {})
Document.render(self, request, attributes).to_a
Document.render(self, request, attributes).to_protocol_response
end

# This is a special context in which a limited set of well defined methods are exposed in the content view.
Expand Down
7 changes: 4 additions & 3 deletions lib/utopia/content/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# Released under the MIT License.
# Copyright, 2010-2025, by Samuel Williams.

require_relative "../response"

module Utopia
module Content
# Compatibility with older versions of rack:
EXPIRES = "expires".freeze
CACHE_CONTROL = "cache-control".freeze
CONTENT_TYPE = "content-type".freeze
Expand Down Expand Up @@ -34,8 +35,8 @@ def lookup(tag)
return nil
end

def to_a
[@status, @headers, @body]
def to_protocol_response
Utopia::Response[@status, @headers, @body]
end

# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
Expand Down
Loading
Loading