Module: Familia::Settings

Included in:
Familia, DataType, Horreum::DefinitionMethods
Defined in:
lib/familia/settings.rb

Overview

Familia::Settings

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#current_key_version(val = nil) ⇒ Object



74
75
76
77
# File 'lib/familia/settings.rb', line 74

def current_key_version(val = nil)
  @current_key_version = val if val
  @current_key_version
end

#default_expiration(val = nil) ⇒ Object



52
53
54
55
# File 'lib/familia/settings.rb', line 52

def default_expiration(val = nil)
  @default_expiration = val unless val.nil?
  @default_expiration
end

#delim(val = nil) ⇒ Object



37
38
39
40
# File 'lib/familia/settings.rb', line 37

def delim(val = nil)
  @delim = val if val
  @delim
end

#encryption_hkdf_salt(val = nil) ⇒ String

HKDF salt for the AES-GCM provider's key derivation (RFC 5869). Provides per-deployment domain separation for AES-GCM, mirroring what

encryption_personalization does for the XChaCha20 (BLAKE2b) providers --

but as a SEPARATE input, because HKDF accepts a salt of any length while BLAKE2b personalization is capped at 16 bytes. There is deliberately no length limit here (see issue #311).

The built-in default ('FamilialMatters') is a shared library fallback: it separates Familia's keys from other HKDF users, but NOT one deployment from another. Set a value unique to each deployment to get deployment-level separation.

Encryption always uses this current value; decryption tries it first, then each entry in #encryption_hkdf_salt_history, then the pre-#310 static salt, so existing ciphertext keeps decrypting across rotations and upgrades.

end

Examples:

Familia.configure do |config|

config.encryption_hkdf_salt = 'MyApp-AESGCM-2025'

Parameters:

  • val (String, nil) (defaults to: nil)

    The HKDF salt, or nil to get current value

Returns:

  • (String)

    Current HKDF salt



158
159
160
161
# File 'lib/familia/settings.rb', line 158

def encryption_hkdf_salt(val = nil)
  @encryption_hkdf_salt = val if val
  @encryption_hkdf_salt
end

#encryption_hkdf_salt_history(val = nil) ⇒ Array<String>

Previous #encryption_hkdf_salt values, kept so that rotating the AES-GCM HKDF salt stays backward-compatible. When you change the current salt, list the prior value(s) here so existing ciphertext can still be decrypted. Encryption always uses the current value; decryption tries the current value first, then each entry here in order. Keep the list short -- every stale salt adds a decryption attempt on a miss.

Examples:

Rotating the AES-GCM HKDF salt without losing old data

Familia.configure do |config|
  config.encryption_hkdf_salt = 'MyApp-2025'
  config.encryption_hkdf_salt_history = ['MyApp-2024']
end

Parameters:

  • val (Array<String>, nil) (defaults to: nil)

    Ordered list of prior values, or nil to read

Returns:

  • (Array<String>)

    Current history list (possibly empty)



178
179
180
181
# File 'lib/familia/settings.rb', line 178

def encryption_hkdf_salt_history(val = nil)
  @encryption_hkdf_salt_history = Array(val) unless val.nil?
  @encryption_hkdf_salt_history || []
end

#encryption_keys(val = nil) ⇒ Object



69
70
71
72
# File 'lib/familia/settings.rb', line 69

def encryption_keys(val = nil)
  @encryption_keys = val if val
  @encryption_keys
end

#encryption_personalization(val = nil) ⇒ String

Personalization string for BLAKE2b key derivation in the XChaCha20Poly1305 providers. Provides cryptographic domain separation so derived keys are unique per application even with identical master keys and contexts.

This knob feeds BLAKE2b's personal parameter ONLY. BLAKE2b caps the personalization at 16 bytes (the value is null-padded to 16), so it must be <= 16 bytes. The AES-GCM provider does NOT read this string -- it has its own #encryption_hkdf_salt, which carries no length limit. Keeping the two inputs separate avoids constraining one cipher family by the other's rules (see issue #311).

Encryption always uses this current value; decryption tries it first, then each entry in #encryption_personalization_history, then the built-in default ('FamilialMatters'), so existing ciphertext keeps decrypting across rotations and upgrades (see issue #333).

end

Examples:

Familia.configure do |config|

config.encryption_personalization = 'MyApp1.0'

Parameters:

  • val (String, nil) (defaults to: nil)

    The personalization string, or nil to get current value

Returns:

  • (String)

    Current personalization string



101
102
103
104
105
106
107
108
109
110
111
# File 'lib/familia/settings.rb', line 101

def encryption_personalization(val = nil)
  if val
    if val.bytesize > 16
      raise ArgumentError,
            'encryption_personalization cannot exceed 16 bytes (BLAKE2b personalization limit). ' \
            'For the AES-GCM HKDF salt, which has no length limit, use encryption_hkdf_salt.'
    end
    @encryption_personalization = val
  end
  @encryption_personalization
end

#encryption_personalization_history(val = nil) ⇒ Array<String>

Previous #encryption_personalization values, kept so that rotating the BLAKE2b personalization used by the XChaCha20 providers stays backward-compatible. When you change the current value, list the prior value(s) here so existing ciphertext can still be decrypted. Encryption always uses the current value; decryption tries the current value first, then each entry here in order. Keep the list short -- every stale entry adds a decryption attempt on a miss. Entries are subject to the same BLAKE2b constraints as the current value (non-empty String, no null bytes, <= 16 bytes); entries that violate them are skipped on decrypt.

Examples:

Rotating the personalization without losing old data

Familia.configure do |config|
  config.encryption_personalization = 'MyApp2.0'
  config.encryption_personalization_history = ['MyApp1.0']
end

Parameters:

  • val (Array<String>, nil) (defaults to: nil)

    Ordered list of prior values, or nil to read

Returns:

  • (Array<String>)

    Current history list (possibly empty)



131
132
133
134
# File 'lib/familia/settings.rb', line 131

def encryption_personalization_history(val = nil)
  @encryption_personalization_history = Array(val) unless val.nil?
  @encryption_personalization_history || []
end

#logical_database(val = nil) ⇒ Object



57
58
59
60
61
# File 'lib/familia/settings.rb', line 57

def logical_database(val = nil)
  Familia.trace :DB, nil, "#{@logical_database} #{val}" if Familia.debug?
  @logical_database = val unless val.nil?
  @logical_database
end

#prefix(val = nil) ⇒ Object



42
43
44
45
# File 'lib/familia/settings.rb', line 42

def prefix(val = nil)
  @prefix = val if val
  @prefix
end

#raise_on_unsaved_parent_write(val = nil) ⇒ Boolean

Controls whether a collection write on a DataType raises when the parent Horreum is a new, unsaved object — one whose hash key does not exist in the database yet. This is the most dangerous dirty-write scenario: the collection lands in Redis while none of the parent's scalar data exists, orphaning the collection if the parent is never saved.

Defaults to true (raise) independently of #strict_write_order, because an orphaned-record bug is rarely what the caller intends. Set to false to downgrade to a (still distinct, still strong) warning emitted via Familia.warn instead.

Note: #strict_write_order, when true, raises for every dirty write and therefore still raises the new-object case even if this is set to false.

Examples:

Downgrade the new, unsaved parent case to a warning

Familia.configure do |config|
  config.raise_on_unsaved_parent_write = false
end

Parameters:

  • val (Boolean, nil) (defaults to: nil)

    The setting value, or nil to get current value

Returns:

  • (Boolean)

    Current raise_on_unsaved_parent_write setting



284
285
286
287
288
289
290
291
# File 'lib/familia/settings.rb', line 284

def raise_on_unsaved_parent_write(val = nil)
  @raise_on_unsaved_parent_write = val unless val.nil?
  # Defaults to true: an unset (nil) value means "raise". Cannot use
  # `|| true` here -- that would coerce an explicit `false` back to true.
  return true if @raise_on_unsaved_parent_write.nil?

  @raise_on_unsaved_parent_write
end

#schema_path(val = nil) ⇒ String, ...

Directory containing schema files for JSON Schema validation. When set, schema files are discovered by convention using the underscored class name (e.g., Customer -> customer.json).

Examples:

Convention-based schema discovery

Familia.configure do |config|
  config.schema_path = 'schemas/models'
end

Parameters:

  • val (String, Pathname, nil) (defaults to: nil)

    The schema directory path, or nil to get current value

Returns:

  • (String, Pathname, nil)

    Current schema path



350
351
352
353
# File 'lib/familia/settings.rb', line 350

def schema_path(val = nil)
  @schema_path = val if val
  @schema_path
end

#schema_validator(val = nil) ⇒ Symbol, Object

Validator type for JSON Schema validation.

Available options:

  • :json_schemer (default): Use the json_schemer gem for validation
  • :none: Disable schema validation entirely
  • Custom instance: Any object responding to #validate

Examples:

Disable validation

Familia.configure do |config|
  config.schema_validator = :none
end

Parameters:

  • val (Symbol, Object, nil) (defaults to: nil)

    The validator type or instance, or nil to get current

Returns:

  • (Symbol, Object)

    Current validator setting



389
390
391
392
# File 'lib/familia/settings.rb', line 389

def schema_validator(val = nil)
  @schema_validator = val if val
  @schema_validator || :json_schemer
end

#schemas(val = nil) ⇒ Hash

Hash mapping class names to their schema file paths. Takes precedence over convention-based discovery via schema_path.

Examples:

Explicit schema mapping

Familia.configure do |config|
  config.schemas = {
    'Customer' => 'schemas/customer.json',
    'Session'  => 'schemas/session.json'
  }
end

Parameters:

  • val (Hash, nil) (defaults to: nil)

    A hash of class name => schema path mappings, or nil to get current

Returns:

  • (Hash)

    Current schema mappings



369
370
371
372
# File 'lib/familia/settings.rb', line 369

def schemas(val = nil)
  @schemas = val if val
  @schemas || {}
end

#strict_write_order(val = nil) ⇒ Boolean

Controls whether collection writes on a DataType raise an error when the parent Horreum object has unsaved scalar field changes.

When false (default), a warning is emitted via Familia.warn. When true, a Familia::Problem exception is raised.

Examples:

Enable strict mode

Familia.configure do |config|
  config.strict_write_order = true
end

Parameters:

  • val (Boolean, nil) (defaults to: nil)

    The setting value, or nil to get current value

Returns:

  • (Boolean)

    Current strict_write_order setting



257
258
259
260
# File 'lib/familia/settings.rb', line 257

def strict_write_order(val = nil)
  @strict_write_order = val unless val.nil?
  @strict_write_order || false
end

#suffix(val = nil) ⇒ Object



47
48
49
50
# File 'lib/familia/settings.rb', line 47

def suffix(val = nil)
  @suffix = val if val
  @suffix
end

#transaction_mode(val = nil) ⇒ Symbol

Controls transaction behavior when connection handlers don't support transactions

Available modes:

  • :warn (default): Log warning and execute commands individually
  • :strict: Raise OperationModeError when transaction unavailable
  • :permissive: Silently execute commands individually

Examples:

Setting transaction mode

Familia.configure do |config|
  config.transaction_mode = :warn
end

Parameters:

  • val (Symbol, nil) (defaults to: nil)

    The transaction mode or nil to get current value

Returns:

  • (Symbol)

    Current transaction mode (:strict, :warn, :permissive)



198
199
200
201
202
203
204
205
206
207
# File 'lib/familia/settings.rb', line 198

def transaction_mode(val = nil)
  if val
    unless %i[strict warn permissive].include?(val)
      raise ArgumentError, 'Transaction mode must be :strict, :warn, or :permissive'
    end

    @transaction_mode = val
  end
  @transaction_mode || :warn  # default to warn mode
end

Instance Method Details

#configure {|Settings| ... } ⇒ Settings Also known as: config

Configure Familia settings

Examples:

Block-based configuration

Familia.configure do |config|
  config.redis_uri = "redis://localhost:6379/1"
  config.ttl = 3600
end

Method chaining

Familia.configure.redis_uri = "redis://localhost:6379/1"

Yields:

  • (Settings)

    self for block-based configuration

Returns:

  • (Settings)

    self for method chaining



407
408
409
410
# File 'lib/familia/settings.rb', line 407

def configure
  yield self if block_given?
  self
end

#default_suffixObject

We define this do-nothing method because it reads better than simply Familia.suffix in some contexts.



65
66
67
# File 'lib/familia/settings.rb', line 65

def default_suffix
  suffix
end

#dirty_write_warnings(val = nil) ⇒ Symbol

Global default for how collection writes react when the parent Horreum has unsaved scalar field changes. Acts as the fallback when a Horreum subclass does not set its own +dirty_write_warnings+ class setting.

The resolved mode drives both warning and raise behavior: :warn/:once control warning frequency, :strict forces a raise, and :off suppresses everything (warnings and raises) for the class. See the precedence note below and Familia::DataType#warn_if_dirty!.

Available modes:

  • :once (default): Warn once per distinct dirty-field signature within a dirty window (deduped). A change to the dirty set warns again.
  • :warn: Warn on every collection write (legacy behavior).
  • :strict: Raise Familia::Problem on every violation.
  • :off: Suppress warnings entirely.

Note: +Familia.strict_write_order = true+ raises for any class whose mode is not explicitly +:off+. A class-level +:off+ is authoritative -- it suppresses both warnings and raises, overriding +strict_write_order+ and +raise_on_unsaved_parent_write+. "Off means off."

Examples:

Temporarily silence all classes during a bulk import

Familia.dirty_write_warnings = :off
import_records(data)
Familia.dirty_write_warnings = :once  # restore

Parameters:

  • val (Symbol, nil) (defaults to: nil)

    The mode, or nil to read the current value

Returns:

  • (Symbol)

    Current mode (defaults to :once when unconfigured)



322
323
324
325
326
327
328
329
330
331
332
# File 'lib/familia/settings.rb', line 322

def dirty_write_warnings(val = nil)
  unless val.nil?
    valid = %i[strict warn once off]
    unless valid.include?(val)
      raise ArgumentError, "dirty_write_warnings must be one of #{valid.inspect}, got #{val.inspect}"
    end

    @dirty_write_warnings = val
  end
  @dirty_write_warnings || :once
end

#dirty_write_warnings=(val) ⇒ Object



334
335
336
# File 'lib/familia/settings.rb', line 334

def dirty_write_warnings=(val)
  dirty_write_warnings(val)
end

#pipelined_mode(val = nil) ⇒ Symbol

Controls pipeline behavior when connection handlers don't support pipelines

Available modes:

  • :warn (default): Log warning and execute commands individually
  • :strict: Raise OperationModeError when pipeline unavailable
  • :permissive: Silently execute commands individually

Examples:

Setting pipeline mode

Familia.configure do |config|
  config.pipelined_mode = :permissive
end

Parameters:

  • val (Symbol, nil) (defaults to: nil)

    The pipeline mode or nil to get current value

Returns:

  • (Symbol)

    Current pipeline mode (:strict, :warn, :permissive)



224
225
226
227
228
229
230
231
232
233
# File 'lib/familia/settings.rb', line 224

def pipelined_mode(val = nil)
  if val
    unless %i[strict warn permissive].include?(val)
      raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
    end

    @pipelined_mode = val
  end
  @pipelined_mode || :warn  # default to warn mode
end

#pipelined_mode=(val) ⇒ Object



235
236
237
238
239
240
241
# File 'lib/familia/settings.rb', line 235

def pipelined_mode=(val)
  unless %i[strict warn permissive].include?(val)
    raise ArgumentError, 'Pipeline mode must be :strict, :warn, or :permissive'
  end

  @pipelined_mode = val
end