diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index aeb9b1ae9..c5de44418 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -2,78 +2,38 @@ name: CI on: push: - branches: [ 'master', 'release-0-8', 'release-0-9', 'release-0-10' ] + branches: ["master"] pull_request: - branches: ['**'] + branches: ["**"] jobs: tests: runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: password - POSTGRES_DB: test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 strategy: fail-fast: false matrix: ruby: - - 2.6 - - 2.7 - - '3.0' - - 3.1 - - 3.2 + - "3.2" + - "3.3" + - "3.4" + - "4.0" rails: - - 7.0.4 - - 6.1.7 - - 6.0.6 - - 5.2.8.1 - - 5.1.7 + - "7.1" + - "7.2" + - "8.0" + - "8.1" database_url: - - postgresql://postgres:password@localhost:5432/test - sqlite3:test_db - exclude: - - ruby: 3.2 - rails: 6.0.6 - - ruby: 3.2 - rails: 5.2.8.1 - - ruby: 3.2 - rails: 5.1.7 - - ruby: 3.1 - rails: 6.0.6 - - ruby: 3.1 - rails: 5.2.8.1 - - ruby: 3.1 - rails: 5.1.7 - - ruby: '3.0' - rails: 6.0.6 - - ruby: '3.0' - rails: 5.2.8.1 - - ruby: '3.0' - rails: 5.1.7 - - ruby: 2.6 - rails: 7.0.4 - - database_url: postgresql://postgres:password@localhost:5432/test - rails: 5.1.7 env: RAILS_VERSION: ${{ matrix.rails }} DATABASE_URL: ${{ matrix.database_url }} name: Ruby ${{ matrix.ruby }} Rails ${{ matrix.rails }} DB ${{ matrix.database_url }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 + bundler-cache: true - name: Run tests run: bundle exec rake test diff --git a/.gitignore b/.gitignore index 800c71c6a..ba614b629 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,6 @@ coverage test/log test_db test_db-journal +test/test_db-* .idea *.iml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..03ce14d91 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing + +All contributions to this project are subject to the [MIT License](https://foss-haas.mit-license.org/). By submitting a contribution, you agree to license your work under these terms. + +## Contribution Process + +### 1. Issue First + +All contributions from outside the core team require an **Issue First** approach. Before submitting a pull request (PR), you must: + +- Open an issue in the repository. +- Ensure the issue includes: + - **Clear problem statement:** Describe the issue or feature request. + - **Reproduction steps:** If reporting a bug, provide steps to reproduce it. + - **Proposed approach:** Outline your suggested solution or implementation. + - **Why this change matters:** Explain the impact or necessity of the change. +- Tag `@sanger/psd-developers` in the issue to bring it to the attention of a maintainer. +- Wait for the issue to be assigned or approved by a maintainer. + +### 2. Pull Request + +Once your issue is approved: + +- Fork the repository and create a branch for your changes. +- Submit a PR referencing the approved issue. +- Ensure your code adheres to the project's coding standards and passes all tests. + +### 3. Review + +Maintainers will review your PR. Address any feedback before merging. \ No newline at end of file diff --git a/Gemfile b/Gemfile index 2535d0200..1b438ec56 100644 --- a/Gemfile +++ b/Gemfile @@ -7,15 +7,14 @@ platforms :jruby do end version = ENV['RAILS_VERSION'] || 'default' +# If version is like 'x.y' add a '.0' to make it 'x.y.0' for correct resolution +version = "#{version}.0" if version =~ /^\d+\.\d+$/ platforms :ruby do gem 'pg' - - if version.start_with?('4.2', '5.0') - gem 'sqlite3', '~> 1.3.13' - else - gem 'sqlite3', '~> 1.4' - end + gem 'mysql2' + gem 'sqlite3' + gem 'csv' end case version @@ -23,7 +22,7 @@ when 'master' gem 'railties', { git: 'https://github.com/rails/rails.git' } gem 'arel', { git: 'https://github.com/rails/arel.git' } when 'default' - gem 'railties', '>= 6.0' + gem 'railties', '~> 8.1.0' else gem 'railties', "~> #{version}" -end \ No newline at end of file +end diff --git a/README.md b/README.md index 377e49304..6c0b80af5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# JSONAPI::Resources [![Gem Version](https://badge.fury.io/rb/jsonapi-resources.svg)](https://badge.fury.io/rb/jsonapi-resources) [![Build Status](https://secure.travis-ci.org/cerebris/jsonapi-resources.svg?branch=master)](http://travis-ci.org/cerebris/jsonapi-resources) [![Code Climate](https://codeclimate.com/github/cerebris/jsonapi-resources/badges/gpa.svg)](https://codeclimate.com/github/cerebris/jsonapi-resources) - -[![Join the chat at https://gitter.im/cerebris/jsonapi-resources](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# JSONAPI::Resources (Sanger fork) `JSONAPI::Resources`, or "JR", provides a framework for developing an API server that complies with the [JSON:API](http://jsonapi.org/) specification. @@ -13,7 +11,7 @@ backed by ActiveRecord models or by custom objects. ## Documentation -Full documentation can be found at [http://jsonapi-resources.com](http://jsonapi-resources.com), including the [v0.10 alpha Guide](http://jsonapi-resources.com/v0.10/guide/) specific to this version. +Full documentation can be found at [http://jsonapi-resources.com](http://jsonapi-resources.com), including the [v0.10 alpha Guide](http://jsonapi-resources.com/v0.10/guide/) specific to this version. ## Demo App @@ -22,54 +20,74 @@ We have a simple demo app, called [Peeps](https://github.com/cerebris/peeps), av ## Client Libraries JSON:API maintains a (non-verified) listing of [client libraries](http://jsonapi.org/implementations/#client-libraries) -which *should* be compatible with JSON:API compliant server implementations such as JR. +which _should_ be compatible with JSON:API compliant server implementations such as JR. ## Installation Add JR to your application's `Gemfile`: -``` +``` gem 'jsonapi-resources' ``` And then execute: -```bash +```bash bundle ``` Or install it yourself as: -```bash +```bash gem install jsonapi-resources ``` **For further usage see the [v0.10 alpha Guide](http://jsonapi-resources.com/v0.10/guide/)** -## Contributing +## Sanger-specific release process + +There are two versions of the gem which we use for production. The version 1 series has unique customisations and is used by Sequencescape. The version 2 series is a more generic version which is used by Traction. + +The aim is to support the 3 most recent Rails versions for each series to allow for seamless upgrades between releases. -1. Submit an issue describing any new features you wish it add or the bug you intend to fix -1. Fork it ( http://github.com/cerebris/jsonapi-resources/fork ) -1. Create your feature branch (`git checkout -b my-new-feature`) -1. Run the full test suite (`rake test`) -1. Fix any failing tests -1. Commit your changes (`git commit -am 'Add some feature'`) -1. Push to the branch (`git push origin my-new-feature`) -1. Create a new Pull Request +Check which versions we have published at https://rubygems.org/gems/sanger-jsonapi-resources -## Did you find a bug? +### For version 1 series -* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/cerebris/jsonapi-resources/issues). +- Create a branch from **develop**, apply fixes. +- Change version number in `lib/jsonapi/resources/version.rb` . This file is read by the gemspec during publication. +- Test Sequencescape with the gem from the branch. +- Merge the branch into develop. +- Create a release from the develop branch with the new version number as the tag. Set Release label to _None_. + +- Checkout the newly-created tag on develop. +- Execute `bundle install` to ensure the gemspec is up to date. +- Execute `gem build jsonapi-resources.gemspec` which builds sanger-jsonapi-resources-0.1.x.gem in this case. +- Execute `gem push sanger-jsonapi-resources-0.1.x.gem` which publishes the gem. -* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/cerebris/jsonapi-resources/issues/new). -Be sure to include a **title and clear description**, as much relevant information as possible, -and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. +### For version 2 series -* If possible, use the relevant bug report templates to create the issue. -Simply copy the content of the appropriate template into a .rb file, make the necessary changes to demonstrate the issue, -and **paste the content into the issue description or attach as a file**: - * [**Rails 5** issues](https://github.com/cerebris/jsonapi-resources/blob/master/lib/bug_report_templates/rails_5_master.rb) +- Create a branch from **master**, apply fixes. +- Change version number in `lib/jsonapi/resources/version.rb` . This file is read by the gemspec during publication. +- Test Traction with the gem from the branch. +- Merge the branch into master. +- Create a release from the master branch with the new version number as the tag. Set Release label to _Latest_. + +- Checkout the newly-created tag on master. +- Execute `bundle install` to ensure the gemspec is up to date. +- Execute `gem build jsonapi-resources.gemspec` which builds sanger-jsonapi-resources-0.2.x.gem in this case. +- Execute `gem push sanger-jsonapi-resources-0.2.x.gem` which publishes the gem. + +### Publication + +You will be notified by email for each. You will also receive one email for API key setup. The first push will ask email and password for the account, which are in KeePass (search for "gem"). + +> [!NOTE] +> The email and password for gem publication is a recent addition to the credentials database. Pull latest changes in the credentials repo. + +## Contributing +See CONTRIBUTING.md for details. ## License diff --git a/Rakefile b/Rakefile index 01619ed8e..699237f4b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,13 +1,20 @@ #!/usr/bin/env rake require 'bundler/gem_tasks' +require 'fileutils' require 'rake/testtask' +task :remove_test_db do + FileUtils.rm_f(File.expand_path('test/test_db', __dir__)) +end + Rake::TestTask.new do |t| - t.verbose = true + t.verbose = false t.warning = false t.test_files = FileList['test/**/*_test.rb'] end +Rake::Task[:test].enhance([:remove_test_db]) + task default: [:test] desc 'Run benchmarks' diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index eb3c67fa5..52c1f1909 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -4,30 +4,33 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'jsonapi/resources/version' Gem::Specification.new do |spec| - spec.name = 'jsonapi-resources' + spec.name = 'sanger-jsonapi-resources' spec.version = JSONAPI::Resources::VERSION - spec.authors = ['Dan Gebhardt', 'Larry Gebhardt'] - spec.email = ['dan@cerebris.com', 'larry@cerebris.com'] + spec.authors = ['PSD Team - Wellcome Trust Sanger Institute'] + spec.email = ['psd@sanger.ac.uk'] spec.summary = 'Easily support JSON API in Rails.' - spec.description = 'A resource-centric approach to implementing the controllers, routes, and serializers needed to support the JSON API spec.' - spec.homepage = 'https://github.com/cerebris/jsonapi-resources' + spec.description = 'Forked from jsonapi-resources. A resource-centric approach to implementing the controllers, routes, and serializers needed to support the JSON API spec.' + spec.homepage = 'https://github.com/sanger/jsonapi-resources' spec.license = 'MIT' spec.files = Dir.glob("{bin,lib}/**/*") + %w(LICENSE.txt README.md) spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.3' + spec.required_ruby_version = '>= 3.2' spec.add_development_dependency 'bundler', '>= 1.17' spec.add_development_dependency 'rake' - spec.add_development_dependency 'minitest', '~> 5.10', '!= 5.10.2' + spec.add_development_dependency 'minitest' + spec.add_development_dependency 'minitest-mock' spec.add_development_dependency 'minitest-spec-rails' + spec.add_development_dependency 'minitest-reporters' spec.add_development_dependency 'simplecov' spec.add_development_dependency 'pry' spec.add_development_dependency 'concurrent-ruby-ext' spec.add_development_dependency 'database_cleaner' - spec.add_dependency 'activerecord', '>= 5.1' - spec.add_dependency 'railties', '>= 5.1' + spec.add_dependency 'activerecord', '>= 7.1', '< 9.0' # versions 7.1, 7.2, 8.0, 8.1, and above, but not 9.0 + spec.add_dependency 'railties', '>= 7.1', '< 9.0' # versions 7.1, 7.2, 8.0, 8.1, and above, but not 9.0 + spec.add_dependency 'rack', '~> 3.0' spec.add_dependency 'concurrent-ruby' end diff --git a/lib/generators/jsonapi/controller_generator.rb b/lib/generators/jsonapi/controller_generator.rb index 41ee4eb1e..d6aba8bf9 100644 --- a/lib/generators/jsonapi/controller_generator.rb +++ b/lib/generators/jsonapi/controller_generator.rb @@ -1,3 +1,4 @@ +require 'rails/generators' module Jsonapi class ControllerGenerator < Rails::Generators::NamedBase source_root File.expand_path('../templates', __FILE__) diff --git a/lib/generators/jsonapi/resource_generator.rb b/lib/generators/jsonapi/resource_generator.rb index 80aa24b4d..25feb14a1 100644 --- a/lib/generators/jsonapi/resource_generator.rb +++ b/lib/generators/jsonapi/resource_generator.rb @@ -1,3 +1,4 @@ +require 'rails/generators' module Jsonapi class ResourceGenerator < Rails::Generators::NamedBase source_root File.expand_path('../templates', __FILE__) diff --git a/lib/jsonapi/active_relation_resource.rb b/lib/jsonapi/active_relation_resource.rb index 581ed1e02..bfe88af26 100644 --- a/lib/jsonapi/active_relation_resource.rb +++ b/lib/jsonapi/active_relation_resource.rb @@ -666,10 +666,14 @@ def find_related_polymorphic_fragments(source_fragments, relationship, options, end relation_position = relation_positions[row[2].downcase.pluralize] - model_fields = relation_position[:model_fields] - cache_field = relation_position[:cache_field] - cache_offset = relation_position[:cache_offset] - field_offset = relation_position[:field_offset] + if relation_position + model_fields = relation_position[:model_fields] + cache_field = relation_position[:cache_field] + cache_offset = relation_position[:cache_offset] + field_offset = relation_position[:field_offset] + else + next # Skip processing if relation_position is nil + end if cache_field related_fragments[rid].cache = cast_to_attribute_type(row[cache_offset], cache_field[:type]) diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index e448fa0ea..8b29043d1 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'csv' - +require_relative 'compatibility_helper' module JSONAPI module ActsAsResourceController MEDIA_TYPE_MATCHER = /.+".+"[^,]*|[^,]+/ @@ -63,16 +63,16 @@ def index_related_resources def get_related_resource # :nocov: - ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resource`"\ - " action. Please use `show_related_resource` instead." + JSONAPI::CompatibilityHelper.deprecation_warn("In #{self.class.name} you exposed a `get_related_resource`"\ + " action. Please use `show_related_resource` instead.") show_related_resource # :nocov: end def get_related_resources # :nocov: - ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\ - " action. Please use `index_related_resources` instead." + JSONAPI::CompatibilityHelper.deprecation_warn("In #{self.class.name} you exposed a `get_related_resources`"\ + " action. Please use `index_related_resources` instead.") index_related_resources # :nocov: end diff --git a/lib/jsonapi/basic_resource.rb b/lib/jsonapi/basic_resource.rb index 2eeba5c5d..d35ad796d 100644 --- a/lib/jsonapi/basic_resource.rb +++ b/lib/jsonapi/basic_resource.rb @@ -2,7 +2,7 @@ require 'jsonapi/callbacks' require 'jsonapi/configuration' - +require_relative 'compatibility_helper' module JSONAPI class BasicResource include Callbacks @@ -547,7 +547,7 @@ def attribute(attribute_name, options = {}) check_reserved_attribute_name(attr) if (attr == :id) && (options[:format].nil?) - ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.') + JSONAPI::CompatibilityHelper.deprecation_warn('Id without format is deprecated. Please specify a format for the id attribute.') end check_duplicate_attribute_name(attr) if options[:format].nil? @@ -609,11 +609,12 @@ def has_one(*attrs) end def belongs_to(*attrs) - ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\ + + JSONAPI::CompatibilityHelper.deprecation_warn( "In #{name} you exposed a `has_one` relationship "\ " using the `belongs_to` class method. We think `has_one`" \ " is more appropriate. If you know what you're doing," \ " and don't want to see this warning again, override the" \ - " `belongs_to` class method on your resource." + " `belongs_to` class method on your resource.") _add_relationship(Relationship::ToOne, *attrs) end diff --git a/lib/jsonapi/compatibility_helper.rb b/lib/jsonapi/compatibility_helper.rb new file mode 100644 index 000000000..be6379914 --- /dev/null +++ b/lib/jsonapi/compatibility_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# JSONAPI::CompatibilityHelper +# +# This module provides a version-safe method for issuing deprecation warnings +# that works across multiple versions of Rails (7.x, 8.x, etc). +# +# Usage: +# JSONAPI::CompatibilityHelper.deprecation_warn("Your deprecation message") +# +# The method will use the public `warn` method if available, otherwise it will +# use Rails 8+ style deprecation warnings. +# +# Example: +# JSONAPI::CompatibilityHelper.deprecation_warn("This feature is deprecated.") + +module JSONAPI + module CompatibilityHelper + def deprecation_warn(message) + ActiveSupport::Deprecation.new(nil, 'JSONAPI').warn(message) + end + module_function :deprecation_warn + end +end diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 6cd5d8e1b..e1a7e1c61 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -3,7 +3,7 @@ require 'jsonapi/formatter' require 'jsonapi/processor' require 'concurrent' - +require_relative 'compatibility_helper' module JSONAPI class Configuration attr_reader :json_key_format, @@ -227,7 +227,7 @@ def exception_class_allowed?(e) end def default_processor_klass=(default_processor_klass) - ActiveSupport::Deprecation.warn('`default_processor_klass` has been replaced by `default_processor_klass_name`.') + JSONAPI::CompatibilityHelper.deprecation_warn('`default_processor_klass` has been replaced by `default_processor_klass_name`.') @default_processor_klass = default_processor_klass end @@ -241,18 +241,18 @@ def default_processor_klass_name=(default_processor_klass_name) end def allow_include=(allow_include) - ActiveSupport::Deprecation.warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.') + JSONAPI::CompatibilityHelper.deprecation_warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.') @default_allow_include_to_one = allow_include @default_allow_include_to_many = allow_include end def whitelist_all_exceptions=(allow_all_exceptions) - ActiveSupport::Deprecation.warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`') + JSONAPI::CompatibilityHelper.deprecation_warn('`whitelist_all_exceptions` has been replaced by `allow_all_exceptions`') @allow_all_exceptions = allow_all_exceptions end def exception_class_whitelist=(exception_class_allowlist) - ActiveSupport::Deprecation.warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`') + JSONAPI::CompatibilityHelper.deprecation_warn('`exception_class_whitelist` has been replaced by `exception_class_allowlist`') @exception_class_allowlist = exception_class_allowlist end diff --git a/lib/jsonapi/error.rb b/lib/jsonapi/error.rb index 12d65f585..9e23d30ca 100644 --- a/lib/jsonapi/error.rb +++ b/lib/jsonapi/error.rb @@ -17,7 +17,7 @@ def initialize(options = {}) @source = options[:source] @links = options[:links] - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s + @status = status_code(options[:status]).to_s @meta = options[:meta] end @@ -48,11 +48,26 @@ def update_with_overrides(error_object_overrides) if error_object_overrides[:status] # :nocov: - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[error_object_overrides[:status]].to_s + @status = status_code(error_object_overrides[:status]).to_s # :nocov: end @meta = error_object_overrides[:meta] || @meta end + + private + + # Extracted from Rack 2 + def status_code(status) + if status.nil? + raise ArgumentError, "Status code is required" + end + + if status.is_a?(Symbol) + Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } + else + status.to_i + end + end end class Warning diff --git a/lib/jsonapi/exceptions.rb b/lib/jsonapi/exceptions.rb index e917118cf..a0c437eea 100644 --- a/lib/jsonapi/exceptions.rb +++ b/lib/jsonapi/exceptions.rb @@ -498,7 +498,7 @@ def errors def json_api_error(attr_key, message) create_error_object(code: JSONAPI::VALIDATION_ERROR, - status: :unprocessable_entity, + status: :unprocessable_content, title: message, detail: detail(attr_key, message), source: { pointer: pointer(attr_key) }, @@ -532,7 +532,7 @@ def general_error?(attr_key) class SaveFailed < Error def errors [create_error_object(code: JSONAPI::SAVE_FAILED, - status: :unprocessable_entity, + status: :unprocessable_content, title: I18n.translate('jsonapi-resources.exceptions.save_failed.title', default: 'Save failed or was cancelled'), detail: I18n.translate('jsonapi-resources.exceptions.save_failed.detail', diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index 8824fc65d..62c17d9c8 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true - +require_relative 'compatibility_helper' module JSONAPI class Relationship attr_reader :acts_as_set, :foreign_key, :options, :name, @@ -21,7 +21,7 @@ def initialize(name, options = {}) @polymorphic = options.fetch(:polymorphic, false) == true @polymorphic_types = options[:polymorphic_types] if options[:polymorphic_relations] - ActiveSupport::Deprecation.warn('Use polymorphic_types instead of polymorphic_relations') + JSONAPI::CompatibilityHelper.deprecation_warn('Use polymorphic_types instead of polymorphic_relations') @polymorphic_types ||= options[:polymorphic_relations] end diff --git a/lib/jsonapi/resource.rb b/lib/jsonapi/resource.rb index 4d34dd290..421b46ea2 100644 --- a/lib/jsonapi/resource.rb +++ b/lib/jsonapi/resource.rb @@ -4,4 +4,4 @@ module JSONAPI class Resource < ActiveRelationResource root_resource end -end \ No newline at end of file +end diff --git a/lib/jsonapi/resources/version.rb b/lib/jsonapi/resources/version.rb index fb4178797..401a9449c 100644 --- a/lib/jsonapi/resources/version.rb +++ b/lib/jsonapi/resources/version.rb @@ -1,5 +1,5 @@ module JSONAPI module Resources - VERSION = '0.11.0.beta1' + VERSION = '0.4.2' end end diff --git a/lib/jsonapi/routing_ext.rb b/lib/jsonapi/routing_ext.rb index b0b940138..8a37b8246 100644 --- a/lib/jsonapi/routing_ext.rb +++ b/lib/jsonapi/routing_ext.rb @@ -46,28 +46,14 @@ def jsonapi_resource(*resources, &_block) options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy') end - resource @resource_type, options do - # :nocov: - if @scope.respond_to? :[]= - # Rails 4 - @scope[:jsonapi_resource] = @resource_type - + resource @resource_type, **options do + jsonapi_resource_scope(SingletonResource.new(@resource_type, api_only?, @scope[:shallow], **options), @resource_type) do if block_given? yield else jsonapi_relationships end - else - # Rails 5 - jsonapi_resource_scope(SingletonResource.new(@resource_type, api_only?, @scope[:shallow], options), @resource_type) do - if block_given? - yield - else - jsonapi_relationships - end - end end - # :nocov: end end @@ -121,27 +107,14 @@ def jsonapi_resources(*resources, &_block) options[:except] << :destroy unless options[:except].include?(:destroy) || options[:except].include?('destroy') end - resources @resource_type, options do - # :nocov: - if @scope.respond_to? :[]= - # Rails 4 - @scope[:jsonapi_resource] = @resource_type + resources @resource_type, **options do + jsonapi_resource_scope(Resource.new(@resource_type, api_only?, @scope[:shallow], **options), @resource_type) do if block_given? yield else jsonapi_relationships end - else - # Rails 5 - jsonapi_resource_scope(Resource.new(@resource_type, api_only?, @scope[:shallow], options), @resource_type) do - if block_given? - yield - else - jsonapi_relationships - end - end end - # :nocov: end end diff --git a/lib/sanger-jsonapi-resources.rb b/lib/sanger-jsonapi-resources.rb new file mode 100644 index 000000000..2b4763cf6 --- /dev/null +++ b/lib/sanger-jsonapi-resources.rb @@ -0,0 +1,7 @@ +# As we are packaging 'sanger-jsonapi-resources' as a separate gem, RubyGems expects +# the main file to be 'lib/sanger-jsonapi-resources.rb' to match the gem name. +# Without this file, requiring the gem or Rails autoloading would fail, even if the internal code is unchanged. +# This file exists to ensure compatibility with RubyGems and Bundler. +# The easiest solution is to use this wrapper file, which simply requires the original 'jsonapi-resources' code, +# so all internal references and modules remain unchanged and compatible. +require_relative 'jsonapi-resources' diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index e2568f979..a2f220664 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -761,7 +761,7 @@ def test_create_link_to_missing_object } } - assert_response :unprocessable_entity + assert_response :unprocessable_content # TODO: check if this validation is working assert_match /author - can't be blank/, response.body assert_nil response.location @@ -864,7 +864,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal "/data/relationships/author", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -2019,7 +2019,7 @@ def test_delete_with_validation_error_base assert_equal "can't destroy me", json_response['errors'][0]['title'] assert_equal "/data", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end def test_delete_with_validation_error_attr @@ -2028,7 +2028,7 @@ def test_delete_with_validation_error_attr assert_equal "is locked", json_response['errors'][0]['title'] assert_equal "/data/attributes/title", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end def test_delete_single @@ -2631,7 +2631,7 @@ def test_create_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal 2, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][1]['code'] @@ -2653,7 +2653,7 @@ def test_update_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal 1, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_match /name - can't be blank/, response.body @@ -3183,7 +3183,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal "/data/attributes/spouse-name", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -3779,7 +3779,7 @@ def test_save_model_callbacks_fail } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_match /Save failed or was cancelled/, json_response['errors'][0]['detail'] end end @@ -4077,7 +4077,7 @@ def test_delete_with_validation_error_base_on_resource assert_equal "can't destroy me", json_response['errors'][0]['title'] assert_equal "/data/attributes/base", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end end diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 1209302fd..67fd44362 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -52,7 +52,7 @@ end create_table :posts, force: true do |t| - t.string :title, length: 255 + t.string :title, limit: 255 t.text :body t.integer :author_id t.integer :parent_post_id @@ -85,17 +85,20 @@ end create_table :posts_tags, force: true do |t| - t.references :post, :tag, index: true + t.references :post, index:true + t.references :tag, index:true end add_index :posts_tags, [:post_id, :tag_id], unique: true create_table :special_post_tags, force: true do |t| - t.references :post, :tag, index: true + t.references :post, index: true + t.references :tag, index: true end add_index :special_post_tags, [:post_id, :tag_id], unique: true create_table :comments_tags, force: true do |t| - t.references :comment, :tag, index: true + t.references :comment, index: true + t.references :tag, index: true end create_table :iso_currencies, id: false, force: true do |t| @@ -324,8 +327,8 @@ create_table :related_things, force: true do |t| t.string :name - t.references :from, references: :thing - t.references :to, references: :thing + t.references :from, foreign_key: { to_table: :things } + t.references :to, foreign_key: { to_table: :things } t.timestamps null: false end @@ -1676,7 +1679,7 @@ class PlanetResource < JSONAPI::Resource attribute :description has_many :moons - belongs_to :planet_type + has_one :planet_type has_many :tags, acts_as_set: true end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 1863b5c7d..b48914afe 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -453,6 +453,9 @@ def test_post_single_minimal_invalid } assert_jsonapi_response 422 + assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] + assert_equal '422', json_response['errors'][0]['status'] + assert_match "can't be blank", json_response['errors'][0]['title'] end def test_update_relationship_without_content_type @@ -578,7 +581,9 @@ def test_put_invalid_json assert_equal 400, status assert_equal 'Bad Request', json_response['errors'][0]['title'] - assert_match 'unexpected token at', json_response['errors'][0]['detail'] + rails_old_msg = 'unexpected token at' + rails_8_1_msg = "expected ',' or '}' after object value, got: '\"attributes\":'" + assert_match (/(#{rails_old_msg}|#{rails_8_1_msg})/), json_response['errors'][0]['detail'] end def test_put_valid_json_but_array @@ -1367,7 +1372,7 @@ def test_deprecated_include_parameter_not_allowed end def test_deprecated_include_message - ActiveSupport::Deprecation.silenced = false + set_deprecation_behavior(:report) original_config = JSONAPI.configuration.dup _out, err = capture_io do eval <<-CODE @@ -1377,7 +1382,7 @@ def test_deprecated_include_message assert_match /DEPRECATION WARNING: `allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options./, err ensure JSONAPI.configuration = original_config - ActiveSupport::Deprecation.silenced = true + set_deprecation_behavior(:silence) end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9850a49c6..8bd1bc236 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,8 +23,9 @@ ENV['DATABASE_URL'] ||= "sqlite3:test_db" require 'active_record/railtie' -require 'rails/test_help' require 'minitest/mock' +require 'minitest/autorun' +require 'minitest/reporters' require 'jsonapi-resources' require 'pry' @@ -33,6 +34,13 @@ require File.expand_path('../helpers/functional_helpers', __FILE__) require File.expand_path('../helpers/configuration_helpers', __FILE__) +if ENV['CI'] == 'true' + # The SpecReporter is easier to read on GitHub + Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new +else + Minitest::Reporters.use! +end + Rails.env = 'test' I18n.load_path += Dir[File.expand_path("../../locales/*.yml", __FILE__)] @@ -42,9 +50,19 @@ config.json_key_format = :camelized_key end -ActiveSupport::Deprecation.silenced = true +def set_deprecation_behavior(mode) + if ActiveSupport::Deprecation.respond_to?(:behavior=) + ActiveSupport::Deprecation.behavior = mode + elsif ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = (mode == :silence) + end +end + +set_deprecation_behavior(:silence) +puts "-" * 32 puts "Testing With RAILS VERSION #{Rails.version}" +puts "-" * 32 class TestApp < Rails::Application config.eager_load = false @@ -62,9 +80,8 @@ class TestApp < Rails::Application config.active_support.halt_callback_chains_on_return_false = false config.active_record.time_zone_aware_types = [:time, :datetime] config.active_record.belongs_to_required_by_default = false - if Rails::VERSION::MAJOR == 5 && Rails::VERSION::MINOR == 2 - config.active_record.sqlite3.represent_boolean_as_integer = true - end + config.active_support.cache_format_version = 7.1 + config.active_support.to_time_preserves_timezone = :zone end DatabaseCleaner.allow_remote_database_url = true @@ -84,9 +101,9 @@ class Engine < ::Rails::Engine # Monkeypatch ActionController::TestCase to delete the RAW_POST_DATA on subsequent calls in the same test. module ClearRawPostHeader - def process(action, **args) + def process(action, *args, **kwargs) @request.delete_header 'RAW_POST_DATA' - super action, **args + super(action, *args, **kwargs) end end @@ -457,12 +474,12 @@ def run_in_transaction? true end - self.fixture_path = "#{Rails.root}/fixtures" + self.fixture_paths = ["#{Rails.root}/fixtures"] fixtures :all end class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" + self.fixture_paths = ["#{Rails.root}/fixtures"] fixtures :all setup do @routes = TestApp.routes @@ -470,7 +487,7 @@ class ActiveSupport::TestCase end class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" + self.fixture_paths = ["#{Rails.root}/fixtures"] fixtures :all def assert_jsonapi_response(expected_status, msg = nil) @@ -521,13 +538,13 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all) end class ActionController::TestCase - def assert_cacheable_get(action, **args) + def assert_cacheable_get(action, **request_options) assert_nil JSONAPI.configuration.resource_cache normal_queries = [] normal_query_callback = lambda {|_, _, _, _, payload| normal_queries.push payload[:sql] } ActiveSupport::Notifications.subscribed(normal_query_callback, 'sql.active_record') do - get action, **args + get action, **request_options end non_caching_response = json_response_sans_all_backtraces non_caching_status = response.status @@ -563,7 +580,7 @@ def assert_cacheable_get(action, **args) @controller = nil setup_controller_request_and_response @request.headers.merge!(orig_request_headers.dup) - get action, **args + get action, **request_options end end rescue Exception diff --git a/test/unit/active_relation_resource_finder/join_manager_test.rb b/test/unit/active_relation_resource_finder/join_manager_test.rb index 840c90ee2..e76f1e3c7 100644 --- a/test/unit/active_relation_resource_finder/join_manager_test.rb +++ b/test/unit/active_relation_resource_finder/join_manager_test.rb @@ -4,12 +4,16 @@ class JoinTreeTest < ActiveSupport::TestCase def db_true + rails_major = 8 + rails_minor = 1 + case ActiveRecord::Base.connection.adapter_name when 'SQLite' - if Rails::VERSION::MAJOR >= 6 || (Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2) - "1" + if Rails::VERSION::MAJOR >= rails_major + 1 || + (Rails::VERSION::MAJOR >= rails_major && ActiveRecord::VERSION::MINOR >= rails_minor) + "TRUE" else - "'t'" + "1" end when 'PostgreSQL' 'TRUE' diff --git a/test/unit/jsonapi_request/jsonapi_error_test.rb b/test/unit/jsonapi_request/jsonapi_error_test.rb new file mode 100644 index 000000000..801b8204d --- /dev/null +++ b/test/unit/jsonapi_request/jsonapi_error_test.rb @@ -0,0 +1,45 @@ +require File.expand_path('../../../test_helper', __FILE__) + +class JSONAPIErrorTest < Minitest::Test + def test_status_code_requires_status + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST) + end + + assert_equal('Status code is required', error.message) + end + + def test_status_code_accepts_symbol + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: :unprocessable_content) + + assert_equal('422', error.status) + end + + def test_status_code_accepts_integer + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: 422) + + assert_equal('422', error.status) + end + + def test_status_code_accepts_string + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: '422') + + assert_equal('422', error.status) + end + + def test_status_code_rejects_unknown_symbol + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST, status: :not_a_real_status) + end + + assert_equal('Unrecognized status code :not_a_real_status', error.message) + end + + def test_status_code_rejects_nil + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST, status: nil) + end + + assert_equal('Status code is required', error.message) + end +end diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index df2df1730..464b9049d 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -434,7 +434,7 @@ def test_key_type_proc def test_id_attr_deprecation - ActiveSupport::Deprecation.silenced = false + set_deprecation_behavior(:report) _out, err = capture_io do eval <<-CODE class ProblemResource < JSONAPI::Resource @@ -442,9 +442,11 @@ class ProblemResource < JSONAPI::Resource end CODE end - assert_match /DEPRECATION WARNING: Id without format is no longer supported. Please remove ids from attributes, or specify a format./, err + err_msg = /DEPRECATION WARNING: Id without format is no longer supported. Please remove ids from attributes, or specify a format./ + err_msg_rails_8 = /\[DUPLICATE ATTRIBUTE\] `id` has already been defined in ProblemResource\./ + assert(err.match?(err_msg) || err.match?(err_msg_rails_8), "Expected either deprecation or duplicate-attribute warning, got: #{err}") ensure - ActiveSupport::Deprecation.silenced = true + set_deprecation_behavior(:silence) end def test_id_attr_with_format