Enforce generated text report identity
This commit is contained in:
@@ -9,9 +9,10 @@ maintainer-facing context fields belong to [report templates](../templates.md).
|
|||||||
## Catalog and validation
|
## Catalog and validation
|
||||||
|
|
||||||
The Daily, Today, Tomorrow, and Hourly report definitions each use structured
|
The Daily, Today, Tomorrow, and Hourly report definitions each use structured
|
||||||
generated text. `LookupDefinition` rejects unknown schema or template IDs and
|
generated text. `LookupDefinition` requires the exact report, schema, and
|
||||||
unsupported schema/template pairs before the run begins. A handler validates raw JSON, returns a typed
|
template triple and rejects unknown IDs, unsupported pairs, and a pair that
|
||||||
value and canonical normalized JSON, loads its canonical schema through
|
belongs to another report before the run begins. A handler validates raw JSON,
|
||||||
|
returns a typed value and canonical normalized JSON, loads its canonical schema through
|
||||||
`internal/promptassets`, builds a render context, and renders through
|
`internal/promptassets`, builds a render context, and renders through
|
||||||
`internal/reporttemplate`.
|
`internal/reporttemplate`.
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ Daily, Today, and Tomorrow use a day-style value with required trimmed summary
|
|||||||
and one or more nonblank discussion paragraphs. Hourly requires trimmed summary
|
and one or more nonblank discussion paragraphs. Hourly requires trimmed summary
|
||||||
and a single trimmed discussion string. Every form also requires the
|
and a single trimmed discussion string. Every form also requires the
|
||||||
`precipitation_timing` field; an empty string means there is no supported timing
|
`precipitation_timing` field; an empty string means there is no supported timing
|
||||||
prose to render. Typed decoding rejects missing required fields and unknown JSON
|
prose to render. Typed decoding requires the exact lowercase JSON field names,
|
||||||
fields; no general-purpose JSON Schema engine is used at runtime.
|
rejects missing, duplicate, case-variant, and unknown fields, and checks field
|
||||||
|
shapes; no general-purpose JSON Schema engine is used at runtime.
|
||||||
|
|
||||||
## Render contexts
|
## Render contexts
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ source:
|
|||||||
| Hourly | `templates/hourly.md.tmpl` (`hourly`) | `hourly` | `weather.hourly_generated_text`; `internal/promptassets/assets/prompts/hourly/` |
|
| Hourly | `templates/hourly.md.tmpl` (`hourly`) | `hourly` | `weather.hourly_generated_text`; `internal/promptassets/assets/prompts/hourly/` |
|
||||||
|
|
||||||
The matching schemas and Promptkit definitions are embedded by
|
The matching schemas and Promptkit definitions are embedded by
|
||||||
`internal/promptassets`. The generated-text catalog pairs each schema ID with
|
`internal/promptassets`. The generated-text catalog requires each report's
|
||||||
its template ID; keep the matching prompt definition aligned with that pair.
|
exact schema/template pair; keep the matching prompt definition aligned with
|
||||||
|
that report-specific triple.
|
||||||
|
|
||||||
Shared partials are under `internal/reporttemplate/templates/partials/`:
|
Shared partials are under `internal/reporttemplate/templates/partials/`:
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -7,10 +7,10 @@ require gopkg.in/yaml.v3 v3.0.1
|
|||||||
require (
|
require (
|
||||||
gitea.maximumdirect.net/eric/distributor v0.5.0
|
gitea.maximumdirect.net/eric/distributor v0.5.0
|
||||||
gitea.maximumdirect.net/eric/promptkit v0.5.0
|
gitea.maximumdirect.net/eric/promptkit v0.5.0
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||||
golang.org/x/sys v0.45.0
|
golang.org/x/sys v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type validator func([]byte) (any, []byte, error)
|
|||||||
type renderContextBuilder func(report.ID, string, briefing.Metadata, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error)
|
type renderContextBuilder func(report.ID, string, briefing.Metadata, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error)
|
||||||
|
|
||||||
type catalogEntry struct {
|
type catalogEntry struct {
|
||||||
|
reportID report.ID
|
||||||
schemaID string
|
schemaID string
|
||||||
templateID string
|
templateID string
|
||||||
validate validator
|
validate validator
|
||||||
@@ -44,24 +45,28 @@ type Handler struct {
|
|||||||
|
|
||||||
var catalog = []catalogEntry{
|
var catalog = []catalogEntry{
|
||||||
{
|
{
|
||||||
|
reportID: report.Hourly,
|
||||||
schemaID: schemaIDHourly,
|
schemaID: schemaIDHourly,
|
||||||
templateID: templateIDHourly,
|
templateID: templateIDHourly,
|
||||||
validate: validateHourly,
|
validate: validateHourly,
|
||||||
renderContextBuilder: buildHourlyContext,
|
renderContextBuilder: buildHourlyContext,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
reportID: report.Daily,
|
||||||
schemaID: schemaIDDaily,
|
schemaID: schemaIDDaily,
|
||||||
templateID: templateIDDaily,
|
templateID: templateIDDaily,
|
||||||
validate: validateDaily,
|
validate: validateDaily,
|
||||||
renderContextBuilder: buildDailyContext,
|
renderContextBuilder: buildDailyContext,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
reportID: report.Today,
|
||||||
schemaID: schemaIDToday,
|
schemaID: schemaIDToday,
|
||||||
templateID: templateIDToday,
|
templateID: templateIDToday,
|
||||||
validate: validateToday,
|
validate: validateToday,
|
||||||
renderContextBuilder: buildTodayContext,
|
renderContextBuilder: buildTodayContext,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
reportID: report.Tomorrow,
|
||||||
schemaID: schemaIDTomorrow,
|
schemaID: schemaIDTomorrow,
|
||||||
templateID: templateIDTomorrow,
|
templateID: templateIDTomorrow,
|
||||||
validate: validateTomorrow,
|
validate: validateTomorrow,
|
||||||
@@ -70,8 +75,7 @@ var catalog = []catalogEntry{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LookupDefinition(definition report.Definition) (Handler, error) {
|
func LookupDefinition(definition report.Definition) (Handler, error) {
|
||||||
|
var schemaKnown, templateKnown, pairKnown bool
|
||||||
var schemaKnown, templateKnown bool
|
|
||||||
for _, entry := range catalog {
|
for _, entry := range catalog {
|
||||||
if entry.schemaID == definition.GeneratedTextSchemaID {
|
if entry.schemaID == definition.GeneratedTextSchemaID {
|
||||||
schemaKnown = true
|
schemaKnown = true
|
||||||
@@ -80,8 +84,12 @@ func LookupDefinition(definition report.Definition) (Handler, error) {
|
|||||||
templateKnown = true
|
templateKnown = true
|
||||||
}
|
}
|
||||||
if entry.schemaID == definition.GeneratedTextSchemaID && entry.templateID == definition.TemplateID {
|
if entry.schemaID == definition.GeneratedTextSchemaID && entry.templateID == definition.TemplateID {
|
||||||
|
pairKnown = true
|
||||||
|
if entry.reportID != definition.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return Handler{
|
return Handler{
|
||||||
reportID: definition.ID,
|
reportID: entry.reportID,
|
||||||
schemaID: entry.schemaID,
|
schemaID: entry.schemaID,
|
||||||
templateID: entry.templateID,
|
templateID: entry.templateID,
|
||||||
validate: entry.validate,
|
validate: entry.validate,
|
||||||
@@ -95,6 +103,9 @@ func LookupDefinition(definition report.Definition) (Handler, error) {
|
|||||||
if !templateKnown {
|
if !templateKnown {
|
||||||
return Handler{}, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
|
return Handler{}, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
|
||||||
}
|
}
|
||||||
|
if pairKnown {
|
||||||
|
return Handler{}, fmt.Errorf("generated text schema %q and report template %q do not belong to report %q", definition.GeneratedTextSchemaID, definition.TemplateID, definition.ID)
|
||||||
|
}
|
||||||
return Handler{}, fmt.Errorf("generated text schema %q and report template %q are not supported together for report %q", definition.GeneratedTextSchemaID, definition.TemplateID, definition.ID)
|
return Handler{}, fmt.Errorf("generated text schema %q and report template %q are not supported together for report %q", definition.GeneratedTextSchemaID, definition.TemplateID, definition.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,17 +8,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCatalogCompleteForGeneratedTextTemplateReports(t *testing.T) {
|
func TestCatalogCompleteForGeneratedTextTemplateReports(t *testing.T) {
|
||||||
for _, definition := range report.DefaultRegistry().All() {
|
expected := []struct {
|
||||||
t.Run(string(definition.ID), func(t *testing.T) {
|
reportID report.ID
|
||||||
|
schemaID string
|
||||||
|
templateID string
|
||||||
|
}{
|
||||||
|
{reportID: report.Daily, schemaID: "daily", templateID: "daily"},
|
||||||
|
{reportID: report.Today, schemaID: "today", templateID: "today"},
|
||||||
|
{reportID: report.Tomorrow, schemaID: "tomorrow", templateID: "tomorrow"},
|
||||||
|
{reportID: report.Hourly, schemaID: "hourly", templateID: "hourly"},
|
||||||
|
}
|
||||||
|
registry := report.DefaultRegistry()
|
||||||
|
for _, expected := range expected {
|
||||||
|
t.Run(string(expected.reportID), func(t *testing.T) {
|
||||||
|
definition := registry.MustLookup(expected.reportID)
|
||||||
|
if definition.GeneratedTextSchemaID != expected.schemaID || definition.TemplateID != expected.templateID {
|
||||||
|
t.Fatalf("definition = %#v, want schema/template %q/%q", definition, expected.schemaID, expected.templateID)
|
||||||
|
}
|
||||||
handler, err := LookupDefinition(definition)
|
handler, err := LookupDefinition(definition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LookupDefinition() error = %v", err)
|
t.Fatalf("LookupDefinition() error = %v", err)
|
||||||
}
|
}
|
||||||
if handler.SchemaID() != definition.GeneratedTextSchemaID {
|
if handler.SchemaID() != expected.schemaID {
|
||||||
t.Fatalf("SchemaID() = %q, want %q", handler.SchemaID(), definition.GeneratedTextSchemaID)
|
t.Fatalf("SchemaID() = %q, want %q", handler.SchemaID(), expected.schemaID)
|
||||||
}
|
}
|
||||||
if handler.TemplateID() != definition.TemplateID {
|
if handler.TemplateID() != expected.templateID {
|
||||||
t.Fatalf("TemplateID() = %q, want %q", handler.TemplateID(), definition.TemplateID)
|
t.Fatalf("TemplateID() = %q, want %q", handler.TemplateID(), expected.templateID)
|
||||||
}
|
}
|
||||||
if handler.validate == nil {
|
if handler.validate == nil {
|
||||||
t.Fatal("validator is nil")
|
t.Fatal("validator is nil")
|
||||||
@@ -40,6 +55,35 @@ func TestCatalogCompleteForGeneratedTextTemplateReports(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCatalogRejectsKnownPairForAnotherReport(t *testing.T) {
|
||||||
|
pairs := []struct {
|
||||||
|
reportID report.ID
|
||||||
|
schemaID string
|
||||||
|
templateID string
|
||||||
|
}{
|
||||||
|
{reportID: report.Daily, schemaID: "daily", templateID: "daily"},
|
||||||
|
{reportID: report.Today, schemaID: "today", templateID: "today"},
|
||||||
|
{reportID: report.Tomorrow, schemaID: "tomorrow", templateID: "tomorrow"},
|
||||||
|
{reportID: report.Hourly, schemaID: "hourly", templateID: "hourly"},
|
||||||
|
}
|
||||||
|
registry := report.DefaultRegistry()
|
||||||
|
for _, definitionPair := range pairs {
|
||||||
|
for _, mismatchedPair := range pairs {
|
||||||
|
if definitionPair.reportID == mismatchedPair.reportID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Run(string(definitionPair.reportID)+"/"+string(mismatchedPair.reportID), func(t *testing.T) {
|
||||||
|
definition := registry.MustLookup(definitionPair.reportID)
|
||||||
|
definition.GeneratedTextSchemaID = mismatchedPair.schemaID
|
||||||
|
definition.TemplateID = mismatchedPair.templateID
|
||||||
|
if _, err := LookupDefinition(definition); err == nil || !strings.Contains(err.Error(), "do not belong") {
|
||||||
|
t.Fatalf("LookupDefinition(%#v) error = %v, want wrong-report error", definition, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCatalogLookupRejectsUnsupportedSchemaAndTemplate(t *testing.T) {
|
func TestCatalogLookupRejectsUnsupportedSchemaAndTemplate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var generatedTextFieldNames = map[string]struct{}{
|
||||||
|
"summary": {},
|
||||||
|
"forecast_discussion": {},
|
||||||
|
"precipitation_timing": {},
|
||||||
|
}
|
||||||
|
|
||||||
func decodeGeneratedText[T any](data []byte, name string) (T, error) {
|
func decodeGeneratedText[T any](data []byte, name string) (T, error) {
|
||||||
var value T
|
var value T
|
||||||
|
if err := validateGeneratedTextObject(data, name); err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
decoder.DisallowUnknownFields()
|
decoder.DisallowUnknownFields()
|
||||||
if err := decoder.Decode(&value); err != nil {
|
if err := decoder.Decode(&value); err != nil {
|
||||||
@@ -25,6 +34,43 @@ func decodeGeneratedText[T any](data []byte, name string) (T, error) {
|
|||||||
return value, fmt.Errorf("decode %s generated text: multiple JSON values", name)
|
return value, fmt.Errorf("decode %s generated text: multiple JSON values", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateGeneratedTextObject(data []byte, name string) error {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode %s generated text: %w", name, err)
|
||||||
|
}
|
||||||
|
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
|
||||||
|
return fmt.Errorf("decode %s generated text: expected JSON object", name)
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(generatedTextFieldNames))
|
||||||
|
for decoder.More() {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode %s generated text: %w", name, err)
|
||||||
|
}
|
||||||
|
field, ok := token.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("decode %s generated text: expected object field name", name)
|
||||||
|
}
|
||||||
|
if _, ok := generatedTextFieldNames[field]; !ok {
|
||||||
|
return fmt.Errorf("decode %s generated text: json: unknown field %q", name, field)
|
||||||
|
}
|
||||||
|
if _, ok := seen[field]; ok {
|
||||||
|
return fmt.Errorf("decode %s generated text: duplicate field %q", name, field)
|
||||||
|
}
|
||||||
|
seen[field] = struct{}{}
|
||||||
|
var value json.RawMessage
|
||||||
|
if err := decoder.Decode(&value); err != nil {
|
||||||
|
return fmt.Errorf("decode %s generated text: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := decoder.Token(); err != nil {
|
||||||
|
return fmt.Errorf("decode %s generated text: %w", name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func requireGeneratedTextStringField(data []byte, name, field string) error {
|
func requireGeneratedTextStringField(data []byte, name, field string) error {
|
||||||
var fields map[string]json.RawMessage
|
var fields map[string]json.RawMessage
|
||||||
if err := json.Unmarshal(data, &fields); err != nil {
|
if err := json.Unmarshal(data, &fields); err != nil {
|
||||||
|
|||||||
130
internal/generatedtext/json_test.go
Normal file
130
internal/generatedtext/json_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package generatedtext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets"
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneratedTextValidatorsRequireExactObjectFields(t *testing.T) {
|
||||||
|
validators := []struct {
|
||||||
|
name string
|
||||||
|
valid string
|
||||||
|
validate func([]byte) error
|
||||||
|
}{
|
||||||
|
{name: "daily", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateDailyJSON},
|
||||||
|
{name: "today", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTodayJSON},
|
||||||
|
{name: "tomorrow", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTomorrowJSON},
|
||||||
|
{name: "hourly", valid: `{"summary":"Summary","forecast_discussion":"Discussion","precipitation_timing":""}`, validate: validateHourlyJSON},
|
||||||
|
}
|
||||||
|
for _, validator := range validators {
|
||||||
|
t.Run(validator.name, func(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{name: "canonical", input: validator.valid, valid: true},
|
||||||
|
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
||||||
|
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
||||||
|
{name: "case variant summary", input: strings.Replace(validator.valid, `"summary"`, `"Summary"`, 1)},
|
||||||
|
{name: "case variant discussion", input: strings.Replace(validator.valid, `"forecast_discussion"`, `"Forecast_Discussion"`, 1)},
|
||||||
|
{name: "duplicate", input: strings.Replace(validator.valid, `"summary":"Summary",`, `"summary":"Summary","summary":"Other",`, 1)},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
err := validator.validate([]byte(test.input))
|
||||||
|
if (err == nil) != test.valid {
|
||||||
|
t.Fatalf("validate(%s) error = %v, want valid = %t", test.input, err, test.valid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypedValidatorsMatchEmbeddedSchemaObjectShape(t *testing.T) {
|
||||||
|
validators := []struct {
|
||||||
|
name string
|
||||||
|
valid string
|
||||||
|
validate func([]byte) error
|
||||||
|
}{
|
||||||
|
{name: "daily", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateDailyJSON},
|
||||||
|
{name: "today", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTodayJSON},
|
||||||
|
{name: "tomorrow", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTomorrowJSON},
|
||||||
|
{name: "hourly", valid: `{"summary":"Summary","forecast_discussion":"Discussion","precipitation_timing":""}`, validate: validateHourlyJSON},
|
||||||
|
}
|
||||||
|
for _, validator := range validators {
|
||||||
|
t.Run(validator.name, func(t *testing.T) {
|
||||||
|
schema := generatedTextSchema(t, validator.name)
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{name: "canonical", input: validator.valid},
|
||||||
|
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
||||||
|
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
||||||
|
{name: "case variant", input: strings.Replace(validator.valid, `"summary"`, `"Summary"`, 1)},
|
||||||
|
{name: "null", input: strings.Replace(validator.valid, `"precipitation_timing":""`, `"precipitation_timing":null`, 1)},
|
||||||
|
{name: "wrong type", input: strings.Replace(validator.valid, `"summary":"Summary"`, `"summary":false`, 1)},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
instance, err := jsonschema.UnmarshalJSON(strings.NewReader(test.input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse schema instance: %v", err)
|
||||||
|
}
|
||||||
|
schemaValid := schema.Validate(instance) == nil
|
||||||
|
typedValid := validator.validate([]byte(test.input)) == nil
|
||||||
|
if schemaValid != typedValid {
|
||||||
|
t.Fatalf("schema valid = %t, typed valid = %t for %s", schemaValid, typedValid, test.input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatedTextSchema(t *testing.T, id string) *jsonschema.Schema {
|
||||||
|
t.Helper()
|
||||||
|
data, err := promptassets.Schema(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Schema(%q) error = %v", id, err)
|
||||||
|
}
|
||||||
|
document, err := jsonschema.UnmarshalJSON(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse schema %q: %v", id, err)
|
||||||
|
}
|
||||||
|
compiler := jsonschema.NewCompiler()
|
||||||
|
location := fmt.Sprintf("https://weatherreporter.example/schemas/%s.json", id)
|
||||||
|
if err := compiler.AddResource(location, document); err != nil {
|
||||||
|
t.Fatalf("add schema %q: %v", id, err)
|
||||||
|
}
|
||||||
|
schema, err := compiler.Compile(location)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compile schema %q: %v", id, err)
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDailyJSON(data []byte) error {
|
||||||
|
_, _, err := ValidateDaily(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTodayJSON(data []byte) error {
|
||||||
|
_, _, err := ValidateToday(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTomorrowJSON(data []byte) error {
|
||||||
|
_, _, err := ValidateTomorrow(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHourlyJSON(data []byte) error {
|
||||||
|
_, _, err := ValidateHourly(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user