A community based topic aggregation platform built on atproto
1package validation
2
3import (
4 "encoding/json"
5 "fmt"
6
7 lexicon "github.com/bluesky-social/indigo/atproto/lexicon"
8)
9
10// LexiconValidator provides a convenient interface for validating atproto records
11type LexiconValidator struct {
12 catalog *lexicon.BaseCatalog
13 flags lexicon.ValidateFlags
14}
15
16// NewLexiconValidator creates a new validator with the specified schema directory
17func NewLexiconValidator(schemaPath string, strict bool) (*LexiconValidator, error) {
18 catalog := lexicon.NewBaseCatalog()
19
20 if err := catalog.LoadDirectory(schemaPath); err != nil {
21 return nil, fmt.Errorf("failed to load lexicon schemas: %w", err)
22 }
23
24 flags := lexicon.ValidateFlags(0)
25 if strict {
26 flags |= lexicon.StrictRecursiveValidation
27 } else {
28 flags |= lexicon.AllowLenientDatetime
29 }
30
31 return &LexiconValidator{
32 catalog: &catalog,
33 flags: flags,
34 }, nil
35}
36
37// ValidateRecord validates a record against its schema
38func (v *LexiconValidator) ValidateRecord(recordData interface{}, recordType string) error {
39 // Convert to map if needed
40 var data map[string]interface{}
41
42 switch rd := recordData.(type) {
43 case map[string]interface{}:
44 data = rd
45 case []byte:
46 if err := json.Unmarshal(rd, &data); err != nil {
47 return fmt.Errorf("failed to parse JSON: %w", err)
48 }
49 case string:
50 if err := json.Unmarshal([]byte(rd), &data); err != nil {
51 return fmt.Errorf("failed to parse JSON: %w", err)
52 }
53 default:
54 // Try to marshal and unmarshal to convert struct to map
55 jsonBytes, err := json.Marshal(recordData)
56 if err != nil {
57 return fmt.Errorf("failed to convert record to JSON: %w", err)
58 }
59 if err := json.Unmarshal(jsonBytes, &data); err != nil {
60 return fmt.Errorf("failed to parse JSON: %w", err)
61 }
62 }
63
64 // Ensure $type field matches recordType
65 if typeField, ok := data["$type"].(string); ok && typeField != recordType {
66 return fmt.Errorf("$type field '%s' does not match expected type '%s'", typeField, recordType)
67 }
68
69 return lexicon.ValidateRecord(v.catalog, data, recordType, v.flags)
70}
71
72// ValidateActorProfile validates an actor profile record
73func (v *LexiconValidator) ValidateActorProfile(profile map[string]interface{}) error {
74 return v.ValidateRecord(profile, "social.coves.actor.profile")
75}
76
77// ValidateCommunityProfile validates a community profile record
78func (v *LexiconValidator) ValidateCommunityProfile(profile map[string]interface{}) error {
79 return v.ValidateRecord(profile, "social.coves.community.profile")
80}
81
82// ValidatePost validates a post record
83func (v *LexiconValidator) ValidatePost(post map[string]interface{}) error {
84 return v.ValidateRecord(post, "social.coves.community.post")
85}
86
87// ValidateComment validates a comment record
88func (v *LexiconValidator) ValidateComment(comment map[string]interface{}) error {
89 return v.ValidateRecord(comment, "social.coves.feed.comment")
90}
91
92// ValidateVote validates a vote record
93func (v *LexiconValidator) ValidateVote(vote map[string]interface{}) error {
94 return v.ValidateRecord(vote, "social.coves.feed.vote")
95}
96
97// ValidateModerationAction validates a moderation action (ban, tribunalVote, etc.)
98func (v *LexiconValidator) ValidateModerationAction(action map[string]interface{}, actionType string) error {
99 return v.ValidateRecord(action, fmt.Sprintf("social.coves.moderation.%s", actionType))
100}
101
102// ResolveReference resolves a schema reference (e.g., "social.coves.community.post.get#postView")
103func (v *LexiconValidator) ResolveReference(ref string) (interface{}, error) {
104 return v.catalog.Resolve(ref)
105}
106
107// GetCatalog returns the underlying lexicon catalog for advanced usage
108func (v *LexiconValidator) GetCatalog() *lexicon.BaseCatalog {
109 return v.catalog
110}