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,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ # Base class for dependency providers
7
+ #
8
+ # Providers allow organizing dependency registrations across multiple files.
9
+ # Each provider is responsible for registering a group of related dependencies.
10
+ #
11
+ # @example Basic provider
12
+ # class UserProvider < SenroUsecaser::Provider
13
+ # def register(container)
14
+ # container.register(:user_repository, UserRepository.new)
15
+ # container.register_singleton(:user_service) do |c|
16
+ # UserService.new(repo: c.resolve(:user_repository))
17
+ # end
18
+ # end
19
+ # end
20
+ #
21
+ # @example Provider with namespace
22
+ # class AdminProvider < SenroUsecaser::Provider
23
+ # namespace :admin
24
+ #
25
+ # def register(container)
26
+ # container.register(:user_repository, AdminUserRepository.new)
27
+ # end
28
+ # end
29
+ #
30
+ # @example Provider with dependencies
31
+ # class PersistenceProvider < SenroUsecaser::Provider
32
+ # depends_on CoreProvider
33
+ #
34
+ # def register(container)
35
+ # container.register(:database, Database.connect)
36
+ # end
37
+ # end
38
+ #
39
+ # @example Conditional provider
40
+ # class DevelopmentProvider < SenroUsecaser::Provider
41
+ # enabled_if { SenroUsecaser.env.development? }
42
+ # end
43
+ #
44
+ class Provider
45
+ class << self
46
+ # Declares dependencies on other providers
47
+ #
48
+ # @example
49
+ # class PersistenceProvider < SenroUsecaser::Provider
50
+ # depends_on CoreProvider
51
+ # depends_on ConfigProvider
52
+ # end
53
+ #
54
+ #: (*singleton(Provider)) -> void
55
+ def depends_on(*provider_classes)
56
+ provider_classes.each do |klass|
57
+ provider_dependencies << klass unless provider_dependencies.include?(klass)
58
+ end
59
+ end
60
+
61
+ # Returns the list of provider dependencies
62
+ #
63
+ #: () -> Array[singleton(Provider)]
64
+ def provider_dependencies
65
+ @provider_dependencies ||= [] #: Array[singleton(Provider)]
66
+ end
67
+
68
+ # Sets a condition for enabling this provider
69
+ #
70
+ # @example
71
+ # class DevelopmentProvider < SenroUsecaser::Provider
72
+ # enabled_if { Rails.env.development? }
73
+ # end
74
+ #
75
+ #: () { () -> bool } -> void
76
+ def enabled_if(&block)
77
+ @enabled_condition = block
78
+ end
79
+
80
+ # Returns whether this provider is enabled
81
+ #
82
+ #: () -> bool
83
+ def enabled?
84
+ return true unless @enabled_condition
85
+
86
+ @enabled_condition.call
87
+ end
88
+
89
+ # Sets the namespace for this provider's registrations
90
+ #
91
+ # @example
92
+ # class AdminProvider < SenroUsecaser::Provider
93
+ # namespace :admin
94
+ # end
95
+ #
96
+ #: ((Symbol | String)) -> void
97
+ def namespace(name = nil)
98
+ if name
99
+ @provider_namespace = name
100
+ else
101
+ @provider_namespace
102
+ end
103
+ end
104
+
105
+ # @rbs!
106
+ # def self.provider_namespace: () -> (Symbol | String)?
107
+ attr_reader :provider_namespace
108
+
109
+ # Registers this provider's dependencies to the given container
110
+ #
111
+ #: (Container) -> void
112
+ def call(container)
113
+ new.register_to(container)
114
+ end
115
+ end
116
+
117
+ # Registers dependencies to the container, wrapped in namespace if declared
118
+ #
119
+ #: (Container) -> void
120
+ def register_to(container)
121
+ before_register(container)
122
+
123
+ ns = effective_namespace
124
+ if ns
125
+ # Capture self to call provider's register method within namespace context
126
+ provider = self
127
+ container.namespace(ns) { provider.register(container) }
128
+ else
129
+ register(container)
130
+ end
131
+ end
132
+
133
+ # Returns the effective namespace for this provider
134
+ # Uses explicitly declared namespace, or infers from module structure if configured
135
+ #
136
+ #: () -> (Symbol | String)?
137
+ def effective_namespace
138
+ # Explicit namespace takes precedence
139
+ return self.class.provider_namespace if self.class.provider_namespace
140
+
141
+ # Infer from module structure if enabled
142
+ return nil unless SenroUsecaser.configuration.infer_namespace_from_module
143
+
144
+ infer_namespace_from_class
145
+ end
146
+
147
+ # Infers namespace from the class's module structure
148
+ #
149
+ # @example
150
+ # Admin::UserProvider => "admin"
151
+ # Admin::Reports::ReportProvider => "admin::reports"
152
+ # CoreProvider => nil
153
+ #
154
+ #: () -> String?
155
+ def infer_namespace_from_class
156
+ class_name = self.class.name
157
+ return nil unless class_name
158
+
159
+ parts = class_name.split("::")
160
+ return nil if parts.length <= 1
161
+
162
+ # Remove the class name itself, keep only module parts
163
+ module_parts = parts[0...-1] || [] #: Array[String]
164
+ return nil if module_parts.empty?
165
+
166
+ # Convert to lowercase namespace format (Admin::Reports => "admin::reports")
167
+ module_parts.map { |part| part.gsub(/([a-z])([A-Z])/, '\1_\2').downcase }.join("::")
168
+ end
169
+
170
+ # Called before register. Override in subclasses.
171
+ #
172
+ # @example
173
+ # def before_register(container)
174
+ # # Setup work
175
+ # end
176
+ #
177
+ #: (Container) -> void
178
+ def before_register(container); end
179
+
180
+ # Override this method to register dependencies
181
+ #
182
+ # @example
183
+ # def register(container)
184
+ # container.register(:logger, Logger.new)
185
+ # end
186
+ #
187
+ #: (Container) -> void
188
+ def register(container)
189
+ raise NotImplementedError, "#{self.class.name}#register must be implemented"
190
+ end
191
+
192
+ # Called after all providers are registered. Override in subclasses.
193
+ #
194
+ # @example
195
+ # def after_boot(container)
196
+ # container.resolve(:database).verify_connection!
197
+ # end
198
+ #
199
+ #: (Container) -> void
200
+ def after_boot(container); end
201
+
202
+ # Called on application shutdown. Override in subclasses.
203
+ #
204
+ # @example
205
+ # def shutdown(container)
206
+ # container.resolve(:database).disconnect
207
+ # end
208
+ #
209
+ #: (Container) -> void
210
+ def shutdown(container); end
211
+ end
212
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ # Represents the result of a UseCase execution
7
+ #
8
+ # Result is a generic type that holds either a success value or an array of errors.
9
+ # Use {.success} and {.failure} class methods to create instances.
10
+ #
11
+ # @example Success case
12
+ # result = SenroUsecaser::Result.success(user)
13
+ # result.success? # => true
14
+ # result.value # => user
15
+ #
16
+ # @example Failure case
17
+ # result = SenroUsecaser::Result.failure(
18
+ # SenroUsecaser::Error.new(code: :not_found, message: "User not found")
19
+ # )
20
+ # result.failure? # => true
21
+ # result.errors # => [#<SenroUsecaser::Error ...>]
22
+ #
23
+ # @rbs generic T
24
+ class Result
25
+ # @rbs!
26
+ # attr_reader value: T?
27
+ # attr_reader errors: Array[Error]
28
+
29
+ # @rbs skip
30
+ attr_reader :value
31
+ # @rbs skip
32
+ attr_reader :errors
33
+
34
+ # Creates a success Result with the given value
35
+ #
36
+ #: [T] (T) -> Result[T]
37
+ def self.success(value)
38
+ new(value: value, errors: [])
39
+ end
40
+
41
+ # Creates a failure Result with the given errors
42
+ #
43
+ #: (*Error) -> Result[untyped]
44
+ def self.failure(*errors)
45
+ errors = errors.flatten
46
+ raise ArgumentError, "At least one error is required for failure" if errors.empty?
47
+
48
+ new(value: nil, errors: errors)
49
+ end
50
+
51
+ # Creates a failure Result from an exception
52
+ #
53
+ # @example
54
+ # begin
55
+ # # some code that raises
56
+ # rescue => e
57
+ # Result.from_exception(e)
58
+ # end
59
+ #
60
+ #: (Exception, ?code: Symbol) -> Result[untyped]
61
+ def self.from_exception(exception, code: :exception)
62
+ error = Error.from_exception(exception, code: code)
63
+ failure(error)
64
+ end
65
+
66
+ # Executes a block and captures any exception as a failure Result
67
+ #
68
+ # @example
69
+ # result = Result.capture { User.find(id) }
70
+ # # If User.find raises, result is a failure with the exception
71
+ # # If User.find succeeds, result is a success with the return value
72
+ #
73
+ # @example With specific exception classes
74
+ # result = Result.capture(ActiveRecord::RecordNotFound, code: :not_found) do
75
+ # User.find(id)
76
+ # end
77
+ #
78
+ #: [T] (*Class, ?code: Symbol) { () -> T } -> Result[T]
79
+ def self.capture(*exception_classes, code: :exception, &block)
80
+ raise ArgumentError, "Block is required" unless block
81
+
82
+ exception_classes = [StandardError] if exception_classes.empty?
83
+ value = block.call
84
+ success(value)
85
+ rescue *exception_classes => e
86
+ from_exception(e, code: code)
87
+ end
88
+
89
+ #: (?value: T?, ?errors: Array[Error]) -> void
90
+ def initialize(value: nil, errors: [])
91
+ @value = value
92
+ @errors = errors.freeze
93
+ freeze
94
+ end
95
+
96
+ # Returns true if the result is a success
97
+ #
98
+ #: () -> bool
99
+ def success?
100
+ errors.empty?
101
+ end
102
+
103
+ # Returns true if the result is a failure
104
+ #
105
+ #: () -> bool
106
+ def failure?
107
+ !success?
108
+ end
109
+
110
+ # Returns the value if success, otherwise raises an error
111
+ #
112
+ #: () -> T
113
+ def value! # steep:ignore MethodBodyTypeMismatch
114
+ raise "Cannot unwrap value from a failure result" if failure?
115
+
116
+ @value
117
+ end
118
+
119
+ # Returns the value if success, otherwise returns the given default
120
+ #
121
+ #: [U] (U) -> (T | U)
122
+ def value_or(default) # steep:ignore MethodBodyTypeMismatch
123
+ if success?
124
+ @value
125
+ else
126
+ default
127
+ end
128
+ end
129
+
130
+ # Applies a block to the value if success, returns failure with same errors if failure
131
+ #
132
+ #: [U] () { (T) -> U } -> Result[U]
133
+ def map(&block)
134
+ return Result.new(value: nil, errors: errors) if failure?
135
+
136
+ # @type var v: untyped
137
+ v = @value
138
+ Result.success(block.call(v))
139
+ end
140
+
141
+ # Applies a block to the value if success, returns failure with same errors if failure
142
+ # The block should return a Result
143
+ #
144
+ #: [U] () { (T) -> Result[U] } -> Result[U]
145
+ def and_then(&block)
146
+ return Result.new(value: nil, errors: errors) if failure?
147
+
148
+ # @type var v: untyped
149
+ v = @value
150
+ block.call(v)
151
+ end
152
+
153
+ # Applies a block to the errors if failure, returns self if success
154
+ #
155
+ #: () { (Array[Error]) -> Result[T] } -> Result[T]
156
+ def or_else(&block)
157
+ return self if success?
158
+
159
+ block.call(errors)
160
+ end
161
+
162
+ #: (Result[untyped]) -> bool
163
+ def ==(other)
164
+ return false unless other.is_a?(Result)
165
+
166
+ # @type var v: untyped
167
+ v = @value
168
+ v == other.value && errors == other.errors
169
+ end
170
+
171
+ #: () -> String
172
+ def inspect
173
+ if success?
174
+ # @type var v: untyped
175
+ v = @value
176
+ "#<#{self.class.name} success value=#{v.inspect}>"
177
+ else
178
+ "#<#{self.class.name} failure errors=#{errors.inspect}>"
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module SenroUsecaser
6
+ #: String
7
+ VERSION = "0.1.0"
8
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ require_relative "senro_usecaser/version"
6
+ require_relative "senro_usecaser/error"
7
+ require_relative "senro_usecaser/result"
8
+ require_relative "senro_usecaser/container"
9
+ require_relative "senro_usecaser/configuration"
10
+ require_relative "senro_usecaser/provider"
11
+ require_relative "senro_usecaser/base"
12
+
13
+ # SenroUsecaser is a type-safe UseCase pattern implementation library for Ruby.
14
+ #
15
+ # It provides:
16
+ # - Type-safe input/output with RBS Inline
17
+ # - DI container with namespaces
18
+ # - Flexible composition with organize/include/extend patterns
19
+ #
20
+ # @example Basic UseCase
21
+ # class CreateUserUseCase < SenroUsecaser::Base
22
+ # def call(name:, email:)
23
+ # user = User.create(name: name, email: email)
24
+ # success(user)
25
+ # end
26
+ # end
27
+ #
28
+ # result = CreateUserUseCase.call(name: "Taro", email: "taro@example.com")
29
+ # if result.success?
30
+ # puts result.value.name
31
+ # end
32
+ module SenroUsecaser
33
+ class << self
34
+ # Returns the global container instance
35
+ #
36
+ # @example
37
+ # SenroUsecaser.container.register(:logger, Logger.new)
38
+ #
39
+ #: () -> Container
40
+ def container
41
+ @container ||= Container.new
42
+ end
43
+
44
+ # Sets the global container instance
45
+ #
46
+ # @example
47
+ # SenroUsecaser.container = MyCustomContainer.new
48
+ #
49
+ #: (Container) -> Container
50
+ attr_writer :container
51
+
52
+ # Resets the global container (useful for testing)
53
+ #
54
+ #: () -> void
55
+ def reset_container!
56
+ @container = nil
57
+ end
58
+
59
+ # Registers a provider to the global container
60
+ #
61
+ # @example
62
+ # SenroUsecaser.register_provider(UserProvider)
63
+ #
64
+ #: (singleton(Provider)) -> void
65
+ def register_provider(provider_class)
66
+ provider_class.call(container)
67
+ end
68
+
69
+ # Registers multiple providers to the global container
70
+ #
71
+ # @example
72
+ # SenroUsecaser.register_providers(UserProvider, OrderProvider, PaymentProvider)
73
+ #
74
+ #: (*singleton(Provider)) -> void
75
+ def register_providers(*provider_classes)
76
+ provider_classes.each { |klass| register_provider(klass) }
77
+ end
78
+
79
+ # Returns the configuration instance
80
+ #
81
+ #: () -> Configuration
82
+ def configuration
83
+ @configuration ||= Configuration.new
84
+ end
85
+
86
+ # Configures SenroUsecaser
87
+ #
88
+ # @example
89
+ # SenroUsecaser.configure do |config|
90
+ # config.providers = [CoreProvider, UserProvider]
91
+ # config.infer_namespace_from_module = true
92
+ # end
93
+ #
94
+ #: () { (Configuration) -> void } -> void
95
+ def configure
96
+ yield configuration
97
+ end
98
+
99
+ # Boots all configured providers in dependency order
100
+ #
101
+ # @example
102
+ # SenroUsecaser.configure do |config|
103
+ # config.providers = [CoreProvider, UserProvider]
104
+ # end
105
+ # SenroUsecaser.boot!
106
+ #
107
+ #: () -> void
108
+ def boot!
109
+ @booter = ProviderBooter.new(configuration.providers, container)
110
+ @booter.boot!
111
+ end
112
+
113
+ # Shuts down all booted providers
114
+ #
115
+ #: () -> void
116
+ def shutdown!
117
+ @booter&.shutdown!
118
+ end
119
+
120
+ # Returns the current environment
121
+ #
122
+ # @example
123
+ # SenroUsecaser.env.development?
124
+ # SenroUsecaser.env.production?
125
+ #
126
+ #: () -> Environment
127
+ def env
128
+ @env ||= Environment.new(detect_environment)
129
+ end
130
+
131
+ # Sets the environment
132
+ #
133
+ #: (String) -> void
134
+ def env=(name)
135
+ @env = Environment.new(name)
136
+ end
137
+
138
+ # Resets all state (useful for testing)
139
+ #
140
+ #: () -> void
141
+ def reset!
142
+ @container = nil
143
+ @configuration = nil
144
+ @booter = nil
145
+ @env = nil
146
+ end
147
+
148
+ private
149
+
150
+ #: () -> String
151
+ def detect_environment
152
+ ENV["SENRO_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
153
+ end
154
+ end
155
+ end