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

View File

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