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 +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/docs/deployment.md +115 -0
- data/docs/routing.md +160 -0
- data/docs/upgrading.md +124 -0
- data/lib/mk_framework/application.rb +147 -0
- data/lib/mk_framework/controller.rb +39 -0
- data/lib/mk_framework/errors.rb +88 -0
- data/lib/mk_framework/request.rb +102 -0
- data/lib/mk_framework/router.rb +165 -0
- data/lib/mk_framework/sequel.rb +56 -0
- data/lib/mk_framework/testing.rb +36 -0
- data/lib/mk_framework/version.rb +5 -0
- data/lib/mk_framework.rb +13 -0
- metadata +137 -0
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.
|
data/docs/deployment.md
ADDED
|
@@ -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.
|