docs: add agent bootstrap and contract read router
This commit is contained in:
@@ -11,3 +11,31 @@ Canonical file transfer UX patterns for Go web applications:
|
||||
This pattern covers UI and UX contracts. Business-specific validation and file schemas remain in
|
||||
the host project's own architecture docs.
|
||||
|
||||
## Export Handler Sketch
|
||||
|
||||
```go
|
||||
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})
|
||||
|
||||
w := csv.NewWriter(c.Writer)
|
||||
w.Comma = ';'
|
||||
w.Write([]string{"ID", "Name", "Price"})
|
||||
|
||||
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 {
|
||||
slog.Error("csv export failed mid-stream", "err", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Locale Notes
|
||||
|
||||
- BOM avoids broken UTF-8 in Excel on Windows.
|
||||
- Semicolon avoids single-column imports in RU/EU locales.
|
||||
- Decimal comma keeps numbers numeric in Excel.
|
||||
- `DD.MM.YYYY` is preferred over ISO dates for user-facing spreadsheet exports.
|
||||
|
||||
@@ -2,124 +2,38 @@
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Import Workflow
|
||||
See `README.md` for the reference export handler and locale examples.
|
||||
|
||||
Recommended stages:
|
||||
## Import Rules
|
||||
|
||||
1. `Upload`
|
||||
2. `Preview / Validate`
|
||||
3. `Confirm`
|
||||
4. `Execute`
|
||||
5. `Result summary`
|
||||
- Recommended flow: `Upload -> Preview / Validate -> Confirm -> Execute -> Result summary`.
|
||||
- Validation preview must be human-readable.
|
||||
- Warnings and errors should be visible per row and in aggregate.
|
||||
- The confirm step must communicate scope and side effects clearly.
|
||||
|
||||
Rules:
|
||||
## Export 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.
|
||||
- The user must explicitly choose export scope when ambiguity exists, such as `selected`, `filtered`, or `all`.
|
||||
- Export format must be explicit.
|
||||
- Download responses must set `Content-Type` and `Content-Disposition` correctly.
|
||||
|
||||
## Export Workflow
|
||||
## CSV Rules
|
||||
|
||||
- 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="..."`
|
||||
- For spreadsheet-facing CSV, write UTF-8 BOM as the first bytes.
|
||||
- Use semicolon `;` as the CSV delimiter.
|
||||
- Use comma as the decimal separator for user-facing numeric values.
|
||||
- Use `DD.MM.YYYY` for user-facing dates.
|
||||
- Use `encoding/csv` with `csv.Writer.Comma = ';'` so quoting and escaping stay correct.
|
||||
|
||||
## CSV Format Rules (Excel-compatible)
|
||||
## Streaming Rules
|
||||
|
||||
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.
|
||||
- Large exports must stream rows directly to the response. Do not load the full dataset into memory first.
|
||||
- Use the canonical flow:
|
||||
`Handler -> Service -> Repository callback -> csv.Writer`
|
||||
- Repository queries should avoid N+1 by using JOINs or another batched shape.
|
||||
- Always call `csv.Writer.Flush()` after writing rows.
|
||||
|
||||
## 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.
|
||||
|
||||
- Import errors must map to clear user-facing messages.
|
||||
- Once streaming has started, export failures are logged server-side only. Do not try to change the HTTP status after headers/body bytes were already sent.
|
||||
|
||||
Reference in New Issue
Block a user