familia 2.0.0.pre12 → 2.0.0.pre14

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -3
  3. data/CHANGELOG.rst +529 -0
  4. data/CLAUDE.md +1 -1
  5. data/Gemfile +1 -6
  6. data/Gemfile.lock +13 -7
  7. data/README.md +21 -2
  8. data/changelog.d/README.md +5 -5
  9. data/{setup.cfg → changelog.d/scriv.ini} +1 -1
  10. data/docs/guides/Feature-System-Autoloading.md +228 -0
  11. data/docs/guides/time-utilities.md +221 -0
  12. data/docs/migrating/v2.0.0-pre11.md +14 -16
  13. data/docs/migrating/v2.0.0-pre13.md +95 -0
  14. data/docs/migrating/v2.0.0-pre14.md +37 -0
  15. data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
  16. data/examples/autoloader/mega_customer.rb +17 -0
  17. data/examples/safe_dump.rb +1 -1
  18. data/familia.gemspec +1 -0
  19. data/lib/familia/autoloader.rb +53 -0
  20. data/lib/familia/base.rb +5 -0
  21. data/lib/familia/data_type.rb +4 -0
  22. data/lib/familia/encryption/encrypted_data.rb +4 -4
  23. data/lib/familia/encryption/manager.rb +6 -4
  24. data/lib/familia/encryption.rb +1 -1
  25. data/lib/familia/errors.rb +3 -0
  26. data/lib/familia/features/autoloadable.rb +113 -0
  27. data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
  28. data/lib/familia/features/expiration.rb +4 -0
  29. data/lib/familia/features/external_identifier.rb +3 -3
  30. data/lib/familia/features/quantization.rb +5 -0
  31. data/lib/familia/features/safe_dump.rb +7 -0
  32. data/lib/familia/features.rb +20 -16
  33. data/lib/familia/field_type.rb +2 -0
  34. data/lib/familia/horreum/core/serialization.rb +3 -3
  35. data/lib/familia/horreum/subclass/definition.rb +3 -4
  36. data/lib/familia/horreum.rb +2 -0
  37. data/lib/familia/json_serializer.rb +70 -0
  38. data/lib/familia/logging.rb +12 -10
  39. data/lib/familia/refinements/logger_trace.rb +57 -0
  40. data/lib/familia/refinements/snake_case.rb +40 -0
  41. data/lib/familia/refinements/time_literals.rb +279 -0
  42. data/lib/familia/refinements.rb +3 -49
  43. data/lib/familia/utils.rb +2 -0
  44. data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
  45. data/lib/familia/validation.rb +1 -1
  46. data/lib/familia/version.rb +1 -1
  47. data/lib/familia.rb +15 -3
  48. data/try/core/autoloader_try.rb +112 -0
  49. data/try/core/extensions_try.rb +38 -21
  50. data/try/core/familia_extended_try.rb +4 -3
  51. data/try/core/time_utils_try.rb +130 -0
  52. data/try/data_types/datatype_base_try.rb +3 -2
  53. data/try/features/autoloadable/autoloadable_try.rb +61 -0
  54. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
  55. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
  56. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
  57. data/try/features/external_identifier/external_identifier_try.rb +26 -0
  58. data/try/features/feature_improvements_try.rb +2 -1
  59. data/try/features/real_feature_integration_try.rb +1 -1
  60. data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
  61. data/try/helpers/test_helpers.rb +24 -0
  62. data/try/integration/cross_component_try.rb +3 -1
  63. metadata +34 -6
  64. data/CHANGELOG.md +0 -247
  65. data/lib/familia/core_ext.rb +0 -135
  66. data/lib/familia/features/autoloader.rb +0 -57
@@ -0,0 +1,17 @@
1
+ # examples/autoloader/mega_customer.rb
2
+
3
+ require_relative '../../lib/familia'
4
+
5
+ class MegaCustomer < Familia::Horreum
6
+ field :custid
7
+ field :username
8
+ field :email
9
+ field :fname
10
+ field :lname
11
+ field :display_name
12
+ field :created_at
13
+ field :updated_at
14
+
15
+ feature :safe_dump
16
+ # feature :deprecated_fields
17
+ end
@@ -273,7 +273,7 @@ puts "LegacyModel fields after set_safe_dump_fields: #{LegacyModel.safe_dump_fie
273
273
  puts
