<commit_before>package coinjar

import (
	"encoding/json"
	"errors"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
)

type client struct {
	apiKey     string
	endpoint   string
	httpClient *http.Client
}

func NewClient(apiKey string) (c *client) {
	c = new(client)
	c.apiKey = apiKey
	c.endpoint = "https://api.coinjar.io/v1"
	c.httpClient = new(http.Client)
	return
}

type User struct {
	Uuid               string
	Email              string
	FullName           string `json:"full_name"`
	AvailableBalance   string `json:"available_balance"`
	UnconfirmedBalance string `json:"unconfirmed_balance"`
}

func (c *client) Account() (obj *User, err error) {
	body, err := c.read("account.json")
	if err != nil {
		return
	}

	var wrapper struct{ User *User }
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.User, nil
}

type BitcoinAddress struct {
	Label          string
	TotalConfirmed string `json:"total_confirmed"`
	TotalReceived  string `json:"total_received"`
	Address        string
}

func (c *client) BitcoinAddresses() ([]BitcoinAddress, error) {
	return c.BitcoinAddressesList(100, 0)
}

func (c *client) BitcoinAddressesList(limit, offset int) (obj []BitcoinAddress, err error) {
	body, err := c.read("bitcoin_addresses.json",
		"limit", strconv.Itoa(limit),
		"offset", strconv.Itoa(offset))
	if err != nil {
		return
	}

	var wrapper struct {
		Addresses []BitcoinAddress `json:"bitcoin_addresses"`
	}
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.Addresses, nil
}

func (c *client) BitcoinAddress(address string) (obj *BitcoinAddress, err error) {
	body, err := c.read("bitcoin_addresses/" + address + ".json")
	if err != nil {
		return
	}
	if string(body) == "null" {
		return nil, errors.New("Bitcoin address not found")
	}

	var wrapper struct {
		Address *BitcoinAddress `json:"bitcoin_address"`
	}
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.Address, nil
}

type FairRate struct {
	Bid  string
	Ask  string
	Spot string
}

func (c *client) FairRate(currency string) (obj *FairRate, err error) {
	body, err := c.read("fair_rate/" + currency + ".json")
	if err != nil {
		return
	}
	obj = new(FairRate)
	err = json.Unmarshal(body, obj)
	if err != nil {
		return
	}
	return
}

func (c *client) read(api string, params ...string) (body []byte, err error) {
	request, _ := http.NewRequest("GET", c.endpoint+"/"+api, nil)
	request.SetBasicAuth(c.apiKey, "")
	request.URL.RawQuery = createQuery(params)

	resp, err := c.httpClient.Do(request)
	if err != nil {
		return
	}

	defer resp.Body.Close()
	body, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return
	}
	return
}

func createQuery(params []string) string {
	plen := len(params)
	if plen%2 == 1 {
		plen = plen - 1
	}
	values := url.Values{}
	for i := 0; i < plen; i += 2 {
		values.Set(params[i], params[i+1])
	}
	return values.Encode()
}
<commit_msg>Uuid -> UUID<commit_after>package coinjar

import (
	"encoding/json"
	"errors"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
)

type client struct {
	apiKey     string
	endpoint   string
	httpClient *http.Client
}

func NewClient(apiKey string) (c *client) {
	c = new(client)
	c.apiKey = apiKey
	c.endpoint = "https://api.coinjar.io/v1"
	c.httpClient = new(http.Client)
	return
}

type User struct {
	UUID               string
	Email              string
	FullName           string `json:"full_name"`
	AvailableBalance   string `json:"available_balance"`
	UnconfirmedBalance string `json:"unconfirmed_balance"`
}

func (c *client) Account() (obj *User, err error) {
	body, err := c.read("account.json")
	if err != nil {
		return
	}

	var wrapper struct{ User *User }
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.User, nil
}

type BitcoinAddress struct {
	Label          string
	TotalConfirmed string `json:"total_confirmed"`
	TotalReceived  string `json:"total_received"`
	Address        string
}

func (c *client) BitcoinAddresses() ([]BitcoinAddress, error) {
	return c.BitcoinAddressesList(100, 0)
}

func (c *client) BitcoinAddressesList(limit, offset int) (obj []BitcoinAddress, err error) {
	body, err := c.read("bitcoin_addresses.json",
		"limit", strconv.Itoa(limit),
		"offset", strconv.Itoa(offset))
	if err != nil {
		return
	}

	var wrapper struct {
		Addresses []BitcoinAddress `json:"bitcoin_addresses"`
	}
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.Addresses, nil
}

func (c *client) BitcoinAddress(address string) (obj *BitcoinAddress, err error) {
	body, err := c.read("bitcoin_addresses/" + address + ".json")
	if err != nil {
		return
	}
	if string(body) == "null" {
		return nil, errors.New("Bitcoin address not found")
	}

	var wrapper struct {
		Address *BitcoinAddress `json:"bitcoin_address"`
	}
	err = json.Unmarshal(body, &wrapper)
	if err != nil {
		return
	}
	return wrapper.Address, nil
}

type FairRate struct {
	Bid  string
	Ask  string
	Spot string
}

func (c *client) FairRate(currency string) (obj *FairRate, err error) {
	body, err := c.read("fair_rate/" + currency + ".json")
	if err != nil {
		return
	}
	obj = new(FairRate)
	err = json.Unmarshal(body, obj)
	if err != nil {
		return
	}
	return
}

func (c *client) read(api string, params ...string) (body []byte, err error) {
	request, _ := http.NewRequest("GET", c.endpoint+"/"+api, nil)
	request.SetBasicAuth(c.apiKey, "")
	request.URL.RawQuery = createQuery(params)

	resp, err := c.httpClient.Do(request)
	if err != nil {
		return
	}

	defer resp.Body.Close()
	body, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		return
	}
	return
}

func createQuery(params []string) string {
	plen := len(params)
	if plen%2 == 1 {
		plen = plen - 1
	}
	values := url.Values{}
	for i := 0; i < plen; i += 2 {
		values.Set(params[i], params[i+1])
	}
	return values.Encode()
}
<|endoftext|>


<commit_before>package box

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// TODO(ttacon):some of these fields pop up everywhere, make
// common struct and anonymously extend the others with it?
// Documentation: https://developers.box.com/docs/#collaborations
type Collaboration struct {
	Type           string  `json:"type"`
	ID             string  `json:"id"`
	CreatedBy      *Item   `json:"created_by"`  // TODO(ttacon): this should be user
	CreatedAt      string  `json:"created_at"`  // TODO(ttacon): transition this to time.Time
	ModifiedAt     string  `json:"modified_at"` // TODO(ttacon): transition to time.Time
	ExpiresAt      *string `json:"expires_at"`  // TODO(ttacon): *time.Time
	Status         string  `json:"status"`
	AccessibleBy   *Item   `json:"accessible_by"`   // TODO(ttacon): turn into user
	Role           string  `json:"role"`            // TODO(ttacon): enum (own file?)
	AcknowledgedAt string  `json:"acknowledged_at"` // TODO(ttacon): time.Time
	Item           *Item   `json:"item"`            // TODO(ttacon): mini-folder struct
}

type Collaborations struct {
	TotalCount int `json:"total_count"`
	Entries    []*Collaboration
}

