mk_framework 0.2.1 → 0.2.3
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 +18 -0
- data/README.md +17 -11
- data/lib/mk_framework/generator/cli.rb +3 -3
- data/lib/mk_framework/generator/configuration.rb +2 -2
- data/lib/mk_framework/generator/project.rb +8 -3
- data/lib/mk_framework/generator/templates/README.md.erb +26 -10
- data/lib/mk_framework/generator/templates/Rakefile.erb +8 -1
- data/lib/mk_framework/generator/templates/app.rb.erb +1 -1
- data/lib/mk_framework/generator/templates/controller.rb.erb +15 -2
- data/lib/mk_framework/generator/templates/database.rb.erb +2 -0
- data/lib/mk_framework/generator/templates/handler.rb.erb +8 -2
- data/lib/mk_framework/generator/templates/request_spec.rb.erb +88 -12
- data/lib/mk_framework/generator/templates/spec_helper.rb.erb +1 -1
- 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: c68f913eebaf1aa8a9b4d423b528e4720a6508b5f0f130c03b15d039458ec6d1
|
|
4
|
+
data.tar.gz: be858c42ce6d158eb5e658bb9a7e20ec9a775795f0f5956b03100ee733dd4c2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79a22c1dcd019cfcb8ad3653a9fdd84c4246de7cdc95faa21132be72dc1306285c49bc5a37f29a3bdd64342718024ac126fcf33e7fe38dcdbde673dca3e73536
|
|
7
|
+
data.tar.gz: f42544ff916968bf3cfc5152b9ed3cd866050b301407e5f1b840637db4b73226f1a3a604b36102b0195b8a9e584c4e7ab90cb590afee744bf9462b1b0ac79538
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Use `rack-test` and `Rack::Test::Methods` in generated request specs.
|
|
6
|
+
- Simplify generated update specs to use PUT and serialize request bodies with
|
|
7
|
+
`.to_json`.
|
|
8
|
+
- Format generated Ruby hashes with spaces inside nonempty braces.
|
|
9
|
+
|
|
10
|
+
## 0.2.2 — 2026-09-08
|
|
11
|
+
|
|
12
|
+
- Generate full CRUD resources: index, show, create, update (PATCH and PUT), and
|
|
13
|
+
delete, with separate controllers and handlers for every action.
|
|
14
|
+
- Make generated apps' default `rake` task invoke `rake dev` and start Puma on
|
|
15
|
+
`127.0.0.1:3000`, with `HOST` and `PORT` overrides.
|
|
16
|
+
- Include generated request specs for reads, partial updates, deletion, missing
|
|
17
|
+
records, validation failures, and persistence; verify all supported field types.
|
|
18
|
+
- Store generated-app timestamps in UTC so datetime updates round-trip consistently.
|
|
19
|
+
- Update generator prompts, next steps, and documentation for the complete app.
|
|
20
|
+
|
|
3
21
|
## 0.2.1 — 2026-09-08
|
|
4
22
|
|
|
5
23
|
- Add `mk_frame_init` with interactive prompts and a non-interactive `--cli`
|
data/README.md
CHANGED
|
@@ -21,14 +21,14 @@ 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.3
|
|
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.3'
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run `bundle install`. Add `sequel` and your database driver if you use
|
|
@@ -36,11 +36,11 @@ Run `bundle install`. Add `sequel` and your database driver if you use
|
|
|
36
36
|
|
|
37
37
|
## Generate an app with `mk_frame_init`
|
|
38
38
|
|
|
39
|
-
Version 0.2.
|
|
39
|
+
Version 0.2.3 generates full CRUD apps with the `mk_frame_init` executable. Install the gem, then run
|
|
40
40
|
it from the parent directory where you want your new app:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
gem install mk_framework -v 0.2.
|
|
43
|
+
gem install mk_framework -v 0.2.3
|
|
44
44
|
mk_frame_init
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -54,7 +54,7 @@ The interactive CLI asks, in order:
|
|
|
54
54
|
3. Resource/table name, defaulting to `posts`.
|
|
55
55
|
4. Each field name and its type, selected by menu number or type name. Leave the
|
|
56
56
|
next field name blank to finish.
|
|
57
|
-
5. Confirmation of the app, model, fields,
|
|
57
|
+
5. Confirmation of the app, model, fields, resource actions, and destination.
|
|
58
58
|
|
|
59
59
|
Use lowercase names with underscores. The supported types are `string`, `text`,
|
|
60
60
|
`integer`, `float`, `boolean`, `date`, and `datetime`. At least one field is required;
|
|
@@ -70,8 +70,9 @@ argument. This mode never prompts or asks for confirmation:
|
|
|
70
70
|
mk_frame_init --cli 'app_name:blog, model_name:posts, fields:[title:string, contents:text, published:boolean]'
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
This creates `./blog`, a `Blog::Post` model backed by `posts`, and
|
|
74
|
-
`
|
|
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
|
|
75
76
|
`model_name` accepts a singular or conventional plural name (`post` or `posts`).
|
|
76
77
|
For a custom table/URL name, add `resource_name:articles`. Names are simple Ruby
|
|
77
78
|
identifiers; the inline format is parsed as data, never evaluated as Ruby.
|
|
@@ -94,8 +95,11 @@ bundle install
|
|
|
94
95
|
bundle exec rake db:migrate
|
|
95
96
|
bundle exec rake routes
|
|
96
97
|
bundle exec rspec
|
|
98
|
+
bundle exec rake # same as rake dev; starts Puma on port 3000
|
|
97
99
|
```
|
|
98
100
|
|
|
101
|
+
Set `HOST` or `PORT` to override the default `127.0.0.1:3000` bind address.
|
|
102
|
+
|
|
99
103
|
The result is self-contained:
|
|
100
104
|
|
|
101
105
|
```text
|
|
@@ -109,8 +113,8 @@ blog_api/
|
|
|
109
113
|
├── config.ru
|
|
110
114
|
├── db/migrations/001_initial.rb
|
|
111
115
|
├── models/post.rb
|
|
112
|
-
├── routes/posts/controllers/create.rb
|
|
113
|
-
├── routes/posts/handlers/create.rb
|
|
116
|
+
├── routes/posts/controllers/{index,show,create,update,delete}.rb
|
|
117
|
+
├── routes/posts/handlers/{index,show,create,update,delete}.rb
|
|
114
118
|
├── spec/spec_helper.rb
|
|
115
119
|
└── spec/request/posts_spec.rb
|
|
116
120
|
```
|
|
@@ -118,7 +122,9 @@ blog_api/
|
|
|
118
122
|
`database.rb` connects to a local SQLite file, or `DATABASE_URL`. Migrations are
|
|
119
123
|
explicit and run before models load. Tests always migrate a private in-memory
|
|
120
124
|
database. The controller permits the chosen fields and returns `Post.new(...)`;
|
|
121
|
-
MK saves once, then the handler returns `{post: ...}` with status 201.
|
|
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
|
|
122
128
|
README includes a local server command, an example request, and extension guidance.
|
|
123
129
|
|
|
124
130
|
The framework checkout also exposes the same generator as a Rake task:
|
|
@@ -626,7 +632,7 @@ bundle exec rake
|
|
|
626
632
|
bundle exec rake build
|
|
627
633
|
```
|
|
628
634
|
|
|
629
|
-
The gem is written to `pkg/mk_framework-0.2.
|
|
635
|
+
The gem is written to `pkg/mk_framework-0.2.3.gem`. See
|
|
630
636
|
[deployment](docs/deployment.md) for migrations, connections, authentication,
|
|
631
637
|
timeouts, logging, and release verification, and [upgrading](docs/upgrading.md)
|
|
632
638
|
for changes from the prototype. CI runs on Ruby 4.0 on Linux.
|
|
@@ -37,7 +37,7 @@ module MK
|
|
|
37
37
|
return generate(config, arguments.first || config.app_name)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
@output.puts 'MK app generator — one resource
|
|
40
|
+
@output.puts 'MK app generator — one model and a full CRUD resource with controllers and handlers.'
|
|
41
41
|
@output.puts 'Press Ctrl-C to cancel. All fields are required; id and timestamps are automatic.'
|
|
42
42
|
app = validated('1. App name (snake_case)') { |value| Configuration.app_name!(value) }
|
|
43
43
|
model = validated('2. Model name (singular snake_case)') { |value| Configuration.model_name!(value) }
|
|
@@ -47,7 +47,7 @@ module MK
|
|
|
47
47
|
fields = collect_fields
|
|
48
48
|
config = Configuration.new(app_name: app, model_name: model, resource: resource, fields: fields)
|
|
49
49
|
destination = File.expand_path(arguments.first || app)
|
|
50
|
-
@output.puts "\nApp: #{config.namespace}; model: #{config.model_class};
|
|
50
|
+
@output.puts "\nApp: #{config.namespace}; model: #{config.model_class}; resource: /#{resource} (index, show, create, update, delete)"
|
|
51
51
|
@output.puts "Fields: #{fields.map { |field| "#{field[:name]}:#{field[:type]}" }.join(', ')}"
|
|
52
52
|
@output.puts "Directory: #{destination}"
|
|
53
53
|
answer = validated('5. Generate these files? (y/n)', default: 'y') do |value|
|
|
@@ -70,7 +70,7 @@ module MK
|
|
|
70
70
|
destination = Project.new(config).generate(destination)
|
|
71
71
|
@output.puts "\nCreated #{destination}\nNext steps:"
|
|
72
72
|
@output.puts " cd #{Shellwords.escape(destination)}"
|
|
73
|
-
@output.puts " bundle install\n bundle exec rake db:migrate\n bundle exec rake routes\n bundle exec rspec"
|
|
73
|
+
@output.puts " bundle install\n bundle exec rake db:migrate\n bundle exec rake routes\n bundle exec rspec\n bundle exec rake"
|
|
74
74
|
0
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -5,6 +5,7 @@ module MK
|
|
|
5
5
|
class InvalidInput < ArgumentError; end
|
|
6
6
|
|
|
7
7
|
class Configuration
|
|
8
|
+
ACTIONS = %w[index show create update delete].freeze
|
|
8
9
|
TYPES = {
|
|
9
10
|
'string' => ['String', 'String', 'Example'],
|
|
10
11
|
'text' => ['String', 'String', 'Example text'],
|
|
@@ -67,8 +68,7 @@ module MK
|
|
|
67
68
|
@app_name = self.class.app_name!(app_name).dup.freeze
|
|
68
69
|
@model_name = self.class.model_name!(model_name).dup.freeze
|
|
69
70
|
@resource = self.class.identifier!(resource, label: 'Resource name').dup.freeze
|
|
70
|
-
if
|
|
71
|
-
self.class.camelize(resource) + 'CreateHandler' == model_class
|
|
71
|
+
if ACTIONS.product(%w[Controller Handler]).any? { |action, kind| action_prefix + action.capitalize + kind == model_class }
|
|
72
72
|
raise InvalidInput, 'Model name conflicts with a generated action class.'
|
|
73
73
|
end
|
|
74
74
|
raise InvalidInput, 'Add at least one field.' if fields.empty?
|
|
@@ -13,8 +13,6 @@ module MK
|
|
|
13
13
|
'README.md' => 'README.md', 'app.rb' => 'app.rb', 'config.ru' => 'config.ru',
|
|
14
14
|
'database.rb' => 'database.rb', 'migration.rb' => 'db/migrations/001_initial.rb',
|
|
15
15
|
'model.rb' => 'models/%{model}.rb',
|
|
16
|
-
'controller.rb' => 'routes/%{resource}/controllers/create.rb',
|
|
17
|
-
'handler.rb' => 'routes/%{resource}/handlers/create.rb',
|
|
18
16
|
'spec_helper.rb' => 'spec/spec_helper.rb', 'request_spec.rb' => 'spec/request/%{resource}_spec.rb'
|
|
19
17
|
}.freeze
|
|
20
18
|
|
|
@@ -43,10 +41,17 @@ module MK
|
|
|
43
41
|
|
|
44
42
|
def render
|
|
45
43
|
config = @configuration
|
|
46
|
-
TEMPLATES.to_h do |template, path|
|
|
44
|
+
files = TEMPLATES.to_h do |template, path|
|
|
47
45
|
source = File.read(File.join(__dir__, 'templates', "#{template}.erb"))
|
|
48
46
|
[format(path, model: config.model_name, resource: config.resource), ERB.new(source, trim_mode: '-').result(binding)]
|
|
49
47
|
end
|
|
48
|
+
Configuration::ACTIONS.each do |action|
|
|
49
|
+
%w[controller handler].each do |kind|
|
|
50
|
+
source = File.read(File.join(__dir__, 'templates', "#{kind}.rb.erb"))
|
|
51
|
+
files["routes/#{config.resource}/#{kind}s/#{action}.rb"] = ERB.new(source, trim_mode: '-').result(binding)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
files
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# <%= config.namespace %>
|
|
2
2
|
|
|
3
|
-
A self-contained MK app with one `<%= config.model_class %>` model and
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A self-contained MK app with one `<%= config.model_class %>` model and a full CRUD resource.
|
|
4
|
+
Each action has its own controller and handler.
|
|
5
|
+
|
|
6
|
+
| Method | Path | Action |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| GET | `/<%= config.resource %>` | index |
|
|
9
|
+
| GET | `/<%= config.resource %>/:id` | show |
|
|
10
|
+
| POST | `/<%= config.resource %>` | create |
|
|
11
|
+
| PATCH / PUT | `/<%= config.resource %>/:id` | update |
|
|
12
|
+
| DELETE | `/<%= config.resource %>/:id` | delete |
|
|
13
|
+
|
|
14
|
+
Create returns 201; the other actions return 200. Index returns
|
|
15
|
+
`{ <%= config.resource %>: [...] }`; member actions return `{ <%= config.model_name %>: { ... } }`,
|
|
16
|
+
including the deleted record for DELETE. Missing records return 404.
|
|
6
17
|
|
|
7
18
|
## Setup
|
|
8
19
|
|
|
@@ -16,23 +27,27 @@ bundle exec rspec
|
|
|
16
27
|
Migrations run explicitly, before boot. Development data defaults to
|
|
17
28
|
`<%= config.app_name %>.db` beside `database.rb`. `DATABASE_URL`, `DB_POOL_SIZE`, and
|
|
18
29
|
`DB_POOL_TIMEOUT` configure other connections. Tests always use private in-memory
|
|
19
|
-
SQLite databases and ignore `DATABASE_URL`.
|
|
30
|
+
SQLite databases and ignore `DATABASE_URL`. Datetimes are stored and read in UTC.
|
|
20
31
|
|
|
21
32
|
To serve the app locally:
|
|
22
33
|
|
|
23
34
|
```sh
|
|
24
|
-
bundle exec
|
|
35
|
+
bundle exec rake # defaults to rake dev; Puma on http://127.0.0.1:3000
|
|
36
|
+
# or: bundle exec rake dev
|
|
25
37
|
```
|
|
26
38
|
|
|
39
|
+
Set `HOST` or `PORT` to override the bind address or port.
|
|
40
|
+
|
|
27
41
|
Then create a record:
|
|
28
42
|
|
|
29
43
|
```sh
|
|
30
|
-
curl -i http://127.0.0.1:
|
|
44
|
+
curl -i http://127.0.0.1:3000/<%= config.resource %> \
|
|
31
45
|
-H 'Content-Type: application/json' \
|
|
32
46
|
-d '<%= JSON.generate(config.example) %>'
|
|
33
47
|
```
|
|
34
48
|
|
|
35
|
-
All chosen fields are required.
|
|
49
|
+
All chosen fields are required on create. PATCH and PUT update only supplied fields
|
|
50
|
+
and preserve omitted fields. Invalid input types return 400; missing fields or
|
|
36
51
|
blank text return 422. Date and datetime inputs use ISO 8601 strings. Booleans
|
|
37
52
|
accept JSON `true` and `false`; numeric fields accept JSON numbers. Unknown fields
|
|
38
53
|
are ignored. IDs and timestamps are assigned by the app.
|
|
@@ -43,12 +58,13 @@ are ignored. IDs and timestamps are assigned by the app.
|
|
|
43
58
|
- `models/<%= config.model_name %>.rb` owns validations and the public field list.
|
|
44
59
|
- `app.rb` loads dependencies and models, configures routes, and calls `boot!`.
|
|
45
60
|
- `config.ru` exposes `<%= config.namespace %>::App.app` to Rack.
|
|
46
|
-
- `routes/<%= config.resource %>/controllers
|
|
47
|
-
- `routes/<%= config.resource %>/handlers
|
|
61
|
+
- `routes/<%= config.resource %>/controllers/` contains index, show, create, update, and delete actions.
|
|
62
|
+
- `routes/<%= config.resource %>/handlers/` filters raw data and formats each response.
|
|
63
|
+
- MK saves create/update models once and destroys delete models once.
|
|
48
64
|
- `spec/request/<%= config.resource %>_spec.rb` checks persistence and error responses.
|
|
49
65
|
|
|
50
66
|
Add a field through a new migration, update permitted input and model validation,
|
|
51
67
|
then decide whether to expose it in `public_attributes_list`. Add another action by
|
|
52
|
-
creating its controller/handler pair and
|
|
68
|
+
creating its controller/handler pair and declaring the route in `app.rb`.
|
|
53
69
|
The starter has no authentication: implement access rules in controllers before
|
|
54
70
|
using it for private data. Handlers must not query or persist records.
|
|
@@ -22,4 +22,11 @@ task :spec do
|
|
|
22
22
|
sh RbConfig.ruby, '-S', 'rspec', File.join(__dir__, 'spec')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
desc 'Start Puma on port 3000 (HOST and PORT override the defaults)'
|
|
26
|
+
task :dev do
|
|
27
|
+
exec RbConfig.ruby, '-S', 'puma', '--bind',
|
|
28
|
+
"tcp://#{ENV.fetch('HOST', '127.0.0.1')}:#{ENV.fetch('PORT', '3000')}",
|
|
29
|
+
File.join(__dir__, 'config.ru'), chdir: __dir__
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: :dev
|
|
@@ -9,7 +9,7 @@ module <%= config.namespace %>
|
|
|
9
9
|
configure root: ROOT, namespace: ::<%= config.namespace %>, legacy_post_routes: false
|
|
10
10
|
|
|
11
11
|
resource_routes do
|
|
12
|
-
resources :<%= config.resource %>,
|
|
12
|
+
resources :<%= config.resource %>, singular: '<%= config.model_name %>'
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
<% if config.fields.any? { |field| %w[date datetime].include?(field[:type]) } -%>
|
|
2
|
+
<% if %w[create update].include?(action) && config.fields.any? { |field| %w[date datetime].include?(field[:type]) } -%>
|
|
3
3
|
|
|
4
4
|
require 'date'
|
|
5
5
|
<% end -%>
|
|
6
6
|
|
|
7
7
|
module <%= config.namespace %>
|
|
8
|
-
class <%= config.action_prefix %>
|
|
8
|
+
class <%= config.action_prefix %><%= action.capitalize %>Controller < MK::Controller
|
|
9
9
|
route do |r|
|
|
10
|
+
<% if action == 'index' -%>
|
|
11
|
+
<%= config.model_class %>.order(:id)
|
|
12
|
+
<% elsif %w[show delete].include?(action) -%>
|
|
13
|
+
<%= config.model_class %>[r.path_params.fetch(:id)] or raise MK::NotFound
|
|
14
|
+
<% else -%>
|
|
15
|
+
<% if action == 'update' -%>
|
|
16
|
+
record = <%= config.model_class %>[r.path_params.fetch(:id)] or raise MK::NotFound
|
|
17
|
+
<% end -%>
|
|
10
18
|
attributes = r.input.permit(
|
|
11
19
|
<% config.fields.each_with_index do |field, index| -%>
|
|
12
20
|
<%= field[:name] %>: <%= MK::Generator::Configuration::TYPES.fetch(field[:type])[1] %><%= index < config.fields.length - 1 ? ',' : '' %>
|
|
@@ -21,7 +29,12 @@ module <%= config.namespace %>
|
|
|
21
29
|
end
|
|
22
30
|
end
|
|
23
31
|
<% end -%>
|
|
32
|
+
<% if action == 'create' -%>
|
|
24
33
|
<%= config.model_class %>.new(attributes)
|
|
34
|
+
<% else -%>
|
|
35
|
+
record.set(attributes)
|
|
36
|
+
<% end -%>
|
|
37
|
+
<% end -%>
|
|
25
38
|
end
|
|
26
39
|
end
|
|
27
40
|
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module <%= config.namespace %>
|
|
4
|
-
class <%= config.action_prefix %>
|
|
4
|
+
class <%= config.action_prefix %><%= action.capitalize %>Handler < MK::Handler
|
|
5
5
|
handler do |r|
|
|
6
|
+
<% if action == 'create' -%>
|
|
6
7
|
r.response.status = 201
|
|
7
|
-
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% if action == 'index' -%>
|
|
10
|
+
{ <%= config.resource %>: model.map { |record| record.slice(*<%= config.model_class %>.public_attributes_list) } }
|
|
11
|
+
<% else -%>
|
|
12
|
+
{ <%= config.model_name %>: model.slice(*<%= config.model_class %>.public_attributes_list) }
|
|
13
|
+
<% end -%>
|
|
8
14
|
end
|
|
9
15
|
end
|
|
10
16
|
end
|
|
@@ -2,37 +2,113 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../spec_helper'
|
|
4
4
|
|
|
5
|
-
RSpec.describe '<%= config.model_class %>
|
|
6
|
-
|
|
7
|
-
let(:attributes) { JSON.parse(<%= JSON.generate(config.example).inspect %>) }
|
|
5
|
+
RSpec.describe '<%= config.model_class %> CRUD' do
|
|
6
|
+
include Rack::Test::Methods
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
let(:app) { Rack::Builder.parse_file(File.join(<%= config.namespace %>::ROOT, 'config.ru')) }
|
|
9
|
+
let(:attributes) { JSON.parse(<%= config.example.to_json.inspect %>) }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
<%= config.namespace %>::<%= config.model_class %>.dataset.delete
|
|
13
|
+
header 'CONTENT_TYPE', 'application/json'
|
|
14
|
+
end
|
|
10
15
|
|
|
11
16
|
it 'persists permitted fields and returns a 201 response through the Rack entrypoint' do
|
|
12
|
-
|
|
13
|
-
'CONTENT_TYPE' => 'application/json')
|
|
17
|
+
post('/<%= config.resource %>', attributes.merge('id' => 999).to_json)
|
|
14
18
|
|
|
15
|
-
expect(
|
|
19
|
+
expect(last_response.status).to eq(201)
|
|
16
20
|
record = <%= config.namespace %>::<%= config.model_class %>.first
|
|
17
21
|
expect(record).not_to be_nil
|
|
18
22
|
expect(record.id).not_to eq(999)
|
|
19
|
-
payload = JSON.parse(
|
|
23
|
+
payload = JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
20
24
|
expect(payload.fetch('id')).to eq(record.id)
|
|
21
25
|
expect(payload.keys).to match_array(%w[id <%= config.field_names.join(' ') %> created_at updated_at])
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
it 'rejects missing required fields without inserting a record' do
|
|
25
|
-
|
|
29
|
+
post('/<%= config.resource %>', {}.to_json)
|
|
26
30
|
|
|
27
|
-
expect(
|
|
31
|
+
expect(last_response.status).to eq(422)
|
|
28
32
|
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
it 'rejects an invalid field type without inserting a record' do
|
|
32
36
|
attributes['<%= config.fields.first[:name] %>'] = []
|
|
33
|
-
|
|
37
|
+
post('/<%= config.resource %>', attributes.to_json)
|
|
34
38
|
|
|
35
|
-
expect(
|
|
39
|
+
expect(last_response.status).to eq(400)
|
|
36
40
|
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
37
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
|
|
50
|
+
get('/<%= config.resource %>')
|
|
51
|
+
expect(JSON.parse(last_response.body)).to eq('<%= config.resource %>' => [])
|
|
52
|
+
records = [create_record, create_record]
|
|
53
|
+
get('/<%= config.resource %>')
|
|
54
|
+
expect(last_response.status).to eq(200)
|
|
55
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.resource %>')).to eq(records)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'shows a single record' do
|
|
59
|
+
record = create_record
|
|
60
|
+
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
61
|
+
expect(last_response.status).to eq(200)
|
|
62
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
<% 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 permitted fields with PUT, preserving omitted fields and the ID' do
|
|
67
|
+
record = create_record
|
|
68
|
+
changes = { '<%= config.fields.first[:name] %>' => <%= updated_value.inspect %>, 'id' => 999 }
|
|
69
|
+
put("/<%= config.resource %>/#{record.fetch('id')}",
|
|
70
|
+
changes.to_json)
|
|
71
|
+
expect(last_response.status).to eq(200)
|
|
72
|
+
payload = JSON.parse(last_response.body).fetch('<%= config.model_name %>')
|
|
73
|
+
expect(payload.fetch('id')).to eq(record.fetch('id'))
|
|
74
|
+
<% if config.fields.first[:type] == 'datetime' -%>
|
|
75
|
+
expect(DateTime.parse(payload.fetch('<%= config.fields.first[:name] %>'))).to eq(DateTime.iso8601(<%= updated_value.inspect %>))
|
|
76
|
+
<% else -%>
|
|
77
|
+
expect(payload.fetch('<%= config.fields.first[:name] %>')).to eq(<%= updated_value.inspect %>)
|
|
78
|
+
<% end -%>
|
|
79
|
+
expect(payload.except('<%= config.fields.first[:name] %>', 'updated_at')).to eq(record.except('<%= config.fields.first[:name] %>', 'updated_at'))
|
|
80
|
+
get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
81
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(payload)
|
|
82
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'rejects invalid updates without changing stored data' do
|
|
86
|
+
record = create_record
|
|
87
|
+
<%= %w[string text].include?(config.fields.first[:type]) ? "[[[], 400], [nil, 400], ['', 422]]" : '[[[], 400], [nil, 400]]' %>.each do |value, status|
|
|
88
|
+
put("/<%= config.resource %>/#{record.fetch('id')}",
|
|
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
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'deletes a record and returns its public attributes' do
|
|
97
|
+
record = create_record
|
|
98
|
+
delete("/<%= config.resource %>/#{record.fetch('id')}")
|
|
99
|
+
expect(last_response.status).to eq(200)
|
|
100
|
+
expect(JSON.parse(last_response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
101
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
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
|
|
113
|
+
end
|
|
38
114
|
end
|
data/lib/mk_framework/version.rb
CHANGED