274
274
  puts '=== Cleaning up test data ==='
275
275
  [User, Product, Order, Address, Customer, LegacyModel].each do |klass|
276
- klass.redis.del(klass.redis.keys("#{klass.name.downcase}:*"))
276
+ klass.dbclient.del(klass.dbclient.keys("#{klass.name.downcase}:*"))
277
277
  rescue StandardError => e
278
278
  puts "Error cleaning #{klass}: #{e.message}"
279
279
  end
data/familia.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'connection_pool', '~> 2.5'
24
24
  spec.add_dependency 'csv', '~> 3.3'
25
25
  spec.add_dependency 'logger', '~> 1.7'
26
+ spec.add_dependency 'oj', '~> 3.16'
26
27
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
27
28
  spec.add_dependency 'stringio', '~> 3.1.1'
28
29
  spec.add_dependency 'uri-valkey', '~> 1.4'
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ # Provides autoloading functionality for Ruby files based on patterns and conventions.
5
+ #
6
+ # Used by the Features module at library startup to load feature files, and available
7
+ # as a utility for other modules requiring file autoloading capabilities.
8
+ module Autoloader
9
+ # Autoloads Ruby files matching the given patterns.
10
+ #
11
+ # @param patterns [String, Array<String>] file patterns to match (supports Dir.glob patterns)
12
+ # @param exclude [Array<String>] basenames to exclude from loading
13
+ # @param log_prefix [String] prefix for debug logging messages
14
+ def self.autoload_files(patterns, exclude: [], log_prefix: 'Autoloader')
15
+ patterns = Array(patterns)
16
+
17
+ patterns.each do |pattern|
18
+ Dir.glob(pattern).each do |file_path|
19
+ basename = File.basename(file_path)
20
+
21
+ # Skip excluded files
22
+ next if exclude.include?(basename)
23
+
24
+ Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{file_path}", caller(1..1) if Familia.debug?
25
+ require File.expand_path(file_path)
26
+ end
27
+ end
28
+ end
29
+
30
+ # Autoloads feature files when this module is included.
31
+ #
32
+ # Discovers and loads all Ruby files in the features/ directory relative to the
33
+ # including module's location. Typically used by Familia::Features.
34
+ #
35
+ # @param base [Module] the module including this autoloader
36
+ def self.included(base)
37
+ # Get the directory where the including module is defined
38
+ # This should be lib/familia for the Features module
39
+ base_path = File.dirname(caller_locations(1, 1).first.path)
40
+ features_dir = File.join(base_path, 'features')
41
+
42
+ Familia.ld "[DEBUG] Autoloader loading features from #{features_dir}"
43
+
44
+ return unless Dir.exist?(features_dir)
45
+
46
+ # Use the shared autoload_files method
47
+ autoload_files(
48
+ File.join(features_dir, '*.rb'),
49
+ log_prefix: 'Autoloader'
50
+ )
51
+ end
52
+ end
53
+ end
data/lib/familia/base.rb CHANGED
@@ -14,6 +14,9 @@ module Familia
14
14
  # @see Familia::DataType
15
15
  #
16
16
  module Base
17
+
18
+ using Familia::Refinements::TimeLiterals
19
+
17
20
  @features_available = nil
18
21
  @feature_definitions = nil
19
22
  @dump_method = :to_json
@@ -24,6 +27,8 @@ module Familia
24
27
  base.extend(ClassMethods)
25
28
  end
26
29
 
30
+ # Familia::Base::ClassMethods
31
+ #
27
32
  module ClassMethods
28
33
  attr_reader :features_available, :feature_definitions
29
34
  attr_accessor :dump_method, :load_method
@@ -3,6 +3,8 @@
3
3
  require_relative 'data_type/commands'
4
4
  require_relative 'data_type/serialization'
5
5
 
6
+ # Familia
7
+ #
6
8
  module Familia
7
9
  # DataType - Base class for Database data type wrappers
8
10
  #
@@ -14,6 +16,8 @@ module Familia
14
16
  include Familia::Base
15
17
  extend Familia::Features
16
18
 
19
+ using Familia::Refinements::TimeLiterals
20
+
17
21
  @registered_types = {}
