mortymer 0.0.9 → 0.0.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e65cf959fadc75136e6438cb640a19a62315987b7825659d24977ca23df81f1
4
- data.tar.gz: '069dac56627ea192bb9f331f4e876f78d0a3a7a2699b4dee484ab591183cb2ce'
3
+ metadata.gz: 1aaae1b0260575ca60caeb8b676577f39766162d8127a6266e1c49e168e11265
4
+ data.tar.gz: 0f92cfaa12e157dcb4954259dd284b7b46f7305dd912d84a55fef597107997f7
5
5
  SHA512:
6
- metadata.gz: 80c1c763739396b3f8bfbe5c2da14ab17f25536ebdfbfc39af256ecb51a706b3fc2994204d992faa4e7fb5a912c32252b86687f4f2fbe115ba9bdf1bafb3ebee
7
- data.tar.gz: 7e67f9ba29deead0905eaf598523aa93566a482925c94b2d635d584415d06e7184f064524ca9edf713002f5cb0cab10a2e25c781dc762ffa9eb51be48589a9b6
6
+ metadata.gz: c5c3932bfb5b0daf315af6f8f4a1ef36b0a448229fd15b9c71f7c8699d8f41fcfeb8a5858686b55d6c7905628936908512e97537b6a5ec0024061f6b3f4bcd60
7
+ data.tar.gz: df393725ff3c29b36ede225c046f8f27cbfea156f09917cd1976b8c3188f9dafa5e57e0aa0876a378f7afda62ab0ea06b17f500d0f46f6d8caae18c249c13acf
@@ -7,7 +7,10 @@ export default defineConfig({
7
7
  description:
8
8
  "Standalone API definition for ruby frameworks based on dry.rb. Rails compatible from day 0 with a full Ruby flavored dependency injection engine.",
9
9
  markdown: {
10
- theme: "catppuccin-mocha",
10
+ theme: {
11
+ light: "catppuccin-latte",
12
+ dark: "catppuccin-mocha",
13
+ },
11
14
  },
12
15
  themeConfig: {
13
16
  nav: [
@@ -36,7 +39,17 @@ export default defineConfig({
36
39
  { text: "Type System", link: "/guide/type-system" },
37
40
  {
38
41
  text: "Dependency Injection",
39
- link: "/guide/dependency-injection",
42
+ items: [
43
+ {
44
+ text: "Introduction to DI in Ruby",
45
+ link: "/guide/dependency-injection",
46
+ },
47
+
48
+ {
49
+ text: "DI in Mortymer",
50
+ link: "/guide/mortymer-dependency-injection",
51
+ },
52
+ ],
40
53
  },
41
54
  { text: "Error Handling", link: "/guide/error-handling" },
42
55
  ],
@@ -63,12 +76,12 @@ export default defineConfig({
63
76
  },
64
77
 
65
78
  socialLinks: [
66
- { icon: "github", link: "https://github.com/yourusername/morty" },
79
+ { icon: "github", link: "https://github.com/adriangs1996/morty" },
67
80
  ],
68
81
 
69
82
  footer: {
70
83
  message: "Released under the MIT License.",
71
- copyright: "Copyright © 2024-present",
84
+ copyright: "Copyright © 2025-present",
72
85
  },
73
86
  },
74
87
  });
@@ -1,5 +1,3 @@
1
-
2
1
  html {
3
2
  font-family: Arial, Helvetica;
4
3
  }
5
-
@@ -0,0 +1,205 @@
1
+ # Dependency Injection
2
+
3
+ ## What is Dependency Injection?
4
+
5
+ Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for managing dependencies between components.
6
+ Instead of having your components create or find their dependencies, these dependencies are "injected" into the component from the outside.
7
+
8
+ Consider this example without dependency injection:
9
+
10
+ ```ruby
11
+ class UserService
12
+ def initialize
13
+ @repository = UserRepository.new
14
+ @mailer = UserMailer.new
15
+ end
16
+
17
+ def create_user(params)
18
+ user = @repository.create(params)
19
+ @mailer.send_welcome_email(user)
20
+ user
21
+ end
22
+ end
23
+ ```
24
+
25
+ In the above code, UserService explicitly creates instances of UserRepository and UserMailer,
26
+ making it tightly coupled to these dependencies. When arguing why DI is usefull, commonly
27
+ the following arguments arise:
28
+
29
+ - <b>Harder to Test</b>: We cannot easily substitute UserRepository or UserMailer with mocks or stubs during testing.
30
+ This is not entirely true, as in Ruby you can mock pretty much everything, so this does not make a really good
31
+ argument, although mocking becomes easier when using DI.
32
+
33
+ - <b>Less Flexible</b>: If we later decide to use a different repository or mailer, we must modify UserService directly.
34
+ Think if we have two implementations of UserMailer, for some reason you want to send emails using SendGrid, but other times
35
+ you want to send emails using your cloud provider solution, or your custom server. UserService really does not cares
36
+ about how this is implemented or where the emails goes, it just need to send an email for whatever channel we choose.
37
+
38
+ - <b>Tightly Coupled</b>: UserService is responsible for constructing its dependencies, making it harder to manage and extend.
39
+
40
+ A DI-based approach improves flexibility and testability:
41
+
42
+ ```ruby
43
+ class UserService
44
+ def initialize(repository:, mailer:)
45
+ @repository = repository
46
+ @mailer = mailer
47
+ end
48
+
49
+ def create_user(params)
50
+ user = @repository.create(params)
51
+ @mailer.send_welcome_email(user)
52
+ user
53
+ end
54
+ end
55
+
56
+ # Inject dependencies
57
+ repository = UserRepository.new
58
+ mailer = UserMailer.new
59
+ service = UserService.new(repository, mailer)
60
+ ```
61
+
62
+ Now, UserService does not need to know how to instantiate its dependencies. Instead, they are provided externally.
63
+
64
+ ## The Ruby Community’s Perspective on Dependency Injection
65
+
66
+ Rubyists often prefer simplicity and flexibility, so DI is not as commonly enforced as in other static-typed languages like Java or C#. This is a topic that spans very different point of views. Just to give an example, read the following Reddit thread
67
+ <https://www.reddit.com/r/ruby/comments/10x6w8q/dependency_injection/>
68
+
69
+ It discusses the use of Dependency Injection (DI) in Ruby, particularly within the context of Ruby on Rails (RoR). Several key points emerge from the conversation:
70
+
71
+ - <b>Testing and Metaprogramming</b>: One user notes that in languages like Java and PHP, DI is often employed to facilitate testing by allowing the injection of mock dependencies. However, in Ruby, the language's metaprogramming capabilities enable developers to mock dependencies directly without the need for DI containers, potentially simplifying the codebase.
72
+
73
+ - <b>Code Organization and Dependency Tracking</b>: Another perspective suggests that DI serves as a method to organize code systematically and maintain an efficient track of dependencies. Improved testability is viewed as a beneficial byproduct of this organization.
74
+
75
+ - <b>Coupling and Code Simplicity</b>: It's acknowledged that while avoiding DI can lead to simpler code, it may also result in tighter coupling between components. This necessitates a careful balance to ensure that the code remains maintainable without becoming overly complex.
76
+
77
+ In summary, the thread reflects a nuanced view within the Ruby community regarding DI. Some developers prefer to leverage Ruby's dynamic features and metaprogramming to manage dependencies, while others advocate for DI as a means to achieve cleaner code organization and dependency management. The choice often depends on the specific requirements and context of the project.
78
+
79
+ ## The way of Mortymer
80
+
81
+ I think that DI is necessary, no matter how you decide to handle it. To be aware of
82
+ what your functionality depends on is crucial when evaluating the impact of changes. While
83
+ testability benefits exist in Ruby through its powerful metaprogramming capabilities, the main
84
+ advantage of DI is the explicitness about what your code does and what it depends on. This
85
+ transparency makes the code more maintainable and easier to understand.
86
+
87
+ Additionally, DI naturally supports the Open-Closed Principle (the 'O' in SOLID): when
88
+ dependencies are injected rather than hardcoded, new functionality can be added by creating
89
+ new implementations of interfaces rather than modifying existing code. While it's technically
90
+ possible to extend functionality without DI in Ruby, doing so often requires more complex
91
+ metaprogramming or direct code modifications, making the system harder to maintain and understand
92
+ over time.
93
+
94
+ With this in mind, we can explore different approaches to do Dependency Injection in Ruby.
95
+
96
+ ### Constructor Injection
97
+
98
+ This is by far the most common implementation of DI. This includes initialization of objects
99
+ and dependencies are passed as parameters. In ruby, we would do it like:
100
+
101
+ ```ruby
102
+ class UserService
103
+ def initialize(repository: UserRepository.new, mailer: UserMailer.new)
104
+ @repository = repository
105
+ @mailer = mailer
106
+ end
107
+
108
+ def find_user(user_id)
109
+ # Use @repository and @mailer
110
+ end
111
+ end
112
+ ```
113
+
114
+ Note how we added a default value to the repository and mailer dependency. This is a convenience
115
+ that will let programmers to initialize easily an `UserService` using just the `#new` method
116
+ and make it clearer which interface ( any repository we use here must conform to the same interface
117
+ as `UserRepository` and the same goes to `UserMailer`)
118
+
119
+ ### Method injection
120
+
121
+ Passing dependencies directly to a method when needed:
122
+
123
+ ```ruby
124
+ class UserService
125
+ def create_user(params, repository: UserRepository.new, mailer: UserMailer.new)
126
+ user = repository.create(params)
127
+ mailer.send_welcome_email(user)
128
+ user
129
+ end
130
+ end
131
+ ```
132
+
133
+ This approach is less common, but is really useful when you have some classes with different
134
+ dependencies for different methods. This is generally easy to avoid tough, by using the Command Pattern
135
+ or Service Objects with constructor Injection.
136
+
137
+ ### Global Injection
138
+
139
+ Global Injection is often used in frameworks like Ruby on Rails,
140
+ where dependencies can be globally configured and injected throughout the application.
141
+
142
+ ```ruby
143
+ # config/initializers/dependencies.rb
144
+ MyAppDependencies = {
145
+ user_repository: UserRepository.new,
146
+ mailer: UserMailer.new
147
+ }
148
+
149
+ # In the service
150
+ class UserService
151
+ def initialize(repository: MyAppDependencies[:user_repository], mailer: MyAppDependencies[:mailer])
152
+ @repository = repository
153
+ @mailer = mailer
154
+ end
155
+
156
+ def create_user(params)
157
+ user = @repository.create(params)
158
+ @mailer.send_welcome_email(user)
159
+ user
160
+ end
161
+ end
162
+ ```
163
+
164
+ ### Dependency Injection Containers
165
+
166
+ Gems like dry-container and dry-auto_inject help manage dependencies in larger applications:
167
+
168
+ ```ruby
169
+ class UserService
170
+ include Import["repositories.user_repository", "mailers.user_mailer"]
171
+
172
+ def create_user(params)
173
+ user = user_repository.create(params)
174
+ user_mailer.send_welcome_email(user)
175
+ user
176
+ end
177
+ end
178
+ ```
179
+
180
+ This category is where Mortymer fit in. Actually, DI Containers all essentially do the same: to make developer
181
+ life easier when declaring and handling this dependencies. For example, the first approach we discuss, **Constructor Injection**,
182
+ becomes really tedious to use, there is a lot of boilerplate code to write. Imaging all the time writing the same
183
+ initialize function receiving your dependencies and setting the instance variables. Well, gems do exactly that, remove
184
+ the boilerplate for you. The main difference is in how they do it and what you get in return. Gems like dry-auto_inject
185
+ , like shown above, focus on constants resolutions through strings, and dynamically created read methods for your dependencies.
186
+
187
+ Mortymer does it in a very different way. We favor dependencies being referenced using constants, because that
188
+ empowers code navigation and tools like LSP with go to definitions and such features. Also, referencing constants, allows
189
+ us to use a similar API that is found on strongly-typed languages like Java. The other main difference is that Mortymer will
190
+ not create methods on the fly for you, you will get instance variables. The same example using Mortymer looks like:
191
+
192
+ ```ruby
193
+ class UserService
194
+ include Mortymer::DependenciesDsl
195
+
196
+ inject Repositories::UserRepository
197
+ inject Mailers::UserMailer, as: :mailer
198
+
199
+ def create_user(params)
200
+ user = @user_repository.create(params)
201
+ @mailer.send_welcome_email(user)
202
+ user
203
+ end
204
+ end
205
+ ```
@@ -0,0 +1,398 @@
1
+ # Mortymer Dependency Injection Guide
2
+
3
+ ## Overview
4
+
5
+ Mortymer provides a powerful yet simple dependency injection system that favors explicit constant references and instance variables over dynamic method creation. This guide will walk you through the various ways to use dependency injection with Mortymer.
6
+
7
+ ## Basic Usage
8
+
9
+ ### Simple Injection
10
+
11
+ The most basic form of dependency injection in Mortymer uses the `inject` directive:
12
+
13
+ ```ruby
14
+ class UserService
15
+ include Mortymer::DependenciesDsl
16
+
17
+ inject UserRepository
18
+
19
+ def find_user(id)
20
+ @user_repository.find(id)
21
+ end
22
+ end
23
+ ```
24
+
25
+ When you include `Mortymer::DependenciesDsl`, you get access to the `inject` directive. This will:
26
+
27
+ 1. Create an instance variable `@user_repository`
28
+ 2. Automatically initialize it with an instance of `UserRepository`
29
+
30
+ ### Custom Variable Names
31
+
32
+ You can customize the instance variable name using the `as` option:
33
+
34
+ ```ruby
35
+ class UserService
36
+ include Mortymer::DependenciesDsl
37
+
38
+ inject UserRepository, as: :users
39
+
40
+ def find_user(id)
41
+ @users.find(id)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### Multiple Dependencies
47
+
48
+ You can inject multiple dependencies in a single class:
49
+
50
+ ```ruby
51
+ class UserService
52
+ include Mortymer::DependenciesDsl
53
+
54
+ inject UserRepository
55
+ inject UserMailer
56
+ inject Logger, as: :logger
57
+
58
+ def create_user(params)
59
+ @logger.info("Creating new user")
60
+ user = @user_repository.create(params)
61
+ @user_mailer.send_welcome_email(user)
62
+ user
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Advanced Usage
68
+
69
+ ### Dependency Scopes
70
+
71
+ Mortymer supports different scopes for dependencies:
72
+
73
+ #### Singleton Scope (Default)
74
+
75
+ By default, dependencies are singleton-scoped, meaning the same instance is shared across the application:
76
+
77
+ ```ruby
78
+ class UserService
79
+ include Mortymer::DependenciesDsl
80
+
81
+ inject UserRepository # Singleton by default
82
+ end
83
+ ```
84
+
85
+ #### Request Scope
86
+
87
+ For dependencies that should be unique per request:
88
+
89
+ ```ruby
90
+ class UserService
91
+ include Mortymer::DependenciesDsl
92
+
93
+ inject UserRepository, scope: :request
94
+ end
95
+ ```
96
+
97
+ #### Transient Scope
98
+
99
+ For dependencies that should be newly instantiated every time:
100
+
101
+ ```ruby
102
+ class UserService
103
+ include Mortymer::DependenciesDsl
104
+
105
+ inject UserRepository, scope: :transient
106
+ end
107
+ ```
108
+
109
+ ### Factory Registration
110
+
111
+ You can register custom factory methods for your dependencies:
112
+
113
+ ```ruby
114
+ Mortymer.configure do |config|
115
+ config.register_factory(UserRepository) do
116
+ UserRepository.new(connection: DatabaseConnection.current)
117
+ end
118
+ end
119
+ ```
120
+
121
+ ### Interface-Based Dependencies
122
+
123
+ Mortymer supports interface-based dependency injection:
124
+
125
+ ```ruby
126
+ module UserRepositoryInterface
127
+ def find(id)
128
+ raise NotImplementedError
129
+ end
130
+ end
131
+
132
+ class PostgresUserRepository
133
+ include UserRepositoryInterface
134
+ # implementation
135
+ end
136
+
137
+ class MongoUserRepository
138
+ include UserRepositoryInterface
139
+ # implementation
140
+ end
141
+
142
+ # Register implementation
143
+ Mortymer.configure do |config|
144
+ config.register_implementation(UserRepositoryInterface, PostgresUserRepository)
145
+ end
146
+
147
+ class UserService
148
+ include Mortymer::DependenciesDsl
149
+
150
+ inject UserRepositoryInterface
151
+ end
152
+ ```
153
+
154
+ ## Testing
155
+
156
+ ### Mocking Dependencies
157
+
158
+ Mortymer makes it easy to mock dependencies in tests:
159
+
160
+ ```ruby
161
+ RSpec.describe UserService do
162
+ let(:repository_mock) { instance_double(UserRepository) }
163
+ let(:mailer_mock) { instance_double(UserMailer) }
164
+
165
+ before do
166
+ Mortymer.stub_dependency(UserRepository, repository_mock)
167
+ Mortymer.stub_dependency(UserMailer, mailer_mock)
168
+ end
169
+
170
+ it "creates a user" do
171
+ service = UserService.new
172
+ expect(repository_mock).to receive(:create)
173
+ expect(mailer_mock).to receive(:send_welcome_email)
174
+
175
+ service.create_user(name: "John")
176
+ end
177
+ end
178
+ ```
179
+
180
+ ### Test-Specific Implementations
181
+
182
+ You can also register test-specific implementations:
183
+
184
+ ```ruby
185
+ class TestUserRepository
186
+ include UserRepositoryInterface
187
+
188
+ def initialize
189
+ @users = {}
190
+ end
191
+
192
+ def find(id)
193
+ @users[id]
194
+ end
195
+ end
196
+
197
+ RSpec.describe UserService do
198
+ before do
199
+ Mortymer.register_implementation(UserRepositoryInterface, TestUserRepository)
200
+ end
201
+ end
202
+ ```
203
+
204
+ ## Best Practices
205
+
206
+ ### 1. Keep Dependencies Explicit
207
+
208
+ Always use explicit constant references rather than strings for dependencies:
209
+
210
+ ```ruby
211
+ # Good
212
+ inject UserRepository
213
+
214
+ # Avoid
215
+ inject "user_repository"
216
+ ```
217
+
218
+ ### 2. Use Meaningful Names
219
+
220
+ Choose descriptive names for your dependencies:
221
+
222
+ ```ruby
223
+ # Good
224
+ inject UserRepository, as: :active_users_repository
225
+
226
+ # Less Clear
227
+ inject UserRepository, as: :repo
228
+ ```
229
+
230
+ ### 3. Group Related Dependencies
231
+
232
+ Keep related dependencies together and organize them logically:
233
+
234
+ ```ruby
235
+ class UserService
236
+ include Mortymer::DependenciesDsl
237
+
238
+ # Authentication dependencies
239
+ inject AuthenticationService
240
+ inject TokenGenerator
241
+
242
+ # User management dependencies
243
+ inject UserRepository
244
+ inject UserMailer
245
+
246
+ # Logging/Monitoring
247
+ inject Logger
248
+ inject MetricsCollector
249
+ end
250
+ ```
251
+
252
+ ### 4. Interface Segregation
253
+
254
+ Create focused interfaces for your dependencies:
255
+
256
+ ```ruby
257
+ module UserReader
258
+ def find(id); end
259
+ def list; end
260
+ end
261
+
262
+ module UserWriter
263
+ def create(attributes); end
264
+ def update(id, attributes); end
265
+ end
266
+
267
+ class UserRepository
268
+ include UserReader
269
+ include UserWriter
270
+ end
271
+
272
+ class UserService
273
+ include Mortymer::DependenciesDsl
274
+
275
+ inject UserReader # Only inject what you need
276
+ end
277
+ ```
278
+
279
+ ## Common Patterns
280
+
281
+ ### Service Objects
282
+
283
+ ```ruby
284
+ class CreateUser
285
+ include Mortymer::DependenciesDsl
286
+
287
+ inject UserRepository
288
+ inject UserMailer
289
+ inject UserValidator
290
+
291
+ def call(params)
292
+ @user_validator.validate!(params)
293
+ user = @user_repository.create(params)
294
+ @user_mailer.send_welcome_email(user)
295
+ user
296
+ end
297
+ end
298
+ ```
299
+
300
+ ### Decorators
301
+
302
+ ```ruby
303
+ class LoggedUserRepository
304
+ include Mortymer::DependenciesDsl
305
+
306
+ inject UserRepository
307
+ inject Logger
308
+
309
+ def find(id)
310
+ @logger.info("Finding user: #{id}")
311
+ result = @user_repository.find(id)
312
+ @logger.info("Found user: #{result&.id}")
313
+ result
314
+ end
315
+ end
316
+ ```
317
+
318
+ ## Configuration
319
+
320
+ ### Global Configuration
321
+
322
+ ```ruby
323
+ Mortymer.configure do |config|
324
+ # Register default implementations
325
+ config.register_implementation(UserRepositoryInterface, PostgresUserRepository)
326
+
327
+ # Register factories
328
+ config.register_factory(Logger) do
329
+ Logger.new($stdout).tap do |logger|
330
+ logger.level = Rails.env.production? ? :info : :debug
331
+ end
332
+ end
333
+
334
+ # Configure scopes
335
+ config.set_scope(UserSession, :request)
336
+ end
337
+ ```
338
+
339
+ ### Environment-Specific Configuration
340
+
341
+ ```ruby
342
+ # config/initializers/mortymer.rb
343
+ Mortymer.configure do |config|
344
+ if Rails.env.test?
345
+ config.register_implementation(UserRepositoryInterface, TestUserRepository)
346
+ elsif Rails.env.development?
347
+ config.register_implementation(UserRepositoryInterface, DevUserRepository)
348
+ else
349
+ config.register_implementation(UserRepositoryInterface, ProductionUserRepository)
350
+ end
351
+ end
352
+ ```
353
+
354
+ ## Troubleshooting
355
+
356
+ ### Common Issues
357
+
358
+ 1. **Circular Dependencies**
359
+
360
+ ```ruby
361
+ # This will raise a CircularDependencyError
362
+ class UserService
363
+ include Mortymer::DependenciesDsl
364
+ inject AccountService
365
+ end
366
+
367
+ class AccountService
368
+ include Mortymer::DependenciesDsl
369
+ inject UserService # Circular dependency!
370
+ end
371
+ ```
372
+
373
+ Solution: Refactor to remove the circular dependency or use method injection.
374
+
375
+ 2. **Missing Dependencies**
376
+
377
+ ```ruby
378
+ # This will raise a DependencyNotFoundError
379
+ class UserService
380
+ include Mortymer::DependenciesDsl
381
+ inject NonExistentService
382
+ end
383
+ ```
384
+
385
+ Solution: Ensure all dependencies are properly registered or the constants exist.
386
+
387
+ 3. **Scope Conflicts**
388
+
389
+ ```ruby
390
+ # This might cause issues
391
+ class UserService
392
+ include Mortymer::DependenciesDsl
393
+ inject SessionManager, scope: :request # Request-scoped
394
+ inject UserRepository # Singleton-scoped
395
+ end
396
+ ```
397
+
398
+ Solution: Be careful mixing different scopes and ensure it makes sense for your use case.
@@ -108,9 +108,7 @@ module Mortymer
108
108
  def resolve_implementation(implementation, key, resolution_stack)
109
109
  case implementation
110
110
  when Proc
111
- result = instance_exec(&implementation)
112
- registry[key] = result
113
- result
111
+ instance_exec(&implementation)
114
112
  when Class
115
113
  resolve_class(implementation, key, resolution_stack)
116
114
  else
@@ -119,14 +117,12 @@ module Mortymer
119
117
  end
120
118
 
121
119
  # Resolve a class implementation with its dependencies
122
- def resolve_class(klass, key, resolution_stack)
123
- instance = if klass.respond_to?(:dependencies)
124
- inject_dependencies(klass, resolution_stack)
125
- else
126
- klass.new
127
- end
128
- registry[key] = instance
129
- instance
120
+ def resolve_class(klass, _key, resolution_stack)
121
+ if klass.respond_to?(:dependencies)
122
+ inject_dependencies(klass, resolution_stack)
123
+ else
124
+ klass.new
125
+ end
130
126
  end
131
127
 
132
128
  # Inject dependencies into a new instance
@@ -4,11 +4,14 @@ require_relative "moldeable"
4
4
  require "dry/validation"
5
5
  require "dry/validation/contract"
6
6
  require_relative "generator"
7
+ require_relative "types"
8
+ require_relative "struct_compiler"
7
9
 
8
10
  module Mortymer
9
11
  # A base model for defining schemas
10
12
  class Contract < Dry::Validation::Contract
11
13
  include Mortymer::Moldeable
14
+ include Mortymer::Types
12
15
 
13
16
  # Exception raised when an error occours in a contract
14
17
  class ContractError < StandardError
@@ -20,15 +23,28 @@ module Mortymer
20
23
  end
21
24
  end
22
25
 
26
+ def self.__internal_struct_repr__
27
+ @__internal_struct_repr__ || StructCompiler.new.compile(schema.json_schema)
28
+ end
29
+
23
30
  def self.json_schema
24
31
  Generator.new.from_validation(self)
25
32
  end
26
33
 
27
34
  def self.structify(params)
28
35
  result = new.call(params)
29
- raise ContractError.new(result.errors.to_h) unless result.errors.empty?
36
+ raise ContractError, result.errors.to_h unless result.errors.empty?
37
+
38
+ __internal_struct_repr__.new(**result.to_h)
39
+ end
30
40
 
31
- result.to_h
41
+ def self.compile!
42
+ # Force eager compilation of the internal struct representation.
43
+ # This provides an optimization by precompiling the struct when
44
+ # the class is defined rather than waiting for the first use.
45
+ # The compilation result is memoized, so subsequent accesses
46
+ # will reuse the compiled struct.
47
+ __internal_struct_repr__
32
48
  end
33
49
  end
34
50
  end
@@ -9,14 +9,14 @@ module Mortymer
9
9
  # A base model for defining schemas
10
10
  class Model < Dry::Struct
11
11
  include Mortymer::Moldeable
12
- include Dry.Types()
12
+ include Mortymer::Types
13
13
 
14
14
  def self.json_schema
15
15
  Generator.new.from_struct(self)
16
16
  end
17
17
 
18
18
  def self.structify(params)
19
- new(params)
19
+ call(params)
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mortymer
4
+ # Sigil provides symbolic type checking for input and outputs
5
+ # of method calls using dry-types
6
+ module Sigil
7
+ class TypeError < StandardError; end
8
+
9
+ # Class methods to be included as part of the dsl
10
+ module ClassMethods
11
+ # Store type signatures for methods before they are defined
12
+ def sign(*positional_types, returns: nil, **keyword_types)
13
+ @pending_type_signature = {
14
+ positional_types: positional_types,
15
+ keyword_types: keyword_types,
16
+ returns: returns
17
+ }
18
+ end
19
+
20
+ # Hook called when a method is defined
21
+ def method_added(method_name)
22
+ return super if @pending_type_signature.nil?
23
+ return super if @processing_type_check
24
+
25
+ signature = @pending_type_signature
26
+ @pending_type_signature = nil
27
+
28
+ # Get the original method
29
+ original_method = instance_method(method_name)
30
+
31
+ @processing_type_check = true
32
+
33
+ # Redefine the method with type checking
34
+ define_method(method_name) do |*args, **kwargs|
35
+ # Validate positional arguments
36
+ procced_args = []
37
+ procced_kwargs = {}
38
+ args.each_with_index do |arg, idx|
39
+ unless (type = signature[:positional_types][idx])
40
+ procced_args << arg
41
+ next
42
+ end
43
+
44
+ begin
45
+ procced_args << (type.respond_to?(:structify) ? type.structify(arg) : type.call(arg))
46
+ rescue Dry::Types::CoercionError => e
47
+ raise TypeError, "Invalid type for argument #{idx}: expected #{type}, got #{arg.class} - #{e.message}"
48
+ end
49
+ end
50
+
51
+ # Validate keyword arguments
52
+ kwargs.each do |key, value|
53
+ unless (type = signature[:keyword_types][key])
54
+ procced_kwargs[key] = value
55
+ next
56
+ end
57
+
58
+ begin
59
+ procced_kwargs[key] = (type.respond_to?(:structify) ? type.structify(value) : type.call(value))
60
+ rescue Dry::Types::CoercionError => e
61
+ raise TypeError,
62
+ "Invalid type for keyword argument #{key}: expected #{type}, got #{value.class} - #{e.message}"
63
+ end
64
+ end
65
+
66
+ # Call the original method
67
+ result = original_method.bind(self).call(*procced_args, **procced_kwargs)
68
+
69
+ # Validate return type if specified
70
+ if (return_type = signature[:returns])
71
+ begin
72
+ return return_type.respond_to?(:structify) ? return_type.structify(result) : return_type.call(result)
73
+ rescue Dry::Types::CoercionError => e
74
+ raise TypeError, "Invalid return type: expected #{return_type}, got #{result.class} - #{e.message}"
75
+ end
76
+ end
77
+
78
+ result
79
+ end
80
+
81
+ @processing_type_check = false
82
+
83
+ # Call super to maintain compatibility with other method hooks
84
+ super
85
+ end
86
+ end
87
+
88
+ def self.included(base)
89
+ base.extend(ClassMethods)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+ require "securerandom"
5
+
6
+ module Mortymer
7
+ class StructCompiler
8
+ PRIMITIVE_TYPE_MAP = {
9
+ "string" => Mortymer::Model::String,
10
+ "integer" => Mortymer::Model::Integer,
11
+ "number" => Mortymer::Model::Float,
12
+ "boolean" => Mortymer::Model::Bool,
13
+ "null" => Mortymer::Model::Nil,
14
+ string: Mortymer::Model::String,
15
+ integer: Mortymer::Model::Integer,
16
+ number: Mortymer::Model::Float,
17
+ boolean: Mortymer::Model::Bool,
18
+ null: Mortymer::Model::Nil
19
+ }.freeze
20
+
21
+ def initialize(class_name = "GeneratedStruct#{SecureRandom.hex(4)}")
22
+ @class_name = class_name
23
+ @types = {}
24
+ end
25
+
26
+ def compile(schema)
27
+ build_type(schema, @class_name)
28
+ end
29
+
30
+ private
31
+
32
+ def build_type(schema, type_name)
33
+ schema = normalize_schema(schema)
34
+ case schema["type"]
35
+ when "object"
36
+ build_object_type(schema, type_name)
37
+ when "array"
38
+ build_array_type(schema)
39
+ else
40
+ build_primitive_type(schema)
41
+ end
42
+ end
43
+
44
+ def normalize_schema(schema)
45
+ return {} if schema.nil?
46
+
47
+ schema = schema.transform_keys(&:to_s)
48
+ if schema["properties"]
49
+ schema["properties"] = schema["properties"].transform_keys(&:to_s)
50
+ schema["properties"].each_value do |prop_schema|
51
+ normalize_schema(prop_schema)
52
+ end
53
+ end
54
+ schema["items"] = normalize_schema(schema["items"]) if schema["items"]
55
+ schema["required"] = schema["required"].map(&:to_s) if schema["required"]
56
+ if schema["enum"]
57
+ schema["enum"] = schema["enum"].map { |v| v.is_a?(Symbol) ? v.to_s : v }
58
+ end
59
+ schema
60
+ end
61
+
62
+ def build_object_type(schema, type_name)
63
+ return {} unless schema["properties"]
64
+
65
+ # Build attribute definitions
66
+ attributes = schema["properties"].map do |name, property_schema|
67
+ name = name.to_s # Ensure name is a string
68
+ nested_type_name = camelize("#{type_name}#{camelize(name)}")
69
+ type = if property_schema["type"] == "object"
70
+ build_type(property_schema, nested_type_name)
71
+ else
72
+ build_type(property_schema, nil)
73
+ end
74
+
75
+ required = schema["required"]&.include?(name)
76
+ [name, required ? type : type.optional, required]
77
+ end
78
+
79
+ # Create a new Struct class for this object
80
+ Class.new(Mortymer::Model) do
81
+ attributes.each do |name, type, required|
82
+ if required
83
+ attribute name.to_sym, type
84
+ else
85
+ attribute? name.to_sym, type
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def build_array_type(schema)
92
+ item_type = build_type(schema["items"], nil)
93
+ Mortymer::Model::Array.of(item_type)
94
+ end
95
+
96
+ def build_primitive_type(schema)
97
+ type_class = PRIMITIVE_TYPE_MAP[schema["type"]] || Mortymer::Model::Any
98
+
99
+ if schema["enum"]
100
+ type_class.enum(*schema["enum"])
101
+ else
102
+ type_class
103
+ end
104
+ end
105
+
106
+ def camelize(string)
107
+ string.split(/[^a-zA-Z0-9]/).map(&:capitalize).join
108
+ end
109
+ end
110
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mortymer
4
- VERSION = "0.0.9"
4
+ VERSION = "0.0.11"
5
5
  end
data/lib/mortymer.rb CHANGED
@@ -2,6 +2,8 @@
2
2
  # typed: true
3
3
 
4
4
  require "dry/struct"
5
+ require "dry/schema"
6
+ Dry::Schema.load_extensions(:json_schema)
5
7
  require "mortymer/types"
6
8
  require "mortymer/uploaded_file"
7
9
  require "mortymer/uploaded_files"
@@ -18,5 +20,7 @@ require "mortymer/openapi_generator"
18
20
  require "mortymer/container"
19
21
  require "mortymer/dependencies_dsl"
20
22
  require "mortymer/security_schemes"
23
+ require "mortymer/struct_compiler"
24
+ require "mortymer/sigil"
21
25
  require "mortymer/rails" if defined?(Rails)
22
26
  require "mortymer/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mortymer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Gonzalez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-20 00:00:00.000000000 Z
10
+ date: 2025-03-31 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dry-struct
@@ -90,8 +90,10 @@ files:
90
90
  - docs/advanced/openapi.md
91
91
  - docs/assets/swagg.png
92
92
  - docs/guide/api-metadata.md
93
+ - docs/guide/dependency-injection.md
93
94
  - docs/guide/introduction.md
94
95
  - docs/guide/models.md
96
+ - docs/guide/mortymer-dependency-injection.md
95
97
  - docs/guide/quick-start.md
96
98
  - docs/index.md
97
99
  - lib/mortymer.rb
@@ -113,6 +115,8 @@ files:
113
115
  - lib/mortymer/rails/routes.rb
114
116
  - lib/mortymer/railtie.rb
115
117
  - lib/mortymer/security_schemes.rb
118
+ - lib/mortymer/sigil.rb
119
+ - lib/mortymer/struct_compiler.rb
116
120
  - lib/mortymer/types.rb
117
121
  - lib/mortymer/uploaded_file.rb
118
122
  - lib/mortymer/uploaded_files.rb