patient_http 1.6.1 → 1.7.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +8 -7
  3. data/CHANGELOG.md +29 -0
  4. data/README.md +539 -515
  5. data/VERSION +1 -1
  6. data/lib/patient_http/callback_args.rb +53 -48
  7. data/lib/patient_http/callback_validator.rb +11 -7
  8. data/lib/patient_http/class_helper.rb +6 -7
  9. data/lib/patient_http/client.rb +28 -22
  10. data/lib/patient_http/client_pool.rb +134 -39
  11. data/lib/patient_http/completion_executor.rb +20 -20
  12. data/lib/patient_http/configuration.rb +367 -119
  13. data/lib/patient_http/connection_endpoint.rb +150 -0
  14. data/lib/patient_http/encryptor.rb +28 -18
  15. data/lib/patient_http/error.rb +24 -18
  16. data/lib/patient_http/external_storage.rb +42 -38
  17. data/lib/patient_http/http_error.rb +30 -26
  18. data/lib/patient_http/http_headers.rb +35 -29
  19. data/lib/patient_http/immediate_retries.rb +98 -0
  20. data/lib/patient_http/inline_task_handler.rb +15 -10
  21. data/lib/patient_http/lifecycle_manager.rb +39 -40
  22. data/lib/patient_http/outgoing_request.rb +25 -23
  23. data/lib/patient_http/payload.rb +28 -26
  24. data/lib/patient_http/payload_store/active_record_store.rb +31 -34
  25. data/lib/patient_http/payload_store/base.rb +42 -46
  26. data/lib/patient_http/payload_store/file_store.rb +22 -26
  27. data/lib/patient_http/payload_store/redis_store.rb +28 -34
  28. data/lib/patient_http/payload_store/s3_store.rb +25 -28
  29. data/lib/patient_http/payload_store.rb +2 -0
  30. data/lib/patient_http/processor.rb +111 -79
  31. data/lib/patient_http/processor_observer.rb +65 -59
  32. data/lib/patient_http/rails/engine.rb +13 -8
  33. data/lib/patient_http/redirect_error.rb +50 -41
  34. data/lib/patient_http/redirect_helper.rb +38 -38
  35. data/lib/patient_http/request.rb +70 -46
  36. data/lib/patient_http/request_error.rb +47 -42
  37. data/lib/patient_http/request_helper.rb +142 -119
  38. data/lib/patient_http/request_preparer.rb +13 -10
  39. data/lib/patient_http/request_task.rb +113 -84
  40. data/lib/patient_http/request_template.rb +87 -64
  41. data/lib/patient_http/response.rb +58 -52
  42. data/lib/patient_http/response_reader.rb +66 -65
  43. data/lib/patient_http/secret_manager.rb +34 -30
  44. data/lib/patient_http/secret_reference.rb +33 -26
  45. data/lib/patient_http/synchronous_executor.rb +67 -95
  46. data/lib/patient_http/task_handler.rb +23 -19
  47. data/lib/patient_http/time_helper.rb +8 -8
  48. data/lib/patient_http.rb +311 -186
  49. data/patient_http.gemspec +3 -2
  50. metadata +21 -5
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PatientHttp
4
- # Handles encoding and decoding of HTTP response bodies for storage.
4
+ # Encodes request and response bodies so they can be serialized to JSON.
5
5
  #
6
- # This class provides compression and encoding strategies for different content types
7
- # to optimize storage and transmission of response data.
6
+ # Large text bodies are compressed with gzip, and binary bodies are encoded
7
+ # with Base64.
8
+ #
9
+ # @api private
8
10
  class Payload
9
- # @return [Symbol] the encoding type
11
+ # @return [Symbol] The encoding type.
10
12
  attr_reader :encoding
11
13
 
12
- # @return [String] the encoded data
14
+ # @return [String] The encoded data.
13
15
  attr_reader :encoded_value
14
16
 
15
- # @return [String, nil] the character set (if applicable)
17
+ # @return [String, nil] The character set (if applicable).
16
18
  attr_reader :charset
17
19
 
18
20
  class << self
19
21
  # Reconstructs a Payload from a hash representation.
20
22
  #
