infinum_json_api_setup 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +45 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +1 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +53 -15
  7. data/Gemfile +1 -0
  8. data/Gemfile.lock +1 -0
  9. data/README.md +19 -0
  10. data/Rakefile +3 -0
  11. data/base.gemfile +14 -0
  12. data/infinum_json_api_setup.gemspec +11 -16
  13. data/lib/generators/infinum_json_api_setup/templates/config/locales/json_api.en.yml +1 -0
  14. data/lib/infinum_json_api_setup/json_api/content_negotiation.rb +2 -2
  15. data/lib/infinum_json_api_setup/json_api/locale_negotiation.rb +45 -0
  16. data/lib/infinum_json_api_setup/json_api/serializer_options.rb +8 -1
  17. data/lib/infinum_json_api_setup/rails.rb +4 -2
  18. data/lib/infinum_json_api_setup/rspec/helpers/response_helper.rb +4 -4
  19. data/lib/infinum_json_api_setup/rspec/matchers/have_empty_data.rb +1 -1
  20. data/lib/infinum_json_api_setup/rspec/matchers/have_error_pointer.rb +1 -1
  21. data/lib/infinum_json_api_setup/rspec/matchers/have_resource_count_of.rb +1 -1
  22. data/lib/infinum_json_api_setup/version.rb +1 -1
  23. data/lib/infinum_json_api_setup.rb +3 -0
  24. data/rails.7.1.gemfile +8 -0
  25. data/rails.7.1.gemfile.lock +361 -0
  26. data/rails.8.0.gemfile +7 -0
  27. data/rails.8.0.gemfile.lock +380 -0
  28. data/spec/dummy/app/controllers/api/v1/base_controller.rb +1 -0
  29. data/spec/dummy/app/controllers/api/v1/hello_controller.rb +11 -0
  30. data/spec/dummy/app/controllers/api/v1/locations_controller.rb +7 -13
  31. data/spec/dummy/app/models/location.rb +1 -1
  32. data/spec/dummy/config/database.yml +3 -0
  33. data/spec/dummy/config/environments/production.rb +1 -1
  34. data/spec/dummy/config/initializers/i18n.rb +3 -0
  35. data/spec/dummy/config/locales/de.yml +2 -0
  36. data/spec/dummy/config/locales/json_api.de.yml +22 -0
  37. data/spec/dummy/config/locales/json_api.en.yml +1 -0
  38. data/spec/dummy/config/routes.rb +1 -0
  39. data/spec/infinum_json_api_setup/json_api/serializer_options_spec.rb +69 -0
  40. data/spec/infinum_json_api_setup/rspec/helpers/response_helper_spec.rb +234 -0
  41. data/spec/infinum_json_api_setup/rspec/matchers/have_empty_data_spec.rb +3 -2
  42. data/spec/rails_helper.rb +2 -6
  43. data/spec/requests/api/v1/content_negotiation_spec.rb +10 -2
  44. data/spec/requests/api/v1/error_handling_spec.rb +37 -19
  45. data/spec/requests/api/v1/locale_negotiation_spec.rb +66 -0
  46. data/spec/requests/api/v1/responder_spec.rb +1 -1
  47. data/spec/requests/api/v1/serializer_options_spec.rb +1 -1
  48. metadata +47 -99
  49. data/Gemfile +0 -10
  50. data/Gemfile.lock +0 -268
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f29be6fb7cd837fda6ba353b4f22fcadbf8139328b8a74c73f373954310abb4f
4
- data.tar.gz: 4a3272b935f333a35c2b671287d02395ce5140e300a60ff9d7cf51cc7109ef7d
3
+ metadata.gz: 02c01a6edfee1465e478a630629fd38b0cd35f08a84a32a2eac73c353440bebc
4
+ data.tar.gz: f94a539f053417cfc9e0a4c938eaa594a02e422930fee2f46b80aebffeaa17e0
5
5
  SHA512:
