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.
- checksums.yaml +7 -0
- data/.claude/settings.local.json +14 -0
- data/.rspec +3 -0
- data/CLAUDE.md +66 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +295 -0
- data/LICENSE.txt +21 -0
- data/README.md +782 -0
- data/Rakefile +10 -0
- data/contextual_config.gemspec +43 -0
- data/lib/contextual_config/concern/configurable.rb +112 -0
- data/lib/contextual_config/concern/lookupable.rb +129 -0
- data/lib/contextual_config/concern/schema_driven_validation.rb +152 -0
- data/lib/contextual_config/configuration.rb +77 -0
- data/lib/contextual_config/generators.rb +4 -0
- data/lib/contextual_config/module_registry.rb +144 -0
- data/lib/contextual_config/services/contextual_matcher.rb +201 -0
- data/lib/contextual_config/version.rb +3 -0
- data/lib/contextual_config.rb +71 -0
- data/lib/generators/contextual_config/configurable_table/USAGE +33 -0
- data/lib/generators/contextual_config/configurable_table/configurable_table_generator.rb +68 -0
- data/lib/generators/contextual_config/configurable_table/templates/migration.rb.tt +52 -0
- metadata +194 -0
data/README.md
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# ContextualConfig
|
|
2
|
+
|
|
3
|
+
A Ruby gem for context-aware configuration management. 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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Context-Aware Matching**: Define rules to match configurations based on runtime context
|
|
8
|
+
- **Priority-Based Resolution**: Handle conflicts through configurable priority systems
|
|
9
|
+
- **Schema Validation**: Optional JSON schema validation for configuration data
|
|
10
|
+
- **Rails Integration**: Built-in Rails generators and ActiveRecord concerns
|
|
11
|
+
- **Flexible Scoping**: Support for complex scoping rules including timing constraints
|
|
12
|
+
- **STI Support**: Single Table Inheritance support for different configuration types
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'contextual_config'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
$ bundle install
|
|
25
|
+
|
|
26
|
+
Or install it yourself as:
|
|
27
|
+
|
|
28
|
+
$ gem install contextual_config
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Generate a Configuration Table
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
rails generate contextual_config:configurable_table YourConfig
|
|
36
|
+
rails db:migrate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Create Your Model
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class YourConfig < ApplicationRecord
|
|
43
|
+
include ContextualConfig::Concern::Configurable
|
|
44
|
+
include ContextualConfig::Concern::Lookupable
|
|
45
|
+
include ContextualConfig::Concern::SchemaDrivenValidation # Optional
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Create Configuration Records
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Global configuration
|
|
53
|
+
YourConfig.create!(
|
|
54
|
+
key: "notification_settings",
|
|
55
|
+
config_data: { email_enabled: true, sms_enabled: false },
|
|
56
|
+
scoping_rules: {}, # Empty rules = applies to everyone
|
|
57
|
+
priority: 100
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Department-specific override
|
|
61
|
+
YourConfig.create!(
|
|
62
|
+
key: "notification_settings",
|
|
63
|
+
config_data: { email_enabled: true, sms_enabled: true },
|
|
64
|
+
scoping_rules: { department_id: "engineering" },
|
|
65
|
+
priority: 50 # Higher priority (lower number)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Time-based configuration
|
|
69
|
+
YourConfig.create!(
|
|
70
|
+
key: "notification_settings",
|
|
71
|
+
config_data: { email_enabled: false, sms_enabled: false },
|
|
72
|
+
scoping_rules: {
|
|
73
|
+
timing: {
|
|
74
|
+
start_date: "2024-12-25",
|
|
75
|
+
end_date: "2024-12-26"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
priority: 10 # Highest priority
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 4. Lookup Configurations
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Find best matching configuration
|
|
86
|
+
context = {
|
|
87
|
+
department_id: "engineering",
|
|
88
|
+
current_date: Date.today
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
config = YourConfig.find_applicable_config(
|
|
92
|
+
key: "notification_settings",
|
|
93
|
+
context: context
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Use the configuration
|
|
97
|
+
if config
|
|
98
|
+
settings = config.config_data
|
|
99
|
+
puts "Email enabled: #{settings['email_enabled']}"
|
|
100
|
+
puts "SMS enabled: #{settings['sms_enabled']}"
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Core Concepts
|
|
105
|
+
|
|
106
|
+
### Configuration Structure
|
|
107
|
+
|
|
108
|
+
Each configuration record contains:
|
|
109
|
+
|
|
110
|
+
- **key**: Identifier for the configuration type
|
|
111
|
+
- **config_data**: The actual configuration payload (JSONB)
|
|
112
|
+
- **scoping_rules**: Rules defining when this config applies (JSONB)
|
|
113
|
+
- **priority**: Lower numbers = higher priority (integer)
|
|
114
|
+
- **is_active**: Enable/disable configurations (boolean)
|
|
115
|
+
- **type**: For Single Table Inheritance (string, optional)
|
|
116
|
+
|
|
117
|
+
### Context Matching
|
|
118
|
+
|
|
119
|
+
Configurations are matched based on context:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
context = {
|
|
123
|
+
department_id: "sales",
|
|
124
|
+
employee_level: "senior",
|
|
125
|
+
current_date: Date.today,
|
|
126
|
+
location_country: "US"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# This will match configs where scoping_rules contain matching values
|
|
130
|
+
config = YourConfig.find_applicable_config(
|
|
131
|
+
key: "expense_policy",
|
|
132
|
+
context: context
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Scoping Rules
|
|
137
|
+
|
|
138
|
+
Scoping rules define when configurations apply:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
# Simple department scoping
|
|
142
|
+
scoping_rules: { department_id: "engineering" }
|
|
143
|
+
|
|
144
|
+
# Multiple criteria
|
|
145
|
+
scoping_rules: {
|
|
146
|
+
department_id: "sales",
|
|
147
|
+
employee_level: "senior"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Time-based rules
|
|
151
|
+
scoping_rules: {
|
|
152
|
+
timing: {
|
|
153
|
+
start_date: "2024-01-01",
|
|
154
|
+
end_date: "2024-12-31"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Global rule (matches everything)
|
|
159
|
+
scoping_rules: {}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Priority Resolution
|
|
163
|
+
|
|
164
|
+
When multiple configurations match:
|
|
165
|
+
|
|
166
|
+
1. **Specificity**: More specific rules (more matching criteria) win
|
|
167
|
+
2. **Priority**: Lower priority numbers win when specificity is equal
|
|
168
|
+
3. **Order**: First match wins when priority and specificity are equal
|
|
169
|
+
|
|
170
|
+
## Advanced Usage
|
|
171
|
+
|
|
172
|
+
### Schema Validation
|
|
173
|
+
|
|
174
|
+
Define JSON schemas for your configuration data:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
class PayrollConfig < ApplicationRecord
|
|
178
|
+
include ContextualConfig::Concern::Configurable
|
|
179
|
+
include ContextualConfig::Concern::Lookupable
|
|
180
|
+
include ContextualConfig::Concern::SchemaDrivenValidation
|
|
181
|
+
|
|
182
|
+
def self.resolve_config_data_schema(_instance = nil)
|
|
183
|
+
@_config_data_schema ||= {
|
|
184
|
+
"type" => "object",
|
|
185
|
+
"properties" => {
|
|
186
|
+
"base_salary" => { "type" => "number", "minimum" => 0 },
|
|
187
|
+
"currency" => { "type" => "string", "enum" => ["USD", "EUR", "SAR"] },
|
|
188
|
+
"overtime_rate" => { "type" => "number", "minimum" => 1.0 }
|
|
189
|
+
},
|
|
190
|
+
"required" => ["base_salary", "currency"]
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Custom Matchers
|
|
197
|
+
|
|
198
|
+
Extend the ContextualMatcher for custom logic:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
# In your application
|
|
202
|
+
module MyApp
|
|
203
|
+
class CustomMatcher < ContextualConfig::Services::ContextualMatcher
|
|
204
|
+
private_class_method
|
|
205
|
+
|
|
206
|
+
def self.evaluate_location_rule(rule_value, context_value)
|
|
207
|
+
# Custom location matching logic
|
|
208
|
+
allowed_locations = rule_value.is_a?(Array) ? rule_value : [rule_value]
|
|
209
|
+
allowed_locations.include?(context_value)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Module-Specific Extensions
|
|
216
|
+
|
|
217
|
+
ContextualConfig is designed to be highly extensible for modular applications. Modules can add their own fields and customize the gem's behavior in several ways:
|
|
218
|
+
|
|
219
|
+
#### 1. Additional Database Columns
|
|
220
|
+
|
|
221
|
+
Add module-specific columns to your configuration table:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
# In migration
|
|
225
|
+
def change
|
|
226
|
+
create_table :configurations do |t|
|
|
227
|
+
# Standard ContextualConfig columns (from generator)
|
|
228
|
+
t.string :key, null: false
|
|
229
|
+
t.jsonb :config_data, null: false, default: {}
|
|
230
|
+
t.jsonb :scoping_rules, null: false, default: {}
|
|
231
|
+
t.integer :priority, null: false, default: 100
|
|
232
|
+
t.boolean :is_active, null: false, default: true
|
|
233
|
+
t.text :description
|
|
234
|
+
t.string :type
|
|
235
|
+
|
|
236
|
+
# Custom module-specific columns
|
|
237
|
+
t.string :module_name
|
|
238
|
+
t.string :created_by_user_id
|
|
239
|
+
t.timestamp :effective_from
|
|
240
|
+
t.timestamp :expires_at
|
|
241
|
+
t.text :approval_notes
|
|
242
|
+
t.jsonb :audit_log, default: {}
|
|
243
|
+
|
|
244
|
+
t.timestamps null: false
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### 2. Module-Specific Configuration Models
|
|
250
|
+
|
|
251
|
+
Create specialized models for different modules using STI:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# Base configuration
|
|
255
|
+
class BaseConfiguration < ApplicationRecord
|
|
256
|
+
include ContextualConfig::Concern::Configurable
|
|
257
|
+
include ContextualConfig::Concern::Lookupable
|
|
258
|
+
include ContextualConfig::Concern::SchemaDrivenValidation
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# HR Module configurations
|
|
262
|
+
class HR::PolicyConfiguration < BaseConfiguration
|
|
263
|
+
def self.resolve_config_data_schema(_instance = nil)
|
|
264
|
+
{
|
|
265
|
+
'type' => 'object',
|
|
266
|
+
'properties' => {
|
|
267
|
+
'leave_approval_workflow' => { 'type' => 'string' },
|
|
268
|
+
'probation_period_days' => { 'type' => 'integer', 'minimum' => 30, 'maximum' => 365 },
|
|
269
|
+
'performance_review_frequency' => { 'type' => 'string', 'enum' => ['quarterly', 'biannual', 'annual'] }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def requires_manager_approval?
|
|
275
|
+
config_data['leave_approval_workflow'] == 'manager_required'
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Finance Module configurations
|
|
280
|
+
class Finance::PayrollConfiguration < BaseConfiguration
|
|
281
|
+
def self.resolve_config_data_schema(_instance = nil)
|
|
282
|
+
{
|
|
283
|
+
'type' => 'object',
|
|
284
|
+
'properties' => {
|
|
285
|
+
'overtime_rate' => { 'type' => 'number', 'minimum' => 1.0, 'maximum' => 3.0 },
|
|
286
|
+
'tax_calculation_method' => { 'type' => 'string', 'enum' => ['standard', 'accelerated', 'deferred'] },
|
|
287
|
+
'accounting_code' => { 'type' => 'string' },
|
|
288
|
+
'cost_center_allocation' => {
|
|
289
|
+
'type' => 'object',
|
|
290
|
+
'properties' => {
|
|
291
|
+
'department_percentage' => { 'type' => 'number' },
|
|
292
|
+
'project_percentage' => { 'type' => 'number' }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def calculate_overtime_pay(base_salary, hours)
|
|
300
|
+
base_rate = base_salary / 160 # assuming 160 hours per month
|
|
301
|
+
overtime_multiplier = config_data['overtime_rate'] || 1.5
|
|
302
|
+
base_rate * overtime_multiplier * hours
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Benefits Module configurations
|
|
307
|
+
class Benefits::InsuranceConfiguration < BaseConfiguration
|
|
308
|
+
def self.resolve_scoping_rules_schema(_instance = nil)
|
|
309
|
+
{
|
|
310
|
+
'type' => 'object',
|
|
311
|
+
'properties' => {
|
|
312
|
+
# Standard scoping
|
|
313
|
+
'department_id' => { 'type' => 'string' },
|
|
314
|
+
'employee_level' => { 'type' => 'string' },
|
|
315
|
+
|
|
316
|
+
# Benefits-specific scoping
|
|
317
|
+
'family_size' => { 'type' => 'integer', 'minimum' => 1, 'maximum' => 10 },
|
|
318
|
+
'age_group' => { 'type' => 'string', 'enum' => ['under_30', '30_to_50', 'over_50'] },
|
|
319
|
+
'employment_tenure_months' => { 'type' => 'integer', 'minimum' => 0 },
|
|
320
|
+
'previous_claims_count' => { 'type' => 'integer', 'minimum' => 0 }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def self.find_for_employee_insurance(employee, family_size: 1)
|
|
326
|
+
context = {
|
|
327
|
+
department_id: employee.department_id.to_s,
|
|
328
|
+
employee_level: employee.level,
|
|
329
|
+
family_size: family_size,
|
|
330
|
+
age_group: employee.age_group,
|
|
331
|
+
employment_tenure_months: employee.tenure_in_months
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
find_applicable_config(key: 'insurance_policy', context: context)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### 3. Extended JSONB Schema
|
|
340
|
+
|
|
341
|
+
Modules can extend the `config_data` schema to include their specific fields:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
class MultiModuleConfiguration < ApplicationRecord
|
|
345
|
+
include ContextualConfig::Concern::Configurable
|
|
346
|
+
include ContextualConfig::Concern::Lookupable
|
|
347
|
+
include ContextualConfig::Concern::SchemaDrivenValidation
|
|
348
|
+
|
|
349
|
+
def self.resolve_config_data_schema(_instance = nil)
|
|
350
|
+
{
|
|
351
|
+
'type' => 'object',
|
|
352
|
+
'properties' => {
|
|
353
|
+
# Core fields
|
|
354
|
+
'description' => { 'type' => 'string' },
|
|
355
|
+
|
|
356
|
+
# HR Module fields
|
|
357
|
+
'hr_approval_required' => { 'type' => 'boolean' },
|
|
358
|
+
'hr_notification_emails' => {
|
|
359
|
+
'type' => 'array',
|
|
360
|
+
'items' => { 'type' => 'string', 'format' => 'email' }
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
# Finance Module fields
|
|
364
|
+
'accounting_code' => { 'type' => 'string' },
|
|
365
|
+
'budget_impact' => { 'type' => 'number' },
|
|
366
|
+
|
|
367
|
+
# Benefits Module fields
|
|
368
|
+
'benefits_integration' => {
|
|
369
|
+
'type' => 'object',
|
|
370
|
+
'properties' => {
|
|
371
|
+
'medical_coverage_level' => { 'type' => 'string', 'enum' => ['basic', 'premium', 'executive'] },
|
|
372
|
+
'vacation_carryover_allowed' => { 'type' => 'boolean' }
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
# Attendance Module fields
|
|
377
|
+
'attendance_tracking' => {
|
|
378
|
+
'type' => 'object',
|
|
379
|
+
'properties' => {
|
|
380
|
+
'flexible_hours_enabled' => { 'type' => 'boolean' },
|
|
381
|
+
'remote_work_percentage' => { 'type' => 'number', 'minimum' => 0, 'maximum' => 100 }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
'additionalProperties' => false
|
|
386
|
+
}
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
#### 4. Module Registration Pattern
|
|
392
|
+
|
|
393
|
+
Create a registry system for modules:
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
# config/initializers/contextual_config_modules.rb
|
|
397
|
+
module ContextualConfig
|
|
398
|
+
class ModuleRegistry
|
|
399
|
+
@modules = {}
|
|
400
|
+
|
|
401
|
+
def self.register(module_name, &block)
|
|
402
|
+
config = ModuleConfig.new
|
|
403
|
+
block.call(config) if block_given?
|
|
404
|
+
@modules[module_name] = config
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def self.get(module_name)
|
|
408
|
+
@modules[module_name]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def self.all
|
|
412
|
+
@modules
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
class ModuleConfig
|
|
417
|
+
attr_accessor :model_class, :default_priority, :schema_file, :key_prefix
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Register modules
|
|
422
|
+
ContextualConfig::ModuleRegistry.register(:hr) do |config|
|
|
423
|
+
config.model_class = HR::PolicyConfiguration
|
|
424
|
+
config.default_priority = 50
|
|
425
|
+
config.key_prefix = 'hr'
|
|
426
|
+
config.schema_file = Rails.root.join('config/schemas/hr_policies.json')
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
ContextualConfig::ModuleRegistry.register(:finance) do |config|
|
|
430
|
+
config.model_class = Finance::PayrollConfiguration
|
|
431
|
+
config.default_priority = 25
|
|
432
|
+
config.key_prefix = 'finance'
|
|
433
|
+
config.schema_file = Rails.root.join('config/schemas/finance_payroll.json')
|
|
434
|
+
end
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### 5. Best Practices for Module Extensions
|
|
438
|
+
|
|
439
|
+
1. **Use meaningful key prefixes**: `hr.leave_policy`, `finance.overtime_rules`, `benefits.insurance_tiers`
|
|
440
|
+
|
|
441
|
+
2. **Leverage JSON schema validation**: Each module should define comprehensive schemas
|
|
442
|
+
|
|
443
|
+
3. **Create module-specific helper methods**: Add convenience methods for common operations
|
|
444
|
+
|
|
445
|
+
4. **Use consistent scoping patterns**: Define standard context fields that work across modules
|
|
446
|
+
|
|
447
|
+
5. **Plan for schema evolution**: Design schemas that can evolve as modules add new features
|
|
448
|
+
|
|
449
|
+
### STI (Single Table Inheritance)
|
|
450
|
+
|
|
451
|
+
Use STI for different configuration types:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
class BaseConfig < ApplicationRecord
|
|
455
|
+
include ContextualConfig::Concern::Configurable
|
|
456
|
+
include ContextualConfig::Concern::Lookupable
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
class PayrollConfig < BaseConfig
|
|
460
|
+
validates :key, uniqueness: { scope: :type }
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class NotificationConfig < BaseConfig
|
|
464
|
+
validates :key, uniqueness: { scope: :type }
|
|
465
|
+
end
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Real-World Examples
|
|
469
|
+
|
|
470
|
+
### Example 1: Payroll Configuration System
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# Global overtime policy (lowest priority)
|
|
474
|
+
PayrollConfig.create!(
|
|
475
|
+
key: 'overtime_policy',
|
|
476
|
+
config_data: {
|
|
477
|
+
overtime_rate: 1.5,
|
|
478
|
+
overtime_threshold_hours: 8,
|
|
479
|
+
weekend_rate_multiplier: 2.0,
|
|
480
|
+
max_overtime_hours_per_month: 40
|
|
481
|
+
},
|
|
482
|
+
scoping_rules: {},
|
|
483
|
+
priority: 100,
|
|
484
|
+
description: 'Standard company overtime policy'
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Engineering department gets better rates
|
|
488
|
+
PayrollConfig.create!(
|
|
489
|
+
key: 'overtime_policy',
|
|
490
|
+
config_data: {
|
|
491
|
+
overtime_rate: 1.75,
|
|
492
|
+
overtime_threshold_hours: 8,
|
|
493
|
+
weekend_rate_multiplier: 2.2,
|
|
494
|
+
max_overtime_hours_per_month: 60
|
|
495
|
+
},
|
|
496
|
+
scoping_rules: { department_id: 'engineering' },
|
|
497
|
+
priority: 50,
|
|
498
|
+
description: 'Engineering overtime policy'
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Senior engineers get premium rates
|
|
502
|
+
PayrollConfig.create!(
|
|
503
|
+
key: 'overtime_policy',
|
|
504
|
+
config_data: {
|
|
505
|
+
overtime_rate: 2.0,
|
|
506
|
+
overtime_threshold_hours: 8,
|
|
507
|
+
weekend_rate_multiplier: 2.5,
|
|
508
|
+
max_overtime_hours_per_month: 80
|
|
509
|
+
},
|
|
510
|
+
scoping_rules: {
|
|
511
|
+
department_id: 'engineering',
|
|
512
|
+
employee_level: 'senior'
|
|
513
|
+
},
|
|
514
|
+
priority: 25,
|
|
515
|
+
description: 'Senior engineering overtime policy'
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Holiday season enhanced rates (highest priority)
|
|
519
|
+
PayrollConfig.create!(
|
|
520
|
+
key: 'overtime_policy',
|
|
521
|
+
config_data: {
|
|
522
|
+
overtime_rate: 2.5,
|
|
523
|
+
overtime_threshold_hours: 6,
|
|
524
|
+
weekend_rate_multiplier: 3.0,
|
|
525
|
+
max_overtime_hours_per_month: 100
|
|
526
|
+
},
|
|
527
|
+
scoping_rules: {
|
|
528
|
+
timing: {
|
|
529
|
+
start_date: '2024-12-01',
|
|
530
|
+
end_date: '2024-12-31'
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
priority: 10,
|
|
534
|
+
description: 'Holiday season enhanced overtime'
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Usage in payroll calculation
|
|
538
|
+
def calculate_employee_overtime(employee, hours_worked)
|
|
539
|
+
context = {
|
|
540
|
+
department_id: employee.department_id.to_s,
|
|
541
|
+
employee_level: employee.level,
|
|
542
|
+
current_date: Date.current
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
policy = PayrollConfig.find_applicable_config(
|
|
546
|
+
key: 'overtime_policy',
|
|
547
|
+
context: context
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
return 0 unless policy
|
|
551
|
+
|
|
552
|
+
base_rate = employee.hourly_rate
|
|
553
|
+
overtime_rate = policy.config_data['overtime_rate']
|
|
554
|
+
|
|
555
|
+
base_rate * overtime_rate * hours_worked
|
|
556
|
+
end
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Example 2: Dynamic Working Hours Policy
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
# Standard working hours
|
|
563
|
+
WorkingHoursConfig.create!(
|
|
564
|
+
key: 'working_hours',
|
|
565
|
+
config_data: {
|
|
566
|
+
daily_hours: 8,
|
|
567
|
+
weekly_hours: 40,
|
|
568
|
+
break_minutes: 60,
|
|
569
|
+
flexible_hours: false,
|
|
570
|
+
remote_work_allowed: false
|
|
571
|
+
},
|
|
572
|
+
scoping_rules: {},
|
|
573
|
+
priority: 100
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Senior staff get reduced hours
|
|
577
|
+
WorkingHoursConfig.create!(
|
|
578
|
+
key: 'working_hours',
|
|
579
|
+
config_data: {
|
|
580
|
+
daily_hours: 7,
|
|
581
|
+
weekly_hours: 35,
|
|
582
|
+
flexible_hours: true,
|
|
583
|
+
remote_work_allowed: true
|
|
584
|
+
},
|
|
585
|
+
scoping_rules: { employee_level: 'senior' },
|
|
586
|
+
priority: 50
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Individual employee exception
|
|
590
|
+
WorkingHoursConfig.create!(
|
|
591
|
+
key: 'working_hours',
|
|
592
|
+
config_data: {
|
|
593
|
+
daily_hours: 6,
|
|
594
|
+
weekly_hours: 30,
|
|
595
|
+
flexible_hours: true,
|
|
596
|
+
remote_work_allowed: true
|
|
597
|
+
},
|
|
598
|
+
scoping_rules: { employee_id: 'EMP001' },
|
|
599
|
+
priority: 10
|
|
600
|
+
)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Example 3: Benefits Configuration by Demographics
|
|
604
|
+
|
|
605
|
+
```ruby
|
|
606
|
+
class Benefits::InsuranceConfig < ApplicationRecord
|
|
607
|
+
include ContextualConfig::Concern::Configurable
|
|
608
|
+
include ContextualConfig::Concern::Lookupable
|
|
609
|
+
include ContextualConfig::Concern::SchemaDrivenValidation
|
|
610
|
+
|
|
611
|
+
def self.resolve_config_data_schema(_instance = nil)
|
|
612
|
+
{
|
|
613
|
+
'type' => 'object',
|
|
614
|
+
'properties' => {
|
|
615
|
+
'medical_premium' => { 'type' => 'number', 'minimum' => 0 },
|
|
616
|
+
'dental_included' => { 'type' => 'boolean' },
|
|
617
|
+
'vision_included' => { 'type' => 'boolean' },
|
|
618
|
+
'family_coverage_multiplier' => { 'type' => 'number', 'minimum' => 1.0 },
|
|
619
|
+
'max_annual_coverage' => { 'type' => 'number', 'minimum' => 0 }
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def self.resolve_scoping_rules_schema(_instance = nil)
|
|
625
|
+
{
|
|
626
|
+
'type' => 'object',
|
|
627
|
+
'properties' => {
|
|
628
|
+
'age_group' => { 'type' => 'string', 'enum' => ['under_30', '30_to_50', 'over_50'] },
|
|
629
|
+
'family_size' => { 'type' => 'integer', 'minimum' => 1, 'maximum' => 10 },
|
|
630
|
+
'employment_tenure_months' => { 'type' => 'integer', 'minimum' => 0 },
|
|
631
|
+
'employee_level' => { 'type' => 'string' }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Young employees get basic coverage
|
|
638
|
+
Benefits::InsuranceConfig.create!(
|
|
639
|
+
key: 'health_insurance',
|
|
640
|
+
config_data: {
|
|
641
|
+
medical_premium: 200,
|
|
642
|
+
dental_included: false,
|
|
643
|
+
vision_included: false,
|
|
644
|
+
family_coverage_multiplier: 2.0,
|
|
645
|
+
max_annual_coverage: 50000
|
|
646
|
+
},
|
|
647
|
+
scoping_rules: { age_group: 'under_30' },
|
|
648
|
+
priority: 50
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Families get enhanced coverage
|
|
652
|
+
Benefits::InsuranceConfig.create!(
|
|
653
|
+
key: 'health_insurance',
|
|
654
|
+
config_data: {
|
|
655
|
+
medical_premium: 150, # Discounted rate for families
|
|
656
|
+
dental_included: true,
|
|
657
|
+
vision_included: true,
|
|
658
|
+
family_coverage_multiplier: 1.5,
|
|
659
|
+
max_annual_coverage: 100000
|
|
660
|
+
},
|
|
661
|
+
scoping_rules: { family_size: 3 }, # 3 or more family members
|
|
662
|
+
priority: 30
|
|
663
|
+
)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
## Performance Considerations
|
|
667
|
+
|
|
668
|
+
### Benchmarks
|
|
669
|
+
|
|
670
|
+
Based on testing with the JisrHR application:
|
|
671
|
+
|
|
672
|
+
- **Average lookup time**: ~3ms per configuration lookup
|
|
673
|
+
- **100 concurrent lookups**: Completed in ~0.3 seconds
|
|
674
|
+
- **Database indexes**: Automatically created by the generator for optimal performance
|
|
675
|
+
- **Memory usage**: Minimal - configurations are loaded on-demand
|
|
676
|
+
|
|
677
|
+
### Optimization Tips
|
|
678
|
+
|
|
679
|
+
1. **Use appropriate indexes**: The generator creates optimal indexes for common queries
|
|
680
|
+
2. **Cache frequently accessed configurations**: Consider caching at the application level for high-traffic scenarios
|
|
681
|
+
3. **Limit scoping rule complexity**: Simpler rules = faster matching
|
|
682
|
+
4. **Use database-level constraints**: Leverage PostgreSQL JSONB indexes for complex queries
|
|
683
|
+
|
|
684
|
+
### Production Deployment
|
|
685
|
+
|
|
686
|
+
```ruby
|
|
687
|
+
# config/initializers/contextual_config.rb
|
|
688
|
+
if Rails.env.production?
|
|
689
|
+
# Configure the gem for production use
|
|
690
|
+
ContextualConfig.configure do |config|
|
|
691
|
+
config.cache_enabled = true
|
|
692
|
+
config.cache_ttl = 300 # 5 minutes in seconds
|
|
693
|
+
config.cache_store = Rails.cache
|
|
694
|
+
config.enable_logging = true
|
|
695
|
+
config.logger = Rails.logger
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Global Configuration
|
|
701
|
+
|
|
702
|
+
ContextualConfig provides global configuration options:
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
# Configure the gem
|
|
706
|
+
ContextualConfig.configure do |config|
|
|
707
|
+
config.cache_enabled = true # Enable caching
|
|
708
|
+
config.cache_ttl = 300 # Cache TTL in seconds
|
|
709
|
+
config.cache_store = Rails.cache # Cache store to use
|
|
710
|
+
config.default_priority = 100 # Default priority for configs
|
|
711
|
+
config.enable_logging = true # Enable gem logging
|
|
712
|
+
config.logger = Rails.logger # Logger to use
|
|
713
|
+
config.timing_evaluation_enabled = true # Enable timing rule evaluation
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Access current configuration
|
|
717
|
+
config = ContextualConfig.configuration
|
|
718
|
+
puts "Cache enabled: #{config.cache_enabled?}"
|
|
719
|
+
|
|
720
|
+
# Reset configuration (useful for testing)
|
|
721
|
+
ContextualConfig.reset_configuration!
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## API Reference
|
|
725
|
+
|
|
726
|
+
### Concerns
|
|
727
|
+
|
|
728
|
+
#### ContextualConfig::Concern::Configurable
|
|
729
|
+
|
|
730
|
+
Provides basic configuration functionality:
|
|
731
|
+
|
|
732
|
+
- Validations for key, priority, is_active
|
|
733
|
+
- Scopes: `active`, `order_by_priority`
|
|
734
|
+
- Database expectations for required columns
|
|
735
|
+
|
|
736
|
+
#### ContextualConfig::Concern::Lookupable
|
|
737
|
+
|
|
738
|
+
Provides configuration lookup methods:
|
|
739
|
+
|
|
740
|
+
- `find_applicable_config(key:, context:)` - Find best match
|
|
741
|
+
- `find_all_applicable_configs(context:)` - Find all matches
|
|
742
|
+
|
|
743
|
+
#### ContextualConfig::Concern::SchemaDrivenValidation
|
|
744
|
+
|
|
745
|
+
Provides JSON schema validation:
|
|
746
|
+
|
|
747
|
+
- Validates `config_data` and `scoping_rules` against schemas
|
|
748
|
+
- Override `resolve_config_data_schema` and `resolve_scoping_rules_schema`
|
|
749
|
+
|
|
750
|
+
### Services
|
|
751
|
+
|
|
752
|
+
#### ContextualConfig::Services::ContextualMatcher
|
|
753
|
+
|
|
754
|
+
Core matching logic:
|
|
755
|
+
|
|
756
|
+
- `find_best_match(candidates:, context:)` - Find single best match
|
|
757
|
+
- `find_all_matches(candidates:, context:)` - Find all matches
|
|
758
|
+
|
|
759
|
+
### Generators
|
|
760
|
+
|
|
761
|
+
#### contextual_config:configurable_table
|
|
762
|
+
|
|
763
|
+
Generate migration for configuration table:
|
|
764
|
+
|
|
765
|
+
```bash
|
|
766
|
+
rails generate contextual_config:configurable_table ModelName
|
|
767
|
+
rails generate contextual_config:configurable_table Finance::Config --table-name=finance_configs
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
## Contributing
|
|
771
|
+
|
|
772
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bazinga012/contextual_config.
|
|
773
|
+
|
|
774
|
+
## License
|
|
775
|
+
|
|
776
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
777
|
+
|
|
778
|
+
## Development
|
|
779
|
+
|
|
780
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
781
|
+
|
|
782
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|