// Documentation: https://developers.box.com/docs/#collaborations-add-a-collaboration
func (c *Client) AddCollaboration(
	itemId,
	itemType,
	accessibleId,
	accessibleType,
	accessibleEmail,
	role string) (*http.Response, *Collaboration, error) {
	// TODO(ttacon): shrink param list

	var dataMap = map[string]interface{}{
		"item": map[string]string{
			"id":   itemId,
			"type": itemType,
		},
		"accessible_by": map[string]string{
			"id":   accessibleId,
			"type": accessibleType,
		},
		"role": role,
	}
	if len(accessibleEmail) > 0 {
		v, _ := dataMap["accessible_by"].(map[string]string)
		v["login"] = accessibleEmail
	}

	req, err := c.NewRequest(
		"POST",
		fmt.Sprintf("/collaborations"), // TODO(ttacon): remove Sprintf call - it's useless
		dataMap,
	)
	if err != nil {
		return nil, nil, err
	}

	var data *Collaboration
	resp, err := c.Do(req, data)
	return resp, data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-edit-a-collaboration
func (c *Client) EditCollaboration(collaborationId, role, status string) (*http.Response, *Collaboration, error) {
	var dataMap = make(map[string]interface{})
	if len(role) > 0 {
		dataMap["role"] = role
	}
	if len(status) > 0 {
		dataMap["status"] = status
	}

	req, err := c.NewRequest(
		"PUT",
		fmt.Sprintf("/collaborations/%s", collaborationId),
		dataMap,
	)
	if err != nil {
		return nil, nil, err
	}

	var data *Collaboration
	resp, err := c.Do(req, data)
	return resp, data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-remove-a-collaboration
func (c *Client) RemoveCollaboration(collaborationId string) (*http.Response, error) {
	req, err := c.NewRequest(
		"DELETE",
		fmt.Sprintf("/collaborations/%s", collaborationId),
		nil,
	)
	if err != nil {
		return nil, err
	}

	return c.Do(req, nil)
}

// Documentation: https://developers.box.com/docs/#collaborations-retrieve-a-collaboration
func (c *Client) RetrieveCollaboration(collaborationId string) (*http.Response, *Collaboration, error) {
	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s/collaborations/%s", BASE_URL, collaborationId),
		nil,
	)
	if err != nil {
		return nil, nil, err
	}

	resp, err := c.Trans.Client().Do(req)
	if err != nil {
		return resp, nil, err
	}

	var data Collaboration
	err = json.NewDecoder(resp.Body).Decode(&data)
	return resp, &data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-get-pending-collaborations
// NOTE(ttacon): not doing to add param since it's just calling the first url with an explicit
// query string (that never changes, why isn't it an actual route then, or bundled into the
// documentation of the first one?)
func (c *Client) GetPendingCollaborations() (*http.Response, *Collaborations, error) {
	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s/collaborations?status=pending", BASE_URL),
		nil,
	)
	if err != nil {
		return nil, nil, err
	}

	resp, err := c.Trans.Client().Do(req)
	if err != nil {
		return resp, nil, err
	}

	var data Collaborations
	err = json.NewDecoder(resp.Body).Decode(&data)
	return resp, &data, err
}
<commit_msg>refactor retrieve collaboration<commit_after>package box

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// TODO(ttacon):some of these fields pop up everywhere, make
// common struct and anonymously extend the others with it?
// Documentation: https://developers.box.com/docs/#collaborations
type Collaboration struct {
	Type           string  `json:"type"`
	ID             string  `json:"id"`
	CreatedBy      *Item   `json:"created_by"`  // TODO(ttacon): this should be user
	CreatedAt      string  `json:"created_at"`  // TODO(ttacon): transition this to time.Time
	ModifiedAt     string  `json:"modified_at"` // TODO(ttacon): transition to time.Time
	ExpiresAt      *string `json:"expires_at"`  // TODO(ttacon): *time.Time
	Status         string  `json:"status"`
	AccessibleBy   *Item   `json:"accessible_by"`   // TODO(ttacon): turn into user
	Role           string  `json:"role"`            // TODO(ttacon): enum (own file?)
	AcknowledgedAt string  `json:"acknowledged_at"` // TODO(ttacon): time.Time
	Item           *Item   `json:"item"`            // TODO(ttacon): mini-folder struct
}

type Collaborations struct {
	TotalCount int `json:"total_count"`
	Entries    []*Collaboration
}

// Documentation: https://developers.box.com/docs/#collaborations-add-a-collaboration
func (c *Client) AddCollaboration(
	itemId,
	itemType,
	accessibleId,
	accessibleType,
	accessibleEmail,
	role string) (*http.Response, *Collaboration, error) {
	// TODO(ttacon): shrink param list

	var dataMap = map[string]interface{}{
		"item": map[string]string{
			"id":   itemId,
			"type": itemType,
		},
		"accessible_by": map[string]string{
			"id":   accessibleId,
			"type": accessibleType,
		},
		"role": role,
	}
	if len(accessibleEmail) > 0 {
		v, _ := dataMap["accessible_by"].(map[string]string)
		v["login"] = accessibleEmail
	}

	req, err := c.NewRequest(
		"POST",
		fmt.Sprintf("/collaborations"), // TODO(ttacon): remove Sprintf call - it's useless
		dataMap,
	)
	if err != nil {
		return nil, nil, err
	}

	var data *Collaboration
	resp, err := c.Do(req, data)
	return resp, data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-edit-a-collaboration
func (c *Client) EditCollaboration(collaborationId, role, status string) (*http.Response, *Collaboration, error) {
	var dataMap = make(map[string]interface{})
	if len(role) > 0 {
		dataMap["role"] = role
	}
	if len(status) > 0 {
		dataMap["status"] = status
	}

	req, err := c.NewRequest(
		"PUT",
		fmt.Sprintf("/collaborations/%s", collaborationId),
		dataMap,
	)
	if err != nil {
		return nil, nil, err
	}

	var data *Collaboration
	resp, err := c.Do(req, data)
	return resp, data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-remove-a-collaboration
func (c *Client) RemoveCollaboration(collaborationId string) (*http.Response, error) {
	req, err := c.NewRequest(
		"DELETE",
		fmt.Sprintf("/collaborations/%s", collaborationId),
		nil,
	)
	if err != nil {
		return nil, err
	}

	return c.Do(req, nil)
}

// Documentation: https://developers.box.com/docs/#collaborations-retrieve-a-collaboration
func (c *Client) RetrieveCollaboration(collaborationId string) (*http.Response, *Collaboration, error) {
	req, err := c.NewRequest(
		"GET",
		fmt.Sprintf("/collaborations/%s", collaborationId),
		nil,
	)
	if err != nil {
		return nil, nil, err
	}

	var data *Collaboration
	resp, err := c.Do(req, data)
	return resp, data, err
}

// Documentation: https://developers.box.com/docs/#collaborations-get-pending-collaborations
// NOTE(ttacon): not doing to add param since it's just calling the first url with an explicit
// query string (that never changes, why isn't it an actual route then, or bundled into the
// documentation of the first one?)
func (c *Client) GetPendingCollaborations() (*http.Response, *Collaborations, error) {
	req, err := http.NewRequest(
		"GET",
		fmt.Sprintf("%s/collaborations?status=pending", BASE_URL),
		nil,
	)
	if err != nil {
		return nil, nil, err
	}

	resp, err := c.Trans.Client().Do(req)
	if err != nil {
		return resp, nil, err
	}

	var data Collaborations
	err = json.NewDecoder(resp.Body).Decode(&data)
	return resp, &data, err
}
<|endoftext|>


<commit_before>package managexe

import (
	"context"
	"testing"
	"time"
)

type testA struct {
	ch chan struct{}
}

func (a *testA) wait() {
	<-a.ch
}

func (a *testA) Exec(ctx context.Context) error {
	time.Sleep(1 * time.Second)
	close(a.ch)
	return nil
}

func TestRun(t *testing.T) {
	manager := NewManager(3, 100)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go manager.Run(ctx)

	start := time.Now()
	list := []*testA{{make(chan struct{})}, {make(chan struct{})}, {make(chan struct{})}}
	for _, a := range list {
		manager.AddTask(a)
	}

	if n := manager.NumTask(); n != 3 {
		t.Errorf("[error] number of task. got %d, wont %d", n, 3)
		return
	}

	for _, a := range list {
		a.wait()
	}
	end := time.Now()

	t.Logf("execution time: %s", end.Sub(start))
}
<commit_msg>pauseとresumeのテストを追加<commit_after>package managexe

import (
	"context"
	"testing"
	"time"
)

type testA struct {
	ch chan struct{}
	fn func()
}

func (a *testA) wait() {
	<-a.ch
}

func (a *testA) Exec(ctx context.Context) error {
	time.Sleep(1 * time.Second)
	if a.fn != nil {
		a.fn()
	}
	close(a.ch)
	return nil
}

func TestRun(t *testing.T) {
	manager := NewManager(3, 100)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go manager.Run(ctx)

	start := time.Now()
	list := []*testA{
		{make(chan struct{}), func() {}},
		{make(chan struct{}), func() {}},
		{make(chan struct{}), func() {}},
	}
	for _, a := range list {
		manager.AddTask(a)
	}

	if n := manager.NumTask(); n != 3 {
		t.Errorf("[error] number of task. got %d, wont %d", n, 3)
		return
	}

	for _, a := range list {
		a.wait()
	}
	end := time.Now()

	t.Logf("execution time: %s", end.Sub(start))
}

func TestPauseAndResume(t *testing.T) {
	manager := NewManager(3, 100)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go manager.Run(ctx)
	time.Sleep(500 * time.Millisecond)
	manager.Pause()

	ch := make(chan struct{})
	count := 0
	go func() {
		for range ch {
			count++
		}
	}()

	list := []*testA{
		{make(chan struct{}), func() { ch <- struct{}{} }},
		{make(chan struct{}), func() { ch <- struct{}{} }},
		{make(chan struct{}), func() { ch <- struct{}{} }},
	}
	for _, a := range list {
		manager.AddTask(a)
	}

	time.Sleep(2 * time.Second)

	if count != 0 {
		t.Errorf("[error] count. got %d, wont %d", count, 0)
		return
	}

	manager.Resume()

	time.Sleep(2 * time.Second)

	if count != 3 {
		t.Errorf("[error] count. got %d, wont %d", count, 3)
		return
	}

	for _, a := range list {
		a.wait()
	}
}
<|endoftext|>


<commit_before>// Copyright 2015 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue is a client for reading GitHub project issues.
//
//	usage: issue [-a] [-p owner/repo] <query>
//
// Issue runs the query against the given project's issue tracker and
// prints a table of matching issues, sorted by issue summary.
// The default owner/repo is golang/go.
//
// If multiple arguments are given as the query, issue joins them by
// spaces to form a single issue search. These two commands are equivalent:
//
//	issue assignee:rsc author:robpike
//	issue "assignee:rsc author:robpike"
//
// Searches are always limited to open issues.
//
// If the query is a single number, issue prints that issue in detail,
// including all comments.
//
// Acme
//
// If the -a flag is specified, issue runs as a collection of acme windows
// instead of a command-line tool. In this mode, the query is optional.
// If no query is given, issue uses "state:open".
//
// There are three kinds of acme windows: issue, issue creation, issue list,
// search result, and milestone list.
//
// The following text forms can be looked for (right clicked on)
// and open a window (or navigate to an existing one).
//
//	nnnn			issue #nnnn
//	#nnnn			issue #nnnn
//	all			the issue list
//	milestone(s)		the milestone list
//	<milestone-name>	the named milestone (e.g., Go1.5)
//
// Executing "New" opens an issue creation window.
//
// Executing "Search <query>" opens a new window showing the
// results of that search.
//
// Issue Window
//
// An issue window, opened by loading an issue number,
// displays full detail about an issue, a header followed by each comment.
// For example:
//
//	Title: time: Duration should implement fmt.Formatter
//	State: closed
//	Assignee: robpike
//	Closed: 2015-01-08 05:20:00
//	Labels: release-none repo-main size-m
//	Milestone:
//	URL: https://github.com/golang/go/issues/8786
//
//	Reported by dsymonds (2014-09-21 23:02:50)
//
//		It'd be nice if http://play.golang.org/p/KCnUQOPyol
//		printed "[+3us]", which would require time.Duration
//		implementing fmt.Formatter to get the '+' flag.
//
//	Comment by rsc (2015-01-08 05:17:06)
//
//		time must not depend on fmt.
//
// Executing "Get" reloads the issue data.
//
// Executing "Put" updates an issue. It saves any changes to the issue header
// and, if any text has been entered between the header and the "Reported by" line,
// posts that text as a new comment. If both succeed, Put then reloads the issue data.
// The "Closed" and "URL" headers cannot be changed.
//
// Issue Creation Window
//
// An issue creation window, opened by executing "New", is like an issue window
// but displays only an empty issue template:
//
//	Title:
//	Assignee:
//	Labels:
//	Milestone:
//
//	<describe issue here>
//
// Once the template has been completed, executing "Put" creates the issue and converts
// the window into a issue window for the new issue.
//
// Issue List Window
//
// An issue list window displays a list of all open issue numbers and titles.
// If the project has any open milestones, they are listed in a header line.
// For example:
//
//	Milestones: Go1.4.1 Go1.5 Go1.5Maybe
//
//	9027	archive/tar: round-trip of Header misses values
//	8669	archive/zip: not possible to a start writing zip at offset other than zero
//	8359	archive/zip: not possible to specify deflate compression level
//	...
//
// As in any window, right clicking on an issue number opens a window for that issue.
//
// Search Result Window
//
// A search result window, opened by executing "Search <query>", displays a list of issues
// matching a search query. It shows the query in a header line. For example:
//
//	Search: author:rsc
//
//	#9131	bench: no documentation
//	#599	cmd/5c, 5g, 8c, 8g: make 64-bit fields 64-bit aligned
//	#6699	cmd/5l: use m to store div/mod denominator
//	#4997	cmd/6a, cmd/8a: MOVL $x-8(SP) and LEAL x-8(SP) are different
//	...
//
// Milestone List Window
//
// The milestone list window, opened by loading any of the names
// "milestone", "Milestone", or "Milestones", displays the open project
// milestones, sorted by due date, along with the number of open issues in each.
// For example:
//
//	2015-01-15	Go1.4.1		1
//	2015-07-31	Go1.5		215
//	2015-07-31	Go1.5Maybe	5
//
// Loading one of the listed milestone names opens a search for issues
// in that milestone.
//
package main // import "rsc.io/github/issue"

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

var (
	acmeFlag = flag.Bool("a", false, "acme")

	project      = flag.String("p", "golang/go", "GitHub owner/repo name")
	projectOwner = ""
	projectRepo  = ""
)

func usage() {
	fmt.Fprintf(os.Stderr, `usage: issue [-a] [-p owner/repo] <query>

If query is a single number, prints the full history for the issue.
Otherwise, prints a table of matching results.
`)
	os.Exit(2)
}

func main() {
	flag.Usage = usage
	flag.Parse()
	log.SetFlags(0)
	log.SetPrefix("issue: ")

	if flag.NArg() == 0 && !*acmeFlag {
		usage()
	}
	q := strings.Join(flag.Args(), " ")

	f := strings.Split(*project, "/")
	if len(f) != 2 {
		log.Fatal("invalid form for -p argument: must be owner/repo, like golang/go")
	}
	projectOwner = f[0]
	projectRepo = f[1]

	loadAuth()

	if *acmeFlag {
		acmeMode()
	}

	n, _ := strconv.Atoi(q)
	if n != 0 {
		if _, err := showIssue(os.Stdout, n); err != nil {
			log.Fatal(err)
		}
		return
	}

	if err := showQuery(os.Stdout, q); err != nil {
		log.Fatal(err)
	}
}

func showIssue(w io.Writer, n int) (*github.Issue, error) {
	issue, _, err := client.Issues.Get(projectOwner, projectRepo, n)
	if err != nil {
		return nil, err
	}
	return issue, printIssue(w, issue)
}

const timeFormat = "2006-01-02 15:04:05"

func printIssue(w io.Writer, issue *github.Issue) error {
	fmt.Fprintf(w, "Title: %s\n", getString(issue.Title))
	fmt.Fprintf(w, "State: %s\n", getString(issue.State))
	fmt.Fprintf(w, "Assignee: %s\n", getUserLogin(issue.Assignee))
	if issue.ClosedAt != nil {
		fmt.Fprintf(w, "Closed: %s\n", getTime(issue.ClosedAt).Format(timeFormat))
	}
	fmt.Fprintf(w, "Labels: %s\n", strings.Join(getLabelNames(issue.Labels), " "))
	fmt.Fprintf(w, "Milestone: %s\n", getMilestoneTitle(issue.Milestone))
	fmt.Fprintf(w, "URL: https://github.com/%s/%s/issues/%d\n", projectOwner, projectRepo, getInt(issue.Number))

	fmt.Fprintf(w, "\nReported by %s (%s)\n", getUserLogin(issue.User), getTime(issue.CreatedAt).Format(timeFormat))
	if issue.Body != nil {
		text := strings.TrimSpace(*issue.Body)
		if text != "" {
			fmt.Fprintf(w, "\n\t%s\n", wrap(text, "\t"))
		}
	}

	for page := 1; ; {
		list, resp, err := client.Issues.ListComments(projectOwner, projectRepo, getInt(issue.Number), &github.IssueListCommentsOptions{
			ListOptions: github.ListOptions{
				Page:    page,
				PerPage: 100,
			},
		})
		for _, com := range list {
			fmt.Fprintf(w, "\nComment by %s (%s)\n", getUserLogin(com.User), getTime(com.CreatedAt).Format(timeFormat))
			if com.Body != nil {
				text := strings.TrimSpace(*com.Body)
				if text != "" {
					fmt.Fprintf(w, "\n\t%s\n", wrap(text, "\t"))
				}
			}
		}
		if err != nil {
			return err
		}
		if resp.NextPage < page {
			break
		}
		page = resp.NextPage
	}
	return nil
}

func showQuery(w io.Writer, q string) error {
	var all []string
	for page := 1; ; {
		x, resp, err := client.Search.Issues("type:issue state:open repo:"+*project+" "+q, &github.SearchOptions{
			ListOptions: github.ListOptions{
				Page:    page,
				PerPage: 100,
			},
		})
		if err != nil {
			return err
		}
		for _, issue := range x.Issues {
			all = append(all, fmt.Sprintf("%s\t%d", getString(issue.Title), getInt(issue.Number)))
		}
		if resp.NextPage < page {
			break
		}
		page = resp.NextPage
	}
	sort.Strings(all)
	for _, s := range all {
		i := strings.LastIndex(s, "\t")
		fmt.Fprintf(w, "%s\t%s\n", s[i+1:], s[:i])
	}
	return nil
}

func loadMilestones() ([]github.Milestone, error) {
	// NOTE(rsc): There appears to be no paging possible.
	all, _, err := client.Issues.ListMilestones(projectOwner, projectRepo, &github.MilestoneListOptions{
		State: "open",
	})
	if err != nil {
		return nil, err
	}
	if all == nil {
		all = []github.Milestone{}
	}
	return all, nil
}

func wrap(t string, prefix string) string {
	out := ""
	t = strings.Replace(t, "\r\n", "\n", -1)
	lines := strings.Split(t, "\n")
	for i, line := range lines {
		if i > 0 {
			out += "\n" + prefix
		}
		s := line
		for len(s) > 70 {
			i := strings.LastIndex(s[:70], " ")
			if i < 0 {
				i = 69
			}
			i++
			out += s[:i] + "\n" + prefix
			s = s[i:]
		}
		out += s
	}
	return out
}

var client *github.Client

// GitHub personal access token, from https://github.com/settings/applications.
var authToken string

func loadAuth() {
	const short = ".github-issue-token"
	filename := filepath.Clean(os.Getenv("HOME") + "/" + short)
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		log.Fatal("reading token: ", err, "\n\n"+
			"Please create a personal access token at https://github.com/settings/tokens/new\n"+
			"and write it to ", filepath.Clean("$HOME/"+short), " to use this program.\n"+
			"The token only needs the repo scope, or private_repo if you want to\n"+
			"view or edit issues for private repositories.\n"+
			"The benefit of using a personal access token over using your GitHub\n"+
			"password directly is that you can limit its use and revoke it at any time.\n\n")
	}
	fi, err := os.Stat(filename)
	if fi.Mode()&0077 != 0 {
		log.Fatalf("reading token: %s mode is %#o, want %#o", filepath.Clean("$HOME/"+short), fi.Mode()&0777, fi.Mode()&0700)
	}
	authToken = strings.TrimSpace(string(data))
	t := &oauth2.Transport{
		Source: &tokenSource{AccessToken: authToken},
	}
	client = github.NewClient(&http.Client{Transport: t})
}

type tokenSource oauth2.Token

func (t *tokenSource) Token() (*oauth2.Token, error) {
	return (*oauth2.Token)(t), nil
}

func getInt(x *int) int {
	if x == nil {
		return 0
	}
	return *x
}

func getString(x *string) string {
	if x == nil {
		return ""
	}
	return *x
}

func getUserLogin(x *github.User) string {
	if x == nil || x.Login == nil {
		return ""
	}
	return *x.Login
}

func getTime(x *time.Time) time.Time {
	if x == nil {
		return time.Time{}
	}
	return *x
}

func getMilestoneTitle(x *github.Milestone) string {
	if x == nil || x.Title == nil {
		return ""
	}
	return *x.Title
}

func getLabelNames(x []github.Label) []string {
	var out []string
	for _, lab := range x {
		out = append(out, getString(lab.Name))
	}
	return out
}
<commit_msg>issue: describe authentication<commit_after>// Copyright 2015 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue is a client for reading GitHub project issues.
//
//	usage: issue [-a] [-p owner/repo] <query>
//
// Issue runs the query against the given project's issue tracker and
// prints a table of matching issues, sorted by issue summary.
// The default owner/repo is golang/go.
//
// If multiple arguments are given as the query, issue joins them by
// spaces to form a single issue search. These two commands are equivalent:
//
//	issue assignee:rsc author:robpike
//	issue "assignee:rsc author:robpike"
//
// Searches are always limited to open issues.
//
// If the query is a single number, issue prints that issue in detail,
// including all comments.
//
// Authentication
//
// Issue expects to find a GitHub "personal access token" in
// $HOME/.github-issue-token and will use that token to authenticate
// to GitHub when reading or writing issue data.
// A token can be created by visiting https://github.com/settings/tokens/new.
// The token only needs the 'repo' scope checkbox, and optionally 'private_repo'
// if you want to work with issue trackers for private repositories.
// It does not need any other permissions.
//
// Acme
//
// If the -a flag is specified, issue runs as a collection of acme windows
// instead of a command-line tool. In this mode, the query is optional.
// If no query is given, issue uses "state:open".
//
// There are three kinds of acme windows: issue, issue creation, issue list,
// search result, and milestone list.
//
// The following text forms can be looked for (right clicked on)
// and open a window (or navigate to an existing one).
//
//	nnnn			issue #nnnn
//	#nnnn			issue #nnnn
//	all			the issue list
//	milestone(s)		the milestone list
//	<milestone-name>	the named milestone (e.g., Go1.5)
//
// Executing "New" opens an issue creation window.
//
// Executing "Search <query>" opens a new window showing the
// results of that search.
//
// Issue Window
//
// An issue window, opened by loading an issue number,
// displays full detail about an issue, a header followed by each comment.
// For example:
//
//	Title: time: Duration should implement fmt.Formatter
//	State: closed
//	Assignee: robpike
//	Closed: 2015-01-08 05:20:00
//	Labels: release-none repo-main size-m
//	Milestone:
//	URL: https://github.com/golang/go/issues/8786
//
//	Reported by dsymonds (2014-09-21 23:02:50)
//
//		It'd be nice if http://play.golang.org/p/KCnUQOPyol
//		printed "[+3us]", which would require time.Duration
//		implementing fmt.Formatter to get the '+' flag.
//
//	Comment by rsc (2015-01-08 05:17:06)
//
//		time must not depend on fmt.
//
// Executing "Get" reloads the issue data.
//
// Executing "Put" updates an issue. It saves any changes to the issue header
// and, if any text has been entered between the header and the "Reported by" line,
// posts that text as a new comment. If both succeed, Put then reloads the issue data.
// The "Closed" and "URL" headers cannot be changed.
//
// Issue Creation Window
//
// An issue creation window, opened by executing "New", is like an issue window
// but displays only an empty issue template:
//
//	Title:
//	Assignee:
//	Labels:
//	Milestone:
//
//	<describe issue here>
//
// Once the template has been completed, executing "Put" creates the issue and converts
// the window into a issue window for the new issue.
//
// Issue List Window
//
// An issue list window displays a list of all open issue numbers and titles.
// If the project has any open milestones, they are listed in a header line.
// For example:
//
//	Milestones: Go1.4.1 Go1.5 Go1.5Maybe
//
//	9027	archive/tar: round-trip of Header misses values
//	8669	archive/zip: not possible to a start writing zip at offset other than zero
//	8359	archive/zip: not possible to specify deflate compression level
//	...
//
// As in any window, right clicking on an issue number opens a window for that issue.
//
// Search Result Window
//
// A search result window, opened by executing "Search <query>", displays a list of issues
// matching a search query. It shows the query in a header line. For example:
//
//	Search: author:rsc
//
//	#9131	bench: no documentation
//	#599	cmd/5c, 5g, 8c, 8g: make 64-bit fields 64-bit aligned
//	#6699	cmd/5l: use m to store div/mod denominator
//	#4997	cmd/6a, cmd/8a: MOVL $x-8(SP) and LEAL x-8(SP) are different
//	...
//
// Milestone List Window
//
// The milestone list window, opened by loading any of the names
// "milestone", "Milestone", or "Milestones", displays the open project
// milestones, sorted by due date, along with the number of open issues in each.
// For example:
//
//	2015-01-15	Go1.4.1		1
//	2015-07-31	Go1.5		215
//	2015-07-31	Go1.5Maybe	5
//
// Loading one of the listed milestone names opens a search for issues
// in that milestone.
//
package main // import "rsc.io/github/issue"

import (
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

var (
	acmeFlag = flag.Bool("a", false, "acme")

	project      = flag.String("p", "golang/go", "GitHub owner/repo name")
	projectOwner = ""
	projectRepo  = ""
)

func usage() {
	fmt.Fprintf(os.Stderr, `usage: issue [-a] [-p owner/repo] <query>

If query is a single number, prints the full history for the issue.
Otherwise, prints a table of matching results.
`)
	os.Exit(2)
}

func main() {
	flag.Usage = usage
	flag.Parse()
	log.SetFlags(0)
	log.SetPrefix("issue: ")

	if flag.NArg() == 0 && !*acmeFlag {
		usage()
	}
	q := strings.Join(flag.Args(), " ")

	f := strings.Split(*project, "/")
	if len(f) != 2 {
		log.Fatal("invalid form for -p argument: must be owner/repo, like golang/go")
	}
	projectOwner = f[0]
	projectRepo = f[1]

	loadAuth()

	if *acmeFlag {
		acmeMode()
	}

	n, _ := strconv.Atoi(q)
	if n != 0 {
		if _, err := showIssue(os.Stdout, n); err != nil {
			log.Fatal(err)
		}
		return
	}

	if err := showQuery(os.Stdout, q); err != nil {
		log.Fatal(err)
	}
}

func showIssue(w io.Writer, n int) (*github.Issue, error) {
	issue, _, err := client.Issues.Get(projectOwner, projectRepo, n)
	if err != nil {
		return nil, err
	}
	return issue, printIssue(w, issue)
}

const timeFormat = "2006-01-02 15:04:05"

func printIssue(w io.Writer, issue *github.Issue) error {
	fmt.Fprintf(w, "Title: %s\n", getString(issue.Title))
	fmt.Fprintf(w, "State: %s\n", getString(issue.State))
	fmt.Fprintf(w, "Assignee: %s\n", getUserLogin(issue.Assignee))
	if issue.ClosedAt != nil {
		fmt.Fprintf(w, "Closed: %s\n", getTime(issue.ClosedAt).Format(timeFormat))
	}
	fmt.Fprintf(w, "Labels: %s\n", strings.Join(getLabelNames(issue.Labels), " "))
	fmt.Fprintf(w, "Milestone: %s\n", getMilestoneTitle(issue.Milestone))
	fmt.Fprintf(w, "URL: https://github.com/%s/%s/issues/%d\n", projectOwner, projectRepo, getInt(issue.Number))

	fmt.Fprintf(w, "\nReported by %s (%s)\n", getUserLogin(issue.User), getTime(issue.CreatedAt).Format(timeFormat))
	if issue.Body != nil {
		text := strings.TrimSpace(*issue.Body)
		if text != "" {
			fmt.Fprintf(w, "\n\t%s\n", wrap(text, "\t"))
		}
	}

	for page := 1; ; {
		list, resp, err := client.Issues.ListComments(projectOwner, projectRepo, getInt(issue.Number), &github.IssueListCommentsOptions{
			ListOptions: github.ListOptions{
				Page:    page,
				PerPage: 100,
			},
		})
		for _, com := range list {
			fmt.Fprintf(w, "\nComment by %s (%s)\n", getUserLogin(com.User), getTime(com.CreatedAt).Format(timeFormat))
			if com.Body != nil {
				text := strings.TrimSpace(*com.Body)
				if text != "" {
					fmt.Fprintf(w, "\n\t%s\n", wrap(text, "\t"))
				}
			}
		}
		if err != nil {
			return err
		}
		if resp.NextPage < page {
			break
		}
		page = resp.NextPage
	}
	return nil
}

func showQuery(w io.Writer, q string) error {
	var all []string
	for page := 1; ; {
		x, resp, err := client.Search.Issues("type:issue state:open repo:"+*project+" "+q, &github.SearchOptions{
			ListOptions: github.ListOptions{
				Page:    page,
				PerPage: 100,
			},
		})
		if err != nil {
			return err
		}
		for _, issue := range x.Issues {
			all = append(all, fmt.Sprintf("%s\t%d", getString(issue.Title), getInt(issue.Number)))
		}
		if resp.NextPage < page {
			break
		}
		page = resp.NextPage
	}
	sort.Strings(all)
	for _, s := range all {
		i := strings.LastIndex(s, "\t")
		fmt.Fprintf(w, "%s\t%s\n", s[i+1:], s[:i])
	}
	return nil
}

func loadMilestones() ([]github.Milestone, error) {
	// NOTE(rsc): There appears to be no paging possible.
	all, _, err := client.Issues.ListMilestones(projectOwner, projectRepo, &github.MilestoneListOptions{
		State: "open",
	})
	if err != nil {
		return nil, err
	}
	if all == nil {
		all = []github.Milestone{}
	}
	return all, nil
}

func wrap(t string, prefix string) string {
	out := ""
	t = strings.Replace(t, "\r\n", "\n", -1)
	lines := strings.Split(t, "\n")
	for i, line := range lines {
		if i > 0 {
			out += "\n" + prefix
		}
		s := line
		for len(s) > 70 {
			i := strings.LastIndex(s[:70], " ")
			if i < 0 {
				i = 69
			}
			i++
			out += s[:i] + "\n" + prefix
			s = s[i:]
		}
		out += s
	}
	return out
}

var client *github.Client

// GitHub personal access token, from https://github.com/settings/applications.
var authToken string

func loadAuth() {
	const short = ".github-issue-token"
	filename := filepath.Clean(os.Getenv("HOME") + "/" + short)
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		log.Fatal("reading token: ", err, "\n\n"+
			"Please create a personal access token at https://github.com/settings/tokens/new\n"+
			"and write it to ", filepath.Clean("$HOME/"+short), " to use this program.\n"+
			"The token only needs the repo scope, or private_repo if you want to\n"+
			"view or edit issues for private repositories.\n"+
			"The benefit of using a personal access token over using your GitHub\n"+
			"password directly is that you can limit its use and revoke it at any time.\n\n")
	}
	fi, err := os.Stat(filename)
	if fi.Mode()&0077 != 0 {
		log.Fatalf("reading token: %s mode is %#o, want %#o", filepath.Clean("$HOME/"+short), fi.Mode()&0777, fi.Mode()&0700)
	}
	authToken = strings.TrimSpace(string(data))
	t := &oauth2.Transport{
		Source: &tokenSource{AccessToken: authToken},
	}
	client = github.NewClient(&http.Client{Transport: t})
}

type tokenSource oauth2.Token

func (t *tokenSource) Token() (*oauth2.Token, error) {
	return (*oauth2.Token)(t), nil
}

func getInt(x *int) int {
	if x == nil {
		return 0
	}
	return *x
}

func getString(x *string) string {
	if x == nil {
		return ""
	}
	return *x
}

func getUserLogin(x *github.User) string {
	if x == nil || x.Login == nil {
		return ""
	}
	return *x.Login
}

func getTime(x *time.Time) time.Time {
	if x == nil {
		return time.Time{}
	}
	return *x
}

func getMilestoneTitle(x *github.Milestone) string {
	if x == nil || x.Title == nil {
		return ""
	}
	return *x.Title
}

func getLabelNames(x []github.Label) []string {
	var out []string
	for _, lab := range x {
		out = append(out, getString(lab.Name))
	}
	return out
}
<|endoftext|>


<commit_before>// Package gcfg reads "gitconfig-like" text-based configuration files with
// "name=value" pairs grouped into sections (gcfg files).
// Support for modifying and/or exporting gcfg files may be added later.
//
// See Parse and the examples to get an idea of how to use it.
//
// This package is still a work in progress, and both the supported syntax and
// the API is subject to change.
//
// The syntax is based on that used by git config:
// http://git-scm.com/docs/git-config#_syntax .
// Note that the gcfg syntax may diverge from that of git config in the future
// to a limited degree.
// Current differences (apart from TODOs listed below) are:
//  - gcfg files must use UTF-8 encoding (for now)
//  - include and "path" type is not supported at the package level
//  - `[sec.sub]` format is not allowed (deprecated in gitconfig)
//  - `[sec ""]` is not allowed
//    - `[sec]` is the equivalent (section name "sec" and empty subsection name)
//
// The package may be usable for handling some of the various "INI file" formats
// used by some programs and libraries, but achieving or maintaining
// compatibility with any of those is not a primary concern.
//
// TODO:
//  - docs
//    - format spec
//  - parsing
//    - define internal representation structure
//    - support multi-value variables
//    - non-regexp based parser
//    - support escaping in strings
//    - support multiple inputs (readers, strings, files)
//    - support declaring encoding (?)
//    - support pointer fields
//  - modifying / exporting gcfg files
//  - error handling
//    - include error context
//    - more helpful error messages
//    - error types / codes?
//    - limit input size?
//
package gcfg

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"regexp"
	"strings"
)

var (
	reCmnt    = regexp.MustCompile(`^([^;#"]*)[;#].*$`)
	reCmntQ   = regexp.MustCompile(`^([^;#"]*"[^"]*"[^;#"]*)[;#].*$`)
	reBlank   = regexp.MustCompile(`^\s*$`)
	reSect    = regexp.MustCompile(`^\s*\[\s*([^"\s]*)\s*\]\s*$`)
	reSectSub = regexp.MustCompile(`^\s*\[\s*([^"\s]*)\s*"([^"]+)"\s*\]\s*$`)
	reVar     = regexp.MustCompile(`^\s*([^"=\s]+)\s*=\s*([^"\s]*)\s*$`)
	reVarQ    = regexp.MustCompile(`^\s*([^"=\s]+)\s*=\s*"([^"\n\\]*)"\s*$`)
	reVarDflt = regexp.MustCompile(`^\s*\b(.*)\b\s*$`)
)

const (
	// Default value string in case a value for a variable isn't provided.
	DefaultValue = "true"
)

type gbool bool

var boolValues = map[string]interface{}{
	"true": true, "yes": true, "on": true, "1": true,
	"false": false, "no": false, "off": false, "0": false}

func scan(state fmt.ScanState, values map[string]interface{}) (
	interface{}, error) {
	//
	var rd []rune
	var r rune
	var err error
	for r, _, err = state.ReadRune(); err == nil; r, _, err = state.ReadRune() {
		rd = append(rd, r)
		part := false
		for s, v := range values {
			if strings.EqualFold(string(rd), s) {
				return v, err
			}
			if len(rd) < len(s) && strings.EqualFold(string(rd), s[:len(rd)]) {
				part = true
			}
		}
		if part == false {
			state.UnreadRune()
			return nil, errors.New("unsupported value " + string(rd))
		}
	}
	return nil, err
}

func (b *gbool) Scan(state fmt.ScanState, verb rune) error {
	v, err := scan(state, boolValues)
	switch bb := v.(type) {
	case bool:
		*b = gbool(bb)
		return err
	}
	return err
}

func fieldFold(v reflect.Value, name string) reflect.Value {
	n := strings.Replace(name, "-", "_", -1)
	return v.FieldByNameFunc(func(fieldName string) bool {
		return strings.EqualFold(n, fieldName)
	})
}

func set(cfg interface{}, sect, sub, name, value string) error {
	vDest := reflect.ValueOf(cfg).Elem()
	vSect := fieldFold(vDest, sect)
	if vSect.Kind() == reflect.Map {
		if vSect.IsNil() {
			vSect.Set(reflect.MakeMap(vSect.Type()))
		}
		k := reflect.ValueOf(sub)
		pv := vSect.MapIndex(k)
		if !pv.IsValid() {
			vType := vSect.Type().Elem().Elem()
			pv = reflect.New(vType)
			vSect.SetMapIndex(k, pv)
		}
		vSect = pv.Elem()
	} else if sub != "" {
		return fmt.Errorf("expected map; section %q subsection %q", sect, sub)
	}
	vName := fieldFold(vSect, name)
	vAddr := vName.Addr().Interface()
	switch v := vAddr.(type) {
	case *string:
		*v = value
		return nil
	case *bool:
		vAddr = (*gbool)(v)
	}
	// attempt to read an extra rune to make sure the value is consumed
	var r rune
	n, err := fmt.Sscanf(value, "%v%c", vAddr, &r)
	switch {
	case n < 1 || n == 1 && err != io.EOF:
		return fmt.Errorf("failed to parse %q as %#v: parse error %v", value,
			vName.Type(), err)
	case n > 1:
		return fmt.Errorf("failed to parse %q as %#v: extra characters", value,
			vName.Type())
	case n == 1 && err == io.EOF:
		return nil
	}
	panic("never reached")
}

// Parse reads gcfg formatted data from reader and sets the values into the
// corresponding fields in config.
//
// Config must be a pointer to a struct.
// Each section corresponds to a struct field in config, and each variable in a
// section corresponds to a data field in the section struct.
// The field names must match that of the section or the variable, ignoring
// case.
// Hyphens are replaced with underscores.
//
// For sections with subsections, the corresponding field in config must be a
// map, rather than a struct, with string keys and pointer-to-struct values.
// Values for subsection variables are stored in the map with the subsection
// name used as the map key.
// (Note that unlike section and variable names, subsection names are case
// sensitive.)
// When using a map, and there is a section with the same section name but
// without a subsection name, its values are stored with the empty string used
// as the key.
//
// The section structs in the config struct may contain arbitrary types.
// For string fields, the (unquoted and unescaped) value string is assigned to
// the field.
// For bool fields, the field is set to true if the value is "true", "yes", "on"
// or "1", and set to false if the value is "false", "no", "off" or "0",
// ignoring case.
// For all other types, fmt.Sscanf is used to parse the value and set it to the
// field.
// This means that built-in Go types are parseable using the standard format,
// and any user-defined types is parseable if it implements the fmt.Scanner
// interface.
//
// See ParseString for examples.
//
func Parse(config interface{}, reader io.Reader) error {
	r := bufio.NewReader(reader)
	sect := (*string)(nil)
	sectsub := ""
	for line := 1; true; line++ {
		l, pre, err := r.ReadLine()
		if err != nil && err != io.EOF {
			return err
		} else if pre {
			return errors.New("line too long")
		}
		// exclude comments
		if c := reCmnt.FindSubmatch(l); c != nil {
			l = c[1]
		} else if c := reCmntQ.FindSubmatch(l); c != nil {
			l = c[1]
		}
		if !reBlank.Match(l) {
			// "switch" based on line contents
			if sec := reSect.FindSubmatch(l); sec != nil {
				strsec := string(sec[1])
				sect, sectsub = &strsec, ""
			} else if sec := reSectSub.FindSubmatch(l); sec != nil {
				strsec := string(sec[1])
				strsub := string(sec[2])
				if strsub == "" {
					return errors.New("empty subsection not allowed")
				}
				sect, sectsub = &strsec, strsub
			} else if v, vq, vd := reVar.FindSubmatch(l),
				reVarQ.FindSubmatch(l), reVarDflt.FindSubmatch(l); //
			v != nil || vq != nil || vd != nil {
				if sect == nil {
					return errors.New("no section")
				}
				var name, value string
				if v != nil {
					name, value = string(v[1]), string(v[2])
				} else if vq != nil {
					name, value = string(vq[1]), string(vq[2])
				} else { // vd != nil
					name, value = string(vd[1]), DefaultValue
				}
				err := set(config, *sect, sectsub, name, value)
				if err != nil {
					return err
				}
			} else {
				return fmt.Errorf("invalid line %q", string(l))
			}
		}
		if err == io.EOF {
			break
		}
	}
	return nil
}

// ParseString reads gcfg formatted data from str and sets the values into the
// corresponding fields in config.
// See Parse(config, reader) for the details of the parsing rules.
func ParseString(config interface{}, str string) error {
	r := strings.NewReader(str)
	return Parse(config, r)
}

// ParseFile reads gcfg formatted data from the file filename and sets the
// values into the corresponding fields in config.
// See Parse(config, reader) for the details of the parsing rules.
func ParseFile(config interface{}, filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	return Parse(config, f)
}
<commit_msg>refactor scan() and export as ScanEnum()<commit_after>// Package gcfg reads "gitconfig-like" text-based configuration files with
// "name=value" pairs grouped into sections (gcfg files).
// Support for modifying and/or exporting gcfg files may be added later.
//
// See Parse and the examples to get an idea of how to use it.
//
// This package is still a work in progress, and both the supported syntax and
// the API is subject to change.
//
// The syntax is based on that used by git config:
// http://git-scm.com/docs/git-config#_syntax .
// Note that the gcfg syntax may diverge from that of git config in the future
// to a limited degree.
// Current differences (apart from TODOs listed below) are:
//  - gcfg files must use UTF-8 encoding (for now)
//  - include and "path" type is not supported at the package level
//  - `[sec.sub]` format is not allowed (deprecated in gitconfig)
//  - `[sec ""]` is not allowed
//    - `[sec]` is the equivalent (section name "sec" and empty subsection name)
//
// The package may be usable for handling some of the various "INI file" formats
// used by some programs and libraries, but achieving or maintaining
// compatibility with any of those is not a primary concern.
//
// TODO:
//  - docs
//    - format spec
//  - parsing
//    - define internal representation structure
//    - support multi-value variables
//    - non-regexp based parser
//    - support escaping in strings
//    - support multiple inputs (readers, strings, files)
//    - support declaring encoding (?)
//    - support pointer fields
//  - modifying / exporting gcfg files
//  - error handling
//    - include error context
//    - more helpful error messages
//    - error types / codes?
//    - limit input size?
//
package gcfg

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"regexp"
	"strings"
)

