Module: ReactOnRails::DoctorSchema
- Defined in:
- lib/react_on_rails/doctor_schema.rb
Overview
Public contract for the machine-readable doctor report.
Check IDs are stable identifiers for automation. Never rename or reuse one; add a new ID for a new check. Additive fields may be introduced without a schema version bump, but removing or changing a documented field requires a new version.
Constant Summary collapse
- VERSION =
1- STATUS_SEVERITIES =
{ "pass" => "info", "warn" => "warning", "fail" => "error" }.freeze
- STATUSES =
STATUS_SEVERITIES.keys.freeze
- SEVERITIES =
STATUS_SEVERITIES.values.uniq.freeze
- DETAIL_LEVELS =
%w[success warning error info].freeze
- EXIT_CODES =
{ pass: 0, warn: 0, fail: 1 }.freeze
- DOCS_URL =
"https://reactonrails.com/docs/api-reference/doctor"- CHECK_METADATA =
{ "environment_prerequisites" => { files: ["package.json", "package manager lockfile"], expected_end_state: "Node.js and the app's JavaScript package manager are installed and detected." }, "react_on_rails_versions" => { files: ["Gemfile", "package.json", "package manager lockfile"], expected_end_state: "The React on Rails gem and npm package versions are compatible and pinned safely." }, "react_on_rails_packages" => { files: ["Gemfile", "config/shakapacker.yml", "config/webpack", "config/rspack"], expected_end_state: "Shakapacker is installed and its configured bundler files exist." }, "javascript_package_dependencies" => { files: ["package.json", "package manager lockfile"], expected_end_state: "Required React and React on Rails JavaScript dependencies are installed and compatible." }, "key_configuration_files" => { files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"], expected_end_state: "The generated React on Rails configuration files exist and match the app setup." }, "configuration_analysis" => { files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/views/layouts"], expected_end_state: "React on Rails, Shakapacker, and server bundle settings agree." }, "bin_dev_launcher_setup" => { files: ["bin/dev", "Procfile.dev", "Procfile.dev-static-assets"], expected_end_state: "The development launcher starts Rails and the configured asset bundler correctly." }, "rails_integration" => { files: ["config/initializers/react_on_rails.rb", "config/application.rb"], expected_end_state: "Rails loads the React on Rails initializer successfully." }, "bundler_configuration" => { files: ["config/shakapacker.yml", "config/webpack", "config/rspack"], expected_end_state: "The configured webpack or Rspack entry point is present and valid." }, "testing_setup" => { files: ["spec/rails_helper.rb", "spec/spec_helper.rb", "test/test_helper.rb"], expected_end_state: "The test suite builds React assets before tests that need them." }, "development_environment" => { files: ["config/environments/development.rb", "config/shakapacker.yml", "package.json"], expected_end_state: "The development server, HMR, and generated bundle settings are consistent." }, "react_on_rails_pro_setup" => { files: ["Gemfile", "package.json", "config/initializers/react_on_rails.rb"], expected_end_state: "React on Rails Pro packages and configuration are mutually compatible." }, "node_renderer_rollout_capacity" => { files: ["renderer/node-renderer.js", "client/node-renderer.js", "deployment environment"], expected_end_state: "The NodeRenderer declares and prewarms its current server/RSC set, and its VM hard cap can hold " \ "rollout overlap." }, "react_server_components" => { files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"], expected_end_state: "RSC packages, generated artifacts, and renderer configuration are consistent." } }.freeze
Class Method Summary collapse
- .assert(condition, message) ⇒ Object
- .docs_url(check_id) ⇒ Object
- .metadata(check_id) ⇒ Object
- .overall_status(statuses) ⇒ Object
- .required_keys!(value, keys, label) ⇒ Object
- .summarize_statuses(statuses) ⇒ Object
-
.validate!(report) ⇒ Object
Runtime validation keeps the emitted payload aligned with the documented contract without adding a runtime JSON-schema dependency to the gem.
- .validate_check!(check) ⇒ Object
- .validate_check_field_types!(check) ⇒ Object
- .validate_detail!(detail, check_id) ⇒ Object
- .validate_details!(check) ⇒ Object
- .validate_remediation!(check) ⇒ Object
- .validate_summary!(report) ⇒ Object
- .validate_unique_check_ids!(checks) ⇒ Object
Class Method Details
.assert(condition, message) ⇒ Object
188 189 190 |
# File 'lib/react_on_rails/doctor_schema.rb', line 188 def assert(condition, ) raise ArgumentError, "Invalid doctor JSON report: #{}" unless condition end |
.docs_url(check_id) ⇒ Object
92 93 94 |
# File 'lib/react_on_rails/doctor_schema.rb', line 92 def docs_url(check_id) "#{DOCS_URL}#check-id-#{check_id.tr('_', '-')}" end |
.metadata(check_id) ⇒ Object
86 87 88 89 90 |
# File 'lib/react_on_rails/doctor_schema.rb', line 86 def (check_id) CHECK_METADATA.fetch(check_id) do raise ArgumentError, "Missing DoctorSchema::CHECK_METADATA entry for check id #{check_id.inspect}" end end |
.overall_status(statuses) ⇒ Object
175 176 177 178 179 180 |
# File 'lib/react_on_rails/doctor_schema.rb', line 175 def overall_status(statuses) return "fail" if statuses.include?("fail") return "warn" if statuses.include?("warn") "pass" end |
.required_keys!(value, keys, label) ⇒ Object
182 183 184 185 186 |
# File 'lib/react_on_rails/doctor_schema.rb', line 182 def required_keys!(value, keys, label) assert(value.is_a?(Hash), "#{label} must be an object") missing = keys - value.keys assert(missing.empty?, "#{label} missing keys: #{missing.join(', ')}") end |
.summarize_statuses(statuses) ⇒ Object
96 97 98 99 |
# File 'lib/react_on_rails/doctor_schema.rb', line 96 def summarize_statuses(statuses) counts = statuses.tally STATUSES.to_h { |status| [status.to_sym, counts.fetch(status, 0)] } end |
.validate!(report) ⇒ Object
Runtime validation keeps the emitted payload aligned with the documented contract without adding a runtime JSON-schema dependency to the gem.
103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/react_on_rails/doctor_schema.rb', line 103 def validate!(report) required_keys!(report, %i[schema_version ror_version status checks summary], "report") assert(report[:schema_version] == VERSION, "unsupported schema_version") assert(report[:ror_version].is_a?(String), "ror_version must be a string") assert(STATUSES.include?(report[:status]), "invalid report status") assert(report[:checks].is_a?(Array), "checks must be an array") report[:checks].each { |check| validate_check!(check) } validate_unique_check_ids!(report[:checks]) validate_summary!(report) report end |
.validate_check!(check) ⇒ Object
115 116 117 118 119 120 121 122 123 124 |
# File 'lib/react_on_rails/doctor_schema.rb', line 115 def validate_check!(check) required_keys!(check, %i[id title status severity message fix_command docs_url remediation details], "check") assert(CHECK_METADATA.key?(check[:id]), "unknown check id #{check[:id].inspect}") assert(STATUSES.include?(check[:status]), "invalid status for #{check[:id]}") assert(SEVERITIES.include?(check[:severity]), "invalid severity for #{check[:id]}") assert(check[:severity] == STATUS_SEVERITIES[check[:status]], "severity does not match status for #{check[:id]}") validate_check_field_types!(check) validate_details!(check) validate_remediation!(check) end |
.validate_check_field_types!(check) ⇒ Object
126 127 128 129 130 131 |
# File 'lib/react_on_rails/doctor_schema.rb', line 126 def validate_check_field_types!(check) assert(check[:title].is_a?(String), "invalid title for #{check[:id]}") assert(check[:message].nil? || check[:message].is_a?(String), "invalid message for #{check[:id]}") assert(check[:fix_command].nil? || check[:fix_command].is_a?(String), "invalid fix_command for #{check[:id]}") assert(check[:docs_url].is_a?(String), "invalid docs_url for #{check[:id]}") end |
.validate_detail!(detail, check_id) ⇒ Object
138 139 140 141 142 |
# File 'lib/react_on_rails/doctor_schema.rb', line 138 def validate_detail!(detail, check_id) required_keys!(detail, %i[level content], "detail for #{check_id}") assert(DETAIL_LEVELS.include?(detail[:level]), "invalid detail level for #{check_id}") assert(detail[:content].is_a?(String), "invalid detail content for #{check_id}") end |
.validate_details!(check) ⇒ Object
133 134 135 136 |
# File 'lib/react_on_rails/doctor_schema.rb', line 133 def validate_details!(check) assert(check[:details].is_a?(Array), "details must be an array for #{check[:id]}") check[:details].each { |detail| validate_detail!(detail, check[:id]) } end |
.validate_remediation!(check) ⇒ Object
144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/react_on_rails/doctor_schema.rb', line 144 def validate_remediation!(check) remediation = check[:remediation] if check[:status] == "pass" assert(remediation.nil?, "passing check #{check[:id]} must not have remediation") return end assert(remediation.is_a?(Hash), "non-passing check #{check[:id]} must have remediation") required_keys!(remediation, %i[prompt files expected_end_state], "remediation") assert(remediation[:prompt].is_a?(String) && !remediation[:prompt].empty?, "invalid remediation prompt") assert(remediation[:files].is_a?(Array) && remediation[:files].all?(String), "invalid remediation files") assert(remediation[:expected_end_state].is_a?(String), "invalid remediation expected_end_state") end |
.validate_summary!(report) ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/react_on_rails/doctor_schema.rb', line 163 def validate_summary!(report) summary = report[:summary] required_keys!(summary, %i[pass warn fail], "summary") assert(summary.values.all? { |count| count.is_a?(Integer) && count >= 0 }, "summary values must be non-negative integers") statuses = report[:checks].map { |check| check[:status] } expected_summary = summarize_statuses(statuses) assert(summary == expected_summary, "summary does not match checks") assert(report[:status] == overall_status(statuses), "report status does not match checks") end |
.validate_unique_check_ids!(checks) ⇒ Object
158 159 160 161 |
# File 'lib/react_on_rails/doctor_schema.rb', line 158 def validate_unique_check_ids!(checks) ids = checks.map { |check| check[:id] } assert(ids.uniq.length == ids.length, "check ids must be unique") end |