6
- metadata.gz: 50ab89b7384a97fd64a450cc10921b1ed2b6b9535603568af7c5e09110dcdb048be752b351e4343db9e30c2fc85fe938caa53d3769904d771570d1c8bd244517
7
- data.tar.gz: 99ada6a80d54574f9cce79e702ba166a98bf07a6363529d3138afcd6d87a0332fc4118d155de6d4e72ae07996c951fc1e73e35f31f21a442215ea375133ba003
6
+ metadata.gz: eadacbfe9298f7345e3f8a639b5dee0d39515b030a9519fa9c020667d2dfe65f52a7326971853dbb52a5a25b5aea407e049da466ea6e7bcf86fe49765568fc0d
7
+ data.tar.gz: 6d811bc78b9f2e999a40c5e08d250277cf5f70bc0307b6f61fba1f336907a36b6f813dd57fb6bcdeebb5d26fc789711521bbae0c17f103bee5ac27449a5973b0
@@ -0,0 +1,45 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ services:
10
+ database:
11
+ image: postgres:14
12
+ env:
13
+ POSTGRES_HOST_AUTH_METHOD: trust
14
+ ports:
15
+ - 5432:5432
16
+ options: >-
17
+ --health-cmd pg_isready
18
+ --health-interval 10s
19
+ --health-timeout 5s
20
+ --health-retries 5
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ ruby: ['3.2.9', '3.3.9', '3.4.5']
25
+ gemfile: ['rails.7.1.gemfile', 'rails.8.0.gemfile']
26
+ env:
27
+ BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
28
+ steps:
29
+ - uses: actions/checkout@v2
30
+ - name: Set up Ruby
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: "${{ matrix.ruby }}"
34
+ - name: Install dependencies
35
+ run: bundle install
36
+ - name: Setup database
37
+ env:
38
+ BUNDLE_GEMFILE: "../../${{ matrix.gemfile }}"
39
+ run: |
40
+ cd spec/dummy
41
+ bundle exec rake db:test:prepare
42
+ - name: Run rubocop
43
+ run: bundle exec rubocop
44
+ - name: Run specs
45
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -4,3 +4,5 @@
4
4
  /spec/dummy/tmp/
5
5
 
6
6
  coverage
7
+
8
+ /pkg
data/.rubocop.yml CHANGED
@@ -22,7 +22,7 @@ Style/FrozenStringLiteralComment:
22
22
  Enabled: false
23
23
 
24
24
  AllCops:
25
- TargetRubyVersion: 2.7
25
+ TargetRubyVersion: 3.2
26
26
  Exclude:
27
27
  - 'spec/dummy/db/schema.rb'
28
28
  - 'spec/dummy/db/migrate/*.rb'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.2
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -1,22 +1,60 @@
1
- ## 0.0.8 (Oct 25th, 2021)
2
- - update and upgrade pagy
1
+ # Change Log for infinum/infinum-json-api-setup
3
2
 
4
- [Compare v0.0.7...v0.0.8](https://github.com/infinum/infinum-json-api-setup/compare/v0.0.7...v0.0.8)
3
+ All notable changes to this project will be documented in this file.
5
4
 
6
- ## 0.0.7 (Oct 25th, 2021)
7
- - fix runtime depenency loading
5
+ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/).
8
6
 
