заведён реестр префиксов, правила канона перенумерованы
- идентификатор правила теперь `<ПРЕФИКС>-<номер>` вместо `R<номер>`: префикс уникален по всему канону, поэтому ссылка больше не требует пути к файлу и не зависит от того, на какой оси файл лежит - префикс выбирается под файл, а не выводится по формуле, и хранится в conventions/prefixes.toml вместе с выбывшими; номера сохранены один в один вместе с дырами
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
prefix: GCFG
|
||||
extends: arch/config.md
|
||||
---
|
||||
|
||||
@@ -13,7 +14,7 @@ extends: arch/config.md
|
||||
|
||||
## Правила
|
||||
|
||||
### R1. Формат конфигурации — TOML
|
||||
### GCFG-1. Формат конфигурации — TOML
|
||||
|
||||
**ДОЛЖЕН.** Конфиг — файл TOML.
|
||||
|
||||
@@ -25,7 +26,7 @@ extends: arch/config.md
|
||||
поправленный руками на сервере, ломается заметно, а не меняет вложенность
|
||||
молча.
|
||||
|
||||
### R2. Разбор и валидация — целиком в `internal/config`
|
||||
### GCFG-2. Разбор и валидация — целиком в `internal/config`
|
||||
|
||||
**ДОЛЖЕН.** Чтение файла, наложение умолчаний и проверки живут в
|
||||
`internal/config`; наружу пакет отдаёт готовую структуру `Config`.
|
||||
@@ -34,11 +35,11 @@ extends: arch/config.md
|
||||
после — уже нет, и это единственная граница, на которой такое утверждение
|
||||
проверяемо. Валидация, размазанная по потребителям, отвечает на вопрос
|
||||
«проверено ли это поле» только чтением всех вызывающих, часть полей
|
||||
неизбежно окажется непроверенной, и fail-fast (R15) выродится в отказ
|
||||
неизбежно окажется непроверенной, и fail-fast (GCFG-15) выродится в отказ
|
||||
посреди работы. Экспортированный разбор вдобавок даёт второй способ
|
||||
получить конфиг — мимо умолчаний (R5).
|
||||
получить конфиг — мимо умолчаний (GCFG-5).
|
||||
|
||||
### R3. Весь конфиг — одна корневая структура
|
||||
### GCFG-3. Весь конфиг — одна корневая структура
|
||||
|
||||
**ДОЛЖЕН.** Конфиг представлен одним значением типа `Config`, собранным из
|
||||
под-структур по секциям.
|
||||
@@ -50,7 +51,7 @@ extends: arch/config.md
|
||||
(включена интеграция — заданы все её поля) при этом перестают быть
|
||||
проверяемыми в одном месте.
|
||||
|
||||
### R4. Под-структуры названы по секциям файла
|
||||
### GCFG-4. Под-структуры названы по секциям файла
|
||||
|
||||
**СЛЕДУЕТ.** Имя под-структуры совпадает с именем секции TOML.
|
||||
|
||||
@@ -60,7 +61,7 @@ extends: arch/config.md
|
||||
восстанавливается чтением тегов, и проделывать это приходится для каждой
|
||||
секции заново.
|
||||
|
||||
### R5. Умолчания задаёт `Default()`
|
||||
### GCFG-5. Умолчания задаёт `Default()`
|
||||
|
||||
**ДОЛЖЕН.** Умолчания собраны в функции `Default()`, разобранный файл
|
||||
накладывается поверх.
|
||||
@@ -72,7 +73,7 @@ extends: arch/config.md
|
||||
подставляют разное. `Default()` — единственное место, откуда список
|
||||
умолчаний читается разом и переносится в образец.
|
||||
|
||||
### R6. Имя файла фиксировано, путь переопределяется флагом
|
||||
### GCFG-6. Имя файла фиксировано, путь переопределяется флагом
|
||||
|
||||
**СЛЕДУЕТ.** По умолчанию читается `config.toml` в рабочей директории,
|
||||
путь переопределяет флаг `--config=path`, образец рядом —
|
||||
@@ -85,7 +86,7 @@ extends: arch/config.md
|
||||
`config.example.toml` вдобавок делает расхождение образца с реальным
|
||||
конфигом видимым обычным `diff`, а не вычиткой.
|
||||
|
||||
### R7. Длительности — собственный тип с `UnmarshalText`
|
||||
### GCFG-7. Длительности — собственный тип с `UnmarshalText`
|
||||
|
||||
**ДОЛЖЕН.** Поля-длительности объявляются своим типом, отдающим
|
||||
`time.Duration`:
|
||||
@@ -105,11 +106,11 @@ func (d Duration) Std() time.Duration { … }
|
||||
в себе и разбирается тем же `time.ParseDuration`, что и остальной код.
|
||||
|
||||
У обёртки есть цена: `UnmarshalText` вызывается на разборе TOML, то есть
|
||||
раньше, чем начинает работать сбор проблем (R12). Ошибка в длительности
|
||||
раньше, чем начинает работать сбор проблем (GCFG-12). Ошибка в длительности
|
||||
приходит отдельно и первой, а остальные проблемы конфига в этом запуске не
|
||||
показываются.
|
||||
|
||||
### R8. Приложение не читает окружение
|
||||
### GCFG-8. Приложение не читает окружение
|
||||
|
||||
**НЕ ДОЛЖЕН.** Значения конфигурации не берутся из переменных окружения.
|
||||
|
||||
@@ -120,9 +121,9 @@ func (d Duration) Std() time.Duration { … }
|
||||
чтением всего кода — а узнают о нём обычно на сервере, где переменная не
|
||||
выставлена.
|
||||
|
||||
### R9. Проверка запрета покрывает всю семью `os`
|
||||
### GCFG-9. Проверка запрета покрывает всю семью `os`
|
||||
|
||||
**ДОЛЖЕН.** Механическая проверка R8 (`forbidigo`) ловит не только
|
||||
**ДОЛЖЕН.** Механическая проверка GCFG-8 (`forbidigo`) ловит не только
|
||||
`os.Getenv`:
|
||||
|
||||
```
|
||||
@@ -137,20 +138,20 @@ func (d Duration) Std() time.Duration { … }
|
||||
Полного покрытия этот паттерн не даёт и дать не может: мимо него проходят
|
||||
`syscall.Getenv`, вызов через алиас пакета и чтение `/proc/self/environ`.
|
||||
Проверка закрывает обычные способы — те, которыми окружение читают не
|
||||
нарочно; сознательный обход она не ловит, и считать R8 полностью
|
||||
нарочно; сознательный обход она не ловит, и считать GCFG-8 полностью
|
||||
механизированным нельзя.
|
||||
|
||||
### R10. За границей приложения запрет не действует
|
||||
### GCFG-10. За границей приложения запрет не действует
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Чтение окружения там, где читающий — не конфигурируемое
|
||||
приложение:
|
||||
|
||||
| № | Кто читает | Вердикт |
|
||||
|---|---|---|
|
||||
| R10.1 | тесты, включая интеграционные | допустимо: креды и адреса внешних сервисов взять больше неоткуда |
|
||||
| R10.2 | Go-рантайм и ОС, а не наш код (`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`) | вне запрета: значение читает не приложение |
|
||||
| GCFG-10.1 | тесты, включая интеграционные | допустимо: креды и адреса внешних сервисов взять больше неоткуда |
|
||||
| GCFG-10.2 | Go-рантайм и ОС, а не наш код (`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`) | вне запрета: значение читает не приложение |
|
||||
|
||||
**Почему.** R8 — про конфигурацию приложения; расширенный до «никто не
|
||||
**Почему.** GCFG-8 — про конфигурацию приложения; расширенный до «никто не
|
||||
трогает окружение», он запрещает то, чем не управляет: `GOMEMLIMIT` читает
|
||||
рантайм, а тесту креды внешнего сервиса взять неоткуда — в репозиторий их
|
||||
не положишь, а отдельный конфиг тестов пришлось бы заводить как ещё один
|
||||
@@ -158,19 +159,19 @@ func (d Duration) Std() time.Duration { … }
|
||||
лечится `//nolint` наугад: там, где легальные случаи приходится глушить
|
||||
руками, вместе с ними проходят и нелегальные.
|
||||
|
||||
### R11. Прокси задаётся конфигом, а не `HTTP_PROXY`
|
||||
### GCFG-11. Прокси задаётся конфигом, а не `HTTP_PROXY`
|
||||
|
||||
**ДОЛЖЕН.** Исходящий прокси приходит полем конфига и явным `Transport`.
|
||||
|
||||
**Почему.** `HTTP_PROXY`/`HTTPS_PROXY` формально читает не наш код, а
|
||||
дефолтный `http.Transport` — но читает он их от имени приложения и меняет
|
||||
поведение приложения, а не рантайма. Оставленные окружению, они дают ровно
|
||||
тот второй канал, который запрещает R8, и притом самый неудобный: маршрут
|
||||
тот второй канал, который запрещает GCFG-8, и притом самый неудобный: маршрут
|
||||
исходящих запросов отличается от машины к машине без единого следа в
|
||||
конфиге и в образце, а расследование начинается с вопроса «почему на
|
||||
сервере ходит не так, как локально».
|
||||
|
||||
### R12. Проблемы конфига собираются `errors.Join`
|
||||
### GCFG-12. Проблемы конфига собираются `errors.Join`
|
||||
|
||||
**ДОЛЖЕН.** Проверки не прерываются на первой неудаче, результат — одна
|
||||
ошибка, собранная `errors.Join`.
|
||||
@@ -181,7 +182,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
своего типа ошибки, и `errors.Is`/`errors.As` продолжают работать по каждой
|
||||
вложенной проблеме.
|
||||
|
||||
### R13. Имя зоны проверяется `time.LoadLocation`
|
||||
### GCFG-13. Имя зоны проверяется `time.LoadLocation`
|
||||
|
||||
**ДОЛЖЕН.** IANA-зона из конфига загружается на старте, в общей валидации.
|
||||
|
||||
@@ -189,9 +190,9 @@ func (d Duration) Std() time.Duration { … }
|
||||
тогда, когда база зон его знает, и никакая проверка формата не отличит
|
||||
`Europe/Moscow` от `Europe/Moskow`. Без загрузки на старте опечатка
|
||||
доживает до первого форматирования времени — то есть до рантайма, мимо
|
||||
fail-fast (R15).
|
||||
fail-fast (GCFG-15).
|
||||
|
||||
### R14. `time/tzdata` импортируется в `main`
|
||||
### GCFG-14. `time/tzdata` импортируется в `main`
|
||||
|
||||
**ДОЛЖЕН.** Импорт `_ "time/tzdata"` стоит в `main`, а не в библиотечном
|
||||
пакете.
|
||||
@@ -199,11 +200,11 @@ fail-fast (R15).
|
||||
**Почему.** Импорт в библиотеке навязывает ~450 КБ zoneinfo каждому
|
||||
импортёру, включая тех, кому зоны не нужны: выбор «встраивать базу или
|
||||
полагаться на системную» принадлежит собираемой программе. Со встроенной
|
||||
базой ошибка `LoadLocation` (R13) означает ровно одно — битое имя зоны; без
|
||||
базой ошибка `LoadLocation` (GCFG-13) означает ровно одно — битое имя зоны; без
|
||||
неё тот же конфиг валиден на машине разработчика и падает в контейнере без
|
||||
zoneinfo, а сообщение указывает не на ту причину.
|
||||
|
||||
### R15. Невалидный конфиг — `ERROR` и выход из `main`
|
||||
### GCFG-15. Невалидный конфиг — `ERROR` и выход из `main`
|
||||
|
||||
**ДОЛЖЕН.** `main` пишет `slog` уровня `ERROR` и вызывает `os.Exit(1)` до
|
||||
старта серверов и воркеров.
|
||||
|
||||
Reference in New Issue
Block a user