var (
	reCmnt    = regexp.MustCompile(`^([^;#"]*)[;#].*$`)
	reCmntQ   = regexp.MustCompile(`^([^;#"]*"[^"]*"[^;#"]*)[;#].*$`)
	reBlank   = regexp.MustCompile(`^\s*$`)
	reSect    = regexp.MustCompile(`^\s*\[\s*([^"\s]*)\s*\]\s*$`)
	reSectSub = regexp.MustCompile(`^\s*\[\s*([^"\s]*)\s*"([^"]+)"\s*\]\s*$`)
	reVar     = regexp.MustCompile(`^\s*([^"=\s]+)\s*=\s*([^"\s]*)\s*$`)
	reVarQ    = regexp.MustCompile(`^\s*([^"=\s]+)\s*=\s*"([^"\n\\]*)"\s*$`)
	reVarDflt = regexp.MustCompile(`^\s*\b(.*)\b\s*$`)
)

const (
	// Default value string in case a value for a variable isn't provided.
	DefaultValue = "true"
)

type gbool bool

var boolValues = map[string]interface{}{
	"true": true, "yes": true, "on": true, "1": true,
	"false": false, "no": false, "off": false, "0": false}

const (
	// initial size of buffer for ScanEnum
	scanEnumBufferHint = 16
)

