1d6ab86a57
- internal/m2m/types.go: enum'ы и simple-типы из XSD НРД (M2MSchemas_260408) - internal/m2m/validators.go: pattern-валидаторы ReferenceID/ISIN/INN/UUID/SecurityCode/IdentityDocSerial/AccountID + перечисления - internal/m2m/messages.go: структуры 6 типов сообщений M2M, choice-типы через указатели, IsM2M=true автоматически в MarshalXML - internal/nsdxml/datetime.go: тип NSDDateTime (формат "YYYY-MM-DDThh:mm:ss(МСК+N)") - internal/nsdxml/codec.go: Marshal/Unmarshal XML в windows-1251 (собственный кодек CP1251, без внешних зависимостей) - internal/m2m/messages_test.go: round-trip тесты на 6 примерах + 2 эталонах из DOC/ Покрытие: m2m 73.9%, nsdxml 92.5%. make ci зелёный. Отклонение от спеки: вместо golang.org/x/text/encoding/charmap собственная таблица CP1251 на ~60 строк, потому что прокси zetit блокирует proxy.golang.org, goproxy.cn и redirect-хосты Go-модулей. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
7.0 KiB
Go
178 lines
7.0 KiB
Go
package m2m
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
)
|
|
|
|
// ErrInvalid — базовая ошибка валидации простого типа M2M.
|
|
var ErrInvalid = errors.New("m2m: invalid value")
|
|
|
|
// Скомпилированные паттерны из XSD НРД (M2MTypesNSD.xsd).
|
|
var (
|
|
reReferenceID = regexp.MustCompile(`^M2M[A-Z0-9]{13}$`)
|
|
reISIN = regexp.MustCompile(`^[A-Z]{2}[A-Z0-9]{9}[0-9]$`)
|
|
reOrganizationINN = regexp.MustCompile(`^[0-9]{10}$`)
|
|
reDeponentCode = regexp.MustCompile(`^[A-Z0-9]+$`)
|
|
reUUID = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
|
|
reSecurityCode = regexp.MustCompile(`^[0-9A-Z_/-]+$`)
|
|
reIdentityDocSerial = regexp.MustCompile(`^\S+$`)
|
|
)
|
|
|
|
// Validate проверяет, что значение соответствует ReferenceIDType
|
|
// (M2M + 13 символов [A-Z0-9], ровно 16 символов).
|
|
func (r ReferenceID) Validate() error {
|
|
if len(r) != 16 {
|
|
return fmt.Errorf("%w: ReferenceID длина %d, ожидается 16", ErrInvalid, len(r))
|
|
}
|
|
if !reReferenceID.MatchString(string(r)) {
|
|
return fmt.Errorf("%w: ReferenceID %q не соответствует ^M2M[A-Z0-9]{13}$", ErrInvalid, string(r))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует ISINtype
|
|
// (2 буквы + 9 буквоцифр + цифра, ровно 12 символов).
|
|
func (i ISIN) Validate() error {
|
|
if len(i) != 12 {
|
|
return fmt.Errorf("%w: ISIN длина %d, ожидается 12", ErrInvalid, len(i))
|
|
}
|
|
if !reISIN.MatchString(string(i)) {
|
|
return fmt.Errorf("%w: ISIN %q не соответствует формату", ErrInvalid, string(i))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует OrganizationINNType
|
|
// (ровно 10 цифр).
|
|
func (n OrganizationINN) Validate() error {
|
|
if len(n) != 10 {
|
|
return fmt.Errorf("%w: ИНН длина %d, ожидается 10", ErrInvalid, len(n))
|
|
}
|
|
if !reOrganizationINN.MatchString(string(n)) {
|
|
return fmt.Errorf("%w: ИНН %q содержит не цифры", ErrInvalid, string(n))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует DeponentCodeType
|
|
// (1..12 символов из [A-Z0-9]).
|
|
func (c DeponentCode) Validate() error {
|
|
if len(c) == 0 || len(c) > 12 {
|
|
return fmt.Errorf("%w: DeponentCode длина %d, ожидается 1..12", ErrInvalid, len(c))
|
|
}
|
|
if !reDeponentCode.MatchString(string(c)) {
|
|
return fmt.Errorf("%w: DeponentCode %q содержит недопустимые символы", ErrInvalid, string(c))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует UUIDType (XSD НРД):
|
|
// 8-4-4-4-12 шестнадцатеричных символов, всего 36 с дефисами. Биты
|
|
// версии/варианта по UUID RFC не контролируются — XSD НРД допускает
|
|
// произвольный hex (в эталонах встречается "11111111-1111-...").
|
|
func (u UUID) Validate() error {
|
|
if len(u) != 36 {
|
|
return fmt.Errorf("%w: UUID длина %d, ожидается 36", ErrInvalid, len(u))
|
|
}
|
|
if !reUUID.MatchString(string(u)) {
|
|
return fmt.Errorf("%w: UUID %q не соответствует формату [hex]-[hex]-...", ErrInvalid, string(u))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует SecurityCodeType
|
|
// (ровно 12 символов из [0-9A-Z_/-]).
|
|
func (c SecurityCode) Validate() error {
|
|
if len(c) != 12 {
|
|
return fmt.Errorf("%w: SecurityCode длина %d, ожидается 12", ErrInvalid, len(c))
|
|
}
|
|
if !reSecurityCode.MatchString(string(c)) {
|
|
return fmt.Errorf("%w: SecurityCode %q содержит недопустимые символы", ErrInvalid, string(c))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует IdentityDocSerialType
|
|
// (хотя бы 1 символ, без пробельных).
|
|
func (s IdentityDocSerial) Validate() error {
|
|
if len(s) == 0 {
|
|
return fmt.Errorf("%w: IdentityDocSerial пустая строка", ErrInvalid)
|
|
}
|
|
if !reIdentityDocSerial.MatchString(string(s)) {
|
|
return fmt.Errorf("%w: IdentityDocSerial %q содержит пробельные символы", ErrInvalid, string(s))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет, что значение соответствует AccountIDType
|
|
// (1..50 символов).
|
|
func (a AccountID) Validate() error {
|
|
if len(a) == 0 || len(a) > 50 {
|
|
return fmt.Errorf("%w: AccountID длина %d, ожидается 1..50", ErrInvalid, len(a))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate проверяет принадлежность к множеству {INFO, ERROR}.
|
|
func (s StatusCode) Validate() error {
|
|
switch s {
|
|
case StatusInfo, StatusError:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: StatusCode %q вне {INFO, ERROR}", ErrInvalid, string(s))
|
|
}
|
|
|
|
// Validate проверяет принадлежность к множеству {T12, T03}.
|
|
func (t IIAContractType) Validate() error {
|
|
switch t {
|
|
case IIAContractT12, IIAContractT03:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: IIAContractType %q вне {T12, T03}", ErrInvalid, string(t))
|
|
}
|
|
|
|
// Validate проверяет принадлежность к множеству {BOND, SHAR, MFUN}.
|
|
func (s SecurityClassification) Validate() error {
|
|
switch s {
|
|
case SecurityBond, SecurityShar, SecurityMfun:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: SecurityClassification %q вне {BOND, SHAR, MFUN}", ErrInvalid, string(s))
|
|
}
|
|
|
|
// Validate проверяет принадлежность к множеству {ORDN, PREF, UKWN}.
|
|
func (c SecurityCategory) Validate() error {
|
|
switch c {
|
|
case CategoryOrdn, CategoryPref, CategoryUkwn:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: SecurityCategory %q вне {ORDN, PREF, UKWN}", ErrInvalid, string(c))
|
|
}
|
|
|
|
// validIdentityDocumentCodes — справочник допустимых кодов документов
|
|
// из XSD НРД (IdentityDocumentCodeEnum).
|
|
var validIdentityDocumentCodes = map[IdentityDocumentCode]struct{}{
|
|
DocCode01: {}, DocCode02: {}, DocCode03: {}, DocCode04: {}, DocCode05: {},
|
|
DocCode06: {}, DocCode07: {}, DocCode09: {}, DocCode10: {}, DocCode11: {},
|
|
DocCode12: {}, DocCode13: {}, DocCode14: {}, DocCode21: {}, DocCode22: {},
|
|
DocCode23: {}, DocCode26: {}, DocCode27: {}, DocCode91: {},
|
|
}
|
|
|
|
// Validate проверяет принадлежность к справочнику кодов документов НРД.
|
|
func (c IdentityDocumentCode) Validate() error {
|
|
if _, ok := validIdentityDocumentCodes[c]; ok {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: IdentityDocumentCode %q вне справочника НРД", ErrInvalid, string(c))
|
|
}
|
|
|
|
// Validate проверяет принадлежность к множеству {SGDN}.
|
|
func (s IsolationStatus) Validate() error {
|
|
if s == IsolationSGDN {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: IsolationStatus %q вне {SGDN}", ErrInvalid, string(s))
|
|
}
|