From e9747d95774666383b072adf3844e6b5be70772e Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Sun, 4 Jun 2017 14:48:21 -0700 Subject: [PATCH 1/7] Adds support for JSON Feed Version 1 JSON Feed Version 1 specification: https://jsonfeed.org/version/1 --- README.md | 46 +++++++++++++- doc.go | 9 ++- feed.go | 17 +++++ feed_test.go | 65 +++++++++++++++++++ json.go | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 json.go diff --git a/README.md b/README.md index e499be8..0df4fc0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ ## gorilla/feeds [![GoDoc](https://godoc.org/github.com/gorilla/feeds?status.svg)](https://godoc.org/github.com/gorilla/feeds) [![Build Status](https://travis-ci.org/gorilla/feeds.png?branch=master)](https://travis-ci.org/gorilla/feeds) -feeds is a web feed generator library for generating RSS and Atom feeds from Go +feeds is a web feed generator library for generating RSS, Atom and JSON feeds from Go applications. ### Goals * Provide a simple interface to create both Atom & RSS 2.0 feeds - * Full support for Atom and RSS2.0 spec elements + * Full support for Atom, RSS2.0, and JSON Feed Version 1 spec elements * Ability to modify particulars for each spec ### Usage @@ -64,7 +64,12 @@ func main() { log.Fatal(err) } - fmt.Println(atom, "\n", rss) + json, err := feed.ToJSON() + if err != nil { + log.Fatal(err) + } + + fmt.Println(atom, "\n", rss, "\n", json) } ``` @@ -136,5 +141,40 @@ Outputs: +{ + "version": "https://jsonfeed.org/version/1", + "title": "jmoiron.net blog", + "home_page_url": "http://jmoiron.net/blog", + "description": "discussion about tech, footie, photos", + "author": { + "name": "Jason Moiron" + }, + "items": [ + { + "id": "", + "url": "http://jmoiron.net/blog/limiting-concurrency-in-go/", + "title": "Limiting Concurrency in Go", + "summary": "A discussion on controlled parallelism in golang", + "date_published": "2013-01-16T03:22:24.530817846-05:00", + "author": { + "name": "Jason Moiron" + } + }, + { + "id": "", + "url": "http://jmoiron.net/blog/logicless-template-redux/", + "title": "Logic-less Template Redux", + "summary": "More thoughts on logicless templates", + "date_published": "2013-01-16T03:22:24.530817846-05:00" + }, + { + "id": "", + "url": "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/", + "title": "Idiomatic Code Reuse in Go", + "summary": "How to use interfaces \u003cem\u003eeffectively\u003c/em\u003e", + "date_published": "2013-01-16T03:22:24.530817846-05:00" + } + ] +} ``` diff --git a/doc.go b/doc.go index 5b005ea..bf7d15e 100644 --- a/doc.go +++ b/doc.go @@ -5,7 +5,7 @@ Installing go get github.com/gorilla/feeds -Feeds provides a simple, generic Feed interface with a generic Item object as well as RSS and Atom specific RssFeed and AtomFeed objects which allow access to all of each spec's defined elements. +Feeds provides a simple, generic Feed interface with a generic Item object as well as RSS, Atom and JSON Feed specific RssFeed, AtomFeed and JSONFeed objects which allow access to all of each spec's defined elements. Examples @@ -49,15 +49,17 @@ Create a Feed and some Items in that feed using the generic interfaces: }, } -From here, you can output Atom or RSS versions of this feed easily +From here, you can output Atom, RSS, or JSON Feed versions of this feed easily atom, err := feed.ToAtom() rss, err := feed.ToRss() + json, err := feed.ToJSON() You can also get access to the underlying objects that feeds uses to export its XML atomFeed := &Atom{feed}.AtomFeed() rssFeed := &Rss{feed}.RssFeed() + jsonFeed := &JSON{feed}.JSONFeed() From here, you can modify or add each syndication's specific fields before outputting @@ -65,6 +67,7 @@ From here, you can modify or add each syndication's specific fields before outpu atom, err := ToXML(atomFeed) rssFeed.Generator = "gorilla/feeds v1.0 (github.com/gorilla/feeds)" rss, err := ToXML(rssFeed) - + jsonFeed.NextUrl = "https://www.example.com/feed.json?page=2" + json, err := jsonFeed.ToJSON() */ package feeds diff --git a/feed.go b/feed.go index 6efce1d..70e0236 100644 --- a/feed.go +++ b/feed.go @@ -1,6 +1,7 @@ package feeds import ( + "encoding/json" "encoding/xml" "io" "time" @@ -105,3 +106,19 @@ func (f *Feed) ToRss() (string, error) { func (f *Feed) WriteRss(w io.Writer) error { return WriteXML(&Rss{f}, w) } + +// creates an JSON representation of this feed +func (f *Feed) ToJSON() (string, error) { + j := &JSON{f} + return j.ToJSON() +} + +// Writes an JSON representation of this feed to the writer. +func (f *Feed) WriteJSON(w io.Writer) error { + j := &JSON{f} + feed := j.JSONFeed() + + e := json.NewEncoder(w) + e.SetIndent("", " ") + return e.Encode(feed) +} diff --git a/feed_test.go b/feed_test.go index c3e31e7..e7ed38c 100644 --- a/feed_test.go +++ b/feed_test.go @@ -101,6 +101,56 @@ var rssOutput = ` ` +var jsonOutput = `{ + "version": "https://jsonfeed.org/version/1", + "title": "jmoiron.net blog", + "home_page_url": "http://jmoiron.net/blog", + "description": "discussion about tech, footie, photos", + "author": { + "name": "Jason Moiron" + }, + "items": [ + { + "id": "", + "url": "http://jmoiron.net/blog/limiting-concurrency-in-go/", + "title": "Limiting Concurrency in Go", + "summary": "A discussion on controlled parallelism in golang", + "date_published": "2013-01-16T21:52:35-05:00", + "author": { + "name": "Jason Moiron" + } + }, + { + "id": "", + "url": "http://jmoiron.net/blog/logicless-template-redux/", + "title": "Logic-less Template Redux", + "summary": "More thoughts on logicless templates", + "date_published": "2013-01-16T21:52:35-05:00" + }, + { + "id": "", + "url": "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/", + "title": "Idiomatic Code Reuse in Go", + "summary": "How to use interfaces \u003cem\u003eeffectively\u003c/em\u003e", + "date_published": "2013-01-16T21:52:35-05:00" + }, + { + "id": "", + "url": "http://example.com/RickRoll.mp3", + "title": "Never Gonna Give You Up Mp3", + "summary": "Never gonna give you up - Never gonna let you down.", + "date_published": "2013-01-16T21:52:35-05:00" + }, + { + "id": "", + "url": "http://example.com/strings", + "title": "String formatting in Go", + "summary": "How to use things like %s, %v, %d, etc.", + "date_published": "2013-01-16T21:52:35-05:00" + } + ] +}` + func TestFeed(t *testing.T) { now, err := time.Parse(time.RFC3339, "2013-01-16T21:52:35-05:00") if err != nil { @@ -180,4 +230,19 @@ func TestFeed(t *testing.T) { if got := buf.String(); got != rssOutput { t.Errorf("Rss not what was expected. Got:\n%s\n\nExpected:\n%s\n", got, rssOutput) } + + json, err := feed.ToJSON() + if err != nil { + t.Errorf("unexpected error encoding JSON: %v", err) + } + if json != jsonOutput { + t.Errorf("JSON not what was expected. Got:\n%s\n\nExpected:\n%s\n", json, jsonOutput) + } + buf.Reset() + if err := feed.WriteJSON(&buf); err != nil { + t.Errorf("unexpected error writing JSON: %v", err) + } + if got := buf.String(); got != jsonOutput+"\n" { //json.Encode appends a newline after the JSON output: https://github.com/golang/go/commit/6f25f1d4c901417af1da65e41992d71c30f64f8f#diff-50848cbd686f250623a2ef6ddb07e157 + t.Errorf("JSON not what was expected. Got:\n||%s||\n\nExpected:\n||%s||\n", got, jsonOutput) + } } diff --git a/json.go b/json.go new file mode 100644 index 0000000..3cd71d1 --- /dev/null +++ b/json.go @@ -0,0 +1,174 @@ +package feeds + +import ( + "encoding/json" + "time" +) + +const jsonFeedVersion = "https://jsonfeed.org/version/1" + +// JSONAuthor represents the author of the feed or of an individual item +// in the feed +type JSONAuthor struct { + Name string `json:"name,omitempty"` + Url string `json:"url,omitempty"` + Avatar string `json:"avatar,omitempty"` +} + +// JSONAttachment represents a related resource. Podcasts, for instance, would +// include an attachment that’s an audio or video file. +type JSONAttachment struct { + Url string `json:"url,omitempty"` + MIMEType string `json:"mime_type,omitempty"` + Title string `json:"title,omitempty"` + Size int32 `json:"size,omitempty"` + Duration time.Duration `json:"duration_in_seconds,omitempty"` +} + +// MarshalJSON implements the json.Marshaler interface. +// The Duration field is marshaled in seconds, all other fields are marshaled +// based upon the definitions in struct tags. +func (a *JSONAttachment) MarshalJSON() ([]byte, error) { + type EmbeddedJSONAttachment JSONAttachment + return json.Marshal(&struct { + Duration float64 `json:"duration_in_seconds,omitempty"` + *EmbeddedJSONAttachment + }{ + EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a), + Duration: a.Duration.Seconds(), + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// The Duration field is expected to be in seconds, all other field types +// match the struct definition. +func (a *JSONAttachment) UnmarshalJSON(data []byte) error { + type EmbeddedJSONAttachment JSONAttachment + var raw struct { + Duration float64 `json:"duration_in_seconds,omitempty"` + *EmbeddedJSONAttachment + } + raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a) + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + if raw.Duration > 0 { + nsec := int64(raw.Duration * float64(time.Second)) + raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec) + } + + return nil +} + +// JSONItem represents a single entry/post for the feed. +type JSONItem struct { + Id string `json:"id"` + Url string `json:"url,omitempty"` + ExternalUrl string `json:"external_url,omitempty"` + Title string `json:"title,omitempty"` + ContentHTML string `json:"content_html,omitempty"` + ContentText string `json:"content_text,omitempty"` + Summary string `json:"summary,omitempty"` + Image string `json:"image,omitempty"` + BannerImage string `json:"banner_,omitempty"` + PublishedDate *time.Time `json:"date_published,omitempty"` + ModifiedDate *time.Time `json:"date_modified,omitempty"` + Author *JSONAuthor `json:"author,omitempty"` + Tags []string `json:"tags,omitempty"` + Attachments []JSONAttachment `json:"attachments,omitempty"` +} + +// JSONHub describes an endpoint that can be used to subscribe to real-time +// notifications from the publisher of this feed. +type JSONHub struct { + Type string `json:"type"` + Url string `json:"url"` +} + +// JSONFeed represents a syndication feed in the JSON Feed Version 1 format. +// Matching the specification found here: https://jsonfeed.org/version/1. +type JSONFeed struct { + Version string `json:"version"` + Title string `json:"title"` + HomePageUrl string `json:"home_page_url,omitempty"` + FeedUrl string `json:"feed_url,omitempty"` + Description string `json:"description,omitempty"` + UserComment string `json:"user_comment,omitempty"` + NextUrl string `json:"next_url,omitempty"` + Icon string `json:"icon,omitempty"` + Favicon string `json:"favicon,omitempty"` + Author *JSONAuthor `json:"author,omitempty"` + Expired *bool `json:"expired,omitempty"` + Hubs []*JSONItem `json:"hubs,omitempty"` + Items []*JSONItem `json:"items,omitempty"` +} + +type JSON struct { + *Feed +} + +func (f *JSON) ToJSON() (string, error) { + return f.JSONFeed().ToJSON() +} + +func (f *JSONFeed) ToJSON() (string, error) { + data, err := json.MarshalIndent(f, "", " ") + if err != nil { + return "", err + } + + return string(data), nil +} + +// JSONFeed creates a new JSONFeed with a generic Feed struct's data +func (a *JSON) JSONFeed() *JSONFeed { + feed := &JSONFeed{ + Version: jsonFeedVersion, + Title: a.Title, + Description: a.Description, + } + + if a.Link != nil { + feed.HomePageUrl = a.Link.Href + } + if a.Author != nil { + feed.Author = &JSONAuthor{ + Name: a.Author.Name, + } + } + for _, e := range a.Items { + feed.Items = append(feed.Items, newJSONItem(e)) + } + return feed +} + +func newJSONItem(i *Item) *JSONItem { + item := &JSONItem{ + Id: i.Id, + Title: i.Title, + Summary: i.Description, + } + + if i.Link != nil { + item.Url = i.Link.Href + } + if i.Source != nil { + item.ExternalUrl = i.Source.Href + } + if i.Author != nil { + item.Author = &JSONAuthor{ + Name: i.Author.Name, + } + } + if !i.Created.IsZero() { + item.PublishedDate = &i.Created + } + if !i.Updated.IsZero() { + item.ModifiedDate = &i.Created + } + + return item +} From 62f87e75325723b8a2bdfac3db77ac89cb450a16 Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Sun, 4 Jun 2017 17:50:49 -0700 Subject: [PATCH 2/7] Removes go versions 1.3, 1.4 and 1.5 from the Travis CI build --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c063c31..fbc847f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ language: go sudo: false go: - - 1.3 - - 1.4 - - 1.5 - 1.7 - 1.8 - tip From 42dd4703797fc06b359c0038a861e0d129076773 Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Sun, 4 Jun 2017 21:03:50 -0700 Subject: [PATCH 3/7] Adds links to the specifications for each syndication feed format Change requested by @elithrar to add a link to the JSON Feed as it is a new specifications and package users are less likely to be familiar with it. It seemed appropriate at that point to link to all three specifications currently supported. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0df4fc0..21f6cf8 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,13 @@ applications. ### Goals * Provide a simple interface to create both Atom & RSS 2.0 feeds - * Full support for Atom, RSS2.0, and JSON Feed Version 1 spec elements + * Full support for [Atom][atom], [RSS 2.0][rss], and [JSON Feed Version 1][jsonfeed] spec elements * Ability to modify particulars for each spec +[atom]: https://tools.ietf.org/html/rfc4287 +[rss]: http://www.rssboard.org/rss-specification +[jsonfeed]: https://jsonfeed.org/version/1 + ### Usage ```go From a6111c9ae6392b3449aa6cdeee072e509cf5b38f Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Sun, 4 Jun 2017 21:10:05 -0700 Subject: [PATCH 4/7] Updates the Travis CI testing matrix Change requested by @elithrar to improve the Go versions tested against. 1.x will always point to the latest version. Master points to the latest rev, which is useful for identifying breaking changes in upcoming versions. --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fbc847f..3ec3993 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,13 @@ language: go sudo: false -go: - - 1.7 - - 1.8 - - tip +matrix: + include: + - go: 1.7 + - go: 1.8 + - go: 1.x + - go: master + allow_failures: + - go: master script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d -s .) From 11d0717014161f27117cdd2fe106bc1d0dd743a6 Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Sun, 4 Jun 2017 21:22:09 -0700 Subject: [PATCH 5/7] Updates docstring comments for JSON Feed methods --- feed.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feed.go b/feed.go index 70e0236..dc685c7 100644 --- a/feed.go +++ b/feed.go @@ -107,13 +107,13 @@ func (f *Feed) WriteRss(w io.Writer) error { return WriteXML(&Rss{f}, w) } -// creates an JSON representation of this feed +// ToJSON creates a JSON Feed representation of this feed func (f *Feed) ToJSON() (string, error) { j := &JSON{f} return j.ToJSON() } -// Writes an JSON representation of this feed to the writer. +// WriteJSON writes an JSON representation of this feed to the writer. func (f *Feed) WriteJSON(w io.Writer) error { j := &JSON{f} feed := j.JSONFeed() From e4dba375711a2ccc07acaa76a6978ef7330aba43 Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Thu, 8 Jun 2017 19:14:17 -0700 Subject: [PATCH 6/7] Updates the receiver variable name for JSONFeed() Previously the variable name was inconsistent with other uses --- json.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/json.go b/json.go index 3cd71d1..266a6fd 100644 --- a/json.go +++ b/json.go @@ -124,22 +124,22 @@ func (f *JSONFeed) ToJSON() (string, error) { } // JSONFeed creates a new JSONFeed with a generic Feed struct's data -func (a *JSON) JSONFeed() *JSONFeed { +func (f *JSON) JSONFeed() *JSONFeed { feed := &JSONFeed{ Version: jsonFeedVersion, - Title: a.Title, - Description: a.Description, + Title: f.Title, + Description: f.Description, } - if a.Link != nil { - feed.HomePageUrl = a.Link.Href + if f.Link != nil { + feed.HomePageUrl = f.Link.Href } - if a.Author != nil { + if f.Author != nil { feed.Author = &JSONAuthor{ - Name: a.Author.Name, + Name: f.Author.Name, } } - for _, e := range a.Items { + for _, e := range f.Items { feed.Items = append(feed.Items, newJSONItem(e)) } return feed From fd3c13502eae29bcb7775820b4e50f2af5720d32 Mon Sep 17 00:00:00 2001 From: Kevin Stock Date: Thu, 8 Jun 2017 19:17:44 -0700 Subject: [PATCH 7/7] Cleans up and adds missing godoc comments related to JSON Feed --- json.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/json.go b/json.go index 266a6fd..8fbb535 100644 --- a/json.go +++ b/json.go @@ -106,14 +106,17 @@ type JSONFeed struct { Items []*JSONItem `json:"items,omitempty"` } +// JSON is used to convert a generic Feed to a JSONFeed. type JSON struct { *Feed } +// ToJSON encodes f into a JSON string. Returns an error if marshalling fails. func (f *JSON) ToJSON() (string, error) { return f.JSONFeed().ToJSON() } +// ToJSON encodes f into a JSON string. Returns an error if marshalling fails. func (f *JSONFeed) ToJSON() (string, error) { data, err := json.MarshalIndent(f, "", " ") if err != nil { @@ -123,7 +126,7 @@ func (f *JSONFeed) ToJSON() (string, error) { return string(data), nil } -// JSONFeed creates a new JSONFeed with a generic Feed struct's data +// JSONFeed creates a new JSONFeed with a generic Feed struct's data. func (f *JSON) JSONFeed() *JSONFeed { feed := &JSONFeed{ Version: jsonFeedVersion,