Share day-style generated text validation

This commit is contained in:
2026-06-16 14:55:22 +00:00
parent 0884eb0ce5
commit e3bcecc5c1
5 changed files with 242 additions and 86 deletions

View File

@@ -1,10 +1,5 @@
package generatedtext
import (
"fmt"
"strings"
)
type Daily struct {
Summary string `json:"summary"`
ForecastDiscussion []string `json:"forecast_discussion"`
@@ -13,25 +8,21 @@ type Daily struct {
}
func ValidateDaily(data []byte) (Daily, []byte, error) {
value, err := decodeGeneratedText[Daily](data, "daily")
if err != nil {
return Daily{}, nil, err
}
value.Summary = strings.TrimSpace(value.Summary)
value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming)
value.Confidence = strings.TrimSpace(value.Confidence)
value.ForecastDiscussion = trimNonEmpty(value.ForecastDiscussion)
if value.Summary == "" {
return Daily{}, nil, fmt.Errorf("daily generated text summary is required")
}
if len(value.ForecastDiscussion) == 0 {
return Daily{}, nil, fmt.Errorf("daily generated text forecast discussion is required")
}
normalized, err := normalizeGeneratedText(value, "daily")
if err != nil {
return Daily{}, nil, err
}
return value, normalized, nil
return validateDayStyleGeneratedText[Daily, *Daily](data, "daily")
}
func (d *Daily) dayStyleFields() dayStyleFields {
return dayStyleFields{
Summary: d.Summary,
ForecastDiscussion: d.ForecastDiscussion,
PrecipitationTiming: d.PrecipitationTiming,
Confidence: d.Confidence,
}
}
func (d *Daily) setDayStyleFields(fields dayStyleFields) {
d.Summary = fields.Summary
d.ForecastDiscussion = fields.ForecastDiscussion
d.PrecipitationTiming = fields.PrecipitationTiming
d.Confidence = fields.Confidence
}

View File

@@ -0,0 +1,63 @@
package generatedtext
import (
"fmt"
"strings"
)
type dayStyleFields struct {
Summary string
ForecastDiscussion []string
PrecipitationTiming string
Confidence string
}
type dayStyleGeneratedText interface {
dayStyleFields() dayStyleFields
setDayStyleFields(dayStyleFields)
}
func validateDayStyleGeneratedText[T any, PT interface {
*T
dayStyleGeneratedText
}](data []byte, name string) (T, []byte, error) {
value, err := decodeGeneratedText[T](data, name)
if err != nil {
var zero T
return zero, nil, err
}
pointer := PT(&value)
fields := pointer.dayStyleFields()
fields.Summary = strings.TrimSpace(fields.Summary)
fields.PrecipitationTiming = strings.TrimSpace(fields.PrecipitationTiming)
fields.Confidence = strings.TrimSpace(fields.Confidence)
fields.ForecastDiscussion = trimNonEmpty(fields.ForecastDiscussion)
if fields.Summary == "" {
var zero T
return zero, nil, fmt.Errorf("%s generated text summary is required", name)
}
if len(fields.ForecastDiscussion) == 0 {
var zero T
return zero, nil, fmt.Errorf("%s generated text forecast discussion is required", name)
}
pointer.setDayStyleFields(fields)
normalized, err := normalizeGeneratedText(value, name)
if err != nil {
var zero T
return zero, nil, err
}
return value, normalized, nil
}
func trimNonEmpty(values []string) []string {
out := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
out = append(out, trimmed)
}
}
return out
}

View File

