···
// Only process .json files
if !info.IsDir() && filepath.Ext(path) == ".json" {
schemaFiles = append(schemaFiles, path)
// Convert file path to schema ID
// e.g., internal/atproto/lexicon/social/coves/actor/profile.json -> social.coves.actor.profile
-
relPath, _ := filepath.Rel(schemaPath, path)
schemaID := filepath.ToSlash(relPath)
schemaID = schemaID[:len(schemaID)-5] // Remove .json extension
schemaID = strings.ReplaceAll(schemaID, "/", ".")
···
return fmt.Errorf("error walking schema directory: %w", err)
···
return fmt.Errorf("error walking schema directory: %w", err)
···
// extractAllSchemaIDs walks the schema directory and returns all schema IDs
func extractAllSchemaIDs(schemaPath string) []string {
-
filepath.Walk(schemaPath, func(path string, info os.FileInfo, err error) error {
// Skip test-data directory
if info.IsDir() && info.Name() == "test-data" {
// Only process .json files
if !info.IsDir() && filepath.Ext(path) == ".json" {
// Convert file path to schema ID
-
relPath, _ := filepath.Rel(schemaPath, path)
schemaID := filepath.ToSlash(relPath)
schemaID = schemaID[:len(schemaID)-5] // Remove .json extension
schemaID = strings.ReplaceAll(schemaID, "/", ".")
// Only include record schemas (not procedures)
-
if strings.Contains(schemaID, ".record") ||
-
strings.Contains(schemaID, ".profile") ||
-
strings.Contains(schemaID, ".rules") ||
-
strings.Contains(schemaID, ".wiki") ||
-
strings.Contains(schemaID, ".subscription") ||
-
strings.Contains(schemaID, ".membership") ||
-
strings.Contains(schemaID, ".vote") ||
-
strings.Contains(schemaID, ".tag") ||
-
strings.Contains(schemaID, ".comment") ||
-
strings.Contains(schemaID, ".share") ||
-
strings.Contains(schemaID, ".tribunalVote") ||
-
strings.Contains(schemaID, ".ruleProposal") ||
-
strings.Contains(schemaID, ".ban") {
schemaIDs = append(schemaIDs, schemaID)
// validateTestData validates test JSON data files against their corresponding schemas
-
func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose bool, strict bool, allSchemas []string) error {
// Check if test data directory exists
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
return fmt.Errorf("test data path does not exist: %s", testDataPath)
···
if !info.IsDir() && filepath.Ext(path) == ".json" {
filename := filepath.Base(path)
isInvalidTest := strings.Contains(filename, "-invalid-")
fmt.Printf("\n Testing (expect failure): %s\n", filename)
···
validationErrors = append(validationErrors, fmt.Sprintf("Failed to open %s: %v", path, err))
-
data, err := io.ReadAll(file)
-
validationErrors = append(validationErrors, fmt.Sprintf("Failed to read %s: %v", path, err))
···
var recordData map[string]interface{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber() // This preserves numbers as json.Number instead of float64
-
if err := decoder.Decode(&recordData); err != nil {
-
validationErrors = append(validationErrors, fmt.Sprintf("Failed to parse JSON in %s: %v", path, err))
// Convert json.Number values to appropriate types
recordData = convertNumbers(recordData).(map[string]interface{})
···
-
err = lexicon.ValidateRecord(catalog, recordData, recordType, flags)
// This file should fail validation
-
fmt.Printf(" ✅ Correctly rejected invalid %s record: %v\n", recordType, err)
validationErrors = append(validationErrors, fmt.Sprintf("Invalid test file %s passed validation when it should have failed", path))
···
// This file should pass validation
-
validationErrors = append(validationErrors, fmt.Sprintf("Validation failed for %s (type: %s): %v", path, recordType, err))
-
fmt.Printf(" ❌ Failed: %v\n", err)
···
return fmt.Errorf("error walking test data directory: %w", err)
···
fmt.Printf("\n📋 Validation Summary:\n")
fmt.Printf(" Valid test files: %d/%d passed\n", validSuccessCount, validFiles)
fmt.Printf(" Invalid test files: %d/%d correctly rejected\n", invalidFailCount, invalidFiles)
if validSuccessCount == validFiles && invalidFailCount == invalidFiles {
fmt.Printf("\n ✅ All test files behaved as expected!\n")
// Show test coverage summary (only for valid files)
fmt.Printf("\n📊 Test Data Coverage Summary:\n")
fmt.Printf(" - Records with test data: %d types\n", len(testedTypes))
fmt.Printf(" - Valid test files: %d\n", validFiles)
fmt.Printf(" - Invalid test files: %d (for error validation)\n", invalidFiles)
fmt.Printf("\n Tested record types:\n")
for recordType := range testedTypes {
fmt.Printf(" ✓ %s\n", recordType)
fmt.Printf("\n ⚠️ Record types without test data:\n")
···
fmt.Println(" (None - full test coverage!)")
-
fmt.Printf("\n Coverage: %d/%d record types have test data (%.1f%%)\n",
-
len(testedTypes), len(allSchemas),
float64(len(testedTypes))/float64(len(allSchemas))*100)
···
"social.coves.richtext.facet#italic",
"social.coves.richtext.facet#strikethrough",
"social.coves.richtext.facet#spoiler",
"social.coves.post.get#postView",
"social.coves.post.get#authorView",
···
"social.coves.post.get#externalView",
"social.coves.post.get#postStats",
"social.coves.post.get#viewerState",
"social.coves.post.record#originalAuthor",
"social.coves.actor.profile#geoLocation",
"social.coves.community.rules#rule",
···
// Only process .json files
if !info.IsDir() && filepath.Ext(path) == ".json" {
schemaFiles = append(schemaFiles, path)
// Convert file path to schema ID
// e.g., internal/atproto/lexicon/social/coves/actor/profile.json -> social.coves.actor.profile
+
relPath, err := filepath.Rel(schemaPath, path)
+
return fmt.Errorf("failed to compute relative path: %w", err)
schemaID := filepath.ToSlash(relPath)
schemaID = schemaID[:len(schemaID)-5] // Remove .json extension
schemaID = strings.ReplaceAll(schemaID, "/", ".")
···
return fmt.Errorf("error walking schema directory: %w", err)
···
return fmt.Errorf("error walking schema directory: %w", err)
···
// extractAllSchemaIDs walks the schema directory and returns all schema IDs
func extractAllSchemaIDs(schemaPath string) []string {
+
if err := filepath.Walk(schemaPath, func(path string, info os.FileInfo, err error) error {
// Skip test-data directory
if info.IsDir() && info.Name() == "test-data" {
// Only process .json files
if !info.IsDir() && filepath.Ext(path) == ".json" {
// Convert file path to schema ID
+
relPath, err := filepath.Rel(schemaPath, path)
schemaID := filepath.ToSlash(relPath)
schemaID = schemaID[:len(schemaID)-5] // Remove .json extension
schemaID = strings.ReplaceAll(schemaID, "/", ".")
// Only include record schemas (not procedures)
+
if strings.Contains(schemaID, ".record") ||
+
strings.Contains(schemaID, ".profile") ||
+
strings.Contains(schemaID, ".rules") ||
+
strings.Contains(schemaID, ".wiki") ||
+
strings.Contains(schemaID, ".subscription") ||
+
strings.Contains(schemaID, ".membership") ||
+
strings.Contains(schemaID, ".vote") ||
+
strings.Contains(schemaID, ".tag") ||
+
strings.Contains(schemaID, ".comment") ||
+
strings.Contains(schemaID, ".share") ||
+
strings.Contains(schemaID, ".tribunalVote") ||
+
strings.Contains(schemaID, ".ruleProposal") ||
+
strings.Contains(schemaID, ".ban") {
schemaIDs = append(schemaIDs, schemaID)
+
log.Printf("Warning: failed to walk schema directory: %v", err)
// validateTestData validates test JSON data files against their corresponding schemas
+
func validateTestData(catalog *lexicon.BaseCatalog, testDataPath string, verbose, strict bool, allSchemas []string) error {
// Check if test data directory exists
if _, err := os.Stat(testDataPath); os.IsNotExist(err) {
return fmt.Errorf("test data path does not exist: %s", testDataPath)
···
if !info.IsDir() && filepath.Ext(path) == ".json" {
filename := filepath.Base(path)
isInvalidTest := strings.Contains(filename, "-invalid-")
fmt.Printf("\n Testing (expect failure): %s\n", filename)
···
validationErrors = append(validationErrors, fmt.Sprintf("Failed to open %s: %v", path, err))
+
if closeErr := file.Close(); closeErr != nil {
+
validationErrors = append(validationErrors, fmt.Sprintf("Failed to close %s: %v", path, closeErr))
+
data, readErr := io.ReadAll(file)
+
validationErrors = append(validationErrors, fmt.Sprintf("Failed to read %s: %v", path, readErr))
···
var recordData map[string]interface{}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber() // This preserves numbers as json.Number instead of float64
+
if decodeErr := decoder.Decode(&recordData); decodeErr != nil {
+
validationErrors = append(validationErrors, fmt.Sprintf("Failed to parse JSON in %s: %v", path, decodeErr))
// Convert json.Number values to appropriate types
recordData = convertNumbers(recordData).(map[string]interface{})
···
+
validateErr := lexicon.ValidateRecord(catalog, recordData, recordType, flags)
// This file should fail validation
+
if validateErr != nil {
+
fmt.Printf(" ✅ Correctly rejected invalid %s record: %v\n", recordType, validateErr)
validationErrors = append(validationErrors, fmt.Sprintf("Invalid test file %s passed validation when it should have failed", path))
···
// This file should pass validation
+
if validateErr != nil {
+
validationErrors = append(validationErrors, fmt.Sprintf("Validation failed for %s (type: %s): %v", path, recordType, validateErr))
+
fmt.Printf(" ❌ Failed: %v\n", validateErr)
···
return fmt.Errorf("error walking test data directory: %w", err)
···
fmt.Printf("\n📋 Validation Summary:\n")
fmt.Printf(" Valid test files: %d/%d passed\n", validSuccessCount, validFiles)
fmt.Printf(" Invalid test files: %d/%d correctly rejected\n", invalidFailCount, invalidFiles)
if validSuccessCount == validFiles && invalidFailCount == invalidFiles {
fmt.Printf("\n ✅ All test files behaved as expected!\n")
// Show test coverage summary (only for valid files)
fmt.Printf("\n📊 Test Data Coverage Summary:\n")
fmt.Printf(" - Records with test data: %d types\n", len(testedTypes))
fmt.Printf(" - Valid test files: %d\n", validFiles)
fmt.Printf(" - Invalid test files: %d (for error validation)\n", invalidFiles)
fmt.Printf("\n Tested record types:\n")
for recordType := range testedTypes {
fmt.Printf(" ✓ %s\n", recordType)
fmt.Printf("\n ⚠️ Record types without test data:\n")
···
fmt.Println(" (None - full test coverage!)")
+
fmt.Printf("\n Coverage: %d/%d record types have test data (%.1f%%)\n",
+
len(testedTypes), len(allSchemas),
float64(len(testedTypes))/float64(len(allSchemas))*100)
···
"social.coves.richtext.facet#italic",
"social.coves.richtext.facet#strikethrough",
"social.coves.richtext.facet#spoiler",
"social.coves.post.get#postView",
"social.coves.post.get#authorView",
···
"social.coves.post.get#externalView",
"social.coves.post.get#postStats",
"social.coves.post.get#viewerState",
"social.coves.post.record#originalAuthor",
"social.coves.actor.profile#geoLocation",
"social.coves.community.rules#rule",