jobpayload 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.
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ # One problem found while checking a fixture.
5
+ #
6
+ # Finding codes are a stable API: their meaning does not change within 0.1.x.
7
+ Finding = Struct.new(
8
+ :code,
9
+ :fixture,
10
+ :job_class,
11
+ :argument_path,
12
+ :message,
13
+ :exception_class,
14
+ :exception_message,
15
+ :cause_chain, # Array of {"class" => String, "message" => String}, outermost first
16
+ :backtraces, # Array of Array<String>, parallel to cause_chain; never serialized to JSON
17
+ keyword_init: true
18
+ )
19
+
20
+ class Finding
21
+ # code => [name, kind]. kind is :breaking, :inconclusive or :fatal.
22
+ CODES = {
23
+ "AJP001" => ["fixture_invalid", :fatal],
24
+ "AJP101" => ["unknown_job_class", :breaking],
25
+ "AJP102" => ["job_deserialize_failed", :breaking],
26
+ "AJP201" => ["argument_deserialize_failed", :breaking],
27
+ "AJP202" => ["globalid_record_missing", :inconclusive],
28
+ "AJP900" => ["environment_error", :fatal]
29
+ }.freeze
30
+
31
+ # Severity strings used in JSON output.
32
+ SEVERITIES = { breaking: "error", inconclusive: "warning", fatal: "fatal" }.freeze
33
+
34
+ def self.build(code:, fixture:, message:, job_class: nil, argument_path: nil, exception: nil)
35
+ raise ArgumentError, "unknown finding code #{code}" unless CODES.key?(code)
36
+
37
+ chain = exception ? ExceptionClassifier.cause_chain(exception) : []
38
+ new(
39
+ code: code,
40
+ fixture: fixture,
41
+ job_class: job_class,
42
+ argument_path: argument_path,
43
+ message: message,
44
+ exception_class: exception && ExceptionClassifier.class_name(exception),
45
+ exception_message: exception && ExceptionClassifier.safe_message(exception),
46
+ cause_chain: chain.map { |e| { "class" => ExceptionClassifier.class_name(e), "message" => ExceptionClassifier.safe_message(e) } },
47
+ backtraces: chain.map { |e| Array(e.backtrace) }
48
+ )
49
+ end
50
+
51
+ def name
52
+ CODES.fetch(code).first
53
+ end
54
+
55
+ def kind
56
+ CODES.fetch(code).last
57
+ end
58
+
59
+ def severity
60
+ SEVERITIES.fetch(kind)
61
+ end
62
+
63
+ def sort_key
64
+ [fixture.to_s, code, argument_path.to_s, message.to_s]
65
+ end
66
+
67
+ def to_json_hash
68
+ {
69
+ "code" => code,
70
+ "name" => name,
71
+ "severity" => severity,
72
+ "fixture" => fixture,
73
+ "job_class" => job_class,
74
+ "argument_path" => argument_path,
75
+ "message" => message,
76
+ "exception_class" => exception_class,
77
+ "exception_message" => exception_message,
78
+ "cause_chain" => cause_chain
79
+ }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module JobPayload
6
+ # A parsed fixture file (schema v1).
7
+ #
8
+ # Fixture.parse never raises for bad content: it returns an Invalid value
9
+ # carrying the reason, which the checker reports as AJP001.
10
+ class Fixture
11
+ Invalid = Struct.new(:name, :path, :reason)
12
+
13
+ attr_reader :name, :path, :job, :source
14
+
15
+ def initialize(name:, path:, job:, source:)
16
+ @name = name
17
+ @path = path
18
+ @job = job
19
+ @source = source
20
+ end
21
+
22
+ def job_class
23
+ job["job_class"]
24
+ end
25
+
26
+ def self.parse(path)
27
+ basename = File.basename(path, ".json")
28
+ reason = catch(:invalid) do
29
+ data = parse_json(path)
30
+ validate!(data, basename)
31
+ return new(name: data["name"], path: path, job: data["job"], source: data["source"])
32
+ end
33
+ # The file name and OS error messages may carry bytes that are not
34
+ # UTF-8; Invalid values end up in JSON output.
35
+ Invalid.new(JobPayload.scrub_utf8(basename), path, JobPayload.scrub_utf8(reason))
36
+ end
37
+
38
+ def self.parse_json(path)
39
+ content = File.binread(path).force_encoding(Encoding::UTF_8)
40
+ invalid!("file is not valid UTF-8") unless content.valid_encoding?
41
+ data = JSON.parse(content, max_nesting: false)
42
+ # Some json versions decode a lone surrogate escape ("\udc80") into
43
+ # bytes that are not valid UTF-8.
44
+ invalid!("file contains a string escape that is not valid UTF-8") unless valid_strings?(data)
45
+ data
46
+ rescue JSON::ParserError => e
47
+ invalid!("invalid JSON: #{e.message.lines.first.to_s.strip}")
48
+ rescue SystemCallError => e
49
+ invalid!("cannot read file: #{e.message}")
50
+ end
51
+
52
+ # Iterative, so that deeply nested fixtures cannot exhaust the stack here.
53
+ def self.valid_strings?(data)
54
+ pending = [data]
55
+ until pending.empty?
56
+ value = pending.pop
57
+ case value
58
+ when String then return false unless value.valid_encoding?
59
+ when Array then pending.concat(value)
60
+ when Hash
61
+ pending.concat(value.keys)
62
+ pending.concat(value.values)
63
+ end
64
+ end
65
+ true
66
+ end
67
+
68
+ def self.validate!(data, basename)
69
+ invalid!("top-level JSON value must be an object") unless data.is_a?(Hash)
70
+ invalid!("missing jobpayload_schema") unless data.key?("jobpayload_schema")
71
+ schema = data["jobpayload_schema"]
72
+ unless schema == FIXTURE_SCHEMA && schema.is_a?(Integer)
73
+ invalid!("unsupported jobpayload_schema #{schema.inspect} (expected #{FIXTURE_SCHEMA})")
74
+ end
75
+ invalid!("invalid file name #{basename.inspect}.json") unless JobPayload.valid_name?(basename)
76
+
77
+ name = data["name"]
78
+ invalid!("name must be a string") unless name.is_a?(String)
79
+ invalid!("invalid name #{name.inspect}") unless JobPayload.valid_name?(name)
80
+ invalid!("name #{name.inspect} does not match file name #{basename}.json") unless name == basename
81
+ invalid!("source must be an object") if data.key?("source") && !data["source"].is_a?(Hash)
82
+
83
+ job = data["job"]
84
+ invalid!("job must be an object") unless job.is_a?(Hash)
85
+ invalid!("job.job_class must be a non-empty string") unless job["job_class"].is_a?(String) && !job["job_class"].empty?
86
+ invalid!("job.arguments must be an array") unless job["arguments"].is_a?(Array)
87
+ end
88
+
89
+ def self.invalid!(reason)
90
+ throw :invalid, reason
91
+ end
92
+ private_class_method :parse_json, :valid_strings?, :validate!, :invalid!
93
+ end
94
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ # Resolves --fixtures (a directory or a single file) into fixture files, in a
5
+ # stable order that does not depend on directory enumeration order.
6
+ #
7
+ # Listing (.files) only touches the file system, so it runs before the host
8
+ # application boots; parsing (.parse) needs the json library and runs after
9
+ # boot, so the application's own json version is the one that gets loaded.
10
+ module FixtureLoader
11
+ module_function
12
+
13
+ def files(path)
14
+ files =
15
+ if File.directory?(path)
16
+ Dir.children(path).select { |entry| entry.end_with?(".json") }.sort.map { |entry| File.join(path, entry) }
17
+ .select { |file| File.file?(file) }
18
+ elsif File.file?(path)
19
+ [path]
20
+ else
21
+ raise ConfigurationError, "fixture path not found: #{path}"
22
+ end
23
+ raise ConfigurationError, "no fixture files (*.json) found in #{path}" if files.empty?
24
+
25
+ files
26
+ end
27
+
28
+ def parse(files)
29
+ files.map { |file| Fixture.parse(file) }
30
+ end
31
+
32
+ def call(path)
33
+ parse(files(path))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module JobPayload
6
+ module Formatter
7
+ # Machine-readable output, schema v1. Field names and meanings are stable
8
+ # for 0.1.x; see README "JSON output".
9
+ module JSON
10
+ SCHEMA_VERSION = 1
11
+
12
+ module_function
13
+
14
+ def call(result, fail_on_inconclusive: false, debug: false) # rubocop:disable Lint/UnusedMethodArgument
15
+ document = {
16
+ "schema_version" => SCHEMA_VERSION,
17
+ "tool_version" => VERSION,
18
+ "status" => result.status(fail_on_inconclusive: fail_on_inconclusive),
19
+ "summary" => result.summary,
20
+ "fixtures" => result.fixture_results.map { |r| { "name" => r.name, "status" => r.status.to_s } },
21
+ "findings" => result.findings.map(&:to_json_hash)
22
+ }
23
+ Canonical.pretty(document)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ module Formatter
5
+ # Human-readable output. Ordering is fully determined by fixture name,
6
+ # finding code and argument path.
7
+ module Text
8
+ LABELS = { breaking: "breaking", inconclusive: "inconclusive", fatal: "fatal" }.freeze
9
+
10
+ NOTES = {
11
+ "AJP202" => "This does not by itself prove a payload compatibility break."
12
+ }.freeze
13
+
14
+ module_function
15
+
16
+ def call(result, fail_on_inconclusive: false, debug: false) # rubocop:disable Lint/UnusedMethodArgument
17
+ blocks = result.fixture_results.flat_map do |fixture_result|
18
+ if fixture_result.findings.empty?
19
+ ["PASS #{fixture_result.name}\n"]
20
+ else
21
+ fixture_result.findings.sort_by(&:sort_key).map { |finding| finding_block(finding, debug: debug) }
22
+ end
23
+ end
24
+ out = +""
25
+ blocks.each_with_index do |block, index|
26
+ previous = blocks[index - 1] if index.positive?
27
+ # Keep consecutive PASS lines together; separate multi-line blocks.
28
+ out << "\n" if previous && (multiline?(block) || multiline?(previous))
29
+ out << block
30
+ end
31
+ out << "\n" << summary(result)
32
+ end
33
+
34
+ def multiline?(block)
35
+ block.count("\n") > 1
36
+ end
37
+
38
+ def finding_block(finding, debug:)
39
+ lines = ["#{finding.code} #{LABELS.fetch(finding.kind)} #{finding.fixture}", ""]
40
+ if finding.job_class
41
+ lines += ["Job:", " #{finding.job_class}", ""]
42
+ end
43
+ if finding.argument_path
44
+ lines += ["Argument:", " #{finding.argument_path}", ""]
45
+ end
46
+ lines << finding.message
47
+ if (note = NOTES[finding.code])
48
+ lines += ["", note]
49
+ end
50
+ if (root = finding.cause_chain.last)
51
+ lines += ["", "Cause:", " #{root['class']}: #{root['message']}"]
52
+ end
53
+ lines += debug_lines(finding) if debug
54
+ "#{lines.join("\n")}\n"
55
+ end
56
+
57
+ def debug_lines(finding)
58
+ return [] if finding.cause_chain.empty?
59
+
60
+ lines = ["", "Cause chain:"]
61
+ finding.cause_chain.each_with_index do |cause, index|
62
+ lines << " #{index + 1}. #{cause['class']}: #{cause['message']}"
63
+ finding.backtraces.fetch(index, []).each { |frame| lines << " #{frame}" }
64
+ end
65
+ lines
66
+ end
67
+
68
+ def summary(result)
69
+ counts = result.summary
70
+ lines = [
71
+ "#{counts['fixtures']} fixtures checked",
72
+ "#{counts['compatible']} compatible",
73
+ "#{counts['incompatible']} incompatible",
74
+ "#{counts['inconclusive']} inconclusive"
75
+ ]
76
+ tool_errors = counts["tool_errors"]
77
+ lines << "#{tool_errors} tool error#{'s' unless tool_errors == 1}" if tool_errors.positive?
78
+ "#{lines.join("\n")}\n"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ # Output formatters for `check` results.
5
+ module Formatter
6
+ autoload :Text, "jobpayload/formatter/text"
7
+ autoload :JSON, "jobpayload/formatter/json"
8
+
9
+ FORMATS = %w[text json].freeze
10
+
11
+ def self.for(name)
12
+ case name
13
+ when "text" then Text
14
+ when "json" then JSON
15
+ else raise UsageError, "unknown format #{name.inspect} (expected one of: #{FORMATS.join(', ')})"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ # Outcome of a `check` run.
5
+ class Result
6
+ # Per-fixture outcome. status is :pass, :fail, :inconclusive or
7
+ # :tool_error (the worst finding wins).
8
+ FixtureResult = Struct.new(:name, :path, :findings) do
9
+ def status
10
+ kinds = findings.map(&:kind)
11
+ if kinds.include?(:fatal) then :tool_error
12
+ elsif kinds.include?(:breaking) then :fail
13
+ elsif kinds.include?(:inconclusive) then :inconclusive
14
+ else :pass
15
+ end
16
+ end
17
+ end
18
+
19
+ EXIT_SUCCESS = 0
20
+ EXIT_FAILURE = 1
21
+ EXIT_ERROR = 2
22
+
23
+ attr_reader :fixture_results
24
+
25
+ def initialize(fixture_results)
26
+ @fixture_results = fixture_results
27
+ end
28
+
29
+ # All findings, sorted by fixture, code, then argument path.
30
+ def findings
31
+ fixture_results.flat_map(&:findings).sort_by(&:sort_key)
32
+ end
33
+
34
+ def summary
35
+ counts = fixture_results.map(&:status).tally
36
+ {
37
+ "fixtures" => fixture_results.size,
38
+ "compatible" => counts.fetch(:pass, 0),
39
+ "incompatible" => counts.fetch(:fail, 0),
40
+ "inconclusive" => counts.fetch(:inconclusive, 0),
41
+ "tool_errors" => counts.fetch(:tool_error, 0)
42
+ }
43
+ end
44
+
45
+ # "pass", "fail" or "tool_error".
46
+ def status(fail_on_inconclusive: false)
47
+ statuses = fixture_results.map(&:status)
48
+ if statuses.include?(:tool_error) then "tool_error"
49
+ elsif statuses.include?(:fail) then "fail"
50
+ elsif fail_on_inconclusive && statuses.include?(:inconclusive) then "fail"
51
+ else "pass"
52
+ end
53
+ end
54
+
55
+ def exit_code(fail_on_inconclusive: false)
56
+ { "tool_error" => EXIT_ERROR, "fail" => EXIT_FAILURE, "pass" => EXIT_SUCCESS }
57
+ .fetch(status(fail_on_inconclusive: fail_on_inconclusive))
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module JobPayload
6
+ # Turns snapshot cases into fixture files.
7
+ #
8
+ # The source of truth is ActiveJob::Base#serialize. Volatile Active Job
9
+ # metadata is replaced with fixed values so repeated snapshots are
10
+ # byte-for-byte identical; every other key (including keys added by a job's
11
+ # own #serialize override) is kept as-is.
12
+ class Snapshotter
13
+ CANONICAL_JOB_ID = "00000000-0000-0000-0000-000000000000"
14
+ CANONICAL_TIME = "2000-01-01T00:00:00.000000000Z"
15
+
16
+ # Volatile keys and the stable values they are replaced with. Keys are only
17
+ # replaced when present, so the shape of #serialize is never changed.
18
+ NORMALIZED = {
19
+ "job_id" => CANONICAL_JOB_ID,
20
+ "provider_job_id" => nil,
21
+ "enqueued_at" => CANONICAL_TIME,
22
+ "executions" => 0,
23
+ "exception_executions" => {}
24
+ }.freeze
25
+
26
+ Entry = Struct.new(:name, :path, :status) # status: :created, :identical, :updated, :skipped
27
+
28
+ def initialize(registry:, output_dir:, update: false)
29
+ @registry = registry
30
+ @output_dir = output_dir
31
+ @update = update
32
+ end
33
+
34
+ # Generates every fixture in memory first, so a failing case leaves the
35
+ # fixture directory untouched, then writes them in name order.
36
+ def call
37
+ documents = @registry.cases.map { |kase| [kase.name, Canonical.generate(build_document(kase))] }
38
+ begin
39
+ FileUtils.mkdir_p(@output_dir)
40
+ rescue SystemCallError => e
41
+ raise ConfigurationError, "cannot create output directory #{@output_dir}: #{e.message}"
42
+ end
43
+ documents.map { |name, content| write(name, content) }
44
+ end
45
+
46
+ def build_document(kase)
47
+ {
48
+ "jobpayload_schema" => FIXTURE_SCHEMA,
49
+ "name" => kase.name,
50
+ "source" => self.class.source_versions,
51
+ "job" => serialize_case(kase)
52
+ }
53
+ end
54
+
55
+ def self.normalize(job_data)
56
+ data = job_data.to_h { |key, value| [key.to_s, value] }
57
+ NORMALIZED.each { |key, value| data[key] = Canonical.deep_copy(value) if data.key?(key) }
58
+ data["scheduled_at"] = CANONICAL_TIME if data.key?("scheduled_at") && !data["scheduled_at"].nil?
59
+ # Round-trip through JSON so the fixture holds exactly what a queue would store.
60
+ JSON.parse(JSON.generate(data))
61
+ end
62
+
63
+ def self.source_versions
64
+ {
65
+ "ruby_version" => RUBY_VERSION,
66
+ "active_job_version" => active_job_version,
67
+ "rails_version" => rails_version,
68
+ "jobpayload_version" => VERSION
69
+ }
70
+ end
71
+
72
+ def self.active_job_version
73
+ spec = Gem.loaded_specs["activejob"]
74
+ return spec.version.to_s if spec
75
+ return ::ActiveJob.version.to_s if defined?(::ActiveJob) && ::ActiveJob.respond_to?(:version)
76
+
77
+ nil
78
+ end
79
+
80
+ def self.rails_version
81
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:version)
82
+
83
+ ::Rails.version.to_s
84
+ end
85
+
86
+ private
87
+
88
+ # Runs the case block and serializes the job it returns. Any error is
89
+ # reported against the case, never as an internal error.
90
+ def serialize_case(kase)
91
+ job = kase.block.call
92
+ unless job.is_a?(::ActiveJob::Base)
93
+ raise ConfigurationError,
94
+ "fixture #{kase.name.inspect} (#{kase.location}) must return an ActiveJob::Base instance, got #{job.class}"
95
+ end
96
+
97
+ self.class.normalize(job.serialize)
98
+ rescue ConfigurationError
99
+ raise
100
+ rescue StandardError, ScriptError => e
101
+ raise ConfigurationError, "fixture #{kase.name.inspect} (#{kase.location}) raised #{e.class}: #{e.message}"
102
+ end
103
+
104
+ def write(name, content)
105
+ path = File.join(@output_dir, "#{name}.json")
106
+ status =
107
+ if !File.exist?(path)
108
+ :created
109
+ elsif File.binread(path) == content.b
110
+ :identical
111
+ elsif @update
112
+ :updated
113
+ else
114
+ :skipped
115
+ end
116
+ Canonical.atomic_write(path, content) if %i[created updated].include?(status)
117
+ Entry.new(name, path, status)
118
+ rescue SystemCallError => e
119
+ raise ConfigurationError, "cannot write fixture #{path}: #{e.message}"
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobPayload
4
+ VERSION = "0.1.0"
5
+ end
data/lib/jobpayload.rb ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point for the jobpayload gem.
4
+ #
5
+ # This file is intentionally lightweight: requiring it must not load Active Job
6
+ # (or any other part of the host application's framework). Active Job APIs are
7
+ # referenced lazily, only after the host application has been booted, so that
8
+ # custom serializer registration and initializer order are left untouched.
9
+ require_relative "jobpayload/version"
10
+ require_relative "jobpayload/errors"
11
+ require_relative "jobpayload/case_registry"
12
+
13
+ module JobPayload
14
+ autoload :CLI, "jobpayload/cli"
15
+ autoload :Boot, "jobpayload/boot"
16
+ autoload :Canonical, "jobpayload/canonical"
17
+ autoload :Snapshotter, "jobpayload/snapshotter"
18
+ autoload :Fixture, "jobpayload/fixture"
19
+ autoload :FixtureLoader, "jobpayload/fixture_loader"
20
+ autoload :Checker, "jobpayload/checker"
21
+ autoload :Result, "jobpayload/result"
22
+ autoload :Finding, "jobpayload/finding"
23
+ autoload :ExceptionClassifier, "jobpayload/exception_classifier"
24
+ autoload :Formatter, "jobpayload/formatter"
25
+
26
+ # Pattern every fixture name must match. Rejects path separators, leading
27
+ # dots and anything else that could escape the fixture directory.
28
+ NAME_PATTERN = /\A[A-Za-z0-9][A-Za-z0-9._-]*\z/
29
+
30
+ # Fixture wrapper schema version written by `snapshot` and read by `check`.
31
+ FIXTURE_SCHEMA = 1
32
+
33
+ # Defines snapshot cases. Called from the cases file (test/jobpayload_cases.rb
34
+ # by default) while `jobpayload snapshot` is loading it.
35
+ #
36
+ # JobPayload.define do
37
+ # fixture "billing-money-v1" do
38
+ # BillingJob.new(Money.new(1_250, "USD"))
39
+ # end
40
+ # end
41
+ def self.define(&block)
42
+ CaseRegistry.current.define(&block)
43
+ end
44
+
45
+ def self.valid_name?(name)
46
+ name.is_a?(String) && NAME_PATTERN.match?(name) && !name.include?("..")
47
+ end
48
+
49
+ # Returns +string+ as valid UTF-8 (invalid bytes become U+FFFD), for text
50
+ # from outside jobpayload (exception messages, file names, paths) that ends
51
+ # up in JSON output.
52
+ def self.scrub_utf8(string)
53
+ if [Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII].include?(string.encoding)
54
+ string.dup.force_encoding(Encoding::UTF_8).scrub("\uFFFD")
55
+ else
56
+ string.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
57
+ end
58
+ rescue EncodingError # no converter (for example UTF-7): keep the bytes
59
+ string.b.force_encoding(Encoding::UTF_8).scrub("\uFFFD")
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jobpayload
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - cottondesu
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: activejob
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '7.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ description: |
33
+ jobpayload snapshots Active Job serialized job data (ActiveJob::Base#serialize) into
34
+ deterministic JSON fixtures and checks that the current application can still
35
+ deserialize them (ActiveJob::Base.deserialize and ActiveJob::Arguments.deserialize)
36
+ without ever performing the job.
37
+ executables:
38
+ - jobpayload
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - CHANGELOG.md
43
+ - LICENSE
44
+ - README.md
45
+ - exe/jobpayload
46
+ - lib/jobpayload.rb
47
+ - lib/jobpayload/boot.rb
48
+ - lib/jobpayload/canonical.rb
49
+ - lib/jobpayload/case_registry.rb
50
+ - lib/jobpayload/checker.rb
51
+ - lib/jobpayload/cli.rb
52
+ - lib/jobpayload/errors.rb
53
+ - lib/jobpayload/exception_classifier.rb
54
+ - lib/jobpayload/finding.rb
55
+ - lib/jobpayload/fixture.rb
56
+ - lib/jobpayload/fixture_loader.rb
57
+ - lib/jobpayload/formatter.rb
58
+ - lib/jobpayload/formatter/json.rb
59
+ - lib/jobpayload/formatter/text.rb
60
+ - lib/jobpayload/result.rb
61
+ - lib/jobpayload/snapshotter.rb
62
+ - lib/jobpayload/version.rb
63
+ homepage: https://github.com/cottondesu/jobpayload
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://github.com/cottondesu/jobpayload
68
+ source_code_uri: https://github.com/cottondesu/jobpayload
69
+ changelog_uri: https://github.com/cottondesu/jobpayload/blob/main/CHANGELOG.md
70
+ rubygems_mfa_required: 'true'
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '3.3'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.6.9
86
+ specification_version: 4
87
+ summary: Checks that previously serialized Active Job payloads still deserialize on
88
+ new code.
89
+ test_files: []