Repository: tableauio/tableau
Pull request #251 (2025-05-28 07:21:04)

feat(wellknownpb): add util for wellknown types

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tableauio/tableau/pull/251?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) Report
Attention: Patch coverage is `73.52941%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 71.15%. Comparing base [(`7f880d7`)](https://app.codecov.io/gh/tableauio/tableau/commit/7f880d7ea58360bd138c093f65cd98cdbdaf9329?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) to head [(`1a804b3`)](https://app.codecov.io/gh/tableauio/tableau/commit/1a804b3c5d4ecfdc6fee98a3217b48b956d0517f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

| [Files with missing lines](https://app.codecov.io/gh/tableauio/tableau/pull/251?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) | Patch % | Lines |
|---|---|---|
| [proto/tableaupb/wellknown\_util.go](https://app.codecov.io/gh/tableauio/tableau/pull/251?src=pr&el=tree&filepath=proto%2Ftableaupb%2Fwellknown_util.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio#diff-cHJvdG8vdGFibGVhdXBiL3dlbGxrbm93bl91dGlsLmdv) | 73.52% | [9 Missing :warning: ](https://app.codecov.io/gh/tableauio/tableau/pull/251?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #251   +/-   ##
=======================================
  Coverage   71.14%   71.15%           
=======================================
  Files          80       81    +1     
  Lines       10289    10323   +34     
=======================================
+ Hits         7320     7345   +25     
- Misses       2402     2411    +9     
  Partials      567      567           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tableauio/tableau/pull/251?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tableauio).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 2e1c078..47c29f8 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -2,6 +2,7 @@ package protogen
 
 import (
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"github.com/emirpasic/gods/sets/treeset"
@@ -26,16 +27,19 @@ type bookExporter struct {
 	wb               *internalpb.Workbook
 
 	gen *Generator
+
+	messagerPatternRegexp *regexp.Regexp
 }
 
 func newBookExporter(protoPackage string, protoFileOptions map[string]string, outputDir, filenameSuffix string, wb *internalpb.Workbook, gen *Generator) *bookExporter {
 	return &bookExporter{
-		ProtoPackage:     protoPackage,
-		ProtoFileOptions: protoFileOptions,
-		OutputDir:        outputDir,
-		FilenameSuffix:   filenameSuffix,
-		wb:               wb,
-		gen:              gen,
+		ProtoPackage:          protoPackage,
+		ProtoFileOptions:      protoFileOptions,
+		OutputDir:             outputDir,
+		FilenameSuffix:        filenameSuffix,
+		wb:                    wb,
+		gen:                   gen,
+		messagerPatternRegexp: regexp.MustCompile(gen.InputOpt.MessagerPattern),
 	}
 }
 
@@ -280,6 +284,9 @@ func (x *sheetExporter) exportUnion() error {
 
 func (x *sheetExporter) exportMessager() error {
 	// log.Debugf("workbook: %s", x.ws.String())
+	if x.be.messagerPatternRegexp != nil && !x.be.messagerPatternRegexp.MatchString(x.ws.Name) {
+		return xerrors.Errorf("messager %s does not match pattern %s", x.ws.Name, x.be.messagerPatternRegexp.String())
+	}
 	x.g.P("message ", x.ws.Name, " {")
 	x.g.P("  option (tableau.worksheet) = {", marshalToText(x.ws.Options), "};")
 	x.g.P("")
diff --git a/options/options.go b/options/options.go
index 9ef352b..fd93615 100644
--- a/options/options.go
+++ b/options/options.go
@@ -158,6 +158,13 @@ type ProtoInputOption struct {
 	//
 	// Default: "".
 	FirstPassMode FirstPassMode `yaml:"firstPassMode"`
+
+	// Specify the name pattern for messager(regexp supported). If the generated
+	// messager name does not match this pattern, an error will be reported.
+	// Example: Conf$
+	//
+	// Default: "".
+	MessagerPattern string `yaml:"messagerPattern"`
 }
 
 // Output options for generating proto files.
diff --git a/proto/tableaupb/wellknown_util.go b/proto/tableaupb/wellknown_util.go
new file mode 100644
index 0000000..17da8dd
--- /dev/null
+++ b/proto/tableaupb/wellknown_util.go
@@ -0,0 +1,53 @@
+package tableaupb
+
+// NewFraction creates a new fraction.
+func NewFraction(num, den int32) *Fraction {
+	return &Fraction{Num: num, Den: den}
+}
+
+// NewIntegerFraction creates a new special fraction: integer. When a fraction has a denominator
+// of 1 (e.g.: a/1), it is referred to as a whole number or an integer.
+func NewIntegerFraction(num int32) *Fraction {
+	return NewFraction(num, 1)
+}
+
+// NewComparator creates a new fraction comparator.
+func NewComparator(sign Comparator_Sign, num, den int32) *Comparator {
+	return &Comparator{
+		Sign:  sign,
+		Value: NewFraction(num, den),
+	}
+}
+
+// NewIntegerComparator creates a new comparator compared to a special fraction: integer.
+// When a fraction has a denominator of 1 (e.g.: a/1), it is referred to as a whole number or an integer.
+func NewIntegerComparator(sign Comparator_Sign, num int32) *Comparator {
+	return &Comparator{
+		Sign:  sign,
+		Value: NewIntegerFraction(num),
+	}
+}
+
+// Compare returns true if the given fraction matches the given comparator.
+func Compare(left *Fraction, cmp *Comparator) bool {
+	right := cmp.GetValue()
+	// cross-multiply to compare
+	lval := int64(left.GetNum()) * int64(right.GetDen())
+	rval := int64(right.GetNum()) * int64(left.GetDen())
+	switch cmp.GetSign() {
+	case Comparator_SIGN_EQUAL:
+		return lval == rval
+	case Comparator_SIGN_NOT_EQUAL:
+		return lval != rval
+	case Comparator_SIGN_LESS:
+		return lval < rval
+	case Comparator_SIGN_LESS_OR_EQUAL:
+		return lval <= rval
+	case Comparator_SIGN_GREATER:
+		return lval > rval
+	case Comparator_SIGN_GREATER_OR_EQUAL:
+		return lval >= rval
+	default:
+		panic("invalid compare operator")
+	}
+}


Test patch:
diff --git a/internal/protogen/exporter_test.go b/internal/protogen/exporter_test.go
index 202f21f..8620061 100644
--- a/internal/protogen/exporter_test.go
+++ b/internal/protogen/exporter_test.go
@@ -1,6 +1,7 @@
 package protogen
 
 import (
+	"regexp"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -415,3 +416,66 @@ func Test_sheetExporter_exportUnion(t *testing.T) {
 		})
 	}
 }
+
+func Test_sheetExporter_exportMessager(t *testing.T) {
+	tests := []struct {
+		name    string
+		x       *sheetExporter
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "export-messager",
+			x: &sheetExporter{
+				ws: &internalpb.Worksheet{
+					Name: "ItemConf",
+					Fields: []*internalpb.Field{
+						{Name: "id", Type: "uint32", FullType: "uint32", Options: &tableaupb.FieldOptions{Name: "ID"}},
+					},
+				},
+				g: NewGeneratedBuf(),
+				be: &bookExporter{
+					gen:                   &Generator{},
+					messagerPatternRegexp: regexp.MustCompile(`Conf$`),
+				},
+				typeInfos:      &xproto.TypeInfos{},
+				nestedMessages: make(map[string]*internalpb.Field),
+			},
+			want: `message ItemConf {
+  option (tableau.worksheet) = {};
+
+  uint32 id = 1 [(tableau.field) = {name:"ID"}];
+}
+
+`,
+			wantErr: false,
+		},
+		{
+			name: "export-messager-pattern-not-match",
+			x: &sheetExporter{
+				ws: &internalpb.Worksheet{
+					Name: "ItemConf",
+					Fields: []*internalpb.Field{
+						{Name: "id", Type: "uint32", FullType: "uint32", Options: &tableaupb.FieldOptions{Name: "ID"}},
+					},
+				},
+				g: NewGeneratedBuf(),
+				be: &bookExporter{
+					gen:                   &Generator{},
+					messagerPatternRegexp: regexp.MustCompile(`Data$`),
+				},
+				typeInfos:      &xproto.TypeInfos{},
+				nestedMessages: make(map[string]*internalpb.Field),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.x.exportMessager(); (err != nil) != tt.wantErr {
+				t.Errorf("sheetExporter.exportMessager() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			assert.Equal(t, tt.want, tt.x.g.String())
+		})
+	}
+}
diff --git a/proto/tableaupb/wellknown_util_test.go b/proto/tableaupb/wellknown_util_test.go
new file mode 100644
index 0000000..a253c59
--- /dev/null
+++ b/proto/tableaupb/wellknown_util_test.go
@@ -0,0 +1,82 @@
+package tableaupb
+
+import (
+	"math"
+	"testing"
+)
+
+func TestCompare(t *testing.T) {
+	type args struct {
+		left *Fraction
+		cmp  *Comparator
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "==",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_EQUAL, 2, 4),
+			},
+			want: true,
+		},
+		{
+			name: "!=",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_NOT_EQUAL, 1, 4),
+			},
+			want: true,
+		},
+		{
+			name: "<",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_LESS, 3, 4),
+			},
+			want: true,
+		},
+		{
+			name: "<=",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_LESS_OR_EQUAL, 3, 4),
+			},
+			want: true,
+		},
+		{
+			name: ">",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_GREATER, 1, 4),
+			},
+			want: true,
+		},
+		{
+			name: ">=",
+			args: args{
+				left: NewFraction(1, 2),
+				cmp:  NewComparator(Comparator_SIGN_GREATER_OR_EQUAL, 2, 4),
+			},
+			want: true,
+		},
+		{
+			name: "max int32",
+			args: args{
+				left: NewFraction(math.MaxInt32-1, math.MaxInt32),              // 2147483646 / 2147483647
+				cmp:  NewComparator(Comparator_SIGN_GREATER, 1, math.MaxInt32), // 1 / 2147483647
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := Compare(tt.args.left, tt.args.cmp); got != tt.want {
+				t.Errorf("Compare() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}