attrify 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|