This repository has been archived on 2022-03-09. You can view files and clone it, but cannot push or open issues or pull requests.
snoo2nebby/vendor/github.com/turnage/redditproto/parser.go

282 lines
8.0 KiB
Go

// redditproto provides protobuffer definitions and JSON parsing utilities for
// Reddit data types.
//
// A note about JSON parsing utilities: These expect to receive JSON unmodified
// from when it is received as a response from a Reddit endpoint which claims to
// provide the named type. E.g. provide to "ParseComment" exactly the body of
// the response from a call to /by_id/{fullname_of_comment}.json.
package redditproto
import (
"encoding/json"
"fmt"
)
// Reddit's responses include a "kind" field that contains a string representing
// the type of the "data" field. These constants are derived from those values.
const (
listingKind = "Listing"
commentKind = "t1"
linkKind = "t3"
messageKind = "t4"
)
// ParseComment parses a JSON message expected to be a Comment.
func ParseComment(raw json.RawMessage) (*Comment, error) {
thing, err := handleThing(raw)
if err != nil {
return nil, err
}
comment, ok := thing.(*Comment)
if !ok {
return nil, fmt.Errorf("JSON message was not a comment")
}
return comment, nil
}
// ParseThread parses a JSON message expected to represent a Link comment page.
func ParseThread(raw json.RawMessage) (*Link, error) {
// The JSON message should be a top level array, holding the link in the
// first index of a Listing at the first index of the top level array,
// and holding all the comments in a Listing at the second index of the
// top level array. I don't know why it's done this way...
listings := []interface{}{
&redditThing{},
&redditThing{},
}
if err := json.Unmarshal(raw, &listings); err != nil {
return nil, err
}
if len(listings) != 2 {
return nil, fmt.Errorf("the top-level JSON message was corrupt")
}
rawLink := listings[0].(*redditThing)
rawComments := listings[1].(*redditThing)
linkThing, err := parseThing(rawLink)
if err != nil {
return nil, err
}
linkBuffer, ok := linkThing.(*listingBuffer)
if !ok {
return nil, fmt.Errorf("link JSON message was nonlisting")
}
if len(linkBuffer.links) != 1 {
return nil, fmt.Errorf("found an unexpected number of links")
}
link := linkBuffer.links[0]
commentsThing, err := parseThing(rawComments)
if err != nil {
return nil, err
}
commentsBuffer, ok := commentsThing.(*listingBuffer)
if !ok {
return nil, fmt.Errorf("comments JSON message was nonlisting")
}
link.Comments = commentsBuffer.comments
return link, nil
}
// ParseListing parses a JSON message expected to be a listing with any mix of
// Links, Comments, or Messages.
func ParseListing(raw json.RawMessage) (
[]*Link,
[]*Comment,
[]*Message,
error,
) {
thing, err := handleThing(raw)
if err != nil {
return nil, nil, nil, err
}
buffer, ok := thing.(*listingBuffer)
if !ok {
return nil, nil, nil, fmt.Errorf("JSON message was nonlisting")
}
return buffer.links, buffer.comments, buffer.messages, nil
}
// handleThing unmarshals Things and parses them.
func handleThing(raw json.RawMessage) (interface{}, error) {
thing, err := unmarshalThing(raw)
if err != nil {
return nil, err
}
return parseThing(thing)
}
// parseThing parses reddit Things and returns the protobuffer that represents
// the Thing according to its Kind.
func parseThing(thing *redditThing) (interface{}, error) {
switch thing.Kind {
case listingKind:
return unmarshalListing(thing.Data)
case commentKind:
return unmarshalComment(thing.Data)
case linkKind:
return unmarshalLink(thing.Data)
case messageKind:
return unmarshalMessage(thing.Data)
}
return nil, fmt.Errorf("Unrecognized message kind")
}
// unmarshalThing unmarshals a JSON message into a redditThing, but leaves the
// Data field as raw JSON so it can be unmarshalled according to the Kind field.
func unmarshalThing(raw json.RawMessage) (*redditThing, error) {
thing := &redditThing{}
return thing, json.Unmarshal(raw, thing)
}
// unmarshalListing unmarshals a JSON message into a listing, whose children are
// unmarshaled into redditThings and then parsed. A buffer is returned with a
// slice of comments, links, messages (etc; see the struct definition) contained
// in the listing; generally the caller should know what to expect.
func unmarshalListing(raw json.RawMessage) (*listingBuffer, error) {
listing := &redditListing{}
if err := json.Unmarshal(raw, listing); err != nil {
return nil, err
}
buffer := &listingBuffer{}
for _, childThing := range listing.Children {
childInterface, err := parseThing(childThing)
if err != nil {
return nil, err
}
if link, ok := childInterface.(*Link); ok {
buffer.links = append(buffer.links, link)
} else if comment, ok := childInterface.(*Comment); ok {
buffer.comments = append(buffer.comments, comment)
} else if message, ok := childInterface.(*Message); ok {
buffer.messages = append(buffer.messages, message)
} else {
return nil, fmt.Errorf("corrupted listing child")
}
}
return buffer, nil
}
// unmarshalComment unmarshals a JSON message into a Comment protobuffer, and
// recursively unmarshals the reply tree.
func unmarshalComment(raw json.RawMessage) (*Comment, error) {
buffer := &commentResponse{}
if err := json.Unmarshal(raw, buffer); err != nil {
return nil, err
}
replies, _, err := unmarshalReplyTree(buffer.Replies)
return &Comment{
ApprovedBy: buffer.ApprovedBy,
Author: buffer.Author,
AuthorFlairCssClass: buffer.AuthorFlairCssClass,
AuthorFlairText: buffer.AuthorFlairText,
BannedBy: buffer.BannedBy,
Body: buffer.Body,
BodyHtml: buffer.BodyHtml,
Gilded: buffer.Gilded,
LinkAuthor: buffer.LinkAuthor,
LinkUrl: buffer.LinkUrl,
NumReports: buffer.NumReports,
ParentId: buffer.ParentId,
Replies: replies,
Subreddit: buffer.Subreddit,
SubredditId: buffer.SubredditId,
Distinguished: buffer.Distinguished,
Created: buffer.Created,
CreatedUtc: buffer.CreatedUtc,
Ups: buffer.Ups,
Downs: buffer.Downs,
Likes: buffer.Likes,
Id: buffer.Id,
Name: buffer.Name,
}, err
}
// unmarshalReplyTree unmarshals the reply field of comments. Sometimes this
// field is a listing, sometimes it is a string. This function handles whatever
// it happens to be and returns a slice of the replies.
func unmarshalReplyTree(raw json.RawMessage) ([]*Comment, []*Message, error) {
repliesThing, err := unmarshalThing(raw)
if err != nil {
// When a supply tree is not included, the field is a string,
// which the JSON unmarshaller chokes on.
return nil, nil, nil
}
bufferInterface, err := parseThing(repliesThing)
if err != nil {
return nil, nil, err
}
buffer, ok := bufferInterface.(*listingBuffer)
if !ok {
return nil,
nil,
fmt.Errorf("listing buffer corrupted or mislabeled")
}
return buffer.comments, buffer.messages, nil
}
// unmarshalLink unmarshals a JSON message into a Link protobuffer.
func unmarshalLink(raw json.RawMessage) (*Link, error) {
link := &Link{}
if err := json.Unmarshal(raw, link); err != nil {
return nil, err
}
return link, nil
}
// unmarshalMessage unmarshals a JSON message into a Message protobuffer.
func unmarshalMessage(raw json.RawMessage) (*Message, error) {
buffer := &messageResponse{}
if err := json.Unmarshal(raw, buffer); err != nil {
return nil, err
}
_, replies, err := unmarshalReplyTree(buffer.Replies)
if err != nil {
return nil, err
}
return &Message{
Author: buffer.Author,
BodyHtml: buffer.BodyHtml,
Body: buffer.Body,
Context: buffer.Context,
FirstMessageName: buffer.FirstMessageName,
Likes: buffer.Likes,
LinkTitle: buffer.LinkTitle,
New: buffer.New,
ParentId: buffer.ParentId,
Subject: buffer.Subject,
Subreddit: buffer.Subreddit,
WasComment: buffer.WasComment,
Created: buffer.Created,
CreatedUtc: buffer.CreatedUtc,
Id: buffer.Id,
Name: buffer.Name,
Messages: replies,
}, nil
}