21
- # @param hash [Hash, nil] hash with "encoding" and "value" keys
22
- # @return [Payload, nil] reconstructed payload or nil if hash is invalid
23
+ # @param hash [Hash, nil] Hash with "encoding" and "value" keys.
24
+ # @return [Payload, nil] Reconstructed payload or nil if hash is invalid.
23
25
  def load(hash)
24
26
  return nil if hash.nil? || hash["value"].nil?
25
27
 
@@ -33,9 +35,9 @@ module PatientHttp
33
35
  # claims is text but that does not hold text is encoded as binary as
34
36
  # well, because the serialized form must survive JSON encoding.
35
37
  #
36
- # @param value [String] the value to encode
37
- # @param mimetype [String, nil] the MIME type of the content
38
- # @return [Array(Symbol, String, String), nil] [encoding, encoded_value, charset] or nil if value is nil
38
+ # @param value [String] The value to encode.
39
+ # @param mimetype [String, nil] The MIME type of the content.
40
+ # @return [Array(Symbol, String, String), nil] [encoding, encoded_value, charset] or nil if value is nil.
39
41
  def encode(value, mimetype)
40
42
  return nil if value.nil?
41
43
 
@@ -49,10 +51,10 @@ module PatientHttp
49
51
 
50
52
  # Decodes an encoded value based on its encoding type.
51
53
  #
52
- # @param encoded_value [String] the encoded data
53
- # @param encoding [Symbol] the encoding type (:text, :binary, :gzipped)
54
- # @param charset [String, nil] the character set (if applicable)
55
- # @return [String, nil] the decoded value or nil if encoded_value is nil
54
+ # @param encoded_value [String] The encoded data.
55
+ # @param encoding [Symbol] The encoding type (:text, :binary, :gzipped).
56
+ # @param charset [String, nil] The character set (if applicable).
57
+ # @return [String, nil] The decoded value or nil if encoded_value is nil.
56
58
  def decode(encoded_value, encoding, charset)
57
59
  return nil if encoded_value.nil?
58
60
 
@@ -70,10 +72,10 @@ module PatientHttp
70
72
 
71
73
  private
72
74
 
73
- # Encode a text value, compressing it when that makes it smaller.
75
+ # Encodes a text value, compressing it when that makes it smaller.
74
76
  #
75
- # @param value [String] the text to encode
76
- # @return [Array(Symbol, String, String)] [encoding, encoded_value, charset]
77
+ # @param value [String] The text to encode.
78
+ # @return [Array(Symbol, String, String)] [encoding, encoded_value, charset].
77
79
  def encode_text(value)
78
80
  return [:text, value, value.encoding.name] if value.bytesize < 4096
79
81
 
@@ -85,13 +87,13 @@ module PatientHttp
85
87
  end
86
88
  end
87
89
 
88
- # Whether a value can be serialized as text. JSON encoding converts a
90
+ # Returns whether a value can be serialized as text. JSON encoding converts a
89
91
  # string to UTF-8, so the value must either be valid text in its own
90
92
  # encoding or hold bytes that are already valid UTF-8. A body still
91
93
  # carrying a content encoding the reader could not decode holds neither,
92
94
  # even though its MIME type names a text type.
93
95
  #
94
- # @param value [String] the value to check
96
+ # @param value [String] The value to check.
95
97
  # @return [Boolean]
96
98
  def text?(value)
97
99
  return value.valid_encoding? unless value.encoding == Encoding::BINARY
@@ -117,7 +119,7 @@ module PatientHttp
117
119
  end
118
120
  end
119
121
 
120
- # Return the value as a UTF-8 encoded string if possible. If the value cannot
122
+ # Returns the value as a UTF-8 encoded string if possible. If the value cannot
121
123
  # be converted to UTF-8, return it in the response charset or ASCII-8BIT.
122
124
  #
123
125
  # Encoding strategy:
@@ -150,9 +152,9 @@ module PatientHttp
150
152
 
151
153
  # Initializes a new Payload.
152
154
  #
153
- # @param encoding [Symbol] the encoding type
154
- # @param encoded_value [String] the encoded data
155
- # @param charset [String, nil] the character set (if applicable)
155
+ # @param encoding [Symbol] The encoding type.
156
+ # @param encoded_value [String] The encoded data.
157
+ # @param charset [String, nil] The character set (if applicable).
156
158
  def initialize(encoding, encoded_value, charset)
