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.community.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}