Skip to content

feat: allow marking values as invalid with "!" - #312

Open
m-ocean-it wants to merge 1 commit into
abice:masterfrom
m-ocean-it:master
Open

feat: allow marking values as invalid with "!"#312
m-ocean-it wants to merge 1 commit into
abice:masterfrom
m-ocean-it:master

Conversation

@m-ocean-it

Copy link
Copy Markdown

Example:

// ENUM(!unknown, red, green, blue)
type Color int

Additional map is generated:

var _ColorValidityMap = map[Color]bool{
	ColorUnknown: false,
	ColorRed:     true,
	ColorGreen:   true,
	ColorBlue:    true,
}

The IsValid method is changed to:

func (x Color) IsValid() bool {
	return _ColorValidityMap[x]
}

The exclamation mark is removed from the string-value of the enum:

const _ColorName = "unknownredgreenblue"

@m-ocean-it
m-ocean-it requested a review from abice as a code owner June 21, 2026 04:59
@m-ocean-it

Copy link
Copy Markdown
Author

Will add testing for new functionality a bit later...

@abice

abice commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Why make a second map, instead o just leaving it out of the original lookup map?

@m-ocean-it

Copy link
Copy Markdown
Author

The original map is used in the String method...

However, I admit that the whole concept is a bit sus.

My desire is to have enums that are not zero-initialized to valid values, so that there're less bugs. One way to achieve that, is to start enumeration from one instead of zero. I've realized that it's already possible:

// ENUM(red=1, green, blue)

However, it's also useful to have a sentinel-value (like Unknown or Unspecified) for when one needs to return an invalid enum-value. Currently, as I see it, it's not possible to achieve with this package.

// ENUM(unknown, red, green, blue)

would generate a zero Unknown value but IsValid would still return true when called on it.

One way of doing what I want is to allow a user to mark specific values as invalid. That's what the PR is about. It is somewhat questionable, though, since it's probably not often when someone needs to have multiple invalid sentinels. It could be more reasonable to add a CLI-arg like --invalid-zero to automatically mark a zero value as invalid (i.e. return false from IsValid when called on it).

@m-ocean-it

m-ocean-it commented Jun 21, 2026

Copy link
Copy Markdown
Author

That could be enough (I can submit a separate PR):
(and the same can be done for string-based enums)

diff --git a/generator/enum.tmpl b/generator/enum.tmpl
index 8d214a540c..c1cb894ecc 100644
--- a/generator/enum.tmpl
+++ b/generator/enum.tmpl
@@ -58,7 +58,11 @@

 // IsValid provides a quick way to determine if the typed value is
 // part of the allowed enumerated values
-func (x {{.enum.Name}}) IsValid() bool {
+func (x {{.enum.Name}}) IsValid() bool { {{if .invalidZero}}var zero {{.enum.Name}}
+    if x == zero {
+        return false
+    }{{end}}
+
 	_, ok := _{{.enum.Name}}Map[x]
 	return ok
 }

diff --git a/generator/generator.go b/generator/generator.go
index cac315ae4c..c7269a8060 100644
--- a/generator/generator.go
+++ b/generator/generator.go
@@ -213,6 +213,7 @@
 			"nocase":        g.CaseInsensitive,
 			"nocomments":    g.NoComments,
 			"noIota":        g.NoIota,
+			"invalidZero":   g.InvalidZero,
 			"marshal":       g.Marshal,
 			"sql":           g.SQL,
 			"sqlint":        g.SQLInt,
diff --git a/generator/options.go b/generator/options.go
index df085941b6..eb066a34cd 100644
--- a/generator/options.go
+++ b/generator/options.go
@@ -4,6 +4,7 @@
 type GeneratorConfig struct {
 	NoPrefix          bool              `json:"no_prefix"`
 	NoIota            bool              `json:"no_iota"`
+	InvalidZero       bool              `json:"invalid_zero"`
 	LowercaseLookup   bool              `json:"lowercase_lookup"`
 	CaseInsensitive   bool              `json:"case_insensitive"`
 	Marshal           bool              `json:"marshal"`
diff --git a/main.go b/main.go
index a149bc4b3d..7a51ee7503 100644
--- a/main.go
+++ b/main.go
@@ -26,6 +26,7 @@
 	FileNames         cli.StringSlice
 	NoPrefix          bool
 	NoIota            bool
+	InvalidZero       bool
 	Lowercase         bool
 	NoCase            bool
 	Marshal           bool
@@ -229,6 +230,11 @@
 				Usage:       "Disables the use of iota in generated enums.",
 				Destination: &argv.NoIota,
 			},
+			&cli.BoolFlag{
+				Name:        "invalid-zero",
+				Usage:       "Treat zero-value as invalid.",
+				Destination: &argv.InvalidZero,
+			},
 		},
 		Action: func(ctx *cli.Context) error {
 			// Validate incompatible flag combinations
@@ -262,6 +268,7 @@
 				config := generator.GeneratorConfig{
 					NoPrefix:          argv.NoPrefix,
 					NoIota:            argv.NoIota,
+					InvalidZero:       argv.InvalidZero,
 					LowercaseLookup:   argv.Lowercase || argv.NoCase,
 					CaseInsensitive:   argv.NoCase,
 					Marshal:           argv.Marshal,

@m-ocean-it

Copy link
Copy Markdown
Author

The --invalid-zero approach wouldn't, however, solve the issue described here: #299

@m-ocean-it

Copy link
Copy Markdown
Author

Why make a second map, instead of just leaving it out of the original lookup map?

The original map is used in the String method...

I see, though, that it could be reasonable to, indeed, just remove the value from the original map. If it's just a sentinel, would it be of benefit to know its name?

@abice

abice commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What about just adding a const value of the invalid value right below the enum, rather than messing with the generation? Then you can use the Sentinel in code but all the enum parsing code would treat it as unknown.

@m-ocean-it

Copy link
Copy Markdown
Author

Wow, never occurred to me ... XD (genuinely, not sarcasm)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants