Files
qfixpt/quickfix/validation.go
2026-03-12 12:14:13 -03:00

468 lines
12 KiB
Go

// Copyright (c) quickfixengine.org All rights reserved.
//
// This file may be distributed under the terms of the quickfixengine.org
// license as defined by quickfixengine.org and appearing in the file
// LICENSE included in the packaging of this file.
//
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE.
//
// See http://www.quickfixengine.org/LICENSE for licensing information.
//
// Contact ask@quickfixengine.org if any conditions of this licensing
// are not clear to you.
package quickfix
import (
"quantex.com/qfixpt/quickfix/datadictionary"
)
const (
UserDefinedTagMin int = 5000
)
// Validator validates a FIX message.
type Validator interface {
Validate(*Message) MessageRejectError
}
// ValidatorSettings describe validation behavior.
type ValidatorSettings struct {
CheckFieldsOutOfOrder bool
RejectInvalidMessage bool
AllowUnknownMessageFields bool
CheckUserDefinedFields bool
CheckFieldsHaveValues bool
}
// Default configuration for message validation.
// See http://www.quickfixengine.org/quickfix/doc/html/configuration.html.
var defaultValidatorSettings = ValidatorSettings{
CheckFieldsOutOfOrder: true,
CheckFieldsHaveValues: true,
RejectInvalidMessage: true,
AllowUnknownMessageFields: false,
CheckUserDefinedFields: true,
}
type fixValidator struct {
dataDictionary *datadictionary.DataDictionary
settings ValidatorSettings
}
type fixtValidator struct {
transportDataDictionary *datadictionary.DataDictionary
appDataDictionary *datadictionary.DataDictionary
settings ValidatorSettings
}
// NewValidator creates a FIX message validator from the given data dictionaries.
func NewValidator(settings ValidatorSettings, appDataDictionary, transportDataDictionary *datadictionary.DataDictionary) Validator {
if transportDataDictionary != nil {
return &fixtValidator{
transportDataDictionary: transportDataDictionary,
appDataDictionary: appDataDictionary,
settings: settings,
}
}
return &fixValidator{
dataDictionary: appDataDictionary,
settings: settings,
}
}
// Validate tests the message against the provided data dictionary.
func (v *fixValidator) Validate(msg *Message) MessageRejectError {
if !msg.Header.Has(tagMsgType) {
return RequiredTagMissing(tagMsgType)
}
msgType, err := msg.Header.GetString(tagMsgType)
if err != nil {
return err
}
return validateFIX(v.dataDictionary, v.settings, msgType, msg)
}
// Validate tests the message against the provided transport and app data dictionaries.
// If the message is an admin message, it will be validated against the transport data dictionary.
func (v *fixtValidator) Validate(msg *Message) MessageRejectError {
if !msg.Header.Has(tagMsgType) {
return RequiredTagMissing(tagMsgType)
}
msgType, err := msg.Header.GetString(tagMsgType)
if err != nil {
return err
}
if isAdminMessageType([]byte(msgType)) {
return validateFIX(v.transportDataDictionary, v.settings, msgType, msg)
}
return validateFIXT(v.transportDataDictionary, v.appDataDictionary, v.settings, msgType, msg)
}
func validateFIX(d *datadictionary.DataDictionary, settings ValidatorSettings, msgType string, msg *Message) MessageRejectError {
if d != nil {
if err := validateMsgType(d, msgType, msg); err != nil {
return err
}
if err := validateRequired(d, d, msgType, msg); err != nil {
return err
}
}
if err := validateFieldContent(msg, settings.CheckFieldsHaveValues, settings.CheckFieldsOutOfOrder); err != nil {
return err
}
if settings.RejectInvalidMessage && d != nil {
if err := validateFields(d, d, settings, msgType, msg); err != nil {
return err
}
if err := validateWalk(d, d, settings, msgType, msg); err != nil {
return err
}
}
return nil
}
func validateFIXT(transportDD, appDD *datadictionary.DataDictionary, settings ValidatorSettings, msgType string, msg *Message) MessageRejectError {
if appDD != nil && transportDD != nil {
if err := validateMsgType(appDD, msgType, msg); err != nil {
return err
}
if err := validateRequired(transportDD, appDD, msgType, msg); err != nil {
return err
}
}
if err := validateFieldContent(msg, settings.CheckFieldsHaveValues, settings.CheckFieldsOutOfOrder); err != nil {
return err
}
if settings.RejectInvalidMessage && appDD != nil && transportDD != nil {
if err := validateFields(transportDD, appDD, settings, msgType, msg); err != nil {
return err
}
if err := validateWalk(transportDD, appDD, settings, msgType, msg); err != nil {
return err
}
}
return nil
}
func validateMsgType(d *datadictionary.DataDictionary, msgType string, _ *Message) MessageRejectError {
if _, validMsgType := d.Messages[msgType]; !validMsgType {
return InvalidMessageType()
}
return nil
}
func validateWalk(transportDD *datadictionary.DataDictionary, appDD *datadictionary.DataDictionary, settings ValidatorSettings, msgType string, msg *Message) MessageRejectError {
remainingFields := msg.fields
iteratedTags := make(datadictionary.TagSet)
var messageDef *datadictionary.MessageDef
var fieldDef *datadictionary.FieldDef
var err MessageRejectError
var ok bool
for len(remainingFields) > 0 {
field := remainingFields[0]
tag := field.tag
switch {
case tag.IsHeader():
messageDef = transportDD.Header
case tag.IsTrailer():
messageDef = transportDD.Trailer
default: // is body
messageDef = appDD.Messages[msgType]
}
if _, duplicate := iteratedTags[int(tag)]; duplicate {
return tagAppearsMoreThanOnce(tag)
}
iteratedTags.Add(int(tag))
if fieldDef, ok = messageDef.Fields[int(tag)]; !ok {
if !checkFieldNotDefined(settings, tag) {
return TagNotDefinedForThisMessageType(tag)
}
remainingFields = remainingFields[1:]
continue
}
if remainingFields, err = validateVisitField(fieldDef, remainingFields); err != nil {
return err
}
}
if len(remainingFields) != 0 {
return TagNotDefinedForThisMessageType(remainingFields[0].tag)
}
return nil
}
func validateVisitField(fieldDef *datadictionary.FieldDef, fields []TagValue) ([]TagValue, MessageRejectError) {
if fieldDef.IsGroup() {
var err MessageRejectError
if fields, err = validateVisitGroupField(fieldDef, fields); err != nil {
return nil, err
}
return fields, nil
}
return fields[1:], nil
}
func validateVisitGroupField(fieldDef *datadictionary.FieldDef, fieldStack []TagValue) ([]TagValue, MessageRejectError) {
numInGroupTag := fieldStack[0].tag
var numInGroup FIXInt
if err := numInGroup.Read(fieldStack[0].value); err != nil {
return nil, IncorrectDataFormatForValue(numInGroupTag)
}
fieldStack = fieldStack[1:]
var childDefs []*datadictionary.FieldDef
groupCount := 0
for len(fieldStack) > 0 {
// Start of repeating group.
if int(fieldStack[0].tag) == fieldDef.Fields[0].Tag() {
childDefs = fieldDef.Fields
groupCount++
}
// Group complete.
if len(childDefs) == 0 {
break
}
if int(fieldStack[0].tag) == childDefs[0].Tag() {
var err MessageRejectError
if fieldStack, err = validateVisitField(childDefs[0], fieldStack); err != nil {
return fieldStack, err
}
} else {
if childDefs[0].Required() {
return fieldStack, RequiredTagMissing(Tag(childDefs[0].Tag()))
}
}
childDefs = childDefs[1:]
}
if groupCount != int(numInGroup) {
return fieldStack, incorrectNumInGroupCountForRepeatingGroup(numInGroupTag)
}
return fieldStack, nil
}
func validateFieldContent(msg *Message, checkFieldsHaveValues, checkFieldsOutOfOrder bool) MessageRejectError {
if !checkFieldsHaveValues && !checkFieldsOutOfOrder {
return nil
}
inHeader := true
inTrailer := false
for _, field := range msg.fields {
t := field.tag
if checkFieldsHaveValues && len(field.value) == 0 {
return TagSpecifiedWithoutAValue(t)
}
switch {
case inHeader && t.IsHeader():
case inHeader && !t.IsHeader():
inHeader = false
case !inHeader && t.IsHeader() && checkFieldsOutOfOrder:
return tagSpecifiedOutOfRequiredOrder(t)
case t.IsTrailer():
inTrailer = true
case inTrailer && !t.IsTrailer() && checkFieldsOutOfOrder:
return tagSpecifiedOutOfRequiredOrder(t)
}
}
return nil
}
func validateRequired(transportDD *datadictionary.DataDictionary, appDD *datadictionary.DataDictionary, msgType string, message *Message) MessageRejectError {
if err := validateRequiredFieldMap(message, transportDD.Header.RequiredTags, message.Header.FieldMap); err != nil {
return err
}
if err := validateRequiredFieldMap(message, appDD.Messages[msgType].RequiredTags, message.Body.FieldMap); err != nil {
return err
}
if err := validateRequiredFieldMap(message, transportDD.Trailer.RequiredTags, message.Trailer.FieldMap); err != nil {
return err
}
return nil
}
func validateRequiredFieldMap(_ *Message, requiredTags map[int]struct{}, fieldMap FieldMap) MessageRejectError {
for required := range requiredTags {
requiredTag := Tag(required)
if !fieldMap.Has(requiredTag) {
return RequiredTagMissing(requiredTag)
}
}
return nil
}
func validateFields(transportDD *datadictionary.DataDictionary,
appDD *datadictionary.DataDictionary,
settings ValidatorSettings,
msgType string,
message *Message,
) MessageRejectError {
for _, field := range message.fields {
switch {
case field.tag.IsHeader():
if err := validateField(transportDD, settings, transportDD.Header.Tags, field); err != nil {
return err
}
case field.tag.IsTrailer():
if err := validateField(transportDD, settings, transportDD.Trailer.Tags, field); err != nil {
return err
}
default:
if err := validateField(appDD, settings, appDD.Messages[msgType].Tags, field); err != nil {
return err
}
}
}
return nil
}
func getFieldType(d *datadictionary.DataDictionary, field int) (*datadictionary.FieldType, bool) {
fieldType, isMessageField := d.FieldTypeByTag[field]
return fieldType, isMessageField
}
func checkFieldNotDefined(settings ValidatorSettings, field Tag) bool {
fail := false
if int(field) < UserDefinedTagMin {
fail = !settings.AllowUnknownMessageFields
} else {
fail = settings.CheckUserDefinedFields
}
return !fail
}
func validateField(d *datadictionary.DataDictionary,
settings ValidatorSettings,
_ datadictionary.TagSet,
field TagValue,
) MessageRejectError {
if len(field.value) == 0 {
return TagSpecifiedWithoutAValue(field.tag)
}
fieldType, isMessageField := getFieldType(d, int(field.tag))
if !isMessageField && !checkFieldNotDefined(settings, field.tag) {
return InvalidTagNumber(field.tag)
}
if !isMessageField {
return nil
}
allowedValues := d.FieldTypeByTag[int(field.tag)].Enums
if len(allowedValues) != 0 {
if _, validValue := allowedValues[string(field.value)]; !validValue {
return ValueIsIncorrect(field.tag)
}
}
var prototype FieldValue
switch fieldType.Type {
case "MULTIPLESTRINGVALUE", "MULTIPLEVALUESTRING":
fallthrough
case "MULTIPLECHARVALUE":
fallthrough
case "CHAR":
fallthrough
case "CURRENCY":
fallthrough
case "DATA":
fallthrough
case "MONTHYEAR":
fallthrough
case "LOCALMKTDATE", "DATE":
fallthrough
case "EXCHANGE":
fallthrough
case "LANGUAGE":
fallthrough
case "XMLDATA":
fallthrough
case "COUNTRY":
fallthrough
case "UTCTIMEONLY":
fallthrough
case "UTCDATEONLY", "UTCDATE":
fallthrough
case "TZTIMEONLY":
fallthrough
case "TZTIMESTAMP":
fallthrough
case "STRING":
prototype = new(FIXString)
case "BOOLEAN":
prototype = new(FIXBoolean)
case "LENGTH":
fallthrough
case "DAYOFMONTH":
fallthrough
case "NUMINGROUP":
fallthrough
case "SEQNUM":
fallthrough
case "INT":
prototype = new(FIXInt)
case "UTCTIMESTAMP", "TIME":
prototype = new(FIXUTCTimestamp)
case "QTY", "QUANTITY":
fallthrough
case "AMT":
fallthrough
case "PRICE":
fallthrough
case "PRICEOFFSET":
fallthrough
case "PERCENTAGE":
fallthrough
case "FLOAT":
prototype = new(FIXFloat)
}
if err := prototype.Read(field.value); err != nil {
return IncorrectDataFormatForValue(field.tag)
}
return nil
}