18
22
  @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
19
23
  @logical_database = nil
@@ -9,7 +9,7 @@ module Familia
9
9
  return false unless json_string.kind_of?(::String)
10
10
 
11
11
  begin
12
- parsed = JSON.parse(json_string, symbolize_names: true)
12
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
13
13
  return false unless parsed.is_a?(Hash)
14
14
 
15
15
  # Check for required fields
@@ -17,7 +17,7 @@ module Familia
17
17
  result = required_fields.all? { |field| parsed.key?(field) }
18
18
  Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
19
19
  result
20
- rescue JSON::ParserError => e
20
+ rescue Familia::SerializerError => e
21
21
  Familia.ld "[valid?] JSON error: #{e.message}"
22
22
  false
23
23
  end
@@ -31,8 +31,8 @@ module Familia
31
31
  end
32
32
 
33
33
  begin
34
- parsed = JSON.parse(json_string, symbolize_names: true)
35
- rescue JSON::ParserError => e
34
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
35
+ rescue Familia::SerializerError => e
36
36
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
37
37
  end
38
38
 
@@ -19,13 +19,15 @@ module Familia
19
19
 
20
20
  result = @provider.encrypt(plaintext, key, additional_data)
21
21
 
22
- Familia::Encryption::EncryptedData.new(
22
+ encrypted_data = Familia::Encryption::EncryptedData.new(
23
23
  algorithm: @provider.algorithm,
24
24
  nonce: Base64.strict_encode64(result[:nonce]),
25
25
  ciphertext: Base64.strict_encode64(result[:ciphertext]),
26
26
  auth_tag: Base64.strict_encode64(result[:auth_tag]),
27
27
  key_version: current_key_version
28
- ).to_h.to_json
28
+ ).to_h
29
+
30
+ Familia::JsonSerializer.dump(encrypted_data)
29
31
  ensure
30
32
  Familia::Encryption.secure_wipe(key) if key
31
33
  end
@@ -37,7 +39,7 @@ module Familia
37
39
  Familia::Encryption.derivation_count.increment
38
40
 
39
41
  begin
40
- data = Familia::Encryption::EncryptedData.new(**JSON.parse(encrypted_json, symbolize_names: true))
42
+ data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json, symbolize_names: true))
41
43
 
42
44
  # Validate algorithm support
43
45
  provider = Registry.get(data.algorithm)
@@ -51,7 +53,7 @@ module Familia
51
53
  provider.decrypt(ciphertext, key, nonce, auth_tag, additional_data)
52
54
  rescue EncryptionError
53
55
  raise
54
- rescue JSON::ParserError => e
56
+ rescue Familia::SerializerError => e
55
57
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
56
58
  rescue StandardError => e
57
59
  raise EncryptionError, "Decryption failed: #{e.message}"
@@ -1,7 +1,7 @@
1
1
  # lib/familia/encryption.rb
2
2
 
3
3
  require 'base64'
4
- require 'json'
4
+ require 'oj'
5
5
  require 'openssl'
6
6
 
7
7
  # Provider system components
@@ -6,6 +6,9 @@ module Familia
6
6
  class NonUniqueKey < Problem; end
7
7
 
8
8
  class FieldTypeError < Problem; end
9
+ class AutoloadError < Problem; end
10
+
11
+ class SerializerError < Problem; end
9
12
 
10
13
  class HighRiskFactor < Problem
