@malamute/ai-rules 1.0.0 → 1.3.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.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.Validator.cs"
|
|
4
|
+
- "**/Validators/**/*.cs"
|
|
5
|
+
- "**/Validation/**/*.cs"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# .NET Validation (FluentValidation)
|
|
9
|
+
|
|
10
|
+
## Basic Validator
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
// Validators/CreateUserRequestValidator.cs
|
|
14
|
+
using FluentValidation;
|
|
15
|
+
|
|
16
|
+
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
|
17
|
+
{
|
|
18
|
+
public CreateUserRequestValidator()
|
|
19
|
+
{
|
|
20
|
+
RuleFor(x => x.Email)
|
|
21
|
+
.NotEmpty().WithMessage("Email is required")
|
|
22
|
+
.EmailAddress().WithMessage("Invalid email format")
|
|
23
|
+
.MaximumLength(255);
|
|
24
|
+
|
|
25
|
+
RuleFor(x => x.Name)
|
|
26
|
+
.NotEmpty().WithMessage("Name is required")
|
|
27
|
+
.Length(2, 100).WithMessage("Name must be between 2 and 100 characters")
|
|
28
|
+
.Matches(@"^[a-zA-Z\s'-]+$").WithMessage("Name contains invalid characters");
|
|
29
|
+
|
|
30
|
+
RuleFor(x => x.Password)
|
|
31
|
+
.NotEmpty()
|
|
32
|
+
.MinimumLength(8)
|
|
33
|
+
.Matches(@"[A-Z]").WithMessage("Password must contain uppercase letter")
|
|
34
|
+
.Matches(@"[a-z]").WithMessage("Password must contain lowercase letter")
|
|
35
|
+
.Matches(@"[0-9]").WithMessage("Password must contain digit")
|
|
36
|
+
.Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain special character");
|
|
37
|
+
|
|
38
|
+
RuleFor(x => x.ConfirmPassword)
|
|
39
|
+
.Equal(x => x.Password).WithMessage("Passwords do not match");
|
|
40
|
+
|
|
41
|
+
RuleFor(x => x.DateOfBirth)
|
|
42
|
+
.NotEmpty()
|
|
43
|
+
.LessThan(DateTime.Today).WithMessage("Date of birth must be in the past")
|
|
44
|
+
.Must(BeAtLeast18).WithMessage("Must be at least 18 years old");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private bool BeAtLeast18(DateTime dateOfBirth)
|
|
48
|
+
{
|
|
49
|
+
return dateOfBirth <= DateTime.Today.AddYears(-18);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Async Validation
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
|
58
|
+
{
|
|
59
|
+
private readonly IUserRepository _userRepository;
|
|
60
|
+
|
|
61
|
+
public CreateUserRequestValidator(IUserRepository userRepository)
|
|
62
|
+
{
|
|
63
|
+
_userRepository = userRepository;
|
|
64
|
+
|
|
65
|
+
RuleFor(x => x.Email)
|
|
66
|
+
.NotEmpty()
|
|
67
|
+
.EmailAddress()
|
|
68
|
+
.MustAsync(BeUniqueEmail).WithMessage("Email already exists");
|
|
69
|
+
|
|
70
|
+
RuleFor(x => x.Username)
|
|
71
|
+
.NotEmpty()
|
|
72
|
+
.MustAsync(BeUniqueUsername).WithMessage("Username is taken");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async Task<bool> BeUniqueEmail(string email, CancellationToken ct)
|
|
76
|
+
{
|
|
77
|
+
return !await _userRepository.ExistsAsync(u => u.Email == email, ct);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async Task<bool> BeUniqueUsername(string username, CancellationToken ct)
|
|
81
|
+
{
|
|
82
|
+
return !await _userRepository.ExistsAsync(u => u.Username == username, ct);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Conditional Validation
|
|
88
|
+
|
|
89
|
+
```csharp
|
|
90
|
+
public class OrderValidator : AbstractValidator<CreateOrderRequest>
|
|
91
|
+
{
|
|
92
|
+
public OrderValidator()
|
|
93
|
+
{
|
|
94
|
+
RuleFor(x => x.ShippingAddress)
|
|
95
|
+
.NotEmpty()
|
|
96
|
+
.When(x => x.DeliveryMethod == DeliveryMethod.Shipping);
|
|
97
|
+
|
|
98
|
+
RuleFor(x => x.PickupLocation)
|
|
99
|
+
.NotEmpty()
|
|
100
|
+
.When(x => x.DeliveryMethod == DeliveryMethod.Pickup);
|
|
101
|
+
|
|
102
|
+
// Complex condition
|
|
103
|
+
When(x => x.PaymentMethod == PaymentMethod.CreditCard, () =>
|
|
104
|
+
{
|
|
105
|
+
RuleFor(x => x.CardNumber).NotEmpty().CreditCard();
|
|
106
|
+
RuleFor(x => x.ExpiryDate).NotEmpty().Must(BeValidExpiryDate);
|
|
107
|
+
RuleFor(x => x.Cvv).NotEmpty().Length(3, 4);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Otherwise
|
|
111
|
+
Otherwise(() =>
|
|
112
|
+
{
|
|
113
|
+
RuleFor(x => x.BankAccountNumber).NotEmpty();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Collection Validation
|
|
120
|
+
|
|
121
|
+
```csharp
|
|
122
|
+
public class OrderValidator : AbstractValidator<CreateOrderRequest>
|
|
123
|
+
{
|
|
124
|
+
public OrderValidator()
|
|
125
|
+
{
|
|
126
|
+
RuleFor(x => x.Items)
|
|
127
|
+
.NotEmpty().WithMessage("Order must contain at least one item");
|
|
128
|
+
|
|
129
|
+
RuleForEach(x => x.Items)
|
|
130
|
+
.SetValidator(new OrderItemValidator());
|
|
131
|
+
|
|
132
|
+
// Custom collection rules
|
|
133
|
+
RuleFor(x => x.Items)
|
|
134
|
+
.Must(items => items.Sum(i => i.Quantity) <= 100)
|
|
135
|
+
.WithMessage("Maximum 100 items per order");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public class OrderItemValidator : AbstractValidator<OrderItem>
|
|
140
|
+
{
|
|
141
|
+
public OrderItemValidator()
|
|
142
|
+
{
|
|
143
|
+
RuleFor(x => x.ProductId).NotEmpty();
|
|
144
|
+
RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(10);
|
|
145
|
+
RuleFor(x => x.Price).GreaterThan(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Nested Objects
|
|
151
|
+
|
|
152
|
+
```csharp
|
|
153
|
+
public class CustomerValidator : AbstractValidator<Customer>
|
|
154
|
+
{
|
|
155
|
+
public CustomerValidator()
|
|
156
|
+
{
|
|
157
|
+
RuleFor(x => x.Name).NotEmpty();
|
|
158
|
+
|
|
159
|
+
RuleFor(x => x.Address)
|
|
160
|
+
.NotNull()
|
|
161
|
+
.SetValidator(new AddressValidator());
|
|
162
|
+
|
|
163
|
+
// Inline nested validation
|
|
164
|
+
RuleFor(x => x.Contact)
|
|
165
|
+
.ChildRules(contact =>
|
|
166
|
+
{
|
|
167
|
+
contact.RuleFor(c => c.Phone).NotEmpty();
|
|
168
|
+
contact.RuleFor(c => c.Email).EmailAddress();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public class AddressValidator : AbstractValidator<Address>
|
|
174
|
+
{
|
|
175
|
+
public AddressValidator()
|
|
176
|
+
{
|
|
177
|
+
RuleFor(x => x.Street).NotEmpty().MaximumLength(200);
|
|
178
|
+
RuleFor(x => x.City).NotEmpty().MaximumLength(100);
|
|
179
|
+
RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}(-\d{4})?$");
|
|
180
|
+
RuleFor(x => x.Country).NotEmpty().Length(2);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Custom Validators
|
|
186
|
+
|
|
187
|
+
```csharp
|
|
188
|
+
// Extensions/ValidationExtensions.cs
|
|
189
|
+
public static class ValidationExtensions
|
|
190
|
+
{
|
|
191
|
+
public static IRuleBuilderOptions<T, string> PhoneNumber<T>(
|
|
192
|
+
this IRuleBuilder<T, string> ruleBuilder)
|
|
193
|
+
{
|
|
194
|
+
return ruleBuilder
|
|
195
|
+
.Matches(@"^\+?[1-9]\d{1,14}$")
|
|
196
|
+
.WithMessage("Invalid phone number format");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public static IRuleBuilderOptions<T, string> Slug<T>(
|
|
200
|
+
this IRuleBuilder<T, string> ruleBuilder)
|
|
201
|
+
{
|
|
202
|
+
return ruleBuilder
|
|
203
|
+
.Matches(@"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
204
|
+
.WithMessage("Invalid slug format");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public static IRuleBuilderOptions<T, decimal> Currency<T>(
|
|
208
|
+
this IRuleBuilder<T, decimal> ruleBuilder)
|
|
209
|
+
{
|
|
210
|
+
return ruleBuilder
|
|
211
|
+
.GreaterThanOrEqualTo(0)
|
|
212
|
+
.PrecisionScale(18, 2, true)
|
|
213
|
+
.WithMessage("Invalid currency format");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Usage
|
|
218
|
+
RuleFor(x => x.Phone).PhoneNumber();
|
|
219
|
+
RuleFor(x => x.Slug).Slug();
|
|
220
|
+
RuleFor(x => x.Price).Currency();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Reusable Property Validators
|
|
224
|
+
|
|
225
|
+
```csharp
|
|
226
|
+
// Validators/PropertyValidators/UniqueEmailValidator.cs
|
|
227
|
+
public class UniqueEmailValidator<T> : AsyncPropertyValidator<T, string>
|
|
228
|
+
{
|
|
229
|
+
private readonly IUserRepository _userRepository;
|
|
230
|
+
private readonly Guid? _excludeUserId;
|
|
231
|
+
|
|
232
|
+
public UniqueEmailValidator(IUserRepository userRepository, Guid? excludeUserId = null)
|
|
233
|
+
{
|
|
234
|
+
_userRepository = userRepository;
|
|
235
|
+
_excludeUserId = excludeUserId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public override string Name => "UniqueEmailValidator";
|
|
239
|
+
|
|
240
|
+
public override async Task<bool> IsValidAsync(
|
|
241
|
+
ValidationContext<T> context,
|
|
242
|
+
string value,
|
|
243
|
+
CancellationToken ct)
|
|
244
|
+
{
|
|
245
|
+
if (string.IsNullOrEmpty(value)) return true;
|
|
246
|
+
|
|
247
|
+
var existingUser = await _userRepository.FindByEmailAsync(value, ct);
|
|
248
|
+
|
|
249
|
+
if (existingUser == null) return true;
|
|
250
|
+
|
|
251
|
+
return _excludeUserId.HasValue && existingUser.Id == _excludeUserId.Value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected override string GetDefaultMessageTemplate(string errorCode)
|
|
255
|
+
=> "Email is already in use";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Usage
|
|
259
|
+
RuleFor(x => x.Email).SetAsyncValidator(new UniqueEmailValidator<Request>(_userRepository));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Registration
|
|
263
|
+
|
|
264
|
+
```csharp
|
|
265
|
+
// Program.cs or Startup.cs
|
|
266
|
+
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserRequestValidator>();
|
|
267
|
+
|
|
268
|
+
// With pipeline behavior for MediatR
|
|
269
|
+
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## MediatR Validation Behavior
|
|
273
|
+
|
|
274
|
+
```csharp
|
|
275
|
+
// Behaviors/ValidationBehavior.cs
|
|
276
|
+
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
|
277
|
+
where TRequest : IRequest<TResponse>
|
|
278
|
+
{
|
|
279
|
+
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
|
280
|
+
|
|
281
|
+
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
|
282
|
+
{
|
|
283
|
+
_validators = validators;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public async Task<TResponse> Handle(
|
|
287
|
+
TRequest request,
|
|
288
|
+
RequestHandlerDelegate<TResponse> next,
|
|
289
|
+
CancellationToken cancellationToken)
|
|
290
|
+
{
|
|
291
|
+
if (!_validators.Any())
|
|
292
|
+
{
|
|
293
|
+
return await next();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var context = new ValidationContext<TRequest>(request);
|
|
297
|
+
|
|
298
|
+
var validationResults = await Task.WhenAll(
|
|
299
|
+
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
|
|
300
|
+
|
|
301
|
+
var failures = validationResults
|
|
302
|
+
.SelectMany(r => r.Errors)
|
|
303
|
+
.Where(f => f != null)
|
|
304
|
+
.ToList();
|
|
305
|
+
|
|
306
|
+
if (failures.Count != 0)
|
|
307
|
+
{
|
|
308
|
+
throw new ValidationException(failures);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return await next();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## API Endpoint Integration
|
|
317
|
+
|
|
318
|
+
```csharp
|
|
319
|
+
// Using Minimal APIs
|
|
320
|
+
app.MapPost("/users", async (
|
|
321
|
+
CreateUserRequest request,
|
|
322
|
+
IValidator<CreateUserRequest> validator,
|
|
323
|
+
IMediator mediator) =>
|
|
324
|
+
{
|
|
325
|
+
var result = await validator.ValidateAsync(request);
|
|
326
|
+
|
|
327
|
+
if (!result.IsValid)
|
|
328
|
+
{
|
|
329
|
+
return Results.ValidationProblem(result.ToDictionary());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var user = await mediator.Send(new CreateUserCommand(request));
|
|
333
|
+
return Results.Created($"/users/{user.Id}", user);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Extension for automatic validation
|
|
337
|
+
public static class ValidationExtensions
|
|
338
|
+
{
|
|
339
|
+
public static async Task<IResult> ValidateAndExecute<T>(
|
|
340
|
+
this T request,
|
|
341
|
+
IValidator<T> validator,
|
|
342
|
+
Func<Task<IResult>> onValid)
|
|
343
|
+
{
|
|
344
|
+
var result = await validator.ValidateAsync(request);
|
|
345
|
+
|
|
346
|
+
return result.IsValid
|
|
347
|
+
? await onValid()
|
|
348
|
+
: Results.ValidationProblem(result.ToDictionary());
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Anti-patterns
|
|
354
|
+
|
|
355
|
+
```csharp
|
|
356
|
+
// BAD: Business logic in validators
|
|
357
|
+
public class OrderValidator : AbstractValidator<Order>
|
|
358
|
+
{
|
|
359
|
+
public OrderValidator(IInventoryService inventory)
|
|
360
|
+
{
|
|
361
|
+
RuleFor(x => x.Items)
|
|
362
|
+
.MustAsync(async (items, ct) =>
|
|
363
|
+
{
|
|
364
|
+
await inventory.ReserveItems(items); // Side effect!
|
|
365
|
+
return true;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// GOOD: Validators only validate, services handle business logic
|
|
371
|
+
|
|
372
|
+
// BAD: Catching exceptions in validators
|
|
373
|
+
RuleFor(x => x.Value)
|
|
374
|
+
.Must(v =>
|
|
375
|
+
{
|
|
376
|
+
try { return Parse(v); }
|
|
377
|
+
catch { return false; } // Silent failure
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// GOOD: Clear validation
|
|
381
|
+
RuleFor(x => x.Value).Must(BeValidFormat).WithMessage("Invalid format");
|
|
382
|
+
|
|
383
|
+
// BAD: Overly specific error messages exposing internals
|
|
384
|
+
.WithMessage($"Query failed: {exception.Message}");
|
|
385
|
+
|
|
386
|
+
// GOOD: User-friendly messages
|
|
387
|
+
.WithMessage("Email validation failed. Please try again.");
|
|
388
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(dotnet run *)",
|
|
5
|
+
"Bash(dotnet build *)",
|
|
6
|
+
"Bash(dotnet test *)",
|
|
7
|
+
"Bash(dotnet watch *)",
|
|
8
|
+
"Bash(dotnet ef *)",
|
|
9
|
+
"Bash(dotnet add *)",
|
|
10
|
+
"Bash(dotnet restore)",
|
|
11
|
+
"Bash(dotnet format *)",
|
|
12
|
+
"Read",
|
|
13
|
+
"Edit",
|
|
14
|
+
"Write"
|
|
15
|
+
],
|
|
16
|
+
"deny": [
|
|
17
|
+
"Bash(git push *)",
|
|
18
|
+
"Bash(git push)",
|
|
19
|
+
"Bash(rm -rf *)",
|
|
20
|
+
"Read(appsettings.*.json)",
|
|
21
|
+
"Read(**/secrets.json)",
|
|
22
|
+
"Read(**/*.pfx)"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"env": {
|
|
26
|
+
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
27
|
+
"DOTNET_WATCH_RESTART_ON_RUDE_EDIT": "true"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# FastAPI Project Guidelines
|
|
2
|
+
|
|
3
|
+
@../_shared/CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- Python 3.12+
|
|
8
|
+
- FastAPI 0.115+ (Pydantic v2 by default)
|
|
9
|
+
- SQLAlchemy 2.0+ (async support)
|
|
10
|
+
- Pydantic v2 for validation
|
|
11
|
+
- pytest + httpx for testing
|
|
12
|
+
- uv or poetry for dependencies
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/app/
|
|
18
|
+
├── main.py # Entry point
|
|
19
|
+
├── config.py # Settings (pydantic-settings)
|
|
20
|
+
├── database.py # DB session, engine
|
|
21
|
+
├── [domain]/ # Feature modules
|
|
22
|
+
│ ├── router.py # API endpoints
|
|
23
|
+
│ ├── schemas.py # Pydantic models (request/response)
|
|
24
|
+
│ ├── models.py # SQLAlchemy models
|
|
25
|
+
│ ├── service.py # Business logic
|
|
26
|
+
│ ├── repository.py # Data access
|
|
27
|
+
│ └── dependencies.py # Route dependencies
|
|
28
|
+
├── core/ # Shared utilities
|
|
29
|
+
│ ├── exceptions.py
|
|
30
|
+
│ └── security.py
|
|
31
|
+
└── common/
|
|
32
|
+
├── models.py # Base models
|
|
33
|
+
└── schemas.py # Shared schemas
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## FastAPI Patterns
|
|
37
|
+
|
|
38
|
+
### Dependency Injection
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from typing import Annotated
|
|
42
|
+
from fastapi import Depends
|
|
43
|
+
|
|
44
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
45
|
+
DbSession = Annotated[AsyncSession, Depends(get_db)]
|
|
46
|
+
|
|
47
|
+
@router.get("/users/me")
|
|
48
|
+
async def get_me(user: CurrentUser, db: DbSession) -> UserResponse:
|
|
49
|
+
return await user_service.get_profile(db, user.id)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Lifespan (not `on_event`)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from contextlib import asynccontextmanager
|
|
56
|
+
|
|
57
|
+
@asynccontextmanager
|
|
58
|
+
async def lifespan(app: FastAPI):
|
|
59
|
+
# Startup
|
|
60
|
+
app.state.db = await create_engine()
|
|
61
|
+
app.state.redis = await aioredis.from_url(settings.redis_url)
|
|
62
|
+
yield
|
|
63
|
+
# Shutdown
|
|
64
|
+
await app.state.db.dispose()
|
|
65
|
+
await app.state.redis.close()
|
|
66
|
+
|
|
67
|
+
app = FastAPI(lifespan=lifespan)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Response Models
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
@router.get(
|
|
74
|
+
"/users/{user_id}",
|
|
75
|
+
response_model=UserResponse,
|
|
76
|
+
status_code=status.HTTP_200_OK,
|
|
77
|
+
)
|
|
78
|
+
async def get_user(user_id: int, db: DbSession) -> User:
|
|
79
|
+
user = await user_repo.get(db, user_id)
|
|
80
|
+
if not user:
|
|
81
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
82
|
+
return user
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Pydantic v2
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pydantic import BaseModel, ConfigDict, Field, EmailStr
|
|
89
|
+
|
|
90
|
+
class UserBase(BaseModel):
|
|
91
|
+
email: EmailStr
|
|
92
|
+
name: str = Field(min_length=1, max_length=100)
|
|
93
|
+
|
|
94
|
+
class UserCreate(UserBase):
|
|
95
|
+
password: str = Field(min_length=8)
|
|
96
|
+
|
|
97
|
+
class UserResponse(UserBase):
|
|
98
|
+
id: int
|
|
99
|
+
model_config = ConfigDict(from_attributes=True)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## SQLAlchemy 2.0
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
|
|
106
|
+
|
|
107
|
+
class Base(DeclarativeBase):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
class User(Base):
|
|
111
|
+
__tablename__ = "users"
|
|
112
|
+
|
|
113
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
114
|
+
email: Mapped[str] = mapped_column(unique=True, index=True)
|
|
115
|
+
hashed_password: Mapped[str]
|
|
116
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Error Handling
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
class AppException(Exception):
|
|
123
|
+
def __init__(self, status_code: int, detail: str):
|
|
124
|
+
self.status_code = status_code
|
|
125
|
+
self.detail = detail
|
|
126
|
+
|
|
127
|
+
@app.exception_handler(AppException)
|
|
128
|
+
async def app_exception_handler(request: Request, exc: AppException):
|
|
129
|
+
return JSONResponse(
|
|
130
|
+
status_code=exc.status_code,
|
|
131
|
+
content={"error": exc.detail},
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Commands
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
uvicorn app.main:app --reload # Dev server
|
|
139
|
+
pytest # Run tests
|
|
140
|
+
pytest --cov=app # Coverage
|
|
141
|
+
ruff check . && ruff format . # Lint + format
|
|
142
|
+
alembic upgrade head # Run migrations
|
|
143
|
+
alembic revision --autogenerate # Generate migration
|
|
144
|
+
```
|