glue_gun_dsl 0.1.0

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