mk_framework 0.2.3 → 0.2.5
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 +6 -0
- data/README.md +218 -36
- data/lib/mk_framework/generator/templates/Gemfile.erb +4 -4
- data/lib/mk_framework/generator/templates/controller.rb.erb +2 -1
- data/lib/mk_framework/generator/templates/database.rb.erb +1 -1
- data/lib/mk_framework/generator/templates/model.rb.erb +3 -3
- data/lib/mk_framework/generator/templates/request_spec.rb.erb +31 -55
- data/lib/mk_framework/generator/templates/spec_helper.rb.erb +2 -0
- data/lib/mk_framework/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28e516ad56933bcfe68ce986d9bc872148ef289ef9fabfe90f9315786fae471e
|
|
4
|
+
data.tar.gz: ab4fda8fc53f07a5fd2df9921c40fc95182caf6be21542207d06c7716f0e255d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2eeebeca751cec2bc9f5a372d4a927d72fcfc28fb86e859c56f0557e905067f3c4dabf6dc57ff28ea118d7d5813c696857a89c75ac7abe4441581a0f6c8efc03
|
|
7
|
+
data.tar.gz: 14dc30546bdbecd0e3eb3bf6e386565c7fc09813353c7b79d2792185c8a15689518221c7e7e4f136f893834a51f4691a90cc0e802f04e28286c440fbdc7e5939
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.5 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Generate Gemfile dependencies in alphabetical order within each group.
|
|
6
|
+
- Align generated database connection keyword arguments for RuboCop.
|
|
7
|
+
- Generate bracketed symbol arrays in model validations and public attribute lists.
|
|
8
|
+
|
|
3
9
|
## 0.2.3 — 2026-09-12
|
|
4
10
|
|
|
5
11
|
- Use `rack-test` and `Rack::Test::Methods` in generated request specs.
|
data/README.md
CHANGED
|
@@ -123,7 +123,7 @@ blog_api/
|
|
|
123
123
|
explicit and run before models load. Tests always migrate a private in-memory
|
|
124
124
|
database. The controller permits the chosen fields and returns `Post.new(...)`;
|
|
125
125
|
MK saves once, then the handler returns `{post: ...}` with status 201. Index returns
|
|
126
|
-
`{posts: [...]}
|
|
126
|
+
`{posts: [...]}` with bounded `limit`/`offset` pagination; show, update, and delete return `{post: ...}` with status 200.
|
|
127
127
|
PATCH and PUT preserve omitted fields; missing records return 404. The generated
|
|
128
128
|
README includes a local server command, an example request, and extension guidance.
|
|
129
129
|
|
|
@@ -140,10 +140,10 @@ task in another project's Rakefile, add `require 'mk_framework/generator/tasks'`
|
|
|
140
140
|
|
|
141
141
|
## Try the examples
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
Twenty paired sample applications (JSON APIs and MkFrame frontends) live in
|
|
144
144
|
[mk_framework_sample_apps](https://github.com/makevoid/mk_framework_sample_apps)
|
|
145
|
-
and install MK from RubyGems.
|
|
146
|
-
|
|
145
|
+
and install MK from RubyGems. Each sample has its own README, database migrations
|
|
146
|
+
and isolated request specs. Backend use does not require a framework source checkout.
|
|
147
147
|
|
|
148
148
|
From a sample-app checkout:
|
|
149
149
|
|
|
@@ -166,9 +166,22 @@ stub HTTP and require no personal API key or internet connection.
|
|
|
166
166
|
| [2](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app2/README.md) | Todo validation and request specs |
|
|
167
167
|
| [3](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app3/README.md) | Custom response envelopes |
|
|
168
168
|
| [4](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app4/README.md) | Blog posts, nested comments, parent scoping |
|
|
169
|
-
| [5](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app5/README.md) | Kanban cards,
|
|
169
|
+
| [5](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app5/README.md) | Kanban cards, validation, nested comments |
|
|
170
170
|
| [6](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app6/README.md) | Weather client, deadlines, atomic cache refresh |
|
|
171
|
-
| [7](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app7/README.md) |
|
|
171
|
+
| [7](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app7/README.md) | Kanban board, ordering, priorities, filters, archive and comments |
|
|
172
|
+
| [8](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app8/README.md) | Single-user ecommerce, product image uploads and a persistent cart |
|
|
173
|
+
| [9](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app9/README.md) | Fictional electric car inventory, charging specs and reservations |
|
|
174
|
+
| [10](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app10/README.md) | Buildlog developer journal, project portfolio and private publishing workbench |
|
|
175
|
+
| [11](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app11/README.md) | Rolecraft job board, employer submissions, moderation and private applications |
|
|
176
|
+
| [12](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app12/README.md) | Reboot Market electronics classifieds, photo uploads, private inquiries, saved searches and moderation |
|
|
177
|
+
| [13](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app13/README.md) | Pinpoint map directory, geographic filters, contributor history and transactional approval |
|
|
178
|
+
| [14](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app14/README.md) | Studio Hours timed resource bookings, Zurich availability, maintenance and transactional conflict prevention |
|
|
179
|
+
| [15](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app15/README.md) | Pantry Table recipes, private meal plans, pantry stock and derived shopping quantities |
|
|
180
|
+
| [16](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app16/README.md) | Assembly community events, workshop capacity, FIFO waitlists, organizer management and check-in |
|
|
181
|
+
| [17](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app17/README.md) | Fairshare private expense groups, exact splits, receipts, balances and reimbursements |
|
|
182
|
+
| [18](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app18/README.md) | Signalboard feedback, voting, moderation, duplicate merges, roadmap and releases |
|
|
183
|
+
| [19](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app19/README.md) | Night Archive NASA discovery, private collections and observing journal |
|
|
184
|
+
| [20](https://github.com/makevoid/mk_framework_sample_apps/blob/main/sample_app20/README.md) | Fieldwork private plant collection, care schedules and growth journals |
|
|
172
185
|
|
|
173
186
|
## Walkthrough: sample app 4, a blog API
|
|
174
187
|
|
|
@@ -186,16 +199,14 @@ parent scoping checks the relationship, while user access rules belong in your a
|
|
|
186
199
|
|
|
187
200
|
### Directory structure
|
|
188
201
|
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
The samples share database and Rake helpers in the repository-level `support/`
|
|
203
|
+
directory. Keep that directory when copying a sample; use the CLI above for a
|
|
204
|
+
self-contained starter. This tree lists
|
|
191
205
|
the six action files explained below; the sample also includes the remaining CRUD
|
|
192
206
|
controllers and handlers for both resources.
|
|
193
207
|
|
|
194
208
|
```text
|
|
195
209
|
sample_app4/
|
|
196
|
-
├── support/
|
|
197
|
-
│ ├── database.rb # SampleApp4::Database: connections and migrations
|
|
198
|
-
│ └── tasks.rb # SampleApp4::Tasks: db:migrate, routes, and specs
|
|
199
210
|
├── Gemfile
|
|
200
211
|
├── Rakefile
|
|
201
212
|
├── database.rb # SampleApp4::ROOT and SampleApp4::DB
|
|
@@ -235,7 +246,7 @@ and request-test dependencies:
|
|
|
235
246
|
```ruby
|
|
236
247
|
source 'https://rubygems.org'
|
|
237
248
|
|
|
238
|
-
gem 'mk_framework', '~> 0.2.
|
|
249
|
+
gem 'mk_framework', '~> 0.2.2'
|
|
239
250
|
gem 'sequel', '>= 5.92', '< 6'
|
|
240
251
|
gem 'sqlite3', '~> 2.9'
|
|
241
252
|
gem 'rake', '~> 13.4'
|
|
@@ -257,8 +268,8 @@ bundle exec rake routes
|
|
|
257
268
|
bundle exec rspec
|
|
258
269
|
```
|
|
259
270
|
|
|
260
|
-
`Rakefile` loads
|
|
261
|
-
`
|
|
271
|
+
`Rakefile` loads `../support/tasks.rb` and installs the shared tasks with
|
|
272
|
+
`SampleTasks.install(__dir__)`.
|
|
262
273
|
`db:migrate` loads `database.rb` and applies `db/migrations/001_initial.rb` before
|
|
263
274
|
any models are loaded. The migration creates `posts` and `comments`, including
|
|
264
275
|
required timestamps and a non-null `comments.post_id` foreign key with cascading
|
|
@@ -269,15 +280,15 @@ deletion. Schema changes are an explicit step; starting the app never migrates i
|
|
|
269
280
|
```ruby
|
|
270
281
|
# frozen_string_literal: true
|
|
271
282
|
|
|
272
|
-
require_relative 'support/database'
|
|
283
|
+
require_relative '../support/database'
|
|
273
284
|
|
|
274
285
|
module SampleApp4
|
|
275
286
|
ROOT = __dir__.freeze
|
|
276
|
-
DB =
|
|
287
|
+
DB = SampleDatabase.connect(root: ROOT, filename: 'blog.db')
|
|
277
288
|
end
|
|
278
289
|
```
|
|
279
290
|
|
|
280
|
-
`
|
|
291
|
+
`SampleDatabase.connect` defaults to `sample_app4/blog.db`, using an absolute path.
|
|
281
292
|
It accepts `DATABASE_URL`, `DB_POOL_SIZE`, and `DB_POOL_TIMEOUT` for deployment.
|
|
282
293
|
In tests it always opens a private in-memory SQLite database; `spec_helper.rb`
|
|
283
294
|
migrates that database before requiring `app.rb`.
|
|
@@ -599,25 +610,196 @@ Known paths with unsupported methods return 405 and `Allow`; unknown paths retur
|
|
|
599
610
|
Read [nested routes and authorization](docs/routing.md) for scopes, namespaces,
|
|
600
611
|
shallow routes, custom actions, explicit class mappings, and parent ownership.
|
|
601
612
|
|
|
602
|
-
##
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
613
|
+
## Working with the request (`r`)
|
|
614
|
+
|
|
615
|
+
`r` is the current Roda request, extended with MK's input and pagination helpers.
|
|
616
|
+
Controllers use `route do |r|` to load records and prepare changes; handlers use
|
|
617
|
+
`handler do |r|` to format the resulting raw data and choose a response status.
|
|
618
|
+
The snippets below belong inside the application's namespace, after defining its
|
|
619
|
+
models. Define routes and configuration before calling `App.boot!`.
|
|
620
|
+
|
|
621
|
+
### Declare routes
|
|
622
|
+
|
|
623
|
+
Inside your `MK::Application` subclass:
|
|
624
|
+
|
|
625
|
+
```ruby
|
|
626
|
+
configure root: ROOT, namespace: Blog, legacy_post_routes: false
|
|
627
|
+
|
|
628
|
+
resource_routes do
|
|
629
|
+
resources :posts do
|
|
630
|
+
resources :comments
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
`resources :posts` maps the CRUD URLs in the table above to
|
|
636
|
+
`PostsIndexController`/`PostsIndexHandler`, `PostsShowController`/`PostsShowHandler`,
|
|
637
|
+
and the corresponding create, update and delete pairs. Files live under
|
|
638
|
+
`routes/posts/controllers/` and `routes/posts/handlers/`. Nested comments still
|
|
639
|
+
use action files under `routes/comments/`; nesting changes the URL, not the files.
|
|
640
|
+
|
|
641
|
+
Restrict actions or add a named endpoint explicitly:
|
|
642
|
+
|
|
643
|
+
```ruby
|
|
644
|
+
resource_routes do
|
|
645
|
+
resources :posts, only: %i[index show] do
|
|
646
|
+
member :publish, via: :post
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
The member action maps `POST /posts/:id/publish` to
|
|
652
|
+
`PostsPublishController` and `PostsPublishHandler`. Supply both classes before
|
|
653
|
+
boot. Custom actions do not automatically save their returned models; persistence
|
|
654
|
+
must be explicit. Inspect the compiled mapping with `puts App.route_table`.
|
|
655
|
+
|
|
656
|
+
### URL identifiers: `r.path_params`
|
|
657
|
+
|
|
658
|
+
For `PUT /posts/12/comments/34`, the nested declaration captures:
|
|
659
|
+
|
|
660
|
+
```ruby
|
|
661
|
+
r.path_params # => { post_id: '12', id: '34' }
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
This is a frozen hash with symbol keys and string values. IDs may be slugs or
|
|
665
|
+
UUIDs; MK does not coerce them to integers. Collection routes contain parent IDs
|
|
666
|
+
but no member `:id`. Body and query fields cannot overwrite these captures.
|
|
667
|
+
|
|
668
|
+
Load a nested record through its parent in the controller:
|
|
669
|
+
|
|
670
|
+
```ruby
|
|
671
|
+
route do |r|
|
|
672
|
+
post = Post[r.path_params.fetch(:post_id)] or raise MK::NotFound
|
|
673
|
+
comment = post.comments_dataset.where(id: r.path_params.fetch(:id)).first
|
|
674
|
+
raise MK::NotFound unless comment
|
|
675
|
+
|
|
676
|
+
comment.set(r.input.permit(content: String))
|
|
677
|
+
end
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
The URL describes the relationship; it does not load or authorize a parent.
|
|
681
|
+
In an authenticated app, first scope the parent query to the current user's
|
|
682
|
+
allowed records. Assign foreign keys from that parent, not from client input.
|
|
683
|
+
|
|
684
|
+
### Body and query fields: `r.input`
|
|
685
|
+
|
|
686
|
+
Use `permit` to select fields and validate their types. It returns a symbol-keyed
|
|
687
|
+
hash, drops unknown fields, and omits fields the client did not send:
|
|
688
|
+
|
|
689
|
+
```ruby
|
|
690
|
+
route do |r|
|
|
691
|
+
attributes = r.input.permit(
|
|
692
|
+
title: String,
|
|
693
|
+
description: [String, NilClass],
|
|
694
|
+
published: :boolean
|
|
695
|
+
)
|
|
696
|
+
Post.new(attributes)
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
`permit` does not require a field to be present. Model validation can reject a
|
|
701
|
+
missing title with 422 when MK saves the returned model. A present value of the
|
|
702
|
+
wrong type raises `MK::BadRequest` (400). `NilClass` explicitly permits JSON null;
|
|
703
|
+
`String` alone does not. Length, uniqueness and other domain rules belong in the
|
|
704
|
+
model or controller. Nested objects are not recursively allowlisted for you.
|
|
705
|
+
|
|
706
|
+
For a required input, call `require` (its default type is `String`):
|
|
707
|
+
|
|
708
|
+
```ruby
|
|
709
|
+
query = r.input.require(:query)
|
|
710
|
+
quantity = r.input.require(:quantity, type: Integer)
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
A missing key or invalid type returns 400. An empty string still has type String;
|
|
714
|
+
check whether it is meaningful in your application. `Integer` accepts a JSON
|
|
715
|
+
integer, not the query string `"3"`. Use the integer helper for numeric query
|
|
716
|
+
parameters or form fields:
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
quantity = r.input.integer(:quantity, default: 1, min: 1, max: 20)
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
This accepts integers and strings of decimal digits, applies the default only
|
|
723
|
+
when the field is absent, and returns 400 for invalid values or bounds. It does
|
|
724
|
+
not silently clamp them. `:boolean` accepts JSON `true`/`false` and the strings
|
|
725
|
+
`"true"`, `"false"`, `"1"`, `"0"`; false is retained in permitted attributes.
|
|
726
|
+
Dates arrive as strings: parse them explicitly, as the CLI's date fields do.
|
|
727
|
+
|
|
728
|
+
`r.input` reads body/query input separately from path captures. The compatibility
|
|
729
|
+
`r.params` hash combines input with string-keyed path captures, and path values
|
|
730
|
+
win on collision. Prefer `r.path_params` for identity and `r.input` for attributes.
|
|
731
|
+
|
|
732
|
+
### Bounded pagination: `r.page`
|
|
733
|
+
|
|
734
|
+
For `GET /posts?limit=10&offset=20`, `r.page` returns
|
|
735
|
+
`{ limit: 10, offset: 20 }`. Apply it to an ordered query:
|
|
736
|
+
|
|
737
|
+
```ruby
|
|
738
|
+
route do |r|
|
|
739
|
+
page = r.page
|
|
740
|
+
Post.order(:id).limit(page[:limit], page[:offset])
|
|
741
|
+
end
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
The helper validates parameters; it does not query the database or add response
|
|
745
|
+
metadata. Defaults are `limit: 25` and `offset: 0`. Allowed limits are 1–100 and
|
|
746
|
+
allowed offsets are 0–10,000. Invalid inputs return 400. Override the bounds in
|
|
747
|
+
the application before boot:
|
|
748
|
+
|
|
749
|
+
```ruby
|
|
750
|
+
configure page_size: 20, max_page_size: 50, max_offset: 5_000
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
All three settings must be positive integers, and `page_size` cannot exceed
|
|
754
|
+
`max_page_size`. Controllers that include `MK::Persistence` can instead use
|
|
755
|
+
`paginate(Post.dataset, r)`, which orders by `id`, applies these bounds and returns
|
|
756
|
+
an array. For large datasets, implement cursor pagination in your application.
|
|
757
|
+
|
|
758
|
+
### Response status, headers and early returns
|
|
759
|
+
|
|
760
|
+
Handlers receive prepared raw data through `model` and return hashes or arrays.
|
|
761
|
+
MK serializes the response; do not call `to_json` in a handler:
|
|
762
|
+
|
|
763
|
+
```ruby
|
|
764
|
+
handler do |r|
|
|
765
|
+
r.response.status = 201
|
|
766
|
+
r.response['location'] = "/posts/#{model.fetch(:id)}"
|
|
767
|
+
{ post: model.slice(:id, :title) }
|
|
768
|
+
end
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Use an MK error for an intentional failure such as `raise MK::NotFound` or
|
|
772
|
+
`raise MK::Forbidden`. Use `r.halt` when you need an explicit early response:
|
|
773
|
+
|
|
774
|
+
```ruby
|
|
775
|
+
r.halt(409, { error: 'Post is already published' })
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Request-scoped application data can be stored in `r.env`, for example a verified
|
|
779
|
+
user set by `before_request`. Native Roda routes also work and run before resource
|
|
780
|
+
routes; return `nil` when they should fall through:
|
|
781
|
+
|
|
782
|
+
```ruby
|
|
783
|
+
route do |r|
|
|
784
|
+
r.get('health') { { ok: true } }
|
|
785
|
+
nil
|
|
786
|
+
end
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
See [the routing guide](docs/routing.md) for authentication hooks, scopes,
|
|
790
|
+
namespaces, shallow routes and custom action mappings.
|
|
791
|
+
|
|
792
|
+
## Errors and request limits
|
|
793
|
+
|
|
794
|
+
JSON bodies must be objects. Malformed JSON and malformed query inputs produce
|
|
795
|
+
400. Bodies above 1 MiB produce 413, including bodies without a content length;
|
|
796
|
+
`max_body_bytes` configures the limit.
|
|
797
|
+
|
|
798
|
+
`MK::BadRequest`, `MK::Unauthorized`, `MK::Forbidden`, `MK::NotFound`,
|
|
799
|
+
`MK::Conflict`, `MK::ValidationError` and `MK::BadGateway` represent intentional
|
|
800
|
+
public errors. Their messages are public, so never put secrets in them.
|
|
801
|
+
Unexpected errors return JSON with `error: "Server error"` and a request ID.
|
|
802
|
+
The same ID appears in `X-Request-ID` and the structured error log.
|
|
621
803
|
|
|
622
804
|
Production is the safe default. Set `RACK_ENV=development` explicitly for debug
|
|
623
805
|
details. Configure a logger using `configure logger: Logger.new($stdout)`, or
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
3
|
gem 'mk_framework', '~> <%= MK::VERSION %>'
|
|
4
|
+
gem 'puma', '~> 8.0'
|
|
5
|
+
gem 'rackup', '~> 2.3'
|
|
6
|
+
gem 'rake', '~> 13.4'
|
|
4
7
|
gem 'sequel', '>= 5.92', '< 6'
|
|
5
8
|
gem 'sqlite3', '~> 2.9'
|
|
6
|
-
gem 'rake', '~> 13.4'
|
|
7
|
-
gem 'rackup', '~> 2.3'
|
|
8
|
-
gem 'puma', '~> 8.0'
|
|
9
9
|
|
|
10
10
|
group :test do
|
|
11
|
-
gem 'rspec', '~> 3.13'
|
|
12
11
|
gem 'rack-test', '~> 2.2'
|
|
12
|
+
gem 'rspec', '~> 3.13'
|
|
13
13
|
end
|
|
@@ -8,7 +8,8 @@ module <%= config.namespace %>
|
|
|
8
8
|
class <%= config.action_prefix %><%= action.capitalize %>Controller < MK::Controller
|
|
9
9
|
route do |r|
|
|
10
10
|
<% if action == 'index' -%>
|
|
11
|
-
|
|
11
|
+
page = r.page
|
|
12
|
+
<%= config.model_class %>.order(:id).limit(page[:limit], page[:offset])
|
|
12
13
|
<% elsif %w[show delete].include?(action) -%>
|
|
13
14
|
<%= config.model_class %>[r.path_params.fetch(:id)] or raise MK::NotFound
|
|
14
15
|
<% else -%>
|
|
@@ -12,6 +12,6 @@ module <%= config.namespace %>
|
|
|
12
12
|
else
|
|
13
13
|
url = ENV.fetch('DATABASE_URL') { "sqlite://#{File.join(ROOT, '<%= config.app_name %>.db')}" }
|
|
14
14
|
Sequel.connect(url, max_connections: Integer(ENV.fetch('DB_POOL_SIZE', '5')),
|
|
15
|
-
|
|
15
|
+
pool_timeout: Integer(ENV.fetch('DB_POOL_TIMEOUT', '5')))
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -7,15 +7,15 @@ module <%= config.namespace %>
|
|
|
7
7
|
|
|
8
8
|
def validate
|
|
9
9
|
super
|
|
10
|
-
validates_not_null
|
|
10
|
+
validates_not_null <%= config.field_names.map(&:to_sym).inspect %>
|
|
11
11
|
<% text_fields = config.fields.select { |field| %w[string text].include?(field[:type]) }.map { |field| field[:name] } -%>
|
|
12
12
|
<% unless text_fields.empty? -%>
|
|
13
|
-
validates_presence
|
|
13
|
+
validates_presence <%= text_fields.map(&:to_sym).inspect %>
|
|
14
14
|
<% end -%>
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.public_attributes_list
|
|
18
|
-
|
|
18
|
+
<%= ([:id] + config.field_names.map(&:to_sym) + [:created_at, :updated_at]).inspect %>
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../spec_helper'
|
|
4
|
+
<% if config.namespace == config.model_class -%>
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
module <%= config.namespace %>
|
|
7
|
+
<% end -%>
|
|
8
|
+
|
|
9
|
+
describe '<%= config.model_class %> CRUD' do
|
|
6
10
|
include Rack::Test::Methods
|
|
7
11
|
|
|
8
12
|
let(:app) { Rack::Builder.parse_file(File.join(<%= config.namespace %>::ROOT, 'config.ru')) }
|
|
9
13
|
let(:attributes) { JSON.parse(<%= config.example.to_json.inspect %>) }
|
|
10
14
|
|
|
11
15
|
before do
|
|
12
|
-
<%= config.
|
|
16
|
+
<%= config.model_class %>.dataset.delete
|
|
13
17
|
header 'CONTENT_TYPE', 'application/json'
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
def create_record
|
|
21
|
+
post('/<%= config.resource %>', attributes.to_json)
|
|
22
|
+
expect(last_response.status).to eq(201)
|
|
23
|
+
JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'creates a record with POST /<%= config.resource %>' do
|
|
17
27
|
post('/<%= config.resource %>', attributes.merge('id' => 999).to_json)
|
|
18
28
|
|
|
19
29
|
expect(last_response.status).to eq(201)
|
|
20
|
-
record = <%= config.
|
|
30
|
+
record = <%= config.model_class %>.first
|
|
21
31
|
expect(record).not_to be_nil
|
|
22
32
|
expect(record.id).not_to eq(999)
|
|
23
33
|
payload = JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
@@ -25,37 +35,18 @@ RSpec.describe '<%= config.model_class %> CRUD' do
|
|
|
25
35
|
expect(payload.keys).to match_array(%w[id <%= config.field_names.join(' ') %> created_at updated_at])
|
|
26
36
|
end
|
|
27
37
|
|
|
28
|
-
it '
|
|
29
|
-
post('/<%= config.resource %>', {}.to_json)
|
|
30
|
-
|
|
31
|
-
expect(last_response.status).to eq(422)
|
|
32
|
-
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
it 'rejects an invalid field type without inserting a record' do
|
|
36
|
-
attributes['<%= config.fields.first[:name] %>'] = []
|
|
37
|
-
post('/<%= config.resource %>', attributes.to_json)
|
|
38
|
-
|
|
39
|
-
expect(last_response.status).to eq(400)
|
|
40
|
-
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def create_record
|
|
44
|
-
post('/<%= config.resource %>', attributes.to_json)
|
|
45
|
-
expect(last_response.status).to eq(201)
|
|
46
|
-
JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
it 'lists records in ID order, including an empty collection' do
|
|
38
|
+
it 'lists records with GET /<%= config.resource %>' do
|
|
50
39
|
get('/<%= config.resource %>')
|
|
51
40
|
expect(JSON.parse(last_response.body)).to eq('<%= config.resource %>' => [])
|
|
52
|
-
records = [create_record, create_record]
|
|
41
|
+
records = [create_record, create_record, create_record]
|
|
53
42
|
get('/<%= config.resource %>')
|
|
54
43
|
expect(last_response.status).to eq(200)
|
|
55
44
|
expect(JSON.parse(last_response.body).fetch('<%= config.resource %>')).to eq(records)
|
|
45
|
+
get('/<%= config.resource %>?limit=1&offset=1')
|
|
46
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.resource %>')).to eq([records[1]])
|
|
56
47
|
end
|
|
57
48
|
|
|
58
|
-
it 'shows a
|
|
49
|
+
it 'shows a record with GET /<%= config.resource %>/:id' do
|
|
59
50
|
record = create_record
|
|
60
51
|
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
61
52
|
expect(last_response.status).to eq(200)
|
|
@@ -63,11 +54,10 @@ RSpec.describe '<%= config.model_class %> CRUD' do
|
|
|
63
54
|
end
|
|
64
55
|
|
|
65
56
|
<% updated_value = { 'string' => 'Updated', 'text' => 'Updated text', 'integer' => 2, 'float' => 2.5, 'boolean' => true, 'date' => '2027-02-03', 'datetime' => '2027-02-03T14:30:00Z' }.fetch(config.fields.first[:type]) -%>
|
|
66
|
-
it 'updates
|
|
57
|
+
it 'updates a record with PUT /<%= config.resource %>/:id' do
|
|
67
58
|
record = create_record
|
|
68
59
|
changes = { '<%= config.fields.first[:name] %>' => <%= updated_value.inspect %>, 'id' => 999 }
|
|
69
|
-
put("/<%= config.resource %>/#{record.fetch('id')}",
|
|
70
|
-
changes.to_json)
|
|
60
|
+
put("/<%= config.resource %>/#{record.fetch('id')}", changes.to_json)
|
|
71
61
|
expect(last_response.status).to eq(200)
|
|
72
62
|
payload = JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
73
63
|
expect(payload.fetch('id')).to eq(record.fetch('id'))
|
|
@@ -79,36 +69,22 @@ RSpec.describe '<%= config.model_class %> CRUD' do
|
|
|
79
69
|
expect(payload.except('<%= config.fields.first[:name] %>', 'updated_at')).to eq(record.except('<%= config.fields.first[:name] %>', 'updated_at'))
|
|
80
70
|
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
81
71
|
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(payload)
|
|
82
|
-
expect(<%= config.
|
|
83
|
-
end
|
|
72
|
+
expect(<%= config.model_class %>.count).to eq(1)
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{ '<%= config.fields.first[:name] %>' => value }.to_json)
|
|
90
|
-
expect(last_response.status).to eq(status)
|
|
91
|
-
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
92
|
-
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
93
|
-
end
|
|
74
|
+
put("/<%= config.resource %>/#{record.fetch('id')}", { '<%= config.fields.first[:name] %>' => [] }.to_json)
|
|
75
|
+
expect(last_response.status).to eq(400)
|
|
76
|
+
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
77
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(payload)
|
|
94
78
|
end
|
|
95
79
|
|
|
96
|
-
it 'deletes a record
|
|
80
|
+
it 'deletes a record with DELETE /<%= config.resource %>/:id' do
|
|
97
81
|
record = create_record
|
|
98
82
|
delete("/<%= config.resource %>/#{record.fetch('id')}")
|
|
99
83
|
expect(last_response.status).to eq(200)
|
|
100
84
|
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
101
|
-
expect(<%= config.
|
|
102
|
-
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
103
|
-
expect(last_response.status).to eq(404)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
%w[get put delete].each do |verb|
|
|
107
|
-
it "returns 404 for #{verb.upcase} of a missing record" do
|
|
108
|
-
public_send(verb, '/<%= config.resource %>/999999',
|
|
109
|
-
attributes.to_json)
|
|
110
|
-
expect(last_response.status).to eq(404)
|
|
111
|
-
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
112
|
-
end
|
|
85
|
+
expect(<%= config.model_class %>.count).to eq(0)
|
|
113
86
|
end
|
|
114
87
|
end
|
|
88
|
+
<% if config.namespace == config.model_class -%>
|
|
89
|
+
end
|
|
90
|
+
<% end -%>
|
|
@@ -9,6 +9,8 @@ require_relative '../database'
|
|
|
9
9
|
Sequel::Migrator.run(<%= config.namespace %>::DB, File.join(<%= config.namespace %>::ROOT, 'db/migrations'))
|
|
10
10
|
require_relative '../app'
|
|
11
11
|
|
|
12
|
+
include <%= config.namespace %>
|
|
13
|
+
|
|
12
14
|
RSpec.configure do |config|
|
|
13
15
|
config.after(:suite) { <%= config.namespace %>::DB.disconnect }
|
|
14
16
|
end
|
data/lib/mk_framework/version.rb
CHANGED