Repository: tableauio/tableau (go)

Issue:
load: report concise error if JSON file syntax is illegal
## Problem
JSON syntax error:
<img width="520" alt="WXWorkCapture_17085917283626" src="https://github.com/tableauio/tableau/assets/4188052/d57819f6-a702-4489-a28f-c3cff8500027">

Load report error: `proto: syntax error (line 35:17): unexpected token "5"`

## Solution

error message should include:

1. json file path
2. indicate json syntax error 



Pull request description:
fix(load): report concise error if failed to unmarshal file content to message
- close #86

Patch:
diff --git a/internal/localizer/i18n/config/ecode/en.yaml b/internal/localizer/i18n/config/ecode/en.yaml
index b122d86..512f47c 100644
--- a/internal/localizer/i18n/config/ecode/en.yaml
+++ b/internal/localizer/i18n/config/ecode/en.yaml
@@ -2,6 +2,10 @@
 E0001:
   desc: sheet not found in book
   text: sheet "{{.SheetName}}" not found in book "{{.BookName}}"
+E0002:
+  desc: cannot unmarshal file content to given proto.Message
+  text: 'failed to unmarshal file "{{.Filename}}" to message "{{.MessageName}}": {{.Error}}'
+  help: check the file content format is correct
 # [1000, 1999]: protogen error
 E1000:
   desc: column name conflicts in name row
diff --git a/internal/localizer/i18n/config/ecode/zh.yaml b/internal/localizer/i18n/config/ecode/zh.yaml
index 71eb4ac..fada545 100644
--- a/internal/localizer/i18n/config/ecode/zh.yaml
+++ b/internal/localizer/i18n/config/ecode/zh.yaml
@@ -2,6 +2,10 @@
 E0001:
   desc: sheet not found in book
   text: 工作表 "{{.SheetName}}" 在工作簿 "{{.BookName}}" 中未找到
+E0002:
+  desc: cannot unmarshal file content to given proto.Message
+  text: '无法将"{{.Filename}}"的文件内容反序列化到Protobuf消息"{{.MessageName}}": {{.Error}}'
+  help: 检查文件内容格式是否正确
 # [1000, 1999]: protogen error
 E1000:
   desc: column name conflicts in name row
diff --git a/internal/localizer/i18n/config/message/en.yaml b/internal/localizer/i18n/config/message/en.yaml
index 9a3b780..818b036 100644
--- a/internal/localizer/i18n/config/message/en.yaml
+++ b/internal/localizer/i18n/config/message/en.yaml
@@ -1,3 +1,7 @@
+default: |
+  error[{{.ErrCode}}]: {{.ErrDesc}}
+  Reason: {{.Reason}}
+  {{ if .Help }}Help: {{.Help}}{{ end }}
 protogen: |
   {{ if .ErrCode }}error[{{.ErrCode}}]: {{.ErrDesc}}{{ end }}
   Workbook: {{.BookName}} {{ if and (.PrimaryBookName) (not (eq .BookName .PrimaryBookName)) }} (Primary: {{.PrimaryBookName}}){{ end }}
diff --git a/internal/localizer/i18n/config/message/zh.yaml b/internal/localizer/i18n/config/message/zh.yaml
index b9884fe..bcd3798 100644
--- a/internal/localizer/i18n/config/message/zh.yaml
+++ b/internal/localizer/i18n/config/message/zh.yaml
@@ -1,3 +1,7 @@
+default: |
+  error[{{.ErrCode}}]: {{.ErrDesc}}
+  错误原因: {{.Reason}}
+  {{ if .Help }}修复建议: {{.Help}}{{ end }}
 protogen: |
   {{ if .ErrCode }}error[{{.ErrCode}}]: {{.ErrDesc}}{{ end }}
   工作簿: {{.BookName}} {{ if and (.PrimaryBookName) (not (eq .BookName .PrimaryBookName)) }}(主工作簿: {{.PrimaryBookName}}){{ end }}
