forked from cadey/xesite
support RSS feed generation
This commit is contained in:
parent
a59e48d948
commit
b89387f6bb
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hash is a simple wrapper around the MD5 algorithm implementation in the
|
||||||
|
// Go standard library. It takes in data and a salt and returns the hashed
|
||||||
|
// representation.
|
||||||
|
func Hash(data string, salt string) string {
|
||||||
|
output := md5.Sum([]byte(data + salt))
|
||||||
|
return fmt.Sprintf("%x", output)
|
||||||
|
}
|
|
@ -13,7 +13,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xe/asarfs"
|
"github.com/Xe/asarfs"
|
||||||
|
"github.com/Xe/ln"
|
||||||
"github.com/gernest/front"
|
"github.com/gernest/front"
|
||||||
|
"github.com/gorilla/feeds"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -155,12 +157,73 @@ func main() {
|
||||||
port = "9090"
|
port = "9090"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/blog.rss", createFeed)
|
||||||
|
http.HandleFunc("/blog.atom", createAtom)
|
||||||
|
|
||||||
n := negroni.Classic()
|
n := negroni.Classic()
|
||||||
n.UseHandler(http.DefaultServeMux)
|
n.UseHandler(http.DefaultServeMux)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":"+port, n))
|
log.Fatal(http.ListenAndServe(":"+port, n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bootTime = time.Now()
|
||||||
|
|
||||||
|
var feed = &feeds.Feed{
|
||||||
|
Title: "Christine Dodrill's Blog",
|
||||||
|
Link: &feeds.Link{Href: "https://christine.website/blog"},
|
||||||
|
Description: "My blog posts and rants about various technology things.",
|
||||||
|
Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"},
|
||||||
|
Created: bootTime,
|
||||||
|
Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, item := range posts {
|
||||||
|
itime, _ := time.Parse("2006-01-02", item.Date)
|
||||||
|
feed.Items = append(feed.Items, &feeds.Item{
|
||||||
|
Title: item.Title,
|
||||||
|
Link: &feeds.Link{Href: "https://christine.website/" + item.Link},
|
||||||
|
Description: item.Summary,
|
||||||
|
Created: itime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrediblySecureSalt *******
|
||||||
|
const IncrediblySecureSalt = "hunter2"
|
||||||
|
|
||||||
|
func createFeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/rss+xml")
|
||||||
|
w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt))
|
||||||
|
|
||||||
|
err := feed.WriteRss(w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
ln.Error(err, ln.F{
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"action": "generating_rss",
|
||||||
|
"uri": r.RequestURI,
|
||||||
|
"host": r.Host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAtom(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/atom+xml")
|
||||||
|
w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt))
|
||||||
|
|
||||||
|
err := feed.WriteAtom(w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
ln.Error(err, ln.F{
|
||||||
|
"remote_addr": r.RemoteAddr,
|
||||||
|
"action": "generating_rss",
|
||||||
|
"uri": r.RequestURI,
|
||||||
|
"host": r.Host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeBlogPosts(w http.ResponseWriter, r *http.Request) {
|
func writeBlogPosts(w http.ResponseWriter, r *http.Request) {
|
||||||
p := []interface{}{}
|
p := []interface{}{}
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
|
|
|
@ -6,3 +6,6 @@ b68094ba95c055dfda888baa8947dfe44c20b1ac github.com/Xe/asarfs
|
||||||
5e4d0891fe789f2da0c2d5afada3b6a1ede6d64c layeh.com/asar
|
5e4d0891fe789f2da0c2d5afada3b6a1ede6d64c layeh.com/asar
|
||||||
3f7ce7b928e14ff890b067e5bbbc80af73690a9c github.com/urfave/negroni
|
3f7ce7b928e14ff890b067e5bbbc80af73690a9c github.com/urfave/negroni
|
||||||
f3687a5cd8e600f93e02174f5c0b91b56d54e8d0 github.com/Xe/gopreload
|
f3687a5cd8e600f93e02174f5c0b91b56d54e8d0 github.com/Xe/gopreload
|
||||||
|
49bd2f58881c34d534aa97bd64bdbdf37be0df91 github.com/Xe/ln
|
||||||
|
441264de03a8117ed530ae8e049d8f601a33a099 github.com/gorilla/feeds
|
||||||
|
ff09b135c25aae272398c51a07235b90a75aa4f0 github.com/pkg/errors
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package ln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter interface for defining chain filters
|
||||||
|
type Filter interface {
|
||||||
|
Apply(Event) bool
|
||||||
|
Run()
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterFunc allows simple functions to implement the Filter interface
|
||||||
|
type FilterFunc func(e Event) bool
|
||||||
|
|
||||||
|
// Apply implements the Filter interface
|
||||||
|
func (ff FilterFunc) Apply(e Event) bool {
|
||||||
|
return ff(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements the Filter interface
|
||||||
|
func (ff FilterFunc) Run() {}
|
||||||
|
|
||||||
|
// Close implements the Filter interface
|
||||||
|
func (ff FilterFunc) Close() {}
|
||||||
|
|
||||||
|
// WriterFilter implements a filter, which arbitrarily writes to an io.Writer
|
||||||
|
type WriterFilter struct {
|
||||||
|
sync.Mutex
|
||||||
|
Out io.Writer
|
||||||
|
Formatter Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriterFilter creates a filter to add to the chain
|
||||||
|
func NewWriterFilter(out io.Writer, format Formatter) *WriterFilter {
|
||||||
|
if format == nil {
|
||||||
|
format = DefaultFormatter
|
||||||
|
}
|
||||||
|
return &WriterFilter{
|
||||||
|
Out: out,
|
||||||
|
Formatter: format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply implements the Filter interface
|
||||||
|
func (w *WriterFilter) Apply(e Event) bool {
|
||||||
|
output, err := w.Formatter.Format(e)
|
||||||
|
if err == nil {
|
||||||
|
w.Lock()
|
||||||
|
w.Out.Write(output)
|
||||||
|
w.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements the Filter interface
|
||||||
|
func (w *WriterFilter) Run() {}
|
||||||
|
|
||||||
|
// Close implements the Filter interface
|
||||||
|
func (w *WriterFilter) Close() {}
|
||||||
|
|
||||||
|
// NilFilter is safe to return as a Filter, but does nothing
|
||||||
|
var NilFilter = FilterFunc(func(e Event) bool { return true })
|
|
@ -0,0 +1,100 @@
|
||||||
|
package ln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultTimeFormat represents the way in which time will be formatted by default
|
||||||
|
DefaultTimeFormat = time.RFC3339
|
||||||
|
)
|
||||||
|
|
||||||
|
// Formatter defines the formatting of events
|
||||||
|
type Formatter interface {
|
||||||
|
Format(Event) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultFormatter is the default way in which to format events
|
||||||
|
var DefaultFormatter Formatter
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultFormatter = NewTextFormatter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextFormatter formats events as key value pairs.
|
||||||
|
// Any remaining text not wrapped in an instance of `F` will be
|
||||||
|
// placed at the end.
|
||||||
|
type TextFormatter struct {
|
||||||
|
TimeFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextFormatter returns a Formatter that outputs as text.
|
||||||
|
func NewTextFormatter() Formatter {
|
||||||
|
return &TextFormatter{TimeFormat: DefaultTimeFormat}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format implements the Formatter interface
|
||||||
|
func (t *TextFormatter) Format(e Event) ([]byte, error) {
|
||||||
|
var writer bytes.Buffer
|
||||||
|
|
||||||
|
writer.WriteString("time=\"")
|
||||||
|
writer.WriteString(e.Time.Format(t.TimeFormat))
|
||||||
|
writer.WriteString("\"")
|
||||||
|
|
||||||
|
for k, v := range e.Data {
|
||||||
|
writer.WriteByte(' ')
|
||||||
|
if shouldQuote(k) {
|
||||||
|
writer.WriteString(fmt.Sprintf("%q", k))
|
||||||
|
} else {
|
||||||
|
writer.WriteString(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteByte('=')
|
||||||
|
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
vs, _ := v.(string)
|
||||||
|
if shouldQuote(vs) {
|
||||||
|
fmt.Fprintf(&writer, "%q", vs)
|
||||||
|
} else {
|
||||||
|
writer.WriteString(vs)
|
||||||
|
}
|
||||||
|
case error:
|
||||||
|
tmperr, _ := v.(error)
|
||||||
|
es := tmperr.Error()
|
||||||
|
|
||||||
|
if shouldQuote(es) {
|
||||||
|
fmt.Fprintf(&writer, "%q", es)
|
||||||
|
} else {
|
||||||
|
writer.WriteString(es)
|
||||||
|
}
|
||||||
|
case time.Time:
|
||||||
|
tmptime, _ := v.(time.Time)
|
||||||
|
writer.WriteString(tmptime.Format(time.RFC3339))
|
||||||
|
default:
|
||||||
|
fmt.Fprint(&writer, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Message) > 0 {
|
||||||
|
fmt.Fprintf(&writer, " _msg=%q", e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteByte('\n')
|
||||||
|
return writer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldQuote(s string) bool {
|
||||||
|
for _, b := range s {
|
||||||
|
if !((b >= 'A' && b <= 'Z') ||
|
||||||
|
(b >= 'a' && b <= 'z') ||
|
||||||
|
(b >= '0' && b <= '9') ||
|
||||||
|
(b == '-' || b == '.' || b == '#' ||
|
||||||
|
b == '/' || b == '_')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package ln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger holds the current priority and list of filters
|
||||||
|
type Logger struct {
|
||||||
|
Filters []Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultLogger is the default implementation of Logger
|
||||||
|
var DefaultLogger *Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var defaultFilters []Filter
|
||||||
|
|
||||||
|
// Default to STDOUT for logging, but allow LN_OUT to change it.
|
||||||
|
out := os.Stdout
|
||||||
|
if os.Getenv("LN_OUT") == "<stderr>" {
|
||||||
|
out = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFilters = append(defaultFilters, NewWriterFilter(out, nil))
|
||||||
|
|
||||||
|
DefaultLogger = &Logger{
|
||||||
|
Filters: defaultFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// F is a key-value mapping for structured data.
|
||||||
|
type F map[string]interface{}
|
||||||
|
|
||||||
|
type Fer interface {
|
||||||
|
F() map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event represents an event
|
||||||
|
type Event struct {
|
||||||
|
Time time.Time
|
||||||
|
Data F
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log is the generic logging method.
|
||||||
|
func (l *Logger) Log(xs ...interface{}) {
|
||||||
|
var bits []interface{}
|
||||||
|
event := Event{Time: time.Now()}
|
||||||
|
|
||||||
|
addF := func(bf F) {
|
||||||
|
if event.Data == nil {
|
||||||
|
event.Data = bf
|
||||||
|
} else {
|
||||||
|
for k, v := range bf {
|
||||||
|
event.Data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the event
|
||||||
|
for _, b := range xs {
|
||||||
|
if bf, ok := b.(F); ok {
|
||||||
|
addF(bf)
|
||||||
|
} else if fer, ok := b.(Fer); ok {
|
||||||
|
addF(F(fer.F()))
|
||||||
|
} else {
|
||||||
|
bits = append(bits, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Message = fmt.Sprint(bits...)
|
||||||
|
|
||||||
|
if os.Getenv("LN_DEBUG_ALL_EVENTS") == "1" {
|
||||||
|
frame := callersFrame()
|
||||||
|
if event.Data == nil {
|
||||||
|
event.Data = make(F)
|
||||||
|
}
|
||||||
|
event.Data["_lineno"] = frame.lineno
|
||||||
|
event.Data["_function"] = frame.function
|
||||||
|
event.Data["_filename"] = frame.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
l.filter(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) filter(e Event) {
|
||||||
|
for _, f := range l.Filters {
|
||||||
|
if !f.Apply(e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error and information about the context of said error.
|
||||||
|
func (l *Logger) Error(err error, xs ...interface{}) {
|
||||||
|
data := F{}
|
||||||
|
frame := callersFrame()
|
||||||
|
|
||||||
|
data["_lineno"] = frame.lineno
|
||||||
|
data["_function"] = frame.function
|
||||||
|
data["_filename"] = frame.filename
|
||||||
|
data["err"] = err
|
||||||
|
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
if cause != nil {
|
||||||
|
data["cause"] = cause.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
xs = append(xs, data)
|
||||||
|
|
||||||
|
l.Log(xs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs this set of values, then exits with status code 1.
|
||||||
|
func (l *Logger) Fatal(xs ...interface{}) {
|
||||||
|
l.Log(xs...)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Implementation
|
||||||
|
|
||||||
|
// Log is the generic logging method.
|
||||||
|
func Log(xs ...interface{}) {
|
||||||
|
DefaultLogger.Log(xs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error and information about the context of said error.
|
||||||
|
func Error(err error, xs ...interface{}) {
|
||||||
|
DefaultLogger.Error(err, xs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs this set of values, then exits with status code 1.
|
||||||
|
func Fatal(xs ...interface{}) {
|
||||||
|
DefaultLogger.Fatal(xs...)
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type frame struct {
|
||||||
|
filename string
|
||||||
|
function string
|
||||||
|
lineno int
|
||||||
|
}
|
||||||
|
|
||||||
|
// skips 2 frames, since Caller returns the current frame, and we need
|
||||||
|
// the caller's caller.
|
||||||
|
func callersFrame() frame {
|
||||||
|
var out frame
|
||||||
|
pc, file, line, ok := runtime.Caller(3)
|
||||||
|
if !ok {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
srcLoc := strings.LastIndex(file, "/src/")
|
||||||
|
if srcLoc >= 0 {
|
||||||
|
file = file[srcLoc+5:]
|
||||||
|
}
|
||||||
|
out.filename = file
|
||||||
|
out.function = functionName(pc)
|
||||||
|
out.lineno = line
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func functionName(pc uintptr) string {
|
||||||
|
fn := runtime.FuncForPC(pc)
|
||||||
|
if fn == nil {
|
||||||
|
return "???"
|
||||||
|
}
|
||||||
|
name := fn.Name()
|
||||||
|
beg := strings.LastIndex(name, string(os.PathSeparator))
|
||||||
|
return name[beg+1:]
|
||||||
|
// end := strings.LastIndex(name, string(os.PathSeparator))
|
||||||
|
// return name[end+1 : len(name)]
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generates Atom feed as XML
|
||||||
|
|
||||||
|
const ns = "http://www.w3.org/2005/Atom"
|
||||||
|
|
||||||
|
type AtomPerson struct {
|
||||||
|
Name string `xml:"name,omitempty"`
|
||||||
|
Uri string `xml:"uri,omitempty"`
|
||||||
|
Email string `xml:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomSummary struct {
|
||||||
|
XMLName xml.Name `xml:"summary"`
|
||||||
|
Content string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomContent struct {
|
||||||
|
XMLName xml.Name `xml:"content"`
|
||||||
|
Content string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomAuthor struct {
|
||||||
|
XMLName xml.Name `xml:"author"`
|
||||||
|
AtomPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomContributor struct {
|
||||||
|
XMLName xml.Name `xml:"contributor"`
|
||||||
|
AtomPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomEntry struct {
|
||||||
|
XMLName xml.Name `xml:"entry"`
|
||||||
|
Xmlns string `xml:"xmlns,attr,omitempty"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Updated string `xml:"updated"` // required
|
||||||
|
Id string `xml:"id"` // required
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Content *AtomContent
|
||||||
|
Rights string `xml:"rights,omitempty"`
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
Published string `xml:"published,omitempty"`
|
||||||
|
Contributor *AtomContributor
|
||||||
|
Link *AtomLink // required if no child 'content' elements
|
||||||
|
Summary *AtomSummary // required if content has src or content is base64
|
||||||
|
Author *AtomAuthor // required if feed lacks an author
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomLink struct {
|
||||||
|
//Atom 1.0 <link rel="enclosure" type="audio/mpeg" title="MP3" href="http://www.example.org/myaudiofile.mp3" length="1234" />
|
||||||
|
XMLName xml.Name `xml:"link"`
|
||||||
|
Href string `xml:"href,attr"`
|
||||||
|
Rel string `xml:"rel,attr,omitempty"`
|
||||||
|
Type string `xml:"type,attr,omitempty"`
|
||||||
|
Length string `xml:"length,attr,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtomFeed struct {
|
||||||
|
XMLName xml.Name `xml:"feed"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Id string `xml:"id"` // required
|
||||||
|
Updated string `xml:"updated"` // required
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Icon string `xml:"icon,omitempty"`
|
||||||
|
Logo string `xml:"logo,omitempty"`
|
||||||
|
Rights string `xml:"rights,omitempty"` // copyright used
|
||||||
|
Subtitle string `xml:"subtitle,omitempty"`
|
||||||
|
Link *AtomLink
|
||||||
|
Author *AtomAuthor `xml:"author,omitempty"`
|
||||||
|
Contributor *AtomContributor
|
||||||
|
Entries []*AtomEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type Atom struct {
|
||||||
|
*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAtomEntry(i *Item) *AtomEntry {
|
||||||
|
id := i.Id
|
||||||
|
// assume the description is html
|
||||||
|
c := &AtomContent{Content: i.Description, Type: "html"}
|
||||||
|
|
||||||
|
if len(id) == 0 {
|
||||||
|
// if there's no id set, try to create one, either from data or just a uuid
|
||||||
|
if len(i.Link.Href) > 0 && (!i.Created.IsZero() || !i.Updated.IsZero()) {
|
||||||
|
dateStr := anyTimeFormat("2006-01-02", i.Updated, i.Created)
|
||||||
|
host, path := i.Link.Href, "/invalid.html"
|
||||||
|
if url, err := url.Parse(i.Link.Href); err == nil {
|
||||||
|
host, path = url.Host, url.Path
|
||||||
|
}
|
||||||
|
id = fmt.Sprintf("tag:%s,%s:%s", host, dateStr, path)
|
||||||
|
} else {
|
||||||
|
id = "urn:uuid:" + NewUUID().String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var name, email string
|
||||||
|
if i.Author != nil {
|
||||||
|
name, email = i.Author.Name, i.Author.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
x := &AtomEntry{
|
||||||
|
Title: i.Title,
|
||||||
|
Link: &AtomLink{Href: i.Link.Href, Rel: i.Link.Rel, Type: i.Link.Type},
|
||||||
|
Content: c,
|
||||||
|
Id: id,
|
||||||
|
Updated: anyTimeFormat(time.RFC3339, i.Updated, i.Created),
|
||||||
|
}
|
||||||
|
|
||||||
|
intLength, err := strconv.ParseInt(i.Link.Length, 10, 64)
|
||||||
|
|
||||||
|
if err == nil && (intLength > 0 || i.Link.Type != "") {
|
||||||
|
i.Link.Rel = "enclosure"
|
||||||
|
x.Link = &AtomLink{Href: i.Link.Href, Rel: i.Link.Rel, Type: i.Link.Type, Length: i.Link.Length}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) > 0 || len(email) > 0 {
|
||||||
|
x.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: name, Email: email}}
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new AtomFeed with a generic Feed struct's data
|
||||||
|
func (a *Atom) AtomFeed() *AtomFeed {
|
||||||
|
updated := anyTimeFormat(time.RFC3339, a.Updated, a.Created)
|
||||||
|
feed := &AtomFeed{
|
||||||
|
Xmlns: ns,
|
||||||
|
Title: a.Title,
|
||||||
|
Link: &AtomLink{Href: a.Link.Href, Rel: a.Link.Rel},
|
||||||
|
Subtitle: a.Description,
|
||||||
|
Id: a.Link.Href,
|
||||||
|
Updated: updated,
|
||||||
|
Rights: a.Copyright,
|
||||||
|
}
|
||||||
|
if a.Author != nil {
|
||||||
|
feed.Author = &AtomAuthor{AtomPerson: AtomPerson{Name: a.Author.Name, Email: a.Author.Email}}
|
||||||
|
}
|
||||||
|
for _, e := range a.Items {
|
||||||
|
feed.Entries = append(feed.Entries, newAtomEntry(e))
|
||||||
|
}
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an XML-Ready object for an Atom object
|
||||||
|
func (a *Atom) FeedXml() interface{} {
|
||||||
|
return a.AtomFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an XML-ready object for an AtomFeed object
|
||||||
|
func (a *AtomFeed) FeedXml() interface{} {
|
||||||
|
return a
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
Syndication (feed) generator library for golang.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
Create a Feed and some Items in that feed using the generic interfaces:
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
. "github.com/gorilla/feeds
|
||||||
|
)
|
||||||
|
|
||||||
|
now = time.Now()
|
||||||
|
|
||||||
|
feed := &Feed{
|
||||||
|
Title: "jmoiron.net blog",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog"},
|
||||||
|
Description: "discussion about tech, footie, photos",
|
||||||
|
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
Copyright: "This work is copyright © Benjamin Button",
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Items = []*Item{
|
||||||
|
&Item{
|
||||||
|
Title: "Limiting Concurrency in Go",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/limiting-concurrency-in-go/"},
|
||||||
|
Description: "A discussion on controlled parallelism in golang",
|
||||||
|
Author: &Author{Name: "Jason Moiron", Email: "jmoiron@jmoiron.net"},
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&Item{
|
||||||
|
Title: "Logic-less Template Redux",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/logicless-template-redux/"},
|
||||||
|
Description: "More thoughts on logicless templates",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
&Item{
|
||||||
|
Title: "Idiomatic Code Reuse in Go",
|
||||||
|
Link: &Link{Href: "http://jmoiron.net/blog/idiomatic-code-reuse-in-go/"},
|
||||||
|
Description: "How to use interfaces <em>effectively</em>",
|
||||||
|
Created: now,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
From here, you can output Atom or RSS versions of this feed easily
|
||||||
|
|
||||||
|
atom, err := feed.ToAtom()
|
||||||
|
rss, err := feed.ToRss()
|
||||||
|
|
||||||
|
You can also get access to the underlying objects that feeds uses to export its XML
|
||||||
|
|
||||||
|
atomFeed := &Atom{feed}.AtomFeed()
|
||||||
|
rssFeed := &Rss{feed}.RssFeed()
|
||||||
|
|
||||||
|
From here, you can modify or add each syndication's specific fields before outputting
|
||||||
|
|
||||||
|
atomFeed.Subtitle = "plays the blues"
|
||||||
|
atom, err := ToXML(atomFeed)
|
||||||
|
rssFeed.Generator = "gorilla/feeds v1.0 (github.com/gorilla/feeds)"
|
||||||
|
rss, err := ToXML(rssFeed)
|
||||||
|
|
||||||
|
*/
|
||||||
|
package feeds
|
|
@ -0,0 +1,106 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Href, Rel, Type, Length string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
Name, Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Title string
|
||||||
|
Link *Link
|
||||||
|
Author *Author
|
||||||
|
Description string // used as description in rss, summary in atom
|
||||||
|
Id string // used as guid in rss, id in atom
|
||||||
|
Updated time.Time
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Feed struct {
|
||||||
|
Title string
|
||||||
|
Link *Link
|
||||||
|
Description string
|
||||||
|
Author *Author
|
||||||
|
Updated time.Time
|
||||||
|
Created time.Time
|
||||||
|
Id string
|
||||||
|
Subtitle string
|
||||||
|
Items []*Item
|
||||||
|
Copyright string
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a new Item to a Feed
|
||||||
|
func (f *Feed) Add(item *Item) {
|
||||||
|
f.Items = append(f.Items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the first non-zero time formatted as a string or ""
|
||||||
|
func anyTimeFormat(format string, times ...time.Time) string {
|
||||||
|
for _, t := range times {
|
||||||
|
if !t.IsZero() {
|
||||||
|
return t.Format(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface used by ToXML to get a object suitable for exporting XML.
|
||||||
|
type XmlFeed interface {
|
||||||
|
FeedXml() interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn a feed object (either a Feed, AtomFeed, or RssFeed) into xml
|
||||||
|
// returns an error if xml marshaling fails
|
||||||
|
func ToXML(feed XmlFeed) (string, error) {
|
||||||
|
x := feed.FeedXml()
|
||||||
|
data, err := xml.MarshalIndent(x, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// strip empty line from default xml header
|
||||||
|
s := xml.Header[:len(xml.Header)-1] + string(data)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a feed object (either a Feed, AtomFeed, or RssFeed) as XML into
|
||||||
|
// the writer. Returns an error if XML marshaling fails.
|
||||||
|
func WriteXML(feed XmlFeed, w io.Writer) error {
|
||||||
|
x := feed.FeedXml()
|
||||||
|
// write default xml header, without the newline
|
||||||
|
if _, err := w.Write([]byte(xml.Header[:len(xml.Header)-1])); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e := xml.NewEncoder(w)
|
||||||
|
e.Indent("", " ")
|
||||||
|
return e.Encode(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates an Atom representation of this feed
|
||||||
|
func (f *Feed) ToAtom() (string, error) {
|
||||||
|
a := &Atom{f}
|
||||||
|
return ToXML(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes an Atom representation of this feed to the writer.
|
||||||
|
func (f *Feed) WriteAtom(w io.Writer) error {
|
||||||
|
return WriteXML(&Atom{f}, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates an Rss representation of this feed
|
||||||
|
func (f *Feed) ToRss() (string, error) {
|
||||||
|
r := &Rss{f}
|
||||||
|
return ToXML(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes an RSS representation of this feed to the writer.
|
||||||
|
func (f *Feed) WriteRss(w io.Writer) error {
|
||||||
|
return WriteXML(&Rss{f}, w)
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
// rss support
|
||||||
|
// validation done according to spec here:
|
||||||
|
// http://cyber.law.harvard.edu/rss/rss.html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// private wrapper around the RssFeed which gives us the <rss>..</rss> xml
|
||||||
|
type rssFeedXml struct {
|
||||||
|
XMLName xml.Name `xml:"rss"`
|
||||||
|
Version string `xml:"version,attr"`
|
||||||
|
Channel *RssFeed
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssImage struct {
|
||||||
|
XMLName xml.Name `xml:"image"`
|
||||||
|
Url string `xml:"url"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
Width int `xml:"width,omitempty"`
|
||||||
|
Height int `xml:"height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssTextInput struct {
|
||||||
|
XMLName xml.Name `xml:"textInput"`
|
||||||
|
Title string `xml:"title"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Link string `xml:"link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssFeed struct {
|
||||||
|
XMLName xml.Name `xml:"channel"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Link string `xml:"link"` // required
|
||||||
|
Description string `xml:"description"` // required
|
||||||
|
Language string `xml:"language,omitempty"`
|
||||||
|
Copyright string `xml:"copyright,omitempty"`
|
||||||
|
ManagingEditor string `xml:"managingEditor,omitempty"` // Author used
|
||||||
|
WebMaster string `xml:"webMaster,omitempty"`
|
||||||
|
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||||
|
LastBuildDate string `xml:"lastBuildDate,omitempty"` // updated used
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Generator string `xml:"generator,omitempty"`
|
||||||
|
Docs string `xml:"docs,omitempty"`
|
||||||
|
Cloud string `xml:"cloud,omitempty"`
|
||||||
|
Ttl int `xml:"ttl,omitempty"`
|
||||||
|
Rating string `xml:"rating,omitempty"`
|
||||||
|
SkipHours string `xml:"skipHours,omitempty"`
|
||||||
|
SkipDays string `xml:"skipDays,omitempty"`
|
||||||
|
Image *RssImage
|
||||||
|
TextInput *RssTextInput
|
||||||
|
Items []*RssItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssItem struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Title string `xml:"title"` // required
|
||||||
|
Link string `xml:"link"` // required
|
||||||
|
Description string `xml:"description"` // required
|
||||||
|
Author string `xml:"author,omitempty"`
|
||||||
|
Category string `xml:"category,omitempty"`
|
||||||
|
Comments string `xml:"comments,omitempty"`
|
||||||
|
Enclosure *RssEnclosure
|
||||||
|
Guid string `xml:"guid,omitempty"` // Id used
|
||||||
|
PubDate string `xml:"pubDate,omitempty"` // created or updated
|
||||||
|
Source string `xml:"source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RssEnclosure struct {
|
||||||
|
//RSS 2.0 <enclosure url="http://example.com/file.mp3" length="123456789" type="audio/mpeg" />
|
||||||
|
XMLName xml.Name `xml:"enclosure"`
|
||||||
|
Url string `xml:"url,attr"`
|
||||||
|
Length string `xml:"length,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rss struct {
|
||||||
|
*Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new RssItem with a generic Item struct's data
|
||||||
|
func newRssItem(i *Item) *RssItem {
|
||||||
|
item := &RssItem{
|
||||||
|
Title: i.Title,
|
||||||
|
Link: i.Link.Href,
|
||||||
|
Description: i.Description,
|
||||||
|
Guid: i.Id,
|
||||||
|
PubDate: anyTimeFormat(time.RFC1123Z, i.Created, i.Updated),
|
||||||
|
}
|
||||||
|
|
||||||
|
intLength, err := strconv.ParseInt(i.Link.Length, 10, 64)
|
||||||
|
|
||||||
|
if err == nil && (intLength > 0 || i.Link.Type != "") {
|
||||||
|
item.Enclosure = &RssEnclosure{Url: i.Link.Href, Type: i.Link.Type, Length: i.Link.Length}
|
||||||
|
}
|
||||||
|
if i.Author != nil {
|
||||||
|
item.Author = i.Author.Name
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new RssFeed with a generic Feed struct's data
|
||||||
|
func (r *Rss) RssFeed() *RssFeed {
|
||||||
|
pub := anyTimeFormat(time.RFC1123Z, r.Created, r.Updated)
|
||||||
|
build := anyTimeFormat(time.RFC1123Z, r.Updated)
|
||||||
|
author := ""
|
||||||
|
if r.Author != nil {
|
||||||
|
author = r.Author.Email
|
||||||
|
if len(r.Author.Name) > 0 {
|
||||||
|
author = fmt.Sprintf("%s (%s)", r.Author.Email, r.Author.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := &RssFeed{
|
||||||
|
Title: r.Title,
|
||||||
|
Link: r.Link.Href,
|
||||||
|
Description: r.Description,
|
||||||
|
ManagingEditor: author,
|
||||||
|
PubDate: pub,
|
||||||
|
LastBuildDate: build,
|
||||||
|
Copyright: r.Copyright,
|
||||||
|
}
|
||||||
|
for _, i := range r.Items {
|
||||||
|
channel.Items = append(channel.Items, newRssItem(i))
|
||||||
|
}
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an XML-Ready object for an Rss object
|
||||||
|
func (r *Rss) FeedXml() interface{} {
|
||||||
|
// only generate version 2.0 feeds for now
|
||||||
|
return r.RssFeed().FeedXml()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an XML-ready object for an RssFeed object
|
||||||
|
func (r *RssFeed) FeedXml() interface{} {
|
||||||
|
return &rssFeedXml{Version: "2.0", Channel: r}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package feeds
|
||||||
|
|
||||||
|
// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// create a new uuid v4
|
||||||
|
func NewUUID() *UUID {
|
||||||
|
u := &UUID{}
|
||||||
|
_, err := rand.Read(u[:16])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u[8] = (u[8] | 0x80) & 0xBf
|
||||||
|
u[6] = (u[6] | 0x40) & 0x4f
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UUID) String() string {
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:])
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
// Package errors provides simple error handling primitives.
|
||||||
|
//
|
||||||
|
// The traditional error handling idiom in Go is roughly akin to
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// which applied recursively up the call stack results in error reports
|
||||||
|
// without context or debugging information. The errors package allows
|
||||||
|
// programmers to add context to the failure path in their code in a way
|
||||||
|
// that does not destroy the original value of the error.
|
||||||
|
//
|
||||||
|
// Adding context to an error
|
||||||
|
//
|
||||||
|
// The errors.Wrap function returns a new error that adds context to the
|
||||||
|
// original error by recording a stack trace at the point Wrap is called,
|
||||||
|
// and the supplied message. For example
|
||||||
|
//
|
||||||
|
// _, err := ioutil.ReadAll(r)
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.Wrap(err, "read failed")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||||
|
// functions destructure errors.Wrap into its component operations of annotating
|
||||||
|
// an error with a stack trace and an a message, respectively.
|
||||||
|
//
|
||||||
|
// Retrieving the cause of an error
|
||||||
|
//
|
||||||
|
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||||
|
// preceding error. Depending on the nature of the error it may be necessary
|
||||||
|
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||||
|
// for inspection. Any error value which implements this interface
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||||
|
// the topmost error which does not implement causer, which is assumed to be
|
||||||
|
// the original cause. For example:
|
||||||
|
//
|
||||||
|
// switch err := errors.Cause(err).(type) {
|
||||||
|
// case *MyError:
|
||||||
|
// // handle specifically
|
||||||
|
// default:
|
||||||
|
// // unknown error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// causer interface is not exported by this package, but is considered a part
|
||||||
|
// of stable public API.
|
||||||
|
//
|
||||||
|
// Formatted printing of errors
|
||||||
|
//
|
||||||
|
// All error values returned from this package implement fmt.Formatter and can
|
||||||
|
// be formatted by the fmt package. The following verbs are supported
|
||||||
|
//
|
||||||
|
// %s print the error. If the error has a Cause it will be
|
||||||
|
// printed recursively
|
||||||
|
// %v see %s
|
||||||
|
// %+v extended format. Each Frame of the error's StackTrace will
|
||||||
|
// be printed in detail.
|
||||||
|
//
|
||||||
|
// Retrieving the stack trace of an error or wrapper
|
||||||
|
//
|
||||||
|
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||||
|
// invoked. This information can be retrieved with the following interface.
|
||||||
|
//
|
||||||
|
// type stackTracer interface {
|
||||||
|
// StackTrace() errors.StackTrace
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Where errors.StackTrace is defined as
|
||||||
|
//
|
||||||
|
// type StackTrace []Frame
|
||||||
|
//
|
||||||
|
// The Frame type represents a call site in the stack trace. Frame supports
|
||||||
|
// the fmt.Formatter interface that can be used for printing information about
|
||||||
|
// the stack trace of this error. For example:
|
||||||
|
//
|
||||||
|
// if err, ok := err.(stackTracer); ok {
|
||||||
|
// for _, f := range err.StackTrace() {
|
||||||
|
// fmt.Printf("%+s:%d", f)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// stackTracer interface is not exported by this package, but is considered a part
|
||||||
|
// of stable public API.
|
||||||
|
//
|
||||||
|
// See the documentation for Frame.Format for more details.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns an error with the supplied message.
|
||||||
|
// New also records the stack trace at the point it was called.
|
||||||
|
func New(message string) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: message,
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf formats according to a format specifier and returns the string
|
||||||
|
// as a value that satisfies error.
|
||||||
|
// Errorf also records the stack trace at the point it was called.
|
||||||
|
func Errorf(format string, args ...interface{}) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fundamental is an error that has a message and a stack, but no caller.
|
||||||
|
type fundamental struct {
|
||||||
|
msg string
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fundamental) Error() string { return f.msg }
|
||||||
|
|
||||||
|
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
f.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", f.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||||
|
// If err is nil, WithStack returns nil.
|
||||||
|
func WithStack(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withStack struct {
|
||||||
|
error
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withStack) Cause() error { return w.error }
|
||||||
|
|
||||||
|
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v", w.Cause())
|
||||||
|
w.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrap is called, and the supplied message.
|
||||||
|
// If err is nil, Wrap returns nil.
|
||||||
|
func Wrap(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapf returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrapf is call, and the format specifier.
|
||||||
|
// If err is nil, Wrapf returns nil.
|
||||||
|
func Wrapf(err error, format string, args ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMessage annotates err with a new message.
|
||||||
|
// If err is nil, WithMessage returns nil.
|
||||||
|
func WithMessage(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withMessage struct {
|
||||||
|
cause error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||||
|
func (w *withMessage) Cause() error { return w.cause }
|
||||||
|
|
||||||
|
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||||
|
io.WriteString(s, w.msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's', 'q':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause returns the underlying cause of the error, if possible.
|
||||||
|
// An error value has a cause if it implements the following
|
||||||
|
// interface:
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If the error does not implement Cause, the original error will
|
||||||
|
// be returned. If the error is nil, nil will be returned without further
|
||||||
|
// investigation.
|
||||||
|
func Cause(err error) error {
|
||||||
|
type causer interface {
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
|
||||||
|
for err != nil {
|
||||||
|
cause, ok := err.(causer)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = cause.Cause()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame represents a program counter inside a stack frame.
|
||||||
|
type Frame uintptr
|
||||||
|
|
||||||
|
// pc returns the program counter for this frame;
|
||||||
|
// multiple frames may have the same PC value.
|
||||||
|
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||||
|
|
||||||
|
// file returns the full path to the file that contains the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) file() string {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
file, _ := fn.FileLine(f.pc())
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// line returns the line number of source code of the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) line() int {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, line := fn.FileLine(f.pc())
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format formats the frame according to the fmt.Formatter interface.
|
||||||
|
//
|
||||||
|
// %s source file
|
||||||
|
// %d source line
|
||||||
|
// %n function name
|
||||||
|
// %v equivalent to %s:%d
|
||||||
|
//
|
||||||
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
|
//
|
||||||
|
// %+s path of source file relative to the compile time GOPATH
|
||||||
|
// %+v equivalent to %+s:%d
|
||||||
|
func (f Frame) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 's':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
pc := f.pc()
|
||||||
|
fn := runtime.FuncForPC(pc)
|
||||||
|
if fn == nil {
|
||||||
|
io.WriteString(s, "unknown")
|
||||||
|
} else {
|
||||||
|
file, _ := fn.FileLine(pc)
|
||||||
|
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
io.WriteString(s, path.Base(f.file()))
|
||||||
|
}
|
||||||
|
case 'd':
|
||||||
|
fmt.Fprintf(s, "%d", f.line())
|
||||||
|
case 'n':
|
||||||
|
name := runtime.FuncForPC(f.pc()).Name()
|
||||||
|
io.WriteString(s, funcname(name))
|
||||||
|
case 'v':
|
||||||
|
f.Format(s, 's')
|
||||||
|
io.WriteString(s, ":")
|
||||||
|
f.Format(s, 'd')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||||
|
type StackTrace []Frame
|
||||||
|
|
||||||
|
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
for _, f := range st {
|
||||||
|
fmt.Fprintf(s, "\n%+v", f)
|
||||||
|
}
|
||||||
|
case s.Flag('#'):
|
||||||
|
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(s, "%v", []Frame(st))
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
fmt.Fprintf(s, "%s", []Frame(st))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack represents a stack of program counters.
|
||||||
|
type stack []uintptr
|
||||||
|
|
||||||
|
func (s *stack) Format(st fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case st.Flag('+'):
|
||||||
|
for _, pc := range *s {
|
||||||
|
f := Frame(pc)
|
||||||
|
fmt.Fprintf(st, "\n%+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stack) StackTrace() StackTrace {
|
||||||
|
f := make([]Frame, len(*s))
|
||||||
|
for i := 0; i < len(f); i++ {
|
||||||
|
f[i] = Frame((*s)[i])
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func callers() *stack {
|
||||||
|
const depth = 32
|
||||||
|
var pcs [depth]uintptr
|
||||||
|
n := runtime.Callers(3, pcs[:])
|
||||||
|
var st stack = pcs[0:n]
|
||||||
|
return &st
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||||
|
func funcname(name string) string {
|
||||||
|
i := strings.LastIndex(name, "/")
|
||||||
|
name = name[i+1:]
|
||||||
|
i = strings.Index(name, ".")
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimGOPATH(name, file string) string {
|
||||||
|
// Here we want to get the source file path relative to the compile time
|
||||||
|
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
|
||||||
|
// GOPATH at runtime, but we can infer the number of path segments in the
|
||||||
|
// GOPATH. We note that fn.Name() returns the function name qualified by
|
||||||
|
// the import path, which does not include the GOPATH. Thus we can trim
|
||||||
|
// segments from the beginning of the file path until the number of path
|
||||||
|
// separators remaining is one more than the number of path separators in
|
||||||
|
// the function name. For example, given:
|
||||||
|
//
|
||||||
|
// GOPATH /home/user
|
||||||
|
// file /home/user/src/pkg/sub/file.go
|
||||||
|
// fn.Name() pkg/sub.Type.Method
|
||||||
|
//
|
||||||
|
// We want to produce:
|
||||||
|
//
|
||||||
|
// pkg/sub/file.go
|
||||||
|
//
|
||||||
|
// From this we can easily see that fn.Name() has one less path separator
|
||||||
|
// than our desired output. We count separators from the end of the file
|
||||||
|
// path until it finds two more than in the function name and then move
|
||||||
|
// one character forward to preserve the initial path segment without a
|
||||||
|
// leading separator.
|
||||||
|
const sep = "/"
|
||||||
|
goal := strings.Count(name, sep) + 2
|
||||||
|
i := len(file)
|
||||||
|
for n := 0; n < goal; n++ {
|
||||||
|
i = strings.LastIndex(file[:i], sep)
|
||||||
|
if i == -1 {
|
||||||
|
// not enough separators found, set i so that the slice expression
|
||||||
|
// below leaves file unmodified
|
||||||
|
i = -len(sep)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get back to 0 or trim the leading separator
|
||||||
|
file = file[i+len(sep):]
|
||||||
|
return file
|
||||||
|
}
|
Loading…
Reference in New Issue