mortymer 0.0.11 → 0.0.12
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 +4 -4
- data/docs/guide/mortymer-dependency-injection.md +50 -284
- data/docs/guide/type-system.md +208 -0
- data/lib/mortymer/api_metadata.rb +16 -3
- data/lib/mortymer/configuration.rb +2 -1
- data/lib/mortymer/model.rb +9 -1
- data/lib/mortymer/openapi_generator.rb +3 -1
- data/lib/mortymer/railtie.rb +2 -1
- data/lib/mortymer/sigil.rb +5 -4
- data/lib/mortymer/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1733f673e867c239bda30efbc34f45f7452ebbee12a702e201dffbcbb02f621
|
4
|
+
data.tar.gz: 9ff44dd993b2ed7a82971437048731e80cb97b1a15d96fbdeb758d9797b6f3e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd31959f30b91390ad9c40efb0fd18154e78829057df484a4b67c8efc5630f0ce97b3bfc47bff4c557bed421226311c6ad5cab077e1aa89da24d9b193defba51
|
7
|
+
data.tar.gz: b9b604148b9ff54cde6cb26bc15d2752e2f8542a9df414cc1d9feb466d8f5e803697336fbfbffab1fb51d8de7b14a5c8a69f53ce3a50638b9df1e4fc36943814
|
@@ -27,6 +27,9 @@ When you include `Mortymer::DependenciesDsl`, you get access to the `inject` dir
|
|
27
27
|
1. Create an instance variable `@user_repository`
|
28
28
|
2. Automatically initialize it with an instance of `UserRepository`
|
29
29
|
|
30
|
+
The name of the injected variable is the snake_case version of the class we are injecting by default. So
|
31
|
+
for example `Repositories::UserRepository` will result in an instance variable called `@user_repository`
|
32
|
+
|
30
33
|
### Custom Variable Names
|
31
34
|
|
32
35
|
You can customize the instance variable name using the `as` option:
|
@@ -66,54 +69,61 @@ end
|
|
66
69
|
|
67
70
|
## Advanced Usage
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
By default, dependencies are singleton-scoped, meaning the same instance is shared across the application:
|
72
|
+
Just with the basic information, Mortymer already provides a really powerful and
|
73
|
+
expressive system to declare dependencies, but as it is, is just a Fancy Factory or Initializer
|
74
|
+
for your classes. Where Dependency Injection really shines, is when you are able to control some
|
75
|
+
other aspects of the dependency injection cycle. For example, Mortymer does not interfere with your
|
76
|
+
usual object initialization.
|
76
77
|
|
77
78
|
```ruby
|
78
|
-
class
|
79
|
+
class MyService
|
79
80
|
include Mortymer::DependenciesDsl
|
81
|
+
inject Mailer
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
+
def initialize(user)
|
84
|
+
# At this point we can be sure that the @mailer instance
|
85
|
+
# is already defined
|
86
|
+
@mailer.send_email("Hello user", user.email)
|
87
|
+
@user = user
|
88
|
+
end
|
83
89
|
```
|
84
90
|
|
85
|
-
|
91
|
+
It might not be clear what is happening here. What is really doing Mortymer is
|
92
|
+
wrapping the `#initialize` method with the dependency resolving algorithm and if
|
93
|
+
any `kwarg` passed to the `#initialize` method matches the name of the injected
|
94
|
+
dependency, then Mortymer will use that as the dependency instead.
|
86
95
|
|
87
|
-
|
96
|
+
This means that you can call your previous service as:
|
88
97
|
|
89
98
|
```ruby
|
90
|
-
class
|
91
|
-
|
92
|
-
|
93
|
-
inject UserRepository, scope: :request
|
94
|
-
end
|
99
|
+
MyService.new(User.first) # will use the Mailer class to instantiate the @mailer variable
|
100
|
+
MyService.new(User.first, mailer: instance_double(Mailer)) # will override @mailer with an instance_double
|
95
101
|
```
|
96
102
|
|
97
|
-
|
103
|
+
### Dependency Scopes
|
98
104
|
|
99
|
-
|
105
|
+
Mortymer supports different scopes for dependencies:
|
100
106
|
|
101
|
-
|
102
|
-
|
103
|
-
|
107
|
+
- By default, dependencies are transient, meaning you will get a new fresh instance each time you ask for one
|
108
|
+
- Dependencies might be singleton, meaning each injection will provide the same instance
|
109
|
+
- Dependencies might be lazy, basically, a block that might compute a value for your dependency
|
104
110
|
|
105
|
-
|
106
|
-
end
|
107
|
-
```
|
111
|
+
To change a dependency's scope, you need to register it in the Mortymer DI Container
|
108
112
|
|
109
|
-
|
113
|
+
#### Singleton Scope
|
110
114
|
|
111
|
-
|
115
|
+
```ruby
|
116
|
+
Mortymer.container.register_constant(Mailer, SingletonMailer.new)
|
117
|
+
```
|
118
|
+
|
119
|
+
#### Lazy Scope
|
112
120
|
|
113
121
|
```ruby
|
114
|
-
Mortymer.
|
115
|
-
|
116
|
-
|
122
|
+
Mortymer.container.register_constant(MAILER_ADMIN_ADDRESS) do
|
123
|
+
if some_condition?
|
124
|
+
"admin+#{Time.current}@example.com"
|
125
|
+
else
|
126
|
+
"admin-ex@example.com"
|
117
127
|
end
|
118
128
|
end
|
119
129
|
```
|
@@ -129,6 +139,12 @@ module UserRepositoryInterface
|
|
129
139
|
end
|
130
140
|
end
|
131
141
|
|
142
|
+
class UserService
|
143
|
+
include Mortymer::DependenciesDsl
|
144
|
+
|
145
|
+
inject UserRepositoryInterface, as: :repo
|
146
|
+
end
|
147
|
+
|
132
148
|
class PostgresUserRepository
|
133
149
|
include UserRepositoryInterface
|
134
150
|
# implementation
|
@@ -140,259 +156,9 @@ class MongoUserRepository
|
|
140
156
|
end
|
141
157
|
|
142
158
|
# Register implementation
|
143
|
-
Mortymer.
|
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
|
159
|
+
Mortymer.container.register_constant(UserRepositoryInterface, PostgresUserRepository.new)
|
245
160
|
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
161
|
+
# Then you can instantiate
|
162
|
+
UserService.new # will use the PostgresUserRepository implementation
|
163
|
+
UserService.new(repo: MongoUserRepository.new) # override the dependency to use a different one
|
352
164
|
```
|
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.
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# Mortymer Type System Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Mortymer provides a way to enforce the types contracts for inputs and
|
6
|
+
outputs of your methods through the `Sigil` module that leverages `dry-types`
|
7
|
+
to enable runtime type checking. Not only that, but, it also will try to coerce
|
8
|
+
the given parameters to the declared type, and will check validation contracts
|
9
|
+
and rules for your inputs and outputs. This is not intended to be a replacement
|
10
|
+
for Type Checkers like Sorbet or Steep, but rather a nice utility to avoid guard
|
11
|
+
code like:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
def func(params)
|
15
|
+
result = MyContract.new.call(params)
|
16
|
+
if result.errors.empty?
|
17
|
+
result[:age].to_i + 10
|
18
|
+
else
|
19
|
+
raise StandardError, result.errors
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
The above snippet is really common when dealing with validation (you may use `#instance_of?` method as well,
|
25
|
+
or strong parameters or whatever, but the essence is that you need to declare the shape of the data
|
26
|
+
that comes in and out of your methods).
|
27
|
+
|
28
|
+
Using Mortymer's Sigil, the above will translate to pretty much the following code:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
sign MyContract
|
32
|
+
def func(params)
|
33
|
+
params.age + 10
|
34
|
+
end
|
35
|
+
|
36
|
+
func(MyContract.structify({age: 10})) # Will work
|
37
|
+
func(age: 10) # Will work, the params would be { age: 10 } which will be coerced and validated with MyCotnract
|
38
|
+
func(age: "10") # Will also work, if MyContract is Coercing the age to Integer
|
39
|
+
func(age: "asd") # will raise a meaningful error
|
40
|
+
```
|
41
|
+
|
42
|
+
## Basic Usage
|
43
|
+
|
44
|
+
### Method Type Signatures
|
45
|
+
|
46
|
+
The most basic form of type checking in Mortymer uses the `sign` directive:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class Calculator
|
50
|
+
include Mortymer::Sigil
|
51
|
+
|
52
|
+
sign Types::Integer, Types::Integer, returns: Types::Integer
|
53
|
+
def add(a, b)
|
54
|
+
a + b
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
When you include `Mortymer::Sigil`, you get access to the `sign` directive. This will:
|
60
|
+
|
61
|
+
1. Check the types of all arguments when the method is called
|
62
|
+
2. Verify the return type matches the specified type
|
63
|
+
3. Raise a `TypeError` if any type mismatch occurs
|
64
|
+
|
65
|
+
### Keyword Arguments
|
66
|
+
|
67
|
+
You can specify types for keyword arguments using a hash syntax:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class UserService
|
71
|
+
include Mortymer::Sigil
|
72
|
+
|
73
|
+
sign name: Types::String, age: Types::Integer, returns: Types::Hash
|
74
|
+
def create_user(name:, age:)
|
75
|
+
{ name: name, age: age }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
### Array Types
|
81
|
+
|
82
|
+
Mortymer supports type checking for arrays of specific types:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
class Calculator
|
86
|
+
include Mortymer::Sigil
|
87
|
+
|
88
|
+
sign Types::Array.of(Types::Integer), returns: Types::Integer
|
89
|
+
def sum(numbers)
|
90
|
+
numbers.sum
|
91
|
+
end
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
## Advanced Usage
|
96
|
+
|
97
|
+
### Contract Integration
|
98
|
+
|
99
|
+
Mortymer's type system integrates seamlessly with Contracts for more complex validations:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
class AgeContract < Mortymer::Contract
|
103
|
+
params do
|
104
|
+
required(:age).value(Integer, gt?: 10)
|
105
|
+
end
|
106
|
+
compile!
|
107
|
+
end
|
108
|
+
|
109
|
+
class UserService
|
110
|
+
include Mortymer::Sigil
|
111
|
+
|
112
|
+
sign AgeContract, returns: Types::Hash
|
113
|
+
def process_age(params)
|
114
|
+
{ processed_age: params.age * 2 }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
### Model Integration
|
120
|
+
|
121
|
+
The type system works directly with Mortymer Models:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class User < Mortymer::Model
|
125
|
+
attribute :name, String
|
126
|
+
attribute :age, Integer
|
127
|
+
end
|
128
|
+
|
129
|
+
class UserService
|
130
|
+
include Mortymer::Sigil
|
131
|
+
|
132
|
+
sign User, returns: User
|
133
|
+
def double_age(user)
|
134
|
+
User.new(name: user.name, age: user.age * 2)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
### Optional Return Types
|
140
|
+
|
141
|
+
You can omit the return type if you don't want to enforce it:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class StringService
|
145
|
+
include Mortymer::Sigil
|
146
|
+
|
147
|
+
sign Types::String
|
148
|
+
def uppercase(str)
|
149
|
+
str.upcase
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
### Type Coercion
|
155
|
+
|
156
|
+
The type system will attempt to coerce values when possible:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
class UserService
|
160
|
+
include Mortymer::Sigil
|
161
|
+
|
162
|
+
sign User, returns: User
|
163
|
+
def process_user(user_data)
|
164
|
+
# Will coerce a hash into a User model
|
165
|
+
user = user_data.is_a?(User) ? user_data : User.new(user_data)
|
166
|
+
User.new(name: user.name.upcase, age: user.age)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Error Handling
|
172
|
+
|
173
|
+
When type checking fails, Mortymer raises descriptive errors:
|
174
|
+
|
175
|
+
- `Mortymer::Sigil::TypeError` for type mismatches
|
176
|
+
- `Mortymer::Contract::ContractError` for contract violations
|
177
|
+
|
178
|
+
Example error messages:
|
179
|
+
|
180
|
+
- "Invalid type for argument 0: expected Integer, got String"
|
181
|
+
- "Invalid type for keyword argument name: expected String, got Integer"
|
182
|
+
- "Invalid return type: expected String, got Integer"
|
183
|
+
|
184
|
+
## Best Practices
|
185
|
+
|
186
|
+
1. **Be Explicit**: Always specify types for critical method parameters
|
187
|
+
2. **Use Contracts**: For complex validations, combine with Mortymer Contracts
|
188
|
+
3. **Return Types**: Specify return types when the output type is important
|
189
|
+
4. **Model Integration**: Use Mortymer Models for structured data
|
190
|
+
5. **Error Handling**: Handle type errors at appropriate boundaries in your application
|
191
|
+
|
192
|
+
## Type System Compatibility
|
193
|
+
|
194
|
+
The type system maintains compatibility with other method hooks and Ruby features:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
class Service
|
198
|
+
include Mortymer::Sigil
|
199
|
+
include OtherModule # with method_added hooks
|
200
|
+
|
201
|
+
sign Types::String, returns: Types::String
|
202
|
+
def process(str)
|
203
|
+
str.upcase
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
|
208
|
+
The type system will properly chain method hooks while maintaining type checking functionality.
|
@@ -4,12 +4,14 @@ require_relative "dry_swagger"
|
|
4
4
|
require_relative "endpoint_registry"
|
5
5
|
require_relative "endpoint"
|
6
6
|
require_relative "utils/string_transformations"
|
7
|
+
require_relative "sigil"
|
7
8
|
|
8
9
|
module Mortymer
|
9
10
|
# Include this module in your classes to register
|
10
11
|
# and configure your classes as API endpoints.
|
11
12
|
module ApiMetadata
|
12
13
|
def self.included(base)
|
14
|
+
base.include(Sigil)
|
13
15
|
base.extend(ClassMethods)
|
14
16
|
end
|
15
17
|
|
@@ -84,12 +86,22 @@ module Mortymer
|
|
84
86
|
rails_wrap_method_with_no_params_call(method_name, input_class, handlers)
|
85
87
|
end
|
86
88
|
|
87
|
-
def rails_wrap_method_with_no_params_call(method_name,
|
89
|
+
def rails_wrap_method_with_no_params_call(method_name, _input_class, handlers)
|
88
90
|
original_method = instance_method(method_name)
|
89
91
|
define_method(method_name) do
|
90
|
-
input
|
92
|
+
# Delegate input handling and checking to Sigil. It will coerce and
|
93
|
+
# validate contracts and structs
|
94
|
+
input = params.to_unsafe_h.to_h.deep_transform_keys(&:to_sym)
|
91
95
|
output = original_method.bind_call(self, input)
|
92
|
-
|
96
|
+
# Output might not be validated, so if it is a hash, we will simple
|
97
|
+
# pass it through, otherwise we will call a to_h method
|
98
|
+
if output.respond_to?(:to_h)
|
99
|
+
render json: output.to_h, status: :ok
|
100
|
+
elsif output.respond_to?(:to_json)
|
101
|
+
render json: output.to_json, status: :ok
|
102
|
+
else
|
103
|
+
render json: output, status: :ok
|
104
|
+
end
|
93
105
|
rescue StandardError => e
|
94
106
|
handler = handlers.find { |h| e.is_a?(h[:exception]) }
|
95
107
|
raise unless handler
|
@@ -105,6 +117,7 @@ module Mortymer
|
|
105
117
|
end
|
106
118
|
|
107
119
|
def register_endpoint(http_method, input_class, output_class, path, security)
|
120
|
+
sign input_class, returns: output_class
|
108
121
|
@__endpoint_signature__ =
|
109
122
|
{
|
110
123
|
http_method: http_method,
|
@@ -19,7 +19,7 @@ module Mortymer
|
|
19
19
|
# Global configuration for Mortymer
|
20
20
|
class Configuration
|
21
21
|
attr_accessor :container, :serve_swagger, :swagger_title, :swagger_path, :swagger_root, :api_version,
|
22
|
-
:api_description, :security_schemes, :api_prefix
|
22
|
+
:api_description, :security_schemes, :api_prefix, :openapi_servers
|
23
23
|
|
24
24
|
def initialize
|
25
25
|
@container = Mortymer::Container.new
|
@@ -31,6 +31,7 @@ module Mortymer
|
|
31
31
|
@api_version = "v1"
|
32
32
|
@security_schemes = {}
|
33
33
|
@api_prefix = "/api/v1"
|
34
|
+
@openapi_servers = []
|
34
35
|
end
|
35
36
|
end
|
36
37
|
end
|
data/lib/mortymer/model.rb
CHANGED
@@ -10,18 +10,20 @@ module Mortymer
|
|
10
10
|
include Utils::StringTransformations
|
11
11
|
|
12
12
|
def initialize(prefix: "", title: "Rick on Rails API", version: "v1", description: "", registry: [], # rubocop:disable Metrics/ParameterLists
|
13
|
-
security_schemes: {})
|
13
|
+
security_schemes: {}, openapi_servers: [])
|
14
14
|
@prefix = prefix
|
15
15
|
@title = title
|
16
16
|
@version = version
|
17
17
|
@description = description
|
18
18
|
@endpoints_registry = registry
|
19
19
|
@security_schemes = security_schemes
|
20
|
+
@openapi_servers = openapi_servers
|
20
21
|
end
|
21
22
|
|
22
23
|
def generate
|
23
24
|
{
|
24
25
|
openapi: "3.0.1",
|
26
|
+
servers: @openapi_servers,
|
25
27
|
info: { title: @title, version: @version, description: @description },
|
26
28
|
paths: generate_paths,
|
27
29
|
components: {
|
data/lib/mortymer/railtie.rb
CHANGED
@@ -28,7 +28,8 @@ module Mortymer
|
|
28
28
|
version: Mortymer.config.api_version,
|
29
29
|
description: Mortymer.config.api_description,
|
30
30
|
security_schemes: Mortymer.config.security_schemes,
|
31
|
-
prefix: Mortymer.config.api_prefix
|
31
|
+
prefix: Mortymer.config.api_prefix,
|
32
|
+
openapi_servers: Mortymer.config.openapi_servers
|
32
33
|
)
|
33
34
|
|
34
35
|
# Save OpenAPI spec to public directory
|
data/lib/mortymer/sigil.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "types"
|
4
|
+
|
3
5
|
module Mortymer
|
4
6
|
# Sigil provides symbolic type checking for input and outputs
|
5
7
|
# of method calls using dry-types
|
@@ -19,8 +21,9 @@ module Mortymer
|
|
19
21
|
|
20
22
|
# Hook called when a method is defined
|
21
23
|
def method_added(method_name)
|
22
|
-
|
23
|
-
return
|
24
|
+
super
|
25
|
+
return if @pending_type_signature.nil?
|
26
|
+
return if @processing_type_check
|
24
27
|
|
25
28
|
signature = @pending_type_signature
|
26
29
|
@pending_type_signature = nil
|
@@ -79,9 +82,7 @@ module Mortymer
|
|
79
82
|
end
|
80
83
|
|
81
84
|
@processing_type_check = false
|
82
|
-
|
83
85
|
# Call super to maintain compatibility with other method hooks
|
84
|
-
super
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
data/lib/mortymer/version.rb
CHANGED
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.
|
4
|
+
version: 0.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Gonzalez
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-04-05 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: dry-struct
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- docs/guide/models.md
|
96
96
|
- docs/guide/mortymer-dependency-injection.md
|
97
97
|
- docs/guide/quick-start.md
|
98
|
+
- docs/guide/type-system.md
|
98
99
|
- docs/index.md
|
99
100
|
- lib/mortymer.rb
|
100
101
|
- lib/mortymer/api_metadata.rb
|