mk_framework 0.2.0 → 0.2.1

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