コンテンツにスキップ

ADR-40 — `config_schema` declared defaults (`{kind:, default:}`)

Status: Accepted, 2026-06-02. {kind:, default:}という拡張されたconfig_schema値形式 + Manifest#config_defaultsアクセサ + Plugin::Base#configのデフォルトマージが実装され、rigor-statesman / rigor-pundit / rigor-actioncable(およびさらなるプラグインが段階的に)がDEFAULT_*定数イディオムから移行されました。後方互換: 素の種別の値形式("key" => :string)は変わりません。

プラグインのconfig_schemaが値の種別と並んでデフォルト値を宣言できるようにする決定を記録します。これによりエンジンがユーザー設定の下に宣言されたデフォルトをマージし、約17個のバンドルプラグインで繰り返されているDEFAULT_*定数 + config.fetch("key", DEFAULT_KEY)イディオムが退役します。

根拠となる計画: docs/design/20260602-plugin-boilerplate-reduction-plan.md § Phase 0d(config.fetch + DEFAULT_*の重複、件数17)。

プラグインのManifestは既に、受け入れる各設定キーを値の種別(:string / :boolean / :integer / :array / :hash / :any)にマップするconfig_schema:を保持しています。Manifest#validate_configはそれを使って、ローダーがユーザーの.rigor.ymlプラグイン設定を読むときに未知のキーや種別不一致の値を拒否します。

しかしスキーマは種別だけを宣言し、デフォルトは決して宣言しません。そのため設定可能なプラグインはすべて、同じ2部構成のイディオム——キーごとのDEFAULT_*定数に加え、読み出し時のfetch-with-default——を再実装しています:

class Statesman < Rigor::Plugin::Base
manifest(
id: "statesman", version: "0.1.0",
config_schema: {
"dsl_method" => :string,
"state_method" => :string,
"transition_method" => :string
}
)
DEFAULT_DSL_METHOD = "state_machine"
DEFAULT_STATE_METHOD = "state"
DEFAULT_TRANSITION_METHOD = "transition_to"
def init(_services)
@dsl_method = config.fetch("dsl_method", DEFAULT_DSL_METHOD).to_sym
@state_method = config.fetch("state_method", DEFAULT_STATE_METHOD).to_sym
@transition_method = config.fetch("transition_method", DEFAULT_TRANSITION_METHOD).to_sym
end
end

デフォルトは2度存在します——一度は概念的にスキーマ内に(スキーマは既にキーを名指ししている)、もう一度は読み出し側がfetchを通して引き回すことを覚えていなければならない独立した定数として。スキーマがデフォルトの自然な置き場です。種別の置き場であるのと全く同じように。これがボイラープレート削減計画がフラグを立てたPhase 0dの重複です。

