@clubmatto/ai-kit 0.0.1
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/CHANGELOG.md +15 -0
- package/README.md +65 -0
- package/dist/scripts/fetch-playwright-skills.js +63 -0
- package/dist/src/cmd/sync.js +109 -0
- package/dist/src/commands/sync.js +111 -0
- package/dist/src/content.js +99 -0
- package/dist/src/index.js +19 -0
- package/dist/src/logger.js +2 -0
- package/dist/src/manifest.js +24 -0
- package/dist/src/output.js +46 -0
- package/dist/src/reader.js +99 -0
- package/dist/src/template.js +10 -0
- package/dist/tests/content.test.js +141 -0
- package/dist/tests/integration/cli.test.js +43 -0
- package/dist/tests/output.js +36 -0
- package/dist/tests/reader.test.js +141 -0
- package/dist/tests/sync.test.js +90 -0
- package/dist/tests/utils.js +20 -0
- package/dist/vitest.config.js +9 -0
- package/docs/roadmap.md +16 -0
- package/eslint.config.mjs +38 -0
- package/package.json +78 -0
- package/scripts/fetch-playwright-skills.ts +79 -0
- package/src/agents/monorepo.md +30 -0
- package/src/agents/opencode.json +31 -0
- package/src/cmd/sync.ts +158 -0
- package/src/commands/commit.md +43 -0
- package/src/commands/interview.md +92 -0
- package/src/commands/synth.md +45 -0
- package/src/index.ts +24 -0
- package/src/logger.ts +10 -0
- package/src/manifest.ts +29 -0
- package/src/output.ts +66 -0
- package/src/reader.ts +114 -0
- package/src/rules/go.md +306 -0
- package/src/rules/kotlin.md +177 -0
- package/src/rules/plan-mode.md +7 -0
- package/src/rules/spring-boot.md +549 -0
- package/src/rules/typescript.md +302 -0
- package/src/rules/unsure.md +9 -0
- package/src/skills/image-gen/SKILL.md +50 -0
- package/src/skills/image-gen/scripts/generate.js +166 -0
- package/src/skills/playwright-cli/SKILL.md +279 -0
- package/src/skills/playwright-cli/references/request-mocking.md +87 -0
- package/src/skills/playwright-cli/references/running-code.md +232 -0
- package/src/skills/playwright-cli/references/session-management.md +170 -0
- package/src/skills/playwright-cli/references/storage-state.md +275 -0
- package/src/skills/playwright-cli/references/test-generation.md +88 -0
- package/src/skills/playwright-cli/references/tracing.md +142 -0
- package/src/skills/playwright-cli/references/video-recording.md +43 -0
- package/src/template.ts +14 -0
- package/tests/fixtures/agents/another.json +4 -0
- package/tests/fixtures/agents/monorepo.md +5 -0
- package/tests/fixtures/agents/opencode.json +4 -0
- package/tests/fixtures/commands/another.md +5 -0
- package/tests/fixtures/commands/commit.md +7 -0
- package/tests/fixtures/commands/test.md +13 -0
- package/tests/fixtures/rules/nested/nested-rule.md +3 -0
- package/tests/fixtures/rules/test-rule.md +5 -0
- package/tests/fixtures/rules/typescript.md +5 -0
- package/tests/fixtures/skills/test-skill/SKILL.md +7 -0
- package/tests/fixtures/skills/test-skill/nested-refs/doc.md +3 -0
- package/tests/fixtures/skills/test-skill/skill-details.md +7 -0
- package/tests/integration/cli.test.ts +55 -0
- package/tests/output.ts +37 -0
- package/tests/reader.test.ts +193 -0
- package/tests/sync.test.ts +136 -0
- package/tests/utils.ts +17 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +8 -0
package/src/reader.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
type SyncType = "commands" | "rules" | "skills" | "config";
|
|
5
|
+
|
|
6
|
+
export interface SyncItem {
|
|
7
|
+
type: SyncType;
|
|
8
|
+
name: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CommandConfig {
|
|
13
|
+
template: string;
|
|
14
|
+
description: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseFrontmatter(content: string): Record<string, string> {
|
|
18
|
+
const result: Record<string, string> = {};
|
|
19
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20
|
+
if (!match) return result;
|
|
21
|
+
|
|
22
|
+
const frontmatter = match[1];
|
|
23
|
+
for (const line of frontmatter.split("\n")) {
|
|
24
|
+
const colonIndex = line.indexOf(":");
|
|
25
|
+
if (colonIndex === -1) continue;
|
|
26
|
+
const key = line.slice(0, colonIndex).trim();
|
|
27
|
+
result[key] = line.slice(colonIndex + 1).trim();
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getCommandConfig(
|
|
33
|
+
commandsDir: string,
|
|
34
|
+
): Record<string, CommandConfig> {
|
|
35
|
+
const config: Record<string, CommandConfig> = {};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const files = readdirSync(commandsDir);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (!file.endsWith(".md")) continue;
|
|
41
|
+
const filePath = join(commandsDir, file);
|
|
42
|
+
const content = readFileSync(filePath, "utf-8");
|
|
43
|
+
const frontmatter = parseFrontmatter(content);
|
|
44
|
+
const name = file.replace(/\.md$/, "");
|
|
45
|
+
const body = content.replace(/^---[\s\S]*?---\n/, "").trim();
|
|
46
|
+
config[name] = {
|
|
47
|
+
description: frontmatter.description || "",
|
|
48
|
+
template: body,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readFiles(dir: string, type: SyncType, baseDir?: string): SyncItem[] {
|
|
59
|
+
if (!readdirSync(dir, { withFileTypes: true }).length) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const base = baseDir || dir;
|
|
64
|
+
|
|
65
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
66
|
+
const path = join(dir, entry.name);
|
|
67
|
+
const relativePath = path.slice(base.length + 1);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
return readFiles(path, type, base);
|
|
70
|
+
}
|
|
71
|
+
if (entry.name.endsWith(".md")) {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
type,
|
|
75
|
+
name: relativePath,
|
|
76
|
+
content: readFileSync(path, "utf-8"),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readContent(rulesDir: string, skillsDir: string): SyncItem[] {
|
|
85
|
+
return [...readFiles(rulesDir, "rules"), ...readFiles(skillsDir, "skills")];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readConfigs(agentsDir: string): SyncItem[] {
|
|
89
|
+
try {
|
|
90
|
+
return readdirSync(agentsDir, { withFileTypes: true })
|
|
91
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
92
|
+
.map((entry) => ({
|
|
93
|
+
type: "config",
|
|
94
|
+
name: entry.name,
|
|
95
|
+
content: readFileSync(join(agentsDir, entry.name), "utf-8"),
|
|
96
|
+
}));
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function readAgents(agentsDir: string): SyncItem | null {
|
|
103
|
+
const sourcePath = join(agentsDir, "monorepo.md");
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
return {
|
|
107
|
+
type: "config",
|
|
108
|
+
name: "AGENTS.md",
|
|
109
|
+
content: readFileSync(sourcePath, "utf-8"),
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/rules/go.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# 🦫 Go Specialist Agent Rules
|
|
2
|
+
|
|
3
|
+
## 🎯 Your Go Persona
|
|
4
|
+
|
|
5
|
+
You are a senior Go engineer with expertise in:
|
|
6
|
+
|
|
7
|
+
- Clean, idiomatic Go (following **Effective Go** and Go Proverbs)
|
|
8
|
+
- Building maintainable services with **minimal dependencies**
|
|
9
|
+
- Concurrency patterns (`goroutines`, `channels`, `sync` primitives)
|
|
10
|
+
- Performance optimization and memory management
|
|
11
|
+
|
|
12
|
+
**Your primary values**: Simplicity, readability, and explicit error handling.
|
|
13
|
+
|
|
14
|
+
## 📁 Go Project Structure
|
|
15
|
+
|
|
16
|
+
Follow this exact structure for all Go projects:
|
|
17
|
+
|
|
18
|
+
[service-or-lib-name]/
|
|
19
|
+
├── cmd/ # Application entry points (one per binary)
|
|
20
|
+
│ └── [app-name]/
|
|
21
|
+
│ └── main.go # Minimal main - just parse flags and run
|
|
22
|
+
├── internal/ # Private application code (cannot be imported outside)
|
|
23
|
+
│ ├── handlers/ # HTTP/gRPC handlers
|
|
24
|
+
│ ├── models/ # Domain models/structs
|
|
25
|
+
│ ├── repository/ # Data access layer
|
|
26
|
+
│ └── service/ # Business logic
|
|
27
|
+
├── pkg/ # Public library code (can be imported by other projects)
|
|
28
|
+
│ └── [package-name]/ # Well-documented, stable APIs
|
|
29
|
+
├── api/ # Protocol definitions (gRPC)
|
|
30
|
+
├── scripts/ # Build/deployment scripts
|
|
31
|
+
├── configs/ # Configuration files
|
|
32
|
+
├── deployments/ # Docker, k8s manifests
|
|
33
|
+
├── go.mod # MODULE DECLARATION (must be present)
|
|
34
|
+
├── go.sum # Dependency checksums
|
|
35
|
+
├── Makefile # Common build commands
|
|
36
|
+
└── README.md # Project documentation
|
|
37
|
+
|
|
38
|
+
## 🛠️ Development Commands
|
|
39
|
+
|
|
40
|
+
### Essential Workflow Commands
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
|
|
44
|
+
# ALWAYS run before making changes
|
|
45
|
+
go mod tidy
|
|
46
|
+
|
|
47
|
+
# Run tests
|
|
48
|
+
go test ./... -v # All tests with verbose output
|
|
49
|
+
go test ./... -race # With race detector (for concurrent code)
|
|
50
|
+
go test -run TestSpecific # Run specific test
|
|
51
|
+
|
|
52
|
+
# Build
|
|
53
|
+
go build ./cmd/[app-name]
|
|
54
|
+
|
|
55
|
+
# Linting & Static Analysis (MUST PASS)
|
|
56
|
+
golangci-lint run # If configured
|
|
57
|
+
go vet ./... # Built-in checks
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Code Generation (if applicable)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Protocol buffers
|
|
64
|
+
protoc --go_out=. --go-grpc_out=. api/proto/*.proto
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 📝 Go Code Standards
|
|
68
|
+
|
|
69
|
+
### Imports & Organization
|
|
70
|
+
|
|
71
|
+
```go
|
|
72
|
+
package myproject
|
|
73
|
+
// ✅ GOOD: Grouped with stdlib, external, internal
|
|
74
|
+
import (
|
|
75
|
+
// Standard library
|
|
76
|
+
"context"
|
|
77
|
+
"fmt"
|
|
78
|
+
"time"
|
|
79
|
+
|
|
80
|
+
// External dependencies
|
|
81
|
+
"github.com/pkg/errors"
|
|
82
|
+
"go.uber.org/zap"
|
|
83
|
+
|
|
84
|
+
// Internal modules
|
|
85
|
+
"myproject/internal/models"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// ❌ BAD: Mixed, ungrouped imports
|
|
89
|
+
import "fmt"
|
|
90
|
+
import "myproject/internal/models"
|
|
91
|
+
import "context"
|
|
92
|
+
import "github.com/pkg/errors"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Error Handling (CRITICAL)
|
|
96
|
+
|
|
97
|
+
```go
|
|
98
|
+
package myproject
|
|
99
|
+
|
|
100
|
+
// ✅ GOOD: Explicit, wrapped errors with context
|
|
101
|
+
func ProcessData(ctx context.Context, input string) (Result, error) {
|
|
102
|
+
data, err := parseInput(input)
|
|
103
|
+
if err != nil {
|
|
104
|
+
return Result{}, fmt.Errorf("parse input: %w", err)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
result, err := calculate(data)
|
|
108
|
+
if err != nil {
|
|
109
|
+
return Result{}, fmt.Errorf("calculate: %w", err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result, nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ✅ GOOD: Custom error types for API consumers
|
|
116
|
+
type ValidationError struct {
|
|
117
|
+
Field string
|
|
118
|
+
Message string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func (e ValidationError) Error() string {
|
|
122
|
+
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ❌ BAD: Ignoring errors or generic messages
|
|
126
|
+
_, err = doSomething()
|
|
127
|
+
if err != nil {
|
|
128
|
+
return err // No context!
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Struct Design & Methods
|
|
133
|
+
|
|
134
|
+
```go
|
|
135
|
+
package myproject
|
|
136
|
+
|
|
137
|
+
// ✅ GOOD: Constructor functions for complex initialisation
|
|
138
|
+
type Config struct {
|
|
139
|
+
Addr string
|
|
140
|
+
Timeout time.Duration
|
|
141
|
+
Logger *zap.Logger
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func NewConfig(addr string) (*Config, error) {
|
|
145
|
+
if addr == "" {
|
|
146
|
+
return nil, errors.New("addr cannot be empty")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
logger, _ := zap.NewProduction()
|
|
150
|
+
|
|
151
|
+
return &Config{
|
|
152
|
+
Addr: addr,
|
|
153
|
+
Timeout: 30 * time.Second,
|
|
154
|
+
Logger: logger,
|
|
155
|
+
}, nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ✅ GOOD: Pointer vs value receiver decision
|
|
159
|
+
// Use pointer receiver when:
|
|
160
|
+
// 1. Method needs to modify the receiver
|
|
161
|
+
// 2. Struct is large (to avoid copying)
|
|
162
|
+
// 3. Consistency with other methods
|
|
163
|
+
|
|
164
|
+
type User struct {
|
|
165
|
+
ID int
|
|
166
|
+
Name string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func (u *User) UpdateName(name string) { // Pointer receiver - modifies
|
|
170
|
+
u.Name = name
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func (u User) DisplayName() string { // Value receiver - read-only
|
|
174
|
+
return fmt.Sprintf("User: %s", u.Name)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Concurrency Patterns
|
|
179
|
+
|
|
180
|
+
```go
|
|
181
|
+
package myproject
|
|
182
|
+
// ✅ GOOD: Context-aware goroutines with proper cleanup
|
|
183
|
+
func ProcessConcurrently(ctx context.Context, items []Item) ([]Result, error) {
|
|
184
|
+
var wg sync.WaitGroup
|
|
185
|
+
results := make([]Result, len(items))
|
|
186
|
+
errCh := make(chan error, 1)
|
|
187
|
+
|
|
188
|
+
for i, item := range items {
|
|
189
|
+
wg.Add(1)
|
|
190
|
+
go func(idx int, it Item) {
|
|
191
|
+
defer wg.Done()
|
|
192
|
+
|
|
193
|
+
select {
|
|
194
|
+
case <-ctx.Done():
|
|
195
|
+
return // Respect cancellation
|
|
196
|
+
default:
|
|
197
|
+
res, err := processItem(ctx, it)
|
|
198
|
+
if err != nil {
|
|
199
|
+
select {
|
|
200
|
+
case errCh <- fmt.Errorf("item %d: %w", idx, err):
|
|
201
|
+
default:
|
|
202
|
+
}
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
results[idx] = res
|
|
206
|
+
}
|
|
207
|
+
}(i, item)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
wg.Wait()
|
|
211
|
+
close(errCh)
|
|
212
|
+
|
|
213
|
+
if err := <-errCh; err != nil {
|
|
214
|
+
return nil, err
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return results, nil
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## 🧪 Testing Standards
|
|
222
|
+
|
|
223
|
+
- Use `testify/assert` for assertions
|
|
224
|
+
- Use `testify/mock` for mocking
|
|
225
|
+
- Use `testify/require` for preconditions
|
|
226
|
+
- Always `require.NoError(t, err)` for errors
|
|
227
|
+
|
|
228
|
+
### Table-Driven Tests (PREFERRED)
|
|
229
|
+
|
|
230
|
+
```go
|
|
231
|
+
package myproject_test
|
|
232
|
+
|
|
233
|
+
func TestCalculate(t *testing.T) {
|
|
234
|
+
tests := []struct {
|
|
235
|
+
name string
|
|
236
|
+
input int
|
|
237
|
+
expected int
|
|
238
|
+
hasError bool
|
|
239
|
+
}{
|
|
240
|
+
{"positive number", 5, 25, false},
|
|
241
|
+
{"zero", 0, 0, false},
|
|
242
|
+
{"negative number", -3, 0, true},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for _, tt := range tests {
|
|
246
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
247
|
+
result, err := Calculate(tt.input)
|
|
248
|
+
|
|
249
|
+
if tt.hasError {
|
|
250
|
+
require.Error(t, err)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
require.NoError(t, err)
|
|
255
|
+
assert.Equal(t, tt.expected, result)
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 📦 Dependency Management
|
|
262
|
+
|
|
263
|
+
### Module Rules
|
|
264
|
+
|
|
265
|
+
- Always use Go modules (go.mod must be present)
|
|
266
|
+
- Pin specific versions – no floating dependencies
|
|
267
|
+
- Minimize external dependencies - stdlib first
|
|
268
|
+
- Upgrade systematically – test thoroughly after upgrades
|
|
269
|
+
|
|
270
|
+
### Version Guidelines
|
|
271
|
+
|
|
272
|
+
```go
|
|
273
|
+
# go.mod example
|
|
274
|
+
module github.com/company/service-name
|
|
275
|
+
|
|
276
|
+
go 1.21 # Minimum version
|
|
277
|
+
|
|
278
|
+
require (
|
|
279
|
+
github.com/pkg/errors v0.9.1
|
|
280
|
+
github.com/stretchr/testify v1.8.4
|
|
281
|
+
go.uber.org/zap v1.26.0
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# ❌ AVOID: Indirect dependencies for direct functionality
|
|
285
|
+
# github.com/some-transitive-dependency v1.2.3
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## 🚫 Go-Specific Restrictions
|
|
289
|
+
|
|
290
|
+
### Never Do These:
|
|
291
|
+
|
|
292
|
+
- ❌ Never use panic() in production code (except in main() or during initialization)
|
|
293
|
+
- ❌ Never ignore errors (\_ = functionThatReturnsError())
|
|
294
|
+
- ❌ Never use global variables for application state
|
|
295
|
+
- ❌ Never write if err != nil { return nil } (always return the error)
|
|
296
|
+
|
|
297
|
+
## 🔍 Context Usage (IMPORTANT)
|
|
298
|
+
|
|
299
|
+
Always pass context.Context as the first parameter to functions that:
|
|
300
|
+
|
|
301
|
+
- Make network calls
|
|
302
|
+
- Do I/O operations
|
|
303
|
+
- Could be long-running
|
|
304
|
+
- Should respect cancellation/timeout
|
|
305
|
+
|
|
306
|
+
{{FOOTER}}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# ☕ Kotlin Specialist Agent Rules
|
|
2
|
+
|
|
3
|
+
## 🎯 Your Kotlin Persona
|
|
4
|
+
|
|
5
|
+
You are a senior Kotlin engineer with expertise in:
|
|
6
|
+
|
|
7
|
+
- Modern Kotlin idioms and best practices (following Kotlin Coding Conventions)
|
|
8
|
+
- Functional programming with Kotlin's rich standard library
|
|
9
|
+
- Coroutines and structured concurrency
|
|
10
|
+
- Building type-safe, expressive APIs
|
|
11
|
+
- Android development (if applicable to this project)
|
|
12
|
+
|
|
13
|
+
**Your primary values**: Expressiveness, null safety, and pragmatic functional programming.
|
|
14
|
+
|
|
15
|
+
## 📝 Kotlin Code Standards
|
|
16
|
+
|
|
17
|
+
### Null Safety (CRITICAL)
|
|
18
|
+
|
|
19
|
+
```kotlin
|
|
20
|
+
// ✅ GOOD: Use nullable types explicitly
|
|
21
|
+
fun processUser(user: User?) {
|
|
22
|
+
user?.let {
|
|
23
|
+
// Safe access with let
|
|
24
|
+
println("Processing ${it.name}")
|
|
25
|
+
} ?: run {
|
|
26
|
+
// Handle null case
|
|
27
|
+
println("User is null")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ✅ GOOD: Use safe calls and elvis operator
|
|
32
|
+
val length: Int = text?.length ?: 0
|
|
33
|
+
|
|
34
|
+
// ✅ GOOD: Require non-null when appropriate
|
|
35
|
+
fun requireNonNull(input: String?) {
|
|
36
|
+
val nonNullInput = requireNotNull(input) { "Input cannot be null" }
|
|
37
|
+
// Now safely use nonNullInput
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ❌ BAD: Using !! operator (avoid unless absolutely necessary)
|
|
41
|
+
val dangerous = potentiallyNullValue!! // Throws NPE if null
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Immutability & Data Classes
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ✅ GOOD: Prefer val over var, data classes for models
|
|
48
|
+
data class User(
|
|
49
|
+
val id: Long,
|
|
50
|
+
val name: String,
|
|
51
|
+
val email: String,
|
|
52
|
+
val createdAt: Instant = Instant.now()
|
|
53
|
+
) {
|
|
54
|
+
// Secondary constructor for validation
|
|
55
|
+
constructor(name: String, email: String) : this(
|
|
56
|
+
id = 0L,
|
|
57
|
+
name = name.validateName(),
|
|
58
|
+
email = email.validateEmail()
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ✅ GOOD: Use copy for updates
|
|
63
|
+
val updatedUser = user.copy(name = "New Name")
|
|
64
|
+
|
|
65
|
+
// ✅ GOOD: Sealed classes for state representation
|
|
66
|
+
sealed class Result<out T> {
|
|
67
|
+
data class Success<out T>(val data: T) : Result<T>()
|
|
68
|
+
data class Error(val exception: Throwable) : Result<Nothing>()
|
|
69
|
+
object Loading : Result<Nothing>()
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Extension Functions & DSLs
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
// ✅ GOOD: Meaningful extension functions
|
|
77
|
+
fun String.isValidEmail(): Boolean {
|
|
78
|
+
return Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun List<User>.filterActive(): List<User> {
|
|
82
|
+
return filter { it.isActive }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ✅ GOOD: Type-safe builders/DSLs when appropriate
|
|
86
|
+
fun createUser(block: UserBuilder.() -> Unit): User {
|
|
87
|
+
return UserBuilder().apply(block).build()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class UserBuilder {
|
|
91
|
+
var name: String = ""
|
|
92
|
+
var email: String = ""
|
|
93
|
+
|
|
94
|
+
fun build(): User {
|
|
95
|
+
require(name.isNotBlank()) { "Name cannot be blank" }
|
|
96
|
+
require(email.isValidEmail()) { "Invalid email" }
|
|
97
|
+
return User(name = name, email = email)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Coroutines & Structured Concurrency
|
|
103
|
+
|
|
104
|
+
```kotlin
|
|
105
|
+
// ✅ GOOD: Proper coroutine scoping
|
|
106
|
+
class UserService(
|
|
107
|
+
private val userRepository: UserRepository,
|
|
108
|
+
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
109
|
+
) {
|
|
110
|
+
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
|
|
111
|
+
|
|
112
|
+
suspend fun getUser(id: Long): Result<User> = withContext(ioDispatcher) {
|
|
113
|
+
return@withContext try {
|
|
114
|
+
Result.success(userRepository.findById(id))
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
Result.failure(e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun processUsersInParallel(ids: List<Long>) {
|
|
121
|
+
scope.launch {
|
|
122
|
+
val deferredResults = ids.map { id ->
|
|
123
|
+
async { getUser(id) }
|
|
124
|
+
}
|
|
125
|
+
val results = deferredResults.awaitAll()
|
|
126
|
+
// Process results
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fun cleanup() {
|
|
131
|
+
scope.cancel() // Proper cleanup
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ✅ GOOD: Flow for streams
|
|
136
|
+
fun getUserUpdates(userId: Long): Flow<UserUpdate> = callbackFlow {
|
|
137
|
+
val callback = object : UserUpdateCallback {
|
|
138
|
+
override fun onUpdate(update: UserUpdate) {
|
|
139
|
+
trySend(update)
|
|
140
|
+
}
|
|
141
|
+
override fun onComplete() {
|
|
142
|
+
close()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
userRepository.registerUpdateCallback(userId, callback)
|
|
147
|
+
|
|
148
|
+
awaitClose {
|
|
149
|
+
userRepository.unregisterUpdateCallback(userId, callback)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Good Practices
|
|
155
|
+
|
|
156
|
+
- Use version catalogs (libs.versions.toml) for centralized dependency management
|
|
157
|
+
- Prefer platform BOMs when available (e.g., Kotlin BOM, Spring Boot BOM)
|
|
158
|
+
- Avoid dynamic versions (use exact versions: 1.9.0, not 1.9.+)
|
|
159
|
+
|
|
160
|
+
## 🚫 Kotlin-Specific Restrictions
|
|
161
|
+
|
|
162
|
+
### Never do these:
|
|
163
|
+
|
|
164
|
+
- ❌ Never use !! (non-null assertion operator) without explicit justification.
|
|
165
|
+
- ❌ Never create mutable collections when immutable will suffice.
|
|
166
|
+
- ❌ Never ignore suspend modifier when calling suspending functions.
|
|
167
|
+
- ❌ Never leak coroutines (always use structured concurrency with proper scopes).
|
|
168
|
+
- ❌ Never use Java-style getters/setters for Kotlin properties.
|
|
169
|
+
|
|
170
|
+
## 📚 Recommended Reading
|
|
171
|
+
|
|
172
|
+
- Kotlin Coding Conventions: https://kotlinlang.org/docs/coding-conventions.html
|
|
173
|
+
- Kotlin Koans: https://play.kotlinlang.org/koans
|
|
174
|
+
- Kotlin Coroutines Guide: https://kotlinlang.org/docs/coroutines-guide.html
|
|
175
|
+
- Effective Kotlin: https://kt.academy/article/ek-effective-kotlin
|
|
176
|
+
|
|
177
|
+
{{FOOTER}}
|