contextual_config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,43 @@
1
+ require_relative 'lib/contextual_config/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'contextual_config'
5
+ spec.version = ContextualConfig::VERSION
6
+ spec.authors = ['bazinga012']
7
+ spec.email = ['vishal18593@gmail.com']
8
+
9
+ spec.summary = 'A Ruby gem for context-aware configuration management'
10
+ spec.description = 'ContextualConfig provides a flexible framework for managing configurations that can be applied based on contextual rules, priorities, and scoping. Perfect for complex applications requiring dynamic configuration resolution.'
11
+ spec.homepage = 'https://github.com/bazinga012/contextual_config'
12
+ spec.license = 'MIT'
13
+ spec.required_ruby_version = '>= 3.0.0' # rubocop:disable Gemspec/RequiredRubyVersion
14
+
15
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/bazinga012/contextual_config'
18
+ spec.metadata['changelog_uri'] = 'https://github.com/bazinga012/contextual_config/blob/main/CHANGELOG.md'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ # Dependencies
31
+ spec.add_dependency('activerecord', '>= 6.0')
32
+ spec.add_dependency('activesupport', '>= 6.0')
33
+ spec.add_dependency('json-schema', '~> 5.1')
34
+
35
+ # Development dependencies
36
+ spec.add_development_dependency('rails', '>= 6.0')
37
+ spec.add_development_dependency('rspec', '~> 3.0')
38
+ spec.add_development_dependency('rubocop', '~> 1.0')
39
+ spec.add_development_dependency('rubocop-rails', '~> 2.0')
40
+ spec.add_development_dependency('rubocop-rspec', '~> 3.0')
41
+ spec.add_development_dependency('sqlite3', '>= 2.1')
42
+ spec.metadata['rubygems_mfa_required'] = 'true'
43
+ end
@@ -0,0 +1,112 @@
1
+ require 'active_support/concern'
2
+
3
+ module ContextualConfig
4
+ module Concern
5
+ module Configurable
6
+ extend ActiveSupport::Concern
7
+
8
+ # --- Class Methods ---
9
+ included do
10
+ # --- Column Expectations ---
11
+ # This concern implicitly expects the including model's table to have columns like:
12
+ # - key:string
13
+ # - config_data:jsonb
14
+ # - scoping_rules:jsonb
15
+ # - priority:integer (default should be a higher number for lower priority)
16
+ # - is_active:boolean (default should be true)
17
+ # - type:string (if STI is to be used on the including model)
18
+ # - created_at:datetime
19
+ # - updated_at:datetime
20
+
21
+ # --- Validations ---
22
+ # Ensures that a 'key' is always present for any configuration.
23
+ validates :key, presence: true
24
+
25
+ # NOTE: Uniqueness validation for 'key' is often more complex.
26
+ # It might need to be scoped by 'type' (if using STI directly on the including model)
27
+ # or by other module-specific identifiers if this concern is used by various
28
+ # top-level configuration models in different tables.
29
+ # Example for STI:
30
+ # validates :key, uniqueness: { scope: :type, message: "must be unique within its type" }
31
+ # This specific validation is commented out here as its exact nature depends on how
32
+ # the including model is structured (STI parent vs. standalone model).
33
+ # It's often better to add this uniqueness validation directly in the consuming model
34
+ # where the scope is clearer.
35
+
36
+ # Ensures 'is_active' is either true or false.
37
+ validates :is_active, inclusion: { in: [true, false], message: 'is not included in the list' }
38
+
39
+ # Ensures 'priority' is an integer.
40
+ validates :priority, numericality: { only_integer: true, message: 'is not a number' }
41
+
42
+ # --- Scopes ---
43
+
44
+ # Scope to retrieve only active configurations.
45
+ # Usage: YourModel.active
46
+ scope :active, -> { where(is_active: true) }
47
+
48
+ # Scope to order configurations by priority.
49
+ # Lower numbers indicate higher priority. Secondary sort by ID for deterministic ordering.
50
+ # Usage: YourModel.order_by_priority
51
+ scope :order_by_priority, -> { order(priority: :asc, id: :asc) } # 'asc' for priority means 1 is higher than 10
52
+
53
+ # --- Default Values ---
54
+ # It's generally recommended to set database-level defaults for `is_active` (true)
55
+ # and `priority` (e.g., a high number like 100 for low priority).
56
+ # If not set at DB level, you can use `after_initialize` here, but DB defaults are safer.
57
+ after_initialize :set_default_values, if: :new_record?
58
+ end
59
+
60
+ # --- Instance Methods ---
61
+
62
+ private
63
+
64
+ def set_default_values
65
+ # Only set defaults if the model is completely new (not loaded from DB and no explicit values set)
66
+ if new_record? && !attributes_before_type_cast.key?('is_active')
67
+ self.is_active = true
68
+ end
69
+
70
+ # Override database default priority with module-specific priority if available
71
+ set_module_default_priority if should_use_module_default_priority?
72
+ end
73
+
74
+ # Check if we should use module default priority
75
+ def should_use_module_default_priority?
76
+ # Use module default if:
77
+ # 1. Priority is nil, OR
78
+ # 2. Priority equals the database default AND we have a module config with different default
79
+ return true if priority.nil?
80
+
81
+ db_default = self.class.column_defaults['priority']&.to_i
82
+ module_config = ContextualConfig.registry.configs_for_model(self.class).first
83
+
84
+ priority == db_default && module_config && module_config.default_priority != db_default
85
+ end
86
+
87
+ # Set priority from module configuration or global configuration
88
+ def set_module_default_priority
89
+ # Try to get priority from module registry
90
+ module_config = ContextualConfig.registry.configs_for_model(self.class).first
91
+ self.priority = if module_config
92
+ module_config.default_priority
93
+ else
94
+ # Fall back to global configuration
95
+ ContextualConfig.configuration.default_priority
96
+ end
97
+ end
98
+
99
+ public
100
+
101
+ # Helper to quickly check if a configuration is currently active
102
+ def active?
103
+ is_active
104
+ end
105
+
106
+ # Get the module configuration for this model if available
107
+ def module_configuration
108
+ ContextualConfig.registry.configs_for_model(self.class).first
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,129 @@
1
+ require 'active_support/concern'
2
+ require 'digest'
3
+
4
+ module ContextualConfig
5
+ module Concern
6
+ module Lookupable
7
+ extend ActiveSupport::Concern
8
+
9
+ # --- Class Methods ---
10
+ module ClassMethods
11
+ # Finds the single most applicable configuration based on a key and context.
12
+ #
13
+ # @param key [String, Symbol] The specific key of the configuration to look for.
14
+ # @param context [Hash] A hash representing the current context (e.g., { employee_id: 'x', current_date: Date.today }).
15
+ # @param fetch_all_of_key [Boolean] If true, fetches all configs with the given key before matching.
16
+ # If false (default), assumes key is unique within the relevant scope (e.g., STI type)
17
+ # and fetches only those.
18
+ # @return [ActiveRecord::Base, nil] The instance of the best matching configuration, or nil if no match.
19
+ def find_applicable_config(key:, context:, fetch_all_of_key: false) # rubocop:disable Lint/UnusedMethodArgument
20
+ # Check cache first if enabled
21
+ if ContextualConfig.configuration.cache_enabled?
22
+ begin
23
+ cache_key = generate_cache_key(key, context)
24
+ cached_result = ContextualConfig.configuration.cache_store.read(cache_key)
25
+ if cached_result
26
+ log_lookup_attempt(key, context, 'cache_hit')
27
+ return cached_result
28
+ end
29
+ rescue => e
30
+ # Log cache error but continue with database lookup
31
+ log_cache_error('read', e)
32
+ end
33
+ end
34
+
35
+ # `self` here refers to the specific class this method is called on
36
+ # (e.g., Finance::TaxConfig or a more generic YourTeam::Configuration).
37
+
38
+ # Start with active configurations, ordered by priority (higher priority first).
39
+ # The `Configurable` concern is expected to provide `active` and `order_by_priority` scopes.
40
+ unless self.respond_to?(:active) && self.respond_to?(:order_by_priority)
41
+ raise(NoMethodError, "#{self.name} must include ContextualConfig::Concern::Configurable to use Lookupable.")
42
+ end
43
+
44
+ log_lookup_attempt(key, context, 'database_lookup')
45
+
46
+ candidates = self.active.where(key: key.to_s).order_by_priority
47
+
48
+ # Delegate to the ContextualMatcher service to find the best match from the candidates.
49
+ # The service will handle the logic of iterating through candidates, evaluating scoping_rules,
50
+ # calculating specificity, and applying priority rules.
51
+ result = ContextualConfig::Services::ContextualMatcher.find_best_match(
52
+ candidates: candidates,
53
+ context: context
54
+ )
55
+
56
+ # Cache the result if caching is enabled
57
+ if ContextualConfig.configuration.cache_enabled? && result
58
+ begin
59
+ cache_key = generate_cache_key(key, context)
60
+ ContextualConfig.configuration.cache_store.write(
61
+ cache_key,
62
+ result,
63
+ expires_in: ContextualConfig.configuration.cache_ttl
64
+ )
65
+ log_lookup_attempt(key, context, 'cache_stored')
66
+ rescue => e
67
+ # Log cache error but don't fail the operation
68
+ log_cache_error('write', e)
69
+ end
70
+ end
71
+
72
+ result
73
+ # Return the full record, the caller can decide to use .config_data or other attributes
74
+ end
75
+
76
+ # Finds all configurations that apply to a given context, without filtering by a specific key.
77
+ # Useful if multiple configurations of the same type (but different keys or scopes) can apply simultaneously,
78
+ # or if you want to see all potentially relevant configurations.
79
+ #
80
+ # @param context [Hash] A hash representing the current context.
81
+ # @return [Array<ActiveRecord::Base>] An array of all matching configuration instances, ordered by priority.
82
+ def find_all_applicable_configs(context:)
83
+ unless self.respond_to?(:active) && self.respond_to?(:order_by_priority)
84
+ raise(NoMethodError, "#{self.name} must include ContextualConfig::Concern::Configurable to use Lookupable.")
85
+ end
86
+
87
+ log_lookup_attempt('all_configs', context, 'all_configs_lookup')
88
+
89
+ # Fetch all active configurations of this type, ordered by priority.
90
+ all_candidates = self.active.order_by_priority
91
+
92
+ # Delegate to the ContextualMatcher service.
93
+ ContextualConfig::Services::ContextualMatcher.find_all_matches(
94
+ candidates: all_candidates,
95
+ context: context
96
+ )
97
+ end
98
+
99
+ private
100
+
101
+ # Generate cache key for configuration lookup
102
+ def generate_cache_key(key, context)
103
+ context_hash = context.is_a?(Hash) ? context.sort.to_h : context
104
+ "contextual_config:#{self.name}:#{key}:#{Digest::MD5.hexdigest(context_hash.to_s)}"
105
+ end
106
+
107
+ # Log configuration lookup attempts
108
+ def log_lookup_attempt(key, context, operation)
109
+ return unless ContextualConfig.configuration.enable_logging?
110
+
111
+ logger = ContextualConfig.configuration.effective_logger
112
+ logger.info(
113
+ "ContextualConfig::Lookupable: #{operation} for #{self.name} - Key: #{key}, Context: #{context}"
114
+ )
115
+ end
116
+
117
+ # Log cache errors
118
+ def log_cache_error(operation, error)
119
+ return unless ContextualConfig.configuration.enable_logging?
120
+
121
+ logger = ContextualConfig.configuration.effective_logger
122
+ logger.warn(
123
+ "ContextualConfig::Lookupable: Cache #{operation} error - #{error.class}: #{error.message}"
124
+ )
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,152 @@
1
+ require 'active_support/concern'
2
+ require 'json-schema'
3
+
4
+ module ContextualConfig
5
+ module Concern
6
+ module SchemaDrivenValidation
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # --- Validation Hooks ---
11
+ # These methods will be called during the ActiveRecord validation lifecycle (e.g., on save, valid?).
12
+ validate :validate_against_config_data_schema, if: -> { config_data.present? }
13
+ validate :validate_against_scoping_rules_schema, if: -> { scoping_rules.present? }
14
+ # We only run validation if the respective JSONB field has data.
15
+ # If it's blank/nil, we assume it's intentionally so, and schema validation isn't applicable,
16
+ # unless the schema itself marks the root object as required with specific properties.
17
+ end
18
+
19
+ # --- Instance Methods for Validation ---
20
+
21
+ private
22
+
23
+ # Validates the `config_data` attribute against its resolved JSON schema.
24
+ def validate_against_config_data_schema
25
+ # Check if validation is enabled for this model's module(s)
26
+ return unless validation_enabled_for_model?
27
+
28
+ schema = self.class.resolve_config_data_schema(self) # Pass instance if schema depends on instance state
29
+
30
+ # Try to get schema from module registry if not provided by class
31
+ if schema.nil?
32
+ module_config = find_module_config_for_model
33
+ schema = module_config&.load_schema
34
+ end
35
+
36
+ # If no schema is defined for this specific class/instance, we can choose to:
37
+ # 1. Silently skip validation (as done here by returning if schema is nil).
38
+ # 2. Raise an error indicating a missing schema definition.
39
+ # 3. Add a model error.
40
+ # Skipping allows for flexibility where some types might not require strict schema validation.
41
+ return unless schema
42
+
43
+ begin
44
+ # Using fully_validate to get an array of all errors.
45
+ errors_found = JSON::Validator.fully_validate(schema, self.config_data, strict: false, insert_defaults: false)
46
+ if errors_found.any?
47
+ errors_found.each do |error_message|
48
+ # Add a somewhat generic error. More specific parsing of error_message could provide better field-level errors.
49
+ errors.add(:config_data, "is invalid - #{error_message}")
50
+ end
51
+ # Log validation failures
52
+ log_schema_error('config_data', "Validation failed: #{errors_found.join('; ')}")
53
+ end
54
+ rescue JSON::Schema::SchemaError => e
55
+ # This rescue is for when the schema itself is invalid, which is a development-time problem.
56
+ errors.add(:base, "Config data JSON schema for #{self.class.name} is invalid: #{e.message}")
57
+ log_schema_error('config_data', e.message)
58
+ end
59
+ end
60
+
61
+ # Validates the `scoping_rules` attribute against its resolved JSON schema.
62
+ def validate_against_scoping_rules_schema
63
+ # Check if validation is enabled for this model's module(s)
64
+ return unless validation_enabled_for_model?
65
+
66
+ schema = self.class.resolve_scoping_rules_schema(self) # Pass instance
67
+ return unless schema # Skip if no schema defined
68
+
69
+ begin
70
+ errors_found = JSON::Validator.fully_validate(schema, self.scoping_rules, strict: false, insert_defaults: false)
71
+ if errors_found.any?
72
+ errors_found.each do |error_message|
73
+ errors.add(:scoping_rules, "is invalid - #{error_message}")
74
+ end
75
+ end
76
+ rescue JSON::Schema::SchemaError => e
77
+ errors.add(:base, "Scoping rules JSON schema for #{self.class.name} is invalid: #{e.message}")
78
+ log_schema_error('scoping_rules', e.message)
79
+ end
80
+ end
81
+
82
+ # Check if validation is enabled for this model in any registered module
83
+ def validation_enabled_for_model?
84
+ # Check module-specific validation settings first
85
+ module_config = find_module_config_for_model
86
+ return module_config.validation_enabled? if module_config
87
+
88
+ # Default to enabled if no module configuration found
89
+ true
90
+ end
91
+
92
+ # Find module configuration for this model
93
+ def find_module_config_for_model
94
+ ContextualConfig.registry.configs_for_model(self.class).first
95
+ end
96
+
97
+ # Log schema validation errors using configuration logger
98
+ def log_schema_error(field_name, error_message)
99
+ return unless ContextualConfig.configuration.enable_logging?
100
+
101
+ logger = ContextualConfig.configuration.effective_logger
102
+ logger.error("JSON Schema Error for #{self.class.name} (#{field_name}): #{error_message}")
103
+ end
104
+
105
+ # --- Class Methods for Schema Resolution ---
106
+ module ClassMethods
107
+ # These methods are expected to be implemented by any class that includes
108
+ # this concern (e.g., a team's base configuration model or an STI subclass).
109
+ # They are responsible for loading and returning the appropriate JSON schema
110
+ # for `config_data` and `scoping_rules` for instances of that class.
111
+ #
112
+ # The `instance` parameter is optional but can be useful if the schema
113
+ # itself needs to vary based on some attribute of the specific instance
114
+ # being validated (though this is a more advanced use case).
115
+
116
+ # @param instance [ActiveRecord::Base, nil] The instance being validated (optional).
117
+ # @return [Hash, nil] The JSON schema for config_data, or nil if no schema.
118
+ def resolve_config_data_schema(_instance = nil)
119
+ # Try to get schema from module registry first
120
+ module_config = ContextualConfig.registry.configs_for_model(self).first
121
+ schema = module_config&.load_schema if module_config
122
+ return schema if schema
123
+
124
+ # Default implementation: logs a warning and returns nil (no validation).
125
+ # Consuming classes (or their STI parents/children) MUST override this
126
+ # to provide their specific schemas.
127
+ # Example override in consuming class:
128
+ # @_config_data_schema ||= JSON.parse(File.read(Rails.root.join('config/schemas/my_model/config_data.json')))
129
+ log_missing_schema_warning('config_data')
130
+ nil
131
+ end
132
+
133
+ # @param instance [ActiveRecord::Base, nil] The instance being validated (optional).
134
+ # @return [Hash, nil] The JSON schema for scoping_rules, or nil if no schema.
135
+ def resolve_scoping_rules_schema(_instance = nil)
136
+ log_missing_schema_warning('scoping_rules')
137
+ nil
138
+ end
139
+
140
+ private
141
+
142
+ # Log missing schema warnings using configuration logger
143
+ def log_missing_schema_warning(field_name)
144
+ return unless ContextualConfig.configuration.enable_logging?
145
+
146
+ logger = ContextualConfig.configuration.effective_logger
147
+ logger.warn("WARN: JSON schema for :#{field_name} not defined for #{name}. Skipping validation.")
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module ContextualConfig
6
+ # Configuration class for gem-wide settings
7
+ class Configuration
8
+ attr_accessor :cache_enabled, :cache_ttl, :cache_store, :default_priority,
9
+ :enable_logging, :logger, :timing_evaluation_enabled
10
+
11
+ def initialize
12
+ @cache_enabled = false
13
+ @cache_ttl = 300 # 5 minutes in seconds
14
+ @cache_store = nil
15
+ @default_priority = 100
16
+ @enable_logging = false
17
+ @logger = nil
18
+ @timing_evaluation_enabled = true
19
+ end
20
+
21
+ # Cache configuration
22
+ def cache_enabled?
23
+ @cache_enabled && cache_store_available?
24
+ end
25
+
26
+ def cache_store_available?
27
+ @cache_store.respond_to?(:read) && @cache_store.respond_to?(:write)
28
+ end
29
+
30
+ # Logging configuration
31
+ def enable_logging?
32
+ @enable_logging && logger_available?
33
+ end
34
+
35
+ def logger_available?
36
+ @logger.respond_to?(:info) && @logger.respond_to?(:error)
37
+ end
38
+
39
+ # Get effective logger
40
+ def effective_logger
41
+ return @logger if logger_available?
42
+ return Rails.logger if defined?(Rails) && Rails.logger
43
+
44
+ Logger.new($stdout)
45
+ end
46
+
47
+ # Timing evaluation configuration
48
+ def timing_evaluation_enabled?
49
+ @timing_evaluation_enabled
50
+ end
51
+
52
+ # Validate configuration
53
+ def validate!
54
+ if @cache_enabled && !cache_store_available?
55
+ raise(ConfigurationError, 'Cache is enabled but cache_store is not properly configured')
56
+ end
57
+
58
+ if cache_ttl <= 0
59
+ raise(ConfigurationError, 'cache_ttl must be a positive number')
60
+ end
61
+
62
+ if default_priority.negative?
63
+ raise(ConfigurationError, 'default_priority must be non-negative')
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ # Reset to defaults
70
+ def reset!
71
+ initialize
72
+ end
73
+ end
74
+
75
+ # Configuration-specific error
76
+ class ConfigurationError < Error; end
77
+ end
@@ -0,0 +1,4 @@
1
+ require 'rails/generators'
2
+
3
+ # Load all generators
4
+ Dir[File.expand_path('generators/**/*_generator.rb', __dir__)].each { |f| require f }
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module ContextualConfig
6
+ # Registry for managing different modules that use ContextualConfig
7
+ class ModuleRegistry
8
+ def initialize
9
+ @modules = {}
10
+ end
11
+
12
+ # Register a module with its configuration
13
+ def register(module_name, &block)
14
+ module_sym = module_name.to_sym
15
+ config = @modules[module_sym] || ModuleConfig.new(module_name)
16
+ yield(config) if block_given?
17
+ @modules[module_sym] = config
18
+ config
19
+ end
20
+
21
+ # Get a registered module configuration
22
+ def get(module_name)
23
+ @modules[module_name.to_sym]
24
+ end
25
+
26
+ # Get all registered modules
27
+ def all
28
+ @modules.dup
29
+ end
30
+
31
+ # Check if a module is registered
32
+ def registered?(module_name)
33
+ @modules.key?(module_name.to_sym)
34
+ end
35
+
36
+ # Unregister a module
37
+ def unregister(module_name)
38
+ @modules.delete(module_name.to_sym)
39
+ end
40
+
41
+ # Clear all registrations
42
+ def clear!
43
+ @modules.clear
44
+ end
45
+
46
+ # Get all module names
47
+ def module_names
48
+ @modules.keys
49
+ end
50
+
51
+ # Get configurations for a specific model class
52
+ def configs_for_model(model_class)
53
+ @modules.values.select { |config| config.model_class == model_class }
54
+ end
55
+
56
+ # Get configurations with a specific key prefix
57
+ def configs_with_key_prefix(prefix)
58
+ @modules.values.select { |config| config.key_prefix == prefix }
59
+ end
60
+ end
61
+
62
+ # Configuration for individual modules
63
+ class ModuleConfig
64
+ attr_accessor :model_class, :default_priority, :schema_file, :key_prefix,
65
+ :description, :enabled, :cache_enabled, :validation_enabled
66
+
67
+ attr_reader :name
68
+
69
+ def initialize(name)
70
+ @name = name.to_sym
71
+ @model_class = nil
72
+ @default_priority = ContextualConfig.configuration.default_priority
73
+ @schema_file = nil
74
+ @key_prefix = name.to_s
75
+ @description = nil
76
+ @enabled = true
77
+ @cache_enabled = false
78
+ @validation_enabled = true
79
+ end
80
+
81
+ # Check if module is enabled
82
+ def enabled?
83
+ @enabled
84
+ end
85
+
86
+ # Check if validation is enabled for this module
87
+ def validation_enabled?
88
+ @validation_enabled
89
+ end
90
+
91
+ # Check if caching is enabled for this module
92
+ def cache_enabled?
93
+ @cache_enabled && ContextualConfig.configuration.cache_enabled?
94
+ end
95
+
96
+ # Load schema from file if specified
97
+ def load_schema
98
+ return nil unless schema_file && File.exist?(schema_file)
99
+
100
+ JSON.parse(File.read(schema_file))
101
+ rescue JSON::ParserError => e
102
+ raise(ConfigurationError, "Invalid JSON schema file for module #{name}: #{e.message}")
103
+ end
104
+
105
+ # Generate default key for this module
106
+ def default_key(suffix = nil)
107
+ parts = [key_prefix]
108
+ parts << suffix if suffix
109
+ parts.join('.')
110
+ end
111
+
112
+ # Validate module configuration
113
+ def validate!
114
+ if model_class && !model_class.is_a?(Class)
115
+ raise(ConfigurationError, "model_class must be a Class for module #{name}")
116
+ end
117
+
118
+ if schema_file && !File.exist?(schema_file)
119
+ raise(ConfigurationError, "Schema file does not exist for module #{name}: #{schema_file}")
120
+ end
121
+
122
+ if default_priority.negative?
123
+ raise(ConfigurationError, "default_priority must be non-negative for module #{name}")
124
+ end
125
+
126
+ true
127
+ end
128
+
129
+ # Convert to hash for serialization
130
+ def to_h
131
+ {
132
+ name: name,
133
+ model_class: model_class&.name,
134
+ default_priority: default_priority,
135
+ schema_file: schema_file,
136
+ key_prefix: key_prefix,
137
+ description: description,
138
+ enabled: enabled?,
139
+ cache_enabled: cache_enabled?,
140
+ validation_enabled: validation_enabled?
141
+ }
142
+ end
143
+ end
144
+ end