Repository: quickfixgo/quickfix

Issue:
ValidateFieldsHaveValues
From QuickFIX,

>If set to N, fields without values (empty) will not be rejected. Useful for connecting to systems which improperly send empty tags.

Hints from issue comments:

Solution patch:
diff --git a/config/configuration.go b/config/configuration.go
index 1425cde7..4b9d322d 100644
--- a/config/configuration.go
+++ b/config/configuration.go
@@ -454,6 +454,19 @@ const (
 	//  - N
 	ValidateFieldsOutOfOrder string = "ValidateFieldsOutOfOrder"
 
+	// ValidateFieldsHaveValues if set to N, fields without values (i.e. |11=| for an empty ClOrdID)
+	// will not be rejected, even if RejectInvalidMessage is set to N.
+	// Useful for connecting to systems that improperly send empty tags.
+	//
+	// Required: No
+	//
+	// Default: Y
+	//
+	// Valid Values:
+	//  - Y
+	//  - N
+	ValidateFieldsHaveValues string = "ValidateFieldsHaveValues"
+
 	// CheckLatency if set to Y, messages must be received from the counter-party within a defined number of seconds.
 	// It is useful to turn this off if a system uses localtime for it's timestamps instead of GMT.
 	//
diff --git a/session_factory.go b/session_factory.go
index a2291cad..613bb206 100644
--- a/session_factory.go
+++ b/session_factory.go
@@ -99,6 +99,12 @@ func (f sessionFactory) newSession(
 		}
 	}
 
