decentworks-hexdigest-support 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/CLAUDE.md +24 -0
- data/.claude/hooks/rubocop-fix.sh +27 -0
- data/.claude/rules/general.md +5 -0
- data/.claude/rules/git.md +6 -0
- data/.claude/rules/rspec.md +24 -0
- data/.claude/rules/ruby.md +11 -0
- data/.claude/settings.json +67 -0
- data/.rspec +3 -0
- data/.rubocop.yml +300 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +532 -0
- data/Rakefile +12 -0
- data/lib/decentworks/hexdigest_support/array.rb +24 -0
- data/lib/decentworks/hexdigest_support/big_decimal.rb +14 -0
- data/lib/decentworks/hexdigest_support/configuration.rb +40 -0
- data/lib/decentworks/hexdigest_support/data.rb +13 -0
- data/lib/decentworks/hexdigest_support/date.rb +24 -0
- data/lib/decentworks/hexdigest_support/hash.rb +28 -0
- data/lib/decentworks/hexdigest_support/input.rb +97 -0
- data/lib/decentworks/hexdigest_support/nil_class.rb +13 -0
- data/lib/decentworks/hexdigest_support/numeric.rb +31 -0
- data/lib/decentworks/hexdigest_support/numeric_like.rb +93 -0
- data/lib/decentworks/hexdigest_support/object.rb +91 -0
- data/lib/decentworks/hexdigest_support/range.rb +24 -0
- data/lib/decentworks/hexdigest_support/set.rb +14 -0
- data/lib/decentworks/hexdigest_support/struct.rb +16 -0
- data/lib/decentworks/hexdigest_support/time.rb +9 -0
- data/lib/decentworks/hexdigest_support/time_like.rb +42 -0
- data/lib/decentworks/hexdigest_support/time_with_zone.rb +19 -0
- data/lib/decentworks/hexdigest_support/version.rb +7 -0
- data/lib/decentworks/hexdigest_support.rb +20 -0
- data/lib/decentworks-hexdigest-support.rb +3 -0
- data/lib/generators/decentworks/hexdigest_support/install/install_generator.rb +38 -0
- data/lib/generators/decentworks/hexdigest_support/install/templates/decentworks_hexdigest_support.rb.tt +16 -0
- data/sig/decentworks/hexdigest_support/array.rbs +3 -0
- data/sig/decentworks/hexdigest_support/configuration.rbs +14 -0
- data/sig/decentworks/hexdigest_support/data.rbs +3 -0
- data/sig/decentworks/hexdigest_support/date.rbs +7 -0
- data/sig/decentworks/hexdigest_support/hash.rbs +3 -0
- data/sig/decentworks/hexdigest_support/input.rbs +11 -0
- data/sig/decentworks/hexdigest_support/nil_class.rbs +3 -0
- data/sig/decentworks/hexdigest_support/object.rbs +19 -0
- data/sig/decentworks/hexdigest_support/range.rbs +3 -0
- data/sig/decentworks/hexdigest_support/set.rbs +3 -0
- data/sig/decentworks/hexdigest_support/struct.rbs +3 -0
- data/sig/decentworks/hexdigest_support/time.rbs +3 -0
- data/sig/decentworks/hexdigest_support/time_like.rbs +8 -0
- data/sig/decentworks/hexdigest_support/time_with_zone.rbs +5 -0
- data/sig/decentworks/hexdigest_support/version.rbs +5 -0
- metadata +123 -0
data/README.md
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Decentworks::HexdigestSupport
|
|
2
|
+
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> 本ライブラリは個人によって開発・保守されています。予告なく仕様変更または提供を終了する場合があります。ご利用にあたってはバージョンを固定のうえ、更新時は変更内容をご確認ください。
|
|
5
|
+
|
|
6
|
+
任意のRubyオブジェクトから、決定的なハッシュ値(16進ダイジェスト)を求めるための拡張ライブラリです。
|
|
7
|
+
|
|
8
|
+
`Object` にダイジェスト生成用のメソッドを追加し、`Array` / `Hash` / `Range` / `Struct` / `Data` / `Set` には構造を考慮した入力生成を、`Integer` / `Float` / `Rational` / `BigDecimal` / `Time` / `Date` / `DateTime` / `ActiveSupport::TimeWithZone` には正規化した入力生成を実装しています。
|
|
9
|
+
|
|
10
|
+
- **型を保持する** — `:a` と `"a"`、`1` と `"1"` は異なるダイジェストになります
|
|
11
|
+
- **順序に依存しない** — 配列・ハッシュは要素をソートしてから連結するため、並び順が違っても同じダイジェストになります
|
|
12
|
+
- **実行環境に依存しない** — `Hash#inspect` や `String#inspect` などネイティブの文字列表現には依存せず、自前で入力を組み立てます(Rubyのバージョンやロケールが変わってもダイジェストは変わりません)
|
|
13
|
+
- **ソルトに対応** — 設定したソルトをダイジェストの入力へ前置します
|
|
14
|
+
- **Railsの日時に対応** — `ActiveSupport::TimeWithZone` も `Time` と同じダイジェストになります
|
|
15
|
+
- **Railsの数値に対応** — `decimal` カラムの `BigDecimal` も `Integer` / `Float` と同じダイジェストになります
|
|
16
|
+
|
|
17
|
+
対応アルゴリズムはMD5 / RMD160 / SHA1 / SHA256 / SHA384 / SHA512です。
|
|
18
|
+
|
|
19
|
+
## インストール
|
|
20
|
+
|
|
21
|
+
Gemfileに追加します。
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem "decentworks-hexdigest-support"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Bundlerを使わない場合は次のコマンドでインストールします。
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ gem install decentworks-hexdigest-support
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
読み込みは `require` で行います。
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "decentworks/hexdigest_support"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
必要なRubyのバージョンは `>= 4.0.0` です。`activesupport` (`>= 8.0`)と `bigdecimal` (`>= 3.1`)に依存します。
|
|
44
|
+
|
|
45
|
+
## セットアップ
|
|
46
|
+
|
|
47
|
+
ソルトを設定すると、ダイジェストの入力へ前置されます(未設定時はソルトなし)。
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
::Decentworks::HexdigestSupport.configure do |config|
|
|
51
|
+
config.salt = "..."
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Rails
|
|
56
|
+
|
|
57
|
+
初期化ファイルはジェネレータで生成できます。
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ bin/rails generate decentworks:hexdigest_support:install
|
|
61
|
+
create config/initializers/decentworks_hexdigest_support.rb
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
生成される初期化ファイルは `credentials` からソルトを読み出します。
|
|
65
|
+
|
|
66
|
+
```console
|
|
67
|
+
$ bin/rails credentials:edit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
decentworks:
|
|
72
|
+
hexdigest_support_salt: <ランダムな文字列(例: `bin/rails secret` の出力)>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
キー名は `--salt-key` で変更できます。
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ bin/rails generate decentworks:hexdigest_support:install --salt-key=custom_salt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 使い方
|
|
82
|
+
|
|
83
|
+
### ダイジェストを求める
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
"user@example.com".to_hexdigest
|
|
87
|
+
# => "8d3b74fd6c74d37b4abf8845a1b2e9c775510b529b0b224c87f5ee989bc2da0a"
|
|
88
|
+
|
|
89
|
+
"user@example.com".to_md5_hexdigest
|
|
90
|
+
# => "ccf86d256ba77a8e4a9c4b8dae6a3019"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
アルゴリズムごとのメソッドと、系統ごとのデフォルトのエイリアスが用意されています。
|
|
94
|
+
|
|
95
|
+
| メソッド | アルゴリズム | 長さ |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `#to_md5_hexdigest` | MD5 | 32 |
|
|
98
|
+
| `#to_rmd160_hexdigest` | RMD160 | 40 |
|
|
99
|
+
| `#to_sha1_hexdigest` | SHA1 | 40 |
|
|
100
|
+
| `#to_sha256_hexdigest` | SHA256 | 64 |
|
|
101
|
+
| `#to_sha384_hexdigest` | SHA384 | 96 |
|
|
102
|
+
| `#to_sha512_hexdigest` | SHA512 | 128 |
|
|
103
|
+
| `#to_md_hexdigest` | MD5のエイリアス | 32 |
|
|
104
|
+
| `#to_rmd_hexdigest` | RMD160のエイリアス | 40 |
|
|
105
|
+
| `#to_sha_hexdigest` | SHA256のエイリアス | 64 |
|
|
106
|
+
| `#to_hexdigest` | SHA256のエイリアス(既定) | 64 |
|
|
107
|
+
|
|
108
|
+
### 型が保持される
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
1.to_hexdigest_input # => "Numeric:\"1\""
|
|
112
|
+
"1".to_hexdigest_input # => "String:\"1\""
|
|
113
|
+
|
|
114
|
+
1.to_hexdigest == "1".to_hexdigest # => false
|
|
115
|
+
:a.to_hexdigest == "a".to_hexdigest # => false
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 配列・ハッシュ・範囲
|
|
119
|
+
|
|
120
|
+
要素はソートされてから連結されるため、順序が違っても同じダイジェストになります。
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
[1, "a", :b].to_hexdigest == [:b, 1, "a"].to_hexdigest # => true
|
|
124
|
+
|
|
125
|
+
{ a: 1, b: 2 }.to_hexdigest == { b: 2, a: 1 }.to_hexdigest # => true
|
|
126
|
+
|
|
127
|
+
# キーの型も区別される
|
|
128
|
+
{ a: 1 }.to_hexdigest == { "a" => 1 }.to_hexdigest # => false
|
|
129
|
+
|
|
130
|
+
# 範囲は始端・終端・終端を含むかで決まる
|
|
131
|
+
(1..3).to_hexdigest == (1...3).to_hexdigest # => false
|
|
132
|
+
|
|
133
|
+
# 端点のない範囲も扱える
|
|
134
|
+
(1..).to_hexdigest
|
|
135
|
+
(..3).to_hexdigest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
ネストした構造もそのまま扱えます。
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
{ id: 1, tags: %w[a b], range: (1..3) }.to_hexdigest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 構造体・集合
|
|
145
|
+
|
|
146
|
+
`Struct` / `Data` はメンバー名と値の組で決まります。`Set` は配列と同じく要素をソートしてから連結します。
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Point = Struct.new(:x, :y)
|
|
150
|
+
Point.new(1, 2).to_hexdigest_source # => '{Symbol:"x"=>Numeric:"1",Symbol:"y"=>Numeric:"2"}'
|
|
151
|
+
|
|
152
|
+
Coord = Data.define(:x, :y)
|
|
153
|
+
Coord.new(x: 1, y: 2).to_hexdigest
|
|
154
|
+
|
|
155
|
+
# メンバー名が違えば値が同じでも異なるダイジェストになる
|
|
156
|
+
Struct.new(:a, :b).new(1, 2).to_hexdigest == Struct.new(:x, :y).new(1, 2).to_hexdigest # => false
|
|
157
|
+
|
|
158
|
+
Set[1, 2].to_hexdigest == Set[2, 1].to_hexdigest # => true
|
|
159
|
+
|
|
160
|
+
# 同じ要素の配列とは異なるダイジェストになる(型で区別される)
|
|
161
|
+
Set[1, 2].to_hexdigest == [1, 2].to_hexdigest # => false
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> [!NOTE]
|
|
165
|
+
> 定数へ代入していない無名の `Struct` / `Data` は型が `Struct` / `Data` へ丸まるため、メンバー名と値が同じであれば別々に生成したもの同士も同じダイジェストになります。型で区別したい場合は定数へ代入してください。
|
|
166
|
+
|
|
167
|
+
### 数値
|
|
168
|
+
|
|
169
|
+
`Integer` / `Float` / `Rational` / `BigDecimal` は有理数として正規化され、**同じ型として扱われます**。同じ数であればクラスが違ってもダイジェストは一致します。
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
1.to_hexdigest_type # => "Numeric"
|
|
173
|
+
BigDecimal("1.0").to_hexdigest_type # => "Numeric"
|
|
174
|
+
|
|
175
|
+
1.to_hexdigest == 1.0.to_hexdigest # => true
|
|
176
|
+
1.to_hexdigest == BigDecimal("1.00").to_hexdigest # => true
|
|
177
|
+
1.to_hexdigest == Rational(2, 2).to_hexdigest # => true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Railsでは同じ数が経路によって別のクラスで現れます(`decimal` カラムは `BigDecimal`、`integer` カラムやJSONの整数は `Integer`、JSONの小数は `Float`)。型をクラス名のままにすると入力経路の違いだけでダイジェストが割れてしまうため、時刻と同じく型を `"Numeric"` へ正規化しています。
|
|
181
|
+
|
|
182
|
+
値は、有限小数で表せる場合は十進表記に、表せない場合は既約分数の表記になります。
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
1.0.to_hexdigest_source # => "1"
|
|
186
|
+
1.5.to_hexdigest_source # => "1.5"
|
|
187
|
+
BigDecimal("1.50").to_hexdigest_source # => "1.5"
|
|
188
|
+
1e20.to_hexdigest_source # => "100000000000000000000"
|
|
189
|
+
(-0.0).to_hexdigest_source # => "0"
|
|
190
|
+
|
|
191
|
+
Rational(1, 3).to_hexdigest_source # => "1/3"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`Float` は2進の厳密値ではなく、`#to_s` が返す十進表記として解釈されます。`0.1` の厳密値は `1/10` ではありませんが、見た目どおりの十進として読むため `BigDecimal("0.1")` と同じダイジェストになります。
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
0.1.to_hexdigest == BigDecimal("0.1").to_hexdigest # => true
|
|
198
|
+
0.1.to_hexdigest == Rational(1, 10).to_hexdigest # => true
|
|
199
|
+
|
|
200
|
+
# 計算誤差は丸められず、そのまま保たれる
|
|
201
|
+
(0.1 + 0.2).to_hexdigest == 0.3.to_hexdigest # => false
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`NaN` と `±Infinity` は有理数にできないため、`#to_s` の結果がそのまま値になります。
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
Float::NAN.to_hexdigest_source # => "NaN"
|
|
208
|
+
Float::INFINITY.to_hexdigest_source # => "Infinity"
|
|
209
|
+
|
|
210
|
+
# NaN同士は#==がfalseになるが、ダイジェストは一致する
|
|
211
|
+
Float::NAN.to_hexdigest == BigDecimal("NaN").to_hexdigest # => true
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> [!NOTE]
|
|
215
|
+
> `Complex` は正規化の対象外で、型は `"Complex"` のままです。`Complex(1, 0) == 1` は真ですが、ダイジェストは一致しません。
|
|
216
|
+
|
|
217
|
+
### 日時
|
|
218
|
+
|
|
219
|
+
`Time` / `DateTime` / `ActiveSupport::TimeWithZone` はUTCへ変換し、ナノ秒までの精度で正規化されます。タイムゾーンの違いはダイジェストに影響しません。
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Time.utc(2026, 8, 13, 4, 5, 6).to_hexdigest_source
|
|
223
|
+
# => "2026-08-13T04:05:06.000000000Z"
|
|
224
|
+
|
|
225
|
+
# 同じ瞬間を指す時刻は同じダイジェストになる
|
|
226
|
+
Time.new(2026, 8, 13, 13, 5, 6, "+09:00").to_hexdigest == Time.utc(2026, 8, 13, 4, 5, 6).to_hexdigest # => true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
さらに、この3つは**同じ型として扱われます**。同じ瞬間を指していればクラスが違ってもダイジェストは一致します。
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
Time.zone = "Asia/Tokyo"
|
|
233
|
+
|
|
234
|
+
Time.zone.local(2026, 8, 13, 13, 5, 6).to_hexdigest_type # => "Time"
|
|
235
|
+
DateTime.new(2026, 8, 13, 13, 5, 6, "+09:00").to_hexdigest_type # => "Time"
|
|
236
|
+
|
|
237
|
+
Time.zone.local(2026, 8, 13, 13, 5, 6).to_hexdigest == Time.utc(2026, 8, 13, 4, 5, 6).to_hexdigest # => true
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Railsでは同じ瞬間が経路によって別のクラスで現れます(`Time.zone.now` とActiveRecordの `datetime` カラムは `ActiveSupport::TimeWithZone`、`Time.now` や `File.mtime` は `Time`)。型をクラス名のままにすると、入力経路の違いだけでダイジェストが割れてしまうため、時刻に限っては型を `"Time"` へ正規化しています。
|
|
241
|
+
|
|
242
|
+
`Date` は「ある一瞬」ではなく1日を指すため、この正規化の対象外です。
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
Date.new(2026, 8, 13).to_hexdigest_source # => "2026-08-13"
|
|
246
|
+
Date.new(2026, 8, 13).to_hexdigest_type # => "Date"
|
|
247
|
+
|
|
248
|
+
# 同じ日付のDateTimeとは異なるダイジェストになる
|
|
249
|
+
Date.new(2026, 8, 13).to_hexdigest == DateTime.new(2026, 8, 13).to_hexdigest # => false
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
> [!NOTE]
|
|
253
|
+
> ナノ秒より細かい精度は切り捨てられます。DBの `timestamp`(多くはマイクロ秒)と往復させても値が変わらない粒度に揃えるためです。
|
|
254
|
+
|
|
255
|
+
### nil・真偽値
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
nil.to_hexdigest_input # => "NilClass:\"nil\""
|
|
259
|
+
true.to_hexdigest_input # => "TrueClass:\"true\""
|
|
260
|
+
false.to_hexdigest_input # => "FalseClass:\"false\""
|
|
261
|
+
|
|
262
|
+
# 空文字とは区別される
|
|
263
|
+
nil.to_hexdigest == "".to_hexdigest # => false
|
|
264
|
+
|
|
265
|
+
# ハッシュの値がnilの場合も、キーそのものがない場合と区別される
|
|
266
|
+
{ a: nil }.to_hexdigest == {}.to_hexdigest # => false
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 独自クラスのダイジェスト
|
|
270
|
+
|
|
271
|
+
既定では `#to_s` の結果が入力になります。値を明示したい場合は `#to_hexdigest_source` をオーバーライドします。型は `#to_hexdigest_input` が付与するため、オーバーライド側で型を意識する必要はありません。
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class User
|
|
275
|
+
attr_reader :id, :email
|
|
276
|
+
|
|
277
|
+
def initialize(id, email)
|
|
278
|
+
@id = id
|
|
279
|
+
@email = email
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def to_hexdigest_source = { id: id, email: email }.to_hexdigest_source
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
User.new(1, "user@example.com").to_hexdigest
|
|
286
|
+
# => "90a52738430094c7aee77ad317032d77dbc8ffa285bb0b5b9cf66d3d4b8727bf"
|
|
287
|
+
|
|
288
|
+
# 値が同じなら同じダイジェストになる
|
|
289
|
+
User.new(1, "user@example.com").to_hexdigest == User.new(1, "user@example.com").to_hexdigest
|
|
290
|
+
# => true
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`#to_s` も `#to_hexdigest_source` も実装していないオブジェクトは、既定の `Object#to_s` が返すオブジェクトIDが値になってしまいます。この場合は例外になります。
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
Object.new.to_hexdigest
|
|
297
|
+
# => Decentworks::HexdigestSupport::NonDeterministicSourceError
|
|
298
|
+
|
|
299
|
+
# Procや無名クラスのように、独自の#to_sがオブジェクトIDを含む型も同様
|
|
300
|
+
proc {}.to_hexdigest
|
|
301
|
+
# => Decentworks::HexdigestSupport::NonDeterministicSourceError
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
配列やハッシュの中に含まれている場合も検出されます。
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
{ user: Object.new }.to_hexdigest
|
|
308
|
+
# => Decentworks::HexdigestSupport::NonDeterministicSourceError
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`String` と `Symbol` は検査の対象外です。値そのものが文字列であり、オブジェクトIDが混入する経路がないためです。オブジェクトIDの表記で始まる文字列(`#inspect` の結果や、それを含むログの1行など)もそのまま扱えます。
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
"#<User:0x00007f9e0c0d1234>".to_hexdigest # => 例外にならない
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
> [!NOTE]
|
|
318
|
+
> 裏を返すと、利用側が自分でオブジェクトを文字列化して渡した場合(`"#{object}"` など)は検出できません。gemから見ればただの文字列であり、他の文字列と区別する手段がないためです。
|
|
319
|
+
|
|
320
|
+
### 値オブジェクトのダイジェスト
|
|
321
|
+
|
|
322
|
+
不変で、値そのものが同一性を決めるクラス(`Money` / `EmailAddress` / `Period` など)は、次の順で検討してください。
|
|
323
|
+
|
|
324
|
+
**1. `Data.define` で足りるなら、それで足りる**
|
|
325
|
+
|
|
326
|
+
属性がそのまま値になるだけなら、`Data` のサポートがそのまま効くため独自の実装は不要です。
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
Money = ::Data.define(:amount, :currency)
|
|
330
|
+
|
|
331
|
+
Money.new(amount: 100, currency: "JPY").to_hexdigest_input
|
|
332
|
+
# => "Money:\"{Symbol:\\\"amount\\\"=>Numeric:\\\"100\\\",Symbol:\\\"currency\\\"=>String:\\\"JPY\\\"}\""
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
継承階層が既にある、`Data` の一部のメンバーはダイジェストへ含めたくない、といった場合に次へ進みます。
|
|
336
|
+
|
|
337
|
+
**2. 値が 1 つなら `#to_s` で足りる**
|
|
338
|
+
|
|
339
|
+
`#to_s` が値そのものを返すクラスは、既定の実装がそのまま使えます。型はクラス名から付与されるため、`#to_s` が同じでも別のクラス同士が衝突することはありません。
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class Currency
|
|
343
|
+
def initialize(code) = @code = code
|
|
344
|
+
|
|
345
|
+
def to_s = @code
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
Currency.new("JPY").to_hexdigest_input # => "Currency:\"JPY\""
|
|
349
|
+
|
|
350
|
+
# 同じ文字列とは異なるダイジェストになる
|
|
351
|
+
Currency.new("JPY").to_hexdigest == "JPY".to_hexdigest # => false
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**3. 属性が複数あるなら、ハッシュへ委譲する**
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
class EmailAddress
|
|
358
|
+
attr_reader :local, :domain
|
|
359
|
+
|
|
360
|
+
def initialize(local, domain)
|
|
361
|
+
@local = local
|
|
362
|
+
@domain = domain
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def to_hexdigest_source = { local:, domain: }.to_hexdigest_source
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
EmailAddress.new("user", "example.com").to_hexdigest_source
|
|
369
|
+
# => "{Symbol:\"domain\"=>String:\"example.com\",Symbol:\"local\"=>String:\"user\"}"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`Hash#to_hexdigest_source` はキーでソートするため、ハッシュへ書く順序はダイジェストに影響しません。
|
|
373
|
+
|
|
374
|
+
> [!IMPORTANT]
|
|
375
|
+
> 委譲先は `#to_hexdigest_source` です。`#to_hexdigest_input` と書くと値に `Hash` という型が混ざり、ダイジェストは正常に求まるものの意図した値になりません。
|
|
376
|
+
>
|
|
377
|
+
> ```ruby
|
|
378
|
+
> # 誤り: to_hexdigest_input へ委譲した場合
|
|
379
|
+
> # => "EmailAddress:\"Hash:\\\"{...}\\\"\""
|
|
380
|
+
> ```
|
|
381
|
+
|
|
382
|
+
#### 属性を追加したときの挙動
|
|
383
|
+
|
|
384
|
+
未設定の属性(`nil`)も値として含まれます。ハッシュの値が `nil` の場合とキーそのものがない場合を区別する扱いと同じです。
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
class Period
|
|
388
|
+
attr_reader :from, :to
|
|
389
|
+
|
|
390
|
+
def initialize(from, to = nil)
|
|
391
|
+
@from = from
|
|
392
|
+
@to = to
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def to_hexdigest_source = { from:, to: }.to_hexdigest_source
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
Period.new(::Date.new(2026, 8, 13)).to_hexdigest_source
|
|
399
|
+
# => "{Symbol:\"from\"=>Date:\"2026-08-13\",Symbol:\"to\"=>NilClass:\"nil\"}"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
したがって、属性を追加して `#to_hexdigest_source` へ含めると、既存の値のダイジェストも変わります。永続化済みの値がある場合は移行方針が必要です([独自クラスのオーバーライド](#独自クラスのオーバーライド)を参照)。
|
|
403
|
+
|
|
404
|
+
`nil` の属性を除外すれば既存のダイジェストは保てますが、推奨しません。「値が `nil`」と「その属性を持たない」が同じダイジェストになるため、`Period` の例では終了日が未定であることと、`to` という属性が存在しなかった時点のデータが区別できなくなります。値オブジェクトでは `nil` 自体が意味を持つことが多く、失うものの方が大きくなります。
|
|
405
|
+
|
|
406
|
+
### 循環参照
|
|
407
|
+
|
|
408
|
+
自身を含む値も例外になります。そのまま辿ると再帰が終わらず、`StandardError` を継承しない `SystemStackError` になってしまうためです。`SystemStackError` は呼び出し側の `rescue` をすり抜けます。
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
values = [1]
|
|
412
|
+
values << values
|
|
413
|
+
|
|
414
|
+
values.to_hexdigest
|
|
415
|
+
# => Decentworks::HexdigestSupport::CircularReferenceError
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
配列・ハッシュ・`Struct` / `Data` / `Set` に加えて、独自クラス同士が参照しあう場合も検出されます。Railsで `belongs_to :parent` と `has_many :children` の両方をダイジェストへ含めた場合などが該当します。
|
|
419
|
+
|
|
420
|
+
```ruby
|
|
421
|
+
class Node
|
|
422
|
+
attr_accessor :parent
|
|
423
|
+
|
|
424
|
+
def to_hexdigest_source = { parent: }.to_hexdigest_source
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
node = Node.new
|
|
428
|
+
node.parent = node
|
|
429
|
+
|
|
430
|
+
node.to_hexdigest
|
|
431
|
+
# => Decentworks::HexdigestSupport::CircularReferenceError
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
同じオブジェクトが兄弟として複数回現れるのは循環ではないため、例外にはなりません。判定に使うのは、その時点で辿っている経路だけです。
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
tags = %w[a b]
|
|
438
|
+
|
|
439
|
+
[tags, tags].to_hexdigest # => 例外にならない
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
> [!NOTE]
|
|
443
|
+
> 自身を含まない深いネスト(1万段など)は `SystemStackError` のままです。循環と違って有限であり、深さの上限を決め打ちすると正当な構造まで弾いてしまうためです。
|
|
444
|
+
|
|
445
|
+
### 入力の確認
|
|
446
|
+
|
|
447
|
+
デバッグ時は、ダイジェストの元になる文字列を確認できます。
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
"user@example.com".to_hexdigest_input
|
|
451
|
+
# => "String:\"user@example.com\""
|
|
452
|
+
|
|
453
|
+
# ソルトを前置した実際の入力
|
|
454
|
+
"user@example.com".to_salted_hexdigest_input
|
|
455
|
+
# => "pepperString:\"user@example.com\""
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## 注意事項
|
|
459
|
+
|
|
460
|
+
### ソルトの変更
|
|
461
|
+
|
|
462
|
+
> [!CAUTION]
|
|
463
|
+
> ソルトを変更すると、同じ値でも異なるダイジェストになります。永続化済みのダイジェストがある場合は、変更前に移行方針を検討してください。
|
|
464
|
+
|
|
465
|
+
ソルトはリポジトリに平文で置かず、Railsであれば `credentials` で管理してください。
|
|
466
|
+
|
|
467
|
+
### 用途
|
|
468
|
+
|
|
469
|
+
ダイジェストは同一性の判定や値の秘匿を目的としたものです。パスワードの保存など、総当たり耐性が必要な用途には適していません(その用途にはbcryptなどのパスワードハッシュを使ってください)。
|
|
470
|
+
|
|
471
|
+
### 値の引用とエスケープ
|
|
472
|
+
|
|
473
|
+
値は引用符で囲まれ、引用符(`"`)とバックスラッシュ(`\`)だけがエスケープされます。`#inspect` は使いません。`#inspect` は非ASCII文字を `Encoding.default_external` が印字可能かどうかでエスケープするか決めるため、同じ値でもロケール次第でダイジェストが変わってしまうためです。
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# UTF-8環境の#inspectと同じ出力になる
|
|
477
|
+
"あ".to_hexdigest_input # => "String:\"あ\""
|
|
478
|
+
|
|
479
|
+
# 制御文字はエスケープせず、そのまま入力に含まれる
|
|
480
|
+
"a\nb".to_hexdigest_input # => "String:\"a\nb\"" (#inspectなら "String:\"a\\nb\"")
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
> [!CAUTION]
|
|
484
|
+
> 改行やタブなどの制御文字を含む値は、`#inspect` を使っていた頃とダイジェストが変わります。ASCIIのみで制御文字を含まない値、およびUTF-8環境で求めた非ASCIIの値のダイジェストは変わりません。
|
|
485
|
+
|
|
486
|
+
### コアクラスの拡張
|
|
487
|
+
|
|
488
|
+
本gemは `Object` / `NilClass` / `Array` / `Hash` / `Range` / `Struct` / `Data` / `Set` / `Integer` / `Float` / `Rational` / `BigDecimal` / `Time` / `Date` / `DateTime` / `ActiveSupport::TimeWithZone` にメソッドを追加するモンキーパッチです。`#to_hexdigest_source` などのメソッド名が他のライブラリと衝突しないか確認してください。
|
|
489
|
+
|
|
490
|
+
### ActiveSupportのコア拡張の読み込み
|
|
491
|
+
|
|
492
|
+
`ActiveSupport::TimeWithZone` は ActiveSupport の autoload 経由でしか解決できないため、本gemは `require "active_support/time"` を無条件に実行します。これに伴い、`Time` / `Date` / `DateTime` / `Integer` / `Numeric` / `String` へのActiveSupportのコア拡張(`3.days` や `String#to_time` など)も読み込まれます。
|
|
493
|
+
|
|
494
|
+
Railsであればいずれも読み込まれているものなので影響はありませんが、Rails以外で使う場合はこの副作用を考慮してください。
|
|
495
|
+
|
|
496
|
+
なお、gem本体が依存するのは `activesupport` のみで、`railties` には依存しません(ジェネレータは `lib/generators` 配下に置かれ、Railsのジェネレータ探索から呼ばれた時にだけ読み込まれます)。
|
|
497
|
+
|
|
498
|
+
### 数値の型の正規化
|
|
499
|
+
|
|
500
|
+
`Integer` / `Float` / `Rational` / `BigDecimal` の `#to_hexdigest_type` は `"Numeric"` を返します。「型で区別する」という本gemの原則に対する意図的な例外です。
|
|
501
|
+
|
|
502
|
+
同じ数が経路によって別のクラスで現れるRailsでは、型を実装クラス名のままにするとダイジェストが割れます。詳細は[数値](#数値)を参照してください。
|
|
503
|
+
|
|
504
|
+
### 時刻の型の正規化
|
|
505
|
+
|
|
506
|
+
`Time` / `DateTime` / `ActiveSupport::TimeWithZone` の `#to_hexdigest_type` は `"Time"` を返します。「型で区別する」という本gemの原則に対する意図的な例外です。
|
|
507
|
+
|
|
508
|
+
同じ瞬間が経路によって別のクラスで現れるRailsでは、型を実装クラス名のままにするとダイジェストが割れます。詳細は[日時](#日時)を参照してください。
|
|
509
|
+
|
|
510
|
+
### 独自クラスのオーバーライド
|
|
511
|
+
|
|
512
|
+
`#to_hexdigest_source` の実装を変更すると、そのクラスのダイジェストも変わります。永続化済みの値がある場合は、ソルト変更と同様に移行方針が必要です。属性の追加・削除も実装の変更にあたります([値オブジェクトのダイジェスト](#値オブジェクトのダイジェスト)を参照)。
|
|
513
|
+
|
|
514
|
+
クラス名を変更した場合も同様です。型はクラス名から付与されるため、リネームだけでダイジェストが変わります。
|
|
515
|
+
|
|
516
|
+
無名クラスは名前を持つ祖先クラスまで遡って型として扱われます。
|
|
517
|
+
|
|
518
|
+
## 開発
|
|
519
|
+
|
|
520
|
+
```console
|
|
521
|
+
$ bin/setup # 依存関係のインストール
|
|
522
|
+
$ bundle exec rake # RSpec + RuboCop
|
|
523
|
+
$ bundle exec rspec # テストのみ
|
|
524
|
+
$ bundle exec rubocop # 静的解析のみ
|
|
525
|
+
$ bin/console # 対話コンソール
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
テストのカバレッジはSimpleCovで計測され、`coverage/` に出力されます。
|
|
529
|
+
|
|
530
|
+
## ライセンス
|
|
531
|
+
|
|
532
|
+
MIT License. 詳細は [LICENSE.txt](LICENSE.txt) を参照してください。
|
data/Rakefile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "object"
|
|
4
|
+
|
|
5
|
+
class Array
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: 要素は#to_hexdigest_sourceではなく#to_hexdigest_inputで文字列化する。
|
|
9
|
+
# 値だけでは型が落ちるため、[:a] と ["a"]、[1] と ["1"] が同じ値になってしまう
|
|
10
|
+
#
|
|
11
|
+
# MEMO: #to_hexdigest_inputは値を#inspectで引用・エスケープ済みのため、ここでの
|
|
12
|
+
# 追加の引用は不要。引用がないと、要素の文字列表現に区切り文字(,)が含まれる
|
|
13
|
+
# 場合に内容が異なる配列同士が同じ文字列になってしまう
|
|
14
|
+
# (例: ["a,b","c"] と ["a","b,c"] が衝突する)
|
|
15
|
+
def to_hexdigest_source
|
|
16
|
+
return "[]" if empty?
|
|
17
|
+
|
|
18
|
+
map(&:to_hexdigest_input)
|
|
19
|
+
.sort
|
|
20
|
+
.join(",")
|
|
21
|
+
.prepend("[")
|
|
22
|
+
.concat("]")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
require_relative "numeric_like"
|
|
6
|
+
|
|
7
|
+
# MEMO: bigdecimalは条件付きではなく無条件に読み込む。Railsのdecimalカラムの値は常に
|
|
8
|
+
# BigDecimalであり、対応の有無が環境によって変わるとダイジェストが割れてしまうため
|
|
9
|
+
#
|
|
10
|
+
# MEMO: BigDecimal#to_rは内部表現どおりの厳密な有理数を返すため、Floatのような
|
|
11
|
+
# 十進への読み替えは不要
|
|
12
|
+
class BigDecimal
|
|
13
|
+
include ::Decentworks::HexdigestSupport::NumericLike
|
|
14
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decentworks
|
|
4
|
+
module HexdigestSupport
|
|
5
|
+
# ハッシュ値化の設定
|
|
6
|
+
#
|
|
7
|
+
# MEMO: 初期化時(Railsならconfig/initializers配下)で以下のように設定する
|
|
8
|
+
#
|
|
9
|
+
# ::Decentworks::HexdigestSupport.configure do |config|
|
|
10
|
+
# config.salt = ::Rails.application.credentials.hexdigest_salt
|
|
11
|
+
# end
|
|
12
|
+
class Configuration
|
|
13
|
+
# ハッシュ値化の入力に前置するソルト
|
|
14
|
+
attr_accessor :salt
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@salt = ""
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# 設定
|
|
23
|
+
def configuration = @configuration ||= ::Decentworks::HexdigestSupport::Configuration.new
|
|
24
|
+
|
|
25
|
+
# 設定の変更
|
|
26
|
+
#
|
|
27
|
+
# MEMO: ダイジェストの値はソルトに依存するため、永続化済みの値がある状態で
|
|
28
|
+
# ソルトを変更すると過去の値と一致しなくなる点に注意
|
|
29
|
+
def configure = yield(configuration)
|
|
30
|
+
|
|
31
|
+
# 設定のリセット(主にテスト用)
|
|
32
|
+
def reset_configuration! = @configuration = nil
|
|
33
|
+
|
|
34
|
+
# ハッシュ値化の入力に前置するソルト
|
|
35
|
+
#
|
|
36
|
+
# MEMO: 未設定(nil)はソルトなし(空文字)として扱う
|
|
37
|
+
def salt = configuration.salt.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hash"
|
|
4
|
+
|
|
5
|
+
class Data
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: DataはStructのサブクラスではないため、Struct側の実装は継承されない。
|
|
9
|
+
# メンバー名を落とさない理由はStructと同じ
|
|
10
|
+
#
|
|
11
|
+
# MEMO: 無名のDataが"Data"へ丸まる点もStructと同じ
|
|
12
|
+
def to_hexdigest_source = to_h.to_hexdigest_source
|
|
13
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
require_relative "time_like"
|
|
6
|
+
|
|
7
|
+
class Date
|
|
8
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
9
|
+
#
|
|
10
|
+
# MEMO: 日付は時刻もタイムゾーンも持たないため、Timeのような正規化は不要。
|
|
11
|
+
# ISO 8601の日付として組み立てる
|
|
12
|
+
#
|
|
13
|
+
# MEMO: 型は"Date"のまま(TimeLikeをincludeしない)。DateはTimeと違って
|
|
14
|
+
# ある一瞬ではなく1日を指すため、同一視すると意味が壊れる
|
|
15
|
+
def to_hexdigest_source = strftime("%Y-%m-%d")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DateTime
|
|
19
|
+
# MEMO: DateTimeはDateのサブクラスだが、指すものはある一瞬なのでTimeLikeへ寄せる。
|
|
20
|
+
# includeしないとDate#to_hexdigest_sourceを継承して時刻が丸ごと落ちてしまう
|
|
21
|
+
#
|
|
22
|
+
# MEMO: includeはDateより手前に入るため、Date#to_hexdigest_sourceより優先される
|
|
23
|
+
include ::Decentworks::HexdigestSupport::TimeLike
|
|
24
|
+
end
|