diff --git a/internal/confgen/confgen.go b/internal/confgen/confgen.go index ea7190fb..e4957b85 100644 --- a/internal/confgen/confgen.go +++ b/internal/confgen/confgen.go @@ -23,7 +23,6 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/descriptorpb" - "google.golang.org/protobuf/types/dynamicpb" ) // Error collection limits at each level. @@ -98,9 +97,7 @@ func (gen *Generator) GenAll() error { return err } // Create a validator with extension type resolver for custom predefined rules. - gen.validator, err = protovalidate.New( - protovalidate.WithExtensionTypeResolver(dynamicpb.NewTypes(prFiles)), - ) + gen.validator, err = NewValidator(prFiles) if err != nil { return err } @@ -126,9 +123,7 @@ func (gen *Generator) GenWorkbook(bookSpecifiers ...string) error { return err } // Create a validator with extension type resolver for custom predefined rules. - gen.validator, err = protovalidate.New( - protovalidate.WithExtensionTypeResolver(dynamicpb.NewTypes(prFiles)), - ) + gen.validator, err = NewValidator(prFiles) if err != nil { return err } diff --git a/internal/confgen/util.go b/internal/confgen/util.go index 8bb3308d..1afa7fde 100644 --- a/internal/confgen/util.go +++ b/internal/confgen/util.go @@ -26,6 +26,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" ) var fieldOptionsPool *sync.Pool @@ -314,9 +315,19 @@ func parseOutputFormats(msg proto.Message, opt *options.ConfOutputOption) []form return format.OutputFormats } -// validate validates a proto message using the provided validator. Each violation +// NewValidator creates a protovalidate validator whose extension type resolver +// is built from prFiles, so custom predefined rules can be recognized. It is +// shared by both the confgen (generate) and load (checker) paths to keep +// validation behavior consistent. +func NewValidator(prFiles *protoregistry.Files) (protovalidate.Validator, error) { + return protovalidate.New( + protovalidate.WithExtensionTypeResolver(dynamicpb.NewTypes(prFiles)), + ) +} + +// Validate validates a proto message using the provided validator. Each violation // is wrapped as an E2027 error and all violations are joined together. -func validate(msg proto.Message, validator protovalidate.Validator) error { +func Validate(msg proto.Message, validator protovalidate.Validator) error { err := validator.Validate(msg) if err == nil { return nil @@ -342,7 +353,7 @@ func validate(msg proto.Message, validator protovalidate.Validator) error { // storeMessage stores a message to one or multiple file formats. func storeMessage(msg proto.Message, name, locationName, outputDir string, opt *options.ConfOutputOption, validator protovalidate.Validator) error { - if err := validate(msg, validator); err != nil { + if err := Validate(msg, validator); err != nil { return err } outputDir = filepath.Join(outputDir, opt.Subdir) diff --git a/internal/confgen/util_test.go b/internal/confgen/util_test.go index 287a8f0e..fadcd6d1 100644 --- a/internal/confgen/util_test.go +++ b/internal/confgen/util_test.go @@ -412,7 +412,7 @@ func Test_validate(t *testing.T) { if err != nil { t.Fatalf("failed to create validator: %v", err) } - err = validate(tt.args.msg, validator) + err = Validate(tt.args.msg, validator) if (err != nil) != tt.wantErr { t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/load/load.go b/load/load.go index 8bd840f8..e9fa22c1 100644 --- a/load/load.go +++ b/load/load.go @@ -8,6 +8,7 @@ import ( "context" "path/filepath" + "buf.build/go/protovalidate" "github.com/tableauio/tableau/format" "github.com/tableauio/tableau/internal/confgen" "github.com/tableauio/tableau/internal/importer" @@ -24,6 +25,16 @@ import ( // LoadMessagerInDir loads message's content in the given dir, based on format and messager options. func LoadMessagerInDir(msg proto.Message, dir string, fmt format.Format, opts *MessagerOptions) error { + if err := loadMessagerInDir(msg, dir, fmt, opts); err != nil { + return err + } + // protovalidate the fully-assembled message. protovalidate operates on the + // final in-memory protobuf, so both input (excel/csv/xml/yaml) and output + // (json/binpb/txtpb) formats get the same coverage as confgen's storeMessage. + return confgen.Validate(msg, protovalidate.GlobalValidator) +} + +func loadMessagerInDir(msg proto.Message, dir string, fmt format.Format, opts *MessagerOptions) error { if format.IsInputFormat(fmt) { return loadOrigin(msg, dir, opts) } diff --git a/load/load_test.go b/load/load_test.go index 6a6e4561..4fc65119 100644 --- a/load/load_test.go +++ b/load/load_test.go @@ -299,6 +299,28 @@ func TestLoadJSON_E0002(t *testing.T) { t.Logf("error: %s", xerrors.NewDesc(err).String()) } +// TestLoadProtovalidate guards the protovalidate step in LoadMessagerInDir: +// protovalidate operates on the final in-memory message, so it must run for +// both input (excel/csv/xml/yaml) and output (json/binpb/txtpb) formats. +// ValidateConf constrains Name to max_len:10, and each fixture supplies a +// longer value, so a fully-assembled message must surface an E2027 violation. +// This catches regressions if the confgen.Validate call is ever dropped. +func TestLoadProtovalidate(t *testing.T) { + t.Run("input-format-csv", func(t *testing.T) { + err := LoadMessagerInDir(&unittestpb.ValidateConf{}, "../testdata/", format.CSV, &MessagerOptions{}) + require.Error(t, err) + require.ErrorIs(t, err, xerrors.ErrE2027) + t.Logf("error: %s", xerrors.NewDesc(err).String()) + }) + + t.Run("output-format-json", func(t *testing.T) { + err := LoadMessagerInDir(&unittestpb.ValidateConf{}, "../testdata/unittest/conf/", format.JSON, &MessagerOptions{}) + require.Error(t, err) + require.ErrorIs(t, err, xerrors.ErrE2027) + t.Logf("error: %s", xerrors.NewDesc(err).String()) + }) +} + func TestLoadWithPatch(t *testing.T) { type args struct { msg proto.Message diff --git a/testdata/unittest/Unittest#ValidateConf.csv b/testdata/unittest/Unittest#ValidateConf.csv new file mode 100644 index 00000000..f6868ec0 --- /dev/null +++ b/testdata/unittest/Unittest#ValidateConf.csv @@ -0,0 +1,4 @@ +ID,Name,Tag,Prop +uint32,string,"[]string","map" +Conf ID,Conf name,Tags,Props +0,this name is too long,"a,b","x:1" diff --git a/testdata/unittest/conf/ValidateConf.json b/testdata/unittest/conf/ValidateConf.json new file mode 100644 index 00000000..0dee462d --- /dev/null +++ b/testdata/unittest/conf/ValidateConf.json @@ -0,0 +1,4 @@ +{ + "id": 0, + "name": "this name is too long" +}