11
14
  attr_reader :value
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../refinements/snake_case'
4
+
5
+ module Familia
6
+ module Features
7
+ # Enables automatic loading of feature-specific files when a feature is included in a user class.
8
+ #
9
+ # When included in a feature module, adds ClassMethods that detect when the feature is
10
+ # included in user classes, derives the feature name, and autoloads files matching
11
+ # conventional patterns in the user class's directory structure.
12
+ module Autoloadable
13
+ using Familia::Refinements::SnakeCase
14
+
15
+ # Sets up a feature module with autoloading capabilities.
16
+ #
17
+ # Extends the feature module with ClassMethods to handle post-inclusion autoloading.
18
+ #
19
+ # @param feature_module [Module] the feature module being enhanced
20
+ def self.included(feature_module)
21
+ feature_module.extend(ClassMethods)
22
+ end
23
+
24
+ # Methods added to feature modules that include Autoloadable.
25
+ module ClassMethods
26
+ # Triggered when the feature is included in a user class.
27
+ #
28
+ # Sets up for post-inclusion autoloading. The actual autoloading
29
+ # is deferred until after feature setup completes.
30
+ #
31
+ # @param base [Class] the user class including this feature
32
+ def included(base)
33
+ # Call parent included method if it exists (defensive programming for mixed-in contexts)
34
+ super if defined?(super)
35
+
36
+ # No autoloading here - it's deferred to post_inclusion_autoload
37
+ # to ensure the feature is fully set up before extension files are loaded
38
+ end
39
+
40
+ # Called by the feature system after the feature is fully included.
41
+ #
42
+ # Uses const_source_location to determine where the base class is defined,
43
+ # then autoloads feature-specific extension files from that location.
44
+ #
45
+ # @param base [Class] the class that included this feature
46
+ # @param feature_name [Symbol] the name of the feature
47
+ # @param options [Hash] feature options (unused but kept for compatibility)
48
+ def post_inclusion_autoload(base, feature_name, options)
49
+ Familia.trace :FEATURE, nil, "[Autoloadable] post_inclusion_autoload called for #{feature_name} on #{base.name || base}", caller(1..1) if Familia.debug?
50
+
51
+ # Get the source location via Ruby's built-in introspection
52
+ source_location = nil
53
+
54
+ # Check for named classes that can be looked up via const_source_location
55
+ # Class#name always returns String or nil, so type check is redundant
56
+ if base.name && !base.name.empty?
57
+ begin
58
+ location_info = Module.const_source_location(base.name)
59
+ source_location = location_info&.first
60
+ Familia.trace :FEATURE, nil, "[Autoloadable] Source location for #{base.name}: #{source_location}", caller(1..1) if Familia.debug?
61
+ rescue NameError => e
62
+ # Handle cases where the class name is not a valid constant name
63
+ # This can happen in test environments with dynamically created classes
64
+ Familia.trace :FEATURE, nil, "[Autoloadable] Cannot resolve source location for #{base.name}: #{e.message}", caller(1..1) if Familia.debug?
65
+ end
66
+ else
67
+ Familia.trace :FEATURE, nil, "[Autoloadable] Skipping source location detection - base.name=#{base.name.inspect}", caller(1..1) if Familia.debug?
68
+ end
69
+
70
+ # Autoload feature-specific files if we have a valid source location
71
+ if source_location && !source_location.include?('-e') # Skip eval/irb contexts
72
+ Familia.trace :FEATURE, nil, "[Autoloadable] Calling autoload_feature_files with #{source_location}", caller(1..1) if Familia.debug?
73
+ autoload_feature_files(source_location, base, feature_name.to_s.snake_case)
74
+ else
75
+ Familia.trace :FEATURE, nil, "[Autoloadable] Skipping autoload - no valid source location", caller(1..1) if Familia.debug?
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ # Autoloads feature-specific files from conventional directory patterns.
82
+ #
83
+ # Searches for files matching patterns like:
84
+ # - model_name/feature_name_*.rb
85
+ # - model_name/features/feature_name_*.rb
86
+ # - features/feature_name_*.rb
87
+ #
88
+ # @param location_path [String] path where the user class is defined
89
+ # @param base [Class] the user class including the feature
90
+ # @param feature_name [String] snake_case name of the feature
91
+ def autoload_feature_files(location_path, base, feature_name)
92
+ base_dir = File.dirname(location_path)
93
+
94
+ # Handle anonymous classes gracefully
95
+ model_name = base.name ? base.name.snake_case : "anonymous_#{base.object_id}"
96
+
97
+ # Look for feature-specific files in conventional locations
98
+ patterns = [
99
+ File.join(base_dir, model_name, "#{feature_name}_*.rb"),
100
+ File.join(base_dir, model_name, 'features', "#{feature_name}_*.rb"),
101
+ File.join(base_dir, 'features', "#{feature_name}_*.rb"),
102
+ ]
103
+
104
+ # Use Autoloader's shared method for consistent file loading
105
+ Familia::Autoloader.autoload_files(
106
+ patterns,
107
+ log_prefix: "Autoloadable(#{feature_name})"
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -37,6 +37,8 @@
37
37
  # user.to_json # Safe - contains [CONCEALED]
38
38
  #
39
39
  class ConcealedString
40
+ REDACTED = '[CONCEALED]'.freeze
41
+
40
42
  # Create a concealed string wrapper
41
43
  #
42
44
  # @param encrypted_data [String] The encrypted JSON data
@@ -264,9 +266,9 @@ class ConcealedString
264
266
  { concealed: true }
265
267
  end
266
268
 
267
- # Prevent exposure in JSON serialization
269
+ # Prevent exposure in JSON serialization - fail closed for security
268
270
  def to_json(*)
269
- '"[CONCEALED]"'
271
+ raise Familia::SerializerError, "ConcealedString cannot be serialized to JSON"
270
272
  end
271
273
 
272
274
  # Prevent exposure in Rails serialization (as_json -> to_json)
@@ -149,6 +149,8 @@ module Familia
149
149
  module Expiration
150
150
  @default_expiration = nil
151
151
 
152
+ using Familia::Refinements::TimeLiterals
153
+
152
154
  def self.included(base)
153
155
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
154
156
  base.extend ClassMethods
@@ -160,6 +162,8 @@ module Familia
160
162
  base.instance_variable_set(:@default_expiration, @default_expiration)
161
163
  end
162
164
 
165
+ # Familia::Expiration::ClassMethods
166
+ #
163
167
  module ClassMethods
164
168
  # Set the default expiration time for instances of this class
165
169
  #
@@ -99,7 +99,7 @@ module Familia
99
99
  klass.define_method :"#{method_name}=" do |value|
100
100
  # Remove old mapping if extid is changing
101
101
  old_value = instance_variable_get(:"@#{field_name}")
102
- self.class.extid_lookup.del(old_value) if old_value && old_value != value && respond_to?(:identifier)
102
+ self.class.extid_lookup.remove_field(old_value) if old_value && old_value != value
103
103
 
104
104
  # Set the new value
105
105
  instance_variable_set(:"@#{field_name}", value)
@@ -153,7 +153,7 @@ module Familia
153
153
  find_by_id(primary_id)
154
154
  rescue Familia::NotFound
155
155
  # If the object was deleted but mapping wasn't cleaned up
156
- extid_lookup.del(extid)
156
+ extid_lookup.remove_field(extid)
157
157
  nil
158
158
  end
159
159
  end
@@ -236,7 +236,7 @@ module Familia
236
236
  def destroy!
237
237
  # Clean up extid mapping when object is destroyed
238
238
  current_extid = instance_variable_get(:@extid)
239
- self.class.extid_lookup.del(current_extid) if current_extid
239
+ self.class.extid_lookup.remove_field(current_extid) if current_extid
240
240
 
241
241
  super if defined?(super)
242
242
  end
@@ -245,11 +245,16 @@ module Familia
245
245
  # NoDefault.qstamp() # Uses 10.minutes as fallback quantum
246
246
  #
247
247
  module Quantization
248
+
249
+ using Familia::Refinements::TimeLiterals
250
+
248
251
  def self.included(base)
249
252
  Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
250
253
  base.extend ClassMethods
251
254
  end
252
255
 
256
+ # Familia::Quantization::ClassMethods
257
+ #
253
258
  module ClassMethods
254
259
  # Generates a quantized timestamp based on the given parameters
255
260
  #
@@ -1,5 +1,6 @@
1
1
  # lib/familia/features/safe_dump.rb
2
2
 
3
+
3
4
  # rubocop:disable ThreadSafety/ClassInstanceVariable
4
5
  #
5
6
  # Class instance variables are used here for feature configuration
@@ -40,10 +41,16 @@ module Familia::Features
40
41
  # of symbols in the order they were defined.
41
42
  #
42
43
  module SafeDump
44
+ include Familia::Features::Autoloadable
45
+ using Familia::Refinements::SnakeCase
46
+
43
47
  @dump_method = :to_json
44
48
  @load_method = :from_json
45
49
 
46
50
  def self.included(base)
51
+ # Call the Autoloadable module's included method for post-inclusion setup
52
+ super
53
+
47
54
  Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
48
55
  base.extend ClassMethods
49
56
 
@@ -1,5 +1,8 @@
1
1
  # lib/familia/features.rb
2
2
 
3
+ # Load the Autoloader first, then use it to load all other features
4
+ require_relative 'autoloader'
5
+
3
6
  module Familia
4
7
  FeatureDefinition = Data.define(:name, :depends_on)
5
8
 
@@ -22,9 +25,9 @@ module Familia
22
25
  # feature options. When you enable a feature with options in different models,
23
26
  # each model stores its own separate configuration without interference.
24
27
  #
25
- # ## Project Organization with Autoloader
28
+ # ## Project Organization with Autoloadable
26
29
  #
27
- # For large projects, use {Familia::Features::Autoloader} to automatically load
30
+ # For large projects, use {Familia::Features::Autoloadable} to automatically load
28
31
  # project-specific features from a dedicated directory structure. This helps
29
32
  # organize complex models by separating features into individual files.
30
33
  #
@@ -54,14 +57,16 @@ module Familia
54
57
  # # In your model file: app/models/customer.rb
55
58
  # class Customer < Familia::Horreum
56
59
  # module Features
57
- # include Familia::Features::Autoloader
60
+ # include Familia::Features::Autoloadable
58
61
  # # Automatically loads all .rb files from app/models/customer/features/
59
62
  # end
60
63
  # end
61
64
  #
62
- # @see Familia::Features::Autoloader For automatic feature loading
65
+ # @see Familia::Features::Autoloadable For automatic feature loading
63
66
  #
64
67
  module Features
68
+ include Familia::Autoloader
69
+
65
70
  @features_enabled = nil
66
71
  attr_reader :features_enabled
67
72
 
@@ -131,14 +136,23 @@ module Familia
131
136
  # Add it to the list available features_enabled for Familia::Base classes.
132
137
  features_enabled << feature_name
133
138
 
134
- # Store feature options if any were provided using the new pattern
135
- if options.any?
139
+ # Always capture and store the calling location for every feature
140
+ calling_location = caller_locations(1, 1)&.first
141
+ options[:calling_location] = calling_location&.path
142
+
143
+ # Add feature options if the class supports them (Horreum classes)
144
+ if respond_to?(:add_feature_options)
136
145
  add_feature_options(feature_name, **options)
137
146
  end
138
147
 
139
148
  # Extend the Familia::Base subclass (e.g. Customer) with the feature module
140
149
  include feature_class
141
150
 
151
+ # Trigger post-inclusion autoloading for features that support it
152
+ if feature_class.respond_to?(:post_inclusion_autoload)
153
+ feature_class.post_inclusion_autoload(self, feature_name, options)
154
+ end
155
+
142
156
  # NOTE: Do we want to extend Familia::DataType here? That would make it
143
157
  # possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
144
158
  #
@@ -152,13 +166,3 @@ module Familia
152
166
  end
153
167
  end
154
168
  end
155
-
156
- # Load all feature files from the features directory
157
- features_dir = File.join(__dir__, 'features')
158
- Familia.ld "[DEBUG] Loading features from #{features_dir}"
159
- if Dir.exist?(features_dir)
160
- Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
161
- Familia.ld "[DEBUG] Loading feature #{feature_file}"
162
- require_relative feature_file
163
- end
164
- end
@@ -29,6 +29,8 @@ module Familia
29
29
  class FieldType
30
30
  attr_reader :name, :options, :method_name, :fast_method_name, :on_conflict, :loggable
31
31
 
32
+ using Familia::Refinements::TimeLiterals
33
+
32
34
  # Initialize a new field type
33
35
  #
34
36
  # @param name [Symbol] The field name
@@ -470,7 +470,7 @@ module Familia
470
470
  # If the distinguisher returns nil, try using the dump_method but only
471
471
  # use JSON serialization for complex types that need it.
472
472
  if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
473
- prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val)
473
+ prepared = val.respond_to?(dump_method) ? val.send(dump_method) : Familia::JsonSerializer.dump(val)
474
474
  end
