Add Tomorrow generated text contract

This commit is contained in:
2026-06-14 23:25:55 +00:00
parent 386263784c
commit 120bce3391
8 changed files with 340 additions and 17 deletions

View File

@@ -0,0 +1,62 @@
package generatedtext
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
)
type Tomorrow struct {
Summary string `json:"summary"`
ForecastDiscussion []string `json:"forecast_discussion"`
PrecipitationTiming string `json:"precipitation_timing,omitempty"`
Confidence string `json:"confidence,omitempty"`
}
func ValidateTomorrow(data []byte) (Tomorrow, []byte, error) {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
var value Tomorrow
if err := decoder.Decode(&value); err != nil {
return Tomorrow{}, nil, fmt.Errorf("decode tomorrow generated text: %w", err)
}
var extra any
if err := decoder.Decode(&extra); err != nil {
if err != io.EOF {
return Tomorrow{}, nil, fmt.Errorf("decode tomorrow generated text: %w", err)
}
} else {
return Tomorrow{}, nil, fmt.Errorf("decode tomorrow generated text: multiple JSON values")
}
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 := json.Marshal(value)
if err != nil {
return Tomorrow{}, nil, fmt.Errorf("normalize tomorrow generated text: %w", 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,111 @@
package generatedtext
import (
"strings"
"testing"
)
func TestValidateTomorrowNormalizesJSON(t *testing.T) {
value, normalized, err := ValidateTomorrow([]byte(`{
"summary": " Storms become more likely tomorrow. ",
"forecast_discussion": [
" A front will keep showers in the forecast. ",
"",
" Temperatures stay seasonable by afternoon. "
],
"precipitation_timing": " Rain is most likely before sunrise. ",
"confidence": " Medium "
}`))
if err != nil {
t.Fatalf("ValidateTomorrow() error = %v", err)
}
if value.Summary != "Storms become more likely tomorrow." {
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
}
if strings.Join(value.ForecastDiscussion, "|") != "A front will keep showers in the forecast.|Temperatures stay seasonable by afternoon." {
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", value.ForecastDiscussion)
}
if value.PrecipitationTiming != "Rain is most likely before sunrise." {
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming)
}
want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely before sunrise.","confidence":"Medium"}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
}
func TestValidateTomorrowOmitsEmptyOptionalFields(t *testing.T) {
_, normalized, err := ValidateTomorrow([]byte(`{
"summary": "Storms become more likely tomorrow.",
"forecast_discussion": ["A front will keep showers in the forecast."],
"precipitation_timing": " ",
"confidence": " "
}`))
if err != nil {
t.Fatalf("ValidateTomorrow() error = %v", err)
}
want := `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."]}`
if string(normalized) != want {
t.Fatalf("normalized = %s, want %s", normalized, want)
}
}
func TestValidateTomorrowRejectsInvalidInput(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "malformed",
in: `{`,
want: "decode tomorrow generated text",
},
{
name: "unknown field",
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."],"extra":"value"}`,
want: `unknown field "extra"`,
},
{
name: "missing summary",
in: `{"forecast_discussion":["A front will keep showers in the forecast."]}`,
want: "summary is required",
},
{
name: "blank summary",
in: `{"summary":" ","forecast_discussion":["A front will keep showers in the forecast."]}`,
want: "summary is required",
},
{
name: "missing forecast discussion",
in: `{"summary":"Storms become more likely tomorrow."}`,
want: "forecast discussion is required",
},
{
name: "blank forecast discussion",
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":[" ",""]}`,
want: "forecast discussion is required",
},
{
name: "forecast discussion wrong type",
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":"A front will keep showers in the forecast."}`,
want: "cannot unmarshal string",
},
{
name: "multiple values",
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."]} {}`,
want: "multiple JSON values",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, _, err := ValidateTomorrow([]byte(test.in))
if err == nil {
t.Fatal("ValidateTomorrow() error = nil, want error")
}
if !strings.Contains(err.Error(), test.want) {
t.Fatalf("ValidateTomorrow() error = %v, want %q", err, test.want)
}
})
}
}