@el-j/magic-helix-core 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/index-B88j4AyE.js +13 -0
- package/dist/index-B88j4AyE.js.map +1 -0
- package/dist/index-CY-pQbuu.cjs +2 -0
- package/dist/index-CY-pQbuu.cjs.map +1 -0
- package/dist/index.cjs +75 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2234 -51
- package/dist/index.mjs.map +1 -1
- package/dist/pattern-combiner.d.ts +1 -1
- package/dist/plugin-loader.d.ts +7 -1
- package/package.json +4 -4
- package/dist/BasePlugin-6wv0hYJ9.js +0 -98
- package/dist/BasePlugin-6wv0hYJ9.js.map +0 -1
- package/dist/BasePlugin-odQJAKA-.cjs +0 -2
- package/dist/BasePlugin-odQJAKA-.cjs.map +0 -1
- package/dist/builtin-plugins/base/BasePlugin.d.ts +0 -69
- package/dist/builtin-plugins/csharp/index.d.ts +0 -20
- package/dist/builtin-plugins/go/index.d.ts +0 -23
- package/dist/builtin-plugins/index.d.ts +0 -15
- package/dist/builtin-plugins/java/index.d.ts +0 -22
- package/dist/builtin-plugins/nodejs/index.d.ts +0 -44
- package/dist/builtin-plugins/php/index.d.ts +0 -20
- package/dist/builtin-plugins/python/index.d.ts +0 -27
- package/dist/builtin-plugins/ruby/index.d.ts +0 -20
- package/dist/builtin-plugins/rust/index.d.ts +0 -53
- package/dist/builtin-plugins/swift/index.d.ts +0 -22
- package/dist/default_templates/angular/angular-core.md +0 -19
- package/dist/default_templates/architecture/codeowners.md +0 -123
- package/dist/default_templates/architecture/monorepo.md +0 -146
- package/dist/default_templates/architecture/nx.md +0 -122
- package/dist/default_templates/architecture/turborepo.md +0 -114
- package/dist/default_templates/ci/github-actions.md +0 -268
- package/dist/default_templates/ci/gitlab-ci.md +0 -330
- package/dist/default_templates/containers/docker-multistage.md +0 -120
- package/dist/default_templates/containers/kubernetes-deploy.md +0 -210
- package/dist/default_templates/devops/docker-compose.md +0 -111
- package/dist/default_templates/devops/docker-dockerfile.md +0 -94
- package/dist/default_templates/devops/github-actions.md +0 -160
- package/dist/default_templates/devops/gitlab-ci.md +0 -210
- package/dist/default_templates/dotnet/framework-aspnetcore.md +0 -205
- package/dist/default_templates/dotnet/framework-blazor.md +0 -271
- package/dist/default_templates/dotnet/lang-csharp.md +0 -162
- package/dist/default_templates/generic/lang-typescript.md +0 -11
- package/dist/default_templates/generic/state-redux.md +0 -21
- package/dist/default_templates/generic/state-rxjs.md +0 -6
- package/dist/default_templates/generic/style-mui.md +0 -23
- package/dist/default_templates/generic/style-tailwind.md +0 -6
- package/dist/default_templates/generic/test-cypress.md +0 -21
- package/dist/default_templates/generic/test-jest.md +0 -20
- package/dist/default_templates/generic/test-playwright.md +0 -21
- package/dist/default_templates/generic/test-vitest.md +0 -6
- package/dist/default_templates/go/lang-go.md +0 -571
- package/dist/default_templates/java/build-gradle.md +0 -102
- package/dist/default_templates/java/build-maven.md +0 -86
- package/dist/default_templates/java/framework-spring-boot.md +0 -179
- package/dist/default_templates/java/lang-java.md +0 -78
- package/dist/default_templates/java/lang-kotlin.md +0 -88
- package/dist/default_templates/meta/magic-helix-meta.md +0 -213
- package/dist/default_templates/meta/meta-debug.md +0 -459
- package/dist/default_templates/meta/meta-implement.md +0 -450
- package/dist/default_templates/meta/meta-roadmap.md +0 -265
- package/dist/default_templates/nestjs/nestjs-core.md +0 -7
- package/dist/default_templates/patterns/architecture/clean-architecture.md +0 -469
- package/dist/default_templates/patterns/architecture/dependency-injection.md +0 -517
- package/dist/default_templates/patterns/architecture/domain-driven-design.md +0 -621
- package/dist/default_templates/patterns/architecture/layered-architecture.md +0 -382
- package/dist/default_templates/patterns/architecture/repository-pattern.md +0 -408
- package/dist/default_templates/patterns/domain-expertise/nextjs-rules.md +0 -115
- package/dist/default_templates/patterns/domain-expertise/react-patterns.md +0 -181
- package/dist/default_templates/patterns/domain-expertise/server-components.md +0 -212
- package/dist/default_templates/patterns/domain-expertise/shadcn-ui.md +0 -52
- package/dist/default_templates/patterns/domain-expertise/tailwind-patterns.md +0 -52
- package/dist/default_templates/patterns/environment/container-awareness.md +0 -17
- package/dist/default_templates/patterns/environment/ide-features.md +0 -17
- package/dist/default_templates/patterns/environment/os-commands.md +0 -17
- package/dist/default_templates/patterns/organization/heading-hierarchy.md +0 -103
- package/dist/default_templates/patterns/organization/sequential-workflows.md +0 -102
- package/dist/default_templates/patterns/organization/xml-rule-groups.md +0 -64
- package/dist/default_templates/patterns/reasoning/agent-loop.md +0 -151
- package/dist/default_templates/patterns/reasoning/confirmation-gates.md +0 -141
- package/dist/default_templates/patterns/reasoning/dependency-analysis.md +0 -132
- package/dist/default_templates/patterns/reasoning/one-tool-per-iteration.md +0 -152
- package/dist/default_templates/patterns/reasoning/preview-before-action.md +0 -194
- package/dist/default_templates/patterns/reasoning/reflection-checkpoints.md +0 -166
- package/dist/default_templates/patterns/reasoning/result-verification.md +0 -157
- package/dist/default_templates/patterns/reasoning/subtask-breakdown.md +0 -131
- package/dist/default_templates/patterns/reasoning/thinking-tags.md +0 -100
- package/dist/default_templates/patterns/role-definition/capability-declarations.md +0 -72
- package/dist/default_templates/patterns/role-definition/expert-identity.md +0 -45
- package/dist/default_templates/patterns/role-definition/scope-boundaries.md +0 -61
- package/dist/default_templates/patterns/safety/code-safety-rules.md +0 -17
- package/dist/default_templates/patterns/safety/credential-handling.md +0 -17
- package/dist/default_templates/patterns/safety/destructive-warnings.md +0 -17
- package/dist/default_templates/patterns/safety/refusal-messages.md +0 -17
- package/dist/default_templates/patterns/tone/adaptive-tone.md +0 -17
- package/dist/default_templates/patterns/tone/concise-communication.md +0 -17
- package/dist/default_templates/patterns/tone/forbidden-phrases.md +0 -17
- package/dist/default_templates/patterns/tool-guidelines/function-schemas.md +0 -143
- package/dist/default_templates/patterns/tool-guidelines/parameter-examples.md +0 -137
- package/dist/default_templates/patterns/tool-guidelines/usage-policies.md +0 -105
- package/dist/default_templates/php/framework-laravel.md +0 -112
- package/dist/default_templates/php/lang-php.md +0 -94
- package/dist/default_templates/python/lang-python.md +0 -508
- package/dist/default_templates/react/react-core.md +0 -677
- package/dist/default_templates/react/react-zustand.md +0 -7
- package/dist/default_templates/ruby/framework-rails.md +0 -309
- package/dist/default_templates/ruby/framework-sinatra.md +0 -227
- package/dist/default_templates/ruby/lang-ruby.md +0 -216
- package/dist/default_templates/rust/lang-rust.md +0 -89
- package/dist/default_templates/swift/framework-vapor.md +0 -352
- package/dist/default_templates/swift/lang-swift.md +0 -291
- package/dist/default_templates/vue/style-primevue.md +0 -6
- package/dist/default_templates/vue/style-quasar.md +0 -22
- package/dist/default_templates/vue/vue-core.md +0 -28
- package/dist/default_templates/vue/vue-pinia.md +0 -5
- package/dist/index-AkVwRl-r.js +0 -92
- package/dist/index-AkVwRl-r.js.map +0 -1
- package/dist/index-B6BeG1yT.cjs +0 -68
- package/dist/index-B6BeG1yT.cjs.map +0 -1
- package/dist/index-B8pyjKdF.js +0 -94
- package/dist/index-B8pyjKdF.js.map +0 -1
- package/dist/index-B_6W_RnJ.cjs +0 -76
- package/dist/index-B_6W_RnJ.cjs.map +0 -1
- package/dist/index-Bg8DD8ku.js +0 -216
- package/dist/index-Bg8DD8ku.js.map +0 -1
- package/dist/index-BkJhe5Af.js +0 -1748
- package/dist/index-BkJhe5Af.js.map +0 -1
- package/dist/index-Bv4Q1Pr7.cjs +0 -33
- package/dist/index-Bv4Q1Pr7.cjs.map +0 -1
- package/dist/index-CN8J45Nc.cjs +0 -24
- package/dist/index-CN8J45Nc.cjs.map +0 -1
- package/dist/index-CPbv2Od1.js +0 -62
- package/dist/index-CPbv2Od1.js.map +0 -1
- package/dist/index-Cf-MC6Al.js +0 -63
- package/dist/index-Cf-MC6Al.js.map +0 -1
- package/dist/index-DDPXXXDy.cjs +0 -19
- package/dist/index-DDPXXXDy.cjs.map +0 -1
- package/dist/index-DO30AzDe.cjs +0 -19
- package/dist/index-DO30AzDe.cjs.map +0 -1
- package/dist/index-Dm37u5ut.js +0 -2128
- package/dist/index-Dm37u5ut.js.map +0 -1
- package/dist/index-DqHvgoXJ.cjs +0 -19
- package/dist/index-DqHvgoXJ.cjs.map +0 -1
- package/dist/index-J1qAfsnO.cjs +0 -2
- package/dist/index-J1qAfsnO.cjs.map +0 -1
- package/dist/index-Jz0HYZ7B.js +0 -13
- package/dist/index-Jz0HYZ7B.js.map +0 -1
- package/dist/index-K39pdw94.cjs +0 -31
- package/dist/index-K39pdw94.cjs.map +0 -1
- package/dist/index-L3IVvhd1.cjs +0 -89
- package/dist/index-L3IVvhd1.cjs.map +0 -1
- package/dist/index-OT2XAJkc.js +0 -117
- package/dist/index-OT2XAJkc.js.map +0 -1
- package/dist/index-TPAX4XKg.cjs +0 -30
- package/dist/index-TPAX4XKg.cjs.map +0 -1
- package/dist/index-WmVSB57y.js +0 -107
- package/dist/index-WmVSB57y.js.map +0 -1
- package/dist/index-mYXvc3Fs.js +0 -68
- package/dist/index-mYXvc3Fs.js.map +0 -1
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Testing: Cypress
|
|
2
|
-
- **ALWAYS** use `data-cy` attributes for element selection.
|
|
3
|
-
- **ALWAYS** avoid using CSS selectors - prefer `data-cy` attributes.
|
|
4
|
-
- **ALWAYS** use page object pattern for reusable selectors.
|
|
5
|
-
- **ALWAYS** use `cy.intercept()` for API mocking and spying.
|
|
6
|
-
- **ALWAYS** use `cy.wait()` sparingly - prefer assertions over arbitrary waits.
|
|
7
|
-
- **ALWAYS** use `should()` for assertions instead of `then()`.
|
|
8
|
-
- **ALWAYS** test user journeys, not implementation details.
|
|
9
|
-
- **ALWAYS** use `cy.session()` for authentication state management.
|
|
10
|
-
- **ALWAYS** organize tests in `cypress/e2e/` directory.
|
|
11
|
-
- **ALWAYS** use `describe()` and `it()` for test organization.
|
|
12
|
-
- **ALWAYS** use `beforeEach()` for test setup and navigation.
|
|
13
|
-
- **ALWAYS** test both happy path and error scenarios.
|
|
14
|
-
- **ALWAYS** use `cy.fixture()` for test data management.
|
|
15
|
-
- **ALWAYS** use `cy.viewport()` to test responsive design.
|
|
16
|
-
- **ALWAYS** use `cy.pause()` during development, remove for CI.
|
|
17
|
-
- **ALWAYS** use `cy.debug()` for debugging, remove for production.
|
|
18
|
-
- **ALWAYS** run tests in headless mode for CI/CD.
|
|
19
|
-
- **ALWAYS** use `cy.request()` for API testing when UI testing isn't needed.
|
|
20
|
-
- **ALWAYS** use custom commands for reusable actions.
|
|
21
|
-
- **ALWAYS** maintain test execution time under 2 minutes per spec.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Testing: Jest
|
|
2
|
-
- **ALWAYS** use descriptive test names that explain what is being tested.
|
|
3
|
-
- **ALWAYS** follow the "Arrange, Act, Assert" pattern in tests.
|
|
4
|
-
- **ALWAYS** use `describe` blocks to group related tests.
|
|
5
|
-
- **ALWAYS** use `beforeEach` and `afterEach` for test setup and teardown.
|
|
6
|
-
- **ALWAYS** mock external dependencies with `jest.mock()` or `jest.fn()`.
|
|
7
|
-
- **ALWAYS** use `expect.assertions()` to ensure assertions are called.
|
|
8
|
-
- **ALWAYS** test both success and error cases.
|
|
9
|
-
- **ALWAYS** use `test.each` for parameterized tests.
|
|
10
|
-
- **ALWAYS** avoid testing implementation details - test behavior.
|
|
11
|
-
- **ALWAYS** use `jest.clearAllMocks()` in `beforeEach` for clean tests.
|
|
12
|
-
- **ALWAYS** use meaningful assertion messages with `expect().toBe(expected, message)`.
|
|
13
|
-
- **ALWAYS** test async code with `async/await` and proper error handling.
|
|
14
|
-
- **ALWAYS** use `jest.spyOn()` for spying on object methods.
|
|
15
|
-
- **ALWAYS** group mocks in a `mocks` directory with descriptive names.
|
|
16
|
-
- **ALWAYS** use `jest.setTimeout()` for long-running async tests.
|
|
17
|
-
- **ALWAYS** test edge cases and boundary conditions.
|
|
18
|
-
- **ALWAYS** use `test.todo()` for planned tests.
|
|
19
|
-
- **ALWAYS** maintain test coverage above 80%.
|
|
20
|
-
- **ALWAYS** run tests in CI/CD pipelines with `--coverage` flag.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Testing: Playwright
|
|
2
|
-
- **ALWAYS** use semantic locators like `getByRole()`, `getByLabel()`, `getByText()`.
|
|
3
|
-
- **ALWAYS** use `data-testid` attributes as fallback for complex selectors.
|
|
4
|
-
- **ALWAYS** use page object model for reusable selectors and actions.
|
|
5
|
-
- **ALWAYS** use `page.route()` for API mocking and network interception.
|
|
6
|
-
- **ALWAYS** use `expect().toBeVisible()` and `expect().toBeHidden()` for visibility checks.
|
|
7
|
-
- **ALWAYS** use `page.waitForLoadState('networkidle')` for page load completion.
|
|
8
|
-
- **ALWAYS** test cross-browser compatibility with different browser contexts.
|
|
9
|
-
- **ALWAYS** use `test.describe()` for grouping related tests.
|
|
10
|
-
- **ALWAYS** use `test.beforeEach()` for test setup and navigation.
|
|
11
|
-
- **ALWAYS** use `page.context().storageState()` for authentication persistence.
|
|
12
|
-
- **ALWAYS** use `page.screenshot()` for visual regression testing.
|
|
13
|
-
- **ALWAYS** use `test.use()` for configuring test fixtures.
|
|
14
|
-
- **ALWAYS** use `page.pause()` during development for debugging.
|
|
15
|
-
- **ALWAYS** use `test.skip()` and `test.only()` for test management.
|
|
16
|
-
- **ALWAYS** use `expect().toMatchSnapshot()` for visual comparisons.
|
|
17
|
-
- **ALWAYS** use `page.keyboard` and `page.mouse` for complex interactions.
|
|
18
|
-
- **ALWAYS** use `page.waitForFunction()` for custom wait conditions.
|
|
19
|
-
- **ALWAYS** run tests in parallel with `workers` configuration.
|
|
20
|
-
- **ALWAYS** use `testInfo.attachments` for test artifacts.
|
|
21
|
-
- **ALWAYS** maintain test execution time under 30 seconds per test.
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
# Testing: Vitest
|
|
2
|
-
- **ALWAYS** use `describe`, `it`, and `expect` syntax.
|
|
3
|
-
- **ALWAYS** mock dependencies using `vi.mock()`.
|
|
4
|
-
- **ALWAYS** clean up mocks after each test using `afterEach(() => { vi.restoreAllMocks(); })`.
|
|
5
|
-
- Use `it.todo('should do a thing')` for pending tests.
|
|
6
|
-
- For component testing, prefer `@vitest/ui` for a visual runner.
|
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
# Language: Go
|
|
2
|
-
|
|
3
|
-
## Architecture: Clean Architecture with Go Idioms
|
|
4
|
-
|
|
5
|
-
**Organize Go applications with clear boundaries: Handlers → Use Cases → Domain → Infrastructure.**
|
|
6
|
-
|
|
7
|
-
### Project Structure
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
project/
|
|
11
|
-
├── cmd/
|
|
12
|
-
│ └── server/
|
|
13
|
-
│ └── main.go # Application entry point
|
|
14
|
-
│
|
|
15
|
-
├── internal/
|
|
16
|
-
│ ├── handlers/ # HTTP handlers (presentation layer)
|
|
17
|
-
│ │ ├── user_handler.go
|
|
18
|
-
│ │ └── order_handler.go
|
|
19
|
-
│ │
|
|
20
|
-
│ ├── usecases/ # Business logic (application layer)
|
|
21
|
-
│ │ ├── user_usecase.go
|
|
22
|
-
│ │ └── order_usecase.go
|
|
23
|
-
│ │
|
|
24
|
-
│ ├── domain/ # Domain models and interfaces
|
|
25
|
-
│ │ ├── user.go # Domain entities
|
|
26
|
-
│ │ ├── order.go
|
|
27
|
-
│ │ └── repository.go # Repository interfaces
|
|
28
|
-
│ │
|
|
29
|
-
│ └── infrastructure/ # External concerns
|
|
30
|
-
│ ├── postgres/
|
|
31
|
-
│ │ └── user_repository.go
|
|
32
|
-
│ └── redis/
|
|
33
|
-
│ └── cache.go
|
|
34
|
-
│
|
|
35
|
-
└── pkg/ # Public, reusable packages
|
|
36
|
-
├── validator/
|
|
37
|
-
└── logger/
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Rule: Handlers Handle HTTP Only
|
|
41
|
-
|
|
42
|
-
**ALWAYS** keep HTTP handlers thin. They parse requests, call use cases, and serialize responses.
|
|
43
|
-
|
|
44
|
-
```go
|
|
45
|
-
// ✅ Good: Thin handler
|
|
46
|
-
package handlers
|
|
47
|
-
|
|
48
|
-
import (
|
|
49
|
-
"encoding/json"
|
|
50
|
-
"net/http"
|
|
51
|
-
"github.com/yourorg/project/internal/domain"
|
|
52
|
-
"github.com/yourorg/project/internal/usecases"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
type UserHandler struct {
|
|
56
|
-
userUseCase usecases.UserUseCase // Interface, not concrete type
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
func NewUserHandler(uc usecases.UserUseCase) *UserHandler {
|
|
60
|
-
return &UserHandler{userUseCase: uc}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|
64
|
-
// Parse request
|
|
65
|
-
var req CreateUserRequest
|
|
66
|
-
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
67
|
-
http.Error(w, "Invalid request", http.StatusBadRequest)
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Call use case (business logic)
|
|
72
|
-
user, err := h.userUseCase.CreateUser(r.Context(), req.Email, req.Name)
|
|
73
|
-
if err != nil {
|
|
74
|
-
if err == domain.ErrEmailExists {
|
|
75
|
-
http.Error(w, err.Error(), http.StatusConflict)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Serialize response
|
|
83
|
-
w.Header().Set("Content-Type", "application/json")
|
|
84
|
-
w.WriteStatus(http.StatusCreated)
|
|
85
|
-
json.NewEncoder(w).Encode(toUserResponse(user))
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ❌ Bad: Business logic in handler
|
|
89
|
-
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|
90
|
-
var req CreateUserRequest
|
|
91
|
-
json.NewDecoder(r.Body).Decode(&req)
|
|
92
|
-
|
|
93
|
-
// ❌ Validation in handler
|
|
94
|
-
if !strings.Contains(req.Email, "@") {
|
|
95
|
-
http.Error(w, "Invalid email", http.StatusBadRequest)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ❌ Database access in handler
|
|
100
|
-
var existing User
|
|
101
|
-
h.db.QueryRow("SELECT * FROM users WHERE email = $1", req.Email).Scan(&existing)
|
|
102
|
-
if existing.ID != "" {
|
|
103
|
-
http.Error(w, "Email exists", http.StatusConflict)
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ❌ Domain logic in handler
|
|
108
|
-
user := User{
|
|
109
|
-
ID: uuid.New().String(),
|
|
110
|
-
Email: req.Email,
|
|
111
|
-
Name: req.Name,
|
|
112
|
-
}
|
|
113
|
-
h.db.Exec("INSERT INTO users (id, email, name) VALUES ($1, $2, $3)",
|
|
114
|
-
user.ID, user.Email, user.Name)
|
|
115
|
-
|
|
116
|
-
json.NewEncoder(w).Encode(user)
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Rule: Use Cases Orchestrate Business Logic
|
|
121
|
-
|
|
122
|
-
**ALWAYS** put business logic in use cases. Use cases coordinate between repositories and domain models.
|
|
123
|
-
|
|
124
|
-
```go
|
|
125
|
-
// ✅ internal/usecases/user_usecase.go
|
|
126
|
-
package usecases
|
|
127
|
-
|
|
128
|
-
import (
|
|
129
|
-
"context"
|
|
130
|
-
"github.com/yourorg/project/internal/domain"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
// UserUseCase interface (for dependency inversion)
|
|
134
|
-
type UserUseCase interface {
|
|
135
|
-
CreateUser(ctx context.Context, email, name string) (*domain.User, error)
|
|
136
|
-
UpdateUserEmail(ctx context.Context, userID, newEmail string) (*domain.User, error)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// userUseCase implementation
|
|
140
|
-
type userUseCase struct {
|
|
141
|
-
userRepo domain.UserRepository // Interface from domain layer
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
func NewUserUseCase(userRepo domain.UserRepository) UserUseCase {
|
|
145
|
-
return &userUseCase{userRepo: userRepo}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
func (uc *userUseCase) CreateUser(ctx context.Context, email, name string) (*domain.User, error) {
|
|
149
|
-
// Validate using domain methods
|
|
150
|
-
if err := domain.ValidateEmail(email); err != nil {
|
|
151
|
-
return nil, err
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Business rule: Check uniqueness
|
|
155
|
-
existing, err := uc.userRepo.FindByEmail(ctx, email)
|
|
156
|
-
if err != nil {
|
|
157
|
-
return nil, err
|
|
158
|
-
}
|
|
159
|
-
if existing != nil {
|
|
160
|
-
return nil, domain.ErrEmailExists
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Create domain entity
|
|
164
|
-
user := domain.NewUser(email, name)
|
|
165
|
-
|
|
166
|
-
// Persist
|
|
167
|
-
if err := uc.userRepo.Save(ctx, user); err != nil {
|
|
168
|
-
return nil, err
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return user, nil
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
func (uc *userUseCase) UpdateUserEmail(ctx context.Context, userID, newEmail string) (*domain.User, error) {
|
|
175
|
-
user, err := uc.userRepo.FindByID(ctx, userID)
|
|
176
|
-
if err != nil {
|
|
177
|
-
return nil, err
|
|
178
|
-
}
|
|
179
|
-
if user == nil {
|
|
180
|
-
return nil, domain.ErrUserNotFound
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Use domain method (business logic)
|
|
184
|
-
if err := user.ChangeEmail(newEmail); err != nil {
|
|
185
|
-
return nil, err
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if err := uc.userRepo.Save(ctx, user); err != nil {
|
|
189
|
-
return nil, err
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return user, nil
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Rule: Domain Layer Contains Business Rules
|
|
197
|
-
|
|
198
|
-
**ALWAYS** put business rules and invariants in domain entities. Keep them infrastructure-agnostic.
|
|
199
|
-
|
|
200
|
-
```go
|
|
201
|
-
// ✅ internal/domain/user.go - Domain entity
|
|
202
|
-
package domain
|
|
203
|
-
|
|
204
|
-
import (
|
|
205
|
-
"errors"
|
|
206
|
-
"regexp"
|
|
207
|
-
"strings"
|
|
208
|
-
"time"
|
|
209
|
-
"github.com/google/uuid"
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
// Domain errors (business errors)
|
|
213
|
-
var (
|
|
214
|
-
ErrInvalidEmail = errors.New("invalid email format")
|
|
215
|
-
ErrEmailExists = errors.New("email already exists")
|
|
216
|
-
ErrUserNotFound = errors.New("user not found")
|
|
217
|
-
ErrInvalidName = errors.New("name must be at least 2 characters")
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
// User is a domain entity
|
|
221
|
-
type User struct {
|
|
222
|
-
ID string
|
|
223
|
-
Email string
|
|
224
|
-
Name string
|
|
225
|
-
CreatedAt time.Time
|
|
226
|
-
IsActive bool
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// NewUser creates a new user with validation
|
|
230
|
-
func NewUser(email, name string) *User {
|
|
231
|
-
return &User{
|
|
232
|
-
ID: uuid.New().String(),
|
|
233
|
-
Email: email,
|
|
234
|
-
Name: name,
|
|
235
|
-
CreatedAt: time.Now().UTC(),
|
|
236
|
-
IsActive: true,
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ChangeEmail changes user email with validation
|
|
241
|
-
func (u *User) ChangeEmail(newEmail string) error {
|
|
242
|
-
if err := ValidateEmail(newEmail); err != nil {
|
|
243
|
-
return err
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if strings.EqualFold(u.Email, newEmail) {
|
|
247
|
-
return errors.New("new email must be different")
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
u.Email = newEmail
|
|
251
|
-
return nil
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Deactivate deactivates the user account
|
|
255
|
-
func (u *User) Deactivate() error {
|
|
256
|
-
if !u.IsActive {
|
|
257
|
-
return errors.New("user is already inactive")
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
u.IsActive = false
|
|
261
|
-
return nil
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// IsAdmin checks if user is admin (business rule)
|
|
265
|
-
func (u *User) IsAdmin() bool {
|
|
266
|
-
return strings.HasSuffix(u.Email, "@company.com")
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ValidateEmail validates email format
|
|
270
|
-
func ValidateEmail(email string) error {
|
|
271
|
-
emailRegex := regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
|
272
|
-
if !emailRegex.MatchString(email) {
|
|
273
|
-
return ErrInvalidEmail
|
|
274
|
-
}
|
|
275
|
-
if len(email) > 255 {
|
|
276
|
-
return errors.New("email too long")
|
|
277
|
-
}
|
|
278
|
-
return nil
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Rule: Repository Interfaces in Domain, Implementation in Infrastructure
|
|
283
|
-
|
|
284
|
-
**ALWAYS** define repository interfaces in domain layer. Implement them in infrastructure layer.
|
|
285
|
-
|
|
286
|
-
```go
|
|
287
|
-
// ✅ internal/domain/repository.go - Interface in domain
|
|
288
|
-
package domain
|
|
289
|
-
|
|
290
|
-
import "context"
|
|
291
|
-
|
|
292
|
-
// UserRepository defines the contract for user data access
|
|
293
|
-
type UserRepository interface {
|
|
294
|
-
Save(ctx context.Context, user *User) error
|
|
295
|
-
FindByID(ctx context.Context, id string) (*User, error)
|
|
296
|
-
FindByEmail(ctx context.Context, email string) (*User, error)
|
|
297
|
-
Delete(ctx context.Context, id string) error
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ✅ internal/infrastructure/postgres/user_repository.go - Implementation
|
|
301
|
-
package postgres
|
|
302
|
-
|
|
303
|
-
import (
|
|
304
|
-
"context"
|
|
305
|
-
"database/sql"
|
|
306
|
-
"errors"
|
|
307
|
-
"github.com/yourorg/project/internal/domain"
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
type userRepository struct {
|
|
311
|
-
db *sql.DB
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// NewUserRepository creates a new PostgreSQL user repository
|
|
315
|
-
func NewUserRepository(db *sql.DB) domain.UserRepository {
|
|
316
|
-
return &userRepository{db: db}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
func (r *userRepository) Save(ctx context.Context, user *domain.User) error {
|
|
320
|
-
query := `
|
|
321
|
-
INSERT INTO users (id, email, name, created_at, is_active)
|
|
322
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
323
|
-
ON CONFLICT (id) DO UPDATE
|
|
324
|
-
SET email = $2, name = $3, is_active = $5
|
|
325
|
-
`
|
|
326
|
-
|
|
327
|
-
_, err := r.db.ExecContext(ctx, query,
|
|
328
|
-
user.ID,
|
|
329
|
-
user.Email,
|
|
330
|
-
user.Name,
|
|
331
|
-
user.CreatedAt,
|
|
332
|
-
user.IsActive,
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
return err
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
func (r *userRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
|
339
|
-
query := `SELECT id, email, name, created_at, is_active FROM users WHERE id = $1`
|
|
340
|
-
|
|
341
|
-
var user domain.User
|
|
342
|
-
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
|
343
|
-
&user.ID,
|
|
344
|
-
&user.Email,
|
|
345
|
-
&user.Name,
|
|
346
|
-
&user.CreatedAt,
|
|
347
|
-
&user.IsActive,
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
if errors.Is(err, sql.ErrNoRows) {
|
|
351
|
-
return nil, nil // Not found
|
|
352
|
-
}
|
|
353
|
-
if err != nil {
|
|
354
|
-
return nil, err
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return &user, nil
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
|
361
|
-
query := `SELECT id, email, name, created_at, is_active FROM users WHERE email = $1`
|
|
362
|
-
|
|
363
|
-
var user domain.User
|
|
364
|
-
err := r.db.QueryRowContext(ctx, query, email).Scan(
|
|
365
|
-
&user.ID,
|
|
366
|
-
&user.Email,
|
|
367
|
-
&user.Name,
|
|
368
|
-
&user.CreatedAt,
|
|
369
|
-
&user.IsActive,
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
if errors.Is(err, sql.ErrNoRows) {
|
|
373
|
-
return nil, nil
|
|
374
|
-
}
|
|
375
|
-
if err != nil {
|
|
376
|
-
return nil, err
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return &user, nil
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
|
383
|
-
query := `DELETE FROM users WHERE id = $1`
|
|
384
|
-
_, err := r.db.ExecContext(ctx, query, id)
|
|
385
|
-
return err
|
|
386
|
-
}
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### Complete Example: Main Setup (Dependency Injection)
|
|
390
|
-
|
|
391
|
-
```go
|
|
392
|
-
// cmd/server/main.go - Wire dependencies
|
|
393
|
-
package main
|
|
394
|
-
|
|
395
|
-
import (
|
|
396
|
-
"database/sql"
|
|
397
|
-
"log"
|
|
398
|
-
"net/http"
|
|
399
|
-
|
|
400
|
-
_ "github.com/lib/pq"
|
|
401
|
-
"github.com/yourorg/project/internal/handlers"
|
|
402
|
-
"github.com/yourorg/project/internal/infrastructure/postgres"
|
|
403
|
-
"github.com/yourorg/project/internal/usecases"
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
func main() {
|
|
407
|
-
// Infrastructure: Database
|
|
408
|
-
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db?sslmode=disable")
|
|
409
|
-
if err != nil {
|
|
410
|
-
log.Fatal(err)
|
|
411
|
-
}
|
|
412
|
-
defer db.Close()
|
|
413
|
-
|
|
414
|
-
// Infrastructure: Repositories (concrete implementations)
|
|
415
|
-
userRepo := postgres.NewUserRepository(db)
|
|
416
|
-
orderRepo := postgres.NewOrderRepository(db)
|
|
417
|
-
|
|
418
|
-
// Use Cases (business logic)
|
|
419
|
-
userUseCase := usecases.NewUserUseCase(userRepo)
|
|
420
|
-
orderUseCase := usecases.NewOrderUseCase(orderRepo, userRepo)
|
|
421
|
-
|
|
422
|
-
// Handlers (HTTP layer)
|
|
423
|
-
userHandler := handlers.NewUserHandler(userUseCase)
|
|
424
|
-
orderHandler := handlers.NewOrderHandler(orderUseCase)
|
|
425
|
-
|
|
426
|
-
// Routes
|
|
427
|
-
http.HandleFunc("/users", userHandler.CreateUser)
|
|
428
|
-
http.HandleFunc("/orders", orderHandler.CreateOrder)
|
|
429
|
-
|
|
430
|
-
log.Println("Server starting on :8080")
|
|
431
|
-
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## Go Best Practices
|
|
436
|
-
|
|
437
|
-
- **ALWAYS** use `gofmt` to format code consistently.
|
|
438
|
-
- **ALWAYS** handle errors explicitly - never ignore them.
|
|
439
|
-
- **ALWAYS** use meaningful variable names (camelCase).
|
|
440
|
-
- **ALWAYS** write comprehensive godoc comments for exported functions/types.
|
|
441
|
-
- **ALWAYS** use `defer` for resource cleanup.
|
|
442
|
-
- **ALWAYS** prefer `struct` embedding over inheritance.
|
|
443
|
-
- **ALWAYS** use interfaces for abstraction and testing.
|
|
444
|
-
- **ALWAYS** use `context.Context` for cancellation and timeouts.
|
|
445
|
-
- **ALWAYS** use `go vet` and `golint` for code quality checks.
|
|
446
|
-
- **ALWAYS** write table-driven tests.
|
|
447
|
-
- **ALWAYS** use `t.Parallel()` for independent test functions.
|
|
448
|
-
- **ALWAYS** use `sync.WaitGroup` or channels for goroutine synchronization.
|
|
449
|
-
- **ALWAYS** avoid global variables - pass dependencies explicitly.
|
|
450
|
-
- **ALWAYS** use `panic` only for unrecoverable errors.
|
|
451
|
-
- **ALWAYS** use `log` package or structured logging for production.
|
|
452
|
-
- **ALWAYS** use `bufio` for efficient I/O operations.
|
|
453
|
-
- **ALWAYS** prefer `strings.Builder` for string concatenation in loops.
|
|
454
|
-
- **ALWAYS** use `time.Time` for time handling, not `int64`.
|
|
455
|
-
- **ALWAYS** use `json` tags for struct field serialization.
|
|
456
|
-
- **ALWAYS** use `iota` for enumerated constants.
|
|
457
|
-
- **ALWAYS** keep functions short and focused on single responsibilities.
|
|
458
|
-
|
|
459
|
-
## Testing Strategy
|
|
460
|
-
|
|
461
|
-
**Handlers:** Test HTTP contracts (mock use cases)
|
|
462
|
-
```go
|
|
463
|
-
// internal/handlers/user_handler_test.go
|
|
464
|
-
func TestCreateUser(t *testing.T) {
|
|
465
|
-
mockUseCase := &MockUserUseCase{
|
|
466
|
-
CreateUserFunc: func(ctx context.Context, email, name string) (*domain.User, error) {
|
|
467
|
-
return &domain.User{ID: "123", Email: email, Name: name}, nil
|
|
468
|
-
},
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
handler := NewUserHandler(mockUseCase)
|
|
472
|
-
|
|
473
|
-
req := httptest.NewRequest("POST", "/users", strings.NewReader(`{"email":"test@example.com","name":"Test"}`))
|
|
474
|
-
w := httptest.NewRecorder()
|
|
475
|
-
|
|
476
|
-
handler.CreateUser(w, req)
|
|
477
|
-
|
|
478
|
-
assert.Equal(t, http.StatusCreated, w.Code)
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
**Use Cases:** Test business logic (mock repositories)
|
|
483
|
-
```go
|
|
484
|
-
// internal/usecases/user_usecase_test.go
|
|
485
|
-
func TestCreateUser_EmailExists(t *testing.T) {
|
|
486
|
-
mockRepo := &MockUserRepository{
|
|
487
|
-
FindByEmailFunc: func(ctx context.Context, email string) (*domain.User, error) {
|
|
488
|
-
return &domain.User{ID: "existing"}, nil // Email exists
|
|
489
|
-
},
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
uc := NewUserUseCase(mockRepo)
|
|
493
|
-
|
|
494
|
-
_, err := uc.CreateUser(context.Background(), "test@example.com", "Test")
|
|
495
|
-
|
|
496
|
-
assert.Equal(t, domain.ErrEmailExists, err)
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
**Domain Models:** Test business rules (no mocking)
|
|
501
|
-
```go
|
|
502
|
-
// internal/domain/user_test.go
|
|
503
|
-
func TestUser_ChangeEmail(t *testing.T) {
|
|
504
|
-
tests := []struct {
|
|
505
|
-
name string
|
|
506
|
-
newEmail string
|
|
507
|
-
expectErr bool
|
|
508
|
-
}{
|
|
509
|
-
{"valid email", "new@example.com", false},
|
|
510
|
-
{"invalid email", "not-an-email", true},
|
|
511
|
-
{"same email", "test@example.com", true},
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
for _, tt := range tests {
|
|
515
|
-
t.Run(tt.name, func(t *testing.T) {
|
|
516
|
-
user := NewUser("test@example.com", "Test")
|
|
517
|
-
err := user.ChangeEmail(tt.newEmail)
|
|
518
|
-
|
|
519
|
-
if tt.expectErr {
|
|
520
|
-
assert.Error(t, err)
|
|
521
|
-
} else {
|
|
522
|
-
assert.NoError(t, err)
|
|
523
|
-
assert.Equal(t, tt.newEmail, user.Email)
|
|
524
|
-
}
|
|
525
|
-
})
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
**Repositories:** Integration tests with real database
|
|
531
|
-
```go
|
|
532
|
-
// internal/infrastructure/postgres/user_repository_test.go
|
|
533
|
-
func TestUserRepository_Save(t *testing.T) {
|
|
534
|
-
db := setupTestDB(t) // Helper to create test DB
|
|
535
|
-
defer db.Close()
|
|
536
|
-
|
|
537
|
-
repo := NewUserRepository(db)
|
|
538
|
-
user := domain.NewUser("test@example.com", "Test User")
|
|
539
|
-
|
|
540
|
-
err := repo.Save(context.Background(), user)
|
|
541
|
-
assert.NoError(t, err)
|
|
542
|
-
|
|
543
|
-
found, err := repo.FindByID(context.Background(), user.ID)
|
|
544
|
-
assert.NoError(t, err)
|
|
545
|
-
assert.Equal(t, user.Email, found.Email)
|
|
546
|
-
}
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
## Architecture Summary
|
|
550
|
-
|
|
551
|
-
1. **Handlers** (`internal/handlers/`) = HTTP layer
|
|
552
|
-
- Parse requests, serialize responses
|
|
553
|
-
- Call use cases
|
|
554
|
-
- No business logic
|
|
555
|
-
|
|
556
|
-
2. **Use Cases** (`internal/usecases/`) = Business logic
|
|
557
|
-
- Orchestrate domain and repositories
|
|
558
|
-
- Application workflows
|
|
559
|
-
- Transaction boundaries
|
|
560
|
-
|
|
561
|
-
3. **Domain** (`internal/domain/`) = Business rules
|
|
562
|
-
- Entities with behavior
|
|
563
|
-
- Repository interfaces
|
|
564
|
-
- Framework-agnostic
|
|
565
|
-
|
|
566
|
-
4. **Infrastructure** (`internal/infrastructure/`) = External concerns
|
|
567
|
-
- Repository implementations
|
|
568
|
-
- Database access
|
|
569
|
-
- External APIs
|
|
570
|
-
|
|
571
|
-
**Reference:** See `patterns/architecture/clean-architecture.md`, `patterns/architecture/repository-pattern.md`, and `patterns/architecture/dependency-injection.md` for detailed architectural guidance.
|