attrify 0.4.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/.rspec +3 -0
- data/.standard.yml +6 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/Rakefile +10 -0
- data/lib/attrify/dsl/base.rb +28 -0
- data/lib/attrify/dsl/compound.rb +30 -0
- data/lib/attrify/dsl/engine.rb +47 -0
- data/lib/attrify/dsl/nested_variant.rb +36 -0
- data/lib/attrify/dsl/variant.rb +34 -0
- data/lib/attrify/helpers.rb +70 -0
- data/lib/attrify/operation_set.rb +220 -0
- data/lib/attrify/parser.rb +202 -0
- data/lib/attrify/variant.rb +67 -0
- data/lib/attrify/variant_registry.rb +108 -0
- data/lib/attrify/version.rb +5 -0
- data/lib/attrify.rb +53 -0
- data/sig/attribute_variants.rbs +4 -0
- metadata +98 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Attrify
|
4
|
+
module Helpers
|
5
|
+
def compute_attributes(hash)
|
6
|
+
results = {}
|
7
|
+
|
8
|
+
hash.each do |key, operations|
|
9
|
+
if operations.is_a?(Hash)
|
10
|
+
results[key] = compute_attributes(operations)
|
11
|
+
elsif operations.is_a?(Array)
|
12
|
+
current_value = []
|
13
|
+
|
14
|
+
# Process each operation in order
|
15
|
+
operations.each do |operation_hash|
|
16
|
+
operation_hash.each do |operation, value|
|
17
|
+
current_value = execute_operation(operation.to_sym, current_value, value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Flatten array and convert to string if not a hash
|
22
|
+
results[key] = current_value.join(" ")
|
23
|
+
else
|
24
|
+
results[key] = operations
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
results
|
29
|
+
end
|
30
|
+
|
31
|
+
def execute_operation(operation, current_value, value)
|
32
|
+
case operation
|
33
|
+
when :append
|
34
|
+
current_value + value
|
35
|
+
when :prepend
|
36
|
+
value + current_value
|
37
|
+
when :remove
|
38
|
+
current_value - value
|
39
|
+
when :set
|
40
|
+
value
|
41
|
+
else
|
42
|
+
current_value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def deep_merge_hashes!(hash1, hash2)
|
47
|
+
hash1.merge!(hash2) do |key, oldval, newval|
|
48
|
+
if oldval.is_a?(Hash)
|
49
|
+
deep_merge_hashes(oldval, newval)
|
50
|
+
elsif oldval.is_a?(Array)
|
51
|
+
oldval + newval # Concatenate arrays
|
52
|
+
else
|
53
|
+
newval # In case of conflicting types or non-container types, prefer newval
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def deep_merge_hashes(hash1, hash2)
|
59
|
+
hash1.merge(hash2) do |key, oldval, newval|
|
60
|
+
if oldval.is_a?(Hash)
|
61
|
+
deep_merge_hashes(oldval, newval)
|
62
|
+
elsif oldval.is_a?(Array)
|
63
|
+
oldval + newval # Concatenate arrays
|
64
|
+
else
|
65
|
+
newval
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view"
|
4
|
+
require "attrify/parser"
|
5
|
+
|
6
|
+
module Attrify
|
7
|
+
class OperationSet
|
8
|
+
include ActionView::Helpers::TagHelper
|
9
|
+
include Helpers
|
10
|
+
|
11
|
+
attr_reader :operations
|
12
|
+
attr_reader :has_procs
|
13
|
+
|
14
|
+
def initialize(operations, has_procs = false)
|
15
|
+
@has_procs = has_procs
|
16
|
+
@operations = operations
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_html
|
20
|
+
tag.attributes(@result)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_tags
|
24
|
+
to_hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def dig(*keys)
|
28
|
+
result = @result.dig(*keys)
|
29
|
+
|
30
|
+
if result.present?
|
31
|
+
if result.has_key?(:attributes)
|
32
|
+
result[:attributes]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def values_for(instance:, keys:)
|
38
|
+
# if @has_procs
|
39
|
+
|
40
|
+
@operations = run_procs_on(@operations, instance)
|
41
|
+
@result = cache_result(@operations)[:value]
|
42
|
+
@result = @result.dig(*keys)
|
43
|
+
if @result.present?
|
44
|
+
if @result.has_key?(:attributes)
|
45
|
+
@result = @result[:attributes]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
self
|
49
|
+
# else
|
50
|
+
# self
|
51
|
+
# end
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_hash
|
55
|
+
@result
|
56
|
+
end
|
57
|
+
|
58
|
+
# Retrieve value by key
|
59
|
+
def [](key)
|
60
|
+
@result[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set value by key
|
64
|
+
def []=(key, value)
|
65
|
+
@result[key] = value
|
66
|
+
end
|
67
|
+
|
68
|
+
# Iterate like a hash
|
69
|
+
def each(&)
|
70
|
+
@result.each(&)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return all keys
|
74
|
+
def keys
|
75
|
+
@result.keys
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return all values
|
79
|
+
def values
|
80
|
+
@result
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
to_html
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def cache_result(hash, current_root = nil)
|
90
|
+
results = {}
|
91
|
+
has_procs = false
|
92
|
+
|
93
|
+
hash.each do |key, operations|
|
94
|
+
# Set current_root if it's not already set (top-level keys)
|
95
|
+
current_root = key if current_root.nil?
|
96
|
+
|
97
|
+
if operations.is_a?(Hash)
|
98
|
+
# Recursively process the nested hash
|
99
|
+
result = cache_result(operations, current_root)
|
100
|
+
results[key] = result[:value]
|
101
|
+
has_procs ||= result[:has_procs]
|
102
|
+
elsif operations.is_a?(Array)
|
103
|
+
current_value = []
|
104
|
+
# Determine if we can cache the value
|
105
|
+
can_cache_value = operations.any? { |operation| operation.key?(:set) } ||
|
106
|
+
current_root == :main
|
107
|
+
|
108
|
+
# Process each operation in order
|
109
|
+
operations.each do |operation_hash|
|
110
|
+
operation_hash.each do |operation, value|
|
111
|
+
# Ensure value is an array
|
112
|
+
value_array = Array(value)
|
113
|
+
has_procs ||= value_array.any? { |c| c.is_a?(Proc) }
|
114
|
+
if can_cache_value
|
115
|
+
current_value = execute_operation(operation.to_sym, current_value, value_array)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Set the result based on whether we can cache the value
|
121
|
+
results[key] = if can_cache_value
|
122
|
+
current_value.join(" ")
|
123
|
+
else
|
124
|
+
operations
|
125
|
+
end
|
126
|
+
else
|
127
|
+
has_procs ||= operations.is_a?(Proc)
|
128
|
+
results[key] = operations
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
{value: results, has_procs: has_procs}
|
133
|
+
end
|
134
|
+
|
135
|
+
def cache_resultss(hash)
|
136
|
+
results = {}
|
137
|
+
has_procs = false
|
138
|
+
|
139
|
+
hash.each do |key, operations|
|
140
|
+
if operations.is_a?(Hash)
|
141
|
+
result = cache_result(operations)
|
142
|
+
results[key] = result[:value]
|
143
|
+
has_procs = true if result[:has_procs]
|
144
|
+
elsif operations.is_a?(Array)
|
145
|
+
current_value = []
|
146
|
+
# If any operation is a SET operation, then we can simply perform all operations now
|
147
|
+
can_cache_value = operations.any? { |operation| operation.key?(:set) } || key == :main
|
148
|
+
|
149
|
+
# Process each operation in order
|
150
|
+
operations.each do |operation_hash|
|
151
|
+
operation_hash.each do |operation, value|
|
152
|
+
has_procs = true if value.any? { |c| c.is_a?(Proc) }
|
153
|
+
if can_cache_value
|
154
|
+
current_value = execute_operation(operation.to_sym, current_value, value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Flatten array and convert to string if not a hash
|
160
|
+
results[key] = if can_cache_value
|
161
|
+
current_value.join(" ")
|
162
|
+
else
|
163
|
+
operations
|
164
|
+
end
|
165
|
+
else
|
166
|
+
has_procs = true if operations.is_a?(Proc)
|
167
|
+
results[key] = operations
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
{value: results, has_procs: has_procs}
|
172
|
+
end
|
173
|
+
|
174
|
+
def merge_arrays(hash)
|
175
|
+
hash.transform_values do |value|
|
176
|
+
case value
|
177
|
+
when Hash
|
178
|
+
merge_arrays(value) # Recursively process nested hashes
|
179
|
+
when Array
|
180
|
+
if value.all? { |v| v.is_a?(String) }
|
181
|
+
value.join(" ") # Join array of strings into a single string
|
182
|
+
else
|
183
|
+
value.map { |v| v.is_a?(Hash) ? merge_arrays(v) : v }
|
184
|
+
end
|
185
|
+
else
|
186
|
+
value # Return the value as is if it's neither a Hash nor an Array
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Recursively traverse a hash and run any procs
|
192
|
+
def run_procs_on(hash, instance)
|
193
|
+
hash.each_with_object({}) do |(key, value), processed_hash|
|
194
|
+
processed_hash[key] = if value.is_a?(Hash)
|
195
|
+
# Recursively handle nested hashes
|
196
|
+
run_procs_on(value, instance)
|
197
|
+
elsif value.is_a?(Array)
|
198
|
+
# Process arrays, replacing any procs
|
199
|
+
value.map do |element|
|
200
|
+
if element.is_a?(Proc)
|
201
|
+
# If it's a proc, execute it with the instance
|
202
|
+
instance.instance_exec(&element)
|
203
|
+
elsif element.is_a?(Hash)
|
204
|
+
# Recursively handle nested hashes
|
205
|
+
run_procs_on(element, instance)
|
206
|
+
else
|
207
|
+
element
|
208
|
+
end
|
209
|
+
end
|
210
|
+
elsif value.is_a?(Proc)
|
211
|
+
# If it's a proc, execute it with the instance
|
212
|
+
instance.instance_exec(&value)
|
213
|
+
else
|
214
|
+
# If it's not a proc or a hash, keep it as is
|
215
|
+
value
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "helpers"
|
4
|
+
|
5
|
+
module Attrify
|
6
|
+
class Parser
|
7
|
+
OPERATIONS = [:append, :prepend, :remove, :set].freeze
|
8
|
+
ALLOWED_NESTED_FIELDS = [:data, :aria].freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
include Helpers
|
12
|
+
|
13
|
+
def parse_base(base)
|
14
|
+
parse_slots(base)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_variants(variants)
|
18
|
+
variants.transform_values do |variant_options|
|
19
|
+
variant_options.transform_values do |option|
|
20
|
+
parse_slots(option)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_compounds(compounds)
|
26
|
+
raise ArgumentError, "Invalid compounds structure: Expected an Array" unless compounds.is_a?(Array)
|
27
|
+
return [] if compounds.empty?
|
28
|
+
|
29
|
+
compounds.map do |compound|
|
30
|
+
# Ensure each compound is a Hash and contains :variant and :attributes keys
|
31
|
+
unless compound.is_a?(Hash) && compound.key?(:variants) && compound.key?(:attributes)
|
32
|
+
raise ArgumentError, "Invalid compound structure: Each compound must have :variants and :attributes keys"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse the attributes section using parse_slots
|
36
|
+
{
|
37
|
+
variants: compound[:variants], # Keep the variants as they are
|
38
|
+
attributes: parse_slots(compound[:attributes]) # Parse the attributes section using parse_slots
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_defaults(defaults)
|
44
|
+
# Ensure the defaults is a hash
|
45
|
+
unless defaults.is_a?(Hash)
|
46
|
+
raise ArgumentError, "Defaults must be a hash, got #{defaults.class}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Ensure that all keys and values are symbols
|
50
|
+
unless defaults.all? { |key, value| key.is_a?(Symbol) && value.is_a?(Symbol) }
|
51
|
+
raise ArgumentError, "Defaults must be a flat hash of symbols. Got: #{defaults.inspect}"
|
52
|
+
end
|
53
|
+
|
54
|
+
defaults
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_slots(slots)
|
58
|
+
parsed_value = parse_slot(slots)
|
59
|
+
if parsed_value.key?(:attributes)
|
60
|
+
parsed_value = {main: parsed_value}
|
61
|
+
end
|
62
|
+
parsed_value
|
63
|
+
end
|
64
|
+
|
65
|
+
# {
|
66
|
+
# class: [{set: %w[bg-blue-500 text-white]}],
|
67
|
+
# style: "width:100px",
|
68
|
+
# data: { controller: "stimulus_controller" },
|
69
|
+
# # this one is a slot
|
70
|
+
# nested: { sub_slot: {class:"red"}, class: "10"}
|
71
|
+
# }
|
72
|
+
def parse_slot(slot)
|
73
|
+
raise ArgumentError, "Invalid slot structure: Expected a Hash #{slot}" unless slot.is_a?(Hash)
|
74
|
+
|
75
|
+
attributes = slot[:attributes] || {}
|
76
|
+
|
77
|
+
nested_slots = nested_slots(slot)
|
78
|
+
additional_attributes = slot.reject { |key, _| key == :attributes || nested_slots.include?(key) }
|
79
|
+
|
80
|
+
deep_merge_hashes!(attributes, additional_attributes)
|
81
|
+
|
82
|
+
parsed_slot = {}
|
83
|
+
parsed_slot[:attributes] = parse_attributes(attributes) unless attributes.empty?
|
84
|
+
|
85
|
+
# Recursively handle nested slots
|
86
|
+
nested_slots.each do |nested_slot_name|
|
87
|
+
parsed_slot[nested_slot_name] = parse_slot(slot[nested_slot_name])
|
88
|
+
end
|
89
|
+
parsed_slot
|
90
|
+
end
|
91
|
+
|
92
|
+
# class: [{set: %w[bg-blue-500 text-white]}]
|
93
|
+
# style: "width:100px"
|
94
|
+
# data: { controller: "stimulus_controller" }
|
95
|
+
# class: "red"
|
96
|
+
# class: "10"
|
97
|
+
def parse_attributes(attributes)
|
98
|
+
unless attributes.is_a?(Hash)
|
99
|
+
raise ArgumentError, "Invalid attributes list: Expected a Hash, got #{attributes.class}"
|
100
|
+
end
|
101
|
+
|
102
|
+
if is_simple_operation?(attributes)
|
103
|
+
raise ArgumentError, "Invalid Attributes List: got an operation"
|
104
|
+
end
|
105
|
+
|
106
|
+
parsed_attributes = {}
|
107
|
+
|
108
|
+
attributes.each do |key, value|
|
109
|
+
parsed_attributes[key] = parse_attribute(key, value)
|
110
|
+
end
|
111
|
+
parsed_attributes
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_attribute(key, value)
|
115
|
+
unless valid_attribute?(key, value)
|
116
|
+
raise ArgumentError, "Invalid attributes list: invalid attribute #{key}"
|
117
|
+
end
|
118
|
+
unless key.is_a?(Symbol)
|
119
|
+
raise ArgumentError, "Attribute: Key must be a symbol #{key}"
|
120
|
+
end
|
121
|
+
|
122
|
+
parse_operations(value)
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_operations(value)
|
126
|
+
case value
|
127
|
+
when Hash
|
128
|
+
# Check if the hash is a simple operation or needs further parsing
|
129
|
+
if is_simple_operation?(value)
|
130
|
+
[parse_operation(value)]
|
131
|
+
else
|
132
|
+
value.transform_values { |v|
|
133
|
+
parsed_operation = parse_operations(v)
|
134
|
+
if is_simple_operation?(parsed_operation)
|
135
|
+
[parsed_operation]
|
136
|
+
else
|
137
|
+
parsed_operation
|
138
|
+
end
|
139
|
+
}
|
140
|
+
end
|
141
|
+
when Array
|
142
|
+
if value.any? { |v| is_simple_operation?(v) }
|
143
|
+
value.map { |v| parse_operation(v) }
|
144
|
+
else
|
145
|
+
[parse_operation(value)]
|
146
|
+
end
|
147
|
+
else
|
148
|
+
[parse_operation(value)]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_operation(operation)
|
153
|
+
case operation
|
154
|
+
when Hash
|
155
|
+
if !is_simple_operation?(operation)
|
156
|
+
raise ArgumentError, "Invalid operation: got #{operation}"
|
157
|
+
end
|
158
|
+
operation.transform_values { |v|
|
159
|
+
if v.is_a?(Array)
|
160
|
+
v.map { |x| x.is_a?(Proc) ? x : x.to_s }
|
161
|
+
else
|
162
|
+
[v.is_a?(Proc) ? v : v.to_s]
|
163
|
+
end
|
164
|
+
}
|
165
|
+
when Array
|
166
|
+
{set: operation.map { |v| v.is_a?(Proc) ? v : v.to_s }}
|
167
|
+
when Proc
|
168
|
+
{set: [operation]}
|
169
|
+
else
|
170
|
+
{set: Array(operation.to_s)}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def nested_slots(hash)
|
177
|
+
hash.keys.select do |key|
|
178
|
+
value = hash[key]
|
179
|
+
next false unless value.is_a?(Hash)
|
180
|
+
|
181
|
+
key != :attributes && !ALLOWED_NESTED_FIELDS.include?(key) && !is_simple_operation?(value)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def valid_attribute?(key, value)
|
186
|
+
return true unless value.is_a?(Hash)
|
187
|
+
ALLOWED_NESTED_FIELDS.include?(key) || is_simple_operation?(value)
|
188
|
+
end
|
189
|
+
|
190
|
+
def is_simple_operation?(operation)
|
191
|
+
operation.is_a?(Hash) && (operation.keys.size == 1) && OPERATIONS.include?(operation.keys.first)
|
192
|
+
end
|
193
|
+
|
194
|
+
def array_of_operations?(array)
|
195
|
+
return false if array.empty?
|
196
|
+
array.all? do |item|
|
197
|
+
item.is_a?(Hash) && is_simple_operation?(item)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "attrify/operation_set"
|
4
|
+
|
5
|
+
module Attrify
|
6
|
+
class Variant
|
7
|
+
include Helpers
|
8
|
+
|
9
|
+
attr_reader :operations
|
10
|
+
attr_reader :attributes
|
11
|
+
attr_reader :has_procs
|
12
|
+
|
13
|
+
def initialize(operations)
|
14
|
+
@operations = operations
|
15
|
+
|
16
|
+
result = cache_result(operations)
|
17
|
+
|
18
|
+
@has_procs = result[:has_procs]
|
19
|
+
@attributes = result[:value]
|
20
|
+
end
|
21
|
+
|
22
|
+
def adjust(hash)
|
23
|
+
OperationSet.new(deep_merge_hashes(@operations, hash))
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def cache_result(hash)
|
29
|
+
results = {}
|
30
|
+
has_procs = false
|
31
|
+
|
32
|
+
hash.each do |key, operations|
|
33
|
+
if operations.is_a?(Hash)
|
34
|
+
result = cache_result(operations)
|
35
|
+
results[key] = result[:value]
|
36
|
+
has_procs = true if result[:has_procs]
|
37
|
+
elsif operations.is_a?(Array)
|
38
|
+
current_value = []
|
39
|
+
# If any operation is a SET operation, then we can simply perform all operations now
|
40
|
+
can_cache_value = operations.any? { |operation| operation.key?(:set) } || key == :main
|
41
|
+
|
42
|
+
# Process each operation in order
|
43
|
+
operations.each do |operation_hash|
|
44
|
+
operation_hash.each do |operation, value|
|
45
|
+
has_procs = true if value.any? { |c| c.is_a?(Proc) }
|
46
|
+
if can_cache_value
|
47
|
+
current_value = execute_operation(operation.to_sym, current_value, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Flatten array and convert to string if not a hash
|
53
|
+
results[key] = if can_cache_value
|
54
|
+
current_value # .join(" ")
|
55
|
+
else
|
56
|
+
operations
|
57
|
+
end
|
58
|
+
else
|
59
|
+
has_procs = true if operations.is_a?(Proc)
|
60
|
+
results[key] = operations
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
{value: results, has_procs: has_procs}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_view/helpers/tag_helper"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/string/inflections"
|
6
|
+
|
7
|
+
require "attrify/parser"
|
8
|
+
require "attrify/variant"
|
9
|
+
require "attrify/helpers"
|
10
|
+
|
11
|
+
module Attrify
|
12
|
+
class VariantRegistry
|
13
|
+
include Helpers
|
14
|
+
|
15
|
+
attr_reader :base, :variants, :defaults, :compounds
|
16
|
+
|
17
|
+
def initialize(base: {}, variants: {}, defaults: {}, compounds: [])
|
18
|
+
self.base = base
|
19
|
+
self.variants = variants
|
20
|
+
self.defaults = defaults
|
21
|
+
self.compounds = compounds
|
22
|
+
@cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def base=(value)
|
26
|
+
@base = Parser.parse_base(value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def variants=(value)
|
30
|
+
@variants = Parser.parse_variants(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def compounds=(value)
|
34
|
+
@compounds = Parser.parse_compounds(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
def defaults=(value)
|
38
|
+
@defaults = Parser.parse_defaults(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Fetch the correct variant, with caching
|
42
|
+
def fetch(**args)
|
43
|
+
# Split args into variant and operations
|
44
|
+
variant_keys = variants.keys
|
45
|
+
variant_args = {}
|
46
|
+
operation_args = {}
|
47
|
+
|
48
|
+
args.each do |key, value|
|
49
|
+
if variant_keys.include?(key)
|
50
|
+
variant_args[key] = Array(value).join("_").to_sym
|
51
|
+
else
|
52
|
+
operation_args[key] = value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
operations = Parser.parse_slots(operation_args)
|
57
|
+
|
58
|
+
cache_key = generate_cache_key(variant_args)
|
59
|
+
|
60
|
+
# Return the cached result if it exists
|
61
|
+
unless @cache.key?(cache_key)
|
62
|
+
@cache[cache_key] = compute_variant(variant: variant_args)
|
63
|
+
end
|
64
|
+
|
65
|
+
@cache[cache_key].adjust(operations)
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize_copy(orig)
|
69
|
+
super
|
70
|
+
@base = @base.dup
|
71
|
+
@variants = @variants.dup
|
72
|
+
@defaults = @defaults.dup
|
73
|
+
@compounds = @compounds.dup
|
74
|
+
@cache = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Generate a unique key based on the variants
|
80
|
+
def generate_cache_key(variant)
|
81
|
+
variant.sort.to_h.to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
def compute_variant(variant: {})
|
85
|
+
# Start with our base attributes
|
86
|
+
result = @base.dup
|
87
|
+
|
88
|
+
# Merge default variants with user-specified variants
|
89
|
+
selected_variants = @defaults.merge(variant)
|
90
|
+
|
91
|
+
# Apply selected variants to the base attributes
|
92
|
+
selected_variants.each do |variant_type, variant_key|
|
93
|
+
variant_defs = @variants.dig(variant_type, variant_key)
|
94
|
+
next unless variant_defs
|
95
|
+
deep_merge_hashes!(result, variant_defs)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Apply compounds variants
|
99
|
+
@compounds.each do |compound_variant|
|
100
|
+
if compound_variant[:variants].all? { |key, value| selected_variants[key] == value }
|
101
|
+
deep_merge_hashes!(result, compound_variant[:attributes])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
Variant.new(result)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|