RailsのStrong Parametersのexpectについて

## 概要
railsではstrong parametersを用いて, 許可されていないパラメータによってモデルの重要な属性が誤って更新されてしまうことを防止することができます
パラメータの許可には`permit`を用います

また必須パラメータが不足している場合, `require`や`fetch`を使うことで例外発生やデフォルトの値を設定することができます

さらにrails8からはパラメータの必須化と許可を同時に行う`expect`が導入されました

この記事では`permit`, `require`と`fetch`ついて確認し, rails8で導入された`expect`について見ていきます

## permitについて
`permit`を用いることで許可するパラメータを決めることができます
スカラー, 配列, ハッシュなどで許可することができます

例として下記のような`params`を用います
`ActionController::Parameters.new`で初期化したオブジェクトの`permited`は`false`となっています
```ruby
params = ActionController::Parameters.new({name: "name", numbers: [3, 5, 7], animal_hash: {dog: "wan", cat: "nya"}})
=> #<ActionController::Parameters {"name"=>"name", "numbers"=>[3, 5, 7], "animal_hash"=>{"dog"=>"wan", "cat"=>"nya"}} permitted: false>
```

### スカラー
キーを指定することで取得できます
```ruby
permited = params.permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>

permited[:name]
=> "name"
```

ハッシュの中のスカラーだけを取得することは出来ません
```ruby
permited = params.permit(:dog)
=> #<ActionController::Parameters {} permitted: true>

permited.has_key?(:dog)
=> false
```

### 配列
キーを指定するだけでは取得出来ません
```ruby
permited = params.permit(:numbers)
=> #<ActionController::Parameters {} permitted: true>

permited.has_key?(:numbers)
=> false
```

空配列を対応付ける必要があります
```ruby
permited = params.permit(numbers: [])
=> #<ActionController::Parameters {"numbers"=>[3, 5, 7]} permitted: true>

permited.has_key?(:numbers)
=> true

permited[:numbers]
=> [3, 5, 7]
```

### ハッシュ
ハッシュはキーを指定するだけ, また空配列を対応付けることで取得することはできません
```ruby
permited = params.permit(:animal_hash)
=> #<ActionController::Parameters {} permitted: true>

permited.has_key?(:animal_hash)
=> false


permited = params.permit(animal_hash: [])
=> #<ActionController::Parameters {} permitted: true>

permited.has_key?(:animal_hash)
=> false
```

ハッシュで取得するときは空のハッシュを割り当てる必要があります
ハッシュで取得するときは`ActionController::Parameters`のオブジェクトのままです

```ruby
permited = params.permit(animal_hash: {})
=> #<ActionController::Parameters {"animal_hash"=>#<ActionController::Parameters {"dog"=>"wan", "cat"=>"nya"} permitted: true>} permitted: true>

permited.has_key?(:animal_hash)
=> true

permited[:animal_hash]
=> #<ActionController::Parameters {"dog"=>"wan", "cat"=>"nya"} permitted: true>

permited[:animal_hash].class
=> ActionController::Parameters

permited[:animal_hash][:dog]
=> "wan"
```


## requireについて
`require`で指定されたキーがない場合には例外が発生します

また`permit`を実行しないと, `permitted`は`false`のままになります
つまり`require`ではパラメータの許可は出来ません

さらに`require`で指定されたものはキーとして取得できなくなります


例として下記の`params`を用います
```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
```

`"title"`と`"content"`を取得するためには`require`に`:article`, `permit`に`:title, :content`を渡します
```ruby
permited = params.require(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
```

`require`のキーで指定した`:article`は取得することができなくなります
```ruby
permited[:article]
=> nil
```

`:title`と`:content`は取得できます
```ruby
permited[:title]
=> "title"

permited[:content]
=> "content"
```

`require`で指定されたキー(`:author`)がパラメータにない場合, 例外が発生します
```ruby
permited = params.require(:author).permit(:name)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
```

また`require`だけでは`permitted`は`false`のままなので, パラメータを許可するためには`permit`を実行する必要があります
```ruby
not_permitted = params.require(:article)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>


not_permitted.permitted?
=> false
```


複数のキー(`:article, :author`)で構成されているものを考えます
```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>
```

1つのキー(`:article`)を指定して取得することができます
```ruby
permited = params.require(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>

permited.keys
=> ["title", "content"]
```

複数のキーをrequireする場合には配列として渡してあげる必要があります
```ruby
permited = params.require(:article, :author)
wrong number of arguments (given 2, expected 1) (ArgumentError)

permited = params.require([:article, :author])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>, #<ActionController::Parameters {"name"=>"name"} permitted: false>]
```

複数のキーでrequireを実行した場合, permitはキーごとに行う必要があります
```ruby
permited = params.require([:article, :author]).permit(:title, :content, :name)
undefined method `permit` for an instance of Array (NoMethodError)

article_permited, author_permited = params.require([:article, :author])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>, #<ActionController::Parameters {"name"=>"name"} permitted: false>]

article_permited.permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>

author_permited.permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>
```


## fetchについて
`require`と異なり, 例外ではなくデフォルトを設定することができます
`fetch`されたキーがなくなること, `permit`の実行が必要なところは一緒になります

例として下記を用います
```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
```