func strEq(s1, s2 string, fold bool) bool {
	return fold && strings.EqualFold(s1, s2) || !fold && s1 == s2
}

// ScanEnum is a helper function to simplify the implementation of fmt.Scanner
// methods for "enum-like" types, that is, user-defined types where the set of
// values and string representations is fixed.
//
// State is the state passed to the implementation of the fmt.Scanner method.
// Values holds as map values the values of the type, with their string
// representations as keys.
// If fold is true, comparison of the string representation uses
// strings.EqualFold, otherwise the equal operator for strings.
//
// On a match, ScanEnum stops after reading the last rune of the matched string,
// and returns the corresponding value together with a nil error.
// On no match, ScanEnum attempts to unread the last rune (the first rune that
// could not potentially match any of the values), and returns a non-nil error,
// togetherwith a nil value for interface{}.
// On I/O error, ScanEnum returns the I/O error, together with a nil value for
// interface{}.
//
func ScanEnum(state fmt.ScanState, values map[string]interface{}, fold bool) (
	interface{}, error) {
	//
	rd := make([]rune, 0, scanEnumBufferHint)
	keys := make(map[string]struct{}, len(values)) // potential keys
	for s, _ := range values {
		keys[s] = struct{}{}
	}
	for {
		r, _, err := state.ReadRune()
		if err != nil {
			return nil, err
		}
		rd = append(rd, r)
		srd := string(rd)
		lrd := len(srd)
		for s, _ := range keys {
			if strEq(srd, s, fold) {
				return values[s], nil
			}
			if len(rd) < len(s) && !strEq(srd, s[:lrd], fold) {
				delete(keys, s)
			}
		}
		if len(keys) == 0 {
			state.UnreadRune()
			return nil, fmt.Errorf("unsupported value %q", srd)
		}
	}
	panic("never reached")
}