diff --git a/load/load.go b/load/load.go
index 9fc0e27..5664121 100644
--- a/load/load.go
+++ b/load/load.go
@@ -12,6 +12,7 @@ import (
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/importer"
 	"github.com/tableauio/tableau/log"
+	"github.com/tableauio/tableau/xerrors"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
@@ -23,9 +24,8 @@ func Load(msg proto.Message, dir string, fmt format.Format, options ...Option) e
 	if format.IsInputFormat(fmt) {
 		return loadOrigin(msg, dir, options...)
 	}
-
-	var path string
 	name := string(msg.ProtoReflect().Descriptor().Name())
+	path := filepath.Join(dir, name+format.Format2Ext(fmt))
 	opts := ParseOptions(options...)
 	if opts.Paths != nil {
 		if p, ok := opts.Paths[name]; ok {
@@ -34,35 +34,29 @@ func Load(msg proto.Message, dir string, fmt format.Format, options ...Option) e
 			fmt = format.GetFormat(path)
 		}
 	}
-	if path == "" {
-		switch fmt {
-		case format.JSON:
-			path = filepath.Join(dir, name+format.JSONExt)
-		case format.Text:
-			path = filepath.Join(dir, name+format.TextExt)
-		case format.Bin:
-			path = filepath.Join(dir, name+format.BinExt)
-		default:
-			return errors.Errorf("unknown format: %v", fmt)
-		}
-	}
 	content, err := os.ReadFile(path)
 	if err != nil {
 		return errors.Wrapf(err, "failed to read file: %v", path)
 	}
+
+	var unmarshalErr error
 	switch fmt {
 	case format.JSON:
-		unmarshOpts := protojson.UnmarshalOptions{
+		unmarshalOpts := protojson.UnmarshalOptions{
 			DiscardUnknown: opts.IgnoreUnknownFields,
 		}
-		return unmarshOpts.Unmarshal(content, msg)
+		unmarshalErr = unmarshalOpts.Unmarshal(content, msg)
 	case format.Text:
-		return prototext.Unmarshal(content, msg)
+		unmarshalErr = prototext.Unmarshal(content, msg)
 	case format.Bin:
-		return proto.Unmarshal(content, msg)
+		unmarshalErr = proto.Unmarshal(content, msg)
 	default:
 		return errors.Errorf("unknown format: %v", fmt)
 	}
+	if unmarshalErr != nil {
+		return xerrors.E0002(path, name, unmarshalErr.Error())
+	}
+	return nil
 }
 
 // loadOrigin loads the origin file(excel/csv/xml) from the given directory.
diff --git a/load/options.go b/load/options.go
index 33168b5..9cdd983 100644
--- a/load/options.go
+++ b/load/options.go
@@ -6,20 +6,25 @@ type Options struct {
 	//
 	// If the name is "" or "UTC", LoadLocation returns UTC.
 	// If the name is "Local", LoadLocation returns Local.
+	//
 	// Default: "Local".
 	LocationName string
 	// IgnoreUnknownFields signifies whether to ignore unknown JSON fields
 	// during parsing.
+	//
 	// Default: false.
 	IgnoreUnknownFields bool
 	// SubdirRewrites rewrites subdir paths (relative to workbook name option
 	// in .proto file).
+	//
 	// Default: nil.
 	SubdirRewrites map[string]string
 	// Paths maps each messager name to a corresponding config file path.
 	// If a messager name is existed, then the messager will be parsed from
 	// the config file directly.
+	//
 	// NOTE: only JSON, bin, and text formats are supported.
+	//
 	// Default: nil.
 	Paths map[string]string
 }
diff --git a/xerrors/desc.go b/xerrors/desc.go
index f15e151..ca7aeae 100644
--- a/xerrors/desc.go
+++ b/xerrors/desc.go
@@ -107,7 +107,7 @@ func (d *Desc) setField(key, val string) {
 }
 
 func (d *Desc) ErrCode() string {
-	val := d.fields["ErrCode"]
+	val := d.GetValue(keyErrCode)
 	if val != nil {
 		ecode, ok := val.(string)
 		if ok {
@@ -119,15 +119,20 @@ func (d *Desc) ErrCode() string {
 
 // String render description in specified language.
 func (d *Desc) String() string {
-	if d.fields[keyReason] == nil || d.fields[KeyModule] == nil {
+	if d.fields[keyReason] == nil {
 		return d.err.Error()
 	}
+	if d.fields[KeyModule] == nil && d.fields[keyErrCode] != nil {
+		d.fields[KeyModule] = ModuleDefault
+	}
 	debugging := fmt.Sprintf("Debugging: \n%s\n", d.DebugString())
-	module := d.fields[KeyModule].(string)
+	var module string
+	val := d.GetValue(KeyModule)
+	if val != nil {
+		module = val.(string)
+	}
 	switch module {
-	case ModuleProto:
-		return debugging + renderSummary(module, d.fields)
-	case ModuleConf:
+	case ModuleDefault, ModuleProto, ModuleConf:
 		return debugging + renderSummary(module, d.fields)
 	default:
 		return d.err.Error()
diff --git a/xerrors/ecode.go b/xerrors/ecode.go
index 328f1b2..7e5c600 100644
--- a/xerrors/ecode.go
+++ b/xerrors/ecode.go
@@ -15,6 +15,15 @@ func E0001(sheetName, bookName string) error {
 	})
 }
 
+// E0002: cannot unmarshal file content to given proto.Message.
+func E0002(filename, messageName, errstr string) error {
+	return renderEcode("E0002", map[string]any{
+		"Filename":    filename,
+		"MessageName": messageName,
+		"Error":       errstr,
+	})
+}
+
 // E1000: column name conflicts in name row.
 func E1000(name, positon1, positon2 string) error {
 	return renderEcode("E1000", map[string]any{


Test patch:
diff --git a/load/load_test.go b/load/load_test.go
index 3c1d4c2..d8f7ea8 100644
--- a/load/load_test.go
+++ b/load/load_test.go
@@ -3,8 +3,10 @@ package load
 import (
 	"testing"
 
+	"github.com/stretchr/testify/require"
 	"github.com/tableauio/tableau/format"
 	"github.com/tableauio/tableau/proto/tableaupb/unittestpb"
+	"github.com/tableauio/tableau/xerrors"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -19,8 +21,8 @@ func TestLoad(t *testing.T) {
 		name    string
 		args    args
 		wantErr bool
+		errcode string
 	}{
-		// TODO: Add test cases.
 		{
 			name: "load-origin",
 			args: args{
@@ -34,9 +36,9 @@ func TestLoad(t *testing.T) {
 		{
 			name: "load-origin-with-sudir-rewrites",
 			args: args{
-				msg:     &unittestpb.ItemConf{},
-				dir:     "../",
-				fmt:     format.CSV,
+				msg: &unittestpb.ItemConf{},
+				dir: "../",
+				fmt: format.CSV,
 				options: []Option{
 					SubdirRewrites(map[string]string{
 						"unittest/": "testdata/unittest/",
@@ -58,6 +60,20 @@ func TestLoad(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "specified-json-format-invalid-syntax",
+			args: args{
+				msg: &unittestpb.ItemConf{},
+				dir: "../testdata/unittest/invalidconf/",
+				fmt: format.JSON,
+				options: []Option{
+					LocationName("Local"),
+					IgnoreUnknownFields(),
+				},
+			},
+			wantErr: true,
+			errcode: "E0002",
+		},
 		{
 			name: "specified-text-format",
 			args: args{
@@ -138,9 +154,16 @@ func TestLoad(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if err := Load(tt.args.msg, tt.args.dir, tt.args.fmt, tt.args.options...); (err != nil) != tt.wantErr {
+			err := Load(tt.args.msg, tt.args.dir, tt.args.fmt, tt.args.options...)
+			if (err != nil) != tt.wantErr {
 				t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
 			}
+			if err != nil {
+				if tt.errcode != "" {
+					desc := xerrors.NewDesc(err)
+					require.Equal(t, tt.errcode, desc.ErrCode())
+				}
+			}
 			// opts := prototext.MarshalOptions{
 			// 	Multiline: true,
 			// 	Indent:    "    ",
diff --git a/testdata/unittest/invalidconf/ItemConf.json b/testdata/unittest/invalidconf/ItemConf.json
new file mode 100644
index 0000000..024013d
--- /dev/null
+++ b/testdata/unittest/invalidconf/ItemConf.json
@@ -0,0 +1,16 @@
+{
+    "itemMap": {
+        "1": {
+            "id": 1,
+            "num": 100
+        },
+        "2": {
+            "id": 2,
+            "num": 200
+        }
+        "3": {
+            "id": 3,
+            "num": 300
+        }
+    }
+}
\ No newline at end of file