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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +529 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/README.md +21 -2
- data/changelog.d/README.md +5 -5
- data/{setup.cfg → changelog.d/scriv.ini} +1 -1
- data/docs/guides/Feature-System-Autoloading.md +228 -0
- data/docs/guides/time-utilities.md +221 -0
- data/docs/migrating/v2.0.0-pre11.md +14 -16
- data/docs/migrating/v2.0.0-pre13.md +95 -0
- data/docs/migrating/v2.0.0-pre14.md +37 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- data/examples/safe_dump.rb +1 -1
- data/familia.gemspec +1 -0
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +5 -0
- data/lib/familia/data_type.rb +4 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -4
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +3 -0
- data/lib/familia/features/autoloadable.rb +113 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
- data/lib/familia/features/expiration.rb +4 -0
- data/lib/familia/features/external_identifier.rb +3 -3
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +7 -0
- data/lib/familia/features.rb +20 -16
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +3 -4
- data/lib/familia/horreum.rb +2 -0
- data/lib/familia/json_serializer.rb +70 -0
- data/lib/familia/logging.rb +12 -10
- data/lib/familia/refinements/logger_trace.rb +57 -0
- data/lib/familia/refinements/snake_case.rb +40 -0
- data/lib/familia/refinements/time_literals.rb +279 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/utils.rb +2 -0
- data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
- data/lib/familia/validation.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -3
- data/try/core/autoloader_try.rb +112 -0
- data/try/core/extensions_try.rb +38 -21
- data/try/core/familia_extended_try.rb +4 -3
- data/try/core/time_utils_try.rb +130 -0
- data/try/data_types/datatype_base_try.rb +3 -2
- data/try/features/autoloadable/autoloadable_try.rb +61 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
- data/try/features/external_identifier/external_identifier_try.rb +26 -0
- data/try/features/feature_improvements_try.rb +2 -1
- data/try/features/real_feature_integration_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/helpers/test_helpers.rb +24 -0
- data/try/integration/cross_component_try.rb +3 -1
- metadata +34 -6
- data/CHANGELOG.md +0 -247
- data/lib/familia/core_ext.rb +0 -135
- 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
|
data/examples/safe_dump.rb
CHANGED
@@ -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.
|
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
|
data/lib/familia/data_type.rb
CHANGED
@@ -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 =
|
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
|
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 =
|
35
|
-
rescue
|
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
|
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(**
|
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
|
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}"
|
data/lib/familia/encryption.rb
CHANGED
data/lib/familia/errors.rb
CHANGED
@@ -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
|
-
|
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.
|
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.
|
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.
|
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
|
|
data/lib/familia/features.rb
CHANGED
@@ -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
|
28
|
+
# ## Project Organization with Autoloadable
|
26
29
|
#
|
27
|
-
# For large projects, use {Familia::Features::
|
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::
|
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::
|
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
|
-
#
|
135
|
-
|
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
|
data/lib/familia/field_type.rb
CHANGED
@@ -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) :
|
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 =
|
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
|
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.
|
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
|
data/lib/familia/horreum.rb
CHANGED
@@ -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
|