475
475
 
476
476
  # If both the distinguisher and dump_method return nil, log an error
@@ -493,11 +493,11 @@ module Familia
493
493
 
494
494
  # Try to parse as JSON first for complex types
495
495
  begin
496
- parsed = JSON.parse(val, symbolize_names: symbolize)
496
+ parsed = Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
497
497
  # Only return parsed value if it's a complex type (Hash/Array)
498
498
  # Simple values should remain as strings
499
499
  return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
500
- rescue JSON::ParserError
500
+ rescue Familia::SerializerError
501
501
  # Not valid JSON, return as-is
502
502
  end
503
503
 
@@ -46,6 +46,8 @@ module Familia
46
46
  include Familia::Settings
47
47
  include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
48
48
 
49
+ using Familia::Refinements::SnakeCase
50
+
49
51
  # Sets or retrieves the unique identifier field for the class.
50
52
  #
51
53
  # This method defines or returns the field or method that contains the unique
@@ -183,10 +185,7 @@ module Familia
183
185
  #
184
186
  # @return [String] The underscored class name as a string
185
187
  def config_name
186
- name.split('::').last
187
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
188
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
189
- .downcase
188
+ name.snake_case
190
189
  end
191
190
 
192
191
  def dump_method
@@ -31,6 +31,8 @@ module Familia
31
31
  include Familia::Horreum::Core
