mk_framework 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f37bb87398d93ff1849ae3df5cd7efac0632ebe204505aca673a8394a65d71c8
4
+ data.tar.gz: 93574025a9c4489ac0b395b968370e48679741a0aa40c229d861023209c78e60
5
+ SHA512:
6
+ metadata.gz: a589434c614336c30e21ccd5f319eb08a1ab98dac0733f4c7ec243cc0beee58faa0abe654b2cec5a56b27980c73329c8bd1a70c1b0f13dd17125feaa50530047
7
+ data.tar.gz: 9d855021ee2a3cebb77f3160292898ad676c8dd79c169c6001651c40d74487a909280f16f6442b569f7ab5edf96433e5274de932605fda737c697c428a393765
data/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-09-08
4
+
5
+ - Release MK as an installable gem with standalone framework tests and packaging.
6
+ - Move all seven sample applications, their shared support, and integration tests
7
+ to [mk_framework_sample_apps](https://github.com/makevoid/mk_framework_sample_apps).
8
+ Applications depend on `mk_framework ~> 0.2.0` from RubyGems.
9
+ - Remove application-only development dependencies and legacy test-helper paths
10
+ from the framework repository.
11
+
12
+ ## 0.1.0 — unreleased
13
+
14
+ - Package MK as an MIT-licensed gem, with a standalone test suite and Ruby 3.2–4.0 CI.
15
+ - Compile an immutable resource tree at explicit application boot. Add deep nesting,
16
+ shallow routes, namespaces, custom actions, action allowlists, configurable IDs,
17
+ route inspection, HEAD, PATCH, PUT, DELETE, and 405/Allow responses.
18
+ - Preserve POST update/delete URLs as configurable compatibility aliases.
19
+ - Persist returned Sequel records by registered action before response handlers:
20
+ create/update save once, delete invokes destroy hooks, show/index materialize data.
21
+ Handlers receive raw hashes/arrays; explicit multi-record transactions remain available.
22
+ - Add typed input allowlists, bounded pagination and request bodies, request IDs,
23
+ resilient JSON errors, recursive parameter redaction, and request hooks.
24
+ - Scope and namespace the sample applications. Add migrations, test-only databases,
25
+ deterministic weather tests, boot checks, and relationship/persistence regressions.
26
+ - Correct the weather entrypoint, explicitly require Excon, bound upstream timeouts,
27
+ atomically refresh cached locations, and label three-hour forecasts accurately.
28
+ - Update dependencies, documentation, and the aggregate test runner.
29
+
30
+ ### Migration required
31
+
32
+ See [the upgrade guide](docs/upgrading.md). The implicit handler persistence API and
33
+ `register_nested_resource` have been replaced. The 0.1.0 prototype was not published; these changes first ship in 0.2.0.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Canessa
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # MK Framework
2
+
3
+ Small, explicit JSON APIs on [Roda](https://roda.jeremyevans.net/), with optional
4
+ [Sequel](https://sequel.jeremyevans.net/) persistence. Ruby 3.2 or newer. MIT licensed.
5
+
6
+ MK gives each action an obvious home:
7
+
8
+ ```text
9
+ app.rb
10
+ models/post.rb
11
+ routes/posts/controllers/create.rb
12
+ routes/posts/handlers/create.rb
13
+ ```
14
+
15
+ Controllers select and prepare records. Framework dispatch persists standard action
16
+ results and converts them to raw data. Handlers filter fields and format responses.
17
+ Authorization, association selection, and multi-record transactions remain explicit Ruby.
18
+ You can read the core in `lib/mk_framework/`; no ORM or database is loaded until you
19
+ require `mk_framework/sequel`.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ gem install mk_framework -v 0.2.0
25
+ ```
26
+
27
+ Or add it to your application's Gemfile:
28
+
29
+ ```ruby
30
+ source 'https://rubygems.org'
31
+ gem 'mk_framework', '~> 0.2.0'
32
+ ```
33
+
34
+ Run `bundle install`. Add `sequel` and your database driver if you use
35
+ `mk_framework/sequel`; both are optional application dependencies.
36
+
37
+ ## Try the examples
38
+
39
+ The seven sample applications live in
40
+ [mk_framework_sample_apps](https://github.com/makevoid/mk_framework_sample_apps)
41
+ and install MK from RubyGems. Both GitHub repositories are private; installing the
42
+ published gem does not require access to the framework repository.
43
+
44
+ From a sample-app checkout:
45
+
46
+ ```sh
47
+ git clone git@github.com:makevoid/mk_framework_sample_apps.git
48
+ cd mk_framework_sample_apps/sample_app4
49
+ bundle install
50
+ bundle exec rake db:migrate # Explicit schema setup; never done on server boot
51
+ bundle exec rake routes
52
+ bundle exec rspec
53
+ ```
54
+
55
+ All sample tests force `RACK_ENV=test` and use private in-memory SQLite databases.
56
+ They never open `DATABASE_URL` or the development SQLite files. The weather tests
57
+ stub HTTP and require no personal API key or internet connection.
58
+
59
+ | Sample | Demonstrates |
60
+ | --- | --- |
61
+ | [1](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app1/README.md) | Basic todos and JSON CRUD |
62
+ | [2](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app2/README.md) | Todo validation and request specs |
63
+ | [3](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app3/README.md) | Custom response envelopes |
64
+ | [4](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app4/README.md) | Blog posts, nested comments, parent scoping |
65
+ | [5](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app5/README.md) | Kanban cards, status validation, nested comments |
66
+ | [6](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app6/README.md) | Weather client, deadlines, atomic cache refresh |
67
+ | [7](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app7/README.md) | Three-column Kanban board, ordering, priorities, filters, archive and comments |
68
+
69
+ ## An application
70
+
71
+ Define classes in your own module. Configure an absolute root, then call `boot!`
72
+ after the class definition. Boot loads route files, resolves action classes, checks
73
+ the route table, and freezes application configuration before serving requests.
74
+
75
+ ```ruby
76
+ require 'mk_framework'
77
+
78
+ module Blog
79
+ class App < MK::Application
80
+ configure root: __dir__, namespace: Blog
81
+
82
+ resource_routes do
83
+ resources :posts do
84
+ resources :comments
85
+ end
86
+ end
87
+ end
88
+
89
+ App.boot!
90
+ end
91
+ ```
92
+
93
+ In `config.ru`:
94
+
95
+ ```ruby
96
+ require_relative 'app'
97
+ run Blog::App.app
98
+ ```
99
+
100
+ By convention, `posts/create` connects `Blog::PostsCreateController` to
101
+ `Blog::PostsCreateHandler`. Without a `resource_routes` block, MK discovers the
102
+ standard action files under `routes/*/controllers`; explicit declarations are
103
+ recommended for APIs with nested or custom routes. Missing handlers fail at boot.
104
+
105
+ ## Controllers prepare; the framework persists; handlers respond
106
+
107
+ These action files assume your application has explicitly required its `Post`
108
+ model, backed by a migrated Sequel dataset, before calling `boot!`.
109
+
110
+ ```ruby
111
+ require 'mk_framework/sequel'
112
+
113
+ module Blog
114
+ class PostsCreateController < MK::Controller
115
+ route do |r|
116
+ Post.new(r.input.permit(title: String, description: [String, NilClass]))
117
+ end
118
+ end
119
+
120
+ class PostsCreateHandler < MK::Handler
121
+ handler do |r|
122
+ r.response.status = 201
123
+ {post: fields(model, :id, :title, :description)}
124
+ end
125
+ end
126
+ end
127
+ ```
128
+
129
+ Requiring `mk_framework/sequel` enables this lifecycle for a controller's returned
130
+ Sequel model, using the registered route action:
131
+
132
+ | Action | Before the handler |
133
+ | --- | --- |
134
+ | create | `save`, then `values` |
135
+ | update | `save`, then `values` |
136
+ | delete | `destroy` with hooks, then `values` |
137
+ | show | `values` |
138
+ | index | Materialize the collection and convert records to `values` |
139
+
140
+ Create controllers return `Post.new(...)`; updates find a record and call `set(...)`;
141
+ deletes return the record to delete. Do not save or destroy these results manually.
142
+ PATCH/PUT and POST compatibility aliases share the same lifecycle. Custom action
143
+ names do not automatically write, regardless of their controller class name.
144
+ Validation failures return 422; expected constraint/hook conflicts return 409;
145
+ unexpected database failures reach the sanitized 500 handler.
146
+
147
+ Records and datasets nested in hashes/arrays are recursively converted to raw data,
148
+ without recursively saving or deleting them. Controllers explicitly select any
149
+ associations to include; conversion does not load associations implicitly. Handlers
150
+ receive hashes and arrays, filter them with `fields` or `slice` and a model's
151
+ `public_attributes_list`, and never query the database.
152
+
153
+ Plain hash/array results remain usable for service-backed actions. For related
154
+ writes, use `DB.transaction` and explicit saves (or `MK::Persistence` helpers), then
155
+ return raw data so the framework does not save a completed write again.
156
+
157
+ Handlers return a Hash or Array, or use
158
+ `r.halt` for an explicit response such as 204. Returning `nil` from a controller
159
+ means the resource was not found.
160
+ Inside action blocks, use `next` for an early result; Ruby's `return` would try to
161
+ return from the context where the block was originally defined.
162
+
163
+ ## Routes and HTTP
164
+
165
+ For `resources :posts`, MK registers:
166
+
167
+ | Method | Path | Action |
168
+ | --- | --- | --- |
169
+ | GET / HEAD | `/posts` | index |
170
+ | POST | `/posts` | create |
171
+ | GET / HEAD | `/posts/:id` | show |
172
+ | PATCH / PUT | `/posts/:id` | update |
173
+ | DELETE | `/posts/:id` | delete |
174
+
175
+ PUT and PATCH reach the same action; the sample controllers update only supplied
176
+ fields. Define a separate custom action if your API needs strict replacement
177
+ semantics. The old `POST /posts/:id` and `POST /posts/:id/delete` routes remain
178
+ enabled by default. Disable them with `configure legacy_post_routes: false`.
179
+ Known paths with unsupported methods return 405 and `Allow`; unknown paths return
180
+ 404. CORS preflights are application policy, not automatically enabled.
181
+
182
+ Read [nested routes and authorization](docs/routing.md) for scopes, namespaces,
183
+ shallow routes, custom actions, explicit class mappings, and parent ownership.
184
+
185
+ ## Inputs, responses, and errors
186
+
187
+ - `r.path_params` is a frozen symbol-keyed hash containing only URL captures.
188
+ - `r.input` validates body/query input independently of path IDs. `require(:name)`
189
+ requires a typed field; `permit(name: String)` allows only declared fields.
190
+ - `:boolean` accepts booleans and the form strings `true`, `false`, `1`, `0`.
191
+ Nullable fields must explicitly include `NilClass` in their accepted types.
192
+ - `r.params` remains compatible with older controllers, with path IDs taking
193
+ precedence. Never use client input to establish a parent relationship.
194
+ - `r.page` validates `limit` (default 25, maximum 100) and `offset` (maximum 10,000).
195
+ `paginate(dataset, r)` in `MK::Persistence` uses an ordered, bounded query.
196
+ Implement cursor pagination in your application when large offsets are needed.
197
+ - JSON request bodies must be objects. Malformed JSON and malformed query inputs
198
+ produce 400. Bodies above 1 MiB produce 413, including bodies without a length.
199
+ - `MK::BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`,
200
+ `ValidationError`, and `BadGateway` represent intentional public errors.
201
+ Their messages are public: never place secrets in them.
202
+ - Unexpected errors return JSON with `error: "Server error"` and a request ID.
203
+ The same ID appears in `X-Request-ID` and the structured error log.
204
+
205
+ Production is the safe default. Set `RACK_ENV=development` explicitly for debug
206
+ details. Configure a logger using `configure logger: Logger.new($stdout)`, or
207
+ `setup_logger(io)`. `configure filter_parameters: %w[ssn]` adds fields to the
208
+ built-in recursive redaction list. Production logs omit raw exception messages
209
+ because database and network exceptions can contain credentials or SQL values.
210
+
211
+ ## Release and deployment
212
+
213
+ ```sh
214
+ bundle exec rake
215
+ bundle exec rake build
216
+ ```
217
+
218
+ The gem is written to `pkg/mk_framework-0.2.0.gem`. See
219
+ [deployment](docs/deployment.md) for migrations, connections, authentication,
220
+ timeouts, logging, and release verification, and [upgrading](docs/upgrading.md)
221
+ for changes from the prototype. CI covers Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
222
+
223
+ Applications can optionally `require 'mk_framework/testing'` and include
224
+ `MK::Framework::Spec` in RSpec to use Rack::Test and the `resp` JSON helper.
225
+ Install `rack-test` separately in the application's test bundle.
@@ -0,0 +1,115 @@
1
+ # Deployment and release
2
+
3
+ MK supplies routing, explicit action dispatch, JSON errors, and request boundaries.
4
+ Authentication policies, schema design, external services, and process operation
5
+ belong to the application. The APIs in the separate
6
+ [example repository](https://github.com/makevoid/mk_framework_sample_apps) are
7
+ demonstrations without authentication.
8
+
9
+ ## Configuration
10
+
11
+ Set `RACK_ENV=production` explicitly in deployments; it is also MK's default when
12
+ the variable is absent. Debug details are enabled only for `development`.
13
+
14
+ Configure the app before `boot!`:
15
+
16
+ ```ruby
17
+ configure root: __dir__, namespace: MyApp,
18
+ logger: Logger.new($stdout),
19
+ filter_parameters: %w[ssn access_code],
20
+ max_body_bytes: 1_048_576,
21
+ page_size: 25, max_page_size: 100, max_offset: 10_000
22
+ ```
23
+
24
+ Boot eagerly loads route files and freezes route/configuration state before
25
+ requests. Restart after code changes. Controller and handler instances are created
26
+ per request. Avoid mutable class variables for request data; use local variables,
27
+ instance variables, or the Rack request environment.
28
+
29
+ ## Database lifecycle
30
+
31
+ The samples accept `DATABASE_URL`, `DB_POOL_SIZE` (default 5), and
32
+ `DB_POOL_TIMEOUT` (default 5 seconds). Without a URL they use a SQLite file under
33
+ the sample directory, independent of the current working directory. Tests always
34
+ use private in-memory databases, regardless of these environment variables.
35
+
36
+ Run migrations once as a release step before starting workers. Keep backups and
37
+ test restore procedures. Do not put schema changes in app boot or request code.
38
+ Sample migrations use Sequel's migration version table and fail on an unexpected
39
+ pre-existing schema; the upgrade guide covers prototype databases.
40
+
41
+ For another adapter, install its driver explicitly and test the migrations and
42
+ queries against that adapter. The local automated suite exercises SQLite; it does
43
+ not certify PostgreSQL/MySQL behavior. Size the pool for the server's request
44
+ threads and the total number of worker processes within the database connection
45
+ budget. Use explicit transactions for related writes and retain database-level
46
+ foreign keys, uniqueness constraints, and indexes.
47
+
48
+ When preloading and forking a server, disconnect Sequel connections before fork
49
+ so child processes do not inherit live connections. Sequel reconnects on demand.
50
+ Apply this to every database owned by your app. The samples expose their database
51
+ as `SampleAppN::DB`. Test the specific Puma worker configuration you deploy;
52
+ Rack entrypoint specs do not exercise process forking.
53
+
54
+ The sample models update timestamps through Sequel's timestamps plugin. SQL writes
55
+ outside those models must set timestamps themselves. Do not use unbounded `.all`
56
+ queries in endpoints; use `paginate` or application-specific cursor pagination.
57
+
58
+ ## Authentication, browser clients, and limits
59
+
60
+ Use Rack/Roda authentication middleware or `before_request` to validate credentials
61
+ and establish a trusted principal. Apply authorization in controllers to both
62
+ resource ownership and the requested action. Never trust a query/body tenant ID.
63
+ See the routing guide and `spec/ownership_spec.rb` for an executable scoping test.
64
+
65
+ For session/cookie authentication, add CSRF protection and appropriate secure,
66
+ HTTP-only, same-site cookie settings using the corresponding Roda plugins.
67
+ For cross-origin clients, explicitly allow trusted origins, headers, and methods
68
+ through your chosen CORS middleware. MK does not enable permissive CORS or infer
69
+ authorization from URL nesting.
70
+
71
+ Terminate TLS at the server or a trusted proxy. Configure trusted host/proxy
72
+ handling for your deployment, especially before generating absolute URLs or
73
+ trusting forwarded client addresses. Set proxy/server header and request timeouts,
74
+ connection limits, and rate limits. MK's body cap bounds buffered request memory;
75
+ it is not a timeout or a rate limiter. File streaming APIs should use a separately
76
+ configured/mounted application with an appropriate body policy.
77
+
78
+ External HTTP clients need connect/read/write deadlines. The weather example uses
79
+ 3/5/5 seconds and atomic cache upserts. Its cache does not implement distributed
80
+ request coalescing: simultaneous misses can make redundant upstream requests, but
81
+ cannot create duplicate location rows. Add quotas/coalescing where upstream costs
82
+ or traffic justify it.
83
+
84
+ ## Logs and observability
85
+
86
+ Each request gets a server-generated `X-Request-ID`. Unexpected failures write a
87
+ structured JSON event through the configured logger, with the same identifier,
88
+ error class, stack, and recursively filtered already-parsed parameters. Production
89
+ logs omit raw exception messages and model values. A failed log sink does not
90
+ replace the error response. Add domain-specific sensitive keys to the filter.
91
+
92
+ Intentional public errors are not logged as unexpected failures. Collect access
93
+ logs, latency, request counts, status counts, database pool pressure, and readiness
94
+ checks with your server/middleware/monitoring stack. Apply redaction to those logs
95
+ too: external middleware is not covered by MK's parameter filter. Log retention
96
+ and access permissions are deployment decisions.
97
+
98
+ ## Release procedure
99
+
100
+ 1. Run `bundle install` and `bundle exec rake` with frozen lockfiles in CI.
101
+ 2. Require the Linux Ruby matrix to pass before publishing. Local results on one
102
+ Ruby version are not a substitute for that matrix.
103
+ 3. Run `bundle exec rake build`. Inspect and install the generated gem in a clean
104
+ environment, including `require 'mk_framework'` without Sequel and the optional
105
+ `require 'mk_framework/sequel'` with its dependency installed.
106
+ 4. Test the production app entrypoint against a newly migrated test database and
107
+ your intended server/proxy configuration. Test existing-data upgrades separately.
108
+ 5. Update the version/changelog and publish the reviewed gem using an authorized
109
+ RubyGems account. The gem metadata requires MFA. No publish task runs automatically.
110
+
111
+ CI runs framework/request tests and builds the gem on Ruby 3.2, 3.3, 3.4, and 4.0.
112
+ The framework root `rake` command runs its standalone specs. The separate sample
113
+ repository runs its integration tests and all seven application suites against the
114
+ published gem, isolating Bundler Gemfile and lockfile paths for each child and
115
+ propagating failures.
data/docs/routing.md ADDED
@@ -0,0 +1,160 @@
1
+ # Resource routing
2
+
3
+ ## Deep resources
4
+
5
+ This declaration belongs in an application's `resource_routes` block. Parent
6
+ resources can use `only: []` when they exist only to scope child endpoints.
7
+
8
+ ```ruby
9
+ scope '/api/v1' do
10
+ resources :organizations, only: [] do
11
+ resources :projects, only: [] do
12
+ resources :comments
13
+ end
14
+ end
15
+ end
16
+ ```
17
+
18
+ `PATCH /api/v1/organizations/one/projects/two/comments/three` dispatches to
19
+ `CommentsUpdateController` and `CommentsUpdateHandler` in the configured module.
20
+ The frozen path parameters are:
21
+
22
+ ```ruby
23
+ {organization_id: 'one', project_id: 'two', id: 'three'}
24
+ ```
25
+
26
+ Nested collection routes contain ancestor IDs but no leaf `id`. Arbitrary string
27
+ identifiers, including UUIDs and slugs, are supported; applications validate their
28
+ identifier formats. Query and JSON input cannot replace these captures.
29
+
30
+ The declarations describe URLs. Action files remain under `routes/comments/`.
31
+ Routing does not infer database associations or load parents automatically.
32
+
33
+ ## Authentication and ownership
34
+
35
+ Use `before_request` for authentication shared by generated and native Roda routes:
36
+
37
+ ```ruby
38
+ before_request do |r|
39
+ # authenticate must verify the token/session and return a trusted user object.
40
+ r.env['current_user'] = authenticate(r) or raise MK::Unauthorized
41
+ end
42
+ ```
43
+
44
+ Load children through an authorized dataset in the controller:
45
+
46
+ ```ruby
47
+ user = r.env.fetch('current_user')
48
+ organization = user.organizations_dataset
49
+ .where(id: r.path_params.fetch(:organization_id)).first or raise MK::NotFound
50
+ project = organization.projects_dataset
51
+ .where(id: r.path_params.fetch(:project_id)).first or raise MK::NotFound
52
+ comment = project.comments_dataset
53
+ .where(id: r.path_params.fetch(:id)).first or raise MK::NotFound
54
+ ```
55
+
56
+ Use that scoped lookup for reads, updates, and deletes. For creates, assign the
57
+ foreign key from the authorized parent, not from `r.input`. Database foreign keys
58
+ remain necessary to protect against concurrent deletion of a parent. Resource
59
+ ownership is separate from whether the user is allowed to perform an action;
60
+ enforce both. The blog and Kanban samples are public demonstrations, not account
61
+ systems. `spec/ownership_spec.rb` exercises authenticated three-level scoping.
62
+
63
+ ## Shallow routes
64
+
65
+ ```ruby
66
+ scope '/api/v1' do
67
+ resources :posts do
68
+ resources :comments, shallow: true
69
+ end
70
+ end
71
+ ```
72
+
73
+ Collections use `/api/v1/posts/:post_id/comments`; members use
74
+ `/api/v1/comments/:id`. There is no top-level comment collection and no nested
75
+ member route in this mode. Shallow members must still be loaded through the
76
+ authenticated user's authorized dataset. The shorter URL is not permission.
77
+
78
+ ## Names, scopes, and actions
79
+
80
+ `scope '/api/v1'` changes the URL only. `namespace :admin` changes both the URL
81
+ prefix and the Ruby namespace (for example `Blog::Admin`). That module must exist
82
+ before route compilation. Use `namespace: SomeModule` on a resource to override
83
+ its action namespace without changing the URL.
84
+
85
+ ```ruby
86
+ resources :people, singular: :person, parent_key: :owner_id do
87
+ resources :user_profiles, param: :slug, only: %i[index show]
88
+ end
89
+ ```
90
+
91
+ `param` names a member's own identifier. `parent_key` names the capture passed to
92
+ descendants. `singular` controls the default parent key and missing-resource label.
93
+ Basic inflection covers underscores, `companies`, and common irregular nouns;
94
+ set `singular` explicitly for domain terms. Repeated resources in one hierarchy
95
+ need distinct parent keys; duplicate parameter names fail at boot.
96
+
97
+ Custom actions can use any controller result, including service results without
98
+ a Sequel model:
99
+
100
+ ```ruby
101
+ resources :posts do
102
+ member :publish, via: :post
103
+ collection :search, via: :get
104
+ end
105
+ ```
106
+
107
+ This connects `PostsPublishController`/`PostsPublishHandler` to
108
+ `POST /posts/:id/publish`, and `PostsSearchController`/`PostsSearchHandler` to
109
+ `GET /posts/search`. Literal routes take precedence over identifier captures.
110
+
111
+ Pass `controller:` and `handler:` classes to custom actions, or override standard
112
+ actions with explicit class pairs:
113
+
114
+ ```ruby
115
+ resources :posts, only: [:show], actions: {
116
+ show: [PublicPostController, PublicPostHandler]
117
+ }
118
+ ```
119
+
120
+ The full default action list is `index`, `show`, `create`, `update`, `delete`.
121
+ Missing actions, missing blocks, and duplicate method/path combinations fail at
122
+ boot. `App.route_table` prints the resolved paths and classes.
123
+
124
+ ## Native Roda routes
125
+
126
+ An ordinary `route` block runs before generated routes. A matching Roda branch
127
+ or a non-nil return value completes the request; nil falls through to resources.
128
+
129
+ ```ruby
130
+ route do |r|
131
+ r.get('health') { {ok: true} }
132
+ r.on('metrics') { r.run(metrics_app) }
133
+ end
134
+ ```
135
+
136
+ Define routes, middleware, configuration, and hooks before `boot!`. The application
137
+ is frozen afterward; development changes require a process restart.
138
+ An unbooted shared application base can be subclassed: configuration, resource
139
+ declarations, and request hooks are inherited. Boot concrete applications only;
140
+ Roda does not allow subclassing an already frozen application.
141
+
142
+ ## Nested writes
143
+
144
+ Nested URLs do not imply automatic nested assignment. Validate the payload and
145
+ allowlist each object's fields explicitly, then use one transaction:
146
+
147
+ ```ruby
148
+ DB.transaction do
149
+ project = persist(Project.new(project_attributes))
150
+ validated_comment_attributes.each do |attributes|
151
+ persist(Comment.new(attributes.merge(project_id: project.id)))
152
+ end
153
+ project.values # Already saved: return raw data to skip automatic action persistence.
154
+ end
155
+ ```
156
+
157
+ Authorize the parent before entering the transaction, bound the number of children,
158
+ and validate each child payload. Let validation exceptions escape the transaction
159
+ so it rolls back. External API calls are not rolled back with database writes;
160
+ perform them outside a database transaction or use an application-owned outbox.