Files
bible/rules/patterns/import-export/contract.md
2026-03-01 17:16:50 +03:00

126 lines
4.2 KiB
Markdown

# Contract: Import / Export Workflows
Version: 1.0
## Import Workflow
Recommended stages:
1. `Upload`
2. `Preview / Validate`
3. `Confirm`
4. `Execute`
5. `Result summary`
Rules:
- Validation preview must be human-readable (table/list), not raw JSON only.
- Warnings and errors should be shown per row and in aggregate summary.
- Confirm step should clearly communicate scope and side effects.
## Export Workflow
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
- Export format should be explicit (`csv`, `json`, etc.).
- Download response must set:
- `Content-Type: text/csv; charset=utf-8`
- `Content-Disposition: attachment; filename="..."`
## CSV Format Rules (Excel-compatible)
These rules are **mandatory** whenever CSV is exported for spreadsheet users.
### Encoding and BOM
- Write UTF-8 BOM (`\xEF\xBB\xBF`) as the very first bytes of the response.
- Without BOM, Excel on Windows opens UTF-8 CSV as ANSI and garbles Cyrillic/special characters.
```go
w.Write([]byte{0xEF, 0xBB, 0xBF})
```
### Delimiter
- Use **semicolon** (`;`) as the field delimiter, not comma.
- Excel in Russian/European locale uses semicolon as the list separator.
- Comma-delimited files open as a single column in these locales.
### Numbers
- Write decimal numbers with a **comma** as the decimal separator: `1 234,56` — not `1234.56`.
- Excel in Russian locale does not recognize period as a decimal separator in numeric cells.
- Format integers and floats explicitly; do not rely on Go's default `%v` or `strconv.FormatFloat`.
- Use a thin non-breaking space (`\u202F`) or regular space as a thousands separator when the value
benefits from readability (e.g. prices, quantities > 9999).
```go
// correct
fmt.Sprintf("%.2f", price) // then replace "." -> ","
strings.ReplaceAll(fmt.Sprintf("%.2f", price), ".", ",")
// wrong — produces "1234.56", Excel treats it as text in RU locale
fmt.Sprintf("%.2f", price)
```
### Dates
- Write dates as `DD.MM.YYYY` — the format Excel in Russian locale parses as a date cell automatically.
- Do not use ISO 8601 (`2006-01-02`) for user-facing CSV; it is not auto-recognized as a date in RU locale.
### Text quoting
- Wrap any field that contains the delimiter (`;`), a newline, or a double-quote in double quotes.
- Escape embedded double-quotes by doubling them: `""`.
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
## Streaming Export Architecture (Go)
For exports with potentially large row counts use a 3-layer streaming pattern.
Never load all rows into memory before writing — stream directly to the response writer.
```
Handler → sets HTTP headers + writes BOM → calls Service
Service → delegates to Repository with a row callback
Repository → queries in batches → calls callback per row
Handler/Service → csv.Writer.Flush() after all rows
```
```go
// Handler
func ExportCSV(c *gin.Context) {
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM
w := csv.NewWriter(c.Writer)
w.Comma = ';'
w.Write([]string{"ID", "Name", "Price"}) // header row
err := svc.StreamRows(ctx, filters, func(row Row) error {
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
})
w.Flush()
if err != nil {
// headers already sent — log only, cannot change status
slog.Error("csv export failed mid-stream", "err", err)
}
}
// Repository — batch fetch with callback
func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error {
rows, err := r.db.QueryContext(ctx, query, args...)
// ... scan and call fn(row) for each row
}
```
- Use `JOIN` in the repository query to avoid N+1 per row.
- Batch size is optional; streaming row-by-row is fine for most datasets.
- Always call `w.Flush()` after the loop — `csv.Writer` buffers internally.
## Error Handling
- Import errors should map to clear user-facing messages.
- Export errors after streaming starts must be logged server-side only — HTTP headers are already
sent and the status code cannot be changed mid-stream.