32
32
  include Familia::Horreum::Settings
33
33
 
34
+ using Familia::Refinements::TimeLiterals
35
+
34
36
  # Singleton Class Context
35
37
  #
36
38
  # The code within this block operates on the singleton class (also known as
@@ -0,0 +1,70 @@
1
+ # lib/familia/json_serializer.rb
2
+
3
+ module Familia
4
+ # JsonSerializer provides a high-performance JSON interface using OJ
5
+ #
6
+ # This module wraps OJ with a clean API that can be easily swapped out
7
+ # or benchmarked against other JSON implementations. Uses OJ's :strict
8
+ # mode for RFC 7159 compliant JSON output.
9
+ #
10
+ # @example Basic usage
11
+ # data = { name: 'test', value: 123 }
12
+ # json = Familia::JsonSerializer.dump(data)
13
+ # parsed = Familia::JsonSerializer.parse(json, symbolize_names: true)
14
+ #
15
+ module JsonSerializer
16
+
17
+ class << self
18
+ # Parse JSON string into Ruby objects
19
+ #
20
+ # @param source [String] JSON string to parse
21
+ # @param opts [Hash] parsing options
22
+ # @option opts [Boolean] :symbolize_names convert hash keys to symbols
23
+ # @return [Object] parsed Ruby object
24
+ # @raise [SerializerError] if JSON is malformed
25
+ def parse(source, opts = {})
26
+ return nil if source.nil? || source == ''
27
+
28
+ symbolize_names = opts[:symbolize_names] || opts['symbolize_names']
29
+
30
+ if symbolize_names
31
+ Oj.load(source, mode: :strict, symbol_keys: true)
32
+ else
33
+ Oj.load(source, mode: :strict)
34
+ end
35
+ rescue Oj::ParseError, Oj::Error, EncodingError => e
36
+ raise SerializerError, e.message
37
+ end
38
+
39
+ # Serialize Ruby object to JSON string
40
+ #
41
+ # @param obj [Object] Ruby object to serialize
42
+ # @return [String] JSON string
43
+ def dump(obj)
44
+ Oj.dump(obj, mode: :strict)
45
+ rescue Oj::Error, TypeError, EncodingError => e
46
+ raise SerializerError, e.message
47
+ end
48
+
49
+ # Alias for dump for JSON gem compatibility
50
+ #
51
+ # @param obj [Object] Ruby object to serialize
52
+ # @return [String] JSON string
53
+ def generate(obj)
54
+ Oj.dump(obj, mode: :strict)
55
+ rescue Oj::Error, TypeError, EncodingError => e
56
+ raise SerializerError, e.message
57
+ end
58
+
59
+ # Serialize Ruby object to pretty-formatted JSON string
60
+ #
61
+ # @param obj [Object] Ruby object to serialize
62
+ # @return [String] pretty-formatted JSON string
63
+ def pretty_generate(obj)
64
+ Oj.dump(obj, mode: :strict, indent: 2)
65
+ rescue Oj::Error, TypeError, EncodingError => e
66
+ raise SerializerError, e.message
67
+ end
68
+ end
69
+ end
70
+ end