+	if settings.HasSetting(config.ValidateFieldsHaveValues) {
+		if validatorSettings.CheckFieldsHaveValues, err = settings.BoolSetting(config.ValidateFieldsHaveValues); err != nil {
+			return
+		}
+	}
+
 	if settings.HasSetting(config.RejectInvalidMessage) {
 		if validatorSettings.RejectInvalidMessage, err = settings.BoolSetting(config.RejectInvalidMessage); err != nil {
 			return
@@ -117,6 +123,8 @@ func (f sessionFactory) newSession(
 		}
 	}
 
+	// Always use a default message validator without data dictionaries
+	s.Validator = NewValidator(validatorSettings, nil, nil)
 	if sessionID.IsFIXT() {
 		if s.DefaultApplVerID, err = settings.Setting(config.DefaultApplVerID); err != nil {
 			return
diff --git a/validation.go b/validation.go
index e9b71813..82b535ac 100644
--- a/validation.go
+++ b/validation.go
@@ -34,12 +34,14 @@ type ValidatorSettings struct {
 	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,
@@ -102,21 +104,21 @@ func (v *fixtValidator) Validate(msg *Message) MessageRejectError {
 }
 
 func validateFIX(d *datadictionary.DataDictionary, settings ValidatorSettings, msgType string, msg *Message) MessageRejectError {
-	if err := validateMsgType(d, msgType, msg); err != nil {
-		return err
-	}
-
-	if err := validateRequired(d, d, msgType, msg); err != nil {
-		return err
-	}
+	if d != nil {
+		if err := validateMsgType(d, msgType, msg); err != nil {
+			return err
+		}
 
-	if settings.CheckFieldsOutOfOrder {
-		if err := validateOrder(msg); err != nil {
+		if err := validateRequired(d, d, msgType, msg); err != nil {
 			return err
 		}
 	}
 
-	if settings.RejectInvalidMessage {
+	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
 		}
@@ -130,21 +132,21 @@ func validateFIX(d *datadictionary.DataDictionary, settings ValidatorSettings, m
 }
 
 func validateFIXT(transportDD, appDD *datadictionary.DataDictionary, settings ValidatorSettings, msgType string, msg *Message) MessageRejectError {
-	if err := validateMsgType(appDD, msgType, msg); err != nil {
-		return err
-	}
-
-	if err := validateRequired(transportDD, appDD, msgType, msg); err != nil {
-		return err
-	}
+	if appDD != nil && transportDD != nil {
+		if err := validateMsgType(appDD, msgType, msg); err != nil {
+			return err
+		}
 
-	if settings.CheckFieldsOutOfOrder {
-		if err := validateOrder(msg); err != nil {
+		if err := validateRequired(transportDD, appDD, msgType, msg); err != nil {
 			return err
 		}
 	}
 
-	if settings.RejectInvalidMessage {
+	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
 		}
@@ -270,20 +272,26 @@ func validateVisitGroupField(fieldDef *datadictionary.FieldDef, fieldStack []Tag
 	return fieldStack, nil
 }
 
-func validateOrder(msg *Message) MessageRejectError {
+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():
+		case !inHeader && t.IsHeader() && checkFieldsOutOfOrder:
 			return tagSpecifiedOutOfRequiredOrder(t)
 		case t.IsTrailer():
 			inTrailer = true
-		case inTrailer && !t.IsTrailer():
+		case inTrailer && !t.IsTrailer() && checkFieldsOutOfOrder:
 			return tagSpecifiedOutOfRequiredOrder(t)
 		}
 	}



Test patch:
diff --git a/validation_test.go b/validation_test.go
index 1647dc94..7973464d 100644
--- a/validation_test.go
+++ b/validation_test.go
@@ -44,6 +44,9 @@ func TestValidate(t *testing.T) {
 		tcInvalidTagNumberTrailerFixT(),
 		tcTagSpecifiedWithoutAValue(),
 		tcTagSpecifiedWithoutAValueFixT(),
+		tcTagSpecifiedWithoutAValueValidateHasValues(),
+		tcTagSpecifiedWithoutAValueFixTValidateHasValues(),
+		tcTagSpecifiedWithoutAValueValidateHasValuesNoDataDictionary(),
 		tcInvalidMsgType(),
 		tcInvalidMsgTypeFixT(),
 		tcValueIsIncorrect(),
@@ -512,6 +515,47 @@ func tcTagSpecifiedWithoutAValue() validateTest {
 	}
 }
 
+func tcTagSpecifiedWithoutAValueValidateHasValues() validateTest {
+	dict, _ := datadictionary.Parse("spec/FIX40.xml")
+	settings := defaultValidatorSettings
+	settings.RejectInvalidMessage = false
+	settings.CheckFieldsHaveValues = true
+	validator := NewValidator(settings, dict, nil)
+	builder := createFIX40NewOrderSingle()
+
+	bogusTag := Tag(109)
+	builder.Body.SetField(bogusTag, FIXString(""))
+	msgBytes := builder.build()
+
+	return validateTest{
+		TestName:             "Tag SpecifiedWithoutAValue",
+		Validator:            validator,
+		MessageBytes:         msgBytes,
+		ExpectedRejectReason: rejectReasonTagSpecifiedWithoutAValue,
+		ExpectedRefTagID:     &bogusTag,
+	}
+}
+
+func tcTagSpecifiedWithoutAValueValidateHasValuesNoDataDictionary() validateTest {
+	settings := defaultValidatorSettings
+	settings.RejectInvalidMessage = false
+	settings.CheckFieldsHaveValues = true
+	validator := NewValidator(settings, nil, nil)
+	builder := createFIX40NewOrderSingle()
+
+	bogusTag := Tag(109)
+	builder.Body.SetField(bogusTag, FIXString(""))
+	msgBytes := builder.build()
+
+	return validateTest{
+		TestName:             "Tag SpecifiedWithoutAValue",
+		Validator:            validator,
+		MessageBytes:         msgBytes,
+		ExpectedRejectReason: rejectReasonTagSpecifiedWithoutAValue,
+		ExpectedRefTagID:     &bogusTag,
+	}
+}
+
 func tcTagSpecifiedWithoutAValueFixT() validateTest {
 	tDict, _ := datadictionary.Parse("spec/FIXT11.xml")
 	appDict, _ := datadictionary.Parse("spec/FIX50SP2.xml")
@@ -531,6 +575,25 @@ func tcTagSpecifiedWithoutAValueFixT() validateTest {
 	}
 }
 
+func tcTagSpecifiedWithoutAValueFixTValidateHasValues() validateTest {
+	tDict, _ := datadictionary.Parse("spec/FIXT11.xml")
+	appDict, _ := datadictionary.Parse("spec/FIX50SP2.xml")
+	validator := NewValidator(defaultValidatorSettings, appDict, tDict)
+	builder := createFIX50SP2NewOrderSingle()
+
+	bogusTag := Tag(109)
+	builder.Body.SetField(bogusTag, FIXString(""))
+	msgBytes := builder.build()
+
+	return validateTest{
+		TestName:             "Tag SpecifiedWithoutAValue FIXT",
+		Validator:            validator,
+		MessageBytes:         msgBytes,
+		ExpectedRejectReason: rejectReasonTagSpecifiedWithoutAValue,
+		ExpectedRefTagID:     &bogusTag,
+	}
+}
+
 func tcInvalidMsgType() validateTest {
 	dict, _ := datadictionary.Parse("spec/FIX40.xml")
 	validator := NewValidator(defaultValidatorSettings, dict, nil)