Skip to content

Commit f6e58a9

Browse files
committed
refactor: extract web boot setup
1 parent cafa1bf commit f6e58a9

5 files changed

Lines changed: 129 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Dev URLs: Ruby app at `http://localhost:4000`, frontend dev server at `http://lo
6868

6969
Backend code under the `Html2rss::Web` namespace now lives under `app/web/**`, so Zeitwerk can mirror constant paths directly instead of relying on directory-specific namespace wiring.
7070
`make ready` also runs `rake zeitwerk:verify`, which eager-loads the app and fails on loader drift early.
71+
For contributors and AI agents changing backend structure, follow the placement rules in [docs/ai-agent-app-web.md](docs/ai-agent-app-web.md).
7172

7273
## Make Targets
7374

app.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_relative 'app/web/boot'
1010

1111
Html2rss::Web::Boot.setup!(reloadable: ENV['RACK_ENV'] == 'development')
12+
Html2rss::Web::Boot::Setup.call!
1213

1314
module Html2rss
1415
module Web
@@ -33,13 +34,6 @@ class App < Roda
3334
def self.development? = EnvironmentValidator.development?
3435

3536
def development? = self.class.development?
36-
EnvironmentValidator.validate_environment!
37-
EnvironmentValidator.validate_production_security!
38-
Flags.validate!
39-
40-
Html2rss::RequestService.register_strategy(:ssrf_filter, SsrfFilterStrategy)
41-
Html2rss::RequestService.default_strategy_name = :ssrf_filter
42-
Html2rss::RequestService.unregister_strategy(:faraday)
4337
opts.merge!(check_dynamic_arity: false, check_arity: :warn)
4438
use RequestContextMiddleware
4539
use Rack::Cache, metastore: 'file:./tmp/rack-cache-meta', entitystore: 'file:./tmp/rack-cache-body',

app/web/boot/setup.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Web
5+
module Boot
6+
##
7+
# Applies boot-time runtime configuration outside the Roda class body.
8+
module Setup
9+
class << self
10+
# Validates environment configuration and wires the request service.
11+
#
12+
# @return [void]
13+
def call!
14+
validate_environment!
15+
configure_request_service!
16+
end
17+
18+
private
19+
20+
# @return [void]
21+
def validate_environment!
22+
EnvironmentValidator.validate_environment!
23+
EnvironmentValidator.validate_production_security!
24+
Flags.validate!
25+
end
26+
27+
# @return [void]
28+
def configure_request_service!
29+
Html2rss::RequestService.register_strategy(:ssrf_filter, SsrfFilterStrategy)
30+
Html2rss::RequestService.default_strategy_name = :ssrf_filter
31+
Html2rss::RequestService.unregister_strategy(:faraday)
32+
end
33+
end
34+
end
35+
end
36+
end
37+
end

docs/ai-agent-app-web.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# `app/web` Rules For AI Agents
2+
3+
This file is intentionally prescriptive. If you are an AI coding agent changing Ruby backend code, follow these rules before adding files or moving code.
4+
5+
## Namespace Contract
6+
7+
- `app/` is the Zeitwerk root for `Html2rss`.
8+
- `app/web/**` maps to the `Html2rss::Web` namespace.
9+
- Do not add `require_relative` calls between files under `app/web/**` unless the file is a non-Zeitwerk boot entrypoint.
10+
- Path, filename, and constant name must match. If a constant is `Html2rss::Web::SecurityLogger`, the file belongs at `app/web/security/security_logger.rb`.
11+
12+
## Directory Placement
13+
14+
Use the narrowest concern folder that fits the object.
15+
16+
- `app/web/api/`: API contract and endpoint implementation objects.
17+
- `app/web/boot/`: process boot, loader setup, dev reload, runtime setup.
18+
- `app/web/config/`: environment flags, local config loading, config snapshots.
19+
- `app/web/domain/`: backend domain helpers that do not belong to API, request, rendering, or security.
20+
- `app/web/errors/`: error classes and error response serialization.
21+
- `app/web/feeds/`: feed fetching, rendering orchestration, cache use, feed service contracts.
22+
- `app/web/http/`: low-level HTTP response/cache helpers.
23+
- `app/web/rendering/`: content negotiation and feed output builders.
24+
- `app/web/request/`: request-scoped context and middleware.
25+
- `app/web/routes/`: Roda route composition only.
26+
- `app/web/security/`: auth, token handling, account access, SSRF request strategy, security logging.
27+
- `app/web/telemetry/`: observability event emission only.
28+
29+
## Placement Heuristics
30+
31+
- Put code in `routes/` only if it mounts or composes Roda request branches.
32+
- Put code in `api/` only if it is specific to `/api/v1` contracts or endpoint behavior.
33+
- Put code in `feeds/` if it is part of fetching, resolving, rendering, or caching feeds.
34+
- Put code in `domain/` only as a last resort. If a better concern folder exists, use it.
35+
- Do not create generic buckets such as `services`, `utils`, `helpers`, or `concerns`.
36+
37+
## Consolidation Rules
38+
39+
- Prefer concern folders over a flat `app/web/` root.
40+
- Do not merge unrelated objects just to reduce file count.
41+
- Consolidate only when one file is clearly a thin wrapper around another concept and the merged object still has a single responsibility.
42+
- If a file defines multiple top-level constants, stop and check whether Zeitwerk naming or the public API would become less clear.
43+
44+
## Boot And Runtime Rules
45+
46+
- `app.rb` should declare the Roda app and its Rack/Roda plugins.
47+
- Process-level boot side effects belong in `app/web/boot/**`.
48+
- Register external runtime integrations, validate environment, and configure shared services in boot objects, not inline in the Roda class body.
49+
50+
## Route Rules
51+
52+
- Keep route composition centralized in `app/web/routes/**`.
53+
- Split route modules by endpoint concern when a route file grows, but preserve matching order.
54+
- Root metadata routes must use exact matching (`r.is`) so they do not swallow subpaths.
55+
56+
## Change Checklist
57+
58+
- Update or add specs for the behavior you moved.
59+
- Run `docker compose -f .devcontainer/docker-compose.yml exec -T app make ready`.
60+
- Smoke the app at `http://127.0.0.1:4001/` when request or UI behavior changed.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
require_relative '../../../../app/web/boot/setup'
6+
7+
RSpec.describe Html2rss::Web::Boot::Setup do
8+
describe '.call!' do
9+
before do
10+
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_environment!)
11+
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_production_security!)
12+
allow(Html2rss::Web::Flags).to receive(:validate!)
13+
allow(Html2rss::RequestService).to receive(:register_strategy)
14+
allow(Html2rss::RequestService).to receive(:default_strategy_name=)
15+
allow(Html2rss::RequestService).to receive(:unregister_strategy)
16+
end
17+
18+
it 'validates environment state and configures the request service', :aggregate_failures do
19+
described_class.call!
20+
21+
expect(Html2rss::Web::EnvironmentValidator).to have_received(:validate_environment!).once
22+
expect(Html2rss::Web::EnvironmentValidator).to have_received(:validate_production_security!).once
23+
expect(Html2rss::Web::Flags).to have_received(:validate!).once
24+
expect(Html2rss::RequestService).to have_received(:register_strategy)
25+
.with(:ssrf_filter, Html2rss::Web::SsrfFilterStrategy).once
26+
expect(Html2rss::RequestService).to have_received(:default_strategy_name=).with(:ssrf_filter).once
27+
expect(Html2rss::RequestService).to have_received(:unregister_strategy).with(:faraday).once
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)