func (b *gbool) Scan(state fmt.ScanState, verb rune) error {
	v, err := ScanEnum(state, boolValues, true)
	switch bb := v.(type) {
	case bool:
		*b = gbool(bb)
		return err
	}
	return err
}

func fieldFold(v reflect.Value, name string) reflect.Value {
	n := strings.Replace(name, "-", "_", -1)
	return v.FieldByNameFunc(func(fieldName string) bool {
		return strings.EqualFold(n, fieldName)
	})
}

func set(cfg interface{}, sect, sub, name, value string) error {
	vDest := reflect.ValueOf(cfg).Elem()
	vSect := fieldFold(vDest, sect)
	if vSect.Kind() == reflect.Map {
		if vSect.IsNil() {
			vSect.Set(reflect.MakeMap(vSect.Type()))
		}
		k := reflect.ValueOf(sub)
		pv := vSect.MapIndex(k)
		if !pv.IsValid() {
			vType := vSect.Type().Elem().Elem()
			pv = reflect.New(vType)
			vSect.SetMapIndex(k, pv)
		}
		vSect = pv.Elem()
	} else if sub != "" {
		return fmt.Errorf("expected map; section %q subsection %q", sect, sub)
	}
	vName := fieldFold(vSect, name)
	vAddr := vName.Addr().Interface()
	switch v := vAddr.(type) {
	case *string:
		*v = value
		return nil
	case *bool:
		vAddr = (*gbool)(v)
	}
	// attempt to read an extra rune to make sure the value is consumed
	var r rune
	n, err := fmt.Sscanf(value, "%v%c", vAddr, &r)
	switch {
	case n < 1 || n == 1 && err != io.EOF:
		return fmt.Errorf("failed to parse %q as %#v: parse error %v", value,
			vName.Type(), err)
	case n > 1:
		return fmt.Errorf("failed to parse %q as %#v: extra characters", value,
			vName.Type())
	case n == 1 && err == io.EOF:
		return nil
	}
	panic("never reached")
}

