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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18971a8a81867b55b44a74d192a3ffd4ff0ed8bd965ec0297b76bc7fd49717cc
4
- data.tar.gz: 603c5122e2544e1838ab57b9157c84ec19b7d9788e391e8d0ccfce727b4aa69c
3
+ metadata.gz: c68f913eebaf1aa8a9b4d423b528e4720a6508b5f0f130c03b15d039458ec6d1
4
+ data.tar.gz: be858c42ce6d158eb5e658bb9a7e20ec9a775795f0f5956b03100ee733dd4c2f
5
5
  SHA512:
6
- metadata.gz: 49c33c23de2b934469e30869b54e3a6a2762c4043a32dbf785d0d21696653d2635f2d82a59b0b80c1f18aeb87edcd135030020c301ab19d24627ca24c52bd067
7
- data.tar.gz: 2dc8032558bb7a1f81dcd1cad21f8f579fcffdf7b7ccc3cbb518a3c554bc07bcf77ca690a27915daa32d7dfe19d90fa9fa41637b858ececfb6eaf3a983a43f6c
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.1
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.1'
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.1 includes the `mk_frame_init` executable. Install the gem, then run
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.1
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, route, and destination.
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 a single
74
- `POST /posts` create route with one controller and one handler. Inline
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. The generated
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.1.gem`. See
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, model, create controller, and handler.'
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}; route: POST /#{resource}"
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 self.class.camelize(resource) + 'CreateController' == model_class ||
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 one resource
4
- route: `POST /<%= config.resource %>`. The create controller prepares the record;
5
- MK saves it once, and the handler returns JSON with status 201.
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 rackup --host 127.0.0.1 --port 9292
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:9292/<%= config.resource %> \
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. Invalid input types return 400; missing fields or
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/create.rb` permits inputs and returns an unsaved model.
47
- - `routes/<%= config.resource %>/handlers/create.rb` filters raw data and formats the response.
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 adding it to `resources` in `app.rb`.
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
- task default: :spec
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 %>, only: [:create], singular: '<%= config.model_name %>'
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 %>CreateController < MK::Controller
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
@@ -3,6 +3,8 @@
3
3
  require 'sequel'
4
4
  require 'sequel/extensions/migration'
5
5
 
6
+ Sequel.default_timezone = :utc
7
+
6
8
  module <%= config.namespace %>
7
9
  ROOT = __dir__.freeze
8
10
  DB = if ENV['RACK_ENV'] == 'test'
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module <%= config.namespace %>
4
- class <%= config.action_prefix %>CreateHandler < MK::Handler
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
- {<%= config.model_name %>: model.slice(*<%= config.model_class %>.public_attributes_list)}
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 %> creation' 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 %>) }
5
+ RSpec.describe '<%= config.model_class %> CRUD' do
6
+ include Rack::Test::Methods
8
7
 
9
- before { <%= config.namespace %>::<%= config.model_class %>.dataset.delete }
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
- response = client.post('/<%= config.resource %>', input: JSON.generate(attributes.merge('id' => 999)),
13
- 'CONTENT_TYPE' => 'application/json')
17
+ post('/<%= config.resource %>', attributes.merge('id' => 999).to_json)
14
18
 
15
- expect(response.status).to eq(201)
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(response.body).fetch('<%= config.model_name %>')
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
- response = client.post('/<%= config.resource %>', input: '{}', 'CONTENT_TYPE' => 'application/json')
29
+ post('/<%= config.resource %>', {}.to_json)
26
30
 
27
- expect(response.status).to eq(422)
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
- response = client.post('/<%= config.resource %>', input: JSON.generate(attributes), 'CONTENT_TYPE' => 'application/json')
37
+ post('/<%= config.resource %>', attributes.to_json)
34
38
 
35
- expect(response.status).to eq(400)
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  ENV['RACK_ENV'] = 'test'
4
4
  require 'rspec'
5
- require 'rack/mock'
5
+ require 'rack/test'
6
6
  require 'rack/builder'
7
7
  require 'json'
8
8
  require_relative '../database'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MK
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.3'
5
5
  end
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.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francesco Canessa