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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f37bb87398d93ff1849ae3df5cd7efac0632ebe204505aca673a8394a65d71c8
4
- data.tar.gz: 93574025a9c4489ac0b395b968370e48679741a0aa40c229d861023209c78e60
3
+ metadata.gz: 116dd1b29e32f3a469fc2806aab54c975373ea40188a91025e89d4ba2a3a45cd
4
+ data.tar.gz: 38ba4c1925e8be343c384e527d17ec0509082f24e4ddbd891f016cdf1ea5f1f4
5
5
  SHA512:
6
- metadata.gz: a589434c614336c30e21ccd5f319eb08a1ab98dac0733f4c7ec243cc0beee58faa0abe654b2cec5a56b27980c73329c8bd1a70c1b0f13dd17125feaa50530047
7
- data.tar.gz: 9d855021ee2a3cebb77f3160292898ad676c8dd79c169c6001651c40d74487a909280f16f6442b569f7ab5edf96433e5274de932605fda737c697c428a393765
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.0
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.0'
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
- ## An application
173
+ ## Walkthrough: sample app 4, a blog API
70
174
 
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.
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
- require 'mk_framework'
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: __dir__, namespace: Blog
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
- In `config.ru`:
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 Blog::App.app
337
+ run SampleApp4::App.app
98
338
  ```
99
339
 
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.
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
- ## Controllers prepare; the framework persists; handlers respond
344
+ ### Example 1: create a post
106
345
 
107
- These action files assume your application has explicitly required its `Post`
108
- model, backed by a migrated Sequel dataset, before calling `boot!`.
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
- require 'mk_framework/sequel'
352
+ # frozen_string_literal: true
112
353
 
113
- module Blog
114
- class PostsCreateController < MK::Controller
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
- {post: fields(model, :id, :title, :description)}
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.0.gem`. See
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 covers Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'mk_framework/generator'
5
+ exit MK::Generator::CLI.run
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 matrix to pass before publishing. Local results on one
102
- Ruby version are not a substitute for that matrix.
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 3.2, 3.3, 3.4, and 4.0.
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