`require`と一緒で`fetch`されたキーがなくなることや`permit`の実行が必要になります
```ruby
permited = params.fetch(:article).permit(:title, :content)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>

permited[:article]
=> nil

permited[:title]
=> "title"

permited[:content]
=> "content"
```

第2引数なしの`fetch`は`require`と同じ挙動になります
つまり`fetch`で指定されたキー(`:author`)がパラメータにない場合, 例外が発生します
```ruby
permited = params.fetch(:author).permit(:name)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
```

第2引数でデフォルトを設定することができます
```ruby
permited = params.fetch(:author, "author")
=> "author"
```

第2引数でスカラーを定義するとpermitは出来ません
```ruby
permited = params.fetch(:author, "author").permit(:name)
undefined method `permit' for an instance of String (NoMethodError)
```

デフォルトにハッシュを指定することで`permit`できるようになります
```ruby
permited = params.fetch(:author, {name: "name"}).permit(:name)
=> #<ActionController::Parameters {"name"=>"name"} permitted: true>

permited[:name]
=> "name"


permited = params.fetch(:author, {}).permit(:name)
=> #<ActionController::Parameters {} permitted: true>

permited[:name]
=> nil
```

`require`とは異なり, 複数のキーを指定して`fetch`することはできません
```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>


# requireみたいに配列で取ってくることはできない
permited = params.fetch([:article, :author])
param is missing or the value is empty: [:article, :author] (ActionController::ParameterMissing)

# 第2引数は特に使われない
permited = params.fetch(:article, :author)
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: false>

# 第2引数はデフォルトになってしまう
permited = params.fetch(:none, :author)
=> :author
```

## permit, requireとfetchからexpectへ
Strong Parametersでパラメータの許可を行うときは`permit`, パラメータの必須化を行うときは`require`と`fetch`が使われていました
しかしrails8ではパラメータの必須化と許可を同時に行う`expect`が導入されました
これからは`expect`をつかっていくことができます

## expectについて

例として下記を用います
```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }})
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}} permitted: false>
```

`"title"`と`"content"`を取得するためには下記のようになります

```ruby
permitted = params.expect(article: [:title, :content])
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
```

`:article`は取得できなくなり, `:title`と`:content`が取得できます

```ruby
permitted[:article]
=> nil

constraints-project(dev)> permitted[:title]
=> "title"

constraints-project(dev)> permitted[:content]
=> "content"
```

また, パラメータの許可も行われています

```ruby
permitted.permitted?
=> true
```

指定されたキーがない場合には`require`と同様に例外が発生します

```ruby
permitted = params.expect(:author)
param is missing or the value is empty or invalid: author (ActionController::ParameterMissing)
```

複数のキー(`:article, :author`)で構成されているものを考えます

```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, author: { name: "name"} })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "author"=>{"name"=>"name"}} permitted: false>
```

1つのキー(`:article`)を指定して取得することができます
```ruby
permitted = params.expect(article: [:title, :content])
=> #<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>
```

複数キー(`:article, :author`)をしてして取得することもできます
```ruby
permitted = params.expect(article: [:title, :content], author: [:name])
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>, #<ActionController::Parameters {"name"=>"name"} permitted: true>]
```

`:author`のように1つの要素しかない場合には, 配列で渡さなくても実行できます
```ruby
permitted = params.expect(article: [:title, :content], author: :name)
=> [#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>, #<ActionController::Parameters {"name"=>"name"} permitted: true
```

また, 下記のような複雑パラメータでも簡単に必須化と許可を行えます

```ruby
params = ActionController::Parameters.new({ article: { title: "title", content: "content" }, authors: [{ name: "name1", age: 30}, {name: "name2", age: 35}] })
=> #<ActionController::Parameters {"article"=>{"title"=>"title", "content"=>"content"}, "authors"=>[{"name"=>"name1", "age"=>30}, {"name"=>"name2", "age"=>35}]} permitted: false>
```

キーの中の配列(`:authors`)も簡単に取得することが出来ます

```ruby
permitted = params.expect(article: [:title, :content], authors: [[:name, :age]])
=>
[#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>,
...

permitted
=>
[#<ActionController::Parameters {"title"=>"title", "content"=>"content"} permitted: true>,
 [#<ActionController::Parameters {"name"=>"name1", "age"=>30} permitted: true>, #<ActionController::Parameters {"name"=>"name2", "age"=>35} permitted: true>]]
 ```

rails8では二重配列構文[[:属性名]]という新しい配列マッチング構文が導入されています

## まとめ
rails8で`expect`が導入されたので簡単にまとめてみました
`expect`は必須化と許可を簡単に行うことが出来, 複雑なパラメータも簡単に扱えるようになりました
`expcet`はあらゆるパラメータフィルタに対応するわけではありませんが, 簡単に導入できるので試してみる価値がありそうです

## ちなみに
今回の変更を取り扱っているプルリクエストは https://github.com/rails/rails/pull/51674 になります

rails8より前はパラメータの処理として
```ruby
user_params = params.require(:user).permit(:name, :age)
```
のように`require`の後に`permit`を用いることが推奨されていました


しかしときには
```ruby
user_params = params.permit(user: [:name, :age]).require(:user)
```
のように先に`permit`する必要もありました

この問題などを解決するために今回の修正が入ったみたいです

## 参考
railsガイド: https://railsguides.jp/action_controller_overview.html#strong-parameters
ActionController::Parameters: https://api.rubyonrails.org/classes/ActionController/Parameters.html