config_schemaの値形式を拡張します: 値は既存の素の種別(Symbol / Stringまたはkind:(必須)とオプションのdefault:を保持するHashのいずれであってもよい(MAY)。

config_schema: {
"dsl_method" => { kind: :string, default: "state_machine" },
"state_method" => { kind: :string, default: "state" },
"transition_method" => { kind: :string, default: "transition_to" }
}
  • Manifestは各値を2つのフリーズされたマップへパースします: 既存のconfig_schema種別マップ({ "key" => :kind }形状は不変なのでvalidate_config / to_h / ==は動き続ける)と、新しいconfig_defaultsマップ({ "key" => default }、デフォルトを宣言したキーに対してのみ)。
  • Manifest#config_defaultsは公開リーダーです(公開APIドリフトスペック + RBSシグで固定)。
  • 宣言されたdefault:はマニフェスト構築時に宣言されたkind:に対して検証されるため(value_matches?を再利用)、タイポのあるデフォルト(kind: :stringの下でのdefault: 5)は使用時に静かにではなく、ロード時に声高に失敗します。
  • Plugin::Base#initializemanifest.config_defaults.merge(config)(ユーザー設定が勝つ)をフリーズされた#configとして格納します。したがってプラグインはconfig.fetch("dsl_method")(またはconfig["dsl_method"])を読むと、DEFAULT_*定数も第2のデフォルト引数もなしに宣言されたデフォルトを得ます。プラグインが依然望む型強制(.to_symArray(...))は読み出し側に留まります。

なぜこれが後方互換でFP安全なのか

Section titled “なぜこれが後方互換でFP安全なのか”
  • 値文法の純粋なスーパーセット。素の種別の値("key" => :string)はデフォルトを記録せず以前と全く同じようにパースされるので、既存のあらゆるマニフェストと未移行のあらゆるプラグインは手つかずです。それらに対してconfig_defaults{}であり、マージはノーオペレーションです。
  • 新しい診断なし、推論変更なし。これはプラグイン設定のエルゴノミクスであり、型束もどのルールも変えません。偽陽性を導入することはできません。
  • キャッシュセーフ。永続キャッシュはプラグインを(id, version,ユーザー設定ハッシュ)Cache::Descriptor::PluginEntry)でキーイングし、マニフェストのto_hではキーイングしません。デフォルトはプラグインのコード(そのバージョン)の一部なので、デフォルトを変えることは他のあらゆる振る舞い変更と同様のバージョンバンプです——既存のキーイングが既にそれを捕捉しています。config_defaultsManifest#to_hに追加してもManifest#== / #hashにのみ影響し、キャッシュキーには決して影響しません。
  1. このADR(メカニズム + 最初のコンシューマー)Manifestのパース + config_defaultsリーダー + デフォルト種別検証 + Base#configマージ + ユニットテスト + ドリフト/RBSの固定。最初のコンシューマー(クリーンなstring / array-defaultのケース)としてrigor-statesman / rigor-pundit / rigor-actioncableを移行する。
  2. 残りプラグインの移行 — その他の設定可能なバンドルプラグイン(activerecord / actionpack / actionmailer / sidekiq / rails-routes / rails-i18n / factorybot / sorbet / …)がDEFAULT_*から段階的に移行し、各々がゴールデンマスター統合スペックに対して振る舞いを保存する。純粋なクリーンアップ、需要駆動。
  • ADR-2 — ADR-2 §「Registration, Configuration, and Caching」が固定するconfig_schemaフィールドを拡張する;新しい値形式はそのサーフェスに対して加法的である。
  • ADR-37 / ADR-38 — 同じ「エンジンが1つの明確に定義された場所で消費する宣言的マニフェストフィールド」モデル;ここでの場所は推論ゲートではなくBase#configである。
  • ボイラープレート削減計画Phase 0a–0e — 0dは、着地済みの0a(Source::Literals)、0b/0c(Diagnostic.from_node / Base.suggest)、0e(Plugin::Inflector、ADR-39)の兄弟である。
候補ステータス理由
別個のconfig_defaults:マニフェストフィールド(config_schema:と並列)Rejected1つの概念を、作者が同期させ続けねばならない2つのフィールドに分割する;種別とデフォルトは同じキーに属するので、{kind:, default:}という値はそれらを一緒に保つ。
素の種別形式のみを維持し、defaults:ハッシュ経由でデフォルトを追加Rejected同じソース分割の問題;またスキーマが種別なしでキーを名指しできる状態を残す。
デフォルト値を深くフリーズする(再帰的に)Deferredデフォルトは読み出し側がコピーするスカラー / 浅い配列(Array(...).map)である;config_defaultsマップの浅いフリーズで十分。可変なネストされたデフォルトが宣言された場合にのみ再検討する。
マージされた値を宣言された種別へ自動的に型強制する(例: :stringString()Rejected読み出し側は既に望む型強制(.to_symArray())を所有している;自動型強制は生のYAML値に依存するプラグインを驚かせ、「デフォルトをマージする」スコープの外である。

ポジティブ:

  • 設定可能なバンドルプラグイン全体でDEFAULT_*定数 + fetch-with-defaultイディオムを退役させる;デフォルトは、既にキーを名指ししているスキーマ内で一度だけ宣言される。
  • 宣言されたデフォルトはロード時にその種別に対して検証され、静かな誤った型付けのデフォルトを声高なマニフェストエラーに変える。
  • 小さな加法的サーフェス: 1つの拡張された値形式、1つのリーダー、Base#initialize内の1行のマージ。新しいフックなし、推論変更なし。

ネガティブ:

  • config_schema内の2つの受け入れられる値形式(素の種別 / {kind:, default:})は、文書化すべき文法がわずかに増える——素の形式が最もシンプルなケースのまま留まり、検証メッセージが期待される形状を名指しすることで緩和される。
  • デフォルトなしだが非nilの「未設定」センチネルを持つキーを望むプラグインは、依然config["key"]nilを読む;デフォルトメカニズムは「必須キー」をモデル化しない(それはvalidate_configの未知キー / 種別の仕事のまま)。今日これを必要とするプラグインはない。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.