// Parse reads gcfg formatted data from reader and sets the values into the
// corresponding fields in config.
//
// Config must be a pointer to a struct.
// Each section corresponds to a struct field in config, and each variable in a
// section corresponds to a data field in the section struct.
// The field names must match that of the section or the variable, ignoring
// case.
// Hyphens are replaced with underscores.
//
// For sections with subsections, the corresponding field in config must be a
// map, rather than a struct, with string keys and pointer-to-struct values.
// Values for subsection variables are stored in the map with the subsection
// name used as the map key.
// (Note that unlike section and variable names, subsection names are case
// sensitive.)
// When using a map, and there is a section with the same section name but
// without a subsection name, its values are stored with the empty string used
// as the key.
//
// The section structs in the config struct may contain arbitrary types.
// For string fields, the (unquoted and unescaped) value string is assigned to
// the field.
// For bool fields, the field is set to true if the value is "true", "yes", "on"
// or "1", and set to false if the value is "false", "no", "off" or "0",
// ignoring case.
// For all other types, fmt.Sscanf is used to parse the value and set it to the
// field.
// This means that built-in Go types are parseable using the standard format,
// and any user-defined types is parseable if it implements the fmt.Scanner
// interface.
//
// See ParseString for examples.
//
func Parse(config interface{}, reader io.Reader) error {
	r := bufio.NewReader(reader)
	sect := (*string)(nil)
	sectsub := ""
	for line := 1; true; line++ {
		l, pre, err := r.ReadLine()
		if err != nil && err != io.EOF {
			return err
		} else if pre {
			return errors.New("line too long")
		}
		// exclude comments
		if c := reCmnt.FindSubmatch(l); c != nil {
			l = c[1]
		} else if c := reCmntQ.FindSubmatch(l); c != nil {
			l = c[1]
		}
		if !reBlank.Match(l) {
			// "switch" based on line contents
			if sec := reSect.FindSubmatch(l); sec != nil {
				strsec := string(sec[1])
				sect, sectsub = &strsec, ""
			} else if sec := reSectSub.FindSubmatch(l); sec != nil {
				strsec := string(sec[1])
				strsub := string(sec[2])
				if strsub == "" {
					return errors.New("empty subsection not allowed")
				}
				sect, sectsub = &strsec, strsub
			} else if v, vq, vd := reVar.FindSubmatch(l),
				reVarQ.FindSubmatch(l), reVarDflt.FindSubmatch(l); //
			v != nil || vq != nil || vd != nil {
				if sect == nil {
					return errors.New("no section")
				}
				var name, value string
				if v != nil {
					name, value = string(v[1]), string(v[2])
				} else if vq != nil {
					name, value = string(vq[1]), string(vq[2])
				} else { // vd != nil
					name, value = string(vd[1]), DefaultValue
				}
				err := set(config, *sect, sectsub, name, value)
				if err != nil {
					return err
				}
			} else {
				return fmt.Errorf("invalid line %q", string(l))
			}
		}
		if err == io.EOF {
			break
		}
	}
	return nil
}

// ParseString reads gcfg formatted data from str and sets the values into the
// corresponding fields in config.
// See Parse(config, reader) for the details of the parsing rules.
func ParseString(config interface{}, str string) error {
	r := strings.NewReader(str)
	return Parse(config, r)
}

// ParseFile reads gcfg formatted data from the file filename and sets the
// values into the corresponding fields in config.
// See Parse(config, reader) for the details of the parsing rules.
func ParseFile(config interface{}, filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	return Parse(config, f)
}
<|endoftext|>


<commit_before>package itunes_test

import (
	"errors"
	"fmt"
	"io/ioutil"
	"mime"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/deepilla/itunes"
)

func TestToRSS(t *testing.T) {

	data := map[string]struct {
		Paths []string
		Feed  string
		Err   error
	}{
		"Go Time": {
			Paths: []string{
				"podcasts/go-time/itunes-page",
			},
			Feed: "https://changelog.com/gotime/feed",
		},
		"Homecoming": {
			Paths: []string{
				"podcasts/homecoming/itunes-page",
				"podcasts/homecoming/plist",
			},
			Feed: "http://feeds.gimletmedia.com/homecomingshow",
		},
		"Linux Voice": {
			Paths: []string{
				"podcasts/linux-voice/itunes-page",
			},
			Feed: "https://www.linuxvoice.com/podcast_mp3.rss",
		},
		"Longform": {
			Paths: []string{
				"podcasts/longform/itunes-page",
			},
			Feed: "http://longform.libsyn.com/rss",
		},
		"No Such Thing As A Fish": {
			Paths: []string{
				"podcasts/no-such-thing-as-a-fish/itunes-page",
				"podcasts/no-such-thing-as-a-fish/plist",
			},
			Feed: "https://audioboom.com/channels/2399216.rss",
		},
		"Pod Save America": {
			Paths: []string{
				"podcasts/pod-save-america/itunes-page",
				"podcasts/pod-save-america/plist",
			},
			Feed: "http://feeds.feedburner.com/pod-save-america",
		},
		"Revisionist History": {
			Paths: []string{
				"podcasts/revisionist-history/itunes-page",
			},
			Feed: "http://feeds.feedburner.com/RevisionistHistory",
		},
		"S-Town": {
			Paths: []string{
				"podcasts/s-town/itunes-page",
				"podcasts/s-town/plist-1",
				"podcasts/s-town/plist-2",
				"podcasts/s-town/plist-3",
			},
			Feed: "http://feeds.stownpodcast.org/stownpodcast",
		},
		"Serial": {
			Paths: []string{
				"podcasts/serial/itunes-page",
				"podcasts/serial/plist",
			},
			Feed: "http://feeds.serialpodcast.org/serialpodcast",
		},
		"The /Filmcast": {
			Paths: []string{
				"podcasts/filmcast/itunes-page",
			},
			Feed: "http://feeds.feedburner.com/filmcast",
		},
		"Wittertainment": {
			Paths: []string{
				"podcasts/wittertainment/itunes-page",
				"podcasts/wittertainment/plist",
			},
			Feed: "https://podcasts.files.bbci.co.uk/b00lvdrj.rss",
		},
		"No Feed": {
			Paths: []string{
				"errors/no-feed/itunes-missing-user-agent",
				"errors/no-feed/itunes-no-episodes",
				"errors/no-feed/itunes-itunesu",
				"errors/no-feed/plist-item-not-available",
				"errors/no-feed/plist-incomplete",
				"errors/no-feed/plist-blank-url",
			},
			Err: itunes.ErrNoFeed,
		},
		"Too Many Redirects": {
			Paths: []string{
				"errors/too-many-redirects/plist-4",
				"errors/too-many-redirects/plist-recursive",
			},
			Err: errors.New("too many redirects"),
		},
	}

	ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
	defer ts.Close()

	client := validateRequests(t, redirectRequests(ts, http.DefaultClient))

	for name, test := range data {
		for i, url := range test.Paths {

			feed, err := itunes.ToRSSClient(client, url)

			if !equalErrors(err, test.Err) {
				t.Errorf("%s [%d/%d]: expected error %s, got %s", name, i+1, len(test.Paths), formatError(test.Err), formatError(err))
			}

			if feed != test.Feed {
				t.Errorf("%s [%d/%d]: expected feed %q, got %q", name, i+1, len(test.Paths), test.Feed, feed)
			}
		}
	}
}

func TestBadURL(t *testing.T) {

	urls := []string{
		"",
		"https://",
		"://itunes.apple.com/podcasts/123456789",
		"1ttps://itunes.apple.com/podcasts/123456789",
		"http://itunes.apple.com/podcasts/123456789#bad%%20escaping",
	}

	skipped := 0

	for _, u := range urls {

		_, err := url.Parse(u)
		if err == nil {
			skipped++
			t.Logf("Warning: Parse(%q) didn't return an error", u)
			continue
		}

		if e, ok := err.(*url.Error); ok {
			err = e.Err
		}

		exp := fmt.Errorf("fetch error: bad URL: %s", err)
		_, got := itunes.ToRSS(u)

		if !equalErrors(got, exp) {
			t.Errorf("URL %q: expected error %s, got %s", u, formatError(exp), formatError(got))
		}
	}

	if skipped == len(urls) {
		t.Errorf("No requests tested")
	}
}

func TestClientError(t *testing.T) {

	msgs := []string{
		"it was nearly eleven when I started to return",
		"the night was unexpectedly dark",
		"to me, walking out of the lighted passage of my cousin's house",
		"it seemed indeed black",
	}

	for _, s := range msgs {

		client := clientFunc(func(*http.Request) (*http.Response, error) {
			return nil, errors.New(s)
		})

		exp := fmt.Errorf("fetch error: %s", s)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("expected error %s, got %s", formatError(exp), formatError(got))
		}
	}
}

func TestBadHTTPStatus(t *testing.T) {

	statusCodes := []int{
		http.StatusForbidden,
		http.StatusNotFound,
		http.StatusGone,
		http.StatusTeapot,
		http.StatusInternalServerError,
		http.StatusBadGateway,
		http.StatusInternalServerError,
		420, // Enhance Your Calm (Twitter)
		444, // No Response (nginx)
		600, // Non-existent
	}

	for _, code := range statusCodes {

		ts := httptest.NewServer(errorHandler(code))
		client := redirectRequests(ts, http.DefaultClient)

		msg := http.StatusText(code)
		if msg == "" {
			msg = fmt.Sprintf("status code %d", code) // Go's default status for unrecognised error codes
		}

		exp := fmt.Errorf("fetch error: HTTP Status %d %s", code, msg)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Status %d: expected error %s, got %s", code, formatError(exp), formatError(got))
		}

		ts.Close()
	}
}

func TestBadContentType(t *testing.T) {

	types := []string{
		"",
		"text/",
		"text/xml; =",
	}

	skipped := 0

	for _, ctype := range types {

		_, _, err := mime.ParseMediaType(ctype)
		if err == nil {
			skipped++
			t.Logf("Warning: ParseMediaType(%q) didn't return an error", ctype)
			continue
		}

		ts := httptest.NewServer(contentTypeHandler(ctype))
		client := redirectRequests(ts, http.DefaultClient)

		exp := fmt.Errorf("bad Content Type %q: %s", ctype, err)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Content Type %q: expected error %s, got %s", ctype, formatError(exp), formatError(got))
		}

		ts.Close()
	}

	if skipped == len(types) {
		t.Errorf("No content types tested")
	}
}

func TestUnexpectedContentType(t *testing.T) {

	types := []string{
		"image/png",
		"text/plain; charset=utf-8",
		"audio/mpeg",
	}

	for _, ctype := range types {

		ts := httptest.NewServer(contentTypeHandler(ctype))
		client := redirectRequests(ts, http.DefaultClient)

		exp := fmt.Errorf("unexpected Content Type %q", ctype)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Content Type %q: expected error %s, got %s", ctype, formatError(exp), formatError(got))
		}

		ts.Close()
	}
}

func contentTypeHandler(typ string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", typ)
	})
}

func errorHandler(code int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "helpful error message", code)
	})
}

type clientFunc func(req *http.Request) (*http.Response, error)

func (f clientFunc) Do(req *http.Request) (*http.Response, error) {
	return f(req)
}

func redirectRequests(ts *httptest.Server, client itunes.Client) itunes.Client {
	return clientFunc(func(req *http.Request) (*http.Response, error) {

		newURL := ts.URL + "/" + req.URL.Path

		u, err := url.Parse(newURL)
		if err != nil {
			return nil, err
		}

		req.URL = u
		return client.Do(req)
	})
}

func validateRequests(t *testing.T, client itunes.Client) itunes.Client {
	return clientFunc(func(req *http.Request) (*http.Response, error) {

		if got, exp := req.Method, "GET"; got != exp {
			t.Fatalf("Bad request: expected Method %q, got %q", exp, got)
		}

		if got, exp := req.Header.Get("User-Agent"), "iTunes/10.1"; got != exp {
			t.Fatalf("Bad request: expected User Agent %q, got %q", exp, got)
		}

		if req.Body != nil {
			b, err := ioutil.ReadAll(req.Body)
			if err != nil {
				t.Fatalf("Bad request: Expected nil body, got something unreadable (read error: %s)", err)
			}
			t.Fatalf("Bad request: Expected nil body, got %q", string(b))
		}

		return client.Do(req)
	})
}

func formatError(err error) string {
	if err == nil {
		return "Nil"
	}
	return "\"" + err.Error() + "\""
}

func equalErrors(err1, err2 error) bool {
	switch {
	case err1 == err2:
		return true
	case err1 == nil, err2 == nil:
		return false
	default:
		return err1.Error() == err2.Error()
	}
}
<commit_msg>Minor refactoring of tests.<commit_after>package itunes_test

