这篇文章将展示一个简短的例子,说明如何从Go网络服务器上创建一个.ics feed。数据源将是一个用mocky创建的假REST端点。
数据也将被缓存在内存中,因此我们不必一直调用API,并能够提供快速的响应时间。
为了创建feed本身,将使用goics库,这有助于编码。
服务器上将有两个端点。
POST /feedURL- 创建一个新的、随机生成的feedURL,并初始化token的feed。GET /feed/{token}- 返回给定token的feed,如果缓存过期,则重新创建。
说了这么多,让我们开始吧!
实施
首先,让我们看一下我们的数据源。我们将使用一个来自mocky 的假JSON响应,JSON看起来像这样。
[
{
"dateStart": "2018-02-01T13:30:01+00:00",
"dateEnd": "2018-02-01T12:30:01+00:00",
"description": "dentist"
},
{
"dateStart": "2018-02-02T13:21:01+00:00",
"dateEnd": "2018-02-02T15:51:01+00:00",
"description": "gym"
},
{
"dateStart": "2018-02-09T13:21:01+00:00",
"dateEnd": "2018-02-09T14:21:01+00:00",
"description": "meeting"
},
{
"dateStart": "2018-02-09T15:21:01+00:00",
"dateEnd": "2018-02-09T17:41:01+00:00",
"description": "cooking class"
},
{
"dateStart": "2018-02-11T13:21:01+00:00",
"dateEnd": "2018-02-11T18:21:01+00:00",
"description": "gym"
},
{
"dateStart": "2018-02-12T13:21:01+00:00",
"dateEnd": "2018-02-12T15:21:01+00:00",
"description": "shopping"
},
{
"dateStart": "2018-02-14T19:00:01+00:00",
"dateEnd": "2018-02-14T21:00:01+00:00",
"description": "valentines"
}
]
为了简单起见,数据已经形成,我们的目的是创建以description 为标题的日历条目。
有了这些数据,我们可以创建我们的数据模型。一个用于解析JSON的Entry 结构和一个作为缓存创建的feed的容器的Feed 结构应该足够了。
// Feed is an iCal feed
type Feed struct {
Content string
ExpiresAt time.Time
}
// Entry is a time entry
type Entry struct {
DateStart time.Time `json:"dateStart"`
DateEnd time.Time `json:"dateEnd"`
Description string `json:"description"`
}
// Entries is a collection of entries
type Entries []*Entry
还有一个Entries 集合类型,我们将需要它来进行.ics 编码,但我们将在后面处理这个问题。
下一步是用上述的路由创建一个简单的Go网络服务器。
const feedPrefix = "/feed/"
const expirationTime = 20 * time.Minute // caching time
func main() {
cache := make(map[string]*Feed)
mux := http.NewServeMux()
mux.HandleFunc("/feedURL", feedURL(cache))
mux.HandleFunc(feedPrefix, feed(cache))
log.Print("Server started on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
我们创建一个简单的 "缓存",在我们的例子中,它只是一个map[string]*Feed ,并为两个路由注册处理函数。
让我们先看一下feedURL 。
func feedURL(cache map[string]*Feed) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := randomToken(20)
_, err := createFeedForToken(token, cache)
if err != nil {
writeError(http.StatusInternalServerError, "Could not create feed", w, err)
return
}
writeSuccess(fmt.Sprintf("FeedToken: %s", token), w)
})
}
这个处理程序使用一个简单的辅助函数crypto/rand ,创建一个随机的token,调用createFeedForToken ,我们将在后面看一下,为给定的token创建一个新的feed,并将token返回给用户。
第二个处理程序,/feed/{token} ,涉及的内容更多。
func feed(cache map[string]*Feed) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "text/calendar")
w.Header().Set("charset", "utf-8")
w.Header().Set("Content-Disposition", "inline")
w.Header().Set("filename", "calendar.ics")
var result string
token := parseToken(r.URL.Path)
feed, ok := cache[token]
if !ok || feed == nil {
writeError(http.StatusNotFound, "No Feed for this Token", w, errors.New("No Feed for this Token"))
return
}
result = feed.Content
if feed.ExpiresAt.Before(time.Now()) {
newFeed, err := createFeedForToken(token, cache)
if err != nil {
writeError(http.StatusInternalServerError, "Could not create feed", w, err)
return
}
result = newFeed.Content
}
writeSuccess(result, w)
})
}
在为.ics 响应设置标准头信息并解析所提供的标记后,我们检查是否有一个给定标记的缓存条目。如果没有,我们会返回一个404 错误。
如果有一个token的feed,但它已经过期,我们使用createFeedForToken 重新创建feed,并返回新创建的feed。
如果有一个条目并且仍然有效,我们就从缓存中返回feed。
现在,让我们看一下createFeedForToken 函数。
func createFeedForToken(token string, cache map[string]*Feed) (*Feed, error) {
res, err := fetchData()
if err != nil {
return nil, err
}
b := bytes.Buffer{}
goics.NewICalEncode(&b).Encode(res)
feed := &Feed{
Content: b.String(),
ExpiresAt: time.Now().Add(expirationTime)
}
cache[token] = feed
return feed, nil
}
fetchData 函数没有做什么花哨的事情。它调用mocky URL,并将生成的JSON解密到Entry 结构的列表中,处理所有可能的错误。
然后,我们使用goics.NewICalEncode().Encode() 创建实际的.ics feed,用给定的token将其放入缓存并返回feed。
为了能够使用我们的Entry 结构列表作为goics.NewICalEncode(&b).Encode() 的有效输入,Entries 类型需要实现ICalEmitter 接口。
// EmitICal implements the interface for goics
func (e Entries) EmitICal() goics.Componenter {
c := goics.NewComponent()
c.SetType("VCALENDAR")
c.AddProperty("CALSCAL", "GREGORIAN")
for _, entry := range e {
s := goics.NewComponent()
s.SetType("VEVENT")
k, v := goics.FormatDateTimeField("DTSTART", entry.DateStart)
s.AddProperty(k, v)
k, v = goics.FormatDateTimeField("DTEND", entry.DateEnd)
s.AddProperty(k, v)
s.AddProperty("SUMMARY", entry.Description)
c.AddComponent(s)
}
return c
}
该方法使用goics 帮助器来创建.ics 输出,如图书馆文档所示。
现在,如果我们调用/feedURL ,并将生成的令牌作为/feed/{token} 的输入,我们应该得到一个有效的calendar.ics 文件,它可以被导入到iCalendar、Google Calendar等应用程序中。
还请注意,向/feedURL 发出的第一个请求(初始化Feed)需要大约100毫秒,而向/feed/ 发出的后续请求(返回缓存的结果)则非常快。
就这样了。你可以在这里找到完整的代码。
总结
这是在Go中的另一个简短的例子,它是网络应用中经常实现的一个基本功能。我没有找到很多用于iCal 的 Go 库,但是我使用的这个库工作得很好,对于这个使用案例来说是足够的。
更广泛地说,我相信这个例子说明了 Go 标准库是多么强大,以及像这样的简单功能可以用它来实现。