mk_framework 0.2.0 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +442 -25
- data/bin/mk_frame_init +5 -0
- data/docs/deployment.md +3 -3
- data/lib/mk_framework/generator/cli.rb +124 -0
- data/lib/mk_framework/generator/configuration.rb +87 -0
- data/lib/mk_framework/generator/options.rb +91 -0
- data/lib/mk_framework/generator/project.rb +58 -0
- data/lib/mk_framework/generator/tasks.rb +13 -0
- data/lib/mk_framework/generator/templates/Gemfile.erb +13 -0
- data/lib/mk_framework/generator/templates/README.md.erb +70 -0
- data/lib/mk_framework/generator/templates/Rakefile.erb +32 -0
- data/lib/mk_framework/generator/templates/app.rb.erb +17 -0
- data/lib/mk_framework/generator/templates/config.ru.erb +4 -0
- data/lib/mk_framework/generator/templates/controller.rb.erb +40 -0
- data/lib/mk_framework/generator/templates/database.rb.erb +17 -0
- data/lib/mk_framework/generator/templates/gitignore.erb +10 -0
- data/lib/mk_framework/generator/templates/handler.rb.erb +16 -0
- data/lib/mk_framework/generator/templates/migration.rb.erb +14 -0
- data/lib/mk_framework/generator/templates/model.rb.erb +21 -0
- data/lib/mk_framework/generator/templates/request_spec.rb.erb +108 -0
- data/lib/mk_framework/generator/templates/spec_helper.rb.erb +14 -0
- data/lib/mk_framework/generator.rb +6 -0
- data/lib/mk_framework/version.rb +1 -1
- metadata +26 -6
- data/docs/upgrading.md +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 116dd1b29e32f3a469fc2806aab54c975373ea40188a91025e89d4ba2a3a45cd
|
|
4
|
+
data.tar.gz: 38ba4c1925e8be343c384e527d17ec0509082f24e4ddbd891f016cdf1ea5f1f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1756e15b4f7ae6c8c5dffdd121ecbd632a264ab7a539cb99b9b2f7158cd83cf57c436f1a03da46bc156ad7f0a0e10dc7575982f4e934e64749cffdf617c3294
|
|
7
|
+
data.tar.gz: 5766d0fd1acd60b0ff39868145c196b07afdb758e11319d7a5c9c75c4f0e27f65bcb70aa0add146818956237324d08a97b37a30c67ad7c2aaa4de67e441fdb9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-09-08
|
|
4
|
+
|
|
5
|
+
- Generate full CRUD resources: index, show, create, update (PATCH and PUT), and
|
|
6
|
+
delete, with separate controllers and handlers for every action.
|
|
7
|
+
- Make generated apps' default `rake` task invoke `rake dev` and start Puma on
|
|
8
|
+
`127.0.0.1:3000`, with `HOST` and `PORT` overrides.
|
|
9
|
+
- Include generated request specs for reads, partial updates, deletion, missing
|
|
10
|
+
records, validation failures, and persistence; verify all supported field types.
|
|
11
|
+
- Store generated-app timestamps in UTC so datetime updates round-trip consistently.
|
|
12
|
+
- Update generator prompts, next steps, and documentation for the complete app.
|
|
13
|
+
|
|
14
|
+
## 0.2.1 — 2026-09-08
|
|
15
|
+
|
|
16
|
+
- Add `mk_frame_init` with interactive prompts and a non-interactive `--cli`
|
|
17
|
+
definition for generating a self-contained app, model, create controller/handler,
|
|
18
|
+
migration, Rack entrypoint, and request specs.
|
|
19
|
+
- Add the reusable `mk_framework:init` Rake task and seven supported field types.
|
|
20
|
+
- Document standalone sample support and the generator installation and CLI flows.
|
|
21
|
+
- Run the Linux CI tests and gem build on Ruby 4.0 only.
|
|
22
|
+
|
|
3
23
|
## 0.2.0 — 2026-09-08
|
|
4
24
|
|
|
5
25
|
- Release MK as an installable gem with standalone framework tests and packaging.
|
data/README.md
CHANGED
|
@@ -21,19 +21,123 @@ require `mk_framework/sequel`.
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
gem install mk_framework -v 0.2.
|
|
24
|
+
gem install mk_framework -v 0.2.2
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Or add it to your application's Gemfile:
|
|
28
28
|
|
|
29
29
|
```ruby
|
|
30
30
|
source 'https://rubygems.org'
|
|
31
|
-
gem 'mk_framework', '~> 0.2.
|
|
31
|
+
gem 'mk_framework', '~> 0.2.2'
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run `bundle install`. Add `sequel` and your database driver if you use
|
|
35
35
|
`mk_framework/sequel`; both are optional application dependencies.
|
|
36
36
|
|
|
37
|
+
## Generate an app with `mk_frame_init`
|
|
38
|
+
|
|
39
|
+
Version 0.2.2 generates full CRUD apps with the `mk_frame_init` executable. Install the gem, then run
|
|
40
|
+
it from the parent directory where you want your new app:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
gem install mk_framework -v 0.2.2
|
|
44
|
+
mk_frame_init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
RubyGems puts `mk_frame_init` in Ruby's executable directory. Inside a bundle that
|
|
48
|
+
includes MK, you can also run `bundle exec mk_frame_init`.
|
|
49
|
+
|
|
50
|
+
The interactive CLI asks, in order:
|
|
51
|
+
|
|
52
|
+
1. App name, such as `blog` (Ruby namespace `Blog`).
|
|
53
|
+
2. Singular model name, such as `post` (`Blog::Post`).
|
|
54
|
+
3. Resource/table name, defaulting to `posts`.
|
|
55
|
+
4. Each field name and its type, selected by menu number or type name. Leave the
|
|
56
|
+
next field name blank to finish.
|
|
57
|
+
5. Confirmation of the app, model, fields, resource actions, and destination.
|
|
58
|
+
|
|
59
|
+
Use lowercase names with underscores. The supported types are `string`, `text`,
|
|
60
|
+
`integer`, `float`, `boolean`, `date`, and `datetime`. At least one field is required;
|
|
61
|
+
all selected fields are required. MK generates `id`, `created_at`, and `updated_at`
|
|
62
|
+
automatically. Date/time inputs use ISO 8601 strings, and numeric inputs use JSON
|
|
63
|
+
numbers. Invalid field types return 400; missing required fields or blank text
|
|
64
|
+
return 422.
|
|
65
|
+
|
|
66
|
+
For scripts and automation, supply the whole definition in a quoted `--cli`
|
|
67
|
+
argument. This mode never prompts or asks for confirmation:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
mk_frame_init --cli 'app_name:blog, model_name:posts, fields:[title:string, contents:text, published:boolean]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This creates `./blog`, a `Blog::Post` model backed by `posts`, and full CRUD routes:
|
|
74
|
+
`GET /posts`, `GET /posts/:id`, `POST /posts`, `PATCH /posts/:id`, `PUT /posts/:id`,
|
|
75
|
+
and `DELETE /posts/:id`. Each action has its own controller and handler. Inline
|
|
76
|
+
`model_name` accepts a singular or conventional plural name (`post` or `posts`).
|
|
77
|
+
For a custom table/URL name, add `resource_name:articles`. Names are simple Ruby
|
|
78
|
+
identifiers; the inline format is parsed as data, never evaluated as Ruby.
|
|
79
|
+
|
|
80
|
+
An optional destination overrides the default app directory. Its parent must
|
|
81
|
+
already exist, and the generator refuses existing destinations, including empty
|
|
82
|
+
directories. Invalid input exits with status 1; successful generation exits with 0.
|
|
83
|
+
No dependencies are installed and no database is opened during generation.
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
mk_frame_init ./blog_api --cli 'app_name:blog, model_name:posts, fields:[title:string, contents:text]'
|
|
87
|
+
mk_frame_init --help
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
After generation:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
cd blog_api
|
|
94
|
+
bundle install
|
|
95
|
+
bundle exec rake db:migrate
|
|
96
|
+
bundle exec rake routes
|
|
97
|
+
bundle exec rspec
|
|
98
|
+
bundle exec rake # same as rake dev; starts Puma on port 3000
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Set `HOST` or `PORT` to override the default `127.0.0.1:3000` bind address.
|
|
102
|
+
|
|
103
|
+
The result is self-contained:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
blog_api/
|
|
107
|
+
├── Gemfile
|
|
108
|
+
├── Rakefile
|
|
109
|
+
├── .gitignore
|
|
110
|
+
├── README.md
|
|
111
|
+
├── database.rb
|
|
112
|
+
├── app.rb
|
|
113
|
+
├── config.ru
|
|
114
|
+
├── db/migrations/001_initial.rb
|
|
115
|
+
├── models/post.rb
|
|
116
|
+
├── routes/posts/controllers/{index,show,create,update,delete}.rb
|
|
117
|
+
├── routes/posts/handlers/{index,show,create,update,delete}.rb
|
|
118
|
+
├── spec/spec_helper.rb
|
|
119
|
+
└── spec/request/posts_spec.rb
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`database.rb` connects to a local SQLite file, or `DATABASE_URL`. Migrations are
|
|
123
|
+
explicit and run before models load. Tests always migrate a private in-memory
|
|
124
|
+
database. The controller permits the chosen fields and returns `Post.new(...)`;
|
|
125
|
+
MK saves once, then the handler returns `{post: ...}` with status 201. Index returns
|
|
126
|
+
`{posts: [...]}`; show, update, and delete return `{post: ...}` with status 200.
|
|
127
|
+
PATCH and PUT preserve omitted fields; missing records return 404. The generated
|
|
128
|
+
README includes a local server command, an example request, and extension guidance.
|
|
129
|
+
|
|
130
|
+
The framework checkout also exposes the same generator as a Rake task:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
bundle exec rake mk_framework:init DESTINATION=./blog_api
|
|
134
|
+
bundle exec rake mk_framework:init DESTINATION=./blog_api \
|
|
135
|
+
APP_SPEC='app_name:blog, model_name:posts, fields:[title:string, contents:text]'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
These are alternative invocations; choose one for a new destination. To expose the
|
|
139
|
+
task in another project's Rakefile, add `require 'mk_framework/generator/tasks'`.
|
|
140
|
+
|
|
37
141
|
## Try the examples
|
|
38
142
|
|
|
39
143
|
The seven sample applications live in
|
|
@@ -66,23 +170,140 @@ stub HTTP and require no personal API key or internet connection.
|
|
|
66
170
|
| [6](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app6/README.md) | Weather client, deadlines, atomic cache refresh |
|
|
67
171
|
| [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
172
|
|
|
69
|
-
##
|
|
173
|
+
## Walkthrough: sample app 4, a blog API
|
|
70
174
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
175
|
+
[Sample app 4](https://github.com/makevoid/mk_framework_sample_apps/tree/main/sample_app4)
|
|
176
|
+
stores posts and their comments in SQLite and exposes JSON CRUD endpoints. A post
|
|
177
|
+
has a title and optional description; a comment belongs to a post and has content
|
|
178
|
+
and an optional author. Both models maintain creation and update timestamps.
|
|
179
|
+
Deleting a post also deletes its comments through a cascading foreign key.
|
|
180
|
+
|
|
181
|
+
Clients can list posts, request their comments with `GET /posts?comments=1`, create
|
|
182
|
+
a post with `POST /posts`, and edit a comment with
|
|
183
|
+
`PATCH /posts/:post_id/comments/:id`. Nested comment lookups check the URL parent,
|
|
184
|
+
so using another post's ID returns 404. This sample has no authentication;
|
|
185
|
+
parent scoping checks the relationship, while user access rules belong in your app.
|
|
186
|
+
|
|
187
|
+
### Directory structure
|
|
188
|
+
|
|
189
|
+
Each sample carries its own namespaced database and Rake helpers in `support/`,
|
|
190
|
+
so you can copy `sample_app4/` alone to start a separate project. This tree lists
|
|
191
|
+
the six action files explained below; the sample also includes the remaining CRUD
|
|
192
|
+
controllers and handlers for both resources.
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
sample_app4/
|
|
196
|
+
├── support/
|
|
197
|
+
│ ├── database.rb # SampleApp4::Database: connections and migrations
|
|
198
|
+
│ └── tasks.rb # SampleApp4::Tasks: db:migrate, routes, and specs
|
|
199
|
+
├── Gemfile
|
|
200
|
+
├── Rakefile
|
|
201
|
+
├── database.rb # SampleApp4::ROOT and SampleApp4::DB
|
|
202
|
+
├── app.rb # Requires, namespace, routes, and boot!
|
|
203
|
+
├── config.ru # Rack entrypoint
|
|
204
|
+
├── db/migrations/
|
|
205
|
+
│ └── 001_initial.rb # posts, comments, indexes, and foreign key
|
|
206
|
+
├── models/
|
|
207
|
+
│ ├── post.rb
|
|
208
|
+
│ └── comment.rb
|
|
209
|
+
├── routes/
|
|
210
|
+
│ ├── posts/
|
|
211
|
+
│ │ ├── controllers/
|
|
212
|
+
│ │ │ ├── create.rb
|
|
213
|
+
│ │ │ └── index.rb
|
|
214
|
+
│ │ └── handlers/
|
|
215
|
+
│ │ ├── create.rb
|
|
216
|
+
│ │ └── index.rb
|
|
217
|
+
│ └── comments/
|
|
218
|
+
│ ├── controllers/update.rb
|
|
219
|
+
│ └── handlers/update.rb
|
|
220
|
+
└── spec/
|
|
221
|
+
├── spec_helper.rb
|
|
222
|
+
├── boot_spec.rb
|
|
223
|
+
└── request/
|
|
224
|
+
├── posts_spec.rb
|
|
225
|
+
├── comments_spec.rb
|
|
226
|
+
├── nested_comments_spec.rb
|
|
227
|
+
└── handler_boundary_spec.rb
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Setup, database, and boot
|
|
231
|
+
|
|
232
|
+
The sample's `Gemfile` includes the framework, Sequel, SQLite, Rack server tools,
|
|
233
|
+
and request-test dependencies:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
source 'https://rubygems.org'
|
|
237
|
+
|
|
238
|
+
gem 'mk_framework', '~> 0.2.0'
|
|
239
|
+
gem 'sequel', '>= 5.92', '< 6'
|
|
240
|
+
gem 'sqlite3', '~> 2.9'
|
|
241
|
+
gem 'rake', '~> 13.4'
|
|
242
|
+
gem 'rackup', '~> 2.3'
|
|
243
|
+
gem 'puma', '~> 8.0'
|
|
244
|
+
|
|
245
|
+
group :test do
|
|
246
|
+
gem 'rspec', '~> 3.13'
|
|
247
|
+
gem 'rack-test', '~> 2.2'
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
From `mk_framework_sample_apps/sample_app4`, run:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
bundle install
|
|
255
|
+
bundle exec rake db:migrate
|
|
256
|
+
bundle exec rake routes
|
|
257
|
+
bundle exec rspec
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`Rakefile` loads `support/tasks.rb` and installs the local tasks with
|
|
261
|
+
`SampleApp4::Tasks.install(__dir__)`.
|
|
262
|
+
`db:migrate` loads `database.rb` and applies `db/migrations/001_initial.rb` before
|
|
263
|
+
any models are loaded. The migration creates `posts` and `comments`, including
|
|
264
|
+
required timestamps and a non-null `comments.post_id` foreign key with cascading
|
|
265
|
+
deletion. Schema changes are an explicit step; starting the app never migrates it.
|
|
266
|
+
|
|
267
|
+
**`sample_app4/database.rb`**
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
# frozen_string_literal: true
|
|
271
|
+
|
|
272
|
+
require_relative 'support/database'
|
|
273
|
+
|
|
274
|
+
module SampleApp4
|
|
275
|
+
ROOT = __dir__.freeze
|
|
276
|
+
DB = Database.connect(root: ROOT, filename: 'blog.db')
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`SampleApp4::Database.connect` defaults to `sample_app4/blog.db`, using an absolute path.
|
|
281
|
+
It accepts `DATABASE_URL`, `DB_POOL_SIZE`, and `DB_POOL_TIMEOUT` for deployment.
|
|
282
|
+
In tests it always opens a private in-memory SQLite database; `spec_helper.rb`
|
|
283
|
+
migrates that database before requiring `app.rb`.
|
|
284
|
+
|
|
285
|
+
**`sample_app4/app.rb`**
|
|
74
286
|
|
|
75
287
|
```ruby
|
|
76
|
-
|
|
288
|
+
# frozen_string_literal: true
|
|
289
|
+
|
|
290
|
+
require 'mk_framework/sequel'
|
|
291
|
+
require_relative 'database'
|
|
292
|
+
require_relative 'models/post'
|
|
293
|
+
require_relative 'models/comment'
|
|
294
|
+
|
|
295
|
+
module SampleApp4
|
|
296
|
+
class Controller < MK::Controller
|
|
297
|
+
end
|
|
77
298
|
|
|
78
|
-
module Blog
|
|
79
299
|
class App < MK::Application
|
|
80
|
-
configure root:
|
|
300
|
+
configure root: ROOT, namespace: SampleApp4
|
|
81
301
|
|
|
82
302
|
resource_routes do
|
|
83
303
|
resources :posts do
|
|
84
304
|
resources :comments
|
|
85
305
|
end
|
|
306
|
+
resources :comments, only: %i[show update delete]
|
|
86
307
|
end
|
|
87
308
|
end
|
|
88
309
|
|
|
@@ -90,42 +311,238 @@ module Blog
|
|
|
90
311
|
end
|
|
91
312
|
```
|
|
92
313
|
|
|
93
|
-
|
|
314
|
+
The load order is deliberate: enable MK's Sequel integration, connect the database,
|
|
315
|
+
load both models, define the shared controller base and application, then call
|
|
316
|
+
`App.boot!`. `ROOT` anchors file loading independently of the working directory;
|
|
317
|
+
`namespace: SampleApp4` tells MK where to resolve controller and handler classes.
|
|
318
|
+
|
|
319
|
+
The nested declaration generates post and comment CRUD routes. The final
|
|
320
|
+
`resources :comments, only: ...` also exposes the sample's compatibility member
|
|
321
|
+
routes, such as `PATCH /comments/:id`; it does not create a parentless comments
|
|
322
|
+
collection. Both forms use the same comment action classes.
|
|
323
|
+
|
|
324
|
+
`boot!` loads Ruby files under `routes/`, resolves controller/handler pairs,
|
|
325
|
+
validates the route table, and freezes configuration. For example,
|
|
326
|
+
`posts/create` resolves to `SampleApp4::PostsCreateController` and
|
|
327
|
+
`SampleApp4::PostsCreateHandler`. Missing handlers fail at boot. Without a
|
|
328
|
+
`resource_routes` block, MK can discover standard actions from
|
|
329
|
+
`routes/*/controllers`; explicit declarations make nested routes easier to inspect.
|
|
330
|
+
|
|
331
|
+
**`sample_app4/config.ru`**
|
|
94
332
|
|
|
95
333
|
```ruby
|
|
334
|
+
# frozen_string_literal: true
|
|
335
|
+
|
|
96
336
|
require_relative 'app'
|
|
97
|
-
run
|
|
337
|
+
run SampleApp4::App.app
|
|
98
338
|
```
|
|
99
339
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
recommended for APIs with nested or custom routes. Missing handlers fail at boot.
|
|
340
|
+
Rack loads this entrypoint and serves the already booted `SampleApp4::App.app`.
|
|
341
|
+
The sample's request and boot specs exercise it without starting a server,
|
|
342
|
+
including loading it from a different working directory.
|
|
104
343
|
|
|
105
|
-
|
|
344
|
+
### Example 1: create a post
|
|
106
345
|
|
|
107
|
-
|
|
108
|
-
|
|
346
|
+
`POST /posts` accepts a JSON object such as
|
|
347
|
+
`{"title":"First post","description":"Notes from the garden"}`.
|
|
348
|
+
|
|
349
|
+
**`sample_app4/routes/posts/controllers/create.rb`**
|
|
109
350
|
|
|
110
351
|
```ruby
|
|
111
|
-
|
|
352
|
+
# frozen_string_literal: true
|
|
112
353
|
|
|
113
|
-
module
|
|
114
|
-
class PostsCreateController <
|
|
354
|
+
module SampleApp4
|
|
355
|
+
class PostsCreateController < Controller
|
|
115
356
|
route do |r|
|
|
116
|
-
Post.new(r.input.permit(title: String, description: [String, NilClass]))
|
|
357
|
+
Post.new(r.input.permit(title: [String, NilClass], description: [String, NilClass]))
|
|
117
358
|
end
|
|
118
359
|
end
|
|
360
|
+
end
|
|
361
|
+
```
|
|
119
362
|
|
|
363
|
+
The controller permits only `title` and `description` and returns an unsaved
|
|
364
|
+
`Post`. Strings and null are accepted at the input boundary; the model requires a
|
|
365
|
+
nonblank title of at most 100 characters. Missing or null titles therefore reach
|
|
366
|
+
model validation and return 422. Unexpected input types return 400.
|
|
367
|
+
|
|
368
|
+
MK recognizes the `create` action, saves the returned model once, and converts
|
|
369
|
+
its attributes to a hash before invoking the handler.
|
|
370
|
+
|
|
371
|
+
**`sample_app4/routes/posts/handlers/create.rb`**
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
# frozen_string_literal: true
|
|
375
|
+
|
|
376
|
+
module SampleApp4
|
|
120
377
|
class PostsCreateHandler < MK::Handler
|
|
121
378
|
handler do |r|
|
|
122
379
|
r.response.status = 201
|
|
123
|
-
{
|
|
380
|
+
{message: 'Post created', post: model.slice(*Post.public_attributes_list)}
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The response is 201 with a `message` and a `post` object containing only the fields
|
|
387
|
+
in `Post.public_attributes_list`: `id`, `title`, `description`, `created_at`, and
|
|
388
|
+
`updated_at`. The handler neither saves the model nor serializes JSON itself.
|
|
389
|
+
|
|
390
|
+
**Evolve it:** add a nullable `slug` column and a unique index in a new migration,
|
|
391
|
+
then add slug validation in `models/post.rb`, permit `slug` in create/update
|
|
392
|
+
controllers, and include it in `Post.public_attributes_list` if clients need it.
|
|
393
|
+
A generated slug belongs in a model hook or controller. Keep the handler focused
|
|
394
|
+
on the response, and extend `spec/request/posts_spec.rb` to cover creation,
|
|
395
|
+
validation, and duplicate slugs.
|
|
396
|
+
|
|
397
|
+
### Example 2: list posts with optional comments
|
|
398
|
+
|
|
399
|
+
`GET /posts?comments=1&limit=10&offset=0` returns up to ten posts with their comments.
|
|
400
|
+
Omit `comments=1` to return only post attributes.
|
|
401
|
+
|
|
402
|
+
**`sample_app4/routes/posts/controllers/index.rb`**
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# frozen_string_literal: true
|
|
406
|
+
|
|
407
|
+
module SampleApp4
|
|
408
|
+
class PostsIndexController < Controller
|
|
409
|
+
route do |r|
|
|
410
|
+
page = r.page
|
|
411
|
+
posts = Post.order(:id).limit(page[:limit], page[:offset])
|
|
412
|
+
posts = posts.eager(:comments) if r.params['comments'] == '1'
|
|
413
|
+
posts.all.map do |post|
|
|
414
|
+
attributes = post.values.dup
|
|
415
|
+
if r.params['comments'] == '1'
|
|
416
|
+
attributes[:comments] = post.comments
|
|
417
|
+
end
|
|
418
|
+
attributes
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`r.page` validates pagination. Posts have a stable ID order, a default page size
|
|
426
|
+
of 25, a maximum of 100, and an offset limit of 10,000. When requested, Sequel
|
|
427
|
+
loads comments eagerly in one additional query, avoiding a separate query per
|
|
428
|
+
post. The controller selects associations explicitly and returns their data;
|
|
429
|
+
MK recursively converts the nested comment models to hashes.
|
|
430
|
+
|
|
431
|
+
**`sample_app4/routes/posts/handlers/index.rb`**
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
# frozen_string_literal: true
|
|
435
|
+
|
|
436
|
+
module SampleApp4
|
|
437
|
+
class PostsIndexHandler < MK::Handler
|
|
438
|
+
handler do |r|
|
|
439
|
+
model.map do |post|
|
|
440
|
+
attributes = post.slice(*Post.public_attributes_list)
|
|
441
|
+
if post.key?(:comments)
|
|
442
|
+
attributes[:comments] = post.fetch(:comments).map { |comment| comment.slice(*Comment.public_attributes_list) }
|
|
443
|
+
end
|
|
444
|
+
attributes
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The response is a JSON array. A post includes `comments` only when the controller
|
|
452
|
+
supplied that key; posts without comments then have `comments: []`. The handler
|
|
453
|
+
filters each supplied hash and performs no queries. Pagination bounds the number
|
|
454
|
+
of posts here, but includes all comments for those posts. Use
|
|
455
|
+
`GET /posts/:post_id/comments?limit=10&offset=0` for a paginated comment collection.
|
|
456
|
+
|
|
457
|
+
**Evolve it:** add a `published` boolean in a migration and validate/permit it in
|
|
458
|
+
the model and write actions. For a public feed, start the index query from
|
|
459
|
+
`Post.where(published: true)` before ordering, pagination, and eager loading.
|
|
460
|
+
Apply the same publication policy to show and comment routes. An authenticated
|
|
461
|
+
editor view can select a broader dataset in its controller; handlers can keep the
|
|
462
|
+
same response shape. Extend the index specs to check which posts are visible and
|
|
463
|
+
that including comments still uses two queries for a nonempty page.
|
|
464
|
+
|
|
465
|
+
To evolve the response into `{posts: [...], pagination: {...}}`, have the controller
|
|
466
|
+
return the selected posts and pagination metadata together, then update the handler
|
|
467
|
+
to filter the posts and build that envelope. Any total-count query belongs in the
|
|
468
|
+
controller. Update client expectations and request specs for the changed shape,
|
|
469
|
+
and keep the handler's no-SQL check.
|
|
470
|
+
|
|
471
|
+
### Example 3: update a comment through its post
|
|
472
|
+
|
|
473
|
+
`PATCH /posts/12/comments/34` with `{"content":"An updated reply"}` changes only
|
|
474
|
+
comment 34 belonging to post 12. Unspecified fields, such as `author`, retain
|
|
475
|
+
their stored values.
|
|
476
|
+
|
|
477
|
+
**`sample_app4/routes/comments/controllers/update.rb`**
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
# frozen_string_literal: true
|
|
481
|
+
|
|
482
|
+
module SampleApp4
|
|
483
|
+
class CommentsUpdateController < Controller
|
|
484
|
+
route do |r|
|
|
485
|
+
comments = Comment.where(id: r.path_params.fetch(:id))
|
|
486
|
+
if (post_id = r.path_params[:post_id])
|
|
487
|
+
post = Post[post_id]
|
|
488
|
+
raise MK::NotFound, 'Post not found' unless post
|
|
489
|
+
|
|
490
|
+
comments = post.comments_dataset.where(id: r.path_params.fetch(:id))
|
|
491
|
+
end
|
|
492
|
+
comment = comments.first
|
|
493
|
+
raise MK::NotFound, 'Comment not found' unless comment
|
|
494
|
+
|
|
495
|
+
comment.set(r.input.permit(content: [String, NilClass], author: [String, NilClass]))
|
|
496
|
+
comment
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
On a nested route, the controller first finds the post, then selects the comment
|
|
503
|
+
through `post.comments_dataset`. A missing post or a comment belonging to another
|
|
504
|
+
post returns 404. Only `content` and `author` can be assigned; a body `post_id`
|
|
505
|
+
cannot move the comment. The parentless compatibility route uses the initial
|
|
506
|
+
comment lookup instead.
|
|
507
|
+
|
|
508
|
+
`comment.set` assigns the permitted fields without writing. Returning the model
|
|
509
|
+
lets MK save it once for the `update` action, validate it, update its timestamp,
|
|
510
|
+
and hand raw attributes to the handler. Blank content fails validation with 422.
|
|
511
|
+
|
|
512
|
+
**`sample_app4/routes/comments/handlers/update.rb`**
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
# frozen_string_literal: true
|
|
516
|
+
|
|
517
|
+
module SampleApp4
|
|
518
|
+
class CommentsUpdateHandler < MK::Handler
|
|
519
|
+
handler do |r|
|
|
520
|
+
{message: 'Comment updated', comment: model.slice(*Comment.public_attributes_list)}
|
|
124
521
|
end
|
|
125
522
|
end
|
|
126
523
|
end
|
|
127
524
|
```
|
|
128
525
|
|
|
526
|
+
The successful response is 200 with `message` and a `comment` object containing
|
|
527
|
+
`id`, `post_id`, `content`, `author`, `created_at`, and `updated_at`.
|
|
528
|
+
|
|
529
|
+
**Evolve it:** for per-user editing, authenticate the request and select the post
|
|
530
|
+
from the signed-in user's authorized dataset before selecting its comment. Add
|
|
531
|
+
any separate comment-edit permission check in the controller. Remove the
|
|
532
|
+
parentless `resources :comments, only: ...` declaration if edits must always use
|
|
533
|
+
a post URL, or apply equivalent authorization to that branch. Extend
|
|
534
|
+
`spec/request/nested_comments_spec.rb` to cover another user's post, a wrong URL
|
|
535
|
+
parent, and a spoofed body `post_id`, confirming denied writes leave data intact.
|
|
536
|
+
The handler can remain unchanged.
|
|
537
|
+
|
|
538
|
+
## Controllers prepare; the framework persists; handlers respond
|
|
539
|
+
|
|
540
|
+
The three pairs above share the same boundary: controllers own queries, input
|
|
541
|
+
assignment, and access rules; handlers own public fields, status, and response
|
|
542
|
+
shape. `Post.public_attributes_list` and `Comment.public_attributes_list` are
|
|
543
|
+
application-defined field lists. The sample's `handler_boundary_spec.rb` checks
|
|
544
|
+
that every handler runs without issuing SQL.
|
|
545
|
+
|
|
129
546
|
Requiring `mk_framework/sequel` enables this lifecycle for a controller's returned
|
|
130
547
|
Sequel model, using the registered route action:
|
|
131
548
|
|
|
@@ -215,10 +632,10 @@ bundle exec rake
|
|
|
215
632
|
bundle exec rake build
|
|
216
633
|
```
|
|
217
634
|
|
|
218
|
-
The gem is written to `pkg/mk_framework-0.2.
|
|
635
|
+
The gem is written to `pkg/mk_framework-0.2.2.gem`. See
|
|
219
636
|
[deployment](docs/deployment.md) for migrations, connections, authentication,
|
|
220
637
|
timeouts, logging, and release verification, and [upgrading](docs/upgrading.md)
|
|
221
|
-
for changes from the prototype. CI
|
|
638
|
+
for changes from the prototype. CI runs on Ruby 4.0 on Linux.
|
|
222
639
|
|
|
223
640
|
Applications can optionally `require 'mk_framework/testing'` and include
|
|
224
641
|
`MK::Framework::Spec` in RSpec to use Rack::Test and the `resp` JSON helper.
|
data/bin/mk_frame_init
ADDED
data/docs/deployment.md
CHANGED
|
@@ -98,8 +98,8 @@ and access permissions are deployment decisions.
|
|
|
98
98
|
## Release procedure
|
|
99
99
|
|
|
100
100
|
1. Run `bundle install` and `bundle exec rake` with frozen lockfiles in CI.
|
|
101
|
-
2. Require the Linux Ruby
|
|
102
|
-
|
|
101
|
+
2. Require the Linux Ruby 4.0 CI job to pass before publishing. Local results are
|
|
102
|
+
not a substitute for that CI check.
|
|
103
103
|
3. Run `bundle exec rake build`. Inspect and install the generated gem in a clean
|
|
104
104
|
environment, including `require 'mk_framework'` without Sequel and the optional
|
|
105
105
|
`require 'mk_framework/sequel'` with its dependency installed.
|
|
@@ -108,7 +108,7 @@ and access permissions are deployment decisions.
|
|
|
108
108
|
5. Update the version/changelog and publish the reviewed gem using an authorized
|
|
109
109
|
RubyGems account. The gem metadata requires MFA. No publish task runs automatically.
|
|
110
110
|
|
|
111
|
-
CI runs framework/request tests and builds the gem on Ruby
|
|
111
|
+
CI runs framework/request tests and builds the gem on Ruby 4.0 on Linux.
|
|
112
112
|
The framework root `rake` command runs its standalone specs. The separate sample
|
|
113
113
|
repository runs its integration tests and all seven application suites against the
|
|
114
114
|
published gem, isolating Bundler Gemfile and lockfile paths for each child and
|