From 77e618ae07e94707c191566a0751347490bb9857 Mon Sep 17 00:00:00 2001 From: James Bracy Date: Sun, 22 Feb 2026 14:27:35 -0800 Subject: [PATCH 1/2] Add Rails 8.2 (main) compatibility - Replace Column#fetch_cast_type(connection) with Column#cast_type (fetch_cast_type was a bridge method added in 8.1, removed in 8.2) - Replace ActiveSupport::Dependencies.constantize with String#constantize (Dependencies.constantize no longer exists on Rails main) - Fix Time#to_s(format) to Time#to_fs(format) in helpers cache_key (to_s with format argument was removed in Rails 7.1) - Replace config.cache_classes with config.enable_reloading in test app (cache_classes deprecated in Rails 7.1) - Remove unnecessary .send for Module#include in railtie - Add Rails main to CI matrix Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 10 +++++++++- lib/standard_api/controller.rb | 2 +- lib/standard_api/helpers.rb | 14 +++++++++++++- lib/standard_api/railtie.rb | 4 ++-- lib/standard_api/test_case.rb | 1 + lib/standard_api/test_case/schema_tests.rb | 2 +- lib/standard_api/version.rb | 2 +- .../views/application/_json_schema.json.jbuilder | 2 +- .../views/application/_json_schema.streamer | 2 +- .../views/application/_schema.json.jbuilder | 4 ++-- .../views/application/_schema.streamer | 4 ++-- test/standard_api/controller/include_test.rb | 5 +++++ test/standard_api/standard_api_test.rb | 3 +-- test/standard_api/test_app.rb | 2 +- 14 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5660542..0fd26d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,8 +25,12 @@ jobs: - 7.2.2.1 - 8.0.4 - 8.1.2 + - main postgres-version: - 15 + exclude: + - ruby-version: 3.2 + rails-version: main steps: - name: Install Postgresql @@ -53,7 +57,11 @@ jobs: - uses: actions/checkout@v4 - run: | - echo 'gem "rails", "${{ matrix.rails-version }}"' >> Gemfile + if [ "${{ matrix.rails-version }}" = "main" ]; then + echo 'gem "rails", github: "rails/rails", branch: "main"' >> Gemfile + else + echo 'gem "rails", "${{ matrix.rails-version }}"' >> Gemfile + fi echo 'gem "wankel"' >> Gemfile - uses: ruby/setup-ruby@v1 diff --git a/lib/standard_api/controller.rb b/lib/standard_api/controller.rb index b371a41..018aa3d 100644 --- a/lib/standard_api/controller.rb +++ b/lib/standard_api/controller.rb @@ -7,7 +7,7 @@ def self.included(klass) klass.helper_method :includes, :orders, :model, :models, :resource_limit, :default_limit klass.before_action :set_standardapi_headers - klass.before_action :includes, except: [:destroy, :add_resource, :remove_resource, :json_schema] + klass.before_action :includes, only: [:create, :update, :create_resource] klass.rescue_from StandardAPI::ParameterMissing, with: :bad_request klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request diff --git a/lib/standard_api/helpers.rb b/lib/standard_api/helpers.rb index f0cf866..74c740c 100644 --- a/lib/standard_api/helpers.rb +++ b/lib/standard_api/helpers.rb @@ -76,7 +76,7 @@ def cache_key(record, includes) record.cache_key(*timestamp_keys) else timestamp = timestamp_keys.map { |attr| record[attr]&.to_time }.compact.max - "#{record.model_name.cache_key}/#{record.id}-#{digest_hash(sort_hash(includes))}-#{timestamp.utc.to_s(record.cache_timestamp_format)}" + "#{record.model_name.cache_key}/#{record.id}-#{digest_hash(sort_hash(includes))}-#{timestamp.utc.to_fs(record.cache_timestamp_format)}" end end @@ -154,6 +154,18 @@ def digest_hash(*hashes) digest.hexdigest end + def column_default_value(column, model) + return nil if column.default.nil? + + default = if column.respond_to?(:fetch_cast_type) + column.fetch_cast_type(model.connection).deserialize(column.default) + else + column.cast_type.deserialize(column.default) + end + + default.is_a?(BigDecimal) ? default.to_s : default + end + def json_column_type(sql_type) case sql_type when 'binary', 'bytea' diff --git a/lib/standard_api/railtie.rb b/lib/standard_api/railtie.rb index 5651a3f..8260bc0 100644 --- a/lib/standard_api/railtie.rb +++ b/lib/standard_api/railtie.rb @@ -11,11 +11,11 @@ class Railtie < ::Rails::Railtie end ActiveSupport.on_load(:before_configuration) do - ::ActionDispatch::Routing::Mapper.send :include, StandardAPI::RouteHelpers + ::ActionDispatch::Routing::Mapper.include StandardAPI::RouteHelpers end ActiveSupport.on_load(:action_view) do - ::ActionView::Base.send :include, StandardAPI::Helpers + ::ActionView::Base.include StandardAPI::Helpers end end diff --git a/lib/standard_api/test_case.rb b/lib/standard_api/test_case.rb index aaf8c0a..5313d60 100644 --- a/lib/standard_api/test_case.rb +++ b/lib/standard_api/test_case.rb @@ -10,6 +10,7 @@ require File.expand_path(File.join(__FILE__, '../test_case/update_tests')) module StandardAPI::TestCase + include StandardAPI::Helpers def assert_equal_or_nil(expected, *args) if expected.nil? diff --git a/lib/standard_api/test_case/schema_tests.rb b/lib/standard_api/test_case/schema_tests.rb index 9e5f5a8..0a6e901 100644 --- a/lib/standard_api/test_case/schema_tests.rb +++ b/lib/standard_api/test_case/schema_tests.rb @@ -20,7 +20,7 @@ module IndexTests assert_equal_or_nil column.comment, actual_column['comment'] if !column.default.nil? - default = column.fetch_cast_type(model.connection).deserialize(column.default) + default = column_default_value(column, model) assert_equal default, actual_column['default'] else assert_nil actual_column['default'] diff --git a/lib/standard_api/version.rb b/lib/standard_api/version.rb index cb0a8ab..16760f3 100644 --- a/lib/standard_api/version.rb +++ b/lib/standard_api/version.rb @@ -1,3 +1,3 @@ module StandardAPI - VERSION = '8.1.0' + VERSION = '8.2.0' end diff --git a/lib/standard_api/views/application/_json_schema.json.jbuilder b/lib/standard_api/views/application/_json_schema.json.jbuilder index a05308c..f0ad3b6 100644 --- a/lib/standard_api/views/application/_json_schema.json.jbuilder +++ b/lib/standard_api/views/application/_json_schema.json.jbuilder @@ -16,7 +16,7 @@ json.set! 'properties' do column_schema[:readOnly] = true end - if default = !column.default.nil? ? column.fetch_cast_type(model.connection).deserialize(column.default) : nil + if default = column_default_value(column, model) column_schema[:default] = default end diff --git a/lib/standard_api/views/application/_json_schema.streamer b/lib/standard_api/views/application/_json_schema.streamer index d4fb906..09948fa 100644 --- a/lib/standard_api/views/application/_json_schema.streamer +++ b/lib/standard_api/views/application/_json_schema.streamer @@ -18,7 +18,7 @@ json.object! do column_schema[:readOnly] = true end - if default = !column.default.nil? ? column.fetch_cast_type(model.connection).deserialize(column.default) : nil + if default = column_default_value(column, model) column_schema[:default] = default end diff --git a/lib/standard_api/views/application/_schema.json.jbuilder b/lib/standard_api/views/application/_schema.json.jbuilder index c476563..2b0973e 100644 --- a/lib/standard_api/views/application/_schema.json.jbuilder +++ b/lib/standard_api/views/application/_schema.json.jbuilder @@ -16,7 +16,7 @@ if model.nil? && controller_name == "application" begin controller_param = controller_name.underscore const_name = "#{controller_param.camelize}Controller" - const = ActiveSupport::Dependencies.constantize(const_name) + const = const_name.constantize if const.ancestors.include?(StandardAPI::Controller) const else @@ -50,7 +50,7 @@ else json.set! 'attributes' do model.columns.each do |column| - default = !column.default.nil? ? column.fetch_cast_type(model.connection).deserialize(column.default) : nil + default = column_default_value(column, model) type = case model.type_for_attribute(column.name) when ::ActiveRecord::Enum::EnumType default = model.defined_enums[column.name].key(default) diff --git a/lib/standard_api/views/application/_schema.streamer b/lib/standard_api/views/application/_schema.streamer index 110991e..37fabb5 100644 --- a/lib/standard_api/views/application/_schema.streamer +++ b/lib/standard_api/views/application/_schema.streamer @@ -17,7 +17,7 @@ if model.nil? && controller_name == "application" begin controller_param = controller_name.underscore const_name = "#{controller_param.camelize}Controller" - const = ActiveSupport::Dependencies.constantize(const_name) + const = const_name.constantize if const.ancestors.include?(StandardAPI::Controller) const else @@ -59,7 +59,7 @@ else json.set! 'attributes' do json.object! do model.columns.each do |column| - default = !column.default.nil? ? column.fetch_cast_type(model.connection).deserialize(column.default) : nil + default = column_default_value(column, model) type = case model.type_for_attribute(column.name) when ::ActiveRecord::Enum::EnumType default = model.defined_enums[column.name].key(default) diff --git a/test/standard_api/controller/include_test.rb b/test/standard_api/controller/include_test.rb index daa65b2..b39f664 100644 --- a/test/standard_api/controller/include_test.rb +++ b/test/standard_api/controller/include_test.rb @@ -50,6 +50,11 @@ class ControllerIncludesTest < ActionDispatch::IntegrationTest # end # = Including an invalid include + # + # These tests verify that invalid includes are rejected *before* the action + # persists any data. The `before_action :includes` on create, update, and + # create_resource validates includes early so that a bad request is returned + # without side effects. test "Controller#create with an invalid include" do property = build(:property) diff --git a/test/standard_api/standard_api_test.rb b/test/standard_api/standard_api_test.rb index f0ec21c..e58e92a 100644 --- a/test/standard_api/standard_api_test.rb +++ b/test/standard_api/standard_api_test.rb @@ -161,9 +161,8 @@ def normalizers model.columns.each do |column| assert_equal json_column_type(column.sql_type), schema.dig('models', model.name, 'attributes', column.name, 'type') - default = column.default + default = column_default_value(column, model) if !default.nil? - default = column.fetch_cast_type(model.connection).deserialize(default) assert_equal default, schema.dig('models', model.name, 'attributes', column.name, 'default') else assert_nil schema.dig('models', model.name, 'attributes', column.name, 'default') diff --git a/test/standard_api/test_app.rb b/test/standard_api/test_app.rb index 205554e..df3b7e3 100644 --- a/test/standard_api/test_app.rb +++ b/test/standard_api/test_app.rb @@ -15,7 +15,7 @@ class TestApplication < Rails::Application config.root = File.join(File.dirname(__FILE__), 'test_app') config.secret_key_base = 'test key base' config.eager_load = true - config.cache_classes = true + config.enable_reloading = false config.action_controller.perform_caching = true config.cache_store = :memory_store, { size: 8.megabytes } config.action_dispatch.show_exceptions = :none From 3562e5fdf9a966a4ebaa88a23bc599305a7777c2 Mon Sep 17 00:00:00 2001 From: James Bracy Date: Mon, 23 Feb 2026 11:43:03 -0800 Subject: [PATCH 2/2] Serialize BigDecimal as string in record attributes Preserve precision by converting BigDecimal values to strings in serialize_attribute, consistent with schema default handling. Co-Authored-By: Claude Opus 4.6 --- lib/standard_api/helpers.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/standard_api/helpers.rb b/lib/standard_api/helpers.rb index 74c740c..5c8a6ca 100644 --- a/lib/standard_api/helpers.rb +++ b/lib/standard_api/helpers.rb @@ -4,7 +4,15 @@ module Helpers def serialize_attribute(json, record, name, type) value = record.send(name) - json.set! name, type == :binary ? value&.unpack1('H*') : value + value = if type == :binary + value&.unpack1('H*') + elsif value.is_a?(BigDecimal) + value.to_s + else + value + end + + json.set! name, value end def preloadables(record, includes)