9
- [Compare v0.0.6...v0.0.7](https://github.com/infinum/infinum-json-api-setup/compare/v0.0.6...v0.0.7)
7
+ ## [Unreleased]
10
8
 
11
- ## 0.0.6 (Oct 25th, 2021)
12
- - update installation instructions in README
13
- - promote jsonapi-query-builder and jsonapi-serializer to runtime dependencies
14
- - start CHANGELOG
9
+ ### Changes
15
10
 
16
- [Compare v0.0.5...v0.0.6](https://github.com/infinum/infinum-json-api-setup/compare/v0.0.5...v0.0.6)
11
+ ## [0.1.1] - 2026-08-13
17
12
 
18
- ## 0.0.5 (Oct 6th, 2021)
19
- - bump jsonapi-query_builder dependency
20
- - bump Nokogiri dependency (due to CVE)
13
+ ### Fixed
14
+ - Merge controller-provided `meta` into pagination meta instead of replacing it when rendering paginated JSON:API collections
21
15
 
22
- [Compare v0.0.4...v0.0.4](https://github.com/infinum/infinum-json-api-setup/compare/v0.0.4...v0.0.5)
16
+ ## [0.1.0] - 2025-11-03
17
+
18
+ ### Added
19
+ - Rails 8 support
20
+ - Locale negotiation
21
+
22
+ ### Changes
23
+ - Reformat CHANGELOG according to infinum/open-source-project-template
24
+ - Drop support for Rails 6.1
25
+ - Relax json_schemer dependency
26
+
27
+ ### Fixed
28
+ - Ruby 3.4 ostruct deprecation warning
29
+
30
+ ## [0.0.8] - 2021-10-25
31
+
32
+ ### Updated
33
+ - Bump pagy dependency
34
+
35
+ ## [0.0.7] - 2021-10-25
36
+
37
+ ### Fixed
38
+ - Runtime depenency loading
39
+
40
+ ## [0.0.6] - 2021-10-25
41
+
42
+ ### Added
43
+ - CHANGELOG
44
+
45
+ ### Changed
46
+ - Update installation instructions in README
47
+ - Promote jsonapi-query-builder and jsonapi-serializer to runtime dependencies
48
+
49
+ ## [0.0.5] - 2021-10-06
50
+
51
+ ### Updated
52
+ - Bump jsonapi-query_builder dependency
53
+ - Bump Nokogiri dependency (due to CVE)
54
+
55
+ [Unreleased]: https://github.com/infinum/infinum-json-api-setup/compare/v0.1.0...HEAD
56
+ [0.1.0]: https://github.com/infinum/infinum-json-api-setup/compare/v0.0.8...v0.1.0
57
+ [0.0.8]: https://github.com/infinum/infinum-json-api-setup/compare/v0.0.7...v0.0.8
58
+ [0.0.7]: https://github.com/infinum/infinum-json-api-setup/compare/v0.0.6...v0.0.7
59
+ [0.0.6]: https://github.com/infinum/infinum-json-api-setup/compare/v0.0.5...v0.0.6
60
+ [0.0.5]: https://github.com/infinum/infinum-json-api-setup/compare/v0.0.4...v0.0.5
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ rails.7.1.gemfile
data/Gemfile.lock ADDED
@@ -0,0 +1 @@
1
+ rails.7.1.gemfile.lock
data/README.md CHANGED
@@ -24,6 +24,7 @@ module Api
24
24
  class BaseController < ActionController::API
25
25
  include InfinumJsonApiSetup::JsonApi::ErrorHandling
26
26
  include InfinumJsonApiSetup::JsonApi::ContentNegotiation
27
+ include InfinumJsonApiSetup::JsonApi::LocaleNegotiation
27
28
 
28
29
  self.responder = InfinumJsonApiSetup::JsonApi::Responder
29
30
  respond_to :json_api
@@ -65,12 +66,30 @@ def destroy
65
66
  end
66
67
  ```
67
68
 
69
+ For paginated collections, pass `pagination_details` from your query object. You can add extra collection-level fields to `meta`; they are merged with pagination meta instead of replacing it.
70
+ ```ruby
71
+ def index
72
+ q = Users::Query.new(User.all, params.to_unsafe_hash)
73
+
74
+ respond_with q.results,
75
+ pagination_details: q.pagination_details,
76
+ meta: { total_unread: current_user.unread_notifications_count }
77
+ end
78
+ ```
79
+
68
80
  ## Internals
69
81
  This section explains the under-the-hood behavior of the library.
70
82
 
71
83
  ### Content negotiation
72
84
  `InfinumJsonApiSetup::JsonApi::ContentNegotiation` module is designed to integrate [server responsibilities](https://jsonapi.org/format/#content-negotiation-servers) of content negotiation protocol described by the JSON:API specification.
73
85
 
86
+ ### Locale negotiation
87
+ `InfinumJsonApiSetup::JsonApi::LocaleNegotiation` module ensures that request handling happens within the locale best matching the `Accept-Language` header.
88
+
89
+ - **Default behavior:** locale falls back to the application's default when the header is missing.
90
+ - **Invalid locales:** enable graceful fallback by setting `self.fallback_to_default_locale_if_invalid = true` in your controller when you want to disregard unrecognized locales instead of raising `json_api.errors.bad_request.invalid_locale`.
91
+ - **Error handling:** when fallback is disabled (default), requests with invalid locales trigger a JSON:API formatted bad request response.
92
+
74
93
  ### Error handling
75
94
  `InfinumJsonApiSetup::JsonApi::ErrorHandling` module is designed to catch and handle common exceptions that might bubble up when processing a request.
76
95
 
data/Rakefile CHANGED
@@ -1 +1,4 @@
1
1
  require 'bundler/gem_tasks'
2
+ require 'gemika/tasks'
3
+
4
+ task default: 'matrix:spec'
data/base.gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'bundler-audit', require: false
4
+ gem 'factory_bot', '~> 6.2'
5
+ gem 'faker', '~> 2.18'
6
+ gem 'overcommit', '~> 0.58', require: false
7
+ gem 'pg'
8
+ gem 'pry-rails'
9
+ gem 'pundit'
10
+ gem 'rake', require: false
11
+ gem 'rubocop', '~> 1.0', require: false
12
+ gem 'rubocop-infinum', require: false
13
+ gem 'rubocop-rake', require: false
14
+ gem 'simplecov', require: false
@@ -10,21 +10,16 @@ Gem::Specification.new do |s|
10
10
  s.files = `git ls-files`.split("\n")
11
11
  s.homepage = 'https://github.com/infinum/infinum-json-api-setup'
12
12
  s.license = 'MIT'
13
- s.required_ruby_version = '> 2.7'
13
+ s.required_ruby_version = '>= 3.2'
14
+ s.metadata = { 'rubygems_mfa_required' => 'true' }
14
15
 
15
- s.add_runtime_dependency 'jsonapi_parameters'
16
- s.add_runtime_dependency 'jsonapi-query_builder'
17
- s.add_runtime_dependency 'jsonapi-serializer'
18
- s.add_runtime_dependency 'json_schemer', '~> 0.2'
19
- s.add_runtime_dependency 'pagy'
20
- s.add_runtime_dependency 'rails'
21
- s.add_runtime_dependency 'responders'
22
-
23
- s.add_development_dependency 'overcommit', '~> 0.58'
24
- s.add_development_dependency 'pg'
25
- s.add_development_dependency 'pry-rails'
26
- s.add_development_dependency 'pundit'
27
- s.add_development_dependency 'rake'
28
- s.add_development_dependency 'rspec-rails', '~> 5.0'
29
- s.add_development_dependency 'simplecov'
16
+ s.add_dependency 'accept_language', '~> 2.0'
17
+ s.add_dependency 'jsonapi_parameters'
18
+ s.add_dependency 'jsonapi-query_builder'
19
+ s.add_dependency 'jsonapi-serializer'
20
+ s.add_dependency 'json_schemer', '>= 0.2', '< 3'
21
+ s.add_dependency 'ostruct'
22
+ s.add_dependency 'pagy', '~> 8.0'
23
+ s.add_dependency 'rails'
24
+ s.add_dependency 'responders'
30
25
  end
@@ -4,6 +4,7 @@ en:
4
4
  bad_request:
5
5
  title: Bad Request
6
6
  detail: Bad request
7
+ invalid_locale: Invalid locale
7
8
  not_found:
8
9
  title: Not found
9
10
  detail: Resource not found
@@ -16,9 +16,9 @@ module InfinumJsonApiSetup
16
16
  end
17
17
 
18
18
  def valid_content_type?
19
- return true if request.body.size.zero?
19
+ return true if request.body.nil? || request.body.size.zero? # rubocop:disable Style/ZeroLengthPredicate
20
20
 
21
- request.content_type == Mime.fetch(:json_api)
21
+ request.content_mime_type == Mime.fetch(:json_api)
22
22
  end
23
23
 
24
24
  def acceptable?
@@ -0,0 +1,45 @@
1
+ module InfinumJsonApiSetup
2
+ module JsonApi
3
+ module LocaleNegotiation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ around_action :setup_locale
8
+ class_attribute :fallback_to_default_locale_if_invalid, default: false
9
+ end
10
+
11
+ def rescue_with_handler(*)
12
+ I18n.with_locale(locale) { super }
13
+ end
14
+
15
+ private
16
+
17
+ def setup_locale(&)
18
+ return render_invalid_locale_error if locale.blank?
19
+
20
+ I18n.with_locale(locale, &)
21
+ end
22
+
23
+ def locale
24
+ return I18n.default_locale if locale_from_request.blank?
25
+
26
+ parsed_locale = AcceptLanguage.parse(locale_from_request).match(*I18n.available_locales)
27
+
28
+ if parsed_locale.present?
29
+ parsed_locale
30
+ elsif fallback_to_default_locale_if_invalid
31
+ I18n.default_locale
32
+ end
33
+ end
34
+
35
+ def locale_from_request
36
+ request.env['HTTP_ACCEPT_LANGUAGE']
37
+ end
38
+
39
+ def render_invalid_locale_error
40
+ message = I18n.t('json_api.errors.bad_request.invalid_locale')
41
+ render_error(InfinumJsonApiSetup::Error::BadRequest.new(message:))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,9 +4,11 @@ module InfinumJsonApiSetup
4
4
  # @param [Hash] opts
5
5
  # @option opts [Hash] :params
6
6
  # @option opts [Object] :pagination_details
7
- def initialize(params:, pagination_details:)
7
+ # @option opts [Hash] :extra_meta additional collection meta merged into pagination meta
8
+ def initialize(params:, pagination_details:, extra_meta: nil)
8
9
  @params = params
9
10
  @pagination_details = pagination_details
11
+ @extra_meta = extra_meta || {}
10
12
  end
11
13
 
12
14
  # @return [Hash]
@@ -23,8 +25,13 @@ module InfinumJsonApiSetup
23
25
 
24
26
  attr_reader :params
25
27
  attr_reader :pagination_details
28
+ attr_reader :extra_meta
26
29
 
27
30
  def meta
31
+ pagination_meta.merge(extra_meta)
32
+ end
33
+
34
+ def pagination_meta
28
35
  return {} unless pagination_details
29
36
 
30
37
  {
@@ -19,10 +19,12 @@ ActiveSupport.on_load(:action_controller) do
19
19
  "#{controller_path.classify.pluralize}::Serializer".constantize
20
20
  end
21
21
  options = InfinumJsonApiSetup::JsonApi::SerializerOptions.new(
22
- params: params.to_unsafe_h, pagination_details: opts[:pagination_details]
22
+ params: params.to_unsafe_h,
23
+ pagination_details: opts[:pagination_details],
24
+ extra_meta: opts[:meta]
23
25
  ).build
24
26
 
25
- serializer.new(resources, options.merge(opts)).serializable_hash.to_json
27
+ serializer.new(resources, options.merge(opts.except(:meta))).serializable_hash.to_json
26
28
  end
27
29
  end
28
30
  end
@@ -9,14 +9,14 @@ module InfinumJsonApiSetup
9
9
  def response_item
10
10
  raise 'json response is not an item' if json_response[:data].is_a?(Array)
11
11
 
12
- OpenStruct.new(json_response[:data][:attributes])
12
+ OpenStruct.new(json_response[:data][:attributes]) # rubocop:disable Style/OpenStructUse
13
13
  end
14
14
 
15
15
  def response_collection
16
16
  raise 'json response is not a collection' unless json_response[:data].is_a?(Array)
17
17
 
18
18
  json_response[:data].map do |item|
19
- OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes])
19
+ OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes]) # rubocop:disable Style/OpenStructUse
20
20
  end
21
21
  end
22
22
 
@@ -24,7 +24,7 @@ module InfinumJsonApiSetup
24
24
  case response_type
25
25
  when :item then json_response.dig(:data, :relationships)
26
26
  when :collection then json_response[:data].pluck(:relationships)
27
- else raise ArgumentError ':response_type must be one of [:item, :collection]'
27
+ else raise ArgumentError, ':response_type must be one of [:item, :collection]'
28
28
  end
29
29
  end
30
30
 
@@ -34,7 +34,7 @@ module InfinumJsonApiSetup
34
34
 
35
35
  def response_included
36
36
  json_response[:included].map do |item|
37
- OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes])
37
+ OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes]) # rubocop:disable Style/OpenStructUse
38
38
  end
39
39
  end
40
40
 
@@ -2,7 +2,7 @@ module InfinumJsonApiSetup
2
2
  module RSpec
3
3
  module Matchers
4
4
  # @return [InfinumJsonApiSetup::Rspec::Matchers::HaveEmptyData]
5
- def have_empty_data # rubocop:disable Naming/PredicateName
5
+ def have_empty_data # rubocop:disable Naming/PredicatePrefix
6
6
  HaveEmptyData.new
7
7
  end
8
8
 
@@ -3,7 +3,7 @@ module InfinumJsonApiSetup
3
3
  module Matchers
4
4
  # @param [String] pointer
5
5
  # @return [InfinumJsonApiSetup::Rspec::Matchers::HaveErrorPointer]
6
- def have_error_pointer(pointer) # rubocop:disable Naming/PredicateName
6
+ def have_error_pointer(pointer) # rubocop:disable Naming/PredicatePrefix
7
7
  HaveErrorPointer.new(pointer)
8
8
  end
9
9
 
@@ -3,7 +3,7 @@ module InfinumJsonApiSetup
3
3
  module Matchers
4
4
  # @param [Integer] expected_count
5
5
  # @return [InfinumJsonApiSetup::Rspec::Matchers::HaveResourceCountOf]
6
- def have_resource_count_of(expected_count) # rubocop:disable Naming/PredicateName
6
+ def have_resource_count_of(expected_count) # rubocop:disable Naming/PredicatePrefix
7
7
  HaveResourceCountOf.new(expected_count)
8
8
  end
9
9
 
@@ -1,3 +1,3 @@
1
1
  module InfinumJsonApiSetup
2
- VERSION = '0.0.8'.freeze
2
+ VERSION = '0.1.1'.freeze
3
3
  end
@@ -1,9 +1,11 @@
1
1
  require 'rails'
2
2
 
3
+ require 'accept_language'
3
4
  require 'json'
4
5
  require 'jsonapi_parameters'
5
6
  require 'jsonapi/serializer'
6
7
  require 'json_schemer'
8
+ require 'ostruct'
7
9
  require 'responders'
8
10
 
9
11
  require 'pagy'
@@ -14,6 +16,7 @@ require 'infinum_json_api_setup/json_api/error_handling'
14
16
  require 'infinum_json_api_setup/json_api/error_serializer'
15
17
 
16
18
  require 'infinum_json_api_setup/json_api/content_negotiation'
19
+ require 'infinum_json_api_setup/json_api/locale_negotiation'
17
20
  require 'infinum_json_api_setup/json_api/request_parsing'
18
21
  require 'infinum_json_api_setup/json_api/serializer_options'
19
22
  require 'infinum_json_api_setup/json_api/responder'
data/rails.7.1.gemfile ADDED
@@ -0,0 +1,8 @@
1
+ eval(File.read(File.expand_path('base.gemfile', __dir__))) # rubocop:disable Security/Eval
2
+
3
+ gem 'gemika'
4
+ gem 'observer'
5
+ gem 'rails', '~> 7.1.0'
6
+ gem 'rspec-rails', '~> 7.0', require: false
7
+
8
+ gemspec