import (
	"errors"
	"fmt"
	"io/ioutil"
	"mime"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/deepilla/itunes"
)

func TestToRSS(t *testing.T) {

	data := map[string]struct {
		Paths []string
		Feed  string
		Err   error
	}{
		"Go Time": {
			Paths: []string{
				"podcasts/go-time/itunes-page",
			},
			Feed: "https://changelog.com/gotime/feed",
		},
		"Homecoming": {
			Paths: []string{
				"podcasts/homecoming/itunes-page",
				"podcasts/homecoming/plist",
			},
			Feed: "http://feeds.gimletmedia.com/homecomingshow",
		},
		"Linux Voice": {
			Paths: []string{
				"podcasts/linux-voice/itunes-page",
			},
			Feed: "https://www.linuxvoice.com/podcast_mp3.rss",
		},
		"Longform": {
			Paths: []string{
				"podcasts/longform/itunes-page",
			},
			Feed: "http://longform.libsyn.com/rss",
		},
		"No Such Thing As A Fish": {
			Paths: []string{
				"podcasts/no-such-thing-as-a-fish/itunes-page",
				"podcasts/no-such-thing-as-a-fish/plist",
			},
			Feed: "https://audioboom.com/channels/2399216.rss",
		},
		"Pod Save America": {
			Paths: []string{
				"podcasts/pod-save-america/itunes-page",
				"podcasts/pod-save-america/plist",
			},
			Feed: "http://feeds.feedburner.com/pod-save-america",
		},
		"Revisionist History": {
			Paths: []string{
				"podcasts/revisionist-history/itunes-page",
			},
			Feed: "http://feeds.feedburner.com/RevisionistHistory",
		},
		"S-Town": {
			Paths: []string{
				"podcasts/s-town/itunes-page",
				"podcasts/s-town/plist-1",
				"podcasts/s-town/plist-2",
				"podcasts/s-town/plist-3",
			},
			Feed: "http://feeds.stownpodcast.org/stownpodcast",
		},
		"Serial": {
			Paths: []string{
				"podcasts/serial/itunes-page",
				"podcasts/serial/plist",
			},
			Feed: "http://feeds.serialpodcast.org/serialpodcast",
		},
		"The /Filmcast": {
			Paths: []string{
				"podcasts/filmcast/itunes-page",
			},
			Feed: "http://feeds.feedburner.com/filmcast",
		},
		"Wittertainment": {
			Paths: []string{
				"podcasts/wittertainment/itunes-page",
				"podcasts/wittertainment/plist",
			},
			Feed: "https://podcasts.files.bbci.co.uk/b00lvdrj.rss",
		},
		"No Feed": {
			Paths: []string{
				"errors/no-feed/itunes-missing-user-agent",
				"errors/no-feed/itunes-no-episodes",
				"errors/no-feed/itunes-itunesu",
				"errors/no-feed/plist-item-not-available",
				"errors/no-feed/plist-incomplete",
				"errors/no-feed/plist-blank-url",
			},
			Err: itunes.ErrNoFeed,
		},
		"Too Many Redirects": {
			Paths: []string{
				"errors/too-many-redirects/plist-4",
				"errors/too-many-redirects/plist-recursive",
			},
			Err: errors.New("too many redirects"),
		},
	}

	ts := httptest.NewServer(http.FileServer(http.Dir("testdata")))
	defer ts.Close()

	client := validateRequests(t, redirectRequests(ts, http.DefaultClient))

	for name, test := range data {
		for i, url := range test.Paths {

			feed, err := itunes.ToRSSClient(client, url)

			if !equalErrors(err, test.Err) {
				t.Errorf("%s [%d/%d]: expected error %s, got %s", name, i+1, len(test.Paths), formatError(test.Err), formatError(err))
			}

			if feed != test.Feed {
				t.Errorf("%s [%d/%d]: expected feed %q, got %q", name, i+1, len(test.Paths), test.Feed, feed)
			}
		}
	}
}

func TestBadURL(t *testing.T) {

	urls := []string{
		"",
		"https://",
		"://itunes.apple.com/podcasts/123456789",
		"1ttps://itunes.apple.com/podcasts/123456789",
		"http://itunes.apple.com/podcasts/123456789#bad%%20escaping",
	}

	skipped := 0
	for _, u := range urls {

		_, err := url.Parse(u)
		if err == nil {
			t.Logf("Warning: Parse(%q) didn't return an error", u)
			skipped++
			if skipped == len(urls) {
				t.Fatalf("No URLs tested")
			}
			continue
		}

		if e, ok := err.(*url.Error); ok {
			err = e.Err
		}

		exp := fmt.Errorf("fetch error: bad URL: %s", err)
		_, got := itunes.ToRSS(u)

		if !equalErrors(got, exp) {
			t.Errorf("URL %q: expected error %s, got %s", u, formatError(exp), formatError(got))
		}
	}
}

func TestClientError(t *testing.T) {

	msgs := []string{
		"it was nearly eleven when I started to return",
		"the night was unexpectedly dark",
		"to me, walking out of the lighted passage of my cousin's house",
		"it seemed indeed black",
	}

	for _, s := range msgs {

		client := clientFunc(func(*http.Request) (*http.Response, error) {
			return nil, errors.New(s)
		})

		exp := fmt.Errorf("fetch error: %s", s)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("expected error %s, got %s", formatError(exp), formatError(got))
		}
	}
}

func TestBadHTTPStatus(t *testing.T) {

	statusCodes := []int{
		http.StatusForbidden,
		http.StatusNotFound,
		http.StatusGone,
		http.StatusTeapot,
		http.StatusInternalServerError,
		http.StatusBadGateway,
		http.StatusInternalServerError,
		420, // Enhance Your Calm (Twitter)
		444, // No Response (nginx)
		600, // Non-existent
	}

	for _, code := range statusCodes {

		ts := httptest.NewServer(errorHandler(code))
		client := redirectRequests(ts, http.DefaultClient)

		msg := http.StatusText(code)
		if msg == "" {
			msg = fmt.Sprintf("status code %d", code) // Go's default status for unrecognised error codes
		}

		exp := fmt.Errorf("fetch error: HTTP Status %d %s", code, msg)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Status %d: expected error %s, got %s", code, formatError(exp), formatError(got))
		}

		ts.Close()
	}
}

func TestBadContentType(t *testing.T) {

	types := []string{
		"",
		"text/",
		"text/xml; =",
	}

	skipped := 0
	for _, ctype := range types {

		_, _, err := mime.ParseMediaType(ctype)
		if err == nil {
			t.Logf("Warning: ParseMediaType(%q) didn't return an error", ctype)
			skipped++
			if skipped == len(types) {
				t.Fatalf("No Content Types tested")
			}
			continue
		}

		ts := httptest.NewServer(contentTypeHandler(ctype))
		client := redirectRequests(ts, http.DefaultClient)

		exp := fmt.Errorf("bad Content Type %q: %s", ctype, err)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Content Type %q: expected error %s, got %s", ctype, formatError(exp), formatError(got))
		}

		ts.Close()
	}
}

func TestUnexpectedContentType(t *testing.T) {

	types := []string{
		"image/png",
		"text/plain; charset=utf-8",
		"audio/mpeg",
	}

	for _, ctype := range types {

		ts := httptest.NewServer(contentTypeHandler(ctype))
		client := redirectRequests(ts, http.DefaultClient)

		exp := fmt.Errorf("unexpected Content Type %q", ctype)
		_, got := itunes.ToRSSClient(client, "")

		if !equalErrors(got, exp) {
			t.Errorf("Content Type %q: expected error %s, got %s", ctype, formatError(exp), formatError(got))
		}

		ts.Close()
	}
}

func contentTypeHandler(typ string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", typ)
	})
}

func errorHandler(code int) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "helpful error message", code)
	})
}

type clientFunc func(req *http.Request) (*http.Response, error)

func (f clientFunc) Do(req *http.Request) (*http.Response, error) {
	return f(req)
}

func redirectRequests(ts *httptest.Server, client itunes.Client) itunes.Client {
	return clientFunc(func(req *http.Request) (*http.Response, error) {

		newURL := ts.URL + "/" + req.URL.Path

		u, err := url.Parse(newURL)
		if err != nil {
			return nil, err
		}

		req.URL = u
		return client.Do(req)
	})
}

func validateRequests(t *testing.T, client itunes.Client) itunes.Client {
	return clientFunc(func(req *http.Request) (*http.Response, error) {

		if got, exp := req.Method, "GET"; got != exp {
			t.Fatalf("Bad Request: expected Method %q, got %q", exp, got)
		}

		if got, exp := req.Header.Get("User-Agent"), "iTunes/10.1"; got != exp {
			t.Fatalf("Bad Request: expected User Agent %q, got %q", exp, got)
		}

		if req.Body != nil {
			b, err := ioutil.ReadAll(req.Body)
			if err != nil {
				t.Fatalf("Bad Request: Expected nil body, got something unreadable (read error: %s)", err)
			}
			t.Fatalf("Bad Request: Expected nil body, got %q", string(b))
		}

		return client.Do(req)
	})
}

func formatError(err error) string {
	if err == nil {
		return "Nil"
	}
	return fmt.Sprintf("%q", err)
}

func equalErrors(err1, err2 error) bool {
	switch {
	case err1 == err2:
		return true
	case err1 == nil, err2 == nil:
		return false
	default:
		return err1.Error() == err2.Error()
	}
}
<|endoftext|>


<commit_before>package command

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/fatih/flags"
	"github.com/mitchellh/cli"
)

// Export
type Export struct{}

// Exporter is an interface with an export method
type Exporter interface {
	Export()
}

// NewExport creates a new instance of Delete
func NewExport() cli.CommandFactory {
	return func() (cli.Command, error) {
		return &Export{}, nil
	}
}

// Run shows a given resource.
func (e *Export) Run(args []string) int {
	if flags.Has("help", args) || len(args) < 1 {
		fmt.Print(e.Help())
		return 1
	}

	// process the subcommand and it's options
	switch args[0] {
	case "csv":
		e.csv()
	case "json":
		e.json()
	case "jira":
		e.jira()
	default:
		fmt.Println("ERROR: invalid option for show\n")
	}

	return 1
}

// Help provides full help inforamation for the subcommand
func (e *Export) Help() string {
	return `Usage: todo-view export <option> <arguments>
  Show a resource
Options:
  csv                Display the todo-view data in csv format
  json               Display the todo-view data in json format

`
}

// Synopsis provides a brief description of the command
func (e *Export) Synopsis() string {
	return "Export todo-view data in a given format"
}

// csv exports the todo-view data in cvs format
func (e *Export) csv() {
	fmt.Print("\ntodo-view export: cvs\n\n")
	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}
	for _, todo := range todos {
		fmt.Fprintf(os.Stdout, "%s,%s,%s,%v,%d\n",
			todo.User(), todo.File(), todo.Message(), todo.Timestamp(), todo.Weight())
	}
}

// json exports the todo-view data in json format
func (e *Export) json() {
	fmt.Print("\ntodo-view export: json\n\n")
	w := NewTabWriter()
	defer w.Flush()

	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}

	m := make(map[string][]Todo)
	m["todos"] = todos

	b, err := json.Marshal(m["todos"][0])
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Fprintln(w, string(b))
}

// jira exports the todo-view data in jira format
func (e *Export) jira() {
	fmt.Print("\ntodo-view export: jira\n\n")
	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}
	//var jira []Exporter
	for _, todo := range todos {
		//jira = append(jira, NewJira(todo.Message(), todo.User(), todo.Weight()))
		nj := NewJira(todo.Message(), todo.User(), todo.Weight())
		nj.Export()
	}
}

