diff --git a/internal/event/validate.go b/internal/event/validate.go new file mode 100644 index 0000000..3ae31ea --- /dev/null +++ b/internal/event/validate.go @@ -0,0 +1,85 @@ +package event + +import ( + "errors" + "fmt" +) + +var validTypes = map[string]bool{ + "session.start": true, + "session.end": true, + "run.start": true, + "run.end": true, + "span.start": true, + "span.end": true, + "error": true, + "metric.snapshot": true, +} + +type ValidationError struct { + Field string + Message string +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("%s: %s", e.Field, e.Message) +} + +func Validate(m map[string]any) error { + // Check schema + schema, ok := m["schema"].(map[string]any) + if !ok { + return ValidationError{Field: "schema", Message: "missing or invalid"} + } + if name, _ := schema["name"].(string); name != "agentmon.event" { + return ValidationError{Field: "schema.name", Message: "must be 'agentmon.event'"} + } + if ver, _ := schema["version"].(float64); ver != 1 { + return ValidationError{Field: "schema.version", Message: "must be 1"} + } + + // Check event + event, ok := m["event"].(map[string]any) + if !ok { + return ValidationError{Field: "event", Message: "missing or invalid"} + } + + if id, _ := event["id"].(string); id == "" { + return ValidationError{Field: "event.id", Message: "required"} + } + + eventType, _ := event["type"].(string) + if eventType == "" { + return ValidationError{Field: "event.type", Message: "required"} + } + if !validTypes[eventType] { + return ValidationError{Field: "event.type", Message: fmt.Sprintf("invalid type '%s'", eventType)} + } + + if event["ts"] == nil { + return ValidationError{Field: "event.ts", Message: "required"} + } + + // Check source + source, ok := event["source"].(map[string]any) + if !ok { + return ValidationError{Field: "event.source", Message: "missing or invalid"} + } + + if fw, _ := source["framework"].(string); fw == "" { + return ValidationError{Field: "event.source.framework", Message: "required"} + } + if cid, _ := source["client_id"].(string); cid == "" { + return ValidationError{Field: "event.source.client_id", Message: "required"} + } + if host, _ := source["host"].(string); host == "" { + return ValidationError{Field: "event.source.host", Message: "required"} + } + + return nil +} + +func IsValidationError(err error) bool { + var ve ValidationError + return errors.As(err, &ve) +} diff --git a/internal/event/validate_test.go b/internal/event/validate_test.go new file mode 100644 index 0000000..2da9317 --- /dev/null +++ b/internal/event/validate_test.go @@ -0,0 +1,81 @@ +package event + +import ( + "encoding/json" + "testing" +) + +func TestValidate_ValidEvent(t *testing.T) { + raw := `{ + "schema": {"name": "agentmon.event", "version": 1}, + "event": { + "id": "e1", + "type": "run.start", + "ts": "2026-01-17T00:00:00Z", + "source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"} + } + }` + var m map[string]any + _ = json.Unmarshal([]byte(raw), &m) + + err := Validate(m) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestValidate_MissingEventID(t *testing.T) { + raw := `{ + "schema": {"name": "agentmon.event", "version": 1}, + "event": { + "type": "run.start", + "ts": "2026-01-17T00:00:00Z", + "source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"} + } + }` + var m map[string]any + _ = json.Unmarshal([]byte(raw), &m) + + err := Validate(m) + if err == nil { + t.Fatal("expected error for missing event.id") + } +} + +func TestValidate_InvalidType(t *testing.T) { + raw := `{ + "schema": {"name": "agentmon.event", "version": 1}, + "event": { + "id": "e1", + "type": "invalid.type", + "ts": "2026-01-17T00:00:00Z", + "source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"} + } + }` + var m map[string]any + _ = json.Unmarshal([]byte(raw), &m) + + err := Validate(m) + if err == nil { + t.Fatal("expected error for invalid type") + } +} + +func TestValidate_WrongSchemaName(t *testing.T) { + raw := `{ + "schema": {"name": "wrong.schema", "version": 1}, + "event": { + "id": "e1", + "type": "run.start", + "ts": "2026-01-17T00:00:00Z", + "source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"} + } + }` + var m map[string]any + _ = json.Unmarshal([]byte(raw), &m) + + err := Validate(m) + if err == nil { + t.Fatal("expected error for wrong schema name") + } +}