mk_framework 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +442 -25
- data/bin/mk_frame_init +5 -0
- data/docs/deployment.md +3 -3
- data/lib/mk_framework/generator/cli.rb +124 -0
- data/lib/mk_framework/generator/configuration.rb +87 -0
- data/lib/mk_framework/generator/options.rb +91 -0
- data/lib/mk_framework/generator/project.rb +58 -0
- data/lib/mk_framework/generator/tasks.rb +13 -0
- data/lib/mk_framework/generator/templates/Gemfile.erb +13 -0
- data/lib/mk_framework/generator/templates/README.md.erb +70 -0
- data/lib/mk_framework/generator/templates/Rakefile.erb +32 -0
- data/lib/mk_framework/generator/templates/app.rb.erb +17 -0
- data/lib/mk_framework/generator/templates/config.ru.erb +4 -0
- data/lib/mk_framework/generator/templates/controller.rb.erb +40 -0
- data/lib/mk_framework/generator/templates/database.rb.erb +17 -0
- data/lib/mk_framework/generator/templates/gitignore.erb +10 -0
- data/lib/mk_framework/generator/templates/handler.rb.erb +16 -0
- data/lib/mk_framework/generator/templates/migration.rb.erb +14 -0
- data/lib/mk_framework/generator/templates/model.rb.erb +21 -0
- data/lib/mk_framework/generator/templates/request_spec.rb.erb +108 -0
- data/lib/mk_framework/generator/templates/spec_helper.rb.erb +14 -0
- data/lib/mk_framework/generator.rb +6 -0
- data/lib/mk_framework/version.rb +1 -1
- metadata +26 -6
- data/docs/upgrading.md +0 -124
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe '<%= config.model_class %> CRUD' do
|
|
6
|
+
let(:client) { Rack::MockRequest.new(Rack::Builder.parse_file(File.join(<%= config.namespace %>::ROOT, 'config.ru'))) }
|
|
7
|
+
let(:attributes) { JSON.parse(<%= JSON.generate(config.example).inspect %>) }
|
|
8
|
+
|
|
9
|
+
before { <%= config.namespace %>::<%= config.model_class %>.dataset.delete }
|
|
10
|
+
|
|
11
|
+
it 'persists permitted fields and returns a 201 response through the Rack entrypoint' do
|
|
12
|
+
response = client.post('/<%= config.resource %>', input: JSON.generate(attributes.merge('id' => 999)),
|
|
13
|
+
'CONTENT_TYPE' => 'application/json')
|
|
14
|
+
|
|
15
|
+
expect(response.status).to eq(201)
|
|
16
|
+
record = <%= config.namespace %>::<%= config.model_class %>.first
|
|
17
|
+
expect(record).not_to be_nil
|
|
18
|
+
expect(record.id).not_to eq(999)
|
|
19
|
+
payload = JSON.parse(response.body).fetch('<%= config.model_name %>')
|
|
20
|
+
expect(payload.fetch('id')).to eq(record.id)
|
|
21
|
+
expect(payload.keys).to match_array(%w[id <%= config.field_names.join(' ') %> created_at updated_at])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'rejects missing required fields without inserting a record' do
|
|
25
|
+
response = client.post('/<%= config.resource %>', input: '{}', 'CONTENT_TYPE' => 'application/json')
|
|
26
|
+
|
|
27
|
+
expect(response.status).to eq(422)
|
|
28
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'rejects an invalid field type without inserting a record' do
|
|
32
|
+
attributes['<%= config.fields.first[:name] %>'] = []
|
|
33
|
+
response = client.post('/<%= config.resource %>', input: JSON.generate(attributes), 'CONTENT_TYPE' => 'application/json')
|
|
34
|
+
|
|
35
|
+
expect(response.status).to eq(400)
|
|
36
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_record
|
|
40
|
+
response = client.post('/<%= config.resource %>', input: JSON.generate(attributes), 'CONTENT_TYPE' => 'application/json')
|
|
41
|
+
expect(response.status).to eq(201)
|
|
42
|
+
JSON.parse(response.body).fetch('<%= config.model_name %>')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'lists records in ID order, including an empty collection' do
|
|
46
|
+
expect(JSON.parse(client.get('/<%= config.resource %>').body)).to eq('<%= config.resource %>' => [])
|
|
47
|
+
records = [create_record, create_record]
|
|
48
|
+
response = client.get('/<%= config.resource %>')
|
|
49
|
+
expect(response.status).to eq(200)
|
|
50
|
+
expect(JSON.parse(response.body).fetch('<%= config.resource %>')).to eq(records)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'shows a single record' do
|
|
54
|
+
record = create_record
|
|
55
|
+
response = client.get("/<%= config.resource %>/#{record.fetch('id')}")
|
|
56
|
+
expect(response.status).to eq(200)
|
|
57
|
+
expect(JSON.parse(response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
<% 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]) -%>
|
|
61
|
+
%w[patch put].each do |verb|
|
|
62
|
+
it "updates permitted fields with #{verb.upcase}, preserving omitted fields and the ID" do
|
|
63
|
+
record = create_record
|
|
64
|
+
changes = {'<%= config.fields.first[:name] %>' => <%= updated_value.inspect %>, 'id' => 999}
|
|
65
|
+
response = client.public_send(verb, "/<%= config.resource %>/#{record.fetch('id')}",
|
|
66
|
+
input: JSON.generate(changes), 'CONTENT_TYPE' => 'application/json')
|
|
67
|
+
expect(response.status).to eq(200)
|
|
68
|
+
payload = JSON.parse(response.body).fetch('<%= config.model_name %>')
|
|
69
|
+
expect(payload.fetch('id')).to eq(record.fetch('id'))
|
|
70
|
+
<% if config.fields.first[:type] == 'datetime' -%>
|
|
71
|
+
expect(DateTime.parse(payload.fetch('<%= config.fields.first[:name] %>'))).to eq(DateTime.iso8601(<%= updated_value.inspect %>))
|
|
72
|
+
<% else -%>
|
|
73
|
+
expect(payload.fetch('<%= config.fields.first[:name] %>')).to eq(<%= updated_value.inspect %>)
|
|
74
|
+
<% end -%>
|
|
75
|
+
expect(payload.except('<%= config.fields.first[:name] %>', 'updated_at')).to eq(record.except('<%= config.fields.first[:name] %>', 'updated_at'))
|
|
76
|
+
expect(JSON.parse(client.get("/<%= config.resource %>/#{record.fetch('id')}").body).fetch('<%= config.model_name %>')).to eq(payload)
|
|
77
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'rejects invalid updates without changing stored data' do
|
|
82
|
+
record = create_record
|
|
83
|
+
<%= %w[string text].include?(config.fields.first[:type]) ? "[[[], 400], [nil, 400], ['', 422]]" : '[[[], 400], [nil, 400]]' %>.each do |value, status|
|
|
84
|
+
response = client.patch("/<%= config.resource %>/#{record.fetch('id')}",
|
|
85
|
+
input: JSON.generate('<%= config.fields.first[:name] %>' => value), 'CONTENT_TYPE' => 'application/json')
|
|
86
|
+
expect(response.status).to eq(status)
|
|
87
|
+
expect(JSON.parse(client.get("/<%= config.resource %>/#{record.fetch('id')}").body).fetch('<%= config.model_name %>')).to eq(record)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'deletes a record and returns its public attributes' do
|
|
92
|
+
record = create_record
|
|
93
|
+
response = client.delete("/<%= config.resource %>/#{record.fetch('id')}")
|
|
94
|
+
expect(response.status).to eq(200)
|
|
95
|
+
expect(JSON.parse(response.body).fetch('<%= config.model_name %>')).to eq(record)
|
|
96
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
97
|
+
expect(client.get("/<%= config.resource %>/#{record.fetch('id')}").status).to eq(404)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
%w[get patch put delete].each do |verb|
|
|
101
|
+
it "returns 404 for #{verb.upcase} of a missing record" do
|
|
102
|
+
response = client.public_send(verb, '/<%= config.resource %>/999999',
|
|
103
|
+
input: JSON.generate(attributes), 'CONTENT_TYPE' => 'application/json')
|
|
104
|
+
expect(response.status).to eq(404)
|
|
105
|
+
expect(<%= config.namespace %>::<%= config.model_class %>.count).to eq(0)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
|
4
|
+
require 'rspec'
|
|
5
|
+
require 'rack/mock'
|
|
6
|
+
require 'rack/builder'
|
|
7
|
+
require 'json'
|
|
8
|
+
require_relative '../database'
|
|
9
|
+
Sequel::Migrator.run(<%= config.namespace %>::DB, File.join(<%= config.namespace %>::ROOT, 'db/migrations'))
|
|
10
|
+
require_relative '../app'
|
|
11
|
+
|
|
12
|
+
RSpec.configure do |config|
|
|
13
|
+
config.after(:suite) { <%= config.namespace %>::DB.disconnect }
|
|
14
|
+
end
|
data/lib/mk_framework/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mk_framework
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Francesco Canessa
|
|
@@ -91,32 +91,52 @@ dependencies:
|
|
|
91
91
|
version: '2'
|
|
92
92
|
description: Resource routing, controllers, and response handlers with optional Sequel
|
|
93
93
|
persistence.
|
|
94
|
-
executables:
|
|
94
|
+
executables:
|
|
95
|
+
- mk_frame_init
|
|
95
96
|
extensions: []
|
|
96
97
|
extra_rdoc_files: []
|
|
97
98
|
files:
|
|
98
99
|
- CHANGELOG.md
|
|
99
100
|
- LICENSE
|
|
100
101
|
- README.md
|
|
102
|
+
- bin/mk_frame_init
|
|
101
103
|
- docs/deployment.md
|
|
102
104
|
- docs/routing.md
|
|
103
|
-
- docs/upgrading.md
|
|
104
105
|
- lib/mk_framework.rb
|
|
105
106
|
- lib/mk_framework/application.rb
|
|
106
107
|
- lib/mk_framework/controller.rb
|
|
107
108
|
- lib/mk_framework/errors.rb
|
|
109
|
+
- lib/mk_framework/generator.rb
|
|
110
|
+
- lib/mk_framework/generator/cli.rb
|
|
111
|
+
- lib/mk_framework/generator/configuration.rb
|
|
112
|
+
- lib/mk_framework/generator/options.rb
|
|
113
|
+
- lib/mk_framework/generator/project.rb
|
|
114
|
+
- lib/mk_framework/generator/tasks.rb
|
|
115
|
+
- lib/mk_framework/generator/templates/Gemfile.erb
|
|
116
|
+
- lib/mk_framework/generator/templates/README.md.erb
|
|
117
|
+
- lib/mk_framework/generator/templates/Rakefile.erb
|
|
118
|
+
- lib/mk_framework/generator/templates/app.rb.erb
|
|
119
|
+
- lib/mk_framework/generator/templates/config.ru.erb
|
|
120
|
+
- lib/mk_framework/generator/templates/controller.rb.erb
|
|
121
|
+
- lib/mk_framework/generator/templates/database.rb.erb
|
|
122
|
+
- lib/mk_framework/generator/templates/gitignore.erb
|
|
123
|
+
- lib/mk_framework/generator/templates/handler.rb.erb
|
|
124
|
+
- lib/mk_framework/generator/templates/migration.rb.erb
|
|
125
|
+
- lib/mk_framework/generator/templates/model.rb.erb
|
|
126
|
+
- lib/mk_framework/generator/templates/request_spec.rb.erb
|
|
127
|
+
- lib/mk_framework/generator/templates/spec_helper.rb.erb
|
|
108
128
|
- lib/mk_framework/request.rb
|
|
109
129
|
- lib/mk_framework/router.rb
|
|
110
130
|
- lib/mk_framework/sequel.rb
|
|
111
131
|
- lib/mk_framework/testing.rb
|
|
112
132
|
- lib/mk_framework/version.rb
|
|
113
|
-
homepage: https://github.com/makevoid/
|
|
133
|
+
homepage: https://github.com/makevoid/mk_framework
|
|
114
134
|
licenses:
|
|
115
135
|
- MIT
|
|
116
136
|
metadata:
|
|
117
137
|
rubygems_mfa_required: 'true'
|
|
118
|
-
source_code_uri: https://github.com/makevoid/
|
|
119
|
-
changelog_uri: https://github.com/makevoid/
|
|
138
|
+
source_code_uri: https://github.com/makevoid/mk_framework
|
|
139
|
+
changelog_uri: https://github.com/makevoid/mk_framework/blob/main/CHANGELOG.md
|
|
120
140
|
rdoc_options: []
|
|
121
141
|
require_paths:
|
|
122
142
|
- lib
|
data/docs/upgrading.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# Upgrading from the prototype
|
|
2
|
-
|
|
3
|
-
0.2.0 changes the Ruby API while retaining the original POST mutation URLs by
|
|
4
|
-
default. Upgrade application code before using the new gem. The six samples have
|
|
5
|
-
already been migrated.
|
|
6
|
-
|
|
7
|
-
## Application boot and class names
|
|
8
|
-
|
|
9
|
-
Place models, controllers, handlers, and the app in an application module. In each
|
|
10
|
-
file, declare that module explicitly; `require` does not inherit its caller's
|
|
11
|
-
lexical namespace. Configure `root: __dir__` and `namespace: YourApp` in the app,
|
|
12
|
-
and call `YourApp::App.boot!` after its class definition.
|
|
13
|
-
|
|
14
|
-
Models must be loaded before boot. MK loads action files relative to the configured
|
|
15
|
-
root and resolves action classes there. `boot!` validates and freezes the app;
|
|
16
|
-
calling `app` before boot is an error. Configure middleware and plugins beforehand.
|
|
17
|
-
Restart the process to pick up source changes.
|
|
18
|
-
|
|
19
|
-
The samples now expose `SampleApp1::App` through `SampleApp6::App` instead of global
|
|
20
|
-
`TodoApp`, `BlogApp`, `KanbanApp`, and `WeatherApp` classes. Their model datasets are
|
|
21
|
-
explicit, so multiple sample apps can coexist without sharing constants or data.
|
|
22
|
-
|
|
23
|
-
## Automatic action persistence and raw handler data
|
|
24
|
-
|
|
25
|
-
Previously, handlers interpreted class-name suffixes and saved/deleted the object
|
|
26
|
-
returned by a controller. Remove handler `success`/`error` registration blocks.
|
|
27
|
-
Controllers return the prepared record; framework dispatch persists it according
|
|
28
|
-
to the registered action and converts it to raw attributes before the handler:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
# Controller
|
|
32
|
-
route do |r|
|
|
33
|
-
Post.new(r.input.permit(title: String))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Handler
|
|
37
|
-
handler do |r|
|
|
38
|
-
r.response.status = 201
|
|
39
|
-
{post: fields(model, :id, :title)}
|
|
40
|
-
end
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Require `mk_framework/sequel` for this lifecycle. Sequel is optional and must be
|
|
44
|
-
listed in your application's Gemfile. Create/update results receive `save` then
|
|
45
|
-
`values`; delete results receive `destroy` then `values`; show/index results are
|
|
46
|
-
converted without writes. Remove explicit `persist`, `save`, and `destroy` calls
|
|
47
|
-
from standard controllers to avoid duplicate writes. For explicit multi-record
|
|
48
|
-
transactions, return raw data after completing the writes. Custom actions do not
|
|
49
|
-
automatically persist records.
|
|
50
|
-
|
|
51
|
-
Handlers receive raw hashes/arrays, including materialized nested results. Replace
|
|
52
|
-
model attribute/association methods with hash access and allowlist filtering.
|
|
53
|
-
Select associations in controllers. A handler does not query or write under any
|
|
54
|
-
action name. Validation uses 422 for
|
|
55
|
-
both create and update; expected constraint conflicts use 409. Unexpected failures
|
|
56
|
-
are sanitized 500s. Deliberate `MK::Error` messages are public.
|
|
57
|
-
|
|
58
|
-
The old `route` declaration in a handler remains an alias for `handler`, but it
|
|
59
|
-
does not move persistence into handlers. Handlers return Hash/Array responses rather
|
|
60
|
-
than calling `to_json`. For an empty success, use `r.halt(204)` in the handler.
|
|
61
|
-
|
|
62
|
-
## Resource declarations
|
|
63
|
-
|
|
64
|
-
Replace `register_nested_resource` with a resource tree:
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
resource_routes do
|
|
68
|
-
resources :posts do
|
|
69
|
-
resources :comments
|
|
70
|
-
end
|
|
71
|
-
# Optional compatibility URLs for the old shallow member endpoints:
|
|
72
|
-
resources :comments, only: %i[show update delete]
|
|
73
|
-
end
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
This exposes fully nested CRUD plus the explicitly requested shallow members.
|
|
77
|
-
For exclusively shallow members, instead use `resources :comments, shallow: true`
|
|
78
|
-
inside the parent. There are no implicit parentless comment collections.
|
|
79
|
-
|
|
80
|
-
Use `r.path_params` for ancestor and member IDs. Existing `r.params['id']` remains
|
|
81
|
-
supported, but `r.input` deliberately contains only query/body input. `r.params`
|
|
82
|
-
gives URL IDs precedence. Scope all nested member lookups through their parent.
|
|
83
|
-
|
|
84
|
-
PATCH, PUT, and DELETE now work. POST update/delete aliases remain on by default;
|
|
85
|
-
turn them off using `configure legacy_post_routes: false` when clients migrate.
|
|
86
|
-
|
|
87
|
-
## Database migration
|
|
88
|
-
|
|
89
|
-
Server boot no longer creates tables. On a new database, run the sample's
|
|
90
|
-
`bundle exec rake db:migrate` before loading its app. Tests use their own in-memory
|
|
91
|
-
databases and run migrations there.
|
|
92
|
-
|
|
93
|
-
Do not run the initial migration blindly against a populated prototype database:
|
|
94
|
-
the existing tables will cause it to fail rather than be silently adopted or
|
|
95
|
-
replaced. Back up that database, compare its schema with `db/migrations/001_initial.rb`,
|
|
96
|
-
and write an application-specific upgrade migration or import into a freshly
|
|
97
|
-
migrated database. Backfill null timestamps and missing defaults before adding
|
|
98
|
-
the new constraints. Mark the initial migration applied only after confirming
|
|
99
|
-
schema equivalence. No existing database is automatically altered by this upgrade.
|
|
100
|
-
|
|
101
|
-
## Responses and clients
|
|
102
|
-
|
|
103
|
-
Lists are bounded to 25 records by default; use `limit` and `offset`. Maximum limit
|
|
104
|
-
is 100 and maximum offset is 10,000. Nested comments included in parent show
|
|
105
|
-
responses use the same bounds. Adapt clients that previously expected every row.
|
|
106
|
-
|
|
107
|
-
The weather API uses `OPENWEATHERMAP_API_KEY`, not a file in the user's home. Its
|
|
108
|
-
response field is now `forecast`, containing eight three-hour periods, replacing
|
|
109
|
-
the inaccurate `hourly_forecast` field. Times include a timezone. Upstream failures
|
|
110
|
-
are sanitized 502 responses; an unknown location is 404; a missing key is 503.
|
|
111
|
-
|
|
112
|
-
Missing-resource responses consistently use an `error` field. Unexpected error
|
|
113
|
-
responses also include a request ID. Do not depend on internal exception messages.
|
|
114
|
-
|
|
115
|
-
## Tests and dependencies
|
|
116
|
-
|
|
117
|
-
Use `bundle exec rake` at the repository root to test the framework and every
|
|
118
|
-
sample in isolated processes. Child failures fail the aggregate task. Sample tests
|
|
119
|
-
force `RACK_ENV=test`; they do not open development databases or `DATABASE_URL`.
|
|
120
|
-
|
|
121
|
-
Install the updated bundles with Ruby 3.2 or later and the locked Bundler version.
|
|
122
|
-
The lockfiles include updates to Roda, Rack, Sequel, SQLite3, Puma, and test tools.
|
|
123
|
-
Puma moved from 6.x to 8.x; review your own server configuration when upgrading.
|
|
124
|
-
See `docs/deployment.md` for deployment and release checks.
|