package launchpad
/*
* A wrapper around the Launchpad API. The documentation can be found at:
* https://launchpad.net/+apidoc/devel.html
*
* TODO:
* - Retrieve all messages associated to bugs
* - Retrieve bug status
* - Retrieve activity log
* - SearchTasks should yield bugs one by one
*
* TODO (maybe):
* - Authentication (this might help retrieving email adresses)
*/
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
const apiRoot = "https://api.launchpad.net/devel"
// Person describes a person on Launchpad (a bug owner, a message author, ...).
type LPPerson struct {
Name string `json:"display_name"`
Login string `json:"name"`
}
// Caching all the LPPerson we know.
// The keys are links to an owner page, such as
// https://api.launchpad.net/devel/~login
var personCache = make(map[string]LPPerson)
func (owner *LPPerson) UnmarshalJSON(data []byte) error {
type LPPersonX LPPerson // Avoid infinite recursion
var ownerLink string
if err := json.Unmarshal(data, &ownerLink); err != nil {
return err
}
// First, try to gather info about the bug owner using our cache.
if cachedPerson, hasKey := personCache[ownerLink]; hasKey {
*owner = cachedPerson
return nil
}
// If the bug owner is not already known, we have to send a request.
req, err := http.NewRequest("GET", ownerLink, nil)
if err != nil {
return nil
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
var p LPPersonX
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
return nil
}
*owner = LPPerson(p)
// Do not forget to update the cache.
personCache[ownerLink] = *owner
return nil
}
// LPBug describes a Launchpad bug.
type LPBug struct {
Title string `json:"title"`
ID int `json:"id"`
Owner LPPerson `json:"owner_link"`
Description string `json:"description"`
CreatedAt string `json:"date_created"`
}
type launchpadBugEntry struct {
BugLink string `json:"bug_link"`
SelfLink string `json:"self_link"`
}
type launchpadAnswer struct {
Entries []launchpadBugEntry `json:"entries"`
Start int `json:"start"`
NextLink string `json:"next_collection_link"`
}
type launchpadAPI struct {
client *http.Client
}
func (lapi *launchpadAPI) Init() error {
lapi.client = &http.Client{}
return nil
}
func (lapi *launchpadAPI) SearchTasks(project string) ([]LPBug, error) {
var bugs []LPBug
// First, let us build the URL. Not all statuses are included by
// default, so we have to explicitely enumerate them.
validStatuses := [13]string{
"New", "Incomplete", "Opinion", "Invalid",
"Won't Fix", "Expired", "Confirmed", "Triaged",
"In Progress", "Fix Committed", "Fix Released",
"Incomplete (with response)", "Incomplete (without response)",
}
queryParams := url.Values{}
queryParams.Add("ws.op", "searchTasks")
for _, validStatus := range validStatuses {
queryParams.Add("status", validStatus)
}
lpURL := fmt.Sprintf("%s/%s?%s", apiRoot, project, queryParams.Encode())
for {
req, err := http.NewRequest("GET", lpURL, nil)
if err != nil {
return nil, err
}
resp, err := lapi.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result launchpadAnswer
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
for _, bugEntry := range result.Entries {
bug, err := lapi.queryBug(bugEntry.BugLink)
if err == nil {
bugs = append(bugs, bug)
}
}
// Launchpad only returns 75 results at a time. We get the next
// page and run another query, unless there is no other page.
lpURL = result.NextLink
if lpURL == "" {
break
}
}
return bugs, nil
}
func (lapi *launchpadAPI) queryBug(url string) (LPBug, error) {
var bug LPBug
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return bug, err
}
resp, err := lapi.client.Do(req)
if err != nil {
return bug, err
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&bug); err != nil {
return bug, err
}
return bug, nil
}