// Jira holds the data necessary to export data in Jira format
type Jira struct {
	Header      string
	Summary     string
	Assignee    string
	Reporter    string
	IssueType   int
	Status      string
	Description string
	Priority    int
}

// NewJira creates a new reference to a Jira type
func NewJira(msg, user string, weight int) *Jira {
	return &Jira{
		Header:   "Summary,Assignee,Reporter,Priority",
		Summary:  msg,
		Assignee: user,
		Reporter: user,
		Priority: weight,
	}
}

// Export prints out the Jira formatted data
func (j *Jira) Export() {
	fmt.Print("\ntodo-view export: cvs\n\n")
	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Fprintf(os.Stdout, j.Header)
	for _, todo := range todos {
		fmt.Fprintf(os.Stdout, "%s,%s,%s,%d\n",
			todo.Message(), todo.User(), todo.User(), todo.Weight())
	}

}
<commit_msg>initial jira export functionality<commit_after>package command

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/fatih/flags"
	"github.com/mitchellh/cli"
)

// Export
type Export struct{}

// Exporter is an interface with an export method
type Exporter interface {
	Export()
}

// NewExport creates a new instance of Delete
func NewExport() cli.CommandFactory {
	return func() (cli.Command, error) {
		return &Export{}, nil
	}
}

// Run shows a given resource.
func (e *Export) Run(args []string) int {
	if flags.Has("help", args) || len(args) < 1 {
		fmt.Print(e.Help())
		return 1
	}

	// process the subcommand and it's options
	switch args[0] {
	case "csv":
		e.csv()
	case "json":
		e.json()
	case "jira":
		e.jira()
	default:
		fmt.Println("ERROR: invalid option for show\n")
	}

	return 1
}

// Help provides full help inforamation for the subcommand
func (e *Export) Help() string {
	return `Usage: todo-view export <option> <arguments>
  Show a resource
Options:
  csv                Display the todo-view data in csv format
  json               Display the todo-view data in json format

`
}

// Synopsis provides a brief description of the command
func (e *Export) Synopsis() string {
	return "Export todo-view data in a given format"
}

// csv exports the todo-view data in cvs format
func (e *Export) csv() {
	fmt.Print("\ntodo-view export: cvs\n\n")
	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}
	for _, todo := range todos {
		fmt.Fprintf(os.Stdout, "%s,%s,%s,%v,%d\n",
			todo.User(), todo.File(), todo.Message(), todo.Timestamp(), todo.Weight())
	}
}

// json exports the todo-view data in json format
func (e *Export) json() {
	fmt.Print("\ntodo-view export: json\n\n")
	w := NewTabWriter()
	defer w.Flush()

	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}

	m := make(map[string][]Todo)
	m["todos"] = todos

	b, err := json.Marshal(m["todos"][0])
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Fprintln(w, string(b))
}

// jira exports the todo-view data in jira format
func (e *Export) jira() {
	fmt.Print("\ntodo-view export: jira\n\n")
	todos, err := search()
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Fprintln(os.Stdout, "Summary,Assignee,Reporter,Priority")
	for _, todo := range todos {
		fmt.Fprintf(os.Stdout, "%s,%s,%s,%d\n",
			todo.Message(), todo.User(), todo.User(), todo.Weight())
	}

	/*var t []Exporter
	for _, todo := range todos {
		nj := NewJira(todo.Message(), todo.User(), todo.Weight())
		t = append(t, nj)
	}
	nj.Export()*/
}

// Jira holds the data necessary to export data in Jira format
type Jira struct {
	Header      string
	Summary     string
	Assignee    string
	Reporter    string
	IssueType   int
	Status      string
	Description string
	Priority    int
}

// NewJira creates a new reference to a Jira type
func NewJira(msg, user string, weight int) *Jira {
	return &Jira{
		Header:   "Summary,Assignee,Reporter,Priority",
		Summary:  msg,
		Assignee: user,
		Reporter: user,
		Priority: weight,
	}
}

// Export prints out the Jira formatted data
func (j *Jira) Export() {
	fmt.Fprintln(os.Stdout, j.Header)
}
<|endoftext|>


<commit_before><commit_msg>stash/gc: fix import<commit_after><|endoftext|>


<commit_before>/*
   Copyright 2017 Artem Sidorenko <artem@posteo.de>

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

// Package generate implments the generate command
package generate

import (
	"io"
	"os"
	"time"

	"github.com/artem-sidorenko/chagen/cli/commands"
	"github.com/artem-sidorenko/chagen/connectors"
	_ "github.com/artem-sidorenko/chagen/connectors/github" //enable github
	"github.com/artem-sidorenko/chagen/data"
	"github.com/artem-sidorenko/chagen/generator"

	"github.com/urfave/cli"
)

// Stdout references the Stdout writer for generate command
var Stdout io.Writer = os.Stdout // nolint: gochecknoglobals
// Connector references the connector ID used for generation
var Connector = "github" // nolint: gochecknoglobals

// Generate implements the CLI subcommand generate
func Generate(c *cli.Context) (err error) {
	tags, issues, mrs, err := getConnectorData(c.String("new-release"), c)
	if err != nil {
		return nil
	}

	gen := generator.New(data.NewReleases(tags, issues, mrs))

	// use stdout if - is given, otherwise create a new file
	filename := c.String("file")
	var wr io.Writer
	if filename != "-" {
		var file *os.File
		if file, err = os.Create(filename); err != nil {
			return err
		}

		defer func() {
			if cerr := file.Close(); err == nil && cerr != nil {
				err = cerr
			}
		}()

		wr = file
	} else {
		wr = Stdout
	}

	err = gen.Render(wr)

	return err
}

// getConnectorData returns all needed data from connector
// if newRelease is specified, a new releases for
// untagged activities is created
func getConnectorData(newRelease string,
	ctx *cli.Context) (data.Tags, data.Issues, data.MRs, error) {
	var (
		connector connectors.Connector
		tags      data.Tags
		issues    data.Issues
		mrs       data.MRs
		err       error
	)

	connector, err = connectors.NewConnector(Connector, ctx)
	if err != nil {
		return nil, nil, nil, err
	}

	tags, err = connector.GetTags()
	if err != nil {
		return nil, nil, nil, err
	}

	if newRelease != "" {
		var relURL string
		relURL, err = connector.GetNewTagURL(newRelease)
		if err != nil {
			return nil, nil, nil, err
		}

		tags = append(tags, data.Tag{
			Name: newRelease,
			Date: time.Now(),
			URL:  relURL,
		})
	}

	issues, err = connector.GetIssues()
	if err != nil {
		return nil, nil, nil, err
	}

	mrs, err = connector.GetMRs()
	if err != nil {
		return nil, nil, nil, err
	}

	return tags, issues, mrs, nil
}

// CLIFlags returns the possible CLI flags for this command
func CLIFlags() []cli.Flag {
	return []cli.Flag{
		cli.StringFlag{
			Name:  "file, f",
			Usage: "File name of changelog, - is accepted for stdout",
			Value: "CHANGELOG.md",
		},
		cli.StringFlag{
			Name:  "new-release, r",
			Usage: "Create a new release for all issues and changes after the last release",
		},
	}
}

func init() { // nolint: gochecknoinits
	flags := CLIFlags()

	connectorFlags, _ := connectors.CLIFlags(Connector) // nolint: gosec

	flags = append(flags, connectorFlags...)

	commands.RegisterCommand(cli.Command{
		Name:      "generate",
		Usage:     "Generate a changelog",
		ArgsUsage: " ", // we do not have any args (only flags), so avoid this help message
		Flags:     flags,
		Action: func(c *cli.Context) error {
			if err := Generate(c); err != nil { // exit 1 and error message if we get any error reported
				return cli.NewExitError(err, 1)
			}
			return nil
		},
	})
}
<commit_msg>Refactoring: proper error handling<commit_after>/*
   Copyright 2017 Artem Sidorenko <artem@posteo.de>

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

// Package generate implments the generate command
package generate

import (
	"io"
	"os"
	"time"

	"github.com/artem-sidorenko/chagen/cli/commands"
	"github.com/artem-sidorenko/chagen/connectors"
	_ "github.com/artem-sidorenko/chagen/connectors/github" //enable github
	"github.com/artem-sidorenko/chagen/data"
	"github.com/artem-sidorenko/chagen/generator"

	"github.com/urfave/cli"
)

// Stdout references the Stdout writer for generate command
var Stdout io.Writer = os.Stdout // nolint: gochecknoglobals
// Connector references the connector ID used for generation
var Connector = "github" // nolint: gochecknoglobals

// Generate implements the CLI subcommand generate
func Generate(c *cli.Context) (err error) {
	tags, issues, mrs, err := getConnectorData(c.String("new-release"), c)
	if err != nil {
		return nil
	}

	gen := generator.New(data.NewReleases(tags, issues, mrs))

	// use stdout if - is given, otherwise create a new file
	filename := c.String("file")
	var wr io.Writer
	if filename != "-" {
		var file *os.File
		if file, err = os.Create(filename); err != nil {
			return err
		}

		defer func() {
			if cerr := file.Close(); err == nil && cerr != nil {
				err = cerr
			}
		}()

		wr = file
	} else {
		wr = Stdout
	}

	err = gen.Render(wr)

	return err
}

// getConnectorData returns all needed data from connector
// if newRelease is specified, a new releases for
// untagged activities is created
func getConnectorData(newRelease string,
	ctx *cli.Context) (data.Tags, data.Issues, data.MRs, error) {
	var (
		connector connectors.Connector
		tags      data.Tags
		issues    data.Issues
		mrs       data.MRs
		err       error
	)

	connector, err = connectors.NewConnector(Connector, ctx)
	if err != nil {
		return nil, nil, nil, err
	}

	tags, err = connector.GetTags()
	if err != nil {
		return nil, nil, nil, err
	}

	if newRelease != "" {
		var relURL string
		relURL, err = connector.GetNewTagURL(newRelease)
		if err != nil {
			return nil, nil, nil, err
		}

		tags = append(tags, data.Tag{
			Name: newRelease,
			Date: time.Now(),
			URL:  relURL,
		})
	}

	issues, err = connector.GetIssues()
	if err != nil {
		return nil, nil, nil, err
	}

	mrs, err = connector.GetMRs()
	if err != nil {
		return nil, nil, nil, err
	}

	return tags, issues, mrs, nil
}

// CLIFlags returns the possible CLI flags for this command
func CLIFlags() []cli.Flag {
	return []cli.Flag{
		cli.StringFlag{
			Name:  "file, f",
			Usage: "File name of changelog, - is accepted for stdout",
			Value: "CHANGELOG.md",
		},
		cli.StringFlag{
			Name:  "new-release, r",
			Usage: "Create a new release for all issues and changes after the last release",
		},
	}
}

func init() { // nolint: gochecknoinits
	flags := CLIFlags()

	connectorFlags, err := connectors.CLIFlags(Connector)
	if err != nil {
		panic(err)
	}

	flags = append(flags, connectorFlags...)

	commands.RegisterCommand(cli.Command{
		Name:      "generate",
		Usage:     "Generate a changelog",
		ArgsUsage: " ", // we do not have any args (only flags), so avoid this help message
		Flags:     flags,
		Action: func(c *cli.Context) error {
			if err := Generate(c); err != nil { // exit 1 and error message if we get any error reported
				return cli.NewExitError(err, 1)
			}
			return nil
		},
	})
}
<|endoftext|>