konsierge-idempotency 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 19645d6689c36335eeda896fb47d1f1e6024b96dabeb2f25de89eb8313fec296
4
+ data.tar.gz: 9b3d7c0f1087efea8b0d86608f0e95ca3f3e81fa2d86158b16a31a86384770ba
5
+ SHA512:
6
+ metadata.gz: 43bd43515a2ec9de227b4626417d7566f4c0027a25f0976eeacc2f16fbd1317f69fe25f0e0b5fadb2efb03ba8f7cce762ab6f3ee73e59ff6f93f726a551b5133
7
+ data.tar.gz: d9da719592ff885131f663e06b6a1ce3a803a07299960bb1f8368530b68672ec862ab91fa357dedade95b381a52639dc0666388f31dc37ebc051a16309efe6dc
data/Appraisals ADDED
@@ -0,0 +1,49 @@
1
+ appraise 'rails-7-0' do
2
+ group :test do
3
+ remove_gem 'rails'
4
+ remove_gem 'sqlite3'
5
+ end
6
+
7
+ gem 'rails', '~> 7.0'
8
+ gem 'sqlite3', '~> 1.4'
9
+ end
10
+
11
+ appraise 'rails-7-1' do
12
+ group :test do
13
+ remove_gem 'rails'
14
+ remove_gem 'sqlite3'
15
+ end
16
+
17
+ gem 'rails', '~> 7.1'
18
+ gem 'sqlite3', '~> 1.4'
19
+ end
20
+
21
+ appraise 'rails-7-2' do
22
+ group :test do
23
+ remove_gem 'rails'
24
+ remove_gem 'sqlite3'
25
+ end
26
+
27
+ gem 'rails', '~> 7.2'
28
+ gem 'sqlite3', '~> 1.4'
29
+ end
30
+
31
+ appraise 'rails-8-0' do
32
+ group :test do
33
+ remove_gem 'rails'
34
+ remove_gem 'sqlite3'
35
+ end
36
+
37
+ gem 'rails', '~> 8.0'
38
+ gem 'sqlite3', '~> 2.1'
39
+ end
40
+
41
+ appraise 'rails-8-1' do
42
+ group :test do
43
+ remove_gem 'rails'
44
+ remove_gem 'sqlite3'
45
+ end
46
+
47
+ gem 'rails', '~> 8.1'
48
+ gem 'sqlite3', '~> 2.1'
49
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.1] - 2026-05-28
4
+
5
+ - Switched idempotency digest from MD5 to SHA256.
6
+ - Extracted object normalization to a dedicated `Normalizer` class.
7
+ - Added configurable concern key column via `self.idempotency_key_column`.
8
+ - Added top-level `Idempotentable` include alias for models.
9
+ - Added Rails generator `rails g konsierge:idempotency Model [column_name]`:
10
+ - migration with conditional `add_column` and `add_index`
11
+ - model update with `include Idempotentable` and `self.idempotency_key_column = :column_name`
12
+ - Added Rails `railtie` integration for generator loading.
13
+ - Added dummy Rails app and appraisal matrix (`rails 7.0 .. 8.1`) for compatibility checks.
14
+ - Split and expanded test coverage for core, concern, and generator behavior.
15
+
16
+ ## [0.1.0] - 2026-05-27
17
+
18
+ - Initial release.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "konsierge-idempotency" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["i@golifox.ru"](mailto:"i@golifox.ru").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 David Rybolovlev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Konsierge::Idempotency
2
+
3
+ Deterministic idempotency key builder for Ruby objects, plus ActiveRecord concern for model-level keys.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ Konsierge::Idempotency.key(
9
+ amount: 100,
10
+ currency: :usd,
11
+ issued_at: Time.utc(2026, 5, 27, 10, 0, 0, 123_456)
12
+ )
13
+ # => "sha256 hash string"
14
+ ```
15
+
16
+ Instance API is also available:
17
+
18
+ ```ruby
19
+ service = Konsierge::Idempotency.new(payload)
20
+ service.key
21
+ ```
22
+
23
+ For ActiveRecord models:
24
+
25
+ ```ruby
26
+ class Payment < ApplicationRecord
27
+ include Idempotentable
28
+ self.idempotency_key_column = :idempotency_key
29
+
30
+ def self.idempotent_columns
31
+ %i[external_id amount currency]
32
+ end
33
+ end
34
+
35
+ # before_create assigns `idempotency_key`
36
+ ```
37
+
38
+ If model has no `idempotency_key` column, concern does not assign anything.
39
+
40
+ ## Rails Generator
41
+
42
+ ```bash
43
+ rails g konsierge:idempotency Order
44
+ rails g konsierge:idempotency order request_fingerprint
45
+ ```
46
+
47
+ Generator behavior:
48
+ - creates migration that adds string column (default `idempotency_key`) if it does not exist
49
+ - adds index for that column if it does not exist
50
+ - updates model with `include Idempotentable`
51
+ - sets `self.idempotency_key_column = :column_name` only for custom column names
52
+
53
+ ## Development
54
+
55
+ After checking out the repo, run `bin/setup` to install dependencies. Then run `bundle exec rspec` to run tests.
56
+ For Rails matrix validation, run `bundle exec appraisal install` and `bundle exec appraisal rspec`.
57
+
58
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
59
+
60
+ ## License
61
+
62
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
63
+
64
+ ## Code of Conduct
65
+
66
+ Everyone interacting in the Konsierge::Idempotency project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://gitlab.konsierge.com/digitalbox/gem/konsierge-idempotency/-/blob/master/CODE_OF_CONDUCT.md?ref_type=heads).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'konsierge-idempotency'
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,31 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rails', '~> 7.0'
6
+ gem 'sqlite3', '~> 1.4'
7
+
8
+ group :development, :test do
9
+ gem 'dotenv'
10
+ gem 'irb'
11
+ gem 'mutex_m'
12
+ gem 'oj'
13
+ gem 'rack-cors'
14
+ gem 'rake'
15
+ end
16
+
17
+ group :development do
18
+ gem 'appraisal'
19
+ gem 'rubocop'
20
+ gem 'rubocop-performance'
21
+ gem 'rubocop-rake'
22
+ gem 'rubocop-rspec'
23
+ end
24
+
25
+ group :test do
26
+ gem 'rspec-rails'
27
+ gem 'simplecov'
28
+ gem 'stub_env'
29
+ end
30
+
31
+ gemspec path: '../'
@@ -0,0 +1,31 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rails', '~> 7.1'
6
+ gem 'sqlite3', '~> 1.4'
7
+
8
+ group :development, :test do
9
+ gem 'dotenv'
10
+ gem 'irb'
11
+ gem 'mutex_m'
12
+ gem 'oj'
13
+ gem 'rack-cors'
14
+ gem 'rake'
15
+ end
16
+
17
+ group :development do
18
+ gem 'appraisal'
19
+ gem 'rubocop'
20
+ gem 'rubocop-performance'
21
+ gem 'rubocop-rake'
22
+ gem 'rubocop-rspec'
23
+ end
24
+
25
+ group :test do
26
+ gem 'rspec-rails'
27
+ gem 'simplecov'
28
+ gem 'stub_env'
29
+ end
30
+
31
+ gemspec path: '../'
@@ -0,0 +1,31 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rails', '~> 7.2'
6
+ gem 'sqlite3', '~> 1.4'
7
+
8
+ group :development, :test do
9
+ gem 'dotenv'
10
+ gem 'irb'
11
+ gem 'mutex_m'
12
+ gem 'oj'
13
+ gem 'rack-cors'
14
+ gem 'rake'
15
+ end
16
+
17
+ group :development do
18
+ gem 'appraisal'
19
+ gem 'rubocop'
20
+ gem 'rubocop-performance'
21
+ gem 'rubocop-rake'
22
+ gem 'rubocop-rspec'
23
+ end
24
+
25
+ group :test do
26
+ gem 'rspec-rails'
27
+ gem 'simplecov'
28
+ gem 'stub_env'
29
+ end
30
+
31
+ gemspec path: '../'
@@ -0,0 +1,31 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rails', '~> 8.0'
6
+ gem 'sqlite3', '~> 2.1'
7
+
8
+ group :development, :test do
9
+ gem 'dotenv'
10
+ gem 'irb'
11
+ gem 'mutex_m'
12
+ gem 'oj'
13
+ gem 'rack-cors'
14
+ gem 'rake'
15
+ end
16
+
17
+ group :development do
18
+ gem 'appraisal'
19
+ gem 'rubocop'
20
+ gem 'rubocop-performance'
21
+ gem 'rubocop-rake'
22
+ gem 'rubocop-rspec'
23
+ end
24
+
25
+ group :test do
26
+ gem 'rspec-rails'
27
+ gem 'simplecov'
28
+ gem 'stub_env'
29
+ end
30
+
31
+ gemspec path: '../'
@@ -0,0 +1,31 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'rails', '~> 8.1'
6
+ gem 'sqlite3', '~> 2.1'
7
+
8
+ group :development, :test do
9
+ gem 'dotenv'
10
+ gem 'irb'
11
+ gem 'mutex_m'
12
+ gem 'oj'
13
+ gem 'rack-cors'
14
+ gem 'rake'
15
+ end
16
+
17
+ group :development do
18
+ gem 'appraisal'
19
+ gem 'rubocop'
20
+ gem 'rubocop-performance'
21
+ gem 'rubocop-rake'
22
+ gem 'rubocop-rspec'
23
+ end
24
+
25
+ group :test do
26
+ gem 'rspec-rails'
27
+ gem 'simplecov'
28
+ gem 'stub_env'
29
+ end
30
+
31
+ gemspec path: '../'
@@ -0,0 +1,70 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Konsierge
5
+ module Generators
6
+ class IdempotencyGenerator < Rails::Generators::NamedBase
7
+ include Rails::Generators::Migration
8
+
9
+ DEFAULT_COLUMN_KEY = 'idempotency_key'.freeze
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+ desc 'Adds idempotency column + index and updates model for Idempotentable concern'
13
+
14
+ argument :column_name, type: :string, default: DEFAULT_COLUMN_KEY, banner: 'column_name'
15
+
16
+ def create_migration_file
17
+ migration_template 'add_idempotency_column.rb.tt', "db/migrate/add_#{column_name}_to_#{table_name}.rb"
18
+ end
19
+
20
+ def update_model
21
+ unless File.exist?(model_full_path)
22
+ say_status :skip, "Model file not found: #{model_path}", :yellow
23
+ return
24
+ end
25
+
26
+ source = File.read(model_full_path)
27
+ lines = []
28
+ lines << " include Idempotentable\n" unless source.match?(/^\s*include\s+Idempotentable\s*$/)
29
+
30
+ unless default_key_column?
31
+ if source.match?(/^\s*self\.idempotency_key_column\s*=\s*:.+$/)
32
+ gsub_file model_path,
33
+ /^\s*self\.idempotency_key_column\s*=\s*:.+$/,
34
+ " self.idempotency_key_column = :#{column_name}"
35
+ else
36
+ lines << "\n self.idempotency_key_column = :#{column_name}\n"
37
+ end
38
+ end
39
+
40
+ return if lines.empty?
41
+
42
+ insert_into_file model_path, lines.join, after: /^\s*class\s+.+\n/
43
+ end
44
+
45
+ def self.next_migration_number(_dirname)
46
+ @migration_number ||= Time.now.utc.strftime('%Y%m%d%H%M%S').to_i
47
+ @migration_number += 1
48
+ @migration_number.to_s
49
+ end
50
+
51
+ private
52
+
53
+ def table_name(*)
54
+ name.to_s.underscore.pluralize.tr('/', '_')
55
+ end
56
+
57
+ def model_path
58
+ File.join('app/models', "#{file_path}.rb")
59
+ end
60
+
61
+ def model_full_path
62
+ File.join(destination_root, model_path)
63
+ end
64
+
65
+ def default_key_column?
66
+ column_name.to_s == DEFAULT_COLUMN_KEY
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,11 @@
1
+ class <%= "Add#{column_name.camelize}To#{table_name.camelize}" %> < ActiveRecord::Migration[<%= "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" %>]
2
+ def change
3
+ unless column_exists?(:<%= table_name %>, :<%= column_name %>)
4
+ add_column :<%= table_name %>, :<%= column_name %>, :string
5
+ end
6
+
7
+ unless index_exists?(:<%= table_name %>, :<%= column_name %>)
8
+ add_index :<%= table_name %>, :<%= column_name %>
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ require 'konsierge/idempotency/idempotentable'
2
+
3
+ Idempotentable = Konsierge::Idempotency::Idempotentable
@@ -0,0 +1,34 @@
1
+ require 'active_support/concern'
2
+
3
+ module Konsierge
4
+ class Idempotency
5
+ module Idempotentable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :idempotency_key_column, instance_accessor: false, default: :idempotency_key
10
+ before_create :set_idempotency_key
11
+ end
12
+
13
+ def idempotent_attributes
14
+ attributes.slice(*self.class.idempotent_columns.map(&:to_s))
15
+ end
16
+
17
+ private
18
+
19
+ def set_idempotency_key
20
+ key_column = self.class.idempotency_key_column.to_s
21
+ return unless has_attribute?(key_column)
22
+ return unless self[key_column].to_s.empty?
23
+
24
+ self[key_column] = Konsierge::Idempotency.key(idempotent_attributes)
25
+ end
26
+
27
+ class_methods do
28
+ def idempotent_columns
29
+ raise Konsierge::Idempotency::Error, "Please implement #idempotent_columns method in #{name}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ require 'date'
2
+
3
+ module Konsierge
4
+ class Idempotency
5
+ class Normalizer
6
+ class << self
7
+ def normalize(...)
8
+ new(...).normalize
9
+ end
10
+ end
11
+
12
+ attr_reader :object
13
+
14
+ def initialize(object)
15
+ @object = object
16
+ end
17
+
18
+ def normalize
19
+ normalize_object(object)
20
+ end
21
+
22
+ private
23
+
24
+ def normalize_object(value)
25
+ case value
26
+ when Hash
27
+ normalized_object = value.each_with_object({}) do |(k, v), hash|
28
+ hash[k.to_s] = normalize_object(v)
29
+ end
30
+ normalized_object.sort.to_h
31
+ when Array
32
+ value.map { |item| normalize_object(item) }
33
+ when Symbol
34
+ value.to_s
35
+ when Time, DateTime
36
+ value.iso8601(6)
37
+ when Date
38
+ value.iso8601
39
+ else
40
+ value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/railtie'
2
+
3
+ module Konsierge
4
+ class Idempotency
5
+ class Railtie < ::Rails::Railtie
6
+ generators do
7
+ require_relative '../../generators/konsierge/idempotency_generator'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Konsierge
2
+ class Idempotency
3
+ VERSION = '0.1.1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ require 'digest/sha2'
2
+ require 'json'
3
+
4
+ module Konsierge
5
+ class Idempotency
6
+ class Error < StandardError; end
7
+
8
+ class << self
9
+ def key(...)
10
+ new(...).key
11
+ end
12
+ end
13
+
14
+ attr_reader :object
15
+
16
+ def initialize(object)
17
+ @object = object
18
+ end
19
+
20
+ def key
21
+ normalized_object = Normalizer.normalize(object)
22
+ Digest::SHA256.hexdigest(JSON.generate(normalized_object))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ require 'pathname'
2
+ require 'zeitwerk'
3
+
4
+ lib_root = Pathname.new(__dir__).expand_path.to_s
5
+
6
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
7
+ loader.push_dir(lib_root)
8
+ loader.ignore(__FILE__)
9
+ loader.ignore("#{lib_root}/generators")
10
+ loader.ignore("#{lib_root}/idempotentdable.rb")
11
+ loader.ignore("#{lib_root}/konsierge/idempotency/{version,idempotentdable}.rb")
12
+ loader.setup
13
+
14
+ require 'konsierge/idempotency'
15
+ require 'konsierge/idempotency/railtie' if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: konsierge-idempotency
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - David Rybolovlev
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: Provides object normalization, idempotency key generation and an optional
27
+ ActiveRecord concern.
28
+ email:
29
+ - i@golifox.ru
30
+ executables:
31
+ - konsierge-idempotency
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - Appraisals
36
+ - CHANGELOG.md
37
+ - CODE_OF_CONDUCT.md
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - exe/konsierge-idempotency
42
+ - gemfiles/.bundle/config
43
+ - gemfiles/rails_7_0.gemfile
44
+ - gemfiles/rails_7_1.gemfile
45
+ - gemfiles/rails_7_2.gemfile
46
+ - gemfiles/rails_8_0.gemfile
47
+ - gemfiles/rails_8_1.gemfile
48
+ - lib/generators/konsierge/idempotency_generator.rb
49
+ - lib/generators/konsierge/templates/add_idempotency_column.rb.tt
50
+ - lib/idempotentable.rb
51
+ - lib/konsierge-idempotency.rb
52
+ - lib/konsierge/idempotency.rb
53
+ - lib/konsierge/idempotency/idempotentable.rb
54
+ - lib/konsierge/idempotency/normalizer.rb
55
+ - lib/konsierge/idempotency/railtie.rb
56
+ - lib/konsierge/idempotency/version.rb
57
+ homepage: https://gitlab.konsierge.com/digitalbox/gem/konsierge-idempotency
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://gitlab.konsierge.com/digitalbox/gem/konsierge-idempotency
62
+ source_code_uri: https://gitlab.konsierge.com/digitalbox/gem/konsierge-idempotency/-/tree/master?ref_type=heads
63
+ changelog_uri: https://gitlab.konsierge.com/digitalbox/gem/konsierge-idempotency/-/blob/master/CHANGELOG.md?ref_type=heads
64
+ rubygems_mfa_required: 'false'
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '2.7'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 4.0.13
80
+ specification_version: 4
81
+ summary: Deterministic idempotency key builder for Ruby objects.
82
+ test_files: []