A community based topic aggregation platform built on atproto

refactor(lexicon): migrate post namespace to social.coves.community.post

Migrates lexicon schemas from social.coves.post.* to social.coves.community.post.*
to better reflect atProto architecture where posts are records in community repositories.

Changes:
- Move post schemas to social.coves.community.post namespace
- Update cross-references in feed/defs.json and embed/post.json
- Update validation tool to handle defs-only files (skip #main validation)
- Add skip logic for *.defs files in lexicon tests
- Remove old social.coves.post/* schemas

This aligns with atProto best practices for community-based content organization.

Changed files
+799 -29
cmd
validate-lexicon
internal
atproto
lexicon
validation
tests
+17 -10
cmd/validate-lexicon/main.go
···
}
for i, schemaID := range schemaIDs {
if _, err := catalog.Resolve(schemaID); err != nil {
validationErrors = append(validationErrors, fmt.Sprintf("Failed to resolve schema %s (from %s): %v", schemaID, schemaFiles[i], err))
} else if verbose {
···
"social.coves.richtext.facet#spoiler",
// Post types and views
-
"social.coves.post.get#postView",
-
"social.coves.post.get#authorView",
-
"social.coves.post.get#communityRef",
-
"social.coves.post.get#imageView",
-
"social.coves.post.get#videoView",
-
"social.coves.post.get#externalView",
-
"social.coves.post.get#postStats",
-
"social.coves.post.get#viewerState",
-
// Post record types
-
"social.coves.post.record#originalAuthor",
// Actor definitions
"social.coves.actor.profile#geoLocation",
···
}
for i, schemaID := range schemaIDs {
+
// Skip validation for definition-only files (*.defs) - they don't need a "main" section
+
// These files only contain shared type definitions referenced by other schemas
+
if strings.HasSuffix(schemaID, ".defs") {
+
if verbose {
+
fmt.Printf(" ⏭️ %s (defs-only file, skipping main validation)\n", schemaID)
+
}
+
continue
+
}
+
if _, err := catalog.Resolve(schemaID); err != nil {
validationErrors = append(validationErrors, fmt.Sprintf("Failed to resolve schema %s (from %s): %v", schemaID, schemaFiles[i], err))
} else if verbose {
···
"social.coves.richtext.facet#spoiler",
// Post types and views
+
"social.coves.community.post.get#postView",
+
"social.coves.community.post.get#authorView",
+
"social.coves.community.post.get#communityRef",
+
"social.coves.community.post.get#postStats",
+
"social.coves.community.post.get#viewerState",
+
"social.coves.community.post.get#notFoundPost",
+
"social.coves.community.post.get#blockedPost",
+
// Post record types (removed - no longer exists in new structure)
// Actor definitions
"social.coves.actor.profile#geoLocation",
+100
internal/atproto/lexicon/social/coves/community/post.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post",
+
"defs": {
+
"main": {
+
"type": "record",
+
"description": "A post in a Coves community. Posts live in community repositories and persist independently of the author.",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": ["community", "author", "createdAt"],
+
"properties": {
+
"community": {
+
"type": "string",
+
"format": "at-identifier",
+
"description": "DID or handle of the community this was posted to"
+
},
+
"author": {
+
"type": "string",
+
"format": "did",
+
"description": "DID of the user who created this post"
+
},
+
"title": {
+
"type": "string",
+
"maxGraphemes": 300,
+
"maxLength": 3000,
+
"description": "Post title (optional for media-only posts)"
+
},
+
"content": {
+
"type": "string",
+
"maxGraphemes": 10000,
+
"maxLength": 100000,
+
"description": "Post content - supports rich text via facets"
+
},
+
"facets": {
+
"type": "array",
+
"description": "Annotations for rich text (mentions, links, tags)",
+
"items": {
+
"type": "ref",
+
"ref": "social.coves.richtext.facet"
+
}
+
},
+
"embed": {
+
"type": "union",
+
"description": "Embedded media, external links, or quoted posts",
+
"refs": [
+
"social.coves.embed.images",
+
"social.coves.embed.video",
+
"social.coves.embed.external",
+
"social.coves.embed.post"
+
]
+
},
+
"langs": {
+
"type": "array",
+
"description": "Languages used in the post content (ISO 639-1)",
+
"maxLength": 3,
+
"items": {
+
"type": "string",
+
"format": "language"
+
}
+
},
+
"labels": {
+
"type": "ref",
+
"ref": "com.atproto.label.defs#selfLabels",
+
"description": "Self-applied content labels (NSFW, spoilers, etc.)"
+
},
+
"tags": {
+
"type": "array",
+
"description": "User-applied topic tags",
+
"maxLength": 8,
+
"items": {
+
"type": "string",
+
"maxLength": 64,
+
"maxGraphemes": 64
+
}
+
},
+
"crosspostOf": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef",
+
"description": "If this is a crosspost, strong reference to the immediate parent post"
+
},
+
"crosspostChain": {
+
"type": "array",
+
"description": "Full chain of crossposts with version pinning. First element is original, last is immediate parent.",
+
"maxLength": 25,
+
"items": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef"
+
}
+
},
+
"createdAt": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Timestamp of post creation"
+
}
+
}
+
}
+
}
+
}
+
}
+119
internal/atproto/lexicon/social/coves/community/post/create.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post.create",
+
"defs": {
+
"main": {
+
"type": "procedure",
+
"description": "Create a new post in a community",
+
"input": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["community"],
+
"properties": {
+
"community": {
+
"type": "string",
+
"format": "at-identifier",
+
"description": "DID or handle of the community to post in"
+
},
+
"title": {
+
"type": "string",
+
"maxGraphemes": 300,
+
"maxLength": 3000,
+
"description": "Post title (optional for media-only posts)"
+
},
+
"content": {
+
"type": "string",
+
"maxGraphemes": 10000,
+
"maxLength": 100000,
+
"description": "Post content - supports rich text via facets"
+
},
+
"facets": {
+
"type": "array",
+
"description": "Annotations for rich text (mentions, links, tags)",
+
"items": {
+
"type": "ref",
+
"ref": "social.coves.richtext.facet"
+
}
+
},
+
"embed": {
+
"type": "union",
+
"description": "Embedded media, external links, or quoted posts",
+
"refs": [
+
"social.coves.embed.images",
+
"social.coves.embed.video",
+
"social.coves.embed.external",
+
"social.coves.embed.post"
+
]
+
},
+
"langs": {
+
"type": "array",
+
"description": "Languages used in the post content (ISO 639-1)",
+
"maxLength": 3,
+
"items": {
+
"type": "string",
+
"format": "language"
+
}
+
},
+
"labels": {
+
"type": "ref",
+
"ref": "com.atproto.label.defs#selfLabels",
+
"description": "Self-applied content labels (NSFW, spoilers, etc.)"
+
},
+
"tags": {
+
"type": "array",
+
"description": "User-applied topic tags",
+
"maxLength": 8,
+
"items": {
+
"type": "string",
+
"maxLength": 64,
+
"maxGraphemes": 64
+
}
+
}
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["uri", "cid"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "AT-URI of the created post"
+
},
+
"cid": {
+
"type": "string",
+
"format": "cid",
+
"description": "CID of the created post"
+
}
+
}
+
}
+
},
+
"errors": [
+
{
+
"name": "CommunityNotFound",
+
"description": "Community not found"
+
},
+
{
+
"name": "NotAuthorized",
+
"description": "User is not authorized to post in this community"
+
},
+
{
+
"name": "Banned",
+
"description": "User is banned from this community"
+
},
+
{
+
"name": "InvalidContent",
+
"description": "Post content violates community rules"
+
},
+
{
+
"name": "ContentRuleViolation",
+
"description": "Post violates community content rules (e.g., embeds not allowed, text too short)"
+
}
+
]
+
}
+
}
+
}
+41
internal/atproto/lexicon/social/coves/community/post/delete.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post.delete",
+
"defs": {
+
"main": {
+
"type": "procedure",
+
"description": "Delete a post",
+
"input": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["uri"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "AT-URI of the post to delete"
+
}
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"properties": {}
+
}
+
},
+
"errors": [
+
{
+
"name": "PostNotFound",
+
"description": "Post not found"
+
},
+
{
+
"name": "NotAuthorized",
+
"description": "User is not authorized to delete this post"
+
}
+
]
+
}
+
}
+
}
+294
internal/atproto/lexicon/social/coves/community/post/get.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post.get",
+
"defs": {
+
"main": {
+
"type": "query",
+
"description": "Get posts by AT-URI. Supports batch fetching for feed hydration. Returns posts in same order as input URIs.",
+
"parameters": {
+
"type": "params",
+
"required": ["uris"],
+
"properties": {
+
"uris": {
+
"type": "array",
+
"description": "List of post AT-URIs to fetch (max 25)",
+
"items": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"maxLength": 25,
+
"minLength": 1
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["posts"],
+
"properties": {
+
"posts": {
+
"type": "array",
+
"description": "Array of post views. May include notFound/blocked entries for missing posts.",
+
"items": {
+
"type": "union",
+
"refs": ["#postView", "#notFoundPost", "#blockedPost"]
+
}
+
}
+
}
+
}
+
},
+
"errors": [
+
{"name": "InvalidRequest", "description": "Invalid URI format or empty array"}
+
]
+
},
+
"postView": {
+
"type": "object",
+
"required": ["uri", "cid", "author", "record", "community", "createdAt", "indexedAt"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"cid": {
+
"type": "string",
+
"format": "cid"
+
},
+
"author": {
+
"type": "ref",
+
"ref": "#authorView"
+
},
+
"record": {
+
"type": "unknown",
+
"description": "The actual post record (text, image, video, etc.)"
+
},
+
"community": {
+
"type": "ref",
+
"ref": "#communityRef"
+
},
+
"title": {
+
"type": "string"
+
},
+
"text": {
+
"type": "string"
+
},
+
"textFacets": {
+
"type": "array",
+
"items": {
+
"type": "ref",
+
"ref": "social.coves.richtext.facet"
+
}
+
},
+
"embed": {
+
"type": "union",
+
"description": "Embedded content (images, video, link preview, or quoted post)",
+
"refs": [
+
"social.coves.embed.images#view",
+
"social.coves.embed.video#view",
+
"social.coves.embed.external#view",
+
"social.coves.embed.record#view",
+
"social.coves.embed.recordWithMedia#view"
+
]
+
},
+
"language": {
+
"type": "string",
+
"format": "language"
+
},
+
"createdAt": {
+
"type": "string",
+
"format": "datetime"
+
},
+
"editedAt": {
+
"type": "string",
+
"format": "datetime"
+
},
+
"indexedAt": {
+
"type": "string",
+
"format": "datetime",
+
"description": "When this post was indexed by the AppView"
+
},
+
"stats": {
+
"type": "ref",
+
"ref": "#postStats"
+
},
+
"viewer": {
+
"type": "ref",
+
"ref": "#viewerState"
+
}
+
}
+
},
+
"authorView": {
+
"type": "object",
+
"required": ["did", "handle"],
+
"properties": {
+
"did": {
+
"type": "string",
+
"format": "did"
+
},
+
"handle": {
+
"type": "string",
+
"format": "handle"
+
},
+
"displayName": {
+
"type": "string"
+
},
+
"avatar": {
+
"type": "string",
+
"format": "uri"
+
},
+
"reputation": {
+
"type": "integer",
+
"description": "Author's reputation in the community"
+
}
+
}
+
},
+
"communityRef": {
+
"type": "object",
+
"required": ["did", "name"],
+
"properties": {
+
"did": {
+
"type": "string",
+
"format": "did"
+
},
+
"name": {
+
"type": "string"
+
},
+
"avatar": {
+
"type": "string",
+
"format": "uri"
+
}
+
}
+
},
+
"notFoundPost": {
+
"type": "object",
+
"description": "Post was not found (deleted, never indexed, or invalid URI)",
+
"required": ["uri", "notFound"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"notFound": {
+
"type": "boolean",
+
"const": true
+
}
+
}
+
},
+
"blockedPost": {
+
"type": "object",
+
"description": "Post is blocked due to viewer blocking author/community, or community moderation",
+
"required": ["uri", "blocked"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"blocked": {
+
"type": "boolean",
+
"const": true
+
},
+
"blockedBy": {
+
"type": "string",
+
"enum": ["author", "community", "moderator"],
+
"description": "What caused the block: viewer blocked author, viewer blocked community, or post was removed by moderators"
+
},
+
"author": {
+
"type": "ref",
+
"ref": "#blockedAuthor"
+
},
+
"community": {
+
"type": "ref",
+
"ref": "#blockedCommunity"
+
}
+
}
+
},
+
"blockedAuthor": {
+
"type": "object",
+
"description": "Minimal author info for blocked posts",
+
"required": ["did"],
+
"properties": {
+
"did": {
+
"type": "string",
+
"format": "did"
+
}
+
}
+
},
+
"blockedCommunity": {
+
"type": "object",
+
"description": "Minimal community info for blocked posts",
+
"required": ["did"],
+
"properties": {
+
"did": {
+
"type": "string",
+
"format": "did"
+
},
+
"name": {
+
"type": "string"
+
}
+
}
+
},
+
"postStats": {
+
"type": "object",
+
"required": ["upvotes", "downvotes", "score", "commentCount"],
+
"properties": {
+
"upvotes": {
+
"type": "integer",
+
"minimum": 0
+
},
+
"downvotes": {
+
"type": "integer",
+
"minimum": 0
+
},
+
"score": {
+
"type": "integer",
+
"description": "Calculated score (upvotes - downvotes)"
+
},
+
"commentCount": {
+
"type": "integer",
+
"minimum": 0
+
},
+
"shareCount": {
+
"type": "integer",
+
"minimum": 0
+
},
+
"tagCounts": {
+
"type": "object",
+
"description": "Aggregate counts of tags applied by community members",
+
"additionalProperties": {
+
"type": "integer",
+
"minimum": 0
+
}
+
}
+
}
+
},
+
"viewerState": {
+
"type": "object",
+
"properties": {
+
"vote": {
+
"type": "string",
+
"enum": ["up", "down"],
+
"description": "Viewer's vote on this post"
+
},
+
"voteUri": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"saved": {
+
"type": "boolean"
+
},
+
"savedUri": {
+
"type": "string",
+
"format": "at-uri"
+
},
+
"tags": {
+
"type": "array",
+
"description": "Tags applied by the viewer to this post",
+
"items": {
+
"type": "string",
+
"maxLength": 32
+
}
+
}
+
}
+
}
+
}
+
}
+80
internal/atproto/lexicon/social/coves/community/post/search.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post.search",
+
"defs": {
+
"main": {
+
"type": "query",
+
"description": "Search for posts",
+
"parameters": {
+
"type": "params",
+
"required": ["q"],
+
"properties": {
+
"q": {
+
"type": "string",
+
"description": "Search query"
+
},
+
"community": {
+
"type": "string",
+
"format": "at-identifier",
+
"description": "Filter by specific community"
+
},
+
"author": {
+
"type": "string",
+
"format": "at-identifier",
+
"description": "Filter by author"
+
},
+
"type": {
+
"type": "string",
+
"enum": ["text", "image", "video", "article", "microblog"],
+
"description": "Filter by post type"
+
},
+
"tags": {
+
"type": "array",
+
"items": {
+
"type": "string"
+
},
+
"description": "Filter by tags"
+
},
+
"sort": {
+
"type": "string",
+
"enum": ["relevance", "new", "top"],
+
"default": "relevance"
+
},
+
"timeframe": {
+
"type": "string",
+
"enum": ["hour", "day", "week", "month", "year", "all"],
+
"default": "all"
+
},
+
"limit": {
+
"type": "integer",
+
"minimum": 1,
+
"maximum": 100,
+
"default": 50
+
},
+
"cursor": {
+
"type": "string"
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["posts"],
+
"properties": {
+
"posts": {
+
"type": "array",
+
"items": {
+
"type": "ref",
+
"ref": "social.coves.feed.defs#feedViewPost"
+
}
+
},
+
"cursor": {
+
"type": "string"
+
}
+
}
+
}
+
}
+
}
+
}
+
}
+119
internal/atproto/lexicon/social/coves/community/post/update.json
···
···
+
{
+
"lexicon": 1,
+
"id": "social.coves.community.post.update",
+
"defs": {
+
"main": {
+
"type": "procedure",
+
"description": "Update an existing post",
+
"input": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["uri"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "AT-URI of the post to update"
+
},
+
"title": {
+
"type": "string",
+
"maxGraphemes": 300,
+
"maxLength": 3000,
+
"description": "Updated title"
+
},
+
"content": {
+
"type": "string",
+
"maxLength": 50000,
+
"description": "Updated content - main text for text posts, description for media, etc."
+
},
+
"facets": {
+
"type": "array",
+
"description": "Updated rich text annotations for content",
+
"items": {
+
"type": "ref",
+
"ref": "social.coves.richtext.facet"
+
}
+
},
+
"embed": {
+
"type": "union",
+
"description": "Updated embedded content (note: changing embed type may be restricted)",
+
"refs": [
+
"social.coves.embed.images",
+
"social.coves.embed.video",
+
"social.coves.embed.external",
+
"social.coves.embed.post"
+
]
+
},
+
"labels": {
+
"type": "ref",
+
"ref": "com.atproto.label.defs#selfLabels",
+
"description": "Updated self-applied content labels"
+
},
+
"langs": {
+
"type": "array",
+
"description": "Updated languages (ISO 639-1)",
+
"maxLength": 3,
+
"items": {
+
"type": "string",
+
"format": "language"
+
}
+
},
+
"tags": {
+
"type": "array",
+
"description": "Updated topic tags",
+
"maxLength": 8,
+
"items": {
+
"type": "string",
+
"maxLength": 64,
+
"maxGraphemes": 64
+
}
+
},
+
"editNote": {
+
"type": "string",
+
"maxLength": 300,
+
"description": "Optional note explaining the edit"
+
}
+
}
+
}
+
},
+
"output": {
+
"encoding": "application/json",
+
"schema": {
+
"type": "object",
+
"required": ["uri", "cid"],
+
"properties": {
+
"uri": {
+
"type": "string",
+
"format": "at-uri",
+
"description": "AT-URI of the updated post"
+
},
+
"cid": {
+
"type": "string",
+
"format": "cid",
+
"description": "New CID of the updated post"
+
}
+
}
+
}
+
},
+
"errors": [
+
{
+
"name": "PostNotFound",
+
"description": "Post not found"
+
},
+
{
+
"name": "NotAuthorized",
+
"description": "User is not authorized to edit this post"
+
},
+
{
+
"name": "EditWindowExpired",
+
"description": "Edit window has expired (posts can only be edited within 24 hours)"
+
},
+
{
+
"name": "InvalidUpdate",
+
"description": "Invalid update operation (e.g., changing post type)"
+
}
+
]
+
}
+
}
+
}
+6 -6
internal/atproto/lexicon/social/coves/embed/post.json
···
"defs": {
"main": {
"type": "object",
-
"description": "Embedded reference to another post",
-
"required": ["uri"],
"properties": {
-
"uri": {
-
"type": "string",
-
"format": "at-uri",
-
"description": "AT-URI of the post being embedded"
}
}
}
···
"defs": {
"main": {
"type": "object",
+
"description": "Embedded reference to another post (quoted post)",
+
"required": ["post"],
"properties": {
+
"post": {
+
"type": "ref",
+
"ref": "com.atproto.repo.strongRef",
+
"description": "Strong reference to the embedded post (includes URI and CID)"
}
}
}
+3 -3
internal/atproto/lexicon/social/coves/feed/defs.json
···
"properties": {
"post": {
"type": "ref",
-
"ref": "social.coves.post.get#postView"
},
"reason": {
"type": "union",
···
"properties": {
"by": {
"type": "ref",
-
"ref": "social.coves.post.get#authorView"
},
"indexedAt": {
"type": "string",
···
"properties": {
"community": {
"type": "ref",
-
"ref": "social.coves.post.get#communityRef"
}
}
},
···
"properties": {
"post": {
"type": "ref",
+
"ref": "social.coves.community.post.get#postView"
},
"reason": {
"type": "union",
···
"properties": {
"by": {
"type": "ref",
+
"ref": "social.coves.community.post.get#authorView"
},
"indexedAt": {
"type": "string",
···
"properties": {
"community": {
"type": "ref",
+
"ref": "social.coves.community.post.get#communityRef"
}
}
},
+3 -3
internal/validation/lexicon.go
···
// ValidatePost validates a post record
func (v *LexiconValidator) ValidatePost(post map[string]interface{}) error {
-
return v.ValidateRecord(post, "social.coves.post.record")
}
// ValidateComment validates a comment record
func (v *LexiconValidator) ValidateComment(comment map[string]interface{}) error {
-
return v.ValidateRecord(comment, "social.coves.interaction.comment")
}
// ValidateVote validates a vote record
···
return v.ValidateRecord(action, fmt.Sprintf("social.coves.moderation.%s", actionType))
}
-
// ResolveReference resolves a schema reference (e.g., "social.coves.post.get#postView")
func (v *LexiconValidator) ResolveReference(ref string) (interface{}, error) {
return v.catalog.Resolve(ref)
}
···
// ValidatePost validates a post record
func (v *LexiconValidator) ValidatePost(post map[string]interface{}) error {
+
return v.ValidateRecord(post, "social.coves.community.post")
}
// ValidateComment validates a comment record
func (v *LexiconValidator) ValidateComment(comment map[string]interface{}) error {
+
return v.ValidateRecord(comment, "social.coves.feed.comment")
}
// ValidateVote validates a vote record
···
return v.ValidateRecord(action, fmt.Sprintf("social.coves.moderation.%s", actionType))
}
+
// ResolveReference resolves a schema reference (e.g., "social.coves.community.post.get#postView")
func (v *LexiconValidator) ResolveReference(ref string) (interface{}, error) {
return v.catalog.Resolve(ref)
}
+7 -3
internal/validation/lexicon_test.go
···
// Valid post
validPost := map[string]interface{}{
-
"$type": "social.coves.post.record",
"community": "did:plc:test123",
"author": "did:plc:author123",
"title": "Test Post",
···
// Invalid post - missing required field (author)
invalidPost := map[string]interface{}{
-
"$type": "social.coves.post.record",
"community": "did:plc:test123",
// Missing required "author" field
"title": "Test Post",
···
// Test with JSON string
jsonString := `{
"$type": "social.coves.interaction.vote",
-
"subject": "at://did:plc:test/social.coves.post.text/abc123",
"createdAt": "2024-01-01T00:00:00Z"
}`
···
// Valid post
validPost := map[string]interface{}{
+
"$type": "social.coves.community.post",
"community": "did:plc:test123",
"author": "did:plc:author123",
"title": "Test Post",
···
// Invalid post - missing required field (author)
invalidPost := map[string]interface{}{
+
"$type": "social.coves.community.post",
"community": "did:plc:test123",
// Missing required "author" field
"title": "Test Post",
···
// Test with JSON string
jsonString := `{
"$type": "social.coves.interaction.vote",
+
"subject": {
+
"uri": "at://did:plc:test/social.coves.community.post/abc123",
+
"cid": "bafyreigj3fwnwjuzr35k2kuzmb5dixxczrzjhqkr5srlqplsh6gq3bj3si"
+
},
+
"direction": "up",
"createdAt": "2024-01-01T00:00:00Z"
}`
+10 -4
tests/lexicon_validation_test.go
···
schemaID := strings.ReplaceAll(relPath, string(filepath.Separator), ".")
t.Run(schemaID, func(t *testing.T) {
if _, resolveErr := catalog.Resolve(schemaID); resolveErr != nil {
t.Errorf("Failed to resolve schema %s: %v", schemaID, resolveErr)
}
···
},
{
name: "Valid post record",
-
recordType: "social.coves.post.record",
recordData: map[string]interface{}{
-
"$type": "social.coves.post.record",
"community": "did:plc:programming123",
"author": "did:plc:testauthor123",
"title": "Test Post",
···
},
{
name: "Invalid post record - missing required field",
-
recordType: "social.coves.post.record",
recordData: map[string]interface{}{
-
"$type": "social.coves.post.record",
"community": "did:plc:programming123",
// Missing required "author" field
"title": "Test Post",
···
schemaID := strings.ReplaceAll(relPath, string(filepath.Separator), ".")
t.Run(schemaID, func(t *testing.T) {
+
// Skip validation for definition-only files (*.defs) - they don't need a "main" section
+
// These files only contain shared type definitions referenced by other schemas
+
if strings.HasSuffix(schemaID, ".defs") {
+
t.Skip("Skipping defs-only file (no main section required)")
+
}
+
if _, resolveErr := catalog.Resolve(schemaID); resolveErr != nil {
t.Errorf("Failed to resolve schema %s: %v", schemaID, resolveErr)
}
···
},
{
name: "Valid post record",
+
recordType: "social.coves.community.post",
recordData: map[string]interface{}{
+
"$type": "social.coves.community.post",
"community": "did:plc:programming123",
"author": "did:plc:testauthor123",
"title": "Test Post",
···
},
{
name: "Invalid post record - missing required field",
+
recordType: "social.coves.community.post",
recordData: map[string]interface{}{
+
"$type": "social.coves.community.post",
"community": "did:plc:programming123",
// Missing required "author" field
"title": "Test Post",