glue_gun_dsl 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/lib/glue_gun/dsl.rb +384 -0
- data/lib/glue_gun/types/hash_type.rb +20 -0
- data/lib/glue_gun/version.rb +5 -0
- data/lib/glue_gun.rb +9 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7bc70bbece9bc81c3cb8144e5911a6b48d1b72463dd2dfa4b89dbad65883d358
|
4
|
+
data.tar.gz: f3f021fa01cabc567c36e224370fd34687d804867bb97f28cfc0d0eb86877c23
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 247ab4c8bb7bc2ce23644640c24bc9ebc1842afe343a6deec0af4077b05ff3c31987106aba8f8fa6066338fe29bf52c1ce365984627f06335b9e622efc6afece
|
7
|
+
data.tar.gz: b6814e46bbb54a9490246e1b43aac4dd1bac17042b46bd0bb0ed28679bd2495cf7e2a0a14ab601823f8e9a3494dc3400f1b0509a77cc1a05577798d3a343b7a4
|
data/lib/glue_gun/dsl.rb
ADDED
@@ -0,0 +1,384 @@
|
|
1
|
+
module GlueGun
|
2
|
+
module DSL
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveModel::Attributes
|
8
|
+
include ActiveModel::Validations
|
9
|
+
include ActiveModel::AttributeAssignment
|
10
|
+
|
11
|
+
class_attribute :attribute_definitions, instance_writer: false, default: {}
|
12
|
+
class_attribute :dependency_definitions, instance_writer: false, default: {}
|
13
|
+
class_attribute :hardcoded_dependencies, instance_writer: false, default: {}
|
14
|
+
|
15
|
+
# Module to hold custom attribute methods
|
16
|
+
attribute_methods_module = Module.new
|
17
|
+
const_set(:AttributeMethods, attribute_methods_module)
|
18
|
+
prepend attribute_methods_module
|
19
|
+
|
20
|
+
# Prepend Initialization module to override initialize
|
21
|
+
prepend Initialization
|
22
|
+
|
23
|
+
# Prepend ClassMethods into the singleton class of the including class
|
24
|
+
class << self
|
25
|
+
prepend ClassMethods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Initialization
|
30
|
+
def initialize(attributes = {})
|
31
|
+
attributes = attributes.symbolize_keys
|
32
|
+
# Separate dependency configurations from normal attributes
|
33
|
+
dependency_attributes = {}
|
34
|
+
normal_attributes = {}
|
35
|
+
|
36
|
+
attributes.each do |key, value|
|
37
|
+
if self.class.dependency_definitions.key?(key)
|
38
|
+
dependency_attributes[key] = value
|
39
|
+
else
|
40
|
+
normal_attributes[key] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
normal_attributes.reverse_merge!(root_dir: detect_root_dir) if attribute_definitions.keys.include?(:root_dir)
|
45
|
+
|
46
|
+
# Call super to allow ActiveModel to assign attributes
|
47
|
+
super(normal_attributes)
|
48
|
+
|
49
|
+
# Initialize dependencies after attributes have been set
|
50
|
+
initialize_dependencies(dependency_attributes)
|
51
|
+
|
52
|
+
# Validate the main object's attributes
|
53
|
+
validate!
|
54
|
+
|
55
|
+
@initialized = true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module ClassMethods
|
60
|
+
# Override the attribute method to define custom setters
|
61
|
+
def attribute(name, type = :string, **options)
|
62
|
+
# Call the original attribute method from ActiveModel::Attributes
|
63
|
+
super(name, type, **options)
|
64
|
+
attribute_definitions[name.to_sym] = { type: type, options: options }
|
65
|
+
|
66
|
+
attribute_methods_module = const_get(:AttributeMethods)
|
67
|
+
|
68
|
+
attribute_methods_module.class_eval do
|
69
|
+
define_method "#{name}=" do |value|
|
70
|
+
super(value)
|
71
|
+
propagate_attribute_change(name, value) if initialized?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def dependency(component_type, &block)
|
77
|
+
dependency_builder = DependencyBuilder.new(component_type)
|
78
|
+
dependency_builder.instance_eval(&block)
|
79
|
+
dependency_definitions[component_type] = dependency_builder
|
80
|
+
|
81
|
+
# Define singleton method to allow hardcoding dependencies in subclasses
|
82
|
+
define_singleton_method component_type do |option = nil, options = {}|
|
83
|
+
if option.is_a?(Hash) && options.empty?
|
84
|
+
options = option
|
85
|
+
option = nil
|
86
|
+
end
|
87
|
+
option ||= dependency_builder.default_option_name
|
88
|
+
hardcoded_dependencies[component_type] = { option_name: option, value: options }
|
89
|
+
end
|
90
|
+
|
91
|
+
define_method component_type do
|
92
|
+
instance_variable_get("@#{component_type}") ||
|
93
|
+
instance_variable_set("@#{component_type}", initialize_dependency(component_type))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def inherited(subclass)
|
98
|
+
super
|
99
|
+
subclass.attribute_definitions = attribute_definitions.deep_dup
|
100
|
+
subclass.dependency_definitions = dependency_definitions.deep_dup
|
101
|
+
subclass.hardcoded_dependencies = hardcoded_dependencies.deep_dup
|
102
|
+
|
103
|
+
# Prepend the AttributeMethods module to the subclass
|
104
|
+
attribute_methods_module = const_get(:AttributeMethods)
|
105
|
+
subclass.prepend(attribute_methods_module)
|
106
|
+
|
107
|
+
# Prepend ClassMethods into the singleton class of the subclass
|
108
|
+
class << subclass
|
109
|
+
prepend ClassMethods
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def detect_root_dir
|
114
|
+
base_path = Module.const_source_location(name)&.first || ""
|
115
|
+
File.dirname(base_path)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def initialized?
|
120
|
+
@initialized == true
|
121
|
+
end
|
122
|
+
|
123
|
+
def detect_root_dir
|
124
|
+
base_path = Module.const_source_location(self.class.name)&.first || ""
|
125
|
+
File.dirname(base_path)
|
126
|
+
end
|
127
|
+
|
128
|
+
def initialize_dependencies(attributes)
|
129
|
+
self.class.dependency_definitions.each do |component_type, _|
|
130
|
+
value = attributes[component_type] || self.class.hardcoded_dependencies[component_type]
|
131
|
+
instance_variable_set("@#{component_type}", initialize_dependency(component_type, value))
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def initialize_dependency(component_type, init_args = {})
|
136
|
+
return init_args if dependency_injected?(component_type, init_args)
|
137
|
+
|
138
|
+
dependency_builder = self.class.dependency_definitions[component_type]
|
139
|
+
|
140
|
+
if init_args && init_args.is_a?(Hash) && init_args.key?(:option_name)
|
141
|
+
option_name = init_args[:option_name]
|
142
|
+
init_args = init_args[:value]
|
143
|
+
else
|
144
|
+
option_name, init_args = determine_option_name(component_type, init_args)
|
145
|
+
end
|
146
|
+
|
147
|
+
option_config = dependency_builder.option_configs[option_name]
|
148
|
+
|
149
|
+
raise ArgumentError, "Unknown #{component_type} option '#{option_name}'" unless option_config
|
150
|
+
|
151
|
+
dep_attributes = init_args.is_a?(Hash) ? init_args : {}
|
152
|
+
|
153
|
+
# Build dependency attributes, including sourcing from parent
|
154
|
+
dep_attributes = build_dependency_attributes(option_config, dep_attributes)
|
155
|
+
|
156
|
+
dependency_instance = instantiate_dependency(option_config, dep_attributes)
|
157
|
+
|
158
|
+
# Keep track of dependencies for attribute binding
|
159
|
+
dependencies[component_type] = {
|
160
|
+
instance: dependency_instance,
|
161
|
+
option: option_config
|
162
|
+
}
|
163
|
+
|
164
|
+
dependency_instance
|
165
|
+
end
|
166
|
+
|
167
|
+
def build_dependency_attributes(option_config, dep_attributes)
|
168
|
+
option_config.attributes.each do |attr_name, attr_config|
|
169
|
+
next if dep_attributes.key?(attr_name)
|
170
|
+
|
171
|
+
value = if attr_config.source && respond_to?(attr_config.source)
|
172
|
+
send(attr_config.source)
|
173
|
+
elsif respond_to?(attr_name)
|
174
|
+
send(attr_name)
|
175
|
+
else
|
176
|
+
attr_config.default
|
177
|
+
end
|
178
|
+
|
179
|
+
value = attr_config.process_value(value, self) if attr_config.respond_to?(:process_value)
|
180
|
+
|
181
|
+
dep_attributes[attr_name] = value
|
182
|
+
end
|
183
|
+
dep_attributes
|
184
|
+
end
|
185
|
+
|
186
|
+
def determine_option_name(component_type, init_args)
|
187
|
+
dependency_builder = self.class.dependency_definitions[component_type]
|
188
|
+
|
189
|
+
option_name = nil
|
190
|
+
|
191
|
+
# Use when block if defined
|
192
|
+
if dependency_builder.when_block
|
193
|
+
result = instance_exec(init_args, &dependency_builder.when_block)
|
194
|
+
if result.is_a?(Hash) && result[:option]
|
195
|
+
option_name = result[:option]
|
196
|
+
as_attr = result[:as]
|
197
|
+
init_args = { as_attr => init_args } if as_attr && init_args
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Detect option from user input
|
202
|
+
if option_name.nil? && (init_args.is_a?(Hash) && init_args.keys.size == 1)
|
203
|
+
if dependency_builder.option_configs.key?(init_args.keys.first)
|
204
|
+
option_name = init_args.keys.first
|
205
|
+
init_args = init_args[option_name] # Extract the inner value
|
206
|
+
else
|
207
|
+
default_option = dependency_builder.get_option(dependency_builder.default_option_name)
|
208
|
+
raise ArgumentError, "Unknown #{component_type} option: #{init_args.keys.first}." unless default_option.only?
|
209
|
+
unless default_option.attributes.keys.include?(init_args.keys.first)
|
210
|
+
raise ArgumentError, "#{default_option.class_name} does not respond to #{init_args.keys.first}"
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Use default option if none determined
|
217
|
+
option_name ||= dependency_builder.default_option_name
|
218
|
+
|
219
|
+
[option_name, init_args]
|
220
|
+
end
|
221
|
+
|
222
|
+
def instantiate_dependency(option_config, dep_attributes)
|
223
|
+
dependency_class = option_config.class_name
|
224
|
+
dependency_instance = dependency_class.new(dep_attributes)
|
225
|
+
dependency_instance.validate! if dependency_instance.respond_to?(:validate!)
|
226
|
+
dependency_instance
|
227
|
+
end
|
228
|
+
|
229
|
+
def propagate_attribute_change(attr_name, value)
|
230
|
+
self.class.dependency_definitions.each do |component_type, _dependency_builder|
|
231
|
+
dependency_instance = send(component_type)
|
232
|
+
option_config = dependencies.dig(component_type, :option)
|
233
|
+
next unless option_config
|
234
|
+
|
235
|
+
bound_attrs = option_config.attributes.select do |_, attr_config|
|
236
|
+
(attr_config.source == attr_name.to_sym) || (attr_config.name == attr_name.to_sym)
|
237
|
+
end
|
238
|
+
|
239
|
+
bound_attrs.each do |dep_attr_name, _|
|
240
|
+
dependency_instance.send("#{dep_attr_name}=", value) if dependency_instance.respond_to?("#{dep_attr_name}=")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def dependency_injected?(component_type, value)
|
246
|
+
dependency_builder = self.class.dependency_definitions[component_type]
|
247
|
+
dependency_builder.option_configs.values.any? do |option|
|
248
|
+
option_class = option.class_name
|
249
|
+
value.is_a?(option_class)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def dependencies
|
254
|
+
@dependencies ||= {}
|
255
|
+
end
|
256
|
+
|
257
|
+
class ConfigAttr
|
258
|
+
attr_reader :name, :default, :source, :block
|
259
|
+
|
260
|
+
def initialize(name, default: nil, source: nil, &block)
|
261
|
+
@name = name.to_sym
|
262
|
+
@default = default
|
263
|
+
@source = source
|
264
|
+
@block = block
|
265
|
+
end
|
266
|
+
|
267
|
+
def process_value(value, _context = nil)
|
268
|
+
value = evaluate_value(value)
|
269
|
+
value = evaluate_value(@default) if value.nil? && !@default.nil?
|
270
|
+
value = @block.call(value) if @block && !value.nil?
|
271
|
+
value
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
def evaluate_value(value)
|
277
|
+
value.is_a?(Proc) ? value.call : value
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class DependencyBuilder
|
282
|
+
attr_reader :component_type, :option_configs, :when_block, :is_only
|
283
|
+
|
284
|
+
def initialize(component_type)
|
285
|
+
@component_type = component_type
|
286
|
+
@option_configs = {}
|
287
|
+
@default_option_name = nil
|
288
|
+
@single_option = nil
|
289
|
+
@is_only = false
|
290
|
+
end
|
291
|
+
|
292
|
+
# Support set_class and define_attr for single-option dependencies
|
293
|
+
def set_class(class_name)
|
294
|
+
single_option.set_class(class_name)
|
295
|
+
set_default_option_name(:default)
|
296
|
+
end
|
297
|
+
|
298
|
+
def attribute(name, default: nil, source: nil, &block)
|
299
|
+
single_option.attribute(name, default: default, source: source, &block)
|
300
|
+
end
|
301
|
+
|
302
|
+
def get_option(name)
|
303
|
+
@option_configs[name]
|
304
|
+
end
|
305
|
+
|
306
|
+
# For multi-option dependencies
|
307
|
+
def option(name, &block)
|
308
|
+
option_builder = OptionBuilder.new(name)
|
309
|
+
option_builder.instance_eval(&block)
|
310
|
+
@option_configs[name] = option_builder
|
311
|
+
set_default_option_name(name) if option_builder.is_default
|
312
|
+
end
|
313
|
+
|
314
|
+
def default_option_name
|
315
|
+
@default_option_name || (@single_option ? :default : nil)
|
316
|
+
end
|
317
|
+
|
318
|
+
def when(&block)
|
319
|
+
@when_block = block
|
320
|
+
end
|
321
|
+
|
322
|
+
def single_option
|
323
|
+
@single_option ||= begin
|
324
|
+
option_builder = OptionBuilder.new(:default)
|
325
|
+
option_builder.only
|
326
|
+
@option_configs[:default] = option_builder
|
327
|
+
set_default_option_name(:default)
|
328
|
+
option_builder
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def only?
|
333
|
+
@is_only == true
|
334
|
+
end
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def set_default_option_name(name)
|
339
|
+
if @default_option_name && @default_option_name != name
|
340
|
+
raise ArgumentError, "Multiple default options found for #{@component_type}"
|
341
|
+
end
|
342
|
+
|
343
|
+
@default_option_name = name
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
class OptionBuilder
|
348
|
+
attr_reader :name, :class_name, :attributes, :is_default, :is_only
|
349
|
+
|
350
|
+
def initialize(name)
|
351
|
+
@name = name
|
352
|
+
@attributes = {}
|
353
|
+
@is_default = false
|
354
|
+
end
|
355
|
+
|
356
|
+
def set_class(name)
|
357
|
+
if name.is_a?(Class)
|
358
|
+
@class_name = name
|
359
|
+
elsif name.is_a?(String)
|
360
|
+
@class_name = name.constantize
|
361
|
+
else
|
362
|
+
raise "Class name #{name} must be a string or class. Cannot find #{name}."
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def attribute(name, default: nil, source: nil, &block)
|
367
|
+
attr = ConfigAttr.new(name, default: default, source: source, &block)
|
368
|
+
@attributes[name.to_sym] = attr
|
369
|
+
end
|
370
|
+
|
371
|
+
def default
|
372
|
+
@is_default = true
|
373
|
+
end
|
374
|
+
|
375
|
+
def only
|
376
|
+
@is_only = true
|
377
|
+
end
|
378
|
+
|
379
|
+
def only?
|
380
|
+
@is_only == true
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GlueGun
|
2
|
+
module Types
|
3
|
+
class HashType < ActiveModel::Type::Value
|
4
|
+
def cast(value)
|
5
|
+
case value
|
6
|
+
when String
|
7
|
+
JSON.parse(value)
|
8
|
+
when Hash
|
9
|
+
value
|
10
|
+
else
|
11
|
+
{}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize(value)
|
16
|
+
value.to_json
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/glue_gun.rb
ADDED
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: glue_gun_dsl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brett Shollenberger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activemodel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '8'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '5.2'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '8'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '5.2'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '8'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: ostruct
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: polars-df
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: pry
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rspec
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '3.0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '3.0'
|
109
|
+
description: GlueGun makes dependency injection and hydration a first-order concern
|
110
|
+
email:
|
111
|
+
- brett.shollenberger@gmail.com
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- lib/glue_gun.rb
|
117
|
+
- lib/glue_gun/dsl.rb
|
118
|
+
- lib/glue_gun/types/hash_type.rb
|
119
|
+
- lib/glue_gun/version.rb
|
120
|
+
homepage: https://github.com/brettshollenberger/glue_gun_dsl
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '2.5'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubygems_version: 3.4.10
|
140
|
+
signing_key:
|
141
|
+
specification_version: 4
|
142
|
+
summary: GlueGun extends ActiveModel for dependency management
|
143
|
+
test_files: []
|