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 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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GlueGunDsl
4
+ VERSION = "0.1.0"
5
+ end
data/lib/glue_gun.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "active_model"
2
+ require "active_support/concern"
3
+
4
+ module GlueGun
5
+ require_relative "glue_gun/types/hash_type"
6
+ ActiveModel::Type.register(:hash, GlueGun::Types::HashType)
7
+
8
+ require_relative "glue_gun/dsl"
9
+ end
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: []