diff --git a/context/cluster-deployment.md b/context/cluster-deployment.md index 6952c244..fad58aff 100644 --- a/context/cluster-deployment.md +++ b/context/cluster-deployment.md @@ -84,7 +84,7 @@ service "cluster" do }, [body]] end - Falcon::Server.middleware(application, cache: false) + Falcon::Server.rack_middleware(application, cache: false) end end diff --git a/context/getting-started.md b/context/getting-started.md index ee200a32..abbb6d4f 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -44,6 +44,23 @@ Then run the application with: $ falcon serve ~~~ +#### Rack Applications Defined in Ruby Files + +Rack can load a Ruby file directly and infer the application constant from its filename. For example, `Rack::Builder.parse_file("app.rb")` requires the file and uses `::App` as the Rack application. + +Falcon reserves `.rb` serve configurations for protocol HTTP middleware. Existing Rack applications can be exposed through `config/serve.rb` using {ruby Protocol::Rack::Adapter}: + +~~~ ruby +# config/serve.rb + +require "protocol/rack" +require_relative "../app" + +run Protocol::Rack::Adapter.new(App) +~~~ + +Running `falcon serve` loads `config/serve.rb` as protocol HTTP middleware, while the adapter translates requests and responses for the existing Rack application. This replaces the older `falcon serve --config app.rb` convention without requiring changes to `App` itself. + ## Running a Local Server For local application development, you can use the `falcon serve` command. This will start a local server on `https://localhost:9292`. Falcon generates self-signed certificates for `localhost`. This allows you to test your application with HTTPS locally. diff --git a/examples/cluster/falcon.rb b/examples/cluster/falcon.rb index 57e17a98..dc650311 100644 --- a/examples/cluster/falcon.rb +++ b/examples/cluster/falcon.rb @@ -34,7 +34,7 @@ def url ] end - Falcon::Server.middleware(rack_application, cache: false) + Falcon::Server.rack_middleware(rack_application, cache: false) end end diff --git a/examples/server/standalone.rb b/examples/server/standalone.rb index 95b4d1e0..71546bf8 100644 --- a/examples/server/standalone.rb +++ b/examples/server/standalone.rb @@ -23,7 +23,7 @@ def self.call(env) Async do websocket_endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:3000") - app = Falcon::Server.middleware(WebSocketApp) + app = Falcon::Server.rack_middleware(WebSocketApp) server = Falcon::Server.new(app, websocket_endpoint) diff --git a/examples/utilization/falcon.rb b/examples/utilization/falcon.rb index 69c1ab22..78ae81d0 100644 --- a/examples/utilization/falcon.rb +++ b/examples/utilization/falcon.rb @@ -30,7 +30,7 @@ def call(env) # Define the middleware stack for this server middleware do - Falcon::Server.middleware(SimpleApp.new, verbose: false, cache: false) + Falcon::Server.rack_middleware(SimpleApp.new, verbose: false, cache: false) end # Define the utilization schema for this service diff --git a/guides/cluster-deployment/readme.md b/guides/cluster-deployment/readme.md index 6952c244..fad58aff 100644 --- a/guides/cluster-deployment/readme.md +++ b/guides/cluster-deployment/readme.md @@ -84,7 +84,7 @@ service "cluster" do }, [body]] end - Falcon::Server.middleware(application, cache: false) + Falcon::Server.rack_middleware(application, cache: false) end end diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index ee200a32..abbb6d4f 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -44,6 +44,23 @@ Then run the application with: $ falcon serve ~~~ +#### Rack Applications Defined in Ruby Files + +Rack can load a Ruby file directly and infer the application constant from its filename. For example, `Rack::Builder.parse_file("app.rb")` requires the file and uses `::App` as the Rack application. + +Falcon reserves `.rb` serve configurations for protocol HTTP middleware. Existing Rack applications can be exposed through `config/serve.rb` using {ruby Protocol::Rack::Adapter}: + +~~~ ruby +# config/serve.rb + +require "protocol/rack" +require_relative "../app" + +run Protocol::Rack::Adapter.new(App) +~~~ + +Running `falcon serve` loads `config/serve.rb` as protocol HTTP middleware, while the adapter translates requests and responses for the existing Rack application. This replaces the older `falcon serve --config app.rb` convention without requiring changes to `App` itself. + ## Running a Local Server For local application development, you can use the `falcon serve` command. This will start a local server on `https://localhost:9292`. Falcon generates self-signed certificates for `localhost`. This allows you to test your application with HTTPS locally. diff --git a/lib/falcon/command/serve.rb b/lib/falcon/command/serve.rb index 06f320ca..7b3730a2 100644 --- a/lib/falcon/command/serve.rb +++ b/lib/falcon/command/serve.rb @@ -8,7 +8,7 @@ require_relative "../endpoint" require_relative "../service/server" require_relative "../environment/server" -require_relative "../environment/rackup" +require_relative "../environment/serve" require "async/service/configuration" require "async/container" @@ -32,7 +32,7 @@ class Serve < Samovar::Command option "-h/--hostname ", "Specify the hostname which would be used for certificates, etc." option "-t/--timeout ", "Specify the maximum time to wait for non-blocking operations.", type: Float, default: nil - option "-c/--config ", "Rackup configuration file to load.", default: "config.ru" + option "-c/--config ", "Application configuration file to load." option "--preload ", "Preload the specified path before creating containers." option "--cache", "Enable the response cache." @@ -72,7 +72,7 @@ def name # @returns [Async::Service::Environment] The configured server environment. def environment Async::Service::Environment.new(Falcon::Environment::Server).with( - Falcon::Environment::Rackup, + Falcon::Environment::Serve, root: Dir.pwd, verbose: self.parent&.verbose?, @@ -81,7 +81,7 @@ def environment container_options: self.container_options, endpoint_options: self.endpoint_options, - rackup_path: @options[:config], + configuration_path: @options[:config], preload: [@options[:preload]].compact, url: @options[:bind], diff --git a/lib/falcon/environment/rackup.rb b/lib/falcon/environment/rackup.rb index 1a767315..184cad60 100644 --- a/lib/falcon/environment/rackup.rb +++ b/lib/falcon/environment/rackup.rb @@ -25,7 +25,7 @@ def rack_app # Build the middleware stack for the rack application. # @returns [Protocol::HTTP::Middleware] The middleware stack. def middleware - ::Falcon::Server.middleware(rack_app, verbose: verbose, cache: cache) + ::Falcon::Server.rack_middleware(rack_app, verbose: verbose, cache: cache) end end end diff --git a/lib/falcon/environment/serve.rb b/lib/falcon/environment/serve.rb new file mode 100644 index 00000000..962cf481 --- /dev/null +++ b/lib/falcon/environment/serve.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "protocol/http/middleware/builder" +require "protocol/rack" + +require_relative "../server" + +module Falcon + module Environment + # Provides application configuration discovery and loading for `falcon serve`. + # + # When {#configuration_path} is `nil`, {#resolved_configuration_path} looks for + # `config/serve.rb` first and falls back to `config.ru`. An explicit + # {#configuration_path} bypasses this discovery order. + # + # The file extension selects the application interface: + # + # - `.rb` files are evaluated by {Protocol::HTTP::Middleware.load} using the + # protocol HTTP middleware builder interface. + # - `.ru` files are parsed as Rack applications and wrapped with + # {Protocol::Rack::Adapter}. + # + # Both application interfaces respond to `call`, so the configuration file + # extension is the explicit contract rather than inspecting the loaded object. + module Serve + # The explicitly specified application configuration path, if any. + # @returns [String | Nil] + def configuration_path + nil + end + + # Resolve the application configuration path. + # @returns [String] The absolute application configuration path. + def resolved_configuration_path + if configuration_path + return File.expand_path(configuration_path, root) + end + + serve_path = File.expand_path("config/serve.rb", root) + if File.file?(serve_path) + return serve_path + end + + rackup_path = File.expand_path("config.ru", root) + if File.file?(rackup_path) + return rackup_path + end + + raise ArgumentError, "Could not find config/serve.rb or config.ru in #{root}!" + end + + # Load and wrap the configured application. + # @returns [Protocol::HTTP::Middleware] The middleware stack. + def middleware + path = resolved_configuration_path + + case File.extname(path) + when ".rb" + application = ::Protocol::HTTP::Middleware.load(path) + return ::Falcon::Server.protocol_middleware(application, verbose: verbose, cache: cache) + when ".ru" + application = ::Protocol::Rack::Adapter.parse_file(path) + return ::Falcon::Server.rack_middleware(application, verbose: verbose, cache: cache) + else + raise ArgumentError, "Unsupported application configuration: #{path}!" + end + end + end + end +end diff --git a/lib/falcon/server.rb b/lib/falcon/server.rb index 5dca1040..b67dc2f9 100644 --- a/lib/falcon/server.rb +++ b/lib/falcon/server.rb @@ -17,11 +17,26 @@ module Falcon # A server listening on a specific endpoint, hosting a specific middleware. class Server < Async::HTTP::Server - # Wrap a rack application into a middleware suitable the server. + # @deprecated Use {rack_middleware} instead. + def self.middleware(...) + warn("`Falcon::Server.middleware` is deprecated, use `.rack_middleware` instead.", uplevel: 1, category: :deprecated) if $VERBOSE + + return self.rack_middleware(...) + end + + # Wrap a Rack application with the standard server middleware. # @parameter rack_app [Proc | Object] A rack application/middleware. # @parameter verbose [Boolean] Whether to add the {Middleware::Verbose} middleware. # @parameter cache [Boolean] Whether to add the {Async::HTTP::Cache} middleware. - def self.middleware(rack_app, verbose: false, cache: true) + def self.rack_middleware(rack_app, verbose: false, cache: true) + return self.protocol_middleware(::Protocol::Rack::Adapter.new(rack_app), verbose: verbose, cache: cache) + end + + # Wrap a protocol application with the standard server middleware. + # @parameter application [Protocol::HTTP::Middleware] The protocol application/middleware. + # @parameter verbose [Boolean] Whether to add the {Middleware::Verbose} middleware. + # @parameter cache [Boolean] Whether to add the {Async::HTTP::Cache} middleware. + def self.protocol_middleware(application, verbose: false, cache: true) ::Protocol::HTTP::Middleware.build do if verbose use Middleware::Verbose @@ -32,9 +47,7 @@ def self.middleware(rack_app, verbose: false, cache: true) end use ::Protocol::HTTP::ContentEncoding - - use ::Protocol::Rack::Adapter - run rack_app + run application end end diff --git a/releases.md b/releases.md index fcd17e91..7bc86423 100644 --- a/releases.md +++ b/releases.md @@ -4,6 +4,12 @@ - Update the Envoy cluster example to use dedicated CDS and EDS services from `async-service-supervisor-envoy` v0.5. +### Rack Compatibility + +Falcon is shifting its application boundary from a Rack-centric design to {ruby Protocol::HTTP::Middleware}. `falcon serve` now prefers protocol HTTP middleware configured by `config/serve.rb`, while continuing to discover and run Rack `config.ru` applications through {ruby Protocol::Rack::Adapter}. + +Use {ruby Falcon::Server.protocol_middleware} for protocol HTTP applications and {ruby Falcon::Server.rack_middleware} for Rack applications. {ruby Falcon::Server.middleware} is deprecated. Explicit `.rb` serve configurations are now interpreted as {ruby Protocol::HTTP::Middleware}; Rack applications defined in Ruby files should use `config.ru` or wrap the application explicitly with {ruby Protocol::Rack::Adapter}. + ## v0.56.0 - Add `Falcon::Environment::Cluster` and `Falcon::Service::Cluster` for running workers with independently bound endpoints. diff --git a/test/falcon/command/.serve/config.rb b/test/falcon/command/.serve/config.rb new file mode 100644 index 00000000..1959c154 --- /dev/null +++ b/test/falcon/command/.serve/config.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +run Protocol::HTTP::Middleware::Okay diff --git a/test/falcon/command/serve.rb b/test/falcon/command/serve.rb index f1185fcb..a0926b01 100644 --- a/test/falcon/command/serve.rb +++ b/test/falcon/command/serve.rb @@ -14,10 +14,12 @@ let(:command) do subject[ "--port", port, - "--config", File.expand_path("config.ru", __dir__), *options + "--config", File.expand_path(configuration, __dir__), *options ] end + let(:configuration) {"config.ru"} + it "can listen on specified port" do configuration = command.configuration controller = configuration.make_controller @@ -56,6 +58,13 @@ it_behaves_like ServeCommand end + with "a protocol application" do + let(:port) {8096} + let(:configuration) {".serve/config.rb"} + + it_behaves_like ServeCommand + end + with "threaded container" do let(:port) {8092} let(:options) {["--count", 4, "--threaded"]} diff --git a/test/falcon/environment/serve.rb b/test/falcon/environment/serve.rb new file mode 100644 index 00000000..082edc6f --- /dev/null +++ b/test/falcon/environment/serve.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "falcon/environment/serve" +require "falcon/environment/server" +require "async/service/environment" +require "sus/fixtures/temporary_directory_context" +require "fileutils" + +describe Falcon::Environment::Serve do + include Sus::Fixtures::TemporaryDirectoryContext + + let(:evaluator) do + Async::Service::Environment.build( + Falcon::Environment::Server, + subject, + root: root, + name: "localhost", + ).evaluator + end + + it "prefers config/serve.rb" do + FileUtils.mkdir_p(File.join(root, "config")) + File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n") + File.write(File.join(root, "config/serve.rb"), "run Protocol::HTTP::Middleware::Okay\n") + + expect(evaluator.resolved_configuration_path).to be == File.join(root, "config/serve.rb") + end + + it "loads config/serve.rb as protocol middleware" do + FileUtils.mkdir_p(File.join(root, "config")) + File.write(File.join(root, "config/serve.rb"), "run Protocol::HTTP::Middleware::Okay\n") + + expect(evaluator.middleware).to be_a(Protocol::HTTP::Middleware) + end + + it "falls back to config.ru" do + File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n") + + expect(evaluator.resolved_configuration_path).to be == File.join(root, "config.ru") + end + + it "uses an explicit configuration path" do + evaluator = Async::Service::Environment.build( + Falcon::Environment::Server, + subject, + root: root, + name: "localhost", + configuration_path: "application.rb", + ).evaluator + + expect(evaluator.resolved_configuration_path).to be == File.join(root, "application.rb") + end + + it "discovers a configuration when the explicit path is nil" do + File.write(File.join(root, "config.ru"), "run ->(env) {[200, {}, []]}\n") + + evaluator = Async::Service::Environment.build( + Falcon::Environment::Server, + subject, + root: root, + name: "localhost", + configuration_path: nil, + ).evaluator + + expect(evaluator.resolved_configuration_path).to be == File.join(root, "config.ru") + end + + it "rejects unsupported configuration extensions" do + evaluator = Async::Service::Environment.build( + Falcon::Environment::Server, + subject, + root: root, + name: "localhost", + configuration_path: "application.txt", + ).evaluator + + expect do + evaluator.middleware + end.to raise_exception(ArgumentError, message: be(:include?, "Unsupported application configuration")) + end + + it "fails when no configuration exists" do + expect do + evaluator.resolved_configuration_path + end.to raise_exception(ArgumentError, message: be(:include?, "Could not find")) + end +end diff --git a/test/falcon/server.rb b/test/falcon/server.rb index 35f87a0a..9a78b1cf 100644 --- a/test/falcon/server.rb +++ b/test/falcon/server.rb @@ -13,7 +13,21 @@ [200, {}, ["OK"]] end + expect(subject.rack_middleware(app, cache: true)).to be_a(Async::HTTP::Cache::General) + end + + it "warns when using the deprecated middleware wrapper" do + app = lambda do |env| + [200, {}, ["OK"]] + end + + verbose = $VERBOSE + $VERBOSE = true + + expect(subject).to receive(:warn).and_return(nil) expect(subject.middleware(app, cache: true)).to be_a(Async::HTTP::Cache::General) + ensure + $VERBOSE = verbose end it "formats large statistics counts" do