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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "object"
|
|
4
|
+
|
|
5
|
+
class Hash
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: キーも値と同じく#to_hexdigest_inputで正規化する。キーをそのまま文字列へ
|
|
9
|
+
# 埋めると、#to_sを実装していないオブジェクトがキーの場合にオブジェクトIDが
|
|
10
|
+
# 混入して非決定的になる。#to_hexdigest_input経由なら検出して例外になる
|
|
11
|
+
#
|
|
12
|
+
# MEMO: キー・値ともに#to_hexdigest_sourceではなく#to_hexdigest_inputを使う。
|
|
13
|
+
# 値だけでは型が落ちるため、{ a: 1 } と { "a" => 1 }、
|
|
14
|
+
# { a: 1, "a" => 2 } と { "a" => 1, a: 2 } が同じ値になってしまう
|
|
15
|
+
#
|
|
16
|
+
# MEMO: Hash#to_sには頼らず自前で文字列を組み立てる。ネイティブの#inspectの出力形式は
|
|
17
|
+
# Rubyのバージョンによって変わりうる(例: Ruby 3.4以降で"=>"の前後にスペースが
|
|
18
|
+
# 入るようになった)ため、バージョンが変わるとダイジェストの値も変わってしまう
|
|
19
|
+
def to_hexdigest_source
|
|
20
|
+
return "{}" if empty?
|
|
21
|
+
|
|
22
|
+
map { |key, value| "#{key.to_hexdigest_input}=>#{value.to_hexdigest_input}" }
|
|
23
|
+
.sort
|
|
24
|
+
.join(",")
|
|
25
|
+
.prepend("{")
|
|
26
|
+
.concat("}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decentworks
|
|
4
|
+
module HexdigestSupport
|
|
5
|
+
# 値がオブジェクトIDを含んでおり、ダイジェストが決定的にならない
|
|
6
|
+
class NonDeterministicSourceError < ::StandardError; end
|
|
7
|
+
|
|
8
|
+
# 値が自身を含んでおり、入力の組み立てが終わらない
|
|
9
|
+
class CircularReferenceError < ::StandardError; end
|
|
10
|
+
|
|
11
|
+
# ダイジェストの入力の組み立て
|
|
12
|
+
class << self
|
|
13
|
+
# 既定の#to_sが返す文字列(例: "#<Object:0x00007f9e0c0d1234>")
|
|
14
|
+
#
|
|
15
|
+
# MEMO: メソッドの定義元(#to_sのowner)ではなく結果の文字列で判定する。
|
|
16
|
+
# Proc#to_sや無名クラスのModule#to_sのように、独自の#to_sを持ちながら
|
|
17
|
+
# オブジェクトIDを含む型も拾いたいため
|
|
18
|
+
DEFAULT_TO_S_PATTERN = /\A#<.*:0x\h+/
|
|
19
|
+
|
|
20
|
+
# 組み立て中のオブジェクトを記録するキー
|
|
21
|
+
#
|
|
22
|
+
# MEMO: Thread.current[]はスレッドではなくFiber単位で値を持つ。組み立ての
|
|
23
|
+
# 途中でFiberをまたぐことはないため、これで取り違えは起きない
|
|
24
|
+
VISITING_KEY = :decentworks_hexdigest_support_visiting
|
|
25
|
+
|
|
26
|
+
# 値を引用・エスケープする
|
|
27
|
+
#
|
|
28
|
+
# MEMO: #inspectを使わない。#inspectは非ASCII文字をEncoding.default_externalが
|
|
29
|
+
# 印字可能かどうかでエスケープするか決めるため、同じ値でも実行環境の
|
|
30
|
+
# ロケール次第で異なる入力になってしまう
|
|
31
|
+
# (UTF-8環境では"あ"、US-ASCII環境では"あ")
|
|
32
|
+
#
|
|
33
|
+
# MEMO: エスケープ対象は引用符とバックスラッシュのみ。UTF-8環境の#inspectと
|
|
34
|
+
# 同じ出力になるため、制御文字を含まない値のダイジェストは変わらない
|
|
35
|
+
#
|
|
36
|
+
# MEMO: 引用しないと、値に区切り文字(:)が含まれる場合に型名との境界が
|
|
37
|
+
# 曖昧になる(例: Foo::Barの"x" と Fooの":Bar:x" が衝突する)
|
|
38
|
+
def quote(value) = %("#{value.to_s.gsub(/[\\"]/) { |char| "\\#{char}" }}")
|
|
39
|
+
|
|
40
|
+
# 値が決定的かを検査する
|
|
41
|
+
#
|
|
42
|
+
# MEMO: 既定のObject#to_sはオブジェクトIDを含むため、#to_sも
|
|
43
|
+
# #to_hexdigest_sourceも実装していないオブジェクトのダイジェストは
|
|
44
|
+
# プロセスごとに変わる。永続化した後で気付くと復旧できないため、
|
|
45
|
+
# 黙って通さず例外にする
|
|
46
|
+
#
|
|
47
|
+
# MEMO: StringとSymbolは検査しない。検査は「既定のObject#to_sへ落ちていないか」を
|
|
48
|
+
# 見るためのものだが、この2つは値そのものが文字列であり、オブジェクトIDが
|
|
49
|
+
# 混入する経路がない。除外しないと "#<User:0x00007f9e0c0d1234>" のような
|
|
50
|
+
# 正当な文字列(ログの1行や#inspectの結果を保持した値)が例外になってしまう
|
|
51
|
+
#
|
|
52
|
+
# MEMO: 裏を返すと、利用側が自分でオブジェクトを文字列化して渡した場合は検出
|
|
53
|
+
# できない。gemから見ればただの文字列であり、他の文字列と区別できないため
|
|
54
|
+
def validate_source!(source, object)
|
|
55
|
+
return if object.is_a?(::String) || object.is_a?(::Symbol)
|
|
56
|
+
return unless DEFAULT_TO_S_PATTERN.match?(source.to_s)
|
|
57
|
+
|
|
58
|
+
raise ::Decentworks::HexdigestSupport::NonDeterministicSourceError,
|
|
59
|
+
"#{object.class}の値がオブジェクトIDを含むため、ダイジェストが決定的になりません" \
|
|
60
|
+
"(#{source})。#to_hexdigest_sourceか#to_sを実装してください"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 組み立て中のオブジェクトを記録しながらブロックを実行する
|
|
64
|
+
#
|
|
65
|
+
# MEMO: 自身を含む値をそのまま辿ると再帰が終わらず、StandardErrorを継承しない
|
|
66
|
+
# SystemStackErrorになる。呼び出し側のrescueをすり抜けてプロセスを
|
|
67
|
+
# 落としてしまうため、自前で検出して例外にする
|
|
68
|
+
#
|
|
69
|
+
# MEMO: 記録するのは現在辿っている経路だけで、組み立てが終わった時点で取り除く。
|
|
70
|
+
# 同じオブジェクトが兄弟として複数回現れるのは循環ではないため、経路に
|
|
71
|
+
# 残っている場合だけを循環とみなす
|
|
72
|
+
#
|
|
73
|
+
# MEMO: 自身を含まない深いネスト(1万段など)はSystemStackErrorのまま。循環と
|
|
74
|
+
# 違って有限であり、深さの上限を決め打ちすると正当な構造まで弾いてしまう
|
|
75
|
+
def detect_circular_reference(object)
|
|
76
|
+
visiting = (::Thread.current[VISITING_KEY] ||= [])
|
|
77
|
+
raise_circular_reference(object) if visiting.include?(object.object_id)
|
|
78
|
+
|
|
79
|
+
visiting.push(object.object_id)
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
yield
|
|
83
|
+
ensure
|
|
84
|
+
visiting.pop
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def raise_circular_reference(object)
|
|
91
|
+
raise ::Decentworks::HexdigestSupport::CircularReferenceError,
|
|
92
|
+
"#{object.class}の値が自身を含んでいるため、ダイジェストを求められません。" \
|
|
93
|
+
"循環参照を取り除いてください"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class NilClass
|
|
4
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
5
|
+
#
|
|
6
|
+
# MEMO: NilClass#to_sは空文字を返すため、既定のままでは入力がNilClass:""となり、
|
|
7
|
+
# 空文字を持つ他の型(String:"")とは型でしか区別できない。#inspectと同じ
|
|
8
|
+
# "nil"を明示することで、入力を目視したときに値の不在だと分かるようにする
|
|
9
|
+
#
|
|
10
|
+
# MEMO: true / falseはTrueClass#to_s / FalseClass#to_sが"true" / "false"を返すため、
|
|
11
|
+
# 既定のObject#to_hexdigest_sourceのままで意図した入力になる
|
|
12
|
+
def to_hexdigest_source = "nil"
|
|
13
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "numeric_like"
|
|
4
|
+
|
|
5
|
+
# MEMO: Numericへ直接includeせず、具体的なクラスごとにincludeする。Numericのサブクラスには
|
|
6
|
+
# 有理数へ変換できないものがあり(Complexは実部と虚部の組なので、虚部があると
|
|
7
|
+
# #to_rがRangeErrorになる)、一律に寄せると壊れるため
|
|
8
|
+
#
|
|
9
|
+
# MEMO: Complexは対象外とする。Complex(1, 0) == 1 が真なので厳密には型で割れるが、
|
|
10
|
+
# アプリケーションで虚数を扱う場面はまれであり、対応のコストに見合わない
|
|
11
|
+
class Integer
|
|
12
|
+
include ::Decentworks::HexdigestSupport::NumericLike
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Rational
|
|
16
|
+
include ::Decentworks::HexdigestSupport::NumericLike
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Float
|
|
20
|
+
include ::Decentworks::HexdigestSupport::NumericLike
|
|
21
|
+
|
|
22
|
+
# 正規化に用いる有理数
|
|
23
|
+
#
|
|
24
|
+
# MEMO: #to_rではなく#to_sを経由して十進として読む。#to_rは2進の厳密値を返すため、
|
|
25
|
+
# 0.1が1/10ではなく3602879701896397/36028797018963968になり、
|
|
26
|
+
# BigDecimal("0.1")やRational(1, 10)と別のダイジェストになってしまう
|
|
27
|
+
#
|
|
28
|
+
# MEMO: Float#to_sは元の値へ復元できる最短の十進表記を返す。よって#==が真になる
|
|
29
|
+
# Float同士は必ず同じ有理数になり、異なるFloatが同じ有理数になることもない
|
|
30
|
+
def to_hexdigest_rational = ::Kernel.Rational(to_s)
|
|
31
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decentworks
|
|
4
|
+
module HexdigestSupport
|
|
5
|
+
# 数を表す型に共通の入力生成
|
|
6
|
+
#
|
|
7
|
+
# MEMO: Integer / Float / Rational / BigDecimalへincludeする。Rubyのクラス階層上は
|
|
8
|
+
# それぞれ別の型だが、いずれも「数」を表す点は同じなのでダイジェストの入力は
|
|
9
|
+
# 共通化する
|
|
10
|
+
module NumericLike
|
|
11
|
+
# ハッシュ値化に用いる型の識別子
|
|
12
|
+
#
|
|
13
|
+
# MEMO: 実装クラス名ではなく"Numeric"へ正規化する。Railsでは同じ数が経路によって
|
|
14
|
+
# 別のクラスで現れる(decimalカラムはBigDecimal、integerカラムやJSONの整数は
|
|
15
|
+
# Integer、JSONの小数はFloat)ため、クラス名を型にすると入力経路の違いだけで
|
|
16
|
+
# ダイジェストが割れてしまう
|
|
17
|
+
#
|
|
18
|
+
# MEMO: 「型で区別する」という本gemの原則に対する意図的な例外。TimeLikeで
|
|
19
|
+
# Time / DateTime / TimeWithZoneを"Time"へ寄せているのと同じ割り切り
|
|
20
|
+
def to_hexdigest_type = "Numeric"
|
|
21
|
+
|
|
22
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
23
|
+
#
|
|
24
|
+
# MEMO: 有理数へ寄せてから文字列化する。#to_sの表記はクラスごとに異なり
|
|
25
|
+
# (1.5は"1.5"、BigDecimal("1.5")は"0.15e1")、同じ数でも別の値になってしまう
|
|
26
|
+
#
|
|
27
|
+
# MEMO: NaNと±Infinityは有理数にできないため#to_sをそのまま使う。FloatとBigDecimalの
|
|
28
|
+
# どちらも"NaN" / "Infinity" / "-Infinity"を返すので、型を寄せても表記は揃う
|
|
29
|
+
#
|
|
30
|
+
# MEMO: NaN同士は#==がfalseになるがダイジェストは一致する。値として区別する術が
|
|
31
|
+
# ない以上、インスタンスごとに異なる入力を作るよりは同一として扱う
|
|
32
|
+
def to_hexdigest_source
|
|
33
|
+
return to_s unless finite?
|
|
34
|
+
|
|
35
|
+
::Decentworks::HexdigestSupport.format_rational(to_hexdigest_rational)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 正規化に用いる有理数
|
|
39
|
+
#
|
|
40
|
+
# MEMO: 十進として解釈すべき型(Float)はこのメソッドを上書きする
|
|
41
|
+
def to_hexdigest_rational = to_r
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 数の正規化
|
|
45
|
+
class << self
|
|
46
|
+
# 有理数を一意な文字列へ整形する
|
|
47
|
+
#
|
|
48
|
+
# MEMO: 有限小数で表せる場合は十進表記、表せない場合は分数表記にする。Rationalは
|
|
49
|
+
# 既約かつ分母が正へ正規化済みのため、どちらの表記も数に対して一意になる
|
|
50
|
+
#
|
|
51
|
+
# MEMO: 分数表記が十進表記と衝突することはない。十進表記に区切り文字(/)は
|
|
52
|
+
# 現れないため
|
|
53
|
+
def format_rational(rational)
|
|
54
|
+
return rational.numerator.to_s if rational.denominator == 1
|
|
55
|
+
|
|
56
|
+
scale = decimal_scale(rational.denominator)
|
|
57
|
+
return "#{rational.numerator}/#{rational.denominator}" unless scale
|
|
58
|
+
|
|
59
|
+
sign = rational.negative? ? "-" : ""
|
|
60
|
+
digits = (rational.numerator.abs * (10**scale / rational.denominator)).to_s.rjust(scale + 1, "0")
|
|
61
|
+
|
|
62
|
+
"#{sign}#{digits[0...-scale]}.#{digits[-scale..]}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# 有限小数で表すのに必要な小数点以下の桁数(表せない場合はnil)
|
|
68
|
+
#
|
|
69
|
+
# MEMO: 分母が2と5のべき乗の積のときだけ有限小数になる。既約分数なので、
|
|
70
|
+
# 2と5で割り切った残りが1かどうかだけで判定できる
|
|
71
|
+
#
|
|
72
|
+
# MEMO: 桁数は2と5の指数の大きい方。これが必要最小の桁数であり、末尾に0が
|
|
73
|
+
# 並ばないため表記が一意になる
|
|
74
|
+
def decimal_scale(denominator)
|
|
75
|
+
rest = denominator
|
|
76
|
+
twos = 0
|
|
77
|
+
fives = 0
|
|
78
|
+
|
|
79
|
+
while (rest % 2).zero?
|
|
80
|
+
rest /= 2
|
|
81
|
+
twos += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
while (rest % 5).zero?
|
|
85
|
+
rest /= 5
|
|
86
|
+
fives += 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
(rest == 1) ? [twos, fives].max : nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "configuration"
|
|
6
|
+
require_relative "input"
|
|
7
|
+
|
|
8
|
+
class Object
|
|
9
|
+
# MD5でハッシュ値化(16進数)
|
|
10
|
+
def to_md5_hexdigest = ::Digest::MD5.hexdigest(to_salted_hexdigest_input)
|
|
11
|
+
|
|
12
|
+
# RMD160でハッシュ値化(16進数)
|
|
13
|
+
def to_rmd160_hexdigest = ::Digest::RMD160.hexdigest(to_salted_hexdigest_input)
|
|
14
|
+
|
|
15
|
+
# SHA1でハッシュ値化(16進数)
|
|
16
|
+
def to_sha1_hexdigest = ::Digest::SHA1.hexdigest(to_salted_hexdigest_input)
|
|
17
|
+
|
|
18
|
+
# SHA256でハッシュ値化(16進数)
|
|
19
|
+
def to_sha256_hexdigest = ::Digest::SHA256.hexdigest(to_salted_hexdigest_input)
|
|
20
|
+
|
|
21
|
+
# SHA384でハッシュ値化(16進数)
|
|
22
|
+
def to_sha384_hexdigest = ::Digest::SHA384.hexdigest(to_salted_hexdigest_input)
|
|
23
|
+
|
|
24
|
+
# SHA512でハッシュ値化(16進数)
|
|
25
|
+
def to_sha512_hexdigest = ::Digest::SHA512.hexdigest(to_salted_hexdigest_input)
|
|
26
|
+
|
|
27
|
+
# MD系のデフォルトアルゴリズム
|
|
28
|
+
alias_method :to_md_hexdigest, :to_md5_hexdigest
|
|
29
|
+
|
|
30
|
+
# RMD系のデフォルトアルゴリズム
|
|
31
|
+
alias_method :to_rmd_hexdigest, :to_rmd160_hexdigest
|
|
32
|
+
|
|
33
|
+
# SHA系のデフォルトアルゴリズム
|
|
34
|
+
alias_method :to_sha_hexdigest, :to_sha256_hexdigest
|
|
35
|
+
|
|
36
|
+
# ハッシュ値化のデフォルトアルゴリズム
|
|
37
|
+
alias_method :to_hexdigest, :to_sha_hexdigest
|
|
38
|
+
|
|
39
|
+
# ソルトを前置したハッシュ値化の入力
|
|
40
|
+
#
|
|
41
|
+
# MEMO: ソルトはダイジェストを求める直前に一度だけ前置する。#to_hexdigest_inputに
|
|
42
|
+
# 含めてしまうと、配列やハッシュの要素ごとに再帰的にソルトが混入し、
|
|
43
|
+
# 構造によってソルトの出現回数が変わってしまう
|
|
44
|
+
def to_salted_hexdigest_input = "#{::Decentworks::HexdigestSupport.salt}#{to_hexdigest_input}"
|
|
45
|
+
|
|
46
|
+
# ハッシュ値化の入力(型 + 値)
|
|
47
|
+
#
|
|
48
|
+
# MEMO: #to_hexdigest_sourceは値を文字列へ射影するだけなので単射にならない。
|
|
49
|
+
# 型を添えないと :a と "a"、1 と "1" が同じ入力になり、内容の異なる
|
|
50
|
+
# オブジェクト同士が同じダイジェストになってしまう
|
|
51
|
+
#
|
|
52
|
+
# MEMO: 引用・エスケープと決定性の検査はHexdigestSupport側へ切り出している。
|
|
53
|
+
# #inspectに任せると実行環境のロケールで出力が変わるため
|
|
54
|
+
# (詳細はinput.rbのMEMOを参照)
|
|
55
|
+
#
|
|
56
|
+
# MEMO: 検査を#to_hexdigest_sourceではなく本メソッドで行うのは、ここが
|
|
57
|
+
# すべての値が通る唯一の経路であるため。オーバーライドされた
|
|
58
|
+
# #to_hexdigest_sourceが返した値も同じように検査される
|
|
59
|
+
#
|
|
60
|
+
# MEMO: 循環参照の検出も同じ理由でここに置く。要素を辿るのは各型の
|
|
61
|
+
# #to_hexdigest_sourceだが、その入り口は必ず本メソッドを通るため、
|
|
62
|
+
# 独自クラス同士が参照しあう循環も検出できる
|
|
63
|
+
def to_hexdigest_input
|
|
64
|
+
source = ::Decentworks::HexdigestSupport.detect_circular_reference(self) { to_hexdigest_source }
|
|
65
|
+
::Decentworks::HexdigestSupport.validate_source!(source, self)
|
|
66
|
+
|
|
67
|
+
"#{to_hexdigest_type}:#{::Decentworks::HexdigestSupport.quote(source)}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ハッシュ値化に用いる型の識別子
|
|
71
|
+
#
|
|
72
|
+
# MEMO: 無名クラスは#nameがnilのため、名前を持つ祖先クラスまで遡る。
|
|
73
|
+
# #ancestorsではなく#superclassを辿るのは、無名クラスがincludeしている
|
|
74
|
+
# モジュール名を型として拾ってしまうのを避けるため
|
|
75
|
+
def to_hexdigest_type
|
|
76
|
+
klass = self.class
|
|
77
|
+
klass = klass.superclass until klass.name
|
|
78
|
+
klass.name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
82
|
+
#
|
|
83
|
+
# MEMO: #to_s以外で値を指定する場合は、本メソッドをオーバーライドすること。
|
|
84
|
+
# 型の識別は#to_hexdigest_inputが担うため、オーバーライド側で
|
|
85
|
+
# 型を意識する必要はない
|
|
86
|
+
#
|
|
87
|
+
# MEMO: #to_sも本メソッドも実装していないオブジェクトは、既定のObject#to_sが
|
|
88
|
+
# 返すオブジェクトIDが値になってしまう。#to_hexdigest_inputで検査して
|
|
89
|
+
# 例外にしているため、そのままダイジェストになることはない
|
|
90
|
+
def to_hexdigest_source = to_s
|
|
91
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hash"
|
|
4
|
+
|
|
5
|
+
class Range
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: 端点は文字列化せずそのままHashに渡す。先に#to_hexdigest_sourceで
|
|
9
|
+
# 文字列にしてしまうと型が落ちて、(1..3) と ("1".."3") が同じ値になってしまう
|
|
10
|
+
#
|
|
11
|
+
# MEMO: 端点の取得に#first/#lastではなく#begin/#endを使う。#firstは始端のない範囲で、
|
|
12
|
+
# #lastは終端のない範囲でRangeErrorになるため、(1..) や (..3) を扱えない。
|
|
13
|
+
# #begin/#endは端点がなければnilを返すので、端点なしをNilClassとして表現できる
|
|
14
|
+
#
|
|
15
|
+
# MEMO: 有界な範囲では#first/#lastと#begin/#endの戻り値は一致し、キー名もfirst/lastの
|
|
16
|
+
# ままとしているため、この実装で既存のダイジェストは変わらない
|
|
17
|
+
def to_hexdigest_source
|
|
18
|
+
{
|
|
19
|
+
first: self.begin,
|
|
20
|
+
last: self.end,
|
|
21
|
+
exclude_end: exclude_end?
|
|
22
|
+
}.to_hexdigest_source
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "array"
|
|
4
|
+
|
|
5
|
+
class Set
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: 要素の文字列化はArrayへ委譲する。Array#to_hexdigest_sourceが要素を
|
|
9
|
+
# ソートするため、挿入順が違っても同じ値になる
|
|
10
|
+
#
|
|
11
|
+
# MEMO: 型は#to_hexdigest_inputが付与するため、同じ要素を持つArrayとは
|
|
12
|
+
# 別のダイジェストになる
|
|
13
|
+
def to_hexdigest_source = to_a.to_hexdigest_source
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hash"
|
|
4
|
+
|
|
5
|
+
class Struct
|
|
6
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
7
|
+
#
|
|
8
|
+
# MEMO: 値の配列(#to_a)ではなく#to_hに委譲する。メンバー名が落ちると、
|
|
9
|
+
# メンバー構成の異なるStruct同士が同じ値になってしまう
|
|
10
|
+
# (例: Struct.new(:a, :b)の(1, 2) と Struct.new(:x, :y)の(1, 2) が衝突する)
|
|
11
|
+
#
|
|
12
|
+
# MEMO: 定数へ代入していない無名のStructは#to_hexdigest_typeが"Struct"へ丸まるため、
|
|
13
|
+
# メンバー名と値が同じなら別々に生成したStruct同士でも同じダイジェストになる。
|
|
14
|
+
# 型で区別したい場合は定数へ代入して名前を与えること
|
|
15
|
+
def to_hexdigest_source = to_h.to_hexdigest_source
|
|
16
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decentworks
|
|
4
|
+
module HexdigestSupport
|
|
5
|
+
# 時刻(ある一瞬)を表す型に共通の入力生成
|
|
6
|
+
#
|
|
7
|
+
# MEMO: Time / DateTime / ActiveSupport::TimeWithZoneへincludeする。Rubyの
|
|
8
|
+
# クラス階層上は無関係な3つだが、いずれも「ある一瞬」を表す点は同じなので
|
|
9
|
+
# ダイジェストの入力は共通化する
|
|
10
|
+
module TimeLike
|
|
11
|
+
# ハッシュ値化に用いる型の識別子
|
|
12
|
+
#
|
|
13
|
+
# MEMO: 実装クラス名ではなく"Time"へ正規化する。Railsでは同じ瞬間が経路によって
|
|
14
|
+
# 別のクラスで現れる(Time.zone.nowやActiveRecordのdatetimeカラムは
|
|
15
|
+
# ActiveSupport::TimeWithZone、Time.nowやFile.mtimeはTime)ため、
|
|
16
|
+
# クラス名を型にすると入力経路の違いだけでダイジェストが割れてしまう
|
|
17
|
+
#
|
|
18
|
+
# MEMO: 「型で区別する」という本gemの原則に対する意図的な例外。Rangeの端点を
|
|
19
|
+
# first / lastというキー名に固定しているのと同じく、実装ではなく意味論に
|
|
20
|
+
# 合わせた割り切り
|
|
21
|
+
def to_hexdigest_type = "Time"
|
|
22
|
+
|
|
23
|
+
# ハッシュ値を求めるためのオリジナルの値
|
|
24
|
+
#
|
|
25
|
+
# MEMO: UTCへ変換してから文字列化する。#to_sはタイムゾーンのオフセットを含むため、
|
|
26
|
+
# 同じ瞬間を指す時刻でも実行環境やTime.zoneの設定次第で異なる値になってしまう
|
|
27
|
+
#
|
|
28
|
+
# MEMO: 精度はナノ秒で固定する。秒へ丸めると同一秒内の異なる時刻が衝突し、
|
|
29
|
+
# 小数部を可変長にすると1.0秒と1秒が異なる値になってしまう
|
|
30
|
+
#
|
|
31
|
+
# MEMO: time(stdlib)の#iso8601ではなく#strftimeで組み立てる。ネイティブの
|
|
32
|
+
# 文字列表現に依存させないという方針はHash#to_hexdigest_sourceと同じ
|
|
33
|
+
#
|
|
34
|
+
# MEMO: Timeはナノ秒より細かい精度を持ちうるが、その桁は切り捨てる。
|
|
35
|
+
# DBのtimestamp(多くはマイクロ秒)と往復させても値が変わらない粒度に揃えるため
|
|
36
|
+
#
|
|
37
|
+
# MEMO: 先に#to_timeを挟むのは、DateTimeとTimeWithZoneをTimeへ寄せるため。
|
|
38
|
+
# Time#to_timeは自身を返すので、Timeにとっては実質的に無害
|
|
39
|
+
def to_hexdigest_source = to_time.getutc.strftime("%Y-%m-%dT%H:%M:%S.%9NZ")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MEMO: active_support/timeを読み込むのは、ActiveSupport::TimeWithZoneがActiveSupportの
|
|
4
|
+
# autoload経由でしか解決できないため。内部のファイルパスに依存しない公開の入口として
|
|
5
|
+
# この行を使う
|
|
6
|
+
require "active_support/time"
|
|
7
|
+
|
|
8
|
+
require_relative "time_like"
|
|
9
|
+
|
|
10
|
+
# MEMO: TimeWithZoneはTimeのサブクラスではない(#is_a?(Time)がtrueを返すのは
|
|
11
|
+
# TimeWithZone側の偽装)ため、Timeへの拡張は届かない。明示的にincludeする
|
|
12
|
+
#
|
|
13
|
+
# MEMO: これを入れないとObject#to_hexdigest_source(=#to_s)へフォールバックし、
|
|
14
|
+
# オフセットを含む文字列が入力になる。Rails上ではTime.zone.nowやActiveRecordの
|
|
15
|
+
# datetimeカラムがこのクラスなので、実質ほぼ全ての時刻が該当する
|
|
16
|
+
#
|
|
17
|
+
# MEMO: class ... endで開き直さず.includeを呼ぶのは、万一TimeWithZoneが未解決だった場合に
|
|
18
|
+
# autoloadを潰して空のクラスを新規定義してしまうのを避けるため
|
|
19
|
+
::ActiveSupport::TimeWithZone.include(::Decentworks::HexdigestSupport::TimeLike)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hexdigest_support/version"
|
|
4
|
+
require_relative "hexdigest_support/configuration"
|
|
5
|
+
require_relative "hexdigest_support/input"
|
|
6
|
+
require_relative "hexdigest_support/object"
|
|
7
|
+
require_relative "hexdigest_support/nil_class"
|
|
8
|
+
require_relative "hexdigest_support/numeric_like"
|
|
9
|
+
require_relative "hexdigest_support/numeric"
|
|
10
|
+
require_relative "hexdigest_support/big_decimal"
|
|
11
|
+
require_relative "hexdigest_support/array"
|
|
12
|
+
require_relative "hexdigest_support/hash"
|
|
13
|
+
require_relative "hexdigest_support/range"
|
|
14
|
+
require_relative "hexdigest_support/struct"
|
|
15
|
+
require_relative "hexdigest_support/data"
|
|
16
|
+
require_relative "hexdigest_support/set"
|
|
17
|
+
require_relative "hexdigest_support/time_like"
|
|
18
|
+
require_relative "hexdigest_support/time"
|
|
19
|
+
require_relative "hexdigest_support/date"
|
|
20
|
+
require_relative "hexdigest_support/time_with_zone"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Decentworks
|
|
6
|
+
module HexdigestSupport
|
|
7
|
+
module Generators
|
|
8
|
+
# 初期化ファイルを生成するジェネレータ
|
|
9
|
+
#
|
|
10
|
+
# bin/rails generate decentworks:hexdigest_support:install
|
|
11
|
+
#
|
|
12
|
+
# MEMO: 本ファイルはlib/decentworks/hexdigest_support.rbからはrequireしない。
|
|
13
|
+
# Railsのジェネレータ探索(lib/generators配下)から呼ばれた時にだけ
|
|
14
|
+
# 読み込まれるため、railtiesを導入していない環境でも利用できる
|
|
15
|
+
#
|
|
16
|
+
# MEMO: gem本体はactivesupportに依存する(ActiveSupport::TimeWithZone対応のため)が、
|
|
17
|
+
# railtiesには依存しない。この遅延読み込みが担保しているのは後者
|
|
18
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
19
|
+
source_root ::File.expand_path("templates", __dir__)
|
|
20
|
+
|
|
21
|
+
desc "config/initializers/decentworks_hexdigest_support.rb を生成する"
|
|
22
|
+
|
|
23
|
+
class_option :salt_key,
|
|
24
|
+
type: :string,
|
|
25
|
+
default: "hexdigest_support_salt",
|
|
26
|
+
desc: "credentialsから読み出すキー名(decentworks配下)"
|
|
27
|
+
|
|
28
|
+
def create_initializer_file
|
|
29
|
+
template "decentworks_hexdigest_support.rb", "config/initializers/decentworks_hexdigest_support.rb"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def salt_key = options[:salt_key]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ハッシュ値化の設定
|
|
4
|
+
#
|
|
5
|
+
# MEMO: ソルトはダイジェストの入力へ前置される。ソルトを変更すると同じ値でも
|
|
6
|
+
# 異なるダイジェストになるため、永続化済みのダイジェストがある場合は
|
|
7
|
+
# 変更前に移行方針を検討すること
|
|
8
|
+
#
|
|
9
|
+
# MEMO: ソルトはリポジトリに平文で置かず、credentialsで管理する。
|
|
10
|
+
# EDITOR="vi" bundle exec rails credentials:edit --environment [development | test | production]
|
|
11
|
+
#
|
|
12
|
+
# decentworks:
|
|
13
|
+
# <%= salt_key %>: <ランダムな文字列(例: `bin/rails secret` の出力)>
|
|
14
|
+
::Decentworks::HexdigestSupport.configure do |config|
|
|
15
|
+
config.salt = ::Rails.application.credentials.dig(:decentworks, :<%= salt_key %>)
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Decentworks
|
|
2
|
+
module HexdigestSupport
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor salt: ::String?
|
|
5
|
+
|
|
6
|
+
def initialize: -> void
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.configuration: -> ::Decentworks::HexdigestSupport::Configuration
|
|
10
|
+
def self.configure: { (::Decentworks::HexdigestSupport::Configuration) -> void } -> void
|
|
11
|
+
def self.reset_configuration!: -> nil
|
|
12
|
+
def self.salt: -> ::String
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Decentworks
|
|
2
|
+
module HexdigestSupport
|
|
3
|
+
class NonDeterministicSourceError < ::StandardError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
DEFAULT_TO_S_PATTERN: ::Regexp
|
|
7
|
+
|
|
8
|
+
def self.quote: (untyped value) -> ::String
|
|
9
|
+
def self.validate_source!: (untyped source, untyped object) -> void
|
|
10
|
+
end
|
|
11
|
+
end
|