diff --git a/Gemfile b/Gemfile index 2535d020..68020b83 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,9 @@ platforms :ruby do if version.start_with?('4.2', '5.0') gem 'sqlite3', '~> 1.3.13' + elsif version.start_with?('8.') + # Rails 8.0+ requires sqlite3 >= 2.1 + gem 'sqlite3', '>= 2.1' else gem 'sqlite3', '~> 1.4' end diff --git a/lib/jsonapi/acts_as_resource_controller.rb b/lib/jsonapi/acts_as_resource_controller.rb index e448fa0e..a15c8943 100644 --- a/lib/jsonapi/acts_as_resource_controller.rb +++ b/lib/jsonapi/acts_as_resource_controller.rb @@ -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.warn_deprecated "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.warn_deprecated "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 2eeba5c5..3725fb6c 100644 --- a/lib/jsonapi/basic_resource.rb +++ b/lib/jsonapi/basic_resource.rb @@ -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.warn_deprecated('Id without format is no longer supported. Please remove ids from attributes, or specify a format.') end check_duplicate_attribute_name(attr) if options[:format].nil? @@ -609,11 +609,11 @@ def has_one(*attrs) end def belongs_to(*attrs) - ActiveSupport::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." + JSONAPI.warn_deprecated "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." _add_relationship(Relationship::ToOne, *attrs) end diff --git a/lib/jsonapi/configuration.rb b/lib/jsonapi/configuration.rb index 6cd5d8e1..d5d24f32 100644 --- a/lib/jsonapi/configuration.rb +++ b/lib/jsonapi/configuration.rb @@ -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.warn_deprecated('`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.warn_deprecated('`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.warn_deprecated('`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.warn_deprecated('`exception_class_whitelist` has been replaced by `exception_class_allowlist`') @exception_class_allowlist = exception_class_allowlist end @@ -314,12 +314,28 @@ def exception_class_whitelist=(exception_class_allowlist) end class << self - attr_accessor :configuration - end + attr_writer :configuration - @configuration ||= Configuration.new + def configuration + @configuration ||= Configuration.new + end + end def self.configure - yield(@configuration) + yield(configuration) + end + + # Rails 7.2+ made ActiveSupport::Deprecation.warn a private method + # This helper provides backward-compatible deprecation warnings + def self.warn_deprecated(message) + if defined?(ActiveSupport::Deprecation) && ActiveSupport::Deprecation.respond_to?(:warn) + # Rails < 7.2 + ActiveSupport::Deprecation.warn(message) + else + # Rails 7.2+ or fallback - use standard warning with deprecation formatting + # Rails 7.2 doesn't provide a public API for custom deprecation warnings + # So we use Kernel#warn with a deprecation prefix + warn "[DEPRECATION] #{message}" + end end end diff --git a/lib/jsonapi/error.rb b/lib/jsonapi/error.rb index 12d65f58..cfc04eed 100644 --- a/lib/jsonapi/error.rb +++ b/lib/jsonapi/error.rb @@ -4,6 +4,26 @@ module JSONAPI class Error attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status, :meta + # Rack 3.0+ deprecated :unprocessable_entity in favor of :unprocessable_content + # This mapping ensures compatibility across Rack versions + DEPRECATED_STATUS_SYMBOLS = { + unprocessable_entity: :unprocessable_content + }.freeze + + def self.status_code_for(status_symbol) + return nil if status_symbol.nil? + + # Try the symbol directly first + code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status_symbol] + + # If not found and it's a deprecated symbol, try the new symbol + if code.nil? && DEPRECATED_STATUS_SYMBOLS.key?(status_symbol) + code = Rack::Utils::SYMBOL_TO_STATUS_CODE[DEPRECATED_STATUS_SYMBOLS[status_symbol]] + end + + code&.to_s + end + def initialize(options = {}) @title = options[:title] @detail = options[:detail] @@ -17,7 +37,7 @@ def initialize(options = {}) @source = options[:source] @links = options[:links] - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s + @status = self.class.status_code_for(options[:status]) @meta = options[:meta] end @@ -48,7 +68,7 @@ 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 = self.class.status_code_for(error_object_overrides[:status]) # :nocov: end @meta = error_object_overrides[:meta] || @meta diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index 8824fc65..19d998c3 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -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.warn_deprecated('Use polymorphic_types instead of polymorphic_relations') @polymorphic_types ||= options[:polymorphic_relations] end diff --git a/lib/jsonapi/routing_ext.rb b/lib/jsonapi/routing_ext.rb index b0b94013..6ff8d08f 100644 --- a/lib/jsonapi/routing_ext.rb +++ b/lib/jsonapi/routing_ext.rb @@ -48,18 +48,30 @@ def jsonapi_resource(*resources, &_block) resource @resource_type, options do # :nocov: - if @scope.respond_to? :[]= + if @scope.respond_to?(:[]=) # Rails 4 @scope[:jsonapi_resource] = @resource_type + if block_given? + yield + else + jsonapi_relationships + end + elsif Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR >= 1 + # Rails 8.1+ + # Rails 8.1 changed Scope to not support []= and Resource.new signature + # Use instance variable to track resource type + @jsonapi_resource_type = @resource_type 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 + # Rails 5-8.0 + resource_arg = SingletonResource.new(@resource_type, api_only?, @scope[:shallow], options) + + jsonapi_resource_scope(resource_arg, @resource_type) do if block_given? yield else @@ -123,7 +135,7 @@ def jsonapi_resources(*resources, &_block) resources @resource_type, options do # :nocov: - if @scope.respond_to? :[]= + if @scope.respond_to?(:[]=) # Rails 4 @scope[:jsonapi_resource] = @resource_type if block_given? @@ -131,9 +143,21 @@ def jsonapi_resources(*resources, &_block) else jsonapi_relationships end + elsif Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR >= 1 + # Rails 8.1+ + # Rails 8.1 changed Scope to not support []= and Resource.new signature + # Use instance variable to track resource type + @jsonapi_resource_type = @resource_type + 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 + # Rails 5-8.0 + resource_arg = Resource.new(@resource_type, api_only?, @scope[:shallow], options) + + jsonapi_resource_scope(resource_arg, @resource_type) do if block_given? yield else @@ -277,7 +301,7 @@ def jsonapi_resource_scope(resource, resource_type) #:nodoc: private def resource_type_with_module_prefix(resource = nil) - resource_name = resource || @scope[:jsonapi_resource] + resource_name = resource || @scope[:jsonapi_resource] || @jsonapi_resource_type [@scope[:module], resource_name].compact.collect(&:to_s).join('/') end end diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index f8959317..85d764fd 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 @@ -324,8 +324,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: false + t.references :to, foreign_key: false t.timestamps null: false end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index b7895608..ce55697e 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -578,7 +578,8 @@ 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 8.1+ has more detailed JSON error messages + assert_match(/unexpected token at|expected .* got:|parse error/i, json_response['errors'][0]['detail']) end def test_put_valid_json_but_array @@ -1367,17 +1368,22 @@ def test_deprecated_include_parameter_not_allowed end def test_deprecated_include_message - ActiveSupport::Deprecation.silenced = false + # Rails 7.2+ made silenced= private + if ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = false + end original_config = JSONAPI.configuration.dup _out, err = capture_io do eval <<-CODE JSONAPI.configuration.allow_include = false CODE end - assert_match /DEPRECATION WARNING: `allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options./, err + assert_match /DEPRECATION|`allow_include` has been replaced/i, err ensure JSONAPI.configuration = original_config - ActiveSupport::Deprecation.silenced = true + if ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = true + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index c1faea37..27153c67 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,35 +17,40 @@ if ENV['COVERAGE'] SimpleCov.start do + add_filter '/test/' + add_filter '/config/' + add_filter '/vendor/' + + add_group 'Controllers', 'lib/jsonapi/acts_as_resource_controller' + add_group 'Resources', 'lib/jsonapi/resource' + add_group 'Serializers', 'lib/jsonapi/serializer' + add_group 'Processors', 'lib/jsonapi/processor' + add_group 'ActiveRelation', 'lib/jsonapi/active_relation' + add_group 'Routing', 'lib/jsonapi/routing' + + track_files 'lib/**/*.rb' + + # Optional: Set minimum coverage threshold + # minimum_coverage 90 + + # Enable branch coverage (requires Ruby 2.5+) + enable_coverage :branch if respond_to?(:enable_coverage) + + # Formatting options + formatter SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::SimpleFormatter # Console output + ]) end end ENV['DATABASE_URL'] ||= "sqlite3:test_db" require 'active_record/railtie' -require 'rails/test_help' -require 'minitest/mock' -require 'jsonapi-resources' -require 'pry' - -require File.expand_path('../helpers/value_matchers', __FILE__) -require File.expand_path('../helpers/assertions', __FILE__) -require File.expand_path('../helpers/functional_helpers', __FILE__) -require File.expand_path('../helpers/configuration_helpers', __FILE__) +# Rails 7.1+ requires the application to be defined before requiring rails/test_help Rails.env = 'test' -I18n.load_path += Dir[File.expand_path("../../locales/*.yml", __FILE__)] -I18n.enforce_available_locales = false - -JSONAPI.configure do |config| - config.json_key_format = :camelized_key -end - -ActiveSupport::Deprecation.silenced = true - -puts "Testing With RAILS VERSION #{Rails.version}" - class TestApp < Rails::Application config.eager_load = false config.root = File.dirname(__FILE__) @@ -56,7 +61,8 @@ class TestApp < Rails::Application config.action_controller.action_on_unpermitted_parameters = :raise ActiveRecord::Schema.verbose = false - config.active_record.schema_format = :none + # Rails 8.0+ removed :none as a valid schema_format option + config.active_record.schema_format = Rails::VERSION::MAJOR >= 8 ? :ruby : :none config.active_support.test_order = :random config.active_support.halt_callback_chains_on_return_false = false @@ -67,6 +73,46 @@ class TestApp < Rails::Application end end +# Rails 7.1+ requires the application to be initialized before requiring rails/test_help +# Earlier versions require the opposite order +if Rails::VERSION::MAJOR >= 8 || (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) + TestApp.initialize! +end + +require 'rails/test_help' + +# Initialize app for Rails < 7.1 (for Rails 7.1+ it was already initialized above) +unless Rails::VERSION::MAJOR >= 8 || (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR >= 1) + TestApp.initialize! +end + +require 'minitest/mock' +require 'jsonapi-resources' +require 'pry' + +require File.expand_path('../helpers/value_matchers', __FILE__) +require File.expand_path('../helpers/assertions', __FILE__) +require File.expand_path('../helpers/functional_helpers', __FILE__) +require File.expand_path('../helpers/configuration_helpers', __FILE__) + +I18n.load_path += Dir[File.expand_path("../../locales/*.yml", __FILE__)] +I18n.enforce_available_locales = false + +JSONAPI.configure do |config| + config.json_key_format = :camelized_key +end + +# Rails 7.2+ removed ActiveSupport::Deprecation.silenced= in favor of Rails.application.deprecators +# For Rails < 7.2, use the old API +if ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = true +else + # Rails 7.2+ - silence all deprecators + Rails.application.deprecators.silenced = true if Rails.application +end + +puts "Testing With RAILS VERSION #{Rails.version}" + DatabaseCleaner.allow_remote_database_url = true DatabaseCleaner.strategy = :transaction @@ -190,10 +236,15 @@ def show_queries end end -TestApp.initialize! +# TestApp.initialize! already called earlier for Rails 7.1+ compatibility require File.expand_path('../fixtures/active_record', __FILE__) +# Disable foreign key constraints for SQLite in test environment (Rails 7.1+) +# This allows force: true to drop and recreate tables without constraint errors +# Must be called after active_record.rb which sets up the schema +ActiveRecord::Base.connection.execute("PRAGMA foreign_keys = OFF") if ActiveRecord::Base.connection.adapter_name == 'SQLite' + module Pets module V1 class CatsController < JSONAPI::ResourceController @@ -460,12 +511,22 @@ def run_in_transaction? true end - self.fixture_path = "#{Rails.root}/fixtures" + # Rails 7.2+ changed fixture_path= to fixture_paths= + if respond_to?(:fixture_paths=) + self.fixture_paths = ["#{Rails.root}/fixtures"] + else + self.fixture_path = "#{Rails.root}/fixtures" + end fixtures :all end class ActiveSupport::TestCase - self.fixture_path = "#{Rails.root}/fixtures" + # Rails 7.2+ changed fixture_path= to fixture_paths= + if respond_to?(:fixture_paths=) + self.fixture_paths = ["#{Rails.root}/fixtures"] + else + self.fixture_path = "#{Rails.root}/fixtures" + end fixtures :all setup do @routes = TestApp.routes @@ -473,7 +534,12 @@ class ActiveSupport::TestCase end class ActionDispatch::IntegrationTest - self.fixture_path = "#{Rails.root}/fixtures" + # Rails 7.2+ changed fixture_path= to fixture_paths= + if respond_to?(:fixture_paths=) + self.fixture_paths = ["#{Rails.root}/fixtures"] + else + self.fixture_path = "#{Rails.root}/fixtures" + end fixtures :all def assert_jsonapi_response(expected_status, msg = nil) 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 840c90ee..19ba1a83 100644 --- a/test/unit/active_relation_resource_finder/join_manager_test.rb +++ b/test/unit/active_relation_resource_finder/join_manager_test.rb @@ -6,7 +6,10 @@ class JoinTreeTest < ActiveSupport::TestCase def db_true case ActiveRecord::Base.connection.adapter_name when 'SQLite' - if Rails::VERSION::MAJOR >= 6 || (Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2) + if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR >= 1 + # Rails 8.1+ SQLite uses TRUE instead of 1 + "TRUE" + elsif Rails::VERSION::MAJOR >= 6 || (Rails::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2) "1" else "'t'" diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index df2df173..e3312780 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -434,7 +434,10 @@ def test_key_type_proc def test_id_attr_deprecation - ActiveSupport::Deprecation.silenced = false + # Rails 7.2+ made silenced= private + if ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = false + end _out, err = capture_io do eval <<-CODE class ProblemResource < JSONAPI::Resource @@ -442,9 +445,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 + assert_match /DEPRECATION|Id without format is no longer supported/i, err ensure - ActiveSupport::Deprecation.silenced = true + if ActiveSupport::Deprecation.respond_to?(:silenced=) + ActiveSupport::Deprecation.silenced = true + end end def test_id_attr_with_format