@@ -0,0 +1,131 @@
package generatedtext
import (
"fmt"
"strings"
"testing"
)
func TestValidateDayStyleGeneratedTextSharedBehavior(t *testing.T) {
reports := []struct {
name string
validate func([]byte) (any, []byte, error)
}{
{
name: "daily",
validate: func(data []byte) (any, []byte, error) {
value, normalized, err := ValidateDaily(data)
return value, normalized, err
},
},
{
name: "today",
validate: func(data []byte) (any, []byte, error) {
value, normalized, err := ValidateToday(data)
return value, normalized, err
},
},
{
name: "tomorrow",
validate: func(data []byte) (any, []byte, error) {
value, normalized, err := ValidateTomorrow(data)
return value, normalized, err
},
},
}
for _, report := range reports {
t.Run(report.name, func(t *testing.T) {
t.Run("requires summary", func(t *testing.T) {
_, _, err := report.validate([]byte(`{"forecast_discussion":["First paragraph."]}`))
want := fmt.Sprintf("%s generated text summary is required", report.name)
if err == nil || err.Error() != want {
t.Fatalf("validate() error = %v, want %q", err, want)
}
})
t.Run("requires non-empty forecast discussion", func(t *testing.T) {
_, _, err := report.validate([]byte(`{"summary":"Shared summary.","forecast_discussion":[" ",""]}`))
want := fmt.Sprintf("%s generated text forecast discussion is required", report.name)
if err == nil || err.Error() != want {
t.Fatalf("validate() error = %v, want %q", err, want)
}
})
t.Run("trims and normalizes", func(t *testing.T) {
value, normalized, err := report.validate([]byte(`{
"summary": " Shared summary. ",
"forecast_discussion": [
" First paragraph. ",
"",
" Second paragraph. "
],
"precipitation_timing": " Afternoon. ",
"confidence": " Medium "
}`))
if err != nil {
t.Fatalf("validate() error = %v", err)
}
fields := dayStyleFieldsForTest(t, value)
if fields.Summary != "Shared summary." {
t.Fatalf("Summary = %q, want trimmed summary", fields.Summary)
}
if strings.Join(fields.ForecastDiscussion, "|") != "First paragraph.|Second paragraph." {
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", fields.ForecastDiscussion)
}
if fields.PrecipitationTiming != "Afternoon." {
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", fields.PrecipitationTiming)
}
if fields.Confidence != "Medium" {
t.Fatalf("Confidence = %q, want trimmed confidence", fields.Confidence)
}
want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph.","Second paragraph."],"precipitation_timing":"Afternoon.","confidence":"Medium"}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
})
t.Run("omits empty optional fields", func(t *testing.T) {
_, normalized, err := report.validate([]byte(`{
"summary": "Shared summary.",
"forecast_discussion": ["First paragraph."],
"precipitation_timing": " ",
"confidence": " "
}`))
if err != nil {
t.Fatalf("validate() error = %v", err)
}
want := `{"summary":"Shared summary.","forecast_discussion":["First paragraph."]}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
})
t.Run("rejects unknown fields", func(t *testing.T) {
_, _, err := report.validate([]byte(`{"summary":"Shared summary.","forecast_discussion":["First paragraph."],"extra":"value"}`))
if err == nil {
t.Fatal("validate() error = nil, want error")
}
if !strings.Contains(err.Error(), `unknown field "extra"`) {
t.Fatalf("validate() error = %v, want unknown field error", err)
}
})
})
}
}
func dayStyleFieldsForTest(t *testing.T, value any) dayStyleFields {
t.Helper()
switch value := value.(type) {
case Daily:
return (&value).dayStyleFields()
case Today:
return (&value).dayStyleFields()
case Tomorrow:
return (&value).dayStyleFields()
default:
t.Fatalf("value type = %T, want day-style generated text", value)
return dayStyleFields{}
}
}

View File

@@ -1,10 +1,5 @@
package generatedtext
import (
"fmt"
"strings"
)
type Today struct {
Summary string `json:"summary"`
ForecastDiscussion []string `json:"forecast_discussion"`
@@ -13,25 +8,21 @@ type Today struct {
}
func ValidateToday(data []byte) (Today, []byte, error) {
value, err := decodeGeneratedText[Today](data, "today")
if err != nil {
return Today{}, nil, err
}
value.Summary = strings.TrimSpace(value.Summary)
value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming)
value.Confidence = strings.TrimSpace(value.Confidence)
value.ForecastDiscussion = trimNonEmpty(value.ForecastDiscussion)
if value.Summary == "" {
return Today{}, nil, fmt.Errorf("today generated text summary is required")
}
if len(value.ForecastDiscussion) == 0 {
return Today{}, nil, fmt.Errorf("today generated text forecast discussion is required")
}
normalized, err := normalizeGeneratedText(value, "today")
if err != nil {
return Today{}, nil, err
}
return value, normalized, nil
return validateDayStyleGeneratedText[Today, *Today](data, "today")
}
func (t *Today) dayStyleFields() dayStyleFields {
return dayStyleFields{
Summary: t.Summary,
ForecastDiscussion: t.ForecastDiscussion,
PrecipitationTiming: t.PrecipitationTiming,
Confidence: t.Confidence,
}
}
func (t *Today) setDayStyleFields(fields dayStyleFields) {
t.Summary = fields.Summary
t.ForecastDiscussion = fields.ForecastDiscussion
t.PrecipitationTiming = fields.PrecipitationTiming
t.Confidence = fields.Confidence
}

View File

@@ -1,10 +1,5 @@
package generatedtext
import (
"fmt"
"strings"
)
type Tomorrow struct {
Summary string `json:"summary"`
ForecastDiscussion []string `json:"forecast_discussion"`
@@ -13,36 +8,21 @@ type Tomorrow struct {
}
func ValidateTomorrow(data []byte) (Tomorrow, []byte, error) {
value, err := decodeGeneratedText[Tomorrow](data, "tomorrow")
if err != nil {
return Tomorrow{}, nil, err
}
value.Summary = strings.TrimSpace(value.Summary)
value.PrecipitationTiming = strings.TrimSpace(value.PrecipitationTiming)
value.Confidence = strings.TrimSpace(value.Confidence)
value.ForecastDiscussion = trimNonEmpty(value.ForecastDiscussion)
if value.Summary == "" {
return Tomorrow{}, nil, fmt.Errorf("tomorrow generated text summary is required")
}
if len(value.ForecastDiscussion) == 0 {
return Tomorrow{}, nil, fmt.Errorf("tomorrow generated text forecast discussion is required")
}
normalized, err := normalizeGeneratedText(value, "tomorrow")
if err != nil {
return Tomorrow{}, nil, err
}
return value, normalized, nil
return validateDayStyleGeneratedText[Tomorrow, *Tomorrow](data, "tomorrow")
}
func trimNonEmpty(values []string) []string {
out := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed != "" {
out = append(out, trimmed)
}
func (t *Tomorrow) dayStyleFields() dayStyleFields {
return dayStyleFields{
Summary: t.Summary,
ForecastDiscussion: t.ForecastDiscussion,
PrecipitationTiming: t.PrecipitationTiming,
Confidence: t.Confidence,
}
return out
}
func (t *Tomorrow) setDayStyleFields(fields dayStyleFields) {
t.Summary = fields.Summary
t.ForecastDiscussion = fields.ForecastDiscussion
t.PrecipitationTiming = fields.PrecipitationTiming
t.Confidence = fields.Confidence
}