senro_usecaser 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.
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ # Configuration for SenroUsecaser
7
+ #
8
+ # @example
9
+ # SenroUsecaser.configure do |config|
10
+ # config.providers = [CoreProvider, UserProvider]
11
+ # config.infer_namespace_from_module = true
12
+ # end
13
+ #
14
+ class Configuration
15
+ # List of provider classes to boot
16
+ #: () -> Array[singleton(Provider)]
17
+ attr_accessor :providers
18
+
19
+ # Whether to infer namespace from module structure
20
+ #: () -> bool
21
+ attr_accessor :infer_namespace_from_module
22
+
23
+ #: () -> void
24
+ def initialize
25
+ @providers = []
26
+ @infer_namespace_from_module = false
27
+ end
28
+ end
29
+
30
+ # Environment detection
31
+ #
32
+ # @example
33
+ # SenroUsecaser.env.development?
34
+ # SenroUsecaser.env.production?
35
+ #
36
+ class Environment
37
+ #: (String) -> void
38
+ def initialize(name)
39
+ @name = name
40
+ end
41
+
42
+ #: () -> String
43
+ attr_reader :name
44
+
45
+ #: () -> bool
46
+ def development?
47
+ @name == "development"
48
+ end
49
+
50
+ #: () -> bool
51
+ def test?
52
+ @name == "test"
53
+ end
54
+
55
+ #: () -> bool
56
+ def production?
57
+ @name == "production"
58
+ end
59
+
60
+ #: () -> String
61
+ def to_s
62
+ @name
63
+ end
64
+ end
65
+
66
+ # Provider boot manager
67
+ #
68
+ # Resolves provider dependencies and boots them in correct order.
69
+ #
70
+ class ProviderBooter
71
+ # Error raised when circular dependencies are detected
72
+ class CircularDependencyError < StandardError; end
73
+
74
+ #: (Array[singleton(Provider)], Container) -> void
75
+ def initialize(provider_classes, container)
76
+ @provider_classes = provider_classes
77
+ @container = container
78
+ @booted_providers = [] #: Array[singleton(Provider)]
79
+ @provider_instances = {} #: Hash[singleton(Provider), Provider]
80
+ end
81
+
82
+ # Boots all providers in dependency order
83
+ #
84
+ #: () -> void
85
+ def boot!
86
+ sorted = topological_sort(@provider_classes)
87
+
88
+ sorted.each do |provider_class|
89
+ next unless provider_class.enabled?
90
+
91
+ instance = provider_class.new
92
+ @provider_instances[provider_class] = instance
93
+ instance.register_to(@container)
94
+ @booted_providers << provider_class
95
+ end
96
+
97
+ # Call after_boot on all providers
98
+ @booted_providers.each do |provider_class|
99
+ @provider_instances[provider_class].after_boot(@container)
100
+ end
101
+ end
102
+
103
+ # Shuts down all providers in reverse order
104
+ #
105
+ #: () -> void
106
+ def shutdown!
107
+ @booted_providers.reverse_each do |provider_class|
108
+ @provider_instances[provider_class].shutdown(@container)
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ # Topologically sorts providers based on dependencies
115
+ #
116
+ #: (Array[singleton(Provider)]) -> Array[singleton(Provider)]
117
+ def topological_sort(providers)
118
+ sorted = [] #: Array[singleton(Provider)]
119
+ visited = {} #: Hash[singleton(Provider), bool]
120
+ visiting = {} #: Hash[singleton(Provider), bool]
121
+
122
+ providers.each do |provider|
123
+ visit(provider, sorted, visited, visiting) unless visited[provider]
124
+ end
125
+
126
+ sorted
127
+ end
128
+
129
+ #: (singleton(Provider), Array[singleton(Provider)], untyped, untyped) -> void
130
+ def visit(provider, sorted, visited, visiting)
131
+ return if visited[provider]
132
+
133
+ if visiting[provider]
134
+ raise CircularDependencyError,
135
+ "Circular dependency detected involving #{provider.name}"
136
+ end
137
+
138
+ visiting[provider] = true
139
+
140
+ provider.provider_dependencies.each do |dep|
141
+ visit(dep, sorted, visited, visiting) unless visited[dep]
142
+ end
143
+
144
+ visiting.delete(provider)
145
+ visited[provider] = true
146
+ sorted << provider
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ # Wrapper for singleton registrations that caches the result
7
+ class SingletonRegistration
8
+ #: (^(Container) -> untyped) -> void
9
+ def initialize(block)
10
+ @block = block
11
+ @resolved = false
12
+ @value = nil
13
+ end
14
+
15
+ #: (Container) -> untyped
16
+ def call(container)
17
+ unless @resolved
18
+ @value = @block.call(container)
19
+ @resolved = true
20
+ end
21
+ @value
22
+ end
23
+ end
24
+
25
+ # DI Container with namespace support
26
+ #
27
+ # @example Basic usage
28
+ # container = SenroUsecaser::Container.new
29
+ # container.register(:logger, Logger.new)
30
+ # container.resolve(:logger) # => Logger instance
31
+ #
32
+ # @example With namespaces
33
+ # container = SenroUsecaser::Container.new
34
+ # container.register(:logger, Logger.new)
35
+ #
36
+ # container.namespace(:admin) do
37
+ # register(:user_repository, AdminUserRepository.new)
38
+ # end
39
+ #
40
+ # # From admin namespace, can resolve both admin and root dependencies
41
+ # container.resolve_in(:admin, :user_repository) # => AdminUserRepository
42
+ # container.resolve_in(:admin, :logger) # => Logger (from root)
43
+ class Container
44
+ # Error raised when a dependency cannot be resolved
45
+ class ResolutionError < StandardError; end
46
+
47
+ # Error raised when a dependency is already registered
48
+ class DuplicateRegistrationError < StandardError; end
49
+
50
+ #: (?parent: Container?) -> void
51
+ def initialize(parent: nil)
52
+ @parent = parent
53
+ @registrations = {} #: Hash[String, untyped]
54
+ @current_namespace = [] #: Array[Symbol]
55
+ end
56
+
57
+ # Creates a scoped child container that inherits from this container
58
+ #
59
+ # @example
60
+ # scoped = container.scope do
61
+ # register(:current_user, current_user)
62
+ # end
63
+ # scoped.resolve(:current_user) # => current_user
64
+ # scoped.resolve(:logger) # => resolved from parent
65
+ #
66
+ #: () ?{ () -> void } -> Container
67
+ def scope(&block)
68
+ child = Container.new(parent: self)
69
+ child.instance_eval(&block) if block # steep:ignore BlockTypeMismatch
70
+ child
71
+ end
72
+
73
+ # Registers a dependency in the current namespace
74
+ #
75
+ # @example With value (returns same value every time)
76
+ # container.register(:logger, Logger.new)
77
+ #
78
+ # @example With block (lazy evaluation, called every time, receives container)
79
+ # container.register(:database) { |container| Database.connect }
80
+ #
81
+ #: (Symbol, ?untyped) ?{ (Container) -> untyped } -> void
82
+ def register(key, value = nil, &block)
83
+ raise ArgumentError, "Provide either a value or a block, not both" if value && block
84
+ raise ArgumentError, "Provide either a value or a block" if value.nil? && block.nil?
85
+
86
+ full_key = build_key(key)
87
+ check_duplicate_registration!(full_key)
88
+
89
+ @registrations[full_key] = block || ->(_) { value }
90
+ end
91
+
92
+ # Registers a lazy dependency (block is called every time on resolve)
93
+ #
94
+ # @example
95
+ # container.register_lazy(:connection) { |c| Database.connect }
96
+ #
97
+ # @example With dependency resolution
98
+ # container.register_lazy(:user_repository) do |container|
99
+ # UserRepository.new(current_user: container.resolve(:current_user))
100
+ # end
101
+ #
102
+ #: [T] (Symbol) { (Container) -> T } -> void
103
+ def register_lazy(key, &block)
104
+ raise ArgumentError, "Block is required for register_lazy" unless block
105
+
106
+ full_key = build_key(key)
107
+ check_duplicate_registration!(full_key)
108
+
109
+ @registrations[full_key] = block
110
+ end
111
+
112
+ # Registers a singleton dependency (block is called once and cached)
113
+ #
114
+ # @example
115
+ # container.register_singleton(:database) { |c| Database.connect }
116
+ # container.resolve(:database) # => same instance every time
117
+ #
118
+ # @example With dependency resolution
119
+ # container.register_singleton(:service) do |container|
120
+ # Service.new(logger: container.resolve(:logger))
121
+ # end
122
+ #
123
+ #: [T] (Symbol) { (Container) -> T } -> void
124
+ def register_singleton(key, &block)
125
+ raise ArgumentError, "Block is required for register_singleton" unless block
126
+
127
+ full_key = build_key(key)
128
+ check_duplicate_registration!(full_key)
129
+
130
+ @registrations[full_key] = SingletonRegistration.new(block)
131
+ end
132
+
133
+ # Resolves a dependency from the current namespace or its ancestors
134
+ #
135
+ # @example
136
+ # container.resolve(:logger)
137
+ #
138
+ #: [T] (Symbol) -> T
139
+ def resolve(key)
140
+ resolve_in(current_namespace_path, key)
141
+ end
142
+
143
+ # Resolves a dependency from a specific namespace or its ancestors
144
+ #
145
+ # @example
146
+ # container.resolve_in(:admin, :logger)
147
+ # container.resolve_in("admin::reports", :generator)
148
+ #
149
+ #: [T] ((Symbol | String | Array[Symbol]), Symbol) -> T
150
+ def resolve_in(namespace, key)
151
+ registration = find_registration(namespace, key)
152
+
153
+ unless registration
154
+ raise ResolutionError,
155
+ "Dependency #{key.inspect} not found in namespace #{namespace.inspect} or its ancestors"
156
+ end
157
+
158
+ # Always invoke with self (the resolving container) for proper scoping
159
+ invoke_registration(registration)
160
+ end
161
+
162
+ # Checks if a dependency is registered
163
+ #
164
+ #: (Symbol) -> bool
165
+ def registered?(key)
166
+ registered_in?(current_namespace_path, key)
167
+ end
168
+
169
+ # Checks if a dependency is registered in a specific namespace or its ancestors
170
+ #
171
+ #: ((Symbol | String | Array[Symbol]), Symbol) -> bool
172
+ def registered_in?(namespace, key)
173
+ namespace_parts = normalize_namespace(namespace)
174
+
175
+ (namespace_parts.length + 1).times do |i|
176
+ current_parts = namespace_parts[0, namespace_parts.length - i] || []
177
+ full_key = build_key_with_namespace(current_parts, key)
178
+ return true if @registrations.key?(full_key)
179
+ end
180
+
181
+ # Check parent container if available
182
+ return @parent.registered_in?(namespace, key) if @parent
183
+
184
+ false
185
+ end
186
+
187
+ # Creates a namespace scope for registering dependencies
188
+ #
189
+ # @example
190
+ # container.namespace(:admin) do
191
+ # register(:user_repository, AdminUserRepository.new)
192
+ #
193
+ # namespace(:reports) do
194
+ # register(:generator, ReportGenerator.new)
195
+ # end
196
+ # end
197
+ #
198
+ #: ((Symbol | String)) { () -> void } -> void
199
+ def namespace(name, &)
200
+ @current_namespace.push(name.to_sym)
201
+ instance_eval(&) # steep:ignore BlockTypeMismatch
202
+ ensure
203
+ @current_namespace.pop
204
+ end
205
+
206
+ # Returns all registered keys (including parent keys)
207
+ #
208
+ #: () -> Array[String]
209
+ def keys
210
+ own_keys = @registrations.keys
211
+ return own_keys unless @parent
212
+
213
+ (own_keys + @parent.keys).uniq
214
+ end
215
+
216
+ # Returns only keys registered in this container (excluding parent)
217
+ #
218
+ #: () -> Array[String]
219
+ def own_keys
220
+ @registrations.keys
221
+ end
222
+
223
+ # Returns the parent container if any
224
+ #
225
+ #: () -> Container?
226
+ attr_reader :parent
227
+
228
+ # Clears all registrations
229
+ #
230
+ #: () -> void
231
+ def clear!
232
+ @registrations.clear
233
+ end
234
+
235
+ private
236
+
237
+ #: () -> String
238
+ def current_namespace_path
239
+ @current_namespace.join("::")
240
+ end
241
+
242
+ #: (Symbol) -> String
243
+ def build_key(key)
244
+ build_key_with_namespace(@current_namespace, key)
245
+ end
246
+
247
+ #: (Array[Symbol], Symbol) -> String
248
+ def build_key_with_namespace(namespace_parts, key)
249
+ if namespace_parts.empty?
250
+ key.to_s
251
+ else
252
+ "#{namespace_parts.join("::")}::#{key}"
253
+ end
254
+ end
255
+
256
+ #: ((Symbol | String | Array[Symbol])) -> Array[Symbol]
257
+ def normalize_namespace(namespace)
258
+ case namespace
259
+ when Array then normalize_array_namespace(namespace)
260
+ when Symbol then normalize_symbol_namespace(namespace)
261
+ when String then normalize_string_namespace(namespace)
262
+ else raise ArgumentError, "Invalid namespace: #{namespace.inspect}"
263
+ end
264
+ end
265
+
266
+ #: (Array[Symbol]) -> Array[Symbol]
267
+ def normalize_array_namespace(namespace)
268
+ namespace.map(&:to_sym)
269
+ end
270
+
271
+ #: (Symbol) -> Array[Symbol]
272
+ def normalize_symbol_namespace(namespace)
273
+ namespace == :root ? [] : [namespace]
274
+ end
275
+
276
+ #: (String) -> Array[Symbol]
277
+ def normalize_string_namespace(namespace)
278
+ namespace.empty? ? [] : namespace.split("::").map(&:to_sym)
279
+ end
280
+
281
+ #: (String) -> void
282
+ def check_duplicate_registration!(full_key)
283
+ return unless @registrations.key?(full_key)
284
+
285
+ raise DuplicateRegistrationError, "Dependency #{full_key.inspect} is already registered"
286
+ end
287
+
288
+ # Invokes a registration, passing the container for dependency resolution
289
+ #
290
+ #: (untyped) -> untyped
291
+ def invoke_registration(registration)
292
+ registration.call(self)
293
+ end
294
+
295
+ protected
296
+
297
+ # Finds a registration in this container or its parent chain
298
+ #
299
+ #: ((Symbol | String | Array[Symbol]), Symbol) -> untyped
300
+ def find_registration(namespace, key)
301
+ namespace_parts = normalize_namespace(namespace)
302
+
303
+ # Try to find in the specified namespace and its ancestors
304
+ (namespace_parts.length + 1).times do |i|
305
+ current_parts = namespace_parts[0, namespace_parts.length - i] || []
306
+ full_key = build_key_with_namespace(current_parts, key)
307
+
308
+ return @registrations[full_key] if @registrations.key?(full_key)
309
+ end
310
+
311
+ # Fall back to parent container if available
312
+ @parent&.find_registration(namespace, key)
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ # Represents an error in a Result
7
+ #
8
+ # @example Basic error
9
+ # error = SenroUsecaser::Error.new(
10
+ # code: :invalid_email,
11
+ # message: "Email format is invalid",
12
+ # field: :email
13
+ # )
14
+ #
15
+ # @example Error from exception
16
+ # begin
17
+ # # some code that raises
18
+ # rescue => e
19
+ # error = SenroUsecaser::Error.new(
20
+ # code: :unexpected_error,
21
+ # message: e.message,
22
+ # cause: e
23
+ # )
24
+ # end
25
+ class Error
26
+ #: Symbol
27
+ attr_reader :code
28
+
29
+ #: String
30
+ attr_reader :message
31
+
32
+ #: Symbol?
33
+ attr_reader :field
34
+
35
+ #: Exception?
36
+ attr_reader :cause
37
+
38
+ #: (code: Symbol, message: String, ?field: Symbol?, ?cause: Exception?) -> void
39
+ def initialize(code:, message:, field: nil, cause: nil)
40
+ @code = code
41
+ @message = message
42
+ @field = field
43
+ @cause = cause
44
+ end
45
+
46
+ # Creates an Error from an exception
47
+ #
48
+ #: (Exception, ?code: Symbol) -> Error
49
+ def self.from_exception(exception, code: :exception)
50
+ new(
51
+ code: code,
52
+ message: exception.message,
53
+ cause: exception
54
+ )
55
+ end
56
+
57
+ # Returns true if this error was caused by an exception
58
+ #
59
+ #: () -> bool
60
+ def caused_by_exception?
61
+ !cause.nil?
62
+ end
63
+
64
+ #: (Error) -> bool
65
+ def ==(other)
66
+ return false unless other.is_a?(Error)
67
+
68
+ code == other.code && message == other.message && field == other.field && cause == other.cause
69
+ end
70
+
71
+ #: () -> String
72
+ def to_s
73
+ base = field ? "[#{field}] #{message} (#{code})" : "#{message} (#{code})"
74
+ cause ? "#{base} caused by #{cause.class}" : base
75
+ end
76
+
77
+ #: () -> String
78
+ def inspect
79
+ parts = [
80
+ "code=#{code.inspect}",
81
+ "message=#{message.inspect}",
82
+ "field=#{field.inspect}"
83
+ ]
84
+ parts << "cause=#{cause.class}" if cause
85
+ "#<#{self.class.name} #{parts.join(" ")}>"
86
+ end
87
+ end
88
+ end