@el-j/magic-helix-plugins 4.0.0-beta.1 → 4.0.0-beta.3
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/dist/architecture/codeowners.md +123 -0
- package/dist/architecture/monorepo.md +146 -0
- package/dist/architecture/nx.md +122 -0
- package/dist/architecture/turborepo.md +114 -0
- package/dist/ci/github-actions.md +268 -0
- package/dist/ci/gitlab-ci.md +330 -0
- package/dist/containers/docker-multistage.md +120 -0
- package/dist/containers/kubernetes-deploy.md +210 -0
- package/dist/cpp/index.cjs +79 -0
- package/dist/cpp/index.mjs +209 -0
- package/dist/csharp/index.cjs +2 -2
- package/dist/csharp/{index.js → index.mjs} +17 -11
- package/dist/csharp/templates/framework-aspnetcore.md +205 -0
- package/dist/csharp/templates/framework-blazor.md +271 -0
- package/dist/csharp/templates/lang-csharp.md +162 -0
- package/dist/devops/docker-compose.md +111 -0
- package/dist/devops/docker-dockerfile.md +94 -0
- package/dist/devops/github-actions.md +160 -0
- package/dist/devops/gitlab-ci.md +210 -0
- package/dist/generic/lang-typescript.md +57 -0
- package/dist/generic/state-redux.md +21 -0
- package/dist/generic/state-rxjs.md +6 -0
- package/dist/generic/style-mui.md +23 -0
- package/dist/generic/style-tailwind.md +76 -0
- package/dist/generic/test-cypress.md +21 -0
- package/dist/generic/test-jest.md +20 -0
- package/dist/generic/test-playwright.md +21 -0
- package/dist/generic/test-vitest.md +131 -0
- package/dist/go/index.cjs +3 -3
- package/dist/go/{index.js → index.mjs} +18 -15
- package/dist/go/templates/lang-go.md +571 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +24 -0
- package/dist/java/index.cjs +2 -2
- package/dist/java/{index.js → index.mjs} +25 -19
- package/dist/java/templates/build-gradle.md +102 -0
- package/dist/java/templates/build-maven.md +86 -0
- package/dist/java/templates/framework-spring-boot.md +179 -0
- package/dist/java/templates/lang-java.md +78 -0
- package/dist/java/templates/lang-kotlin.md +88 -0
- package/dist/meta/magic-helix-meta.md +213 -0
- package/dist/meta/meta-debug.md +459 -0
- package/dist/meta/meta-implement.md +450 -0
- package/dist/meta/meta-roadmap.md +265 -0
- package/dist/nodejs/templates/angular-core.md +19 -0
- package/dist/nodejs/templates/lang-typescript.md +57 -0
- package/dist/nodejs/templates/nestjs-core.md +7 -0
- package/dist/nodejs/templates/react-core.md +677 -0
- package/dist/nodejs/templates/react-zustand.md +7 -0
- package/dist/nodejs/templates/state-redux.md +21 -0
- package/dist/nodejs/templates/state-rxjs.md +6 -0
- package/dist/nodejs/templates/style-primevue.md +6 -0
- package/dist/nodejs/templates/style-quasar.md +22 -0
- package/dist/nodejs/templates/style-tailwind.md +76 -0
- package/dist/nodejs/templates/test-cypress.md +21 -0
- package/dist/nodejs/templates/test-jest.md +20 -0
- package/dist/nodejs/templates/test-playwright.md +21 -0
- package/dist/nodejs/templates/test-vitest.md +131 -0
- package/dist/nodejs/templates/vue-core.md +108 -0
- package/dist/nodejs/templates/vue-pinia.md +5 -0
- package/dist/patterns/architecture/clean-architecture.md +469 -0
- package/dist/patterns/architecture/dependency-injection.md +517 -0
- package/dist/patterns/architecture/domain-driven-design.md +621 -0
- package/dist/patterns/architecture/layered-architecture.md +382 -0
- package/dist/patterns/architecture/repository-pattern.md +408 -0
- package/dist/patterns/domain-expertise/nextjs-rules.md +115 -0
- package/dist/patterns/domain-expertise/react-patterns.md +181 -0
- package/dist/patterns/domain-expertise/server-components.md +212 -0
- package/dist/patterns/domain-expertise/shadcn-ui.md +52 -0
- package/dist/patterns/domain-expertise/tailwind-patterns.md +52 -0
- package/dist/patterns/environment/container-awareness.md +17 -0
- package/dist/patterns/environment/ide-features.md +17 -0
- package/dist/patterns/environment/os-commands.md +17 -0
- package/dist/patterns/organization/heading-hierarchy.md +103 -0
- package/dist/patterns/organization/sequential-workflows.md +102 -0
- package/dist/patterns/organization/xml-rule-groups.md +64 -0
- package/dist/patterns/reasoning/agent-loop.md +151 -0
- package/dist/patterns/reasoning/confirmation-gates.md +141 -0
- package/dist/patterns/reasoning/dependency-analysis.md +132 -0
- package/dist/patterns/reasoning/one-tool-per-iteration.md +152 -0
- package/dist/patterns/reasoning/preview-before-action.md +194 -0
- package/dist/patterns/reasoning/reflection-checkpoints.md +166 -0
- package/dist/patterns/reasoning/result-verification.md +157 -0
- package/dist/patterns/reasoning/subtask-breakdown.md +131 -0
- package/dist/patterns/reasoning/thinking-tags.md +100 -0
- package/dist/patterns/role-definition/capability-declarations.md +72 -0
- package/dist/patterns/role-definition/expert-identity.md +45 -0
- package/dist/patterns/role-definition/scope-boundaries.md +61 -0
- package/dist/patterns/safety/code-safety-rules.md +17 -0
- package/dist/patterns/safety/credential-handling.md +17 -0
- package/dist/patterns/safety/destructive-warnings.md +17 -0
- package/dist/patterns/safety/refusal-messages.md +17 -0
- package/dist/patterns/tone/adaptive-tone.md +17 -0
- package/dist/patterns/tone/concise-communication.md +17 -0
- package/dist/patterns/tone/forbidden-phrases.md +17 -0
- package/dist/patterns/tool-guidelines/function-schemas.md +143 -0
- package/dist/patterns/tool-guidelines/parameter-examples.md +137 -0
- package/dist/patterns/tool-guidelines/usage-policies.md +105 -0
- package/dist/php/index.cjs +2 -2
- package/dist/php/{index.js → index.mjs} +12 -6
- package/dist/php/templates/framework-laravel.md +112 -0
- package/dist/php/templates/lang-php.md +94 -0
- package/dist/python/index.cjs +4 -4
- package/dist/python/{index.js → index.mjs} +10 -7
- package/dist/python/templates/lang-python.md +508 -0
- package/dist/ruby/index.cjs +2 -2
- package/dist/ruby/{index.js → index.mjs} +16 -10
- package/dist/ruby/templates/framework-rails.md +309 -0
- package/dist/ruby/templates/framework-sinatra.md +227 -0
- package/dist/ruby/templates/lang-ruby.md +216 -0
- package/dist/rust/index.cjs +3 -3
- package/dist/rust/{index.js → index.mjs} +24 -18
- package/dist/rust/templates/lang-rust.md +89 -0
- package/dist/swift/index.cjs +32 -0
- package/dist/swift/index.mjs +112 -0
- package/dist/swift/templates/framework-vapor.md +352 -0
- package/dist/swift/templates/lang-swift.md +291 -0
- package/package.json +31 -21
- package/dist/index.js +0 -20
- /package/dist/nodejs/{index.js → index.mjs} +0 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
# Language: Python
|
|
2
|
+
|
|
3
|
+
## Architecture: Layered Architecture for Python Applications
|
|
4
|
+
|
|
5
|
+
**Organize Python applications with clear separation of concerns: API → Services → Domain → Data Access.**
|
|
6
|
+
|
|
7
|
+
### Layer Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── api/ # Presentation layer (FastAPI/Flask routes)
|
|
12
|
+
│ ├── routes/
|
|
13
|
+
│ │ ├── users.py # HTTP handlers, request/response
|
|
14
|
+
│ │ └── orders.py
|
|
15
|
+
│ └── schemas/
|
|
16
|
+
│ └── user_schema.py # Pydantic models for API contracts
|
|
17
|
+
│
|
|
18
|
+
├── services/ # Business logic layer
|
|
19
|
+
│ ├── user_service.py # Orchestrates domain operations
|
|
20
|
+
│ └── order_service.py
|
|
21
|
+
│
|
|
22
|
+
├── domain/ # Domain models and business rules
|
|
23
|
+
│ ├── models/
|
|
24
|
+
│ │ ├── user.py # Domain entities
|
|
25
|
+
│ │ └── order.py
|
|
26
|
+
│ └── value_objects/
|
|
27
|
+
│ └── email.py # Immutable value objects
|
|
28
|
+
│
|
|
29
|
+
├── infrastructure/ # Data access and external services
|
|
30
|
+
│ ├── repositories/
|
|
31
|
+
│ │ ├── user_repository.py
|
|
32
|
+
│ │ └── order_repository.py
|
|
33
|
+
│ └── database/
|
|
34
|
+
│ └── db.py # Database connection
|
|
35
|
+
│
|
|
36
|
+
└── main.py # Application entry point
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Rule: Routes Handle HTTP Only
|
|
40
|
+
|
|
41
|
+
**ALWAYS** keep route handlers thin. They should only handle HTTP concerns (parsing, validation, serialization).
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# ✅ Good: Thin route handler (FastAPI)
|
|
45
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
46
|
+
from src.services.user_service import UserService
|
|
47
|
+
from src.api.schemas.user_schema import CreateUserRequest, UserResponse
|
|
48
|
+
|
|
49
|
+
router = APIRouter()
|
|
50
|
+
|
|
51
|
+
@router.post("/users", response_model=UserResponse, status_code=201)
|
|
52
|
+
async def create_user(
|
|
53
|
+
request: CreateUserRequest,
|
|
54
|
+
user_service: UserService = Depends()
|
|
55
|
+
):
|
|
56
|
+
"""Create a new user - HTTP handler only."""
|
|
57
|
+
try:
|
|
58
|
+
user = await user_service.create_user(
|
|
59
|
+
email=request.email,
|
|
60
|
+
name=request.name
|
|
61
|
+
)
|
|
62
|
+
return UserResponse.from_domain(user)
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
65
|
+
|
|
66
|
+
# ❌ Bad: Business logic in route
|
|
67
|
+
@router.post("/users")
|
|
68
|
+
async def create_user(request: CreateUserRequest, db: Session = Depends()):
|
|
69
|
+
# ❌ Validation logic in route
|
|
70
|
+
if "@" not in request.email:
|
|
71
|
+
raise HTTPException(status_code=400, detail="Invalid email")
|
|
72
|
+
|
|
73
|
+
# ❌ Database access in route
|
|
74
|
+
existing = db.query(UserModel).filter_by(email=request.email).first()
|
|
75
|
+
if existing:
|
|
76
|
+
raise HTTPException(status_code=400, detail="Email exists")
|
|
77
|
+
|
|
78
|
+
# ❌ Domain logic in route
|
|
79
|
+
user = UserModel(email=request.email, name=request.name)
|
|
80
|
+
db.add(user)
|
|
81
|
+
db.commit()
|
|
82
|
+
|
|
83
|
+
return user
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Rule: Services Orchestrate Business Logic
|
|
87
|
+
|
|
88
|
+
**ALWAYS** put business logic in services. Services coordinate between repositories and domain models.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
# ✅ services/user_service.py - Business logic layer
|
|
92
|
+
from src.domain.models.user import User
|
|
93
|
+
from src.domain.value_objects.email import Email
|
|
94
|
+
from src.infrastructure.repositories.user_repository import IUserRepository
|
|
95
|
+
|
|
96
|
+
class UserService:
|
|
97
|
+
"""Orchestrates user-related business operations."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, user_repository: IUserRepository):
|
|
100
|
+
self._user_repo = user_repository
|
|
101
|
+
|
|
102
|
+
async def create_user(self, email: str, name: str) -> User:
|
|
103
|
+
"""Create a new user with validation and business rules."""
|
|
104
|
+
# Validate email using value object
|
|
105
|
+
email_vo = Email(email) # Raises ValueError if invalid
|
|
106
|
+
|
|
107
|
+
# Business rule: Check uniqueness
|
|
108
|
+
existing = await self._user_repo.find_by_email(email_vo)
|
|
109
|
+
if existing:
|
|
110
|
+
raise ValueError(f"User with email {email} already exists")
|
|
111
|
+
|
|
112
|
+
# Create domain entity
|
|
113
|
+
user = User.create(email=email_vo, name=name)
|
|
114
|
+
|
|
115
|
+
# Persist
|
|
116
|
+
await self._user_repo.save(user)
|
|
117
|
+
|
|
118
|
+
return user
|
|
119
|
+
|
|
120
|
+
async def update_user_email(self, user_id: str, new_email: str) -> User:
|
|
121
|
+
"""Update user email with validation."""
|
|
122
|
+
user = await self._user_repo.find_by_id(user_id)
|
|
123
|
+
if not user:
|
|
124
|
+
raise ValueError(f"User {user_id} not found")
|
|
125
|
+
|
|
126
|
+
# Use domain method for business logic
|
|
127
|
+
email_vo = Email(new_email)
|
|
128
|
+
user.change_email(email_vo)
|
|
129
|
+
|
|
130
|
+
await self._user_repo.save(user)
|
|
131
|
+
return user
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Rule: Domain Models Encapsulate Business Rules
|
|
135
|
+
|
|
136
|
+
**ALWAYS** put business rules and invariants in domain models. Keep them database-agnostic.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# ✅ domain/models/user.py - Domain entity
|
|
140
|
+
from dataclasses import dataclass
|
|
141
|
+
from datetime import datetime
|
|
142
|
+
from uuid import UUID, uuid4
|
|
143
|
+
from src.domain.value_objects.email import Email
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class User:
|
|
147
|
+
"""User domain entity with business logic."""
|
|
148
|
+
id: UUID
|
|
149
|
+
email: Email
|
|
150
|
+
name: str
|
|
151
|
+
created_at: datetime
|
|
152
|
+
is_active: bool = True
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def create(cls, email: Email, name: str) -> "User":
|
|
156
|
+
"""Factory method for creating new users."""
|
|
157
|
+
if not name or len(name) < 2:
|
|
158
|
+
raise ValueError("Name must be at least 2 characters")
|
|
159
|
+
|
|
160
|
+
return cls(
|
|
161
|
+
id=uuid4(),
|
|
162
|
+
email=email,
|
|
163
|
+
name=name,
|
|
164
|
+
created_at=datetime.utcnow(),
|
|
165
|
+
is_active=True
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def change_email(self, new_email: Email) -> None:
|
|
169
|
+
"""Change user email with validation."""
|
|
170
|
+
if new_email == self.email:
|
|
171
|
+
raise ValueError("New email must be different")
|
|
172
|
+
|
|
173
|
+
self.email = new_email
|
|
174
|
+
|
|
175
|
+
def deactivate(self) -> None:
|
|
176
|
+
"""Deactivate user account."""
|
|
177
|
+
if not self.is_active:
|
|
178
|
+
raise ValueError("User is already inactive")
|
|
179
|
+
|
|
180
|
+
self.is_active = False
|
|
181
|
+
|
|
182
|
+
def is_admin(self) -> bool:
|
|
183
|
+
"""Business rule: Admins have company email."""
|
|
184
|
+
return self.email.value.endswith("@company.com")
|
|
185
|
+
|
|
186
|
+
# ✅ domain/value_objects/email.py - Value object
|
|
187
|
+
@dataclass(frozen=True) # Immutable
|
|
188
|
+
class Email:
|
|
189
|
+
"""Email value object with validation."""
|
|
190
|
+
value: str
|
|
191
|
+
|
|
192
|
+
def __post_init__(self):
|
|
193
|
+
"""Validate email format on creation."""
|
|
194
|
+
if "@" not in self.value or "." not in self.value.split("@")[1]:
|
|
195
|
+
raise ValueError(f"Invalid email format: {self.value}")
|
|
196
|
+
|
|
197
|
+
if len(self.value) > 255:
|
|
198
|
+
raise ValueError("Email too long")
|
|
199
|
+
|
|
200
|
+
def __str__(self) -> str:
|
|
201
|
+
return self.value
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Rule: Repositories Abstract Data Access
|
|
205
|
+
|
|
206
|
+
**ALWAYS** define repository interfaces in domain layer, implement in infrastructure layer.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# ✅ domain/repositories/user_repository.py - Interface
|
|
210
|
+
from abc import ABC, abstractmethod
|
|
211
|
+
from typing import Optional
|
|
212
|
+
from uuid import UUID
|
|
213
|
+
from src.domain.models.user import User
|
|
214
|
+
from src.domain.value_objects.email import Email
|
|
215
|
+
|
|
216
|
+
class IUserRepository(ABC):
|
|
217
|
+
"""Repository interface for User aggregate."""
|
|
218
|
+
|
|
219
|
+
@abstractmethod
|
|
220
|
+
async def save(self, user: User) -> None:
|
|
221
|
+
"""Save or update a user."""
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
async def find_by_id(self, user_id: UUID) -> Optional[User]:
|
|
226
|
+
"""Find user by ID."""
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
@abstractmethod
|
|
230
|
+
async def find_by_email(self, email: Email) -> Optional[User]:
|
|
231
|
+
"""Find user by email."""
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
@abstractmethod
|
|
235
|
+
async def delete(self, user_id: UUID) -> None:
|
|
236
|
+
"""Delete a user."""
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
# ✅ infrastructure/repositories/user_repository.py - Implementation
|
|
240
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
241
|
+
from sqlalchemy import select
|
|
242
|
+
from src.domain.repositories.user_repository import IUserRepository
|
|
243
|
+
from src.domain.models.user import User
|
|
244
|
+
from src.infrastructure.database.models import UserModel
|
|
245
|
+
|
|
246
|
+
class SQLAlchemyUserRepository(IUserRepository):
|
|
247
|
+
"""SQLAlchemy implementation of user repository."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, session: AsyncSession):
|
|
250
|
+
self._session = session
|
|
251
|
+
|
|
252
|
+
async def save(self, user: User) -> None:
|
|
253
|
+
"""Save domain user to database."""
|
|
254
|
+
# Check if exists
|
|
255
|
+
stmt = select(UserModel).where(UserModel.id == user.id)
|
|
256
|
+
result = await self._session.execute(stmt)
|
|
257
|
+
db_user = result.scalar_one_or_none()
|
|
258
|
+
|
|
259
|
+
if db_user:
|
|
260
|
+
# Update existing
|
|
261
|
+
db_user.email = str(user.email)
|
|
262
|
+
db_user.name = user.name
|
|
263
|
+
db_user.is_active = user.is_active
|
|
264
|
+
else:
|
|
265
|
+
# Create new
|
|
266
|
+
db_user = UserModel(
|
|
267
|
+
id=user.id,
|
|
268
|
+
email=str(user.email),
|
|
269
|
+
name=user.name,
|
|
270
|
+
created_at=user.created_at,
|
|
271
|
+
is_active=user.is_active
|
|
272
|
+
)
|
|
273
|
+
self._session.add(db_user)
|
|
274
|
+
|
|
275
|
+
await self._session.commit()
|
|
276
|
+
|
|
277
|
+
async def find_by_id(self, user_id: UUID) -> Optional[User]:
|
|
278
|
+
"""Find user by ID and map to domain model."""
|
|
279
|
+
stmt = select(UserModel).where(UserModel.id == user_id)
|
|
280
|
+
result = await self._session.execute(stmt)
|
|
281
|
+
db_user = result.scalar_one_or_none()
|
|
282
|
+
|
|
283
|
+
if not db_user:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
return self._map_to_domain(db_user)
|
|
287
|
+
|
|
288
|
+
async def find_by_email(self, email: Email) -> Optional[User]:
|
|
289
|
+
stmt = select(UserModel).where(UserModel.email == str(email))
|
|
290
|
+
result = await self._session.execute(stmt)
|
|
291
|
+
db_user = result.scalar_one_or_none()
|
|
292
|
+
|
|
293
|
+
return self._map_to_domain(db_user) if db_user else None
|
|
294
|
+
|
|
295
|
+
def _map_to_domain(self, db_user: UserModel) -> User:
|
|
296
|
+
"""Map database model to domain model."""
|
|
297
|
+
return User(
|
|
298
|
+
id=db_user.id,
|
|
299
|
+
email=Email(db_user.email),
|
|
300
|
+
name=db_user.name,
|
|
301
|
+
created_at=db_user.created_at,
|
|
302
|
+
is_active=db_user.is_active
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Complete Example: FastAPI Application
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# main.py - Application entry point
|
|
310
|
+
from fastapi import FastAPI
|
|
311
|
+
from src.api.routes import users, orders
|
|
312
|
+
from src.infrastructure.database.db import init_db
|
|
313
|
+
|
|
314
|
+
app = FastAPI()
|
|
315
|
+
|
|
316
|
+
@app.on_event("startup")
|
|
317
|
+
async def startup():
|
|
318
|
+
await init_db()
|
|
319
|
+
|
|
320
|
+
app.include_router(users.router, prefix="/api")
|
|
321
|
+
app.include_router(orders.router, prefix="/api")
|
|
322
|
+
|
|
323
|
+
# api/routes/users.py - HTTP layer
|
|
324
|
+
from fastapi import APIRouter, Depends
|
|
325
|
+
from src.services.user_service import UserService
|
|
326
|
+
from src.api.dependencies import get_user_service
|
|
327
|
+
|
|
328
|
+
router = APIRouter()
|
|
329
|
+
|
|
330
|
+
@router.post("/users")
|
|
331
|
+
async def create_user(
|
|
332
|
+
request: CreateUserRequest,
|
|
333
|
+
service: UserService = Depends(get_user_service)
|
|
334
|
+
):
|
|
335
|
+
user = await service.create_user(request.email, request.name)
|
|
336
|
+
return UserResponse.from_domain(user)
|
|
337
|
+
|
|
338
|
+
# api/schemas/user_schema.py - API contracts
|
|
339
|
+
from pydantic import BaseModel, EmailStr
|
|
340
|
+
|
|
341
|
+
class CreateUserRequest(BaseModel):
|
|
342
|
+
email: EmailStr
|
|
343
|
+
name: str
|
|
344
|
+
|
|
345
|
+
class UserResponse(BaseModel):
|
|
346
|
+
id: str
|
|
347
|
+
email: str
|
|
348
|
+
name: str
|
|
349
|
+
is_active: bool
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def from_domain(cls, user: User) -> "UserResponse":
|
|
353
|
+
return cls(
|
|
354
|
+
id=str(user.id),
|
|
355
|
+
email=str(user.email),
|
|
356
|
+
name=user.name,
|
|
357
|
+
is_active=user.is_active
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# api/dependencies.py - Dependency injection
|
|
361
|
+
from fastapi import Depends
|
|
362
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
363
|
+
from src.infrastructure.database.db import get_session
|
|
364
|
+
from src.infrastructure.repositories.user_repository import SQLAlchemyUserRepository
|
|
365
|
+
from src.services.user_service import UserService
|
|
366
|
+
|
|
367
|
+
async def get_user_service(
|
|
368
|
+
session: AsyncSession = Depends(get_session)
|
|
369
|
+
) -> UserService:
|
|
370
|
+
repository = SQLAlchemyUserRepository(session)
|
|
371
|
+
return UserService(repository)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Python Best Practices
|
|
375
|
+
|
|
376
|
+
- **ALWAYS** use type hints for function parameters and return values.
|
|
377
|
+
- **ALWAYS** use descriptive variable and function names (snake_case).
|
|
378
|
+
- **ALWAYS** follow PEP 8 style guidelines.
|
|
379
|
+
- **ALWAYS** use docstrings for modules, classes, and functions.
|
|
380
|
+
- **ALWAYS** handle exceptions properly with specific exception types.
|
|
381
|
+
- **ALWAYS** use context managers (`with` statements) for resource management.
|
|
382
|
+
- **ALWAYS** prefer list comprehensions and generator expressions over loops when appropriate.
|
|
383
|
+
- **ALWAYS** use `if __name__ == "__main__":` for script entry points.
|
|
384
|
+
- **ALWAYS** import modules at the top of files.
|
|
385
|
+
- **ALWAYS** use relative imports within packages.
|
|
386
|
+
- **ALWAYS** validate input parameters and return values.
|
|
387
|
+
- **ALWAYS** use logging instead of print statements for production code.
|
|
388
|
+
- **ALWAYS** write unit tests with pytest or unittest.
|
|
389
|
+
- **ALWAYS** use virtual environments for dependency management.
|
|
390
|
+
- **ALWAYS** keep functions small and focused on single responsibilities.
|
|
391
|
+
- **ALWAYS** use dataclasses or namedtuples for simple data structures.
|
|
392
|
+
- **ALWAYS** prefer `pathlib` over `os.path` for path operations.
|
|
393
|
+
- **ALWAYS** use f-strings for string formatting.
|
|
394
|
+
- **ALWAYS** handle encoding explicitly when working with files.
|
|
395
|
+
- **ALWAYS** use type checking tools like mypy in CI/CD pipelines.
|
|
396
|
+
|
|
397
|
+
## Testing Strategy
|
|
398
|
+
|
|
399
|
+
**Routes:** Test HTTP contracts (request/response)
|
|
400
|
+
```python
|
|
401
|
+
# tests/api/test_users.py
|
|
402
|
+
import pytest
|
|
403
|
+
from httpx import AsyncClient
|
|
404
|
+
from src.main import app
|
|
405
|
+
|
|
406
|
+
@pytest.mark.asyncio
|
|
407
|
+
async def test_create_user():
|
|
408
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
409
|
+
response = await client.post(
|
|
410
|
+
"/api/users",
|
|
411
|
+
json={"email": "test@example.com", "name": "Test User"}
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
assert response.status_code == 201
|
|
415
|
+
data = response.json()
|
|
416
|
+
assert data["email"] == "test@example.com"
|
|
417
|
+
assert data["name"] == "Test User"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Services:** Test business logic (mock repositories)
|
|
421
|
+
```python
|
|
422
|
+
# tests/services/test_user_service.py
|
|
423
|
+
import pytest
|
|
424
|
+
from unittest.mock import AsyncMock
|
|
425
|
+
from src.services.user_service import UserService
|
|
426
|
+
from src.domain.value_objects.email import Email
|
|
427
|
+
|
|
428
|
+
@pytest.mark.asyncio
|
|
429
|
+
async def test_create_user_validates_uniqueness():
|
|
430
|
+
# Arrange
|
|
431
|
+
mock_repo = AsyncMock()
|
|
432
|
+
mock_repo.find_by_email.return_value = User(...) # Existing user
|
|
433
|
+
|
|
434
|
+
service = UserService(mock_repo)
|
|
435
|
+
|
|
436
|
+
# Act & Assert
|
|
437
|
+
with pytest.raises(ValueError, match="already exists"):
|
|
438
|
+
await service.create_user("test@example.com", "Test")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Domain Models:** Test business rules (no mocking)
|
|
442
|
+
```python
|
|
443
|
+
# tests/domain/test_user.py
|
|
444
|
+
import pytest
|
|
445
|
+
from src.domain.models.user import User
|
|
446
|
+
from src.domain.value_objects.email import Email
|
|
447
|
+
|
|
448
|
+
def test_user_change_email():
|
|
449
|
+
user = User.create(Email("old@example.com"), "Test User")
|
|
450
|
+
|
|
451
|
+
user.change_email(Email("new@example.com"))
|
|
452
|
+
|
|
453
|
+
assert user.email.value == "new@example.com"
|
|
454
|
+
|
|
455
|
+
def test_user_change_email_rejects_same_email():
|
|
456
|
+
user = User.create(Email("test@example.com"), "Test User")
|
|
457
|
+
|
|
458
|
+
with pytest.raises(ValueError, match="must be different"):
|
|
459
|
+
user.change_email(Email("test@example.com"))
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Repositories:** Test database operations (integration tests)
|
|
463
|
+
```python
|
|
464
|
+
# tests/infrastructure/test_user_repository.py
|
|
465
|
+
import pytest
|
|
466
|
+
from src.infrastructure.repositories.user_repository import SQLAlchemyUserRepository
|
|
467
|
+
from src.domain.models.user import User
|
|
468
|
+
from src.domain.value_objects.email import Email
|
|
469
|
+
|
|
470
|
+
@pytest.mark.asyncio
|
|
471
|
+
async def test_save_and_find_user(db_session):
|
|
472
|
+
# Arrange
|
|
473
|
+
repo = SQLAlchemyUserRepository(db_session)
|
|
474
|
+
user = User.create(Email("test@example.com"), "Test User")
|
|
475
|
+
|
|
476
|
+
# Act
|
|
477
|
+
await repo.save(user)
|
|
478
|
+
found = await repo.find_by_id(user.id)
|
|
479
|
+
|
|
480
|
+
# Assert
|
|
481
|
+
assert found is not None
|
|
482
|
+
assert found.email.value == "test@example.com"
|
|
483
|
+
assert found.name == "Test User"
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Architecture Summary
|
|
487
|
+
|
|
488
|
+
1. **API Layer** (routes, schemas) = HTTP concerns
|
|
489
|
+
- Parse requests, serialize responses
|
|
490
|
+
- Call services, map domain ↔ DTOs
|
|
491
|
+
- No business logic
|
|
492
|
+
|
|
493
|
+
2. **Service Layer** (services) = Business logic orchestration
|
|
494
|
+
- Coordinate repositories and domain
|
|
495
|
+
- Transaction boundaries
|
|
496
|
+
- Application-specific workflows
|
|
497
|
+
|
|
498
|
+
3. **Domain Layer** (models, value objects) = Business rules
|
|
499
|
+
- Entities with identity and behavior
|
|
500
|
+
- Value objects (immutable)
|
|
501
|
+
- Framework-agnostic
|
|
502
|
+
|
|
503
|
+
4. **Infrastructure Layer** (repositories, database) = External concerns
|
|
504
|
+
- Database access
|
|
505
|
+
- External APIs
|
|
506
|
+
- File system
|
|
507
|
+
|
|
508
|
+
**Reference:** See `patterns/architecture/layered-architecture.md`, `patterns/architecture/repository-pattern.md`, and `patterns/architecture/domain-driven-design.md` for detailed architectural guidance.
|
package/dist/ruby/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("node:path"),l=require("../BasePlugin-odQJAKA-.cjs");function o(i){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(i){for(const t in i)if(t!=="default"){const n=Object.getOwnPropertyDescriptor(i,t);Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>i[t]})}}return e.default=i,Object.freeze(e)}const u=o(a);class c extends l.BasePlugin{constructor(){super(...arguments),this.name="ruby",this.displayName="Ruby",this.version="3.0.0",this.priority=70}async detect(e){if(!this.fileExists(e,"Gemfile"))return null;const t=this.readFile(e,"Gemfile"),n={};if(t){const r=t.matchAll(/gem\s+['"]([^'"]+)['"](?:,\s*['"]([^'"]+)['"])?/g);for(const s of r)n[s[1]]=s[2]||"*"}return{language:"Ruby",name:this.getProjectName(e),dependencies:n,manifestFile:"Gemfile",projectPath:e}}getTemplates(){return[{name:"ruby-core",tags:["ruby"],content:()=>this.loadTemplateFromFile(u.join(__dirname,"templates/lang-ruby.md")).then(e=>e||this.getRubyFallbackTemplate())}]}getRubyFallbackTemplate(){return`# Ruby Development Guidelines
|
|
2
2
|
|
|
3
3
|
This project uses Ruby.
|
|
4
4
|
|
|
@@ -15,4 +15,4 @@ This project uses Ruby.
|
|
|
15
15
|
## Testing
|
|
16
16
|
- Write tests with RSpec/Minitest
|
|
17
17
|
- Use fixtures and factories
|
|
18
|
-
- Aim for good coverage`}
|
|
18
|
+
- Aim for good coverage`}getDependencyTagMap(){return{rails:"rails",rspec:"rspec",sinatra:"sinatra"}}}exports.RubyPlugin=c;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
import * as a from "node:path";
|
|
1
2
|
import { B as r } from "../BasePlugin-6wv0hYJ9.js";
|
|
2
|
-
class
|
|
3
|
+
class m extends r {
|
|
3
4
|
constructor() {
|
|
4
5
|
super(...arguments), this.name = "ruby", this.displayName = "Ruby", this.version = "3.0.0", this.priority = 70;
|
|
5
6
|
}
|
|
6
7
|
async detect(e) {
|
|
7
8
|
if (!this.fileExists(e, "Gemfile"))
|
|
8
9
|
return null;
|
|
9
|
-
const t = this.readFile(e, "Gemfile"),
|
|
10
|
+
const t = this.readFile(e, "Gemfile"), i = {};
|
|
10
11
|
if (t) {
|
|
11
12
|
const n = t.matchAll(/gem\s+['"]([^'"]+)['"](?:,\s*['"]([^'"]+)['"])?/g);
|
|
12
|
-
for (const
|
|
13
|
-
s[
|
|
13
|
+
for (const s of n)
|
|
14
|
+
i[s[1]] = s[2] || "*";
|
|
14
15
|
}
|
|
15
16
|
return {
|
|
16
17
|
language: "Ruby",
|
|
17
18
|
name: this.getProjectName(e),
|
|
18
|
-
dependencies:
|
|
19
|
+
dependencies: i,
|
|
19
20
|
manifestFile: "Gemfile",
|
|
20
21
|
projectPath: e
|
|
21
22
|
};
|
|
@@ -25,7 +26,14 @@ class c extends r {
|
|
|
25
26
|
{
|
|
26
27
|
name: "ruby-core",
|
|
27
28
|
tags: ["ruby"],
|
|
28
|
-
content:
|
|
29
|
+
content: () => this.loadTemplateFromFile(
|
|
30
|
+
a.join(__dirname, "templates/lang-ruby.md")
|
|
31
|
+
).then((e) => e || this.getRubyFallbackTemplate())
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
getRubyFallbackTemplate() {
|
|
36
|
+
return `# Ruby Development Guidelines
|
|
29
37
|
|
|
30
38
|
This project uses Ruby.
|
|
31
39
|
|
|
@@ -42,9 +50,7 @@ This project uses Ruby.
|
|
|
42
50
|
## Testing
|
|
43
51
|
- Write tests with RSpec/Minitest
|
|
44
52
|
- Use fixtures and factories
|
|
45
|
-
- Aim for good coverage
|
|
46
|
-
}
|
|
47
|
-
];
|
|
53
|
+
- Aim for good coverage`;
|
|
48
54
|
}
|
|
49
55
|
getDependencyTagMap() {
|
|
50
56
|
return {
|
|
@@ -55,5 +61,5 @@ This project uses Ruby.
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
export {
|
|
58
|
-
|
|
64
|
+
m as RubyPlugin
|
|
59
65
|
};
|