157
159
  @encoded_value = encoded_value
158
160
  @encoding = encoding
@@ -161,14 +163,14 @@ module PatientHttp
161
163
 
162
164
  # Returns the decoded value.
163
165
  #
164
- # @return [String, nil] the decoded data
166
+ # @return [String, nil] The decoded data.
165
167
  def value
166
168
  self.class.decode(encoded_value, encoding, charset)
167
169
  end
168
170
 
169
171
  # Converts to a hash representation for serialization.
170
172
  #
171
- # @return [Hash] hash with "encoding" and "value" keys
173
+ # @return [Hash] Hash with "encoding" and "value" keys.
172
174
  def as_json
173
175
  {
174
176
  "encoding" => encoding.to_s,
@@ -4,32 +4,31 @@ require_relative "base"
4
4
 
5
5
  module PatientHttp
6
6
  module PayloadStore
7
- # ActiveRecord-based payload store for production deployments.
7
+ # A payload store that keeps payloads as JSON in a database table. Use it in
8
+ # production when you want payloads in your database.
8
9
  #
9
- # Stores payloads as JSON in a database table. This store is recommended
10
- # when you need database-backed storage with transactional guarantees.
10
+ # Active Record is responsible for connection pooling and thread safety.
11
+ # The store requires the `patient_http_payloads` table.
11
12
  #
12
- # Thread-safe: ActiveRecord handles connection pooling and thread safety.
13
- #
14
- # @example Configuration
15
- # require "patient_http/payload_store/active_record_store"
13
+ # @example Register an Active Record store
16
14
  # config.register_payload_store(:database, adapter: :active_record)
17
15
  #
18
- # @example With custom model
16
+ # @example Register a store with a custom model
19
17
  # config.register_payload_store(:database, adapter: :active_record,
20
18
  # model: MyApp::PayloadRecord
21
19
  # )
22
20
  class ActiveRecordStore < Base
23
21
  Base.register :active_record, self
24
22
 
25
- # ActiveRecord model for payload storage.
23
+ # The default Active Record model for stored payloads.
26
24
  #
27
- # Defined in this file to avoid loading ActiveRecord until explicitly required.
28
- # The table must be created using the migration provided by this gem.
25
+ # The model is defined in this file, so Active Record loads only when
26
+ # the store is used. Create the table with the migration from this gem.
29
27
  #
30
- # @example Install migrations in a Rails app
31
- # rails patient_http:install:migrations
32
- # rails db:migrate
28
+ # @example Install the migration in a Rails app
29
+ # # Add require "patient_http/rails/engine" to an initializer.
30
+ # bin/rails patient_http:install:migrations
31
+ # bin/rails db:migrate
33
32
  class Payload < ::ActiveRecord::Base
34
33
  self.table_name = "patient_http_payloads"
35
34
  self.primary_key = "key"
@@ -37,23 +36,23 @@ module PatientHttp
37
36
  scope :older_than, ->(time) { where(created_at: nil...time) }
38
37
  end
39
38
 
40
- # @return [Class] The ActiveRecord model class used for storage
39
+ # @return [Class] The Active Record model for stored payloads.
41
40
  attr_reader :model
42
41
 
43
- # Initialize a new ActiveRecord store.
42
+ # Creates an Active Record store.
44
43
  #
45
- # @param model [Class] ActiveRecord model class to use for storage.
46
- # Defaults to PatientHttp::PayloadStore::ActiveRecordStore::Payload.
47
- # Custom models must have: key (string PK), data (text), timestamps
44
+ # @param model [Class, nil] The Active Record model. If `nil`, {Payload}
45
+ # applies. A custom model must have a `key` string primary key, a `data`
46
+ # text column, and timestamps.
48
47
  def initialize(model: nil)
49
48
  @model = model || Payload
50
49
  end
51
50
 
52
- # Store pre-serialized JSON string directly in the database.
51
+ # Stores a JSON string in the database.
53
52
  #
54
- # @param key [String] Unique key (used as primary key)
55
- # @param json [String] Pre-serialized JSON string
56
- # @return [String] The key
53
+ # @param key [String] The unique key. The primary key is this key.
54
+ # @param json [String] The serialized JSON.
55
+ # @return [String] The key.
57
56
  def store_json(key, json)
58
57
  now = Time.current
59
58
 
@@ -68,10 +67,10 @@ module PatientHttp
68
67
  key
69
68
  end
70
69
 
71
- # Fetch data from the database.
70
+ # Fetches stored data.
72
71
  #
73
- # @param key [String] The key to fetch
74
- # @return [Hash, nil] The stored data or nil if not found
72
+ # @param key [String] The key.
73
+ # @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
75
74
  def fetch(key)
76
75
  record = @model.find_by(key: key)
77
76
  return nil unless record
@@ -79,21 +78,19 @@ module PatientHttp
79
78
  JSON.parse(record.data)
80
79
  end
81
80
 
82
- # Delete a payload from the database.
83
- #
84
- # Idempotent - does not raise if record doesn't exist.
81
+ # Deletes stored data. Doesn't raise an error if the key doesn't exist.
85
82
  #
86
- # @param key [String] The key to delete
87
- # @return [Boolean] true
83
+ # @param key [String] The key.
84
+ # @return [Boolean] `true`.
88
85
  def delete(key)
89
86
  @model.where(key: key).delete_all
90
87
  true
91
88
  end
92
89
 
93
- # Check if a payload exists.
90
+ # Returns whether a payload exists.
94
91
  #
95
- # @param key [String] The key to check
96
- # @return [Boolean] true if the payload exists
92
+ # @param key [String] The key.
93
+ # @return [Boolean] `true` if the payload exists.
97
94
  def exists?(key)
98
95
  @model.exists?(key: key)
99
96
  end
@@ -4,15 +4,15 @@ require "securerandom"
4
4
 
5
5
  module PatientHttp
6
6
  module PayloadStore
7
- # Abstract base class for payload stores.
7
+ # The abstract base class for payload stores.
8
8
  #
9
- # Payload stores provide external storage for Request and Response objects
10
- # that exceed the configured size threshold. This keeps job arguments
11
- # small while allowing large payloads to be processed.
9
+ # A payload store holds serialized payloads that are larger than
10
+ # `payload_store_threshold`, so that job arguments stay small.
12
11
  #
13
- # Subclasses must implement the abstract methods: store, fetch, and delete.
12
+ # Subclasses must implement {#store_json}, {#fetch}, and {#delete}, and be
13
+ # thread-safe.
14
14
  #
15
- # @example Creating a custom store
15
+ # @example Create a custom store
16
16
  # class MyStore < PatientHttp::PayloadStore::Base
17
17
  # register :my_store, self
18
18
  #
@@ -21,8 +21,8 @@ module PatientHttp
21
21
  # @mutex = Mutex.new
22
22
  # end
23
23
  #
24
- # def store(key, data)
25
- # @mutex.synchronize { @connection.set(key, JSON.generate(data)) }
24
+ # def store_json(key, json)
25
+ # @mutex.synchronize { @connection.set(key, json) }
26
26
  # key
27
27
  # end
28
28
  #
@@ -41,10 +41,10 @@ module PatientHttp
41
41
  # end
42
42
  class Base
43
43
  class << self
44
- # Register a payload store adapter.
44
+ # Registers a payload store adapter.
45
45
  #
46
- # @param name [Symbol] Unique identifier for this adapter
47
- # @param klass [Class] The adapter class
46
+ # @param name [Symbol] The unique adapter name.
47
+ # @param klass [Class] The adapter class.
48
48
  # @return [void]
49
49
  def register(name, klass)
50
50
  registry_mutex.synchronize do
@@ -52,22 +52,22 @@ module PatientHttp
52
52
  end
53
53
  end
54
54
 
55
- # Look up a registered adapter by name.
55
+ # Returns a registered adapter class.
56
56
  #
57
- # @param name [Symbol, String] The adapter name
58
- # @return [Class, nil] The adapter class or nil if not found
57
+ # @param name [Symbol, String] The adapter name.
58
+ # @return [Class, nil] The adapter class, or `nil` if it isn't registered.
59
59
  def lookup(name)
60
60
  registry_mutex.synchronize do
61
61
  registry[name.to_sym]
62
62
  end
63
63
  end
64
64
 
65
- # Create a new store instance from a registered adapter.
65
+ # Creates a store from a registered adapter.
66
66
  #
67
- # @param name [Symbol, String] The adapter name
68
- # @param options [Hash] Options to pass to the adapter constructor
69
- # @return [Base] A new store instance
70
- # @raise [ArgumentError] If the adapter is not registered
67
+ # @param name [Symbol, String] The adapter name.
68
+ # @param options [Hash] The options for the adapter constructor.
69
+ # @return [Base] The store.
70
+ # @raise [ArgumentError] If the adapter isn't registered.
71
71
  def create(name, **options)
72
72
  klass = lookup(name)
73
73
  raise ArgumentError, "Unknown payload store adapter: #{name.inspect}" unless klass
@@ -75,9 +75,9 @@ module PatientHttp
75
75
  klass.new(**options)
76
76
  end
77
77
 
78
- # List all registered adapter names.
78
+ # Returns the names of all registered adapters.
79
79
  #
80
- # @return [Array<Symbol>] Registered adapter names
80
+ # @return [Array<Symbol>] The adapter names.
81
81
  def registered_adapters
82
82
  registry_mutex.synchronize do
83
83
  registry.keys
@@ -95,53 +95,49 @@ module PatientHttp
95
95
  end
96
96
  end
97
97
 
98
- # Store data with the given key.
98
+ # Stores a hash as JSON. This method calls {#store_json}.
99
99
  #
100
- # @param key [String] Unique key for this data
101
- # @param data [Hash] The data to store (will be serialized as JSON)
102
- # @return [String] The key
100
+ # @param key [String] The unique key.
101
+ # @param data [Hash] The data to store.
102
+ # @return [String] The key.
103
103
  def store(key, data)
104
104
  json = JSON.generate(data)
105
105
  store_json(key, json)
106
106
  end
107
107
 
108
- # Store pre-serialized JSON data with the given key.
108
+ # Stores a JSON string. {ExternalStorage} calls this method, so
109
+ # subclasses must implement it.
109
110
  #
110
- # Subclasses that serialize in #store should override this to write
111
- # the string directly, avoiding double serialization.
112
- #
113
- # @param key [String] Unique key for this data
114
- # @param json [String] Pre-serialized JSON string
115
- # @return [String] The key
116
- # @raise [NotImplementedError] Subclasses must implement this method
111
+ # @param key [String] The unique key.
112
+ # @param json [String] The serialized JSON.
113
+ # @return [String] The key.
114
+ # @raise [NotImplementedError] If the subclass doesn't implement it.
117
115
  def store_json(key, json)
118
116
  raise NotImplementedError, "#{self.class.name} must implement #store_json"
119
117
  end
120
118
 
121
- # Fetch data by key.
119
+ # Fetches stored data.
122
120
  #
123
- # @param key [String] The key to fetch
124
- # @return [Hash, nil] The stored data or nil if not found
125
- # @raise [NotImplementedError] Subclasses must implement this method
121
+ # @param key [String] The key.
122
+ # @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
123
+ # @raise [NotImplementedError] If the subclass doesn't implement it.
126
124
  def fetch(key)
127
125
  raise NotImplementedError, "#{self.class.name} must implement #fetch"
128
126
  end
129
127
 
130
- # Delete data by key.
131
- #
132
- # This method should be idempotent - deleting a non-existent key
133
- # should not raise an error.
128
+ # Deletes stored data. The method must be idempotent: deleting a key that
129
+ # doesn't exist must not raise an error.
134
130
  #
135
- # @param key [String] The key to delete
136
- # @return [Boolean] true
137
- # @raise [NotImplementedError] Subclasses must implement this method
131
+ # @param key [String] The key.
132
+ # @return [Boolean] `true`.
133
+ # @raise [NotImplementedError] If the subclass doesn't implement it.
138
134
  def delete(key)
139
135
  raise NotImplementedError, "#{self.class.name} must implement #delete"
140
136
  end
141
137
 
142
- # Generate a unique key for storing data.
138
+ # Generates a unique key.
143
139
  #
144
- # @return [String] A UUID key
140
+ # @return [String] A UUID.
145
141
  def generate_key
146
142
  SecureRandom.uuid
147
143
  end
@@ -4,37 +4,35 @@ require "fileutils"
4
4
 
5
5
  module PatientHttp
6
6
  module PayloadStore
7
- # File-based payload store for testing and development.
7
+ # A payload store that keeps payloads as JSON files in a directory. Use it
8
+ # only for development and tests, because hosts don't share the files. In
9
+ # production, use another store.
8
10
  #
9
- # Stores payloads as JSON files in a directory. This store is intended
10
- # for local development and testing only - use Redis or S3 stores for
11
- # production deployments.
11
+ # The store is thread-safe.
12
12
  #
13
- # Thread-safe through mutex synchronization.
14
- #
15
- # @example Configuration
13
+ # @example Register a file store
16
14
  # config.register_payload_store(:files, adapter: :file, directory: "/tmp/payloads")
17
15
  class FileStore < Base
18
16
  Base.register :file, self
19
17
 
20
- # @return [String] The directory where payload files are stored
18
+ # @return [String] The directory for the payload files.
21
19
  attr_reader :directory
22
20
 
23
- # Initialize a new file store.
21
+ # Creates a file store.
24
22
  #
25
- # @param directory [String] Directory for storing payload files.
26
- # Defaults to Dir.tmpdir. Will be created if it doesn't exist.
23
+ # @param directory [String] The directory for the payload files. The
24
+ # default is `Dir.tmpdir`. The directory is created if it doesn't exist.
27
25
  def initialize(directory: nil)
28
26
  @directory = directory || Dir.tmpdir
29
27
  @mutex = Mutex.new
30
28
  FileUtils.mkdir_p(@directory)
31
29
  end
32
30
 
33
- # Store pre-serialized JSON string directly to a file.
31
+ # Writes a JSON string to a file.
34
32
  #
35
- # @param key [String] Unique key (used as filename)
36
- # @param json [String] Pre-serialized JSON string
37
- # @return [String] The key
33
+ # @param key [String] The unique key. The file name is the key.
34
+ # @param json [String] The serialized JSON.
35
+ # @return [String] The key.
38
36
  def store_json(key, json)
39
37
  path = file_path(key)
40
38
  @mutex.synchronize do
@@ -43,10 +41,10 @@ module PatientHttp
43
41
  key
44
42
  end
45
43
 
46
- # Fetch data from a JSON file.
44
+ # Fetches stored data.
47
45
  #
48
- # @param key [String] The key to fetch
49
- # @return [Hash, nil] The stored data or nil if not found
46
+ # @param key [String] The key.
47
+ # @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
50
48
  def fetch(key)
51
49
  path = file_path(key)
52
50
  @mutex.synchronize do
@@ -56,12 +54,10 @@ module PatientHttp
56
54
  end
57
55
  end
58
56
 
59
- # Delete a payload file.
60
- #
61
- # Idempotent - does not raise if file doesn't exist.
57
+ # Deletes stored data. Doesn't raise an error if the key doesn't exist.
62
58
  #
63
- # @param key [String] The key to delete
64
- # @return [Boolean] true
59
+ # @param key [String] The key.
60
+ # @return [Boolean] `true`.
65
61
  def delete(key)
66
62
  path = file_path(key)
67
63
  @mutex.synchronize do
@@ -72,10 +68,10 @@ module PatientHttp
72
68
  true
73
69
  end
74
70
 
75
- # Check if a payload exists.
71
+ # Returns whether a payload exists.
76
72
  #
77
- # @param key [String] The key to check
78
- # @return [Boolean] true if the payload exists
73
+ # @param key [String] The key.
74
+ # @return [Boolean] `true` if the payload exists.
79
75
  def exists?(key)
80
76
  @mutex.synchronize do
81
77
  File.exist?(file_path(key))
@@ -2,37 +2,32 @@
2
2
 
3
3
  module PatientHttp
4
4
  module PayloadStore
5
- # Redis-based payload store for production deployments.
5
+ # A payload store that keeps payloads as JSON strings in Redis. Use it in
6
+ # production when several processes share payloads.
6
7
  #
7
- # Stores payloads as JSON strings in Redis. This store is recommended
8
- # for production environments where multiple processes need to share
9
- # payload data.
8
+ # The client must respond to `set`, `get`, `del`, and `exists`, like a client
9
+ # from the `redis` gem. The client is responsible for thread safety.
10
10
  #
11
- # Thread-safe: Redis clients handle their own thread safety.
12
- #
13
- # The client must respond to `set`, `get`, `del`, and `exists` (the
14
- # interface provided by the `redis` gem).
15
- #
16
- # @example Configuration with direct Redis client
11
+ # @example Register a Redis store
17
12
  # redis = Redis.new(url: ENV["REDIS_URL"])
18
13
  # config.register_payload_store(:redis, adapter: :redis, redis: redis, ttl: 86400)
19
14
  class RedisStore < Base
20
15
  Base.register :redis, self
21
16
 
22
- # @return [String] The key prefix used for all stored payloads
17
+ # @return [String] The prefix for the keys of all stored payloads.
23
18
  attr_reader :key_prefix
24
19
 
25
- # @return [Float, nil] TTL in seconds for stored payloads
20
+ # @return [Float, nil] The time to live in seconds for stored payloads.
26
21
  attr_reader :ttl
27
22
 
28
- # Initialize a new Redis store.
23
+ # Creates a Redis store.
29
24
  #
30
- # @param redis [Object] Redis client instance. Required.
31
- # @param ttl [Float, nil] Time-to-live in seconds for stored payloads.
32
- # Supports fractional seconds (e.g., 0.5 for 500ms). If nil, payloads do not expire.
33
- # @param key_prefix [String] Prefix for all Redis keys.
34
- # Defaults to "patient_http:payloads:"
35
- # @raise [ArgumentError] If redis client is not provided
25
+ # @param redis [Object] The Redis client.
26
+ # @param ttl [Float, nil] The time to live in seconds for stored payloads.
27
+ # Fractions are allowed, for example `0.5` for 500 milliseconds. If `nil`,
28
+ # payloads don't expire.
29
+ # @param key_prefix [String] The prefix for all Redis keys.
30
+ # @raise [ArgumentError] If the Redis client is missing.
36
31
  def initialize(redis:, ttl: nil, key_prefix: nil)
37
32
  raise ArgumentError, "redis client is required" unless redis
38
33
 
@@ -41,11 +36,12 @@ module PatientHttp
41
36
  @key_prefix = key_prefix || "patient_http:payloads:"
42
37
  end
43
38
 
44
- # Store pre-serialized JSON string directly in Redis.
39
+ # Stores a JSON string in Redis.
45
40
  #
46
- # @param key [String] Unique key (appended to key_prefix)
47
- # @param json [String] Pre-serialized JSON string
48
- # @return [String] The key
41
+ # @param key [String] The unique key. The Redis key is the key prefix and
42
+ # this key.
43
+ # @param json [String] The serialized JSON.
44
+ # @return [String] The key.
49
45
  def store_json(key, json)
50
46
  full_key = key_with_prefix(key)
51
47
 
@@ -58,10 +54,10 @@ module PatientHttp
58
54
  key
59
55
  end
60
56
 
61
- # Fetch data from Redis.
57
+ # Fetches stored data.
62
58
  #
63
- # @param key [String] The key to fetch
64
- # @return [Hash, nil] The stored data or nil if not found
59
+ # @param key [String] The key.
60
+ # @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
65
61
  def fetch(key)
66
62
  full_key = key_with_prefix(key)
67
63
  json = @redis.get(full_key)
@@ -70,22 +66,20 @@ module PatientHttp
70
66
  JSON.parse(json)
71
67
  end
72
68
 
73
- # Delete a payload from Redis.
74
- #
75
- # Idempotent - does not raise if key doesn't exist.
69
+ # Deletes stored data. Doesn't raise an error if the key doesn't exist.
76
70
  #
77
- # @param key [String] The key to delete
78
- # @return [Boolean] true
71
+ # @param key [String] The key.
72
+ # @return [Boolean] `true`.
79
73
  def delete(key)
80
74
  full_key = key_with_prefix(key)
81
75
  @redis.del(full_key)
82
76
  true
83
77
  end
84
78
 
85
- # Check if a payload exists.
79
+ # Returns whether a payload exists.
86
80
  #
87
- # @param key [String] The key to check
88
- # @return [Boolean] true if the payload exists
81
+ # @param key [String] The key.
82
+ # @return [Boolean] `true` if the payload exists.
89
83
  def exists?(key)
90
84
  full_key = key_with_prefix(key)
91
85
  @redis.exists(full_key) > 0