|
| 1 | +# AI Coding Agent Instructions for go-zulip |
| 2 | + |
| 3 | + |
| 4 | +## Project Overview |
| 5 | + |
| 6 | +**go-zulip** is a Go API client for the Zulip group chat platform. The codebase consists of 100+ Go files organized into a modular service-based architecture. The project structure and tooling have been updated for improved maintainability and code quality. |
| 7 | + |
| 8 | + |
| 9 | +### Key Facts |
| 10 | +- **Language**: Go 1.25.2 |
| 11 | +- **Scope**: Zulip REST API v1.0.0 client |
| 12 | +- **Testing**: Integration tests with live Zulip server, automated via GitHub Actions |
| 13 | +- **Linters/Formatters**: `golangci-lint`, `golangci-lint fmt`, enforced via pre-commit hooks |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | + |
| 18 | +## Directory Structure |
| 19 | + |
| 20 | +``` |
| 21 | +go.mod, go.sum, LICENSE, README.md |
| 22 | +internal/ |
| 23 | + apiutils/ # API utility helpers |
| 24 | + clients/ # HTTP client implementations and helpers |
| 25 | + enum/ # Enum types |
| 26 | + strict_decoder/ # Strict JSON decoding utilities |
| 27 | + test_utils/ # Test helpers |
| 28 | + union/ # Union type marshaling logic |
| 29 | +scripts/ # Dev scripts |
| 30 | +zulip/ |
| 31 | + *.go # Data models, API logic, and tests |
| 32 | + api/ # API domain service implementations |
| 33 | + client/ # Main client and statistics |
| 34 | + events/ # Event types and event handling |
| 35 | +.github/ |
| 36 | + workflows/ # CI configuration (GitHub Actions) |
| 37 | +.pre-commit-config.yaml # Pre-commit hooks config |
| 38 | +``` |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## Architecture Patterns |
| 43 | + |
| 44 | +### 1. **Service-Based Client Architecture** |
| 45 | +The codebase uses a **service-oriented architecture** where the main client (`client.Client`) aggregates multiple API domain services: |
| 46 | +- **Client**: `client.Client` interface in `zulip/client/client.go` aggregates multiple API domain interfaces |
| 47 | +- **Services**: Each API domain (authentication, channels, messages, etc.) has its own service implementation |
| 48 | +- **API Client**: Core HTTP client in `zulip/internal/api_client/APIClient` handles HTTP communication |
| 49 | +- **Why**: Clean separation of concerns, easy testing, and modular design |
| 50 | + |
| 51 | +### 2. **Builder Pattern for Requests** |
| 52 | +Every API endpoint uses the **Builder Pattern** for fluent configuration: |
| 53 | +```go |
| 54 | +// Example from api/channels/api_channels.go |
| 55 | +resp, httpRes, err := client.APIChannels.AddDefaultChannel(ctx). |
| 56 | + ChannelID(123). |
| 57 | + Execute() |
| 58 | +``` |
| 59 | +Structure: |
| 60 | +- Service method returns a `Request` struct (e.g., `AddDefaultChannelRequest`) |
| 61 | +- Setter methods (e.g., `.ChannelID()`) return the same request type for chaining |
| 62 | +- `.Execute()` finalizes and sends the request |
| 63 | + |
| 64 | +### 3. **API Domain Organization** |
| 65 | +API functionality is organized by domain with clear separation: |
| 66 | +- `zulip/api/*/api_*.go` - API service implementations with request builders |
| 67 | +- `zulip/api/*/api_*_responses.go` - Response types for each domain |
| 68 | +- `zulip/*.go` - Data models (150+ enum and struct types) |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | + |
| 73 | +## CI, Testing, and Linters |
| 74 | + |
| 75 | +### Continuous Integration (CI) |
| 76 | +- **GitHub Actions**: All pushes and PRs trigger workflows in `.github/workflows/`. |
| 77 | + - Linting: Runs `golangci-lint run` and `golangci-lint fmt` checks. |
| 78 | + - Testing: Runs `go test ./zulip/...` (requires Zulip server credentials for integration tests). |
| 79 | + - Pre-commit: Validates all pre-commit hooks pass. |
| 80 | + |
| 81 | +### Pre-commit Hooks |
| 82 | +- Configured in `.pre-commit-config.yaml` (run `pre-commit install` to enable). |
| 83 | +- Enforces: |
| 84 | + - `golangci-lint` (static analysis, style, and bug checks) |
| 85 | + - `golangci-lint fmt` (formatting) |
| 86 | + - Trailing whitespace, end-of-file, and YAML checks |
| 87 | + |
| 88 | +### Running Locally |
| 89 | +- **Lint:** `golangci-lint run` |
| 90 | +- **Format:** `golangci-lint fmt ./...` |
| 91 | +- **Test:** `go test ./zulip/...` |
| 92 | +- **Pre-commit:** `pre-commit run --all-files` |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +## Configuration & Authentication |
| 97 | + |
| 98 | +### Client Configuration |
| 99 | +Create a client using `zulip.RC` config with email, API key, and server URL: |
| 100 | +```go |
| 101 | +config := &z.RC{ |
| 102 | + |
| 103 | + APIKey: "your-api-key", |
| 104 | + Site: "https://zulip.example.com", |
| 105 | +} |
| 106 | +client, err := client.NewClient(config) |
| 107 | +``` |
| 108 | + |
| 109 | +Optional configuration: |
| 110 | +- Client certificates: `ClientCert`, `ClientCertKey`, `CertBundle` |
| 111 | +- Skip TLS verification: `Insecure: true` |
| 112 | +- Custom HTTP client: `client.WithHTTPClient(httpClient)` |
| 113 | +- Custom logger: Configure via `client.WithLogger()` option (uses `slog` by default) |
| 114 | + |
| 115 | +For testing, there are helper functions that automatically fetch test credentials from the development server. |
| 116 | + |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Critical Patterns & Conventions |
| 121 | + |
| 122 | +### 1. **Error Handling** |
| 123 | +Three error types to know: |
| 124 | +- `APIError` - General API failures (wraps body and unmarshaled model) |
| 125 | +- `CodedError` - Server error with `code` field (e.g., "BAD_REQUEST", "UNAUTHORIZED") |
| 126 | +- Special errors: `RateLimitedError`, `BadEventQueueIdError`, `NonExistingChannelIdError` |
| 127 | + |
| 128 | +Example: |
| 129 | +```go |
| 130 | +resp, _, err := client.GetUser(ctx, 123).Execute() |
| 131 | +if err != nil { |
| 132 | + var codedErr z.CodedError |
| 133 | + if errors.As(err, &codedErr) { |
| 134 | + log.Printf("API error %s: %s", codedErr.Code, codedErr.Msg) |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +### 2. **Union Types** (in `zulip/internal/utils/union.go`) |
| 140 | +Handles OpenAPI discriminated unions using reflection-based marshaling/unmarshaling: |
| 141 | +- Struct with pointer fields for each union variant |
| 142 | +- `MarshalUnionType()`/`UnmarshalUnionType()` ensures exactly ONE field is set |
| 143 | +- Used for ambiguous response types (e.g., narrow types, event types) |
| 144 | + |
| 145 | +**Note:** Event types live in the `events` package (e.g., `events/message_event.go`) instead of the `zulip` package, because they blow up the package docks significantly and the real-time events API behaves very differently. |
| 146 | + |
| 147 | +### 3. **Narrow Types** (in `narrow.go`) |
| 148 | +Represents query filters for messages - complex union type with 15+ variants: |
| 149 | +```go |
| 150 | +client.GetMessages(ctx). |
| 151 | + Narrow(z.Where(z.ChannelNameIs("announcements")). |
| 152 | + And(z.TopicIs("releases"))) |
| 153 | +``` |
| 154 | + |
| 155 | +### 4. **Response Objects** |
| 156 | +All API responses inherit from `Response`: |
| 157 | +```go |
| 158 | +type Response struct { |
| 159 | + Result string `json:"result"` // "success" or "error" |
| 160 | + Msg string `json:"msg,omitempty"` // error message if result="error" |
| 161 | +} |
| 162 | +``` |
| 163 | +Specific responses add domain fields (e.g., `GetUsersResponse.Members []User`). |
| 164 | + |
| 165 | +**Event types are now located in the `events` package.** For example, see `events/message_event.go` for message event types. |
| 166 | + |
| 167 | +### 5. **Timestamp Handling** |
| 168 | +Timestamps from Zulip API are returned as integers or floats, but the client converts them to from and to `time.Time` or `time.Duration` transparently. |
| 169 | + |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +## Testing Patterns |
| 174 | + |
| 175 | +### Test File Structure |
| 176 | +Tests follow standard Go patterns with integration focus: |
| 177 | +- Tests in `*_test.go` files in separate `zulip_test` package |
| 178 | +- Requires valid Zulip server running in development mode |
| 179 | +- Tests use helper functions defined within test files |
| 180 | + |
| 181 | +### Common Test Patterns |
| 182 | +```go |
| 183 | +// 1. Setup - create resources |
| 184 | +channelID := createChannelWithAllClients(t) |
| 185 | + |
| 186 | +// 2. Execute |
| 187 | +resp, _, err := client.AddReaction(ctx, msgID). |
| 188 | + EmojiName("smile"). |
| 189 | + Execute() |
| 190 | + |
| 191 | +// 3. Verify |
| 192 | +require.NoError(t, err) |
| 193 | +require.NotNil(t, resp) |
| 194 | +``` |
| 195 | + |
| 196 | +### Multi-Client Testing |
| 197 | +Tests often run against multiple clients (normal, admin, bot) to verify permission handling: |
| 198 | +```go |
| 199 | +t.Run("TestName", RunForAllClients(t, func(t *testing.T, apiClient z.Client) { |
| 200 | + // test runs once per client |
| 201 | +})) |
| 202 | +``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | + |
| 207 | +### Build, Lint, and Test |
| 208 | +```bash |
| 209 | +# Build |
| 210 | +go build ./... |
| 211 | + |
| 212 | +# Lint (static analysis) |
| 213 | +golangci-lint run |
| 214 | + |
| 215 | +# Format |
| 216 | +golangci-lint fmt ./... |
| 217 | + |
| 218 | +# Run all tests (requires live Zulip server) |
| 219 | +go test ./... -v |
| 220 | + |
| 221 | +# Run specific test |
| 222 | +go test ./... -run TestName |
| 223 | + |
| 224 | +# Run all pre-commit hooks |
| 225 | +pre-commit run -a |
| 226 | +``` |
| 227 | + |
| 228 | +### Manual Code Organization |
| 229 | +This is a hand-written codebase with clear separation of concerns: |
| 230 | +- `zulip/api/*/api_*.go` - API domain service implementations with request builders |
| 231 | +- `zulip/api/*/api_*_responses.go` - Response types organized by domain |
| 232 | +- `zulip/*.go` - Data models and enums |
| 233 | +- `zulip/client/client.go` - Main client aggregating all API services |
| 234 | +- `zulip/internal/api_client/api_client.go` - Core HTTP client implementation |
| 235 | +- `zulip/errors.go` - Error type definitions |
| 236 | +- `zulip/internal/utils/union.go` - Union type marshaling logic |
| 237 | +- `zulip/zuliprc/zuliprc.go` - Configuration loading from INI files |
| 238 | +- `*_test.go` - Integration tests |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | + |
| 243 | +## Key Files & Directories |
| 244 | + |
| 245 | +| File/Dir | Purpose | |
| 246 | +|----------|---------| |
| 247 | +| `zulip/client/client.go` | Client interface definition and service aggregation | |
| 248 | +| `internal/clients/` | HTTP client implementations and helpers | |
| 249 | +| `internal/union/union.go` | Union type marshaling logic | |
| 250 | +| `zulip/rc.go` | Configuration loading from INI files | |
| 251 | +| `zulip/errors.go` | Error type definitions | |
| 252 | +| `zulip/api/*/api_*.go` | Domain-specific API service implementations | |
| 253 | +| `zulip/events/` | Event types and event handling | |
| 254 | +| `zulip/*.go` | Data model structs and enums | |
| 255 | +| `*_test.go` | Integration tests | |
| 256 | + |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +## When Adding New Features |
| 261 | +1. **Add to appropriate API interface** - Domain files already have interface definitions |
| 262 | +2. **Update request type** - Add request struct and setter methods (follow existing patterns in `api_*.go`) |
| 263 | +3. **Write integration tests** - Use helpers and `RunForAllClients()` for multi-client testing |
| 264 | +4. **Handle edge cases** - Account for permission-based API differences (admin vs user vs bot) |
| 265 | +5. **Update documentation** - Ensure README and code comments reflect new functionality |
| 266 | +6. **Lint and test** - Run `golangci-lint` and tests to ensure code quality |
0 commit comments