Skip to content

Commit 0d99e6e

Browse files
committed
feat: report a json gem that Rails cannot use
The json gem 3.x removed an option that Active Support older than 8.1.4 passes to the decoder. Each JSON column then raises "unknown keyword: quirks_mode", and the cause is not clear from the error. The doctor now has a json check, and the boot prints a warning. Both decode one document with Active Support. They do not compare version numbers, so they stay correct when Rails or json changes. The gemspec does not pin json, because a pin would stop applications on a Rails release that supports json 3.
1 parent c944eb1 commit 0d99e6e

9 files changed

Lines changed: 157 additions & 5 deletions

File tree

‎docs/operations.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ an older Rails release, pin the 2.x series of `json`:
4343
gem "json", "~> 2"
4444
```
4545

46+
`bin/rails solid_objects:doctor` reports this problem as a failure of its
47+
`json` check, and the boot prints a warning that starts with
48+
`[solid_objects]`. Both decode one JSON document with Active Support. They do
49+
not compare version numbers.
50+
4651
Review [CHANGELOG.md](../CHANGELOG.md) for compatibility and deployment-order
4752
notes, then update the gem:
4853

‎lib/solid_objects.rb‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require "solid_objects/diagnostics"
2121
require "solid_objects/log_subscriber"
2222
require "solid_objects/serialization"
23+
require "solid_objects/json_compatibility"
2324
require "solid_objects/context"
2425
require "solid_objects/actor_registry"
2526
require "solid_objects/state"

‎lib/solid_objects/doctor.rb‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,25 @@ def ping(value:)
9999

100100
# @rbs @connection: untyped
101101
# @rbs @configuration: Configuration
102+
# @rbs @json_compatibility: JsonCompatibility
102103

103-
# @rbs (?connection: untyped, ?configuration: Configuration) -> void
104+
# @rbs (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
104105
def initialize(
105106
connection: SolidObjects::Record.connection,
106-
configuration: SolidObjects.configuration
107+
configuration: SolidObjects.configuration,
108+
json_compatibility: JsonCompatibility.new
107109
)
108110
@connection = connection
109111
@configuration = configuration
112+
@json_compatibility = json_compatibility
110113
end
111114

112115
# @rbs () -> Report
113116
def call
114117
configuration_check = check_configuration
115118
schema_check = check_schema
116119
checks = [
120+
check_json,
117121
configuration_check,
118122
schema_check,
119123
check_authorization,
@@ -129,7 +133,15 @@ def call
129133

130134
private
131135

132-
attr_reader :connection, :configuration
136+
attr_reader :connection, :configuration, :json_compatibility
137+
138+
# @rbs () -> Check
139+
def check_json
140+
problem = json_compatibility.problem
141+
return fail_check(:json, problem) if problem
142+
143+
pass(:json, "Active Support decodes JSON with the json gem #{JSON::VERSION}")
144+
end
133145

134146
# @rbs () -> Check
135147
def check_configuration

‎lib/solid_objects/engine.rb‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class Engine < ::Rails::Engine
1515
SolidObjects::LogSubscriber.install
1616
end
1717

18+
initializer "solid_objects.json_compatibility" do
19+
JsonCompatibility.new.warn_about_problem
20+
end
21+
1822
initializer "solid_objects.actors" do |application|
1923
application.config.to_prepare { ApplicationActorLoader.new.call }
2024
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# rbs_inline: enabled
2+
3+
module SolidObjects
4+
class JsonCompatibility
5+
PROBE = '{"probe":1}'
6+
7+
# @rbs @decode: ^(String) -> untyped
8+
9+
# @rbs (?decode: ^(String) -> untyped) -> void
10+
def initialize(decode: ActiveSupport::JSON.method(:decode))
11+
@decode = decode
12+
end
13+
14+
# @rbs () -> String?
15+
def problem
16+
decode.call(PROBE)
17+
nil
18+
rescue ArgumentError => error
19+
"Active Support #{ActiveSupport.version} cannot decode JSON with the json gem #{JSON::VERSION} " \
20+
"(#{error.message}). Add gem \"json\", \"~> 2\" to the Gemfile, " \
21+
"or use a Rails release that supports json 3"
22+
end
23+
24+
# @rbs () -> void
25+
def warn_about_problem
26+
message = problem
27+
return unless message
28+
29+
warn "[solid_objects] #{message}"
30+
end
31+
32+
private
33+
34+
attr_reader :decode
35+
end
36+
end

‎sig/generated/lib/solid_objects/doctor.rbs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ module SolidObjects
4747
def ping: (value: String) -> String
4848
end
4949

50+
@json_compatibility: JsonCompatibility
51+
5052
@configuration: Configuration
5153

5254
@connection: untyped
5355

54-
# @rbs (?connection: untyped, ?configuration: Configuration) -> void
55-
def initialize: (?connection: untyped, ?configuration: Configuration) -> void
56+
# @rbs (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
57+
def initialize: (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
5658

5759
# @rbs () -> Report
5860
def call: () -> Report
@@ -63,6 +65,11 @@ module SolidObjects
6365

6466
attr_reader configuration: untyped
6567

68+
attr_reader json_compatibility: untyped
69+
70+
# @rbs () -> Check
71+
def check_json: () -> Check
72+
6673
# @rbs () -> Check
6774
def check_configuration: () -> Check
6875

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated from lib/solid_objects/json_compatibility.rb with RBS::Inline
2+
3+
module SolidObjects
4+
class JsonCompatibility
5+
PROBE: ::String
6+
7+
@decode: ^(String) -> untyped
8+
9+
# @rbs (?decode: ^(String) -> untyped) -> void
10+
def initialize: (?decode: ^(String) -> untyped) -> void
11+
12+
# @rbs () -> String?
13+
def problem: () -> String?
14+
15+
# @rbs () -> void
16+
def warn_about_problem: () -> void
17+
18+
private
19+
20+
attr_reader decode: untyped
21+
end
22+
end

‎test/integration/doctor_test.rb‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ class DoctorTest < ActiveSupport::TestCase
2727
assert_empty SolidObjects::Process.where(kind: "caller")
2828
end
2929

30+
test "passes the json check when Active Support decodes JSON" do
31+
report = SolidObjects::Doctor.new.call
32+
33+
assert_equal :pass, report.check(:json).status
34+
assert_includes report.check(:json).message, JSON::VERSION
35+
end
36+
37+
test "fails the json check and names the correction when Active Support cannot decode JSON" do
38+
refused = SolidObjects::JsonCompatibility.new(
39+
decode: ->(_text) { raise ArgumentError, "unknown keyword: :quirks_mode" }
40+
)
41+
42+
report = SolidObjects::Doctor.new(json_compatibility: refused).call
43+
44+
assert_not report.healthy?
45+
assert_equal :fail, report.check(:json).status
46+
assert_includes report.check(:json).message, 'gem "json", "~> 2"'
47+
end
48+
3049
test "runs its probe on a caller process the application cannot adopt" do
3150
SolidObjects.caller_process.define_singleton_method(:process_registry) do
3251
raise "the doctor probe must not share the application caller process"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class JsonCompatibilityTest < ActiveSupport::TestCase
6+
REFUSED_KEYWORD = ->(_text) { raise ArgumentError, "unknown keyword: :quirks_mode" }
7+
8+
test "reports no problem when Active Support decodes JSON" do
9+
assert_nil SolidObjects::JsonCompatibility.new.problem
10+
end
11+
12+
test "names the versions and the correction when Active Support cannot decode JSON" do
13+
problem = SolidObjects::JsonCompatibility.new(decode: REFUSED_KEYWORD).problem
14+
15+
assert_includes problem, "unknown keyword: :quirks_mode"
16+
assert_includes problem, "json gem #{JSON::VERSION}"
17+
assert_includes problem, "Active Support #{ActiveSupport.version}"
18+
assert_includes problem, 'gem "json", "~> 2"'
19+
end
20+
21+
test "does not hide a different decode failure" do
22+
broken = ->(_text) { raise NoMethodError, "decode is gone" }
23+
24+
assert_raises(NoMethodError) { SolidObjects::JsonCompatibility.new(decode: broken).problem }
25+
end
26+
27+
test "warns on the error stream when there is a problem" do
28+
_output, errors = capture_io { SolidObjects::JsonCompatibility.new(decode: REFUSED_KEYWORD).warn_about_problem }
29+
30+
assert_includes errors, "[solid_objects]"
31+
assert_includes errors, 'gem "json", "~> 2"'
32+
end
33+
34+
test "writes nothing when there is no problem" do
35+
output, errors = capture_io { SolidObjects::JsonCompatibility.new.warn_about_problem }
36+
37+
assert_empty output
38+
assert_empty errors
39+
end
40+
41+
test "the engine checks JSON compatibility during the boot" do
42+
names = SolidObjects::Engine.instance.initializers.map(&:name)
43+
44+
assert_includes names, "solid_objects.json_compatibility"
45+
end
46+
end

0 commit comments

Comments
 (0)