From 10dc0b70e0af5169609da6912929551bc395d0dd Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Wed, 30 May 2018 16:28:31 -0700 Subject: [PATCH 1/9] partial jira implementation; TODO: all Find/Read and Link cases. --- internal/cli/app.go | 2 + internal/cli/procedure.go | 15 ++- internal/cli/sync.go | 9 +- internal/config/config.go | 35 +++++- internal/jira/jira.go | 200 +++++++++++++++++++++++++++++++ internal/jira/jira_test.go | 9 ++ internal/model/plugin.go | 6 +- internal/plugin/github/github.go | 3 +- internal/render/controller.go | 9 +- internal/ticket/scheduler.go | 25 +++- 10 files changed, 294 insertions(+), 19 deletions(-) create mode 100644 internal/jira/jira.go create mode 100644 internal/jira/jira_test.go diff --git a/internal/cli/app.go b/internal/cli/app.go index e4bf7e8..54297eb 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/strongdm/comply/internal/config" + "github.com/strongdm/comply/internal/jira" "github.com/strongdm/comply/internal/plugin/github" "github.com/urfave/cli" ) @@ -55,6 +56,7 @@ func newApp() *cli.App { // Plugins github.Register() + jira.Register() return app } diff --git a/internal/cli/procedure.go b/internal/cli/procedure.go index 89ee608..4e7acd4 100644 --- a/internal/cli/procedure.go +++ b/internal/cli/procedure.go @@ -3,6 +3,7 @@ package cli import ( "fmt" + "github.com/strongdm/comply/internal/config" "github.com/strongdm/comply/internal/model" "github.com/urfave/cli" ) @@ -28,14 +29,22 @@ func procedureAction(c *cli.Context) error { procedureID := c.Args().First() + ts, err := config.Config().TicketSystem() + if err != nil { + return cli.NewExitError("error in ticket system configuration", 1) + } + + tp := model.GetPlugin(model.TicketSystem(ts)) + for _, procedure := range procedures { if procedure.ID == procedureID { - // TODO: don't hardcode GH - tp := model.GetPlugin(model.GitHub) - tp.Create(&model.Ticket{ + err = tp.Create(&model.Ticket{ Name: procedure.Name, Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID), }, []string{"comply", "comply-procedure"}) + if err != nil { + return err + } return nil } } diff --git a/internal/cli/sync.go b/internal/cli/sync.go index 76ba710..569d3e6 100644 --- a/internal/cli/sync.go +++ b/internal/cli/sync.go @@ -1,6 +1,7 @@ package cli import ( + "github.com/strongdm/comply/internal/config" "github.com/strongdm/comply/internal/model" "github.com/urfave/cli" ) @@ -13,8 +14,12 @@ var syncCommand = cli.Command{ } func syncAction(c *cli.Context) error { - // TODO: unhardcode plugin - tp := model.GetPlugin(model.GitHub) + ts, err := config.Config().TicketSystem() + if err != nil { + return cli.NewExitError("error in ticket system configuration", 1) + } + + tp := model.GetPlugin(model.TicketSystem(ts)) tickets, err := tp.FindByTagName("comply") if err != nil { return err diff --git a/internal/config/config.go b/internal/config/config.go index cdac061..e10000e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "io/ioutil" "os" "path/filepath" @@ -12,6 +13,12 @@ var projectRoot string var dockerAvailable, pandocAvailable bool +const ( + Jira = "jira" + GitHub = "github" + NoTickets = "none" +) + const ( // UseDocker invokes pandoc within Docker UseDocker = "docker" @@ -73,14 +80,14 @@ func Exists() bool { } // Config is the parsed contents of ProjectRoot()/config.yml. -func Config() Project { +func Config() *Project { p := Project{} cfgBytes, err := ioutil.ReadFile(filepath.Join(ProjectRoot(), "comply.yml")) if err != nil { panic("unable to load config.yml: " + err.Error()) } yaml.Unmarshal(cfgBytes, &p) - return p + return &p } // ProjectRoot is the fully-qualified path to the root directory. @@ -95,3 +102,27 @@ func ProjectRoot() string { return projectRoot } + +// TicketSystem indicates the type of the configured ticket system +func (p *Project) TicketSystem() (string, error) { + if len(p.Tickets) > 1 { + return NoTickets, errors.New("multiple ticket systems configured") + } + + for k := range p.Tickets { + switch k { + case GitHub: + return GitHub, nil + case Jira: + return Jira, nil + case NoTickets: + return NoTickets, nil + default: + // explicit error for this case + return "", errors.New("unrecognized ticket system configured") + } + } + + // no ticket block configured + return NoTickets, nil +} diff --git a/internal/jira/jira.go b/internal/jira/jira.go new file mode 100644 index 0000000..a7d7219 --- /dev/null +++ b/internal/jira/jira.go @@ -0,0 +1,200 @@ +package jira + +import ( + "fmt" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/strongdm/comply/internal/model" + + jira "github.com/andygrunwald/go-jira" +) + +const ( + cfgUsername = "username" + cfgPassword = "password" + cfgURL = "url" + cfgProject = "project" +) + +var prompts = map[string]string{ + cfgUsername: "Jira Username", + cfgPassword: "Jira Password", + cfgURL: "Jira URL", + cfgProject: "Jira Project Code", +} + +// Prompts are human-readable configuration element names +func (j *jiraPlugin) Prompts() map[string]string { + return prompts +} + +// Register causes the Github plugin to register itself +func Register() { + model.Register(model.Jira, &jiraPlugin{}) +} + +type jiraPlugin struct { + username string + password string + url string + project string + + clientMu sync.Mutex + client *jira.Client +} + +func (j *jiraPlugin) api() *jira.Client { + j.clientMu.Lock() + defer j.clientMu.Unlock() + + if j.client == nil { + tp := jira.BasicAuthTransport{ + Username: j.username, + Password: j.password, + } + + client, _ := jira.NewClient(tp.Client(), j.url) + j.client = client + } + return j.client +} + +func (j *jiraPlugin) Get(ID string) (*model.Ticket, error) { + return nil, nil +} + +func (j *jiraPlugin) Configured() bool { + return j.username != "" && j.password != "" && j.url != "" && j.project != "" +} + +func (j *jiraPlugin) Links() model.TicketLinks { + links := model.TicketLinks{} + links.AuditAll = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) + links.AuditOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) + links.ProcedureAll = fmt.Sprintf("%s/issues?q=is%3Aissue+label%3Acomply+label%3Aprocedure", j.url) + links.ProcedureOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aprocedure", j.url) + return links +} + +func (j *jiraPlugin) Configure(cfg map[string]interface{}) error { + var err error + + if j.username, err = getCfg(cfg, cfgUsername); err != nil { + return err + } + if j.password, err = getCfg(cfg, cfgPassword); err != nil { + return err + } + if j.url, err = getCfg(cfg, cfgURL); err != nil { + return err + } + if j.project, err = getCfg(cfg, cfgProject); err != nil { + return err + } + + return nil +} + +func getCfg(cfg map[string]interface{}, k string) (string, error) { + v, ok := cfg[k] + if !ok { + return "", errors.New("Missing key: " + k) + } + + vS, ok := v.(string) + if !ok { + return "", errors.New("Malformatted key: " + k) + } + return vS, nil +} + +func (j *jiraPlugin) FindOpen() ([]*model.Ticket, error) { + return []*model.Ticket{}, nil + // issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{ + // State: "open", + // }) + + // if err != nil { + // return nil, errors.Wrap(err, "error during FindOpen") + // } + + // return toTickets(issues), nil +} + +func (j *jiraPlugin) FindByTag(name, value string) ([]*model.Ticket, error) { + panic("not implemented") +} + +func (j *jiraPlugin) FindByTagName(name string) ([]*model.Ticket, error) { + return []*model.Ticket{}, nil + // issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{ + // State: "all", + // Labels: []string{name}, + // }) + + // if err != nil { + // return nil, errors.Wrap(err, "error during FindOpen") + // } + + // return toTickets(issues), nil +} + +func (j *jiraPlugin) LinkFor(t *model.Ticket) string { + // return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID) + panic("not implemented") +} + +func (j *jiraPlugin) Create(ticket *model.Ticket, labels []string) error { + i := jira.Issue{ + Fields: &jira.IssueFields{ + Type: jira.IssueType{ + Name: "Task", + }, + Project: jira.Project{ + Key: j.project, + }, + Summary: ticket.Name, + Description: ticket.Body, + Labels: labels, + }, + } + + _, _, err := j.api().Issue.Create(&i) + if err != nil { + return errors.Wrap(err, "unable to create ticket") + } + return nil +} + +func toTickets(issues []*jira.Issue) []*model.Ticket { + var tickets []*model.Ticket + for _, i := range issues { + tickets = append(tickets, toTicket(i)) + } + return tickets +} + +func toTicket(i *jira.Issue) *model.Ticket { + t := &model.Ticket{Attributes: make(map[string]interface{})} + t.ID = i.ID + t.Name = i.Fields.Description + t.Body = i.Fields.Summary + createdAt := time.Time(i.Fields.Created) + t.CreatedAt = &createdAt + t.State = toState(i.Fields.Status) + + for _, l := range i.Fields.Labels { + t.SetBool(l) + } + return t +} + +func toState(status *jira.Status) model.TicketState { + switch status.Name { + case "Closed": + return model.Closed + } + return model.Open +} diff --git a/internal/jira/jira_test.go b/internal/jira/jira_test.go new file mode 100644 index 0000000..1e662ef --- /dev/null +++ b/internal/jira/jira_test.go @@ -0,0 +1,9 @@ +package jira + +import ( + "testing" +) + +func TestJira(t *testing.T) { + createOne() +} diff --git a/internal/model/plugin.go b/internal/model/plugin.go index e3ce58d..5d2fd53 100644 --- a/internal/model/plugin.go +++ b/internal/model/plugin.go @@ -17,11 +17,11 @@ type TicketSystem string const ( // Jira from Atlassian. - Jira = TicketSystem("jira") + Jira = TicketSystem(config.Jira) // GitHub from GitHub. - GitHub = TicketSystem("github") + GitHub = TicketSystem(config.GitHub) // NoTickets indicates no ticketing system integration. - NoTickets = TicketSystem("none") + NoTickets = TicketSystem(config.NoTickets) ) type TicketLinks struct { diff --git a/internal/plugin/github/github.go b/internal/plugin/github/github.go index 21591e5..9510e44 100644 --- a/internal/plugin/github/github.go +++ b/internal/plugin/github/github.go @@ -135,7 +135,8 @@ func (g *githubPlugin) FindByTagName(name string) ([]*model.Ticket, error) { } func (g *githubPlugin) LinkFor(t *model.Ticket) string { - return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID) + // return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID) + panic("not implemented") } func (g *githubPlugin) Create(ticket *model.Ticket, labels []string) error { diff --git a/internal/render/controller.go b/internal/render/controller.go index 2778e54..3ac0398 100644 --- a/internal/render/controller.go +++ b/internal/render/controller.go @@ -5,6 +5,7 @@ import ( "sort" "time" + "github.com/pkg/errors" "github.com/strongdm/comply/internal/config" "github.com/strongdm/comply/internal/model" ) @@ -93,8 +94,12 @@ func load() (*model.Data, *renderData, error) { rd.Name = project.OrganizationName rd.Controls = controls - // TODO: unhardcode plugin - tp := model.GetPlugin(model.GitHub) + ts, err := config.Config().TicketSystem() + if err != nil { + return nil, nil, errors.Wrap(err, "error in ticket system configuration") + } + + tp := model.GetPlugin(model.TicketSystem(ts)) if tp.Configured() { links := tp.Links() rd.Links = &links diff --git a/internal/ticket/scheduler.go b/internal/ticket/scheduler.go index 6103024..5ff2fda 100644 --- a/internal/ticket/scheduler.go +++ b/internal/ticket/scheduler.go @@ -5,7 +5,9 @@ import ( "sort" "time" + "github.com/pkg/errors" "github.com/robfig/cron" + "github.com/strongdm/comply/internal/config" "github.com/strongdm/comply/internal/model" ) @@ -68,7 +70,10 @@ func TriggerScheduled() error { // in the future, nothing to do continue } - trigger(procedure) + err = trigger(procedure) + if err != nil { + return err + } } else { // don't go back further than 13 months tooOld := time.Now().Add(-1 * time.Hour * 24 * (365 + 30)) @@ -88,7 +93,10 @@ func TriggerScheduled() error { } // is in the past? then trigger. - trigger(procedure) + err = trigger(procedure) + if err != nil { + return err + } break SEARCH } } @@ -97,13 +105,18 @@ func TriggerScheduled() error { return nil } -func trigger(procedure *model.Procedure) { +func trigger(procedure *model.Procedure) error { fmt.Printf("triggering procedure %s (cron expression: %s)\n", procedure.Name, procedure.Cron) - // TODO: don't hardcode GH - tp := model.GetPlugin(model.GitHub) - tp.Create(&model.Ticket{ + ts, err := config.Config().TicketSystem() + if err != nil { + return errors.Wrap(err, "error in ticket system configuration") + } + + tp := model.GetPlugin(model.TicketSystem(ts)) + err = tp.Create(&model.Ticket{ Name: procedure.Name, Body: fmt.Sprintf("%s\n\n\n---\nProcedure-ID: %s", procedure.Body, procedure.ID), }, []string{"comply", "comply-procedure"}) + return err } From 4969d179ec4177274070df430d66e88ce7264f56 Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 16:57:06 -0700 Subject: [PATCH 2/9] find by tag / label --- internal/jira/jira.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/internal/jira/jira.go b/internal/jira/jira.go index a7d7219..773172d 100644 --- a/internal/jira/jira.go +++ b/internal/jira/jira.go @@ -71,6 +71,8 @@ func (j *jiraPlugin) Configured() bool { func (j *jiraPlugin) Links() model.TicketLinks { links := model.TicketLinks{} + // http://localhost:8080/issues/?jql=labels+%3D+comply + links.AuditAll = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) links.AuditOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) links.ProcedureAll = fmt.Sprintf("%s/issues?q=is%3Aissue+label%3Acomply+label%3Aprocedure", j.url) @@ -128,17 +130,11 @@ func (j *jiraPlugin) FindByTag(name, value string) ([]*model.Ticket, error) { } func (j *jiraPlugin) FindByTagName(name string) ([]*model.Ticket, error) { - return []*model.Ticket{}, nil - // issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{ - // State: "all", - // Labels: []string{name}, - // }) - - // if err != nil { - // return nil, errors.Wrap(err, "error during FindOpen") - // } - - // return toTickets(issues), nil + issues, _, err := j.api().Issue.Search("labels=comply", &jira.SearchOptions{MaxResults: 1000}) + if err != nil { + return nil, errors.Wrap(err, "unable to fetch Jira issues") + } + return toTickets(issues), nil } func (j *jiraPlugin) LinkFor(t *model.Ticket) string { @@ -168,10 +164,10 @@ func (j *jiraPlugin) Create(ticket *model.Ticket, labels []string) error { return nil } -func toTickets(issues []*jira.Issue) []*model.Ticket { +func toTickets(issues []jira.Issue) []*model.Ticket { var tickets []*model.Ticket for _, i := range issues { - tickets = append(tickets, toTicket(i)) + tickets = append(tickets, toTicket(&i)) } return tickets } From 4d830789ecfe0409a164b81b2870f0e610a9ec73 Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:01:22 -0700 Subject: [PATCH 3/9] never pull the docker container if pandoc is present and working in the PATH --- internal/cli/app.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/cli/app.go b/internal/cli/app.go index 54297eb..6f948c5 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -106,12 +106,18 @@ func pandocMustExist(c *cli.Context) error { pandocExistErr := pandocBinaryMustExist(c) dockerExistErr := dockerMustExist(c) + config.SetPandoc(pandocExistErr == nil, dockerExistErr == nil) if pandocExistErr != nil && dockerExistErr != nil { return eitherMustExistErr } + // if we don't have pandoc, but we do have docker, execute a pull + if pandocExistErr != nil && dockerExistErr == nil { + dockerPull(c) + } + return nil } @@ -170,6 +176,23 @@ func dockerMustExist(c *cli.Context) error { return dockerErr } + _, err = cli.Ping(ctx) + if err != nil { + return dockerErr + } + + return nil +} + +func dockerPull(c *cli.Context) error { + dockerErr := fmt.Errorf("Docker must be available in order to run `%s`", c.Command.Name) + + ctx := context.Background() + cli, err := client.NewEnvClient() + if err != nil { + return dockerErr + } + done := make(chan struct{}) defer close(done) From 2d5e6b48cb6e8d3a9a1f06e5a927afee61175cd5 Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:04:52 -0700 Subject: [PATCH 4/9] config override should cause image to be pulled --- internal/cli/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/app.go b/internal/cli/app.go index 6f948c5..cdfbaab 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -114,7 +114,7 @@ func pandocMustExist(c *cli.Context) error { } // if we don't have pandoc, but we do have docker, execute a pull - if pandocExistErr != nil && dockerExistErr == nil { + if (pandocExistErr != nil && dockerExistErr == nil) || config.WhichPandoc() == config.UseDocker { dockerPull(c) } From 25f7156ac212fc2a7c59f73fb9d2ff8c2930633a Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:07:36 -0700 Subject: [PATCH 5/9] enable Jira --- internal/cli/init.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/cli/init.go b/internal/cli/init.go index 4b12d3f..9c98bba 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -116,8 +116,7 @@ func initAction(c *cli.Context) error { case 0: ticketing = model.GitHub case 1: - fmt.Println("\nHello Jira user! The Jira ticketing plugin is currently in development, please join us on Slack for a status update.") - ticketing = model.NoTickets + ticketing = model.Jira default: ticketing = model.NoTickets } From f6c9f8979258e680832caf080ef18603b0445b9b Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:18:41 -0700 Subject: [PATCH 6/9] use Resolution field rather than Status --- internal/jira/jira.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/jira/jira.go b/internal/jira/jira.go index 773172d..335d5e3 100644 --- a/internal/jira/jira.go +++ b/internal/jira/jira.go @@ -179,7 +179,7 @@ func toTicket(i *jira.Issue) *model.Ticket { t.Body = i.Fields.Summary createdAt := time.Time(i.Fields.Created) t.CreatedAt = &createdAt - t.State = toState(i.Fields.Status) + t.State = toState(i.Fields.Resolution) for _, l := range i.Fields.Labels { t.SetBool(l) @@ -187,9 +187,12 @@ func toTicket(i *jira.Issue) *model.Ticket { return t } -func toState(status *jira.Status) model.TicketState { +func toState(status *jira.Resolution) model.TicketState { + if status == nil { + return model.Open + } switch status.Name { - case "Closed": + case "Done": return model.Closed } return model.Open From 75a80189ce9fe1eb5543a8b8653dfbeb6f3b1ccb Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:27:56 -0700 Subject: [PATCH 7/9] correct tag name --- internal/render/controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/render/controller.go b/internal/render/controller.go index 3ac0398..8d7ef62 100644 --- a/internal/render/controller.go +++ b/internal/render/controller.go @@ -138,7 +138,7 @@ func addStats(modelData *model.Data, renderData *renderData) { } if t.State == model.Open { - if t.Bool("procedure") { + if t.Bool("comply-procedure") { stats.ProcedureOpen++ if t.CreatedAt != nil { age := int(time.Since(*t.CreatedAt).Hours() / float64(24)) From 0ff74208cc6654a4edadb62d942ed1ea2f8781db Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:36:41 -0700 Subject: [PATCH 8/9] link format --- internal/jira/jira.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/internal/jira/jira.go b/internal/jira/jira.go index 335d5e3..b68bb3e 100644 --- a/internal/jira/jira.go +++ b/internal/jira/jira.go @@ -71,12 +71,10 @@ func (j *jiraPlugin) Configured() bool { func (j *jiraPlugin) Links() model.TicketLinks { links := model.TicketLinks{} - // http://localhost:8080/issues/?jql=labels+%3D+comply - - links.AuditAll = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) - links.AuditOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) - links.ProcedureAll = fmt.Sprintf("%s/issues?q=is%3Aissue+label%3Acomply+label%3Aprocedure", j.url) - links.ProcedureOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aprocedure", j.url) + links.ProcedureAll = fmt.Sprintf("%s/issues/?jql=labels+=+comply-procedure", j.url) + links.ProcedureOpen = fmt.Sprintf("%s/issues/?jql=labels+=+comply-procedure+AND+resolution+=+Unresolved", j.url) + // links.AuditAll = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) + // links.AuditOpen = fmt.Sprintf("%s/issues?q=is%3Aissue+is%3Aopen+label%3Acomply+label%3Aaudit", j.url) return links } @@ -113,16 +111,7 @@ func getCfg(cfg map[string]interface{}, k string) (string, error) { } func (j *jiraPlugin) FindOpen() ([]*model.Ticket, error) { - return []*model.Ticket{}, nil - // issues, _, err := j.api().Issues.ListByRepo(context.Background(), j.username, j.reponame, &github.IssueListByRepoOptions{ - // State: "open", - // }) - - // if err != nil { - // return nil, errors.Wrap(err, "error during FindOpen") - // } - - // return toTickets(issues), nil + panic("not implemented") } func (j *jiraPlugin) FindByTag(name, value string) ([]*model.Ticket, error) { @@ -138,7 +127,6 @@ func (j *jiraPlugin) FindByTagName(name string) ([]*model.Ticket, error) { } func (j *jiraPlugin) LinkFor(t *model.Ticket) string { - // return fmt.Sprintf("https://github.com/strongdm/comply/issues/%s", t.ID) panic("not implemented") } From 4d63cf559b2cb5261b90cf8a1674eb1d494c6c31 Mon Sep 17 00:00:00 2001 From: Justin McCarthy Date: Fri, 1 Jun 2018 17:37:01 -0700 Subject: [PATCH 9/9] dep ensure --- Gopkg.lock | 23 +- .../andygrunwald/go-jira/.gitignore | 29 + .../andygrunwald/go-jira/.travis.yml | 17 + .../andygrunwald/go-jira/Gopkg.lock | 36 + .../andygrunwald/go-jira/Gopkg.toml | 46 + .../github.com/andygrunwald/go-jira/LICENSE | 22 + .../github.com/andygrunwald/go-jira/Makefile | 2 + .../github.com/andygrunwald/go-jira/README.md | 271 ++++ .../andygrunwald/go-jira/authentication.go | 187 +++ .../github.com/andygrunwald/go-jira/board.go | 166 +++ .../github.com/andygrunwald/go-jira/error.go | 82 ++ .../github.com/andygrunwald/go-jira/group.go | 153 +++ .../github.com/andygrunwald/go-jira/issue.go | 1100 +++++++++++++++++ .../github.com/andygrunwald/go-jira/jira.go | 432 +++++++ .../andygrunwald/go-jira/metaissue.go | 194 +++ .../andygrunwald/go-jira/project.go | 162 +++ .../github.com/andygrunwald/go-jira/sprint.go | 106 ++ .../github.com/andygrunwald/go-jira/user.go | 119 ++ .../andygrunwald/go-jira/version.go | 96 ++ vendor/github.com/fatih/structs/.gitignore | 23 + vendor/github.com/fatih/structs/.travis.yml | 11 + vendor/github.com/fatih/structs/LICENSE | 21 + vendor/github.com/fatih/structs/README.md | 163 +++ vendor/github.com/fatih/structs/field.go | 141 +++ vendor/github.com/fatih/structs/structs.go | 586 +++++++++ vendor/github.com/fatih/structs/tags.go | 32 + vendor/github.com/trivago/tgo/LICENSE | 202 +++ .../trivago/tgo/tcontainer/arrays.go | 113 ++ .../trivago/tgo/tcontainer/bytepool.go | 157 +++ .../trivago/tgo/tcontainer/marshalmap.go | 464 +++++++ .../github.com/trivago/tgo/tcontainer/trie.go | 227 ++++ .../trivago/tgo/treflect/reflection.go | 373 ++++++ .../trivago/tgo/treflect/typeregistry.go | 97 ++ 33 files changed, 5852 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/andygrunwald/go-jira/.gitignore create mode 100644 vendor/github.com/andygrunwald/go-jira/.travis.yml create mode 100644 vendor/github.com/andygrunwald/go-jira/Gopkg.lock create mode 100644 vendor/github.com/andygrunwald/go-jira/Gopkg.toml create mode 100644 vendor/github.com/andygrunwald/go-jira/LICENSE create mode 100644 vendor/github.com/andygrunwald/go-jira/Makefile create mode 100644 vendor/github.com/andygrunwald/go-jira/README.md create mode 100644 vendor/github.com/andygrunwald/go-jira/authentication.go create mode 100644 vendor/github.com/andygrunwald/go-jira/board.go create mode 100644 vendor/github.com/andygrunwald/go-jira/error.go create mode 100644 vendor/github.com/andygrunwald/go-jira/group.go create mode 100644 vendor/github.com/andygrunwald/go-jira/issue.go create mode 100644 vendor/github.com/andygrunwald/go-jira/jira.go create mode 100644 vendor/github.com/andygrunwald/go-jira/metaissue.go create mode 100644 vendor/github.com/andygrunwald/go-jira/project.go create mode 100644 vendor/github.com/andygrunwald/go-jira/sprint.go create mode 100644 vendor/github.com/andygrunwald/go-jira/user.go create mode 100644 vendor/github.com/andygrunwald/go-jira/version.go create mode 100644 vendor/github.com/fatih/structs/.gitignore create mode 100644 vendor/github.com/fatih/structs/.travis.yml create mode 100644 vendor/github.com/fatih/structs/LICENSE create mode 100644 vendor/github.com/fatih/structs/README.md create mode 100644 vendor/github.com/fatih/structs/field.go create mode 100644 vendor/github.com/fatih/structs/structs.go create mode 100644 vendor/github.com/fatih/structs/tags.go create mode 100644 vendor/github.com/trivago/tgo/LICENSE create mode 100644 vendor/github.com/trivago/tgo/tcontainer/arrays.go create mode 100644 vendor/github.com/trivago/tgo/tcontainer/bytepool.go create mode 100644 vendor/github.com/trivago/tgo/tcontainer/marshalmap.go create mode 100644 vendor/github.com/trivago/tgo/tcontainer/trie.go create mode 100644 vendor/github.com/trivago/tgo/treflect/reflection.go create mode 100644 vendor/github.com/trivago/tgo/treflect/typeregistry.go diff --git a/Gopkg.lock b/Gopkg.lock index f48b2a7..5d4d161 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,6 +7,12 @@ revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f" version = "v0.4.7" +[[projects]] + name = "github.com/andygrunwald/go-jira" + packages = ["."] + revision = "5cfdb85cc91c6299f75b6504a1d0ec174c21be39" + version = "v1.3.0" + [[projects]] branch = "master" name = "github.com/chzyer/readline" @@ -79,6 +85,12 @@ revision = "507f6050b8568533fb3f5504de8e5205fa62a114" version = "v1.6.0" +[[projects]] + name = "github.com/fatih/structs" + packages = ["."] + revision = "a720dfa8df582c51dee1b36feabb906bde1588bd" + version = "v1.0" + [[projects]] name = "github.com/fsnotify/fsnotify" packages = ["."] @@ -194,6 +206,15 @@ packages = ["open"] revision = "75fb7ed4208cf72d323d7d02fd1a5964a7a9073c" +[[projects]] + name = "github.com/trivago/tgo" + packages = [ + "tcontainer", + "treflect" + ] + revision = "e4d1ddd28c17dd89ed26327cf69fded22060671b" + version = "v1.0.1" + [[projects]] name = "github.com/urfave/cli" packages = ["."] @@ -257,6 +278,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4fd2ff9f9869c3f3e30601504f4b00fce69d282ae8df42583a1c60848bfd0766" + inputs-digest = "b8eb855eeef730f7fcaabe3acceb26a99b7bce186d815c3f654d7a1cbce97f2a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/andygrunwald/go-jira/.gitignore b/vendor/github.com/andygrunwald/go-jira/.gitignore new file mode 100644 index 0000000..1e57f8a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/.gitignore @@ -0,0 +1,29 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Don't check in vendor +vendor/ + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof +*.iml +.idea diff --git a/vendor/github.com/andygrunwald/go-jira/.travis.yml b/vendor/github.com/andygrunwald/go-jira/.travis.yml new file mode 100644 index 0000000..5b477ff --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/.travis.yml @@ -0,0 +1,17 @@ +language: go + +sudo: false + +go: + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - 1.9 + +before_install: + - go get -t ./... + +script: + - GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./... diff --git a/vendor/github.com/andygrunwald/go-jira/Gopkg.lock b/vendor/github.com/andygrunwald/go-jira/Gopkg.lock new file mode 100644 index 0000000..00b3051 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/Gopkg.lock @@ -0,0 +1,36 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/fatih/structs" + packages = ["."] + revision = "a720dfa8df582c51dee1b36feabb906bde1588bd" + version = "v1.0" + +[[projects]] + branch = "master" + name = "github.com/google/go-querystring" + packages = ["query"] + revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/trivago/tgo" + packages = [ + "tcontainer", + "treflect" + ] + revision = "e4d1ddd28c17dd89ed26327cf69fded22060671b" + version = "v1.0.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "e84ca9eea6d233e0947b0d760913db2983fd4cbf6fd0d8690c737a71affb635c" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/andygrunwald/go-jira/Gopkg.toml b/vendor/github.com/andygrunwald/go-jira/Gopkg.toml new file mode 100644 index 0000000..5ebe7d4 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/Gopkg.toml @@ -0,0 +1,46 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/fatih/structs" + version = "1.0.0" + +[[constraint]] + branch = "master" + name = "github.com/google/go-querystring" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[[constraint]] + name = "github.com/trivago/tgo" + version = "1.0.1" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/andygrunwald/go-jira/LICENSE b/vendor/github.com/andygrunwald/go-jira/LICENSE new file mode 100644 index 0000000..692f6be --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andy Grunwald + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/andygrunwald/go-jira/Makefile b/vendor/github.com/andygrunwald/go-jira/Makefile new file mode 100644 index 0000000..d9ddca9 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/Makefile @@ -0,0 +1,2 @@ +test: + go test -v ./... \ No newline at end of file diff --git a/vendor/github.com/andygrunwald/go-jira/README.md b/vendor/github.com/andygrunwald/go-jira/README.md new file mode 100644 index 0000000..272494a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/README.md @@ -0,0 +1,271 @@ +# go-jira + +[![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira) +[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira) +[![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira) + +[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira). + +![Go client library for Atlassian JIRA](./img/logo_small.png "Go client library for Atlassian JIRA.") + +## Features + +* Authentication (HTTP Basic, OAuth, Session Cookie) +* Create and retrieve issues +* Create and retrieve issue transitions (status updates) +* Call every API endpoint of the JIRA, even if it is not directly implemented in this library + +This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/). + +## Compatible JIRA versions + +This package was tested against JIRA v6.3.4 and v7.1.2. + +## Installation + +It is go gettable + + $ go get github.com/andygrunwald/go-jira + +For stable versions you can use one of our tags with [gopkg.in](http://labix.org/gopkg.in). E.g. + +```go +package main + +import ( + jira "gopkg.in/andygrunwald/go-jira.v1" +) +... +``` + +(optional) to run unit / example tests: + + $ cd $GOPATH/src/github.com/andygrunwald/go-jira + $ go test -v ./... + +## API + +Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description. + +The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package. + +## Examples + +Further a few examples how the API can be used. +A few more examples are available in the [GoDoc examples section](https://godoc.org/github.com/andygrunwald/go-jira#pkg-examples). + +### Get a single issue + +Lets retrieve [MESOS-3325](https://issues.apache.org/jira/browse/MESOS-3325) from the [Apache Mesos](http://mesos.apache.org/) project. + +```go +package main + +import ( + "fmt" + "github.com/andygrunwald/go-jira" +) + +func main() { + jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/") + issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil) + + fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary) + fmt.Printf("Type: %s\n", issue.Fields.Type.Name) + fmt.Printf("Priority: %s\n", issue.Fields.Priority.Name) + + // MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart + // Type: Bug + // Priority: Critical +} +``` + +### Authentication + +The `go-jira` library does not handle most authentication directly. Instead, authentication should be handled within +an `http.Client`. That client can then be passed into the `NewClient` function when creating a jira client. + +For convenience, capability for basic and cookie-based authentication is included in the main library. + +#### Basic auth example + +A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. + +```go +func main() { + tp := jira.BasicAuthTransport{ + Username: "username", + Password: "password", + } + + client, err := jira.NewClient(tp.Client(), "https://my.jira.com") + + u, _, err := client.User.Get("some_user") + + fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress) +} +``` + +#### Authenticate with session cookie + +A more thorough, [runnable example](examples/cookieauth/main.go) is provided in the examples directory. + +Note: The `AuthURL` is almost always going to have the path `/rest/auth/1/session` + +```go + tp := jira.CookieAuthTransport{ + Username: "username", + Password: "password", + AuthURL: "https://my.jira.com/rest/auth/1/session", + } + + client, err := jira.NewClient(tp.Client(), "https://my.jira.com") + u, _, err := client.User.Get("admin") + + fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress) +} +``` + +#### Authenticate with OAuth + +If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus). + +For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56). + +### Create an issue + +Example how to create an issue. + +```go +package main + +import ( + "fmt" + "github.com/andygrunwald/go-jira" +) + +func main() { + base := "https://my.jira.com" + tp := jira.CookieAuthTransport{ + Username: "username", + Password: "password", + AuthURL: fmt.Sprintf("%s/rest/auth/1/session", base), + } + + jiraClient, err := jira.NewClient(tp.Client(), base) + if err != nil { + panic(err) + } + + i := jira.Issue{ + Fields: &jira.IssueFields{ + Assignee: &jira.User{ + Name: "myuser", + }, + Reporter: &jira.User{ + Name: "youruser", + }, + Description: "Test Issue", + Type: jira.IssueType{ + Name: "Bug", + }, + Project: jira.Project{ + Key: "PROJ1", + }, + Summary: "Just a demo issue", + }, + } + issue, _, err := jiraClient.Issue.Create(&i) + if err != nil { + panic(err) + } + + fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary) +} +``` + +### Call a not implemented API endpoint + +Not all API endpoints of the JIRA API are implemented into *go-jira*. +But you can call them anyway: +Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/). + +```go +package main + +import ( + "fmt" + "github.com/andygrunwald/go-jira" +) + +func main() { + base := "https://my.jira.com" + tp := jira.CookieAuthTransport{ + Username: "username", + Password: "password", + AuthURL: fmt.Sprintf("%s/rest/auth/1/session", base), + } + + jiraClient, err := jira.NewClient(tp.Client(), base) + req, _ := jiraClient.NewRequest("GET", "/rest/api/2/project", nil) + + projects := new([]jira.Project) + _, err := jiraClient.Do(req, projects) + if err != nil { + panic(err) + } + + for _, project := range *projects { + fmt.Printf("%s: %s\n", project.Key, project.Name) + } + + // ... + // BAM: Bamboo + // BAMJ: Bamboo JIRA Plugin + // CLOV: Clover + // CONF: Confluence + // ... +} +``` + +## Implementations + +* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker + +## Code structure + +The code structure of this package was inspired by [google/go-github](https://github.com/google/go-github). + +There is one main part (the client). +Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`. +These services own a responsibility of the single endpoints / usecases of JIRA. + +## Contribution + +Contribution, in any kind of way, is highly welcome! +It doesn't matter if you are not able to write code. +Creating issues or holding talks and help other people to use [go-jira](https://github.com/andygrunwald/go-jira) is contribution, too! +A few examples: + +* Correct typos in the README / documentation +* Reporting bugs +* Implement a new feature or endpoint +* Sharing the love if [go-jira](https://github.com/andygrunwald/go-jira) and help people to get use to it + +If you are new to pull requests, checkout [Collaborating on projects using issues and pull requests / Creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + +### Dependency management + +`go-jira` uses `dep` for dependency management. After cloning the repo, it's easy to make sure you have the correct dependencies by running `dep ensure`. + +For adding new dependencies, updating dependencies, and other operations, the [Daily Dep](https://golang.github.io/dep/docs/daily-dep.html) is a good place to start. + +### Sandbox environment for testing + +Jira offers sandbox test environments at http://go.atlassian.com/cloud-dev. + +You can read more about them at https://developer.atlassian.com/blog/2016/04/cloud-ecosystem-dev-env/. + +## License + +This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License). diff --git a/vendor/github.com/andygrunwald/go-jira/authentication.go b/vendor/github.com/andygrunwald/go-jira/authentication.go new file mode 100644 index 0000000..f848a1d --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/authentication.go @@ -0,0 +1,187 @@ +package jira + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +const ( + // HTTP Basic Authentication + authTypeBasic = 1 + // HTTP Session Authentication + authTypeSession = 2 +) + +// AuthenticationService handles authentication for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication +type AuthenticationService struct { + client *Client + + // Authentication type + authType int + + // Basic auth username + username string + + // Basic auth password + password string +} + +// Session represents a Session JSON response by the JIRA API. +type Session struct { + Self string `json:"self,omitempty"` + Name string `json:"name,omitempty"` + Session struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"session,omitempty"` + LoginInfo struct { + FailedLoginCount int `json:"failedLoginCount"` + LoginCount int `json:"loginCount"` + LastFailedLoginTime string `json:"lastFailedLoginTime"` + PreviousLoginTime string `json:"previousLoginTime"` + } `json:"loginInfo"` + Cookies []*http.Cookie +} + +// AcquireSessionCookie creates a new session for a user in JIRA. +// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header. +// The header will by automatically applied to every API request. +// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. +// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// +// Deprecated: Use CookieAuthTransport instead +func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { + apiEndpoint := "rest/auth/1/session" + body := struct { + Username string `json:"username"` + Password string `json:"password"` + }{ + username, + password, + } + + req, err := s.client.NewRequest("POST", apiEndpoint, body) + if err != nil { + return false, err + } + + session := new(Session) + resp, err := s.client.Do(req, session) + + if resp != nil { + session.Cookies = resp.Cookies() + } + + if err != nil { + return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err) + } + if resp != nil && resp.StatusCode != 200 { + return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) + } + + s.client.session = session + s.authType = authTypeSession + + return true, nil +} + +// SetBasicAuth sets username and password for the basic auth against the JIRA instance. +// +// Deprecated: Use BasicAuthTransport instead +func (s *AuthenticationService) SetBasicAuth(username, password string) { + s.username = username + s.password = password + s.authType = authTypeBasic +} + +// Authenticated reports if the current Client has authentication details for JIRA +func (s *AuthenticationService) Authenticated() bool { + if s != nil { + if s.authType == authTypeSession { + return s.client.session != nil + } else if s.authType == authTypeBasic { + return s.username != "" + } + + } + return false +} + +// Logout logs out the current user that has been authenticated and the session in the client is destroyed. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// +// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the +// client anymore +func (s *AuthenticationService) Logout() error { + if s.authType != authTypeSession || s.client.session == nil { + return fmt.Errorf("no user is authenticated") + } + + apiEndpoint := "rest/auth/1/session" + req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + if err != nil { + return fmt.Errorf("Creating the request to log the user out failed : %s", err) + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return fmt.Errorf("Error sending the logout request: %s", err) + } + if resp.StatusCode != 204 { + return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode) + } + + // If logout successful, delete session + s.client.session = nil + + return nil + +} + +// GetCurrentUser gets the details of the current user. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +func (s *AuthenticationService) GetCurrentUser() (*Session, error) { + if s == nil { + return nil, fmt.Errorf("AUthenticaiton Service is not instantiated") + } + if s.authType != authTypeSession || s.client.session == nil { + return nil, fmt.Errorf("No user is authenticated yet") + } + + apiEndpoint := "rest/auth/1/session" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, fmt.Errorf("Could not create request for getting user info : %s", err) + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return nil, fmt.Errorf("Error sending request to get user info : %s", err) + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode) + } + + defer resp.Body.Close() + ret := new(Session) + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("Couldn't read body from the response : %s", err) + } + + err = json.Unmarshal(data, &ret) + + if err != nil { + return nil, fmt.Errorf("Could not unmarshall received user info : %s", err) + } + + return ret, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/board.go b/vendor/github.com/andygrunwald/go-jira/board.go new file mode 100644 index 0000000..e206ccd --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/board.go @@ -0,0 +1,166 @@ +package jira + +import ( + "fmt" + "time" +) + +// BoardService handles Agile Boards for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/ +type BoardService struct { + client *Client +} + +// BoardsList reflects a list of agile boards +type BoardsList struct { + MaxResults int `json:"maxResults" structs:"maxResults"` + StartAt int `json:"startAt" structs:"startAt"` + Total int `json:"total" structs:"total"` + IsLast bool `json:"isLast" structs:"isLast"` + Values []Board `json:"values" structs:"values"` +} + +// Board represents a JIRA agile board +type Board struct { + ID int `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitemtpy"` + Type string `json:"type,omitempty" structs:"type,omitempty"` + FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"` +} + +// BoardListOptions specifies the optional parameters to the BoardService.GetList +type BoardListOptions struct { + // BoardType filters results to boards of the specified type. + // Valid values: scrum, kanban. + BoardType string `url:"boardType,omitempty"` + // Name filters results to boards that match or partially match the specified name. + Name string `url:"name,omitempty"` + // ProjectKeyOrID filters results to boards that are relevant to a project. + // Relevance meaning that the JQL filter defined in board contains a reference to a project. + ProjectKeyOrID string `url:"projectKeyOrId,omitempty"` + + SearchOptions +} + +// Wrapper struct for search result +type sprintsResult struct { + Sprints []Sprint `json:"values" structs:"values"` +} + +// Sprint represents a sprint on JIRA agile board +type Sprint struct { + ID int `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + CompleteDate *time.Time `json:"completeDate" structs:"completeDate"` + EndDate *time.Time `json:"endDate" structs:"endDate"` + StartDate *time.Time `json:"startDate" structs:"startDate"` + OriginBoardID int `json:"originBoardId" structs:"originBoardId"` + Self string `json:"self" structs:"self"` + State string `json:"state" structs:"state"` +} + +// GetAllBoards will returns all boards. This only includes boards that the user has permission to view. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards +func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { + apiEndpoint := "rest/agile/1.0/board" + url, err := addOptions(apiEndpoint, opt) + req, err := s.client.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + boards := new(BoardsList) + resp, err := s.client.Do(req, boards) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return boards, resp, err +} + +// GetBoard will returns the board for the given boardID. +// This board will only be returned if the user has permission to view it. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard +func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + board := new(Board) + resp, err := s.client.Do(req, board) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return board, resp, nil +} + +// CreateBoard creates a new board. Board name, type and filter Id is required. +// name - Must be less than 255 characters. +// type - Valid values: scrum, kanban +// filterId - Id of a filter that the user has permissions to view. +// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private +// board will be created instead (remember that board sharing depends on the filter sharing). +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard +func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { + apiEndpoint := "rest/agile/1.0/board" + req, err := s.client.NewRequest("POST", apiEndpoint, board) + if err != nil { + return nil, nil, err + } + + responseBoard := new(Board) + resp, err := s.client.Do(req, responseBoard) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return responseBoard, resp, nil +} + +// DeleteBoard will delete an agile board. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard +func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) + req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + return nil, resp, err +} + +// GetAllSprints will returns all sprints from a board, for a given board Id. +// This only includes sprints that the user has permission to view. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint +func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%s/sprint", boardID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + result := new(sprintsResult) + resp, err := s.client.Do(req, result) + if err != nil { + err = NewJiraError(resp, err) + } + + return result.Sprints, resp, err +} diff --git a/vendor/github.com/andygrunwald/go-jira/error.go b/vendor/github.com/andygrunwald/go-jira/error.go new file mode 100644 index 0000000..0cb8c4b --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/error.go @@ -0,0 +1,82 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/pkg/errors" +) + +// Error message from JIRA +// See https://docs.atlassian.com/jira/REST/cloud/#error-responses +type Error struct { + HTTPError error + ErrorMessages []string `json:"errorMessages"` + Errors map[string]string `json:"errors"` +} + +// NewJiraError creates a new jira Error +func NewJiraError(resp *Response, httpError error) error { + if resp == nil { + return errors.Wrap(httpError, "No response returned") + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, httpError.Error()) + } + + jerr := Error{HTTPError: httpError} + err = json.Unmarshal(body, &jerr) + if err != nil { + httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error()) + return errors.Wrap(err, httpError.Error()) + } + + return &jerr +} + +// Error is a short string representing the error +func (e *Error) Error() string { + if len(e.ErrorMessages) > 0 { + // return fmt.Sprintf("%v", e.HTTPError) + return fmt.Sprintf("%s: %v", e.ErrorMessages[0], e.HTTPError) + } + if len(e.Errors) > 0 { + for key, value := range e.Errors { + return fmt.Sprintf("%s - %s: %v", key, value, e.HTTPError) + } + } + return e.HTTPError.Error() +} + +// LongError is a full representation of the error as a string +func (e *Error) LongError() string { + var msg bytes.Buffer + if e.HTTPError != nil { + msg.WriteString("Original:\n") + msg.WriteString(e.HTTPError.Error()) + msg.WriteString("\n") + } + if len(e.ErrorMessages) > 0 { + msg.WriteString("Messages:\n") + for _, v := range e.ErrorMessages { + msg.WriteString(" - ") + msg.WriteString(v) + msg.WriteString("\n") + } + } + if len(e.Errors) > 0 { + for key, value := range e.Errors { + msg.WriteString(" - ") + msg.WriteString(key) + msg.WriteString(" - ") + msg.WriteString(value) + msg.WriteString("\n") + } + } + return msg.String() +} diff --git a/vendor/github.com/andygrunwald/go-jira/group.go b/vendor/github.com/andygrunwald/go-jira/group.go new file mode 100644 index 0000000..9e2ecaf --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/group.go @@ -0,0 +1,153 @@ +package jira + +import ( + "fmt" + "net/url" +) + +// GroupService handles Groups for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group +type GroupService struct { + client *Client +} + +// groupMembersResult is only a small wrapper around the Group* methods +// to be able to parse the results +type groupMembersResult struct { + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` + Members []GroupMember `json:"values"` +} + +// Group represents a JIRA group +type Group struct { + ID string `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Properties groupProperties `json:"properties"` + AdditionalProperties bool `json:"additionalProperties"` +} + +type groupProperties struct { + Name groupPropertiesName `json:"name"` +} + +type groupPropertiesName struct { + Type string `json:"type"` +} + +// GroupMember reflects a single member of a group +type GroupMember struct { + Self string `json:"self,omitempty"` + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` + EmailAddress string `json:"emailAddress,omitempty"` + DisplayName string `json:"displayName,omitempty"` + Active bool `json:"active,omitempty"` + TimeZone string `json:"timeZone,omitempty"` +} + +type GroupSearchOptions struct { + StartAt int + MaxResults int + IncludeInactiveUsers bool +} + +// Get returns a paginated list of users who are members of the specified group and its subgroups. +// Users in the page are ordered by user names. +// User of this resource is required to have sysadmin or admin permissions. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +// +// WARNING: This API only returns the first page of group members +func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + group := new(groupMembersResult) + resp, err := s.client.Do(req, group) + if err != nil { + return nil, resp, err + } + + return group.Members, resp, nil +} + +// Get returns a paginated list of members of the specified group and its subgroups. +// Users in the page are ordered by user names. +// User of this resource is required to have sysadmin or admin permissions. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { + var apiEndpoint string + if options == nil { + apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) + } else { + apiEndpoint = fmt.Sprintf( + "/rest/api/2/group/member?groupname=%s&startAt=%d&maxResults=%d&includeInactiveUsers=%t", + url.QueryEscape(name), + options.StartAt, + options.MaxResults, + options.IncludeInactiveUsers, + ) + } + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + group := new(groupMembersResult) + resp, err := s.client.Do(req, group) + if err != nil { + return nil, resp, err + } + return group.Members, resp, nil +} + +// Add adds user to group +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup +func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname) + var user struct { + Name string `json:"name"` + } + user.Name = username + req, err := s.client.NewRequest("POST", apiEndpoint, &user) + if err != nil { + return nil, nil, err + } + + responseGroup := new(Group) + resp, err := s.client.Do(req, responseGroup) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return responseGroup, resp, nil +} + +// Remove removes user from group +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup +func (s *GroupService) Remove(groupname string, username string) (*Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username) + req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/issue.go b/vendor/github.com/andygrunwald/go-jira/issue.go new file mode 100644 index 0000000..c29b7b0 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/issue.go @@ -0,0 +1,1100 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/url" + "reflect" + "strings" + "time" + + "github.com/fatih/structs" + "github.com/google/go-querystring/query" + "github.com/trivago/tgo/tcontainer" +) + +const ( + // AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA + AssigneeAutomatic = "-1" +) + +// IssueService handles Issues for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue +type IssueService struct { + client *Client +} + +// Issue represents a JIRA issue. +type Issue struct { + Expand string `json:"expand,omitempty" structs:"expand,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"` + Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"` +} + +// ChangelogItems reflects one single changelog item of a history item +type ChangelogItems struct { + Field string `json:"field" structs:"field"` + FieldType string `json:"fieldtype" structs:"fieldtype"` + From interface{} `json:"from" structs:"from"` + FromString string `json:"fromString" structs:"fromString"` + To interface{} `json:"to" structs:"to"` + ToString string `json:"toString" structs:"toString"` +} + +// ChangelogHistory reflects one single changelog history entry +type ChangelogHistory struct { + Id string `json:"id" structs:"id"` + Author User `json:"author" structs:"author"` + Created string `json:"created" structs:"created"` + Items []ChangelogItems `json:"items" structs:"items"` +} + +// Changelog reflects the change log of an issue +type Changelog struct { + Histories []ChangelogHistory `json:"histories,omitempty"` +} + +// Attachment represents a JIRA attachment +type Attachment struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Filename string `json:"filename,omitempty" structs:"filename,omitempty"` + Author *User `json:"author,omitempty" structs:"author,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Size int `json:"size,omitempty" structs:"size,omitempty"` + MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"` + Content string `json:"content,omitempty" structs:"content,omitempty"` + Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"` +} + +// Epic represents the epic to which an issue is associated +// Not that this struct does not process the returned "color" value +type Epic struct { + ID int `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Self string `json:"self" structs:"self"` + Name string `json:"name" structs:"name"` + Summary string `json:"summary" structs:"summary"` + Done bool `json:"done" structs:"done"` +} + +// IssueFields represents single fields of a JIRA issue. +// Every JIRA issue has several fields attached. +type IssueFields struct { + // TODO Missing fields + // * "aggregatetimespent": null, + // * "workratio": -1, + // * "lastViewed": null, + // * "aggregatetimeoriginalestimate": null, + // * "aggregatetimeestimate": null, + // * "environment": null, + Expand string `json:"expand,omitempty" structs:"expand,omitempty"` + Type IssueType `json:"issuetype,omitempty" structs:"issuetype,omitempty"` + Project Project `json:"project,omitempty" structs:"project,omitempty"` + Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` + Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"` + Resolutiondate Time `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` + Created Time `json:"created,omitempty" structs:"created,omitempty"` + Duedate Date `json:"duedate,omitempty" structs:"duedate,omitempty"` + Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"` + Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"` + Updated Time `json:"updated,omitempty" structs:"updated,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + Summary string `json:"summary,omitempty" structs:"summary,omitempty"` + Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"` + Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"` + Components []*Component `json:"components,omitempty" structs:"components,omitempty"` + Status *Status `json:"status,omitempty" structs:"status,omitempty"` + Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"` + AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"` + TimeTracking *TimeTracking `json:"timetracking,omitempty" structs:"timetracking,omitempty"` + TimeSpent int `json:"timespent,omitempty" structs:"timespent,omitempty"` + TimeEstimate int `json:"timeestimate,omitempty" structs:"timeestimate,omitempty"` + TimeOriginalEstimate int `json:"timeoriginalestimate,omitempty" structs:"timeoriginalestimate,omitempty"` + Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"` + IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"` + Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"` + FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"` + Labels []string `json:"labels,omitempty" structs:"labels,omitempty"` + Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"` + Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"` + Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"` + Sprint *Sprint `json:"sprint,omitempty" structs:"sprint,omitempty"` + Parent *Parent `json:"parent,omitempty" structs:"parent,omitempty"` + Unknowns tcontainer.MarshalMap +} + +// MarshalJSON is a custom JSON marshal function for the IssueFields structs. +// It handles JIRA custom fields and maps those from / to "Unknowns" key. +func (i *IssueFields) MarshalJSON() ([]byte, error) { + m := structs.Map(i) + unknowns, okay := m["Unknowns"] + if okay { + // if unknowns present, shift all key value from unknown to a level up + for key, value := range unknowns.(tcontainer.MarshalMap) { + m[key] = value + } + delete(m, "Unknowns") + } + return json.Marshal(m) +} + +// UnmarshalJSON is a custom JSON marshal function for the IssueFields structs. +// It handles JIRA custom fields and maps those from / to "Unknowns" key. +func (i *IssueFields) UnmarshalJSON(data []byte) error { + + // Do the normal unmarshalling first + // Details for this way: http://choly.ca/post/go-json-marshalling/ + type Alias IssueFields + aux := &struct { + *Alias + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + totalMap := tcontainer.NewMarshalMap() + err := json.Unmarshal(data, &totalMap) + if err != nil { + return err + } + + t := reflect.TypeOf(*i) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tagDetail := field.Tag.Get("json") + if tagDetail == "" { + // ignore if there are no tags + continue + } + options := strings.Split(tagDetail, ",") + + if len(options) == 0 { + return fmt.Errorf("No tags options found for %s", field.Name) + } + // the first one is the json tag + key := options[0] + if _, okay := totalMap.Value(key); okay { + delete(totalMap, key) + } + + } + i = (*IssueFields)(aux.Alias) + // all the tags found in the struct were removed. Whatever is left are unknowns to struct + i.Unknowns = totalMap + return nil + +} + +// IssueType represents a type of a JIRA issue. +// Typical types are "Request", "Bug", "Story", ... +type IssueType struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Subtask bool `json:"subtask,omitempty" structs:"subtask,omitempty"` + AvatarID int `json:"avatarId,omitempty" structs:"avatarId,omitempty"` +} + +// Resolution represents a resolution of a JIRA issue. +// Typical types are "Fixed", "Suspended", "Won't Fix", ... +type Resolution struct { + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Description string `json:"description" structs:"description"` + Name string `json:"name" structs:"name"` +} + +// Priority represents a priority of a JIRA issue. +// Typical types are "Normal", "Moderate", "Urgent", ... +type Priority struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` +} + +// Watches represents a type of how many and which user are "observing" a JIRA issue to track the status / updates. +type Watches struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"` + IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,omitempty"` + Watchers []*Watcher `json:"watchers,omitempty" structs:"watchers,omitempty"` +} + +// Watcher represents a simplified user that "observes" the issue +type Watcher struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` + Active bool `json:"active,omitempty" structs:"active,omitempty"` +} + +// AvatarUrls represents different dimensions of avatars / images +type AvatarUrls struct { + Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"` + Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"` + One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"` + Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"` +} + +// Component represents a "component" of a JIRA issue. +// Components can be user defined in every JIRA instance. +type Component struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` +} + +// Status represents the current status of a JIRA issue. +// Typical status are "Open", "In Progress", "Closed", ... +// Status can be user defined in every JIRA instance. +type Status struct { + Self string `json:"self" structs:"self"` + Description string `json:"description" structs:"description"` + IconURL string `json:"iconUrl" structs:"iconUrl"` + Name string `json:"name" structs:"name"` + ID string `json:"id" structs:"id"` + StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"` +} + +// StatusCategory represents the category a status belongs to. +// Those categories can be user defined in every JIRA instance. +type StatusCategory struct { + Self string `json:"self" structs:"self"` + ID int `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Key string `json:"key" structs:"key"` + ColorName string `json:"colorName" structs:"colorName"` +} + +// Progress represents the progress of a JIRA issue. +type Progress struct { + Progress int `json:"progress" structs:"progress"` + Total int `json:"total" structs:"total"` +} + +// Parent represents the parent of a JIRA issue, to be used with subtask issue types. +type Parent struct { + ID string `json:"id,omitempty" structs:"id"` + Key string `json:"key,omitempty" structs:"key"` +} + +// Time represents the Time definition of JIRA as a time.Time of go +type Time time.Time + +// Date represents the Date definition of JIRA as a time.Time of go +type Date time.Time + +// Wrapper struct for search result +type transitionResult struct { + Transitions []Transition `json:"transitions" structs:"transitions"` +} + +// Transition represents an issue transition in JIRA +type Transition struct { + ID string `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + To Status `json:"to" structs:"status"` + Fields map[string]TransitionField `json:"fields" structs:"fields"` +} + +// TransitionField represents the value of one Transition +type TransitionField struct { + Required bool `json:"required" structs:"required"` +} + +// CreateTransitionPayload is used for creating new issue transitions +type CreateTransitionPayload struct { + Transition TransitionPayload `json:"transition" structs:"transition"` + Fields TransitionPayloadFields `json:"fields" structs:"fields"` +} + +// TransitionPayload represents the request payload of Transition calls like DoTransition +type TransitionPayload struct { + ID string `json:"id" structs:"id"` +} + +// TransitionPayloadFields represents the fields that can be set when executing a transition +type TransitionPayloadFields struct { + Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` +} + +// Option represents an option value in a SelectList or MultiSelect +// custom issue field +type Option struct { + Value string `json:"value" structs:"value"` +} + +// UnmarshalJSON will transform the JIRA time into a time.Time +// during the transformation of the JIRA JSON response +func (t *Time) UnmarshalJSON(b []byte) error { + // Ignore null, like in the main JSON package. + if string(b) == "null" { + return nil + } + ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b)) + if err != nil { + return err + } + *t = Time(ti) + return nil +} + +// UnmarshalJSON will transform the JIRA date into a time.Time +// during the transformation of the JIRA JSON response +func (t *Date) UnmarshalJSON(b []byte) error { + // Ignore null, like in the main JSON package. + if string(b) == "null" { + return nil + } + ti, err := time.Parse("\"2006-01-02\"", string(b)) + if err != nil { + return err + } + *t = Date(ti) + return nil +} + +// MarshalJSON will transform the Date object into a short +// date string as JIRA expects during the creation of a +// JIRA request +func (t Date) MarshalJSON() ([]byte, error) { + time := time.Time(t) + return []byte(time.Format("\"2006-01-02\"")), nil +} + +// Worklog represents the work log of a JIRA issue. +// One Worklog contains zero or n WorklogRecords +// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html +type Worklog struct { + StartAt int `json:"startAt" structs:"startAt"` + MaxResults int `json:"maxResults" structs:"maxResults"` + Total int `json:"total" structs:"total"` + Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"` +} + +// WorklogRecord represents one entry of a Worklog +type WorklogRecord struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Author *User `json:"author,omitempty" structs:"author,omitempty"` + UpdateAuthor *User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"` + Comment string `json:"comment,omitempty" structs:"comment,omitempty"` + Created *Time `json:"created,omitempty" structs:"created,omitempty"` + Updated *Time `json:"updated,omitempty" structs:"updated,omitempty"` + Started *Time `json:"started,omitempty" structs:"started,omitempty"` + TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"` + TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"` +} + +// TimeTracking represents the timetracking fields of a JIRA issue. +type TimeTracking struct { + OriginalEstimate string `json:"originalEstimate,omitempty" structs:"originalEstimate,omitempty"` + RemainingEstimate string `json:"remainingEstimate,omitempty" structs:"remainingEstimate,omitempty"` + TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"` + OriginalEstimateSeconds int `json:"originalEstimateSeconds,omitempty" structs:"originalEstimateSeconds,omitempty"` + RemainingEstimateSeconds int `json:"remainingEstimateSeconds,omitempty" structs:"remainingEstimateSeconds,omitempty"` + TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"` +} + +// Subtasks represents all issues of a parent issue. +type Subtasks struct { + ID string `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Self string `json:"self" structs:"self"` + Fields IssueFields `json:"fields" structs:"fields"` +} + +// IssueLink represents a link between two issues in JIRA. +type IssueLink struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Type IssueLinkType `json:"type" structs:"type"` + OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"` + InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"` + Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"` +} + +// IssueLinkType represents a type of a link between to issues in JIRA. +// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc. +type IssueLinkType struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name" structs:"name"` + Inward string `json:"inward" structs:"inward"` + Outward string `json:"outward" structs:"outward"` +} + +// Comments represents a list of Comment. +type Comments struct { + Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"` +} + +// Comment represents a comment by a person to an issue in JIRA. +type Comment struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Author User `json:"author,omitempty" structs:"author,omitempty"` + Body string `json:"body,omitempty" structs:"body,omitempty"` + UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"` + Updated string `json:"updated,omitempty" structs:"updated,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"` +} + +// FixVersion represents a software release in which an issue is fixed. +type FixVersion struct { + Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` + ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` + Released *bool `json:"released,omitempty" structs:"released,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` +} + +// CommentVisibility represents he visibility of a comment. +// E.g. Type could be "role" and Value "Administrators" +type CommentVisibility struct { + Type string `json:"type,omitempty" structs:"type,omitempty"` + Value string `json:"value,omitempty" structs:"value,omitempty"` +} + +// SearchOptions specifies the optional parameters to various List methods that +// support pagination. +// Pagination is used for the JIRA REST APIs to conserve server resources and limit +// response size for resources that return potentially large collection of items. +// A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata +// Default Pagination options +type SearchOptions struct { + // StartAt: The starting index of the returned projects. Base index: 0. + StartAt int `url:"startAt,omitempty"` + // MaxResults: The maximum number of projects to return per page. Default: 50. + MaxResults int `url:"maxResults,omitempty"` + // Expand: Expand specific sections in the returned issues + Expand string `url:"expand,omitempty"` + Fields []string + // ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict. + ValidateQuery string `url:"validateQuery,omitempty"` +} + +// searchResult is only a small wrapper around the Search (with JQL) method +// to be able to parse the results +type searchResult struct { + Issues []Issue `json:"issues" structs:"issues"` + StartAt int `json:"startAt" structs:"startAt"` + MaxResults int `json:"maxResults" structs:"maxResults"` + Total int `json:"total" structs:"total"` +} + +// GetQueryOptions specifies the optional parameters for the Get Issue methods +type GetQueryOptions struct { + // Fields is the list of fields to return for the issue. By default, all fields are returned. + Fields string `url:"fields,omitempty"` + Expand string `url:"expand,omitempty"` + // Properties is the list of properties to return for the issue. By default no properties are returned. + Properties string `url:"properties,omitempty"` + // FieldsByKeys if true then fields in issues will be referenced by keys instead of ids + FieldsByKeys bool `url:"fieldsByKeys,omitempty"` + UpdateHistory bool `url:"updateHistory,omitempty"` + ProjectKeys string `url:"projectKeys,omitempty"` +} + +// CustomFields represents custom fields of JIRA +// This can heavily differ between JIRA instances +type CustomFields map[string]string + +// Get returns a full representation of the issue for the given issue key. +// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. +// This can be an issue id, or an issue key. +// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. +// +// The given options will be appended to the query string +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue +func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + issue := new(Issue) + resp, err := s.client.Do(req, issue) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return issue, resp, nil +} + +// DownloadAttachment returns a Response of an attachment for a given attachmentID. +// The attachment is in the Response.Body of the response. +// This is an io.ReadCloser. +// The caller should close the resp.Body. +func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { + apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// PostAttachment uploads r (io.Reader) as an attachment to a given issueID +func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID) + + b := new(bytes.Buffer) + writer := multipart.NewWriter(b) + + fw, err := writer.CreateFormFile("file", attachmentName) + if err != nil { + return nil, nil, err + } + + if r != nil { + // Copy the file + if _, err = io.Copy(fw, r); err != nil { + return nil, nil, err + } + } + writer.Close() + + req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b) + if err != nil { + return nil, nil, err + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + // PostAttachment response returns a JSON array (as multiple attachments can be posted) + attachment := new([]Attachment) + resp, err := s.client.Do(req, attachment) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return attachment, resp, nil +} + +// GetWorklogs gets all the worklogs for an issue. +// This method is especially important if you need to read all the worklogs, not just the first page. +// +// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog +func (s *IssueService) GetWorklogs(issueID string) (*Worklog, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + v := new(Worklog) + resp, err := s.client.Do(req, v) + return v, resp, err +} + +// Create creates an issue or a sub-task from a JSON representation. +// Creating a sub-task is similar to creating a regular issue, with two important differences: +// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues +func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { + apiEndpoint := "rest/api/2/issue/" + req, err := s.client.NewRequest("POST", apiEndpoint, issue) + if err != nil { + return nil, nil, err + } + resp, err := s.client.Do(req, nil) + if err != nil { + // incase of error return the resp for further inspection + return nil, resp, err + } + + responseIssue := new(Issue) + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, resp, fmt.Errorf("Could not read the returned data") + } + err = json.Unmarshal(data, responseIssue) + if err != nil { + return nil, resp, fmt.Errorf("Could not unmarshall the data into struct") + } + return responseIssue, resp, nil +} + +// Update updates an issue from a JSON representation. The issue is found by key. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue +func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key) + req, err := s.client.NewRequest("PUT", apiEndpoint, issue) + if err != nil { + return nil, nil, err + } + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + // This is just to follow the rest of the API's convention of returning an issue. + // Returning the same pointer here is pointless, so we return a copy instead. + ret := *issue + return &ret, resp, nil +} + +// UpdateIssue updates an issue from a JSON representation. The issue is found by key. +// +// https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue +func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraID) + req, err := s.client.NewRequest("PUT", apiEndpoint, data) + if err != nil { + return nil, err + } + resp, err := s.client.Do(req, nil) + if err != nil { + return resp, err + } + + // This is just to follow the rest of the API's convention of returning an issue. + // Returning the same pointer here is pointless, so we return a copy instead. + return resp, nil +} + +// AddComment adds a new comment to issueID. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment +func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID) + req, err := s.client.NewRequest("POST", apiEndpoint, comment) + if err != nil { + return nil, nil, err + } + + responseComment := new(Comment) + resp, err := s.client.Do(req, responseComment) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return responseComment, resp, nil +} + +// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment +func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { + reqBody := struct { + Body string `json:"body"` + }{ + Body: comment.Body, + } + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID) + req, err := s.client.NewRequest("POST", apiEndpoint, reqBody) + if err != nil { + return nil, nil, err + } + + responseComment := new(Comment) + resp, err := s.client.Do(req, responseComment) + if err != nil { + return nil, resp, err + } + + return responseComment, resp, nil +} + +// AddWorklogRecord adds a new worklog record to issueID. +// +// https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-worklog-post +func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord) (*WorklogRecord, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) + req, err := s.client.NewRequest("POST", apiEndpoint, record) + if err != nil { + return nil, nil, err + } + + responseRecord := new(WorklogRecord) + resp, err := s.client.Do(req, responseRecord) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return responseRecord, resp, nil +} + +// AddLink adds a link between two issues. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink +func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issueLink") + req, err := s.client.NewRequest("POST", apiEndpoint, issueLink) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} + +// Search will search for tickets according to the jql +// +// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues +func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { + var u string + if options == nil { + u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql)) + } else { + u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d&expand=%s&fields=%s&validateQuery=%s", url.QueryEscape(jql), + options.StartAt, options.MaxResults, options.Expand, strings.Join(options.Fields, ","), options.ValidateQuery) + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return []Issue{}, nil, err + } + + v := new(searchResult) + resp, err := s.client.Do(req, v) + if err != nil { + err = NewJiraError(resp, err) + } + return v.Issues, resp, err +} + +// SearchPages will get issues from all pages in a search +// +// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues +func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { + if options == nil { + options = &SearchOptions{ + StartAt: 0, + MaxResults: 50, + } + } + + if options.MaxResults == 0 { + options.MaxResults = 50 + } + + issues, resp, err := s.Search(jql, options) + if err != nil { + return err + } + + for { + for _, issue := range issues { + err = f(issue) + if err != nil { + return err + } + } + + if resp.StartAt+resp.MaxResults >= resp.Total { + return nil + } + + options.StartAt += resp.MaxResults + issues, resp, err = s.Search(jql, options) + if err != nil { + return err + } + } +} + +// GetCustomFields returns a map of customfield_* keys with string values +func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + issue := new(map[string]interface{}) + resp, err := s.client.Do(req, issue) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + m := *issue + f := m["fields"] + cf := make(CustomFields) + if f == nil { + return cf, resp, nil + } + + if rec, ok := f.(map[string]interface{}); ok { + for key, val := range rec { + if strings.Contains(key, "customfield") { + if valMap, ok := val.(map[string]interface{}); ok { + if v, ok := valMap["value"]; ok { + val = v + } + } + cf[key] = fmt.Sprint(val) + } + } + } + return cf, resp, nil +} + +// GetTransitions gets a list of the transitions possible for this issue by the current user, +// along with fields that are required and their types. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions +func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + result := new(transitionResult) + resp, err := s.client.Do(req, result) + if err != nil { + err = NewJiraError(resp, err) + } + return result.Transitions, resp, err +} + +// DoTransition performs a transition on an issue. +// When performing the transition you can update or set other issue fields. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition +func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { + payload := CreateTransitionPayload{ + Transition: TransitionPayload{ + ID: transitionID, + }, + } + return s.DoTransitionWithPayload(ticketID, payload) +} + +// DoTransitionWithPayload performs a transition on an issue using any payload. +// When performing the transition you can update or set other issue fields. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition +func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID) + + req, err := s.client.NewRequest("POST", apiEndpoint, payload) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} + +// InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set. +// * metaProject should contain metaInformation about the project where the issue should be created. +// * metaIssuetype is the MetaInformation about the Issuetype that needs to be created. +// * fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI +// And value is the string value for that particular key. +// Note: This method doesn't verify that the fieldsConfig is complete with mandatory fields. The fieldsConfig is +// supposed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return +// error if the key is not found. +// All values will be packed into Unknowns. This is much convenient. If the struct fields needs to be +// configured as well, marshalling and unmarshalling will set the proper fields. +func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIssueType, fieldsConfig map[string]string) (*Issue, error) { + issue := new(Issue) + issueFields := new(IssueFields) + issueFields.Unknowns = tcontainer.NewMarshalMap() + + // map the field names the User presented to jira's internal key + allFields, _ := metaIssuetype.GetAllFields() + for key, value := range fieldsConfig { + jiraKey, found := allFields[key] + if !found { + return nil, fmt.Errorf("key %s is not found in the list of fields", key) + } + + valueType, err := metaIssuetype.Fields.String(jiraKey + "/schema/type") + if err != nil { + return nil, err + } + switch valueType { + case "array": + elemType, err := metaIssuetype.Fields.String(jiraKey + "/schema/items") + if err != nil { + return nil, err + } + switch elemType { + case "component": + issueFields.Unknowns[jiraKey] = []Component{{Name: value}} + case "option": + issueFields.Unknowns[jiraKey] = []map[string]string{{"value": value}} + default: + issueFields.Unknowns[jiraKey] = []string{value} + } + case "string": + issueFields.Unknowns[jiraKey] = value + case "date": + issueFields.Unknowns[jiraKey] = value + case "datetime": + issueFields.Unknowns[jiraKey] = value + case "any": + // Treat any as string + issueFields.Unknowns[jiraKey] = value + case "project": + issueFields.Unknowns[jiraKey] = Project{ + Name: metaProject.Name, + ID: metaProject.Id, + } + case "priority": + issueFields.Unknowns[jiraKey] = Priority{Name: value} + case "user": + issueFields.Unknowns[jiraKey] = User{ + Name: value, + } + case "issuetype": + issueFields.Unknowns[jiraKey] = IssueType{ + Name: value, + } + case "option": + issueFields.Unknowns[jiraKey] = Option{ + Value: value, + } + default: + return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key) + } + } + + issue.Fields = issueFields + + return issue, nil +} + +// Delete will delete a specified issue. +func (s *IssueService) Delete(issueID string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) + + // to enable deletion of subtasks; without this, the request will fail if the issue has subtasks + deletePayload := make(map[string]interface{}) + deletePayload["deleteSubtasks"] = "true" + content, _ := json.Marshal(deletePayload) + + req, err := s.client.NewRequest("DELETE", apiEndpoint, content) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + return resp, err +} + +// GetWatchers wil return all the users watching/observing the given issue +// +// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers +func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { + watchesAPIEndpoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) + + req, err := s.client.NewRequest("GET", watchesAPIEndpoint, nil) + if err != nil { + return nil, nil, err + } + + watches := new(Watches) + resp, err := s.client.Do(req, watches) + if err != nil { + return nil, nil, NewJiraError(resp, err) + } + + result := []User{} + user := new(User) + for _, watcher := range watches.Watchers { + user, resp, err = s.client.User.Get(watcher.Name) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + result = append(result, *user) + } + + return &result, resp, nil +} + +// AddWatcher adds watcher to the given issue +// +// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher +func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) + + req, err := s.client.NewRequest("POST", apiEndPoint, userName) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} + +// RemoveWatcher removes given user from given issue +// +// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher +func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) + + req, err := s.client.NewRequest("DELETE", apiEndPoint, userName) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} diff --git a/vendor/github.com/andygrunwald/go-jira/jira.go b/vendor/github.com/andygrunwald/go-jira/jira.go new file mode 100644 index 0000000..1070379 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/jira.go @@ -0,0 +1,432 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "time" + + "github.com/google/go-querystring/query" + "github.com/pkg/errors" +) + +// A Client manages communication with the JIRA API. +type Client struct { + // HTTP client used to communicate with the API. + client *http.Client + + // Base URL for API requests. + baseURL *url.URL + + // Session storage if the user authentificate with a Session cookie + session *Session + + // Services used for talking to different parts of the JIRA API. + Authentication *AuthenticationService + Issue *IssueService + Project *ProjectService + Board *BoardService + Sprint *SprintService + User *UserService + Group *GroupService + Version *VersionService +} + +// NewClient returns a new JIRA API client. +// If a nil httpClient is provided, http.DefaultClient will be used. +// To use API methods which require authentication you can follow the preferred solution and +// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library). +// As an alternative you can use Session Cookie based authentication provided by this package as well. +// See https://docs.atlassian.com/jira/REST/latest/#authentication +// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash. +func NewClient(httpClient *http.Client, baseURL string) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + + parsedBaseURL, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + c := &Client{ + client: httpClient, + baseURL: parsedBaseURL, + } + c.Authentication = &AuthenticationService{client: c} + c.Issue = &IssueService{client: c} + c.Project = &ProjectService{client: c} + c.Board = &BoardService{client: c} + c.Sprint = &SprintService{client: c} + c.User = &UserService{client: c} + c.Group = &GroupService{client: c} + c.Version = &VersionService{client: c} + + return c, nil +} + +// NewRawRequest creates an API request. +// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. +// Relative URLs should always be specified without a preceding slash. +// Allows using an optional native io.Reader for sourcing the request body. +func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.baseURL.ResolveReference(rel) + + req, err := http.NewRequest(method, u.String(), body) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + // Set authentication information + if c.Authentication.authType == authTypeSession { + // Set session cookie if there is one + if c.session != nil { + for _, cookie := range c.session.Cookies { + req.AddCookie(cookie) + } + } + } else if c.Authentication.authType == authTypeBasic { + // Set basic auth information + if c.Authentication.username != "" { + req.SetBasicAuth(c.Authentication.username, c.Authentication.password) + } + } + + return req, nil +} + +// NewRequest creates an API request. +// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. +// Relative URLs should always be specified without a preceding slash. +// If specified, the value pointed to by body is JSON encoded and included as the request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.baseURL.ResolveReference(rel) + + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + err = json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + // Set authentication information + if c.Authentication.authType == authTypeSession { + // Set session cookie if there is one + if c.session != nil { + for _, cookie := range c.session.Cookies { + req.AddCookie(cookie) + } + } + } else if c.Authentication.authType == authTypeBasic { + // Set basic auth information + if c.Authentication.username != "" { + req.SetBasicAuth(c.Authentication.username, c.Authentication.password) + } + } + + return req, nil +} + +// addOptions adds the parameters in opt as URL query parameters to s. opt +// must be a struct whose fields may contain "url" tags. +func addOptions(s string, opt interface{}) (string, error) { + v := reflect.ValueOf(opt) + if v.Kind() == reflect.Ptr && v.IsNil() { + return s, nil + } + + u, err := url.Parse(s) + if err != nil { + return s, err + } + + qs, err := query.Values(opt) + if err != nil { + return s, err + } + + u.RawQuery = qs.Encode() + return u.String(), nil +} + +// NewMultiPartRequest creates an API request including a multi-part file. +// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. +// Relative URLs should always be specified without a preceding slash. +// If specified, the value pointed to by buf is a multipart form. +func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { + rel, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + u := c.baseURL.ResolveReference(rel) + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + // Set required headers + req.Header.Set("X-Atlassian-Token", "nocheck") + + // Set authentication information + if c.Authentication.authType == authTypeSession { + // Set session cookie if there is one + if c.session != nil { + for _, cookie := range c.session.Cookies { + req.AddCookie(cookie) + } + } + } else if c.Authentication.authType == authTypeBasic { + // Set basic auth information + if c.Authentication.username != "" { + req.SetBasicAuth(c.Authentication.username, c.Authentication.password) + } + } + + return req, nil +} + +// Do sends an API request and returns the API response. +// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred. +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { + httpResp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + err = CheckResponse(httpResp) + if err != nil { + // Even though there was an error, we still return the response + // in case the caller wants to inspect it further + return newResponse(httpResp, nil), err + } + + if v != nil { + // Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to + defer httpResp.Body.Close() + err = json.NewDecoder(httpResp.Body).Decode(v) + } + + resp := newResponse(httpResp, v) + return resp, err +} + +// CheckResponse checks the API response for errors, and returns them if present. +// A response is considered an error if it has a status code outside the 200 range. +// The caller is responsible to analyze the response body. +// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes). +func CheckResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + + err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) + return err +} + +// GetBaseURL will return you the Base URL. +// This is the same URL as in the NewClient constructor +func (c *Client) GetBaseURL() url.URL { + return *c.baseURL +} + +// Response represents JIRA API response. It wraps http.Response returned from +// API and provides information about paging. +type Response struct { + *http.Response + + StartAt int + MaxResults int + Total int +} + +func newResponse(r *http.Response, v interface{}) *Response { + resp := &Response{Response: r} + resp.populatePageValues(v) + return resp +} + +// Sets paging values if response json was parsed to searchResult type +// (can be extended with other types if they also need paging info) +func (r *Response) populatePageValues(v interface{}) { + switch value := v.(type) { + case *searchResult: + r.StartAt = value.StartAt + r.MaxResults = value.MaxResults + r.Total = value.Total + case *groupMembersResult: + r.StartAt = value.StartAt + r.MaxResults = value.MaxResults + r.Total = value.Total + } + return +} + +// BasicAuthTransport is an http.RoundTripper that authenticates all requests +// using HTTP Basic Authentication with the provided username and password. +type BasicAuthTransport struct { + Username string + Password string + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip implements the RoundTripper interface. We just add the +// basic auth and return the RoundTripper for this transport type. +func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := cloneRequest(req) // per RoundTripper contract + + req2.SetBasicAuth(t.Username, t.Password) + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using HTTP Basic Authentication. This is a nice little bit of sugar +// so we can just get the client instead of creating the client in the calling code. +// If it's necessary to send more information on client init, the calling code can +// always skip this and set the transport itself. +func (t *BasicAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *BasicAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// CookieAuthTransport is an http.RoundTripper that authenticates all requests +// using Jira's cookie-based authentication. +// +// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. +// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session +type CookieAuthTransport struct { + Username string + Password string + AuthURL string + + // SessionObject is the authenticated cookie string.s + // It's passed in each call to prove the client is authenticated. + SessionObject []*http.Cookie + + // Transport is the underlying HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// RoundTrip adds the session object to the request. +func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.SessionObject == nil { + err := t.setSessionObject() + if err != nil { + return nil, errors.Wrap(err, "cookieauth: no session object has been set") + } + } + + req2 := cloneRequest(req) // per RoundTripper contract + for _, cookie := range t.SessionObject { + req2.AddCookie(cookie) + } + + return t.transport().RoundTrip(req2) +} + +// Client returns an *http.Client that makes requests that are authenticated +// using cookie authentication +func (t *CookieAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +// setSessionObject attempts to authenticate the user and set +// the session object (e.g. cookie) +func (t *CookieAuthTransport) setSessionObject() error { + req, err := t.buildAuthRequest() + if err != nil { + return err + } + + var authClient = &http.Client{ + Timeout: time.Second * 60, + } + resp, err := authClient.Do(req) + if err != nil { + return err + } + + t.SessionObject = resp.Cookies() + return nil +} + +// getAuthRequest assembles the request to get the authenticated cookie +func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) { + body := struct { + Username string `json:"username"` + Password string `json:"password"` + }{ + t.Username, + t.Password, + } + + b := new(bytes.Buffer) + json.NewEncoder(b).Encode(body) + + req, err := http.NewRequest("POST", t.AuthURL, b) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + return req, nil +} + +func (t *CookieAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + return r2 +} diff --git a/vendor/github.com/andygrunwald/go-jira/metaissue.go b/vendor/github.com/andygrunwald/go-jira/metaissue.go new file mode 100644 index 0000000..1981378 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/metaissue.go @@ -0,0 +1,194 @@ +package jira + +import ( + "fmt" + "strings" + + "github.com/google/go-querystring/query" + "github.com/trivago/tgo/tcontainer" +) + +// CreateMetaInfo contains information about fields and their attributed to create a ticket. +type CreateMetaInfo struct { + Expand string `json:"expand,omitempty"` + Projects []*MetaProject `json:"projects,omitempty"` +} + +// MetaProject is the meta information about a project returned from createmeta api +type MetaProject struct { + Expand string `json:"expand,omitempty"` + Self string `json:"self,omitempty"` + Id string `json:"id,omitempty"` + Key string `json:"key,omitempty"` + Name string `json:"name,omitempty"` + // omitted avatarUrls + IssueTypes []*MetaIssueType `json:"issuetypes,omitempty"` +} + +// MetaIssueType represents the different issue types a project has. +// +// Note: Fields is interface because this is an object which can +// have arbitraty keys related to customfields. It is not possible to +// expect these for a general way. This will be returning a map. +// Further processing must be done depending on what is required. +type MetaIssueType struct { + Self string `json:"self,omitempty"` + Id string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + IconUrl string `json:"iconurl,omitempty"` + Name string `json:"name,omitempty"` + Subtasks bool `json:"subtask,omitempty"` + Expand string `json:"expand,omitempty"` + Fields tcontainer.MarshalMap `json:"fields,omitempty"` +} + +// GetCreateMeta makes the api call to get the meta information required to create a ticket +func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) +} + +// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey +func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { + apiEndpoint := "rest/api/2/issue/createmeta" + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + meta := new(CreateMetaInfo) + resp, err := s.client.Do(req, meta) + + if err != nil { + return nil, resp, err + } + + return meta, resp, nil +} + +// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil. +// The comparison of the name is case insensitive. +func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { + for _, m := range m.Projects { + if strings.ToLower(m.Name) == strings.ToLower(name) { + return m + } + } + return nil +} + +// GetProjectWithKey returns a project with "name" from the meta information received. If not found, this returns nil. +// The comparison of the name is case insensitive. +func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { + for _, m := range m.Projects { + if strings.ToLower(m.Key) == strings.ToLower(key) { + return m + } + } + return nil +} + +// GetIssueTypeWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil. +// The comparison of the name is case insensitive +func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType { + for _, m := range p.IssueTypes { + if strings.ToLower(m.Name) == strings.ToLower(name) { + return m + } + } + return nil +} + +// GetMandatoryFields returns a map of all the required fields from the MetaIssueTypes. +// if a field returned by the api was: +// "customfield_10806": { +// "required": true, +// "schema": { +// "type": "any", +// "custom": "com.pyxis.greenhopper.jira:gh-epic-link", +// "customId": 10806 +// }, +// "name": "Epic Link", +// "hasDefaultValue": false, +// "operations": [ +// "set" +// ] +// } +// the returned map would have "Epic Link" as the key and "customfield_10806" as value. +// This choice has been made so that the it is easier to generate the create api request later. +func (t *MetaIssueType) GetMandatoryFields() (map[string]string, error) { + ret := make(map[string]string) + for key := range t.Fields { + required, err := t.Fields.Bool(key + "/required") + if err != nil { + return nil, err + } + if required { + name, err := t.Fields.String(key + "/name") + if err != nil { + return nil, err + } + ret[name] = key + } + } + return ret, nil +} + +// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required. +// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema. +func (t *MetaIssueType) GetAllFields() (map[string]string, error) { + ret := make(map[string]string) + for key := range t.Fields { + + name, err := t.Fields.String(key + "/name") + if err != nil { + return nil, err + } + ret[name] = key + } + return ret, nil +} + +// CheckCompleteAndAvailable checks if the given fields satisfies the mandatory field required to create a issue for the given type +// And also if the given fields are available. +func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (bool, error) { + mandatory, err := t.GetMandatoryFields() + if err != nil { + return false, err + } + all, err := t.GetAllFields() + if err != nil { + return false, err + } + + // check templateconfig against mandatory fields + for key := range mandatory { + if _, okay := config[key]; !okay { + var requiredFields []string + for name := range mandatory { + requiredFields = append(requiredFields, name) + } + return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields) + } + } + + // check templateConfig against all fields to verify they are available + for key := range config { + if _, okay := all[key]; !okay { + var availableFields []string + for name := range all { + availableFields = append(availableFields, name) + } + return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields) + } + } + + return true, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/project.go b/vendor/github.com/andygrunwald/go-jira/project.go new file mode 100644 index 0000000..b71b5bb --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/project.go @@ -0,0 +1,162 @@ +package jira + +import ( + "fmt" + + "github.com/google/go-querystring/query" +) + +// ProjectService handles projects for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project +type ProjectService struct { + client *Client +} + +// ProjectList represent a list of Projects +type ProjectList []struct { + Expand string `json:"expand" structs:"expand"` + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Name string `json:"name" structs:"name"` + AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"` + ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"` + ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"` + IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"` +} + +// ProjectCategory represents a single project category +type ProjectCategory struct { + Self string `json:"self" structs:"self,omitempty"` + ID string `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Description string `json:"description" structs:"description,omitempty"` +} + +// Project represents a JIRA Project. +type Project struct { + Expand string `json:"expand,omitempty" structs:"expand,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + Lead User `json:"lead,omitempty" structs:"lead,omitempty"` + Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"` + IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"` + URL string `json:"url,omitempty" structs:"url,omitempty"` + Email string `json:"email,omitempty" structs:"email,omitempty"` + AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"` + Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Roles struct { + Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"` + } `json:"roles,omitempty" structs:"roles,omitempty"` + AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` + ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"` +} + +// ProjectComponent represents a single component of a project +type ProjectComponent struct { + Self string `json:"self" structs:"self,omitempty"` + ID string `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Description string `json:"description" structs:"description,omitempty"` + Lead User `json:"lead,omitempty" structs:"lead,omitempty"` + AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"` + Assignee User `json:"assignee" structs:"assignee,omitempty"` + RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"` + RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"` + IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"` + Project string `json:"project" structs:"project,omitempty"` + ProjectID int `json:"projectId" structs:"projectId,omitempty"` +} + +// PermissionScheme represents the permission scheme for the project +type PermissionScheme struct { + Expand string `json:"expand" structs:"expand,omitempty"` + Self string `json:"self" structs:"self,omitempty"` + ID int `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Description string `json:"description" structs:"description,omitempty"` +} + +// GetList gets all projects form JIRA +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects +func (s *ProjectService) GetList() (*ProjectList, *Response, error) { + return s.ListWithOptions(&GetQueryOptions{}) +} + +// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get +// a list of all projects and their supported issuetypes +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects +func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { + apiEndpoint := "rest/api/2/project" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + projectList := new(ProjectList) + resp, err := s.client.Do(req, projectList) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return projectList, resp, nil +} + +// Get returns a full representation of the project for the given issue key. +// JIRA will attempt to identify the project by the projectIdOrKey path parameter. +// This can be an project id, or an project key. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject +func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + project := new(Project) + resp, err := s.client.Do(req, project) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return project, resp, nil +} + +// GetPermissionScheme returns a full representation of the permission scheme for the project +// JIRA will attempt to identify the project by the projectIdOrKey path parameter. +// This can be an project id, or an project key. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject +func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + ps := new(PermissionScheme) + resp, err := s.client.Do(req, ps) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return ps, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/sprint.go b/vendor/github.com/andygrunwald/go-jira/sprint.go new file mode 100644 index 0000000..65ba873 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/sprint.go @@ -0,0 +1,106 @@ +package jira + +import ( + "fmt" + "github.com/google/go-querystring/query" +) + +// SprintService handles sprints in JIRA Agile API. +// See https://docs.atlassian.com/jira-software/REST/cloud/ +type SprintService struct { + client *Client +} + +// IssuesWrapper represents a wrapper struct for moving issues to sprint +type IssuesWrapper struct { + Issues []string `json:"issues"` +} + +// IssuesInSprintResult represents a wrapper struct for search result +type IssuesInSprintResult struct { + Issues []Issue `json:"issues"` +} + +// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id. +// Issues can only be moved to open or active sprints. +// The maximum number of issues that can be moved in one operation is 50. +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint +func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) + + payload := IssuesWrapper{Issues: issueIDs} + + req, err := s.client.NewRequest("POST", apiEndpoint, payload) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + return resp, err +} + +// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id. +// This only includes issues that the user has permission to view. +// By default, the returned issues are ordered by rank. +// +// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint +func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + + if err != nil { + return nil, nil, err + } + + result := new(IssuesInSprintResult) + resp, err := s.client.Do(req, result) + if err != nil { + err = NewJiraError(resp, err) + } + + return result.Issues, resp, err +} + +// Get returns a full representation of the issue for the given issue key. +// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. +// This can be an issue id, or an issue key. +// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. +// +// The given options will be appended to the query string +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue +// +// TODO: create agile service for holding all agile apis' implementation +func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID) + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + + if err != nil { + return nil, nil, err + } + + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + issue := new(Issue) + resp, err := s.client.Do(req, issue) + + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return issue, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/user.go b/vendor/github.com/andygrunwald/go-jira/user.go new file mode 100644 index 0000000..7b2bd59 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/user.go @@ -0,0 +1,119 @@ +package jira + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +// UserService handles users for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user +type UserService struct { + client *Client +} + +// User represents a JIRA user. +type User struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Password string `json:"-"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"` + AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` + DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` + Active bool `json:"active,omitempty" structs:"active,omitempty"` + TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"` + ApplicationKeys []string `json:"applicationKeys,omitempty" structs:"applicationKeys,omitempty"` +} + +// UserGroup represents the group list +type UserGroup struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` +} + +// Get gets user info from JIRA +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser +func (s *UserService) Get(username string) (*User, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + user := new(User) + resp, err := s.client.Do(req, user) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return user, resp, nil +} + +// Create creates an user in JIRA. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser +func (s *UserService) Create(user *User) (*User, *Response, error) { + apiEndpoint := "/rest/api/2/user" + req, err := s.client.NewRequest("POST", apiEndpoint, user) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return nil, resp, err + } + + responseUser := new(User) + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + e := fmt.Errorf("Could not read the returned data") + return nil, resp, NewJiraError(resp, e) + } + err = json.Unmarshal(data, responseUser) + if err != nil { + e := fmt.Errorf("Could not unmarshall the data into struct") + return nil, resp, NewJiraError(resp, e) + } + return responseUser, resp, nil +} + +// GetGroups returns the groups which the user belongs to +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups +func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + userGroups := new([]UserGroup) + resp, err := s.client.Do(req, userGroups) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return userGroups, resp, nil +} + +// Find searches for user info from JIRA: +// It can find users by email, username or name +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers +func (s *UserService) Find(property string) ([]User, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?username=%s", property) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + users := []User{} + resp, err := s.client.Do(req, &users) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return users, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/version.go b/vendor/github.com/andygrunwald/go-jira/version.go new file mode 100644 index 0000000..152005e --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/version.go @@ -0,0 +1,96 @@ +package jira + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +// VersionService handles Versions for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version +type VersionService struct { + client *Client +} + +// Version represents a single release version of a project +type Version struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Description string `json:"description,omitempty" structs:"name,omitempty"` + Archived bool `json:"archived,omitempty" structs:"archived,omitempty"` + Released bool `json:"released,omitempty" structs:"released,omitempty"` + ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` + UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` + ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number +} + +// Get gets version info from JIRA +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get +func (s *VersionService) Get(versionID int) (*Version, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + version := new(Version) + resp, err := s.client.Do(req, version) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return version, resp, nil +} + +// Create creates a version in JIRA. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post +func (s *VersionService) Create(version *Version) (*Version, *Response, error) { + apiEndpoint := "/rest/api/2/version" + req, err := s.client.NewRequest("POST", apiEndpoint, version) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return nil, resp, err + } + + responseVersion := new(Version) + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + e := fmt.Errorf("Could not read the returned data") + return nil, resp, NewJiraError(resp, e) + } + err = json.Unmarshal(data, responseVersion) + if err != nil { + e := fmt.Errorf("Could not unmarshall the data into struct") + return nil, resp, NewJiraError(resp, e) + } + return responseVersion, resp, nil +} + +// Update updates a version from a JSON representation. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put +func (s *VersionService) Update(version *Version) (*Version, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID) + req, err := s.client.NewRequest("PUT", apiEndpoint, version) + if err != nil { + return nil, nil, err + } + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + // This is just to follow the rest of the API's convention of returning a version. + // Returning the same pointer here is pointless, so we return a copy instead. + ret := *version + return &ret, resp, nil +} diff --git a/vendor/github.com/fatih/structs/.gitignore b/vendor/github.com/fatih/structs/.gitignore new file mode 100644 index 0000000..8365624 --- /dev/null +++ b/vendor/github.com/fatih/structs/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/vendor/github.com/fatih/structs/.travis.yml b/vendor/github.com/fatih/structs/.travis.yml new file mode 100644 index 0000000..cbf2ccc --- /dev/null +++ b/vendor/github.com/fatih/structs/.travis.yml @@ -0,0 +1,11 @@ +language: go +go: + - 1.7.x + - tip +sudo: false +before_install: +- go get github.com/axw/gocov/gocov +- go get github.com/mattn/goveralls +- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi +script: +- $HOME/gopath/bin/goveralls -service=travis-ci diff --git a/vendor/github.com/fatih/structs/LICENSE b/vendor/github.com/fatih/structs/LICENSE new file mode 100644 index 0000000..34504e4 --- /dev/null +++ b/vendor/github.com/fatih/structs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/fatih/structs/README.md b/vendor/github.com/fatih/structs/README.md new file mode 100644 index 0000000..44e0100 --- /dev/null +++ b/vendor/github.com/fatih/structs/README.md @@ -0,0 +1,163 @@ +# Structs [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/structs) [![Build Status](http://img.shields.io/travis/fatih/structs.svg?style=flat-square)](https://travis-ci.org/fatih/structs) [![Coverage Status](http://img.shields.io/coveralls/fatih/structs.svg?style=flat-square)](https://coveralls.io/r/fatih/structs) + +Structs contains various utilities to work with Go (Golang) structs. It was +initially used by me to convert a struct into a `map[string]interface{}`. With +time I've added other utilities for structs. It's basically a high level +package based on primitives from the reflect package. Feel free to add new +functions or improve the existing code. + +## Install + +```bash +go get github.com/fatih/structs +``` + +## Usage and Examples + +Just like the standard lib `strings`, `bytes` and co packages, `structs` has +many global functions to manipulate or organize your struct data. Lets define +and declare a struct: + +```go +type Server struct { + Name string `json:"name,omitempty"` + ID int + Enabled bool + users []string // not exported + http.Server // embedded +} + +server := &Server{ + Name: "gopher", + ID: 123456, + Enabled: true, +} +``` + +```go +// Convert a struct to a map[string]interface{} +// => {"Name":"gopher", "ID":123456, "Enabled":true} +m := structs.Map(server) + +// Convert the values of a struct to a []interface{} +// => ["gopher", 123456, true] +v := structs.Values(server) + +// Convert the names of a struct to a []string +// (see "Names methods" for more info about fields) +n := structs.Names(server) + +// Convert the values of a struct to a []*Field +// (see "Field methods" for more info about fields) +f := structs.Fields(server) + +// Return the struct name => "Server" +n := structs.Name(server) + +// Check if any field of a struct is initialized or not. +h := structs.HasZero(server) + +// Check if all fields of a struct is initialized or not. +z := structs.IsZero(server) + +// Check if server is a struct or a pointer to struct +i := structs.IsStruct(server) +``` + +### Struct methods + +The structs functions can be also used as independent methods by creating a new +`*structs.Struct`. This is handy if you want to have more control over the +structs (such as retrieving a single Field). + +```go +// Create a new struct type: +s := structs.New(server) + +m := s.Map() // Get a map[string]interface{} +v := s.Values() // Get a []interface{} +f := s.Fields() // Get a []*Field +n := s.Names() // Get a []string +f := s.Field(name) // Get a *Field based on the given field name +f, ok := s.FieldOk(name) // Get a *Field based on the given field name +n := s.Name() // Get the struct name +h := s.HasZero() // Check if any field is initialized +z := s.IsZero() // Check if all fields are initialized +``` + +### Field methods + +We can easily examine a single Field for more detail. Below you can see how we +get and interact with various field methods: + + +```go +s := structs.New(server) + +// Get the Field struct for the "Name" field +name := s.Field("Name") + +// Get the underlying value, value => "gopher" +value := name.Value().(string) + +// Set the field's value +name.Set("another gopher") + +// Get the field's kind, kind => "string" +name.Kind() + +// Check if the field is exported or not +if name.IsExported() { + fmt.Println("Name field is exported") +} + +// Check if the value is a zero value, such as "" for string, 0 for int +if !name.IsZero() { + fmt.Println("Name is initialized") +} + +// Check if the field is an anonymous (embedded) field +if !name.IsEmbedded() { + fmt.Println("Name is not an embedded field") +} + +// Get the Field's tag value for tag name "json", tag value => "name,omitempty" +tagValue := name.Tag("json") +``` + +Nested structs are supported too: + +```go +addrField := s.Field("Server").Field("Addr") + +// Get the value for addr +a := addrField.Value().(string) + +// Or get all fields +httpServer := s.Field("Server").Fields() +``` + +We can also get a slice of Fields from the Struct type to iterate over all +fields. This is handy if you wish to examine all fields: + +```go +s := structs.New(server) + +for _, f := range s.Fields() { + fmt.Printf("field name: %+v\n", f.Name()) + + if f.IsExported() { + fmt.Printf("value : %+v\n", f.Value()) + fmt.Printf("is zero : %+v\n", f.IsZero()) + } +} +``` + +## Credits + + * [Fatih Arslan](https://github.com/fatih) + * [Cihangir Savas](https://github.com/cihangir) + +## License + +The MIT License (MIT) - see LICENSE.md for more details diff --git a/vendor/github.com/fatih/structs/field.go b/vendor/github.com/fatih/structs/field.go new file mode 100644 index 0000000..e697832 --- /dev/null +++ b/vendor/github.com/fatih/structs/field.go @@ -0,0 +1,141 @@ +package structs + +import ( + "errors" + "fmt" + "reflect" +) + +var ( + errNotExported = errors.New("field is not exported") + errNotSettable = errors.New("field is not settable") +) + +// Field represents a single struct field that encapsulates high level +// functions around the field. +type Field struct { + value reflect.Value + field reflect.StructField + defaultTag string +} + +// Tag returns the value associated with key in the tag string. If there is no +// such key in the tag, Tag returns the empty string. +func (f *Field) Tag(key string) string { + return f.field.Tag.Get(key) +} + +// Value returns the underlying value of the field. It panics if the field +// is not exported. +func (f *Field) Value() interface{} { + return f.value.Interface() +} + +// IsEmbedded returns true if the given field is an anonymous field (embedded) +func (f *Field) IsEmbedded() bool { + return f.field.Anonymous +} + +// IsExported returns true if the given field is exported. +func (f *Field) IsExported() bool { + return f.field.PkgPath == "" +} + +// IsZero returns true if the given field is not initialized (has a zero value). +// It panics if the field is not exported. +func (f *Field) IsZero() bool { + zero := reflect.Zero(f.value.Type()).Interface() + current := f.Value() + + return reflect.DeepEqual(current, zero) +} + +// Name returns the name of the given field +func (f *Field) Name() string { + return f.field.Name +} + +// Kind returns the fields kind, such as "string", "map", "bool", etc .. +func (f *Field) Kind() reflect.Kind { + return f.value.Kind() +} + +// Set sets the field to given value v. It returns an error if the field is not +// settable (not addressable or not exported) or if the given value's type +// doesn't match the fields type. +func (f *Field) Set(val interface{}) error { + // we can't set unexported fields, so be sure this field is exported + if !f.IsExported() { + return errNotExported + } + + // do we get here? not sure... + if !f.value.CanSet() { + return errNotSettable + } + + given := reflect.ValueOf(val) + + if f.value.Kind() != given.Kind() { + return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind()) + } + + f.value.Set(given) + return nil +} + +// Zero sets the field to its zero value. It returns an error if the field is not +// settable (not addressable or not exported). +func (f *Field) Zero() error { + zero := reflect.Zero(f.value.Type()).Interface() + return f.Set(zero) +} + +// Fields returns a slice of Fields. This is particular handy to get the fields +// of a nested struct . A struct tag with the content of "-" ignores the +// checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field *http.Request `structs:"-"` +// +// It panics if field is not exported or if field's kind is not struct +func (f *Field) Fields() []*Field { + return getFields(f.value, f.defaultTag) +} + +// Field returns the field from a nested struct. It panics if the nested struct +// is not exported or if the field was not found. +func (f *Field) Field(name string) *Field { + field, ok := f.FieldOk(name) + if !ok { + panic("field not found") + } + + return field +} + +// FieldOk returns the field from a nested struct. The boolean returns whether +// the field was found (true) or not (false). +func (f *Field) FieldOk(name string) (*Field, bool) { + value := &f.value + // value must be settable so we need to make sure it holds the address of the + // variable and not a copy, so we can pass the pointer to strctVal instead of a + // copy (which is not assigned to any variable, hence not settable). + // see "https://blog.golang.org/laws-of-reflection#TOC_8." + if f.value.Kind() != reflect.Ptr { + a := f.value.Addr() + value = &a + } + v := strctVal(value.Interface()) + t := v.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: v.FieldByName(name), + }, true +} diff --git a/vendor/github.com/fatih/structs/structs.go b/vendor/github.com/fatih/structs/structs.go new file mode 100644 index 0000000..be3816a --- /dev/null +++ b/vendor/github.com/fatih/structs/structs.go @@ -0,0 +1,586 @@ +// Package structs contains various utilities functions to work with structs. +package structs + +import ( + "fmt" + + "reflect" +) + +var ( + // DefaultTagName is the default tag name for struct fields which provides + // a more granular to tweak certain structs. Lookup the necessary functions + // for more info. + DefaultTagName = "structs" // struct's field default tag name +) + +// Struct encapsulates a struct type to provide several high level functions +// around the struct. +type Struct struct { + raw interface{} + value reflect.Value + TagName string +} + +// New returns a new *Struct with the struct s. It panics if the s's kind is +// not struct. +func New(s interface{}) *Struct { + return &Struct{ + raw: s, + value: strctVal(s), + TagName: DefaultTagName, + } +} + +// Map converts the given struct to a map[string]interface{}, where the keys +// of the map are the field names and the values of the map the associated +// values of the fields. The default key string is the struct field name but +// can be changed in the struct field's tag value. The "structs" key in the +// struct's field tag value is the key name. Example: +// +// // Field appears in map as key "myName". +// Name string `structs:"myName"` +// +// A tag value with the content of "-" ignores that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A tag value with the content of "string" uses the stringer to get the value. Example: +// +// // The value will be output of Animal's String() func. +// // Map will panic if Animal does not implement String(). +// Field *Animal `structs:"field,string"` +// +// A tag value with the option of "flatten" used in a struct field is to flatten its fields +// in the output map. Example: +// +// // The FieldStruct's fields will be flattened into the output map. +// FieldStruct time.Time `structs:",flatten"` +// +// A tag value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field if +// the field value is empty. Example: +// +// // Field appears in map as key "myName", but the field is +// // skipped if empty. +// Field string `structs:"myName,omitempty"` +// +// // Field appears in map as key "Field" (the default), but +// // the field is skipped if empty. +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Map() map[string]interface{} { + out := make(map[string]interface{}) + s.FillMap(out) + return out +} + +// FillMap is the same as Map. Instead of returning the output, it fills the +// given map. +func (s *Struct) FillMap(out map[string]interface{}) { + if out == nil { + return + } + + fields := s.structFields() + + for _, field := range fields { + name := field.Name + val := s.value.FieldByName(name) + isSubStruct := false + var finalVal interface{} + + tagName, tagOpts := parseTag(field.Tag.Get(s.TagName)) + if tagName != "" { + name = tagName + } + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if !tagOpts.Has("omitnested") { + finalVal = s.nested(val) + + v := reflect.ValueOf(val.Interface()) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Map, reflect.Struct: + isSubStruct = true + } + } else { + finalVal = val.Interface() + } + + if tagOpts.Has("string") { + s, ok := val.Interface().(fmt.Stringer) + if ok { + out[name] = s.String() + } + continue + } + + if isSubStruct && (tagOpts.Has("flatten")) { + for k := range finalVal.(map[string]interface{}) { + out[k] = finalVal.(map[string]interface{})[k] + } + } else { + out[name] = finalVal + } + } +} + +// Values converts the given s struct's field values to a []interface{}. A +// struct tag with the content of "-" ignores the that particular field. +// Example: +// +// // Field is ignored by this package. +// Field int `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Fields is not processed further by this package. +// Field time.Time `structs:",omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// A tag value with the option of "omitempty" ignores that particular field and +// is not added to the values if the field value is empty. Example: +// +// // Field is skipped if empty +// Field string `structs:",omitempty"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. +func (s *Struct) Values() []interface{} { + fields := s.structFields() + + var t []interface{} + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + // if the value is a zero value and the field is marked as omitempty do + // not include + if tagOpts.Has("omitempty") { + zero := reflect.Zero(val.Type()).Interface() + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + continue + } + } + + if tagOpts.Has("string") { + s, ok := val.Interface().(fmt.Stringer) + if ok { + t = append(t, s.String()) + } + continue + } + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + // look out for embedded structs, and convert them to a + // []interface{} to be added to the final values slice + for _, embeddedVal := range Values(val.Interface()) { + t = append(t, embeddedVal) + } + } else { + t = append(t, val.Interface()) + } + } + + return t +} + +// Fields returns a slice of Fields. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Fields() []*Field { + return getFields(s.value, s.TagName) +} + +// Names returns a slice of field names. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// It panics if s's kind is not struct. +func (s *Struct) Names() []string { + fields := getFields(s.value, s.TagName) + + names := make([]string, len(fields)) + + for i, field := range fields { + names[i] = field.Name() + } + + return names +} + +func getFields(v reflect.Value, tagName string) []*Field { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + t := v.Type() + + var fields []*Field + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if tag := field.Tag.Get(tagName); tag == "-" { + continue + } + + f := &Field{ + field: field, + value: v.FieldByName(field.Name), + } + + fields = append(fields, f) + + } + + return fields +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entity. It panics if the field is not found. +func (s *Struct) Field(name string) *Field { + f, ok := s.FieldOk(name) + if !ok { + panic("field not found") + } + + return f +} + +// FieldOk returns a new Field struct that provides several high level functions +// around a single struct field entity. The boolean returns true if the field +// was found. +func (s *Struct) FieldOk(name string) (*Field, bool) { + t := s.value.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: s.value.FieldByName(name), + defaultTag: s.TagName, + }, true +} + +// IsZero returns true if all fields in a struct is a zero value (not +// initialized) A struct tag with the content of "-" ignores the checking of +// that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) IsZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := IsZero(val.Interface()) + if !ok { + return false + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if !reflect.DeepEqual(current, zero) { + return false + } + } + + return true +} + +// HasZero returns true if a field in a struct is not initialized (zero value). +// A struct tag with the content of "-" ignores the checking of that particular +// field. Example: +// +// // Field is ignored by this package. +// Field bool `structs:"-"` +// +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func (s *Struct) HasZero() bool { + fields := s.structFields() + + for _, field := range fields { + val := s.value.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(s.TagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + ok := HasZero(val.Interface()) + if ok { + return true + } + + continue + } + + // zero value of the given field, such as "" for string, 0 for int + zero := reflect.Zero(val.Type()).Interface() + + // current value of the given field + current := val.Interface() + + if reflect.DeepEqual(current, zero) { + return true + } + } + + return false +} + +// Name returns the structs's type name within its package. For more info refer +// to Name() function. +func (s *Struct) Name() string { + return s.value.Type().Name() +} + +// structFields returns the exported struct fields for a given s struct. This +// is a convenient helper method to avoid duplicate code in some of the +// functions. +func (s *Struct) structFields() []reflect.StructField { + t := s.value.Type() + + var f []reflect.StructField + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + // we can't access the value of unexported fields + if field.PkgPath != "" { + continue + } + + // don't check if it's omitted + if tag := field.Tag.Get(s.TagName); tag == "-" { + continue + } + + f = append(f, field) + } + + return f +} + +func strctVal(s interface{}) reflect.Value { + v := reflect.ValueOf(s) + + // if pointer get the underlying element≤ + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + panic("not struct") + } + + return v +} + +// Map converts the given struct to a map[string]interface{}. For more info +// refer to Struct types Map() method. It panics if s's kind is not struct. +func Map(s interface{}) map[string]interface{} { + return New(s).Map() +} + +// FillMap is the same as Map. Instead of returning the output, it fills the +// given map. +func FillMap(s interface{}, out map[string]interface{}) { + New(s).FillMap(out) +} + +// Values converts the given struct to a []interface{}. For more info refer to +// Struct types Values() method. It panics if s's kind is not struct. +func Values(s interface{}) []interface{} { + return New(s).Values() +} + +// Fields returns a slice of *Field. For more info refer to Struct types +// Fields() method. It panics if s's kind is not struct. +func Fields(s interface{}) []*Field { + return New(s).Fields() +} + +// Names returns a slice of field names. For more info refer to Struct types +// Names() method. It panics if s's kind is not struct. +func Names(s interface{}) []string { + return New(s).Names() +} + +// IsZero returns true if all fields is equal to a zero value. For more info +// refer to Struct types IsZero() method. It panics if s's kind is not struct. +func IsZero(s interface{}) bool { + return New(s).IsZero() +} + +// HasZero returns true if any field is equal to a zero value. For more info +// refer to Struct types HasZero() method. It panics if s's kind is not struct. +func HasZero(s interface{}) bool { + return New(s).HasZero() +} + +// IsStruct returns true if the given variable is a struct or a pointer to +// struct. +func IsStruct(s interface{}) bool { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // uninitialized zero value of a struct + if v.Kind() == reflect.Invalid { + return false + } + + return v.Kind() == reflect.Struct +} + +// Name returns the structs's type name within its package. It returns an +// empty string for unnamed types. It panics if s's kind is not struct. +func Name(s interface{}) string { + return New(s).Name() +} + +// nested retrieves recursively all types for the given value and returns the +// nested value. +func (s *Struct) nested(val reflect.Value) interface{} { + var finalVal interface{} + + v := reflect.ValueOf(val.Interface()) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + switch v.Kind() { + case reflect.Struct: + n := New(val.Interface()) + n.TagName = s.TagName + m := n.Map() + + // do not add the converted value if there are no exported fields, ie: + // time.Time + if len(m) == 0 { + finalVal = val.Interface() + } else { + finalVal = m + } + case reflect.Map: + // get the element type of the map + mapElem := val.Type() + switch val.Type().Kind() { + case reflect.Ptr, reflect.Array, reflect.Map, + reflect.Slice, reflect.Chan: + mapElem = val.Type().Elem() + if mapElem.Kind() == reflect.Ptr { + mapElem = mapElem.Elem() + } + } + + // only iterate over struct types, ie: map[string]StructType, + // map[string][]StructType, + if mapElem.Kind() == reflect.Struct || + (mapElem.Kind() == reflect.Slice && + mapElem.Elem().Kind() == reflect.Struct) { + m := make(map[string]interface{}, val.Len()) + for _, k := range val.MapKeys() { + m[k.String()] = s.nested(val.MapIndex(k)) + } + finalVal = m + break + } + + // TODO(arslan): should this be optional? + finalVal = val.Interface() + case reflect.Slice, reflect.Array: + if val.Type().Kind() == reflect.Interface { + finalVal = val.Interface() + break + } + + // TODO(arslan): should this be optional? + // do not iterate of non struct types, just pass the value. Ie: []int, + // []string, co... We only iterate further if it's a struct. + // i.e []foo or []*foo + if val.Type().Elem().Kind() != reflect.Struct && + !(val.Type().Elem().Kind() == reflect.Ptr && + val.Type().Elem().Elem().Kind() == reflect.Struct) { + finalVal = val.Interface() + break + } + + slices := make([]interface{}, val.Len(), val.Len()) + for x := 0; x < val.Len(); x++ { + slices[x] = s.nested(val.Index(x)) + } + finalVal = slices + default: + finalVal = val.Interface() + } + + return finalVal +} diff --git a/vendor/github.com/fatih/structs/tags.go b/vendor/github.com/fatih/structs/tags.go new file mode 100644 index 0000000..8859341 --- /dev/null +++ b/vendor/github.com/fatih/structs/tags.go @@ -0,0 +1,32 @@ +package structs + +import "strings" + +// tagOptions contains a slice of tag options +type tagOptions []string + +// Has returns true if the given optiton is available in tagOptions +func (t tagOptions) Has(opt string) bool { + for _, tagOpt := range t { + if tagOpt == opt { + return true + } + } + + return false +} + +// parseTag splits a struct field's tag into its name and a list of options +// which comes after a name. A tag is in the form of: "name,option1,option2". +// The name can be neglectected. +func parseTag(tag string) (string, tagOptions) { + // tag is one of followings: + // "" + // "name" + // "name,opt" + // "name,opt,opt2" + // ",opt" + + res := strings.Split(tag, ",") + return res[0], res[1:] +} diff --git a/vendor/github.com/trivago/tgo/LICENSE b/vendor/github.com/trivago/tgo/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/vendor/github.com/trivago/tgo/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/vendor/github.com/trivago/tgo/tcontainer/arrays.go b/vendor/github.com/trivago/tgo/tcontainer/arrays.go new file mode 100644 index 0000000..4aa4d03 --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/arrays.go @@ -0,0 +1,113 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 tcontainer + +import "sort" + +// Int64Slice is a typedef to allow sortable int64 slices +type Int64Slice []int64 + +func (s Int64Slice) Len() int { + return len(s) +} + +func (s Int64Slice) Less(i, j int) bool { + return s[i] < s[j] +} + +func (s Int64Slice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Sort is a shortcut for sort.Sort(s) +func (s Int64Slice) Sort() { + sort.Sort(s) +} + +// IsSorted is a shortcut for sort.IsSorted(s) +func (s Int64Slice) IsSorted() bool { + return sort.IsSorted(s) +} + +// Set sets all values in this slice to the given value +func (s Int64Slice) Set(v int64) { + for i := range s { + s[i] = v + } +} + +// Uint64Slice is a typedef to allow sortable uint64 slices +type Uint64Slice []uint64 + +func (s Uint64Slice) Len() int { + return len(s) +} + +func (s Uint64Slice) Less(i, j int) bool { + return s[i] < s[j] +} + +func (s Uint64Slice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Sort is a shortcut for sort.Sort(s) +func (s Uint64Slice) Sort() { + sort.Sort(s) +} + +// IsSorted is a shortcut for sort.IsSorted(s) +func (s Uint64Slice) IsSorted() bool { + return sort.IsSorted(s) +} + +// Set sets all values in this slice to the given value +func (s Uint64Slice) Set(v uint64) { + for i := range s { + s[i] = v + } +} + +// Float32Slice is a typedef to allow sortable float32 slices +type Float32Slice []float32 + +func (s Float32Slice) Len() int { + return len(s) +} + +func (s Float32Slice) Less(i, j int) bool { + return s[i] < s[j] +} + +func (s Float32Slice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Sort is a shortcut for sort.Sort(s) +func (s Float32Slice) Sort() { + sort.Sort(s) +} + +// IsSorted is a shortcut for sort.IsSorted(s) +func (s Float32Slice) IsSorted() bool { + return sort.IsSorted(s) +} + +// Set sets all values in this slice to the given value +func (s Float32Slice) Set(v float32) { + for i := range s { + s[i] = v + } +} diff --git a/vendor/github.com/trivago/tgo/tcontainer/bytepool.go b/vendor/github.com/trivago/tgo/tcontainer/bytepool.go new file mode 100644 index 0000000..b39409e --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/bytepool.go @@ -0,0 +1,157 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 tcontainer + +import ( + "reflect" + "runtime" + "sync/atomic" + "unsafe" +) + +const ( + tiny = 64 + small = 512 + medium = 1024 + large = 1024 * 10 + huge = 1024 * 100 + + tinyCount = 16384 // 1 MB + smallCount = 2048 // 1 MB + mediumCount = 1024 // 1 MB + largeCount = 102 // ~1 MB + hugeCount = 10 // ~1 MB +) + +type byteSlab struct { + buffer []byte + bufferSize uintptr + stride uintptr + basePtr *uintptr + nextPtr *uintptr +} + +// BytePool is a fragmentation friendly way to allocated byte slices. +type BytePool struct { + tinySlab byteSlab + smallSlab byteSlab + mediumSlab byteSlab + largeSlab byteSlab + hugeSlab byteSlab +} + +func newByteSlab(size, count int) byteSlab { + bufferSize := count * size + buffer := make([]byte, bufferSize) + basePtr := (*reflect.SliceHeader)(unsafe.Pointer(&buffer)).Data + nextPtr := basePtr + uintptr(bufferSize) + + return byteSlab{ + buffer: buffer, + bufferSize: uintptr(bufferSize), + stride: uintptr(size), + basePtr: &basePtr, + nextPtr: &nextPtr, + } +} + +func (slab *byteSlab) getSlice(size int) (chunk []byte) { + chunkHeader := (*reflect.SliceHeader)(unsafe.Pointer(&chunk)) + chunkHeader.Len = size + chunkHeader.Cap = int(slab.stride) + + for { + // WARNING: The following two lines are order sensitive + basePtr := atomic.LoadUintptr(slab.basePtr) + nextPtr := atomic.AddUintptr(slab.nextPtr, -slab.stride) + lastPtr := basePtr + slab.bufferSize + + switch { + case nextPtr < basePtr || nextPtr >= lastPtr: + // out of range either means alloc while realloc or race between + // base and next during realloc. In the latter case we lose a chunk. + runtime.Gosched() + + case nextPtr == basePtr: + // Last item: realloc + slab.buffer = make([]byte, slab.bufferSize) + dataPtr := (*reflect.SliceHeader)(unsafe.Pointer(&slab.buffer)).Data + + // WARNING: The following two lines are order sensitive + atomic.StoreUintptr(slab.nextPtr, dataPtr+slab.bufferSize) + atomic.StoreUintptr(slab.basePtr, dataPtr) + fallthrough + + default: + chunkHeader.Data = nextPtr + return + } + } +} + +// NewBytePool creates a new BytePool with each slab using 1 MB of storage. +// The pool contains 5 slabs of different sizes: 64B, 512B, 1KB, 10KB and 100KB. +// Allocations above 100KB will be allocated directly. +func NewBytePool() BytePool { + return BytePool{ + tinySlab: newByteSlab(tiny, tinyCount), + smallSlab: newByteSlab(small, smallCount), + mediumSlab: newByteSlab(medium, mediumCount), + largeSlab: newByteSlab(large, largeCount), + hugeSlab: newByteSlab(huge, hugeCount), + } +} + +// NewBytePoolWithSize creates a new BytePool with each slab size using n MB of +// storage. See NewBytePool() for slab size details. +func NewBytePoolWithSize(n int) BytePool { + if n <= 0 { + n = 1 + } + return BytePool{ + tinySlab: newByteSlab(tiny, tinyCount*n), + smallSlab: newByteSlab(small, smallCount*n), + mediumSlab: newByteSlab(medium, mediumCount*n), + largeSlab: newByteSlab(large, largeCount*n), + hugeSlab: newByteSlab(huge, hugeCount*n), + } +} + +// Get returns a slice allocated to a normalized size. +// Sizes are organized in evenly sized buckets so that fragmentation is kept low. +func (b *BytePool) Get(size int) []byte { + switch { + case size == 0: + return []byte{} + + case size <= tiny: + return b.tinySlab.getSlice(size) + + case size <= small: + return b.smallSlab.getSlice(size) + + case size <= medium: + return b.mediumSlab.getSlice(size) + + case size <= large: + return b.largeSlab.getSlice(size) + + case size <= huge: + return b.hugeSlab.getSlice(size) + + default: + return make([]byte, size) + } +} diff --git a/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go b/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go new file mode 100644 index 0000000..37fb9de --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go @@ -0,0 +1,464 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 tcontainer + +import ( + "fmt" + "github.com/trivago/tgo/treflect" + "reflect" + "strconv" + "strings" + "time" +) + +// MarshalMap is a wrapper type to attach converter methods to maps normally +// returned by marshalling methods, i.e. key/value parsers. +// All methods that do a conversion will return an error if the value stored +// behind key is not of the expected type or if the key is not existing in the +// map. +type MarshalMap map[string]interface{} + +const ( + // MarshalMapSeparator defines the rune used for path separation + MarshalMapSeparator = '/' + // MarshalMapArrayBegin defines the rune starting array index notation + MarshalMapArrayBegin = '[' + // MarshalMapArrayEnd defines the rune ending array index notation + MarshalMapArrayEnd = ']' +) + +// NewMarshalMap creates a new marshal map (string -> interface{}) +func NewMarshalMap() MarshalMap { + return make(map[string]interface{}) +} + +// TryConvertToMarshalMap converts collections to MarshalMap if possible. +// This is a deep conversion, i.e. each element in the collection will be +// traversed. You can pass a formatKey function that will be applied to all +// string keys that are detected. +func TryConvertToMarshalMap(value interface{}, formatKey func(string) string) interface{} { + valueMeta := reflect.ValueOf(value) + switch valueMeta.Kind() { + default: + return value + + case reflect.Array, reflect.Slice: + arrayLen := valueMeta.Len() + converted := make([]interface{}, arrayLen) + for i := 0; i < arrayLen; i++ { + converted[i] = TryConvertToMarshalMap(valueMeta.Index(i).Interface(), formatKey) + } + return converted + + case reflect.Map: + converted := NewMarshalMap() + keys := valueMeta.MapKeys() + + for _, keyMeta := range keys { + strKey, isString := keyMeta.Interface().(string) + if !isString { + continue + } + if formatKey != nil { + strKey = formatKey(strKey) + } + val := valueMeta.MapIndex(keyMeta).Interface() + converted[strKey] = TryConvertToMarshalMap(val, formatKey) + } + return converted // ### return, converted MarshalMap ### + } +} + +// ConvertToMarshalMap tries to convert a compatible map type to a marshal map. +// Compatible types are map[interface{}]interface{}, map[string]interface{} and of +// course MarshalMap. The same rules as for ConvertValueToMarshalMap apply. +func ConvertToMarshalMap(value interface{}, formatKey func(string) string) (MarshalMap, error) { + converted := TryConvertToMarshalMap(value, formatKey) + if result, isMap := converted.(MarshalMap); isMap { + return result, nil + } + return nil, fmt.Errorf("Root value cannot be converted to MarshalMap") +} + +// Bool returns a value at key that is expected to be a boolean +func (mmap MarshalMap) Bool(key string) (bool, error) { + val, exists := mmap.Value(key) + if !exists { + return false, fmt.Errorf(`"%s" is not set`, key) + } + + boolValue, isBool := val.(bool) + if !isBool { + return false, fmt.Errorf(`"%s" is expected to be a boolean`, key) + } + return boolValue, nil +} + +// Uint returns a value at key that is expected to be an uint64 or compatible +// integer value. +func (mmap MarshalMap) Uint(key string) (uint64, error) { + val, exists := mmap.Value(key) + if !exists { + return 0, fmt.Errorf(`"%s" is not set`, key) + } + + if intVal, isNumber := treflect.Uint64(val); isNumber { + return intVal, nil + } + + return 0, fmt.Errorf(`"%s" is expected to be an unsigned number type`, key) +} + +// Int returns a value at key that is expected to be an int64 or compatible +// integer value. +func (mmap MarshalMap) Int(key string) (int64, error) { + val, exists := mmap.Value(key) + if !exists { + return 0, fmt.Errorf(`"%s" is not set`, key) + } + + if intVal, isNumber := treflect.Int64(val); isNumber { + return intVal, nil + } + + return 0, fmt.Errorf(`"%s" is expected to be a signed number type`, key) +} + +// Float returns a value at key that is expected to be a float64 or compatible +// float value. +func (mmap MarshalMap) Float(key string) (float64, error) { + val, exists := mmap.Value(key) + if !exists { + return 0, fmt.Errorf(`"%s" is not set`, key) + } + + if floatVal, isNumber := treflect.Float64(val); isNumber { + return floatVal, nil + } + + return 0, fmt.Errorf(`"%s" is expected to be a signed number type`, key) +} + +// Duration returns a value at key that is expected to be a string +func (mmap MarshalMap) Duration(key string) (time.Duration, error) { + val, exists := mmap.Value(key) + if !exists { + return time.Duration(0), fmt.Errorf(`"%s" is not set`, key) + } + + switch val.(type) { + case time.Duration: + return val.(time.Duration), nil + case string: + return time.ParseDuration(val.(string)) + } + + return time.Duration(0), fmt.Errorf(`"%s" is expected to be a duration or string`, key) +} + +// String returns a value at key that is expected to be a string +func (mmap MarshalMap) String(key string) (string, error) { + val, exists := mmap.Value(key) + if !exists { + return "", fmt.Errorf(`"%s" is not set`, key) + } + + strValue, isString := val.(string) + if !isString { + return "", fmt.Errorf(`"%s" is expected to be a string`, key) + } + return strValue, nil +} + +// Array returns a value at key that is expected to be a []interface{} +func (mmap MarshalMap) Array(key string) ([]interface{}, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + arrayValue, isArray := val.([]interface{}) + if !isArray { + return nil, fmt.Errorf(`"%s" is expected to be an array`, key) + } + return arrayValue, nil +} + +// Map returns a value at key that is expected to be a +// map[interface{}]interface{}. +func (mmap MarshalMap) Map(key string) (map[interface{}]interface{}, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + mapValue, isMap := val.(map[interface{}]interface{}) + if !isMap { + return nil, fmt.Errorf(`"%s" is expected to be a map`, key) + } + return mapValue, nil +} + +func castToStringArray(key string, value interface{}) ([]string, error) { + switch value.(type) { + case string: + return []string{value.(string)}, nil + + case []interface{}: + arrayVal := value.([]interface{}) + stringArray := make([]string, 0, len(arrayVal)) + + for _, val := range arrayVal { + strValue, isString := val.(string) + if !isString { + return nil, fmt.Errorf(`"%s" does not contain string keys`, key) + } + stringArray = append(stringArray, strValue) + } + return stringArray, nil + + case []string: + return value.([]string), nil + + default: + return nil, fmt.Errorf(`"%s" is not a valid string array type`, key) + } +} + +// StringArray returns a value at key that is expected to be a []string +// This function supports conversion (by copy) from +// * []interface{} +func (mmap MarshalMap) StringArray(key string) ([]string, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + return castToStringArray(key, val) +} + +func castToInt64Array(key string, value interface{}) ([]int64, error) { + switch value.(type) { + case int: + return []int64{value.(int64)}, nil + + case []interface{}: + arrayVal := value.([]interface{}) + intArray := make([]int64, 0, len(arrayVal)) + + for _, val := range arrayVal { + intValue, isInt := val.(int64) + if !isInt { + return nil, fmt.Errorf(`"%s" does not contain int keys`, key) + } + intArray = append(intArray, intValue) + } + return intArray, nil + + case []int64: + return value.([]int64), nil + + default: + return nil, fmt.Errorf(`"%s" is not a valid string array type`, key) + } +} + +// IntArray returns a value at key that is expected to be a []int64 +// This function supports conversion (by copy) from +// * []interface{} +func (mmap MarshalMap) Int64Array(key string) ([]int64, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + return castToInt64Array(key, val) +} + +// StringMap returns a value at key that is expected to be a map[string]string. +// This function supports conversion (by copy) from +// * map[interface{}]interface{} +// * map[string]interface{} +func (mmap MarshalMap) StringMap(key string) (map[string]string, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + switch val.(type) { + case map[string]string: + return val.(map[string]string), nil + + default: + valueMeta := reflect.ValueOf(val) + if valueMeta.Kind() != reflect.Map { + return nil, fmt.Errorf(`"%s" is expected to be a map[string]string but is %T`, key, val) + } + + result := make(map[string]string) + for _, keyMeta := range valueMeta.MapKeys() { + strKey, isString := keyMeta.Interface().(string) + if !isString { + return nil, fmt.Errorf(`"%s" is expected to be a map[string]string. Key is not a string`, key) + } + + value := valueMeta.MapIndex(keyMeta) + strValue, isString := value.Interface().(string) + if !isString { + return nil, fmt.Errorf(`"%s" is expected to be a map[string]string. Value is not a string`, key) + } + + result[strKey] = strValue + } + + return result, nil + } +} + +// StringArrayMap returns a value at key that is expected to be a +// map[string][]string. This function supports conversion (by copy) from +// * map[interface{}][]interface{} +// * map[interface{}]interface{} +// * map[string]interface{} +func (mmap MarshalMap) StringArrayMap(key string) (map[string][]string, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + switch val.(type) { + case map[string][]string: + return val.(map[string][]string), nil + + default: + valueMeta := reflect.ValueOf(val) + if valueMeta.Kind() != reflect.Map { + return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string but is %T`, key, val) + } + + result := make(map[string][]string) + for _, keyMeta := range valueMeta.MapKeys() { + strKey, isString := keyMeta.Interface().(string) + if !isString { + return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string. Key is not a string`, key) + } + + value := valueMeta.MapIndex(keyMeta) + arrayValue, err := castToStringArray(strKey, value.Interface()) + if err != nil { + return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string. Value is not a []string`, key) + } + + result[strKey] = arrayValue + } + + return result, nil + } +} + +// MarshalMap returns a value at key that is expected to be another MarshalMap +// This function supports conversion (by copy) from +// * map[interface{}]interface{} +func (mmap MarshalMap) MarshalMap(key string) (MarshalMap, error) { + val, exists := mmap.Value(key) + if !exists { + return nil, fmt.Errorf(`"%s" is not set`, key) + } + + return ConvertToMarshalMap(val, nil) +} + +// Value returns a value from a given value path. +// Fields can be accessed by their name. Nested fields can be accessed by using +// "/" as a separator. Arrays can be addressed using the standard array +// notation "[]". +// Examples: +// "key" -> mmap["key"] single value +// "key1/key2" -> mmap["key1"]["key2"] nested map +// "key1[0]" -> mmap["key1"][0] nested array +// "key1[0]key2" -> mmap["key1"][0]["key2"] nested array, nested map +func (mmap MarshalMap) Value(key string) (interface{}, bool) { + return mmap.resolvePath(key, mmap) +} + +func (mmap MarshalMap) resolvePathKey(key string) (int, int) { + keyEnd := len(key) + nextKeyStart := keyEnd + pathIdx := strings.IndexRune(key, MarshalMapSeparator) + arrayIdx := strings.IndexRune(key, MarshalMapArrayBegin) + + if pathIdx > -1 && pathIdx < keyEnd { + keyEnd = pathIdx + nextKeyStart = pathIdx + 1 // don't include slash + } + if arrayIdx > -1 && arrayIdx < keyEnd { + keyEnd = arrayIdx + nextKeyStart = arrayIdx // include bracket because of multidimensional arrays + } + + // a -> key: "a", remain: "" -- value + // a/b/c -> key: "a", remain: "b/c" -- nested map + // a[1]b/c -> key: "a", remain: "[1]b/c" -- nested array + + return keyEnd, nextKeyStart +} + +func (mmap MarshalMap) resolvePath(key string, value interface{}) (interface{}, bool) { + if len(key) == 0 { + return value, true // ### return, found requested value ### + } + + valueMeta := reflect.ValueOf(value) + switch valueMeta.Kind() { + case reflect.Array, reflect.Slice: + startIdx := strings.IndexRune(key, MarshalMapArrayBegin) // Must be first char, otherwise malformed + endIdx := strings.IndexRune(key, MarshalMapArrayEnd) // Must be > startIdx, otherwise malformed + + if startIdx == -1 || endIdx == -1 { + return nil, false + } + + if startIdx == 0 && endIdx > startIdx { + index, err := strconv.Atoi(key[startIdx+1 : endIdx]) + + // [1] -> index: "1", remain: "" -- value + // [1]a/b -> index: "1", remain: "a/b" -- nested map + // [1][2] -> index: "1", remain: "[2]" -- nested array + + if err == nil && index < valueMeta.Len() { + item := valueMeta.Index(index).Interface() + key := key[endIdx+1:] + return mmap.resolvePath(key, item) // ### return, nested array ### + } + } + + case reflect.Map: + keyMeta := reflect.ValueOf(key) + if storedValue := valueMeta.MapIndex(keyMeta); storedValue.IsValid() { + return storedValue.Interface(), true + } + + keyEnd, nextKeyStart := mmap.resolvePathKey(key) + pathKey := key[:keyEnd] + keyMeta = reflect.ValueOf(pathKey) + + if storedValue := valueMeta.MapIndex(keyMeta); storedValue.IsValid() { + remain := key[nextKeyStart:] + return mmap.resolvePath(remain, storedValue.Interface()) // ### return, nested map ### + } + } + + return nil, false +} diff --git a/vendor/github.com/trivago/tgo/tcontainer/trie.go b/vendor/github.com/trivago/tgo/tcontainer/trie.go new file mode 100644 index 0000000..288748b --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/trie.go @@ -0,0 +1,227 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 tcontainer + +// TrieNode represents a single node inside a trie. +// Each node can contain a payload which can be retrieved after a successfull +// match. In addition to that PathLen will contain the length of the match. +type TrieNode struct { + suffix []byte + children []*TrieNode + longestPath int + PathLen int + Payload interface{} +} + +// NewTrie creates a new root TrieNode +func NewTrie(data []byte, payload interface{}) *TrieNode { + return &TrieNode{ + suffix: data, + children: []*TrieNode{}, + longestPath: len(data), + PathLen: len(data), + Payload: payload, + } +} + +func (node *TrieNode) addNewChild(data []byte, payload interface{}, pathLen int) { + if node.longestPath < pathLen { + node.longestPath = pathLen + } + + idx := len(node.children) + node.children = append(node.children, nil) + + for idx > 0 { + nextIdx := idx - 1 + if node.children[nextIdx].longestPath > pathLen { + break + } + node.children[idx] = node.children[nextIdx] + idx = nextIdx + } + + node.children[idx] = &TrieNode{ + suffix: data, + children: []*TrieNode{}, + longestPath: pathLen, + PathLen: pathLen, + Payload: payload, + } +} + +func (node *TrieNode) replace(oldChild *TrieNode, newChild *TrieNode) { + for i, child := range node.children { + if child == oldChild { + node.children[i] = newChild + return // ### return, replaced ### + } + } +} + +// ForEach applies a function to each node in the tree including and below the +// passed node. +func (node *TrieNode) ForEach(callback func(*TrieNode)) { + callback(node) + for _, child := range node.children { + child.ForEach(callback) + } +} + +// Add adds a new data path to the trie. +// The TrieNode returned is the (new) root node so you should always reassign +// the root with the return value of Add. +func (node *TrieNode) Add(data []byte, payload interface{}) *TrieNode { + return node.addPath(data, payload, len(data), nil) +} + +func (node *TrieNode) addPath(data []byte, payload interface{}, pathLen int, parent *TrieNode) *TrieNode { + dataLen := len(data) + suffixLen := len(node.suffix) + testLen := suffixLen + if dataLen < suffixLen { + testLen = dataLen + } + + var splitIdx int + for splitIdx = 0; splitIdx < testLen; splitIdx++ { + if data[splitIdx] != node.suffix[splitIdx] { + break // ### break, split found ### + } + } + + if splitIdx == suffixLen { + // Continue down or stop here (full suffix match) + + if splitIdx == dataLen { + node.Payload = payload // may overwrite + return node // ### return, path already stored ### + } + + data = data[splitIdx:] + if suffixLen > 0 { + for _, child := range node.children { + if child.suffix[0] == data[0] { + child.addPath(data, payload, pathLen, node) + return node // ### return, continue on path ### + } + } + } + + node.addNewChild(data, payload, pathLen) + return node // ### return, new leaf ### + } + + if splitIdx == dataLen { + // Make current node a subpath of new data node (full data match) + // This case implies that dataLen < suffixLen as splitIdx == suffixLen + // did not match. + + node.suffix = node.suffix[splitIdx:] + + newParent := NewTrie(data, payload) + newParent.PathLen = pathLen + newParent.longestPath = node.longestPath + newParent.children = []*TrieNode{node} + + if parent != nil { + parent.replace(node, newParent) + } + return newParent // ### return, rotation ### + } + + // New parent required with both nodes as children (partial match) + + node.suffix = node.suffix[splitIdx:] + + newParent := NewTrie(data[:splitIdx], nil) + newParent.PathLen = 0 + newParent.longestPath = node.longestPath + newParent.children = []*TrieNode{node} + newParent.addNewChild(data[splitIdx:], payload, pathLen) + + if parent != nil { + parent.replace(node, newParent) + } + return newParent // ### return, new parent ### +} + +// Match compares the trie to the given data stream. +// Match returns true if data can be completely matched to the trie. +func (node *TrieNode) Match(data []byte) *TrieNode { + dataLen := len(data) + suffixLen := len(node.suffix) + if dataLen < suffixLen { + return nil // ### return, cannot be fully matched ### + } + + for i := 0; i < suffixLen; i++ { + if data[i] != node.suffix[i] { + return nil // ### return, no match ### + } + } + + if dataLen == suffixLen { + if node.PathLen > 0 { + return node // ### return, full match ### + } + return nil // ### return, invalid match ### + } + + data = data[suffixLen:] + numChildren := len(node.children) + for i := 0; i < numChildren; i++ { + matchedNode := node.children[i].Match(data) + if matchedNode != nil { + return matchedNode // ### return, match found ### + } + } + + return nil // ### return, no valid path ### +} + +// MatchStart compares the trie to the beginning of the given data stream. +// MatchStart returns true if the beginning of data can be matched to the trie. +func (node *TrieNode) MatchStart(data []byte) *TrieNode { + dataLen := len(data) + suffixLen := len(node.suffix) + if dataLen < suffixLen { + return nil // ### return, cannot be fully matched ### + } + + for i := 0; i < suffixLen; i++ { + if data[i] != node.suffix[i] { + return nil // ### return, no match ### + } + } + + // Match longest path first + + data = data[suffixLen:] + numChildren := len(node.children) + for i := 0; i < numChildren; i++ { + matchedNode := node.children[i].MatchStart(data) + if matchedNode != nil { + return matchedNode // ### return, match found ### + } + } + + // May be only a part of data but we have a valid match + + if node.PathLen > 0 { + return node // ### return, full match ### + } + return nil // ### return, no valid path ### +} diff --git a/vendor/github.com/trivago/tgo/treflect/reflection.go b/vendor/github.com/trivago/tgo/treflect/reflection.go new file mode 100644 index 0000000..b28a5ae --- /dev/null +++ b/vendor/github.com/trivago/tgo/treflect/reflection.go @@ -0,0 +1,373 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 treflect + +import ( + "fmt" + "reflect" + "unsafe" +) + +// GetMissingMethods checks if a given object implements all methods of a +// given interface. It returns the interface coverage [0..1] as well as an array +// of error messages. If the interface is correctly implemented the coverage is +// 1 and the error message array is empty. +func GetMissingMethods(objType reflect.Type, ifaceType reflect.Type) (float32, []string) { + missing := []string{} + if objType.Implements(ifaceType) { + return 1.0, missing + } + + methodCount := ifaceType.NumMethod() + for mIdx := 0; mIdx < methodCount; mIdx++ { + ifaceMethod := ifaceType.Method(mIdx) + objMethod, exists := objType.MethodByName(ifaceMethod.Name) + signatureMismatch := false + + switch { + case !exists: + missing = append(missing, fmt.Sprintf("Missing: \"%s\" %v", ifaceMethod.Name, ifaceMethod.Type)) + continue // ### continue, error found ### + + case ifaceMethod.Type.NumOut() != objMethod.Type.NumOut(): + signatureMismatch = true + + case ifaceMethod.Type.NumIn()+1 != objMethod.Type.NumIn(): + signatureMismatch = true + + default: + for oIdx := 0; !signatureMismatch && oIdx < ifaceMethod.Type.NumOut(); oIdx++ { + signatureMismatch = ifaceMethod.Type.Out(oIdx) != objMethod.Type.Out(oIdx) + } + for iIdx := 0; !signatureMismatch && iIdx < ifaceMethod.Type.NumIn(); iIdx++ { + signatureMismatch = ifaceMethod.Type.In(iIdx) != objMethod.Type.In(iIdx+1) + } + } + + if signatureMismatch { + missing = append(missing, fmt.Sprintf("Invalid: \"%s\" %v is not %v", ifaceMethod.Name, objMethod.Type, ifaceMethod.Type)) + } + } + + return float32(methodCount-len(missing)) / float32(methodCount), missing +} + +// Int64 converts any signed number type to an int64. +// The second parameter is returned as false if a non-number type was given. +func Int64(v interface{}) (int64, bool) { + + switch reflect.TypeOf(v).Kind() { + case reflect.Int: + return int64(v.(int)), true + case reflect.Int8: + return int64(v.(int8)), true + case reflect.Int16: + return int64(v.(int16)), true + case reflect.Int32: + return int64(v.(int32)), true + case reflect.Int64: + return v.(int64), true + case reflect.Float32: + return int64(v.(float32)), true + case reflect.Float64: + return int64(v.(float64)), true + } + + fmt.Printf("%t\n%#v\n%#v\n", v, v, reflect.TypeOf(v).Kind()) + + return 0, false +} + +// Uint64 converts any unsigned number type to an uint64. +// The second parameter is returned as false if a non-number type was given. +func Uint64(v interface{}) (uint64, bool) { + + switch reflect.TypeOf(v).Kind() { + case reflect.Uint: + return uint64(v.(uint)), true + case reflect.Uint8: + return uint64(v.(uint8)), true + case reflect.Uint16: + return uint64(v.(uint16)), true + case reflect.Uint32: + return uint64(v.(uint32)), true + case reflect.Uint64: + return v.(uint64), true + } + + return 0, false +} + +// Float32 converts any number type to an float32. +// The second parameter is returned as false if a non-number type was given. +func Float32(v interface{}) (float32, bool) { + + switch reflect.TypeOf(v).Kind() { + case reflect.Int: + return float32(v.(int)), true + case reflect.Uint: + return float32(v.(uint)), true + case reflect.Int8: + return float32(v.(int8)), true + case reflect.Uint8: + return float32(v.(uint8)), true + case reflect.Int16: + return float32(v.(int16)), true + case reflect.Uint16: + return float32(v.(uint16)), true + case reflect.Int32: + return float32(v.(int32)), true + case reflect.Uint32: + return float32(v.(uint32)), true + case reflect.Int64: + return float32(v.(int64)), true + case reflect.Uint64: + return float32(v.(uint64)), true + case reflect.Float32: + return v.(float32), true + case reflect.Float64: + return float32(v.(float64)), true + } + + return 0, false +} + +// Float64 converts any number type to an float64. +// The second parameter is returned as false if a non-number type was given. +func Float64(v interface{}) (float64, bool) { + + switch reflect.TypeOf(v).Kind() { + case reflect.Int: + return float64(v.(int)), true + case reflect.Uint: + return float64(v.(uint)), true + case reflect.Int8: + return float64(v.(int8)), true + case reflect.Uint8: + return float64(v.(uint8)), true + case reflect.Int16: + return float64(v.(int16)), true + case reflect.Uint16: + return float64(v.(uint16)), true + case reflect.Int32: + return float64(v.(int32)), true + case reflect.Uint32: + return float64(v.(uint32)), true + case reflect.Int64: + return float64(v.(int64)), true + case reflect.Uint64: + return float64(v.(uint64)), true + case reflect.Float32: + return float64(v.(float32)), true + case reflect.Float64: + return v.(float64), true + } + + return 0, false +} + +// RemovePtrFromType will return the type of t and strips away any pointer(s) +// in front of the actual type. +func RemovePtrFromType(t interface{}) reflect.Type { + var v reflect.Type + if rt, isType := t.(reflect.Type); isType { + v = rt + } else { + v = reflect.TypeOf(t) + } + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + return v +} + +// RemovePtrFromValue will return the value of t and strips away any pointer(s) +// in front of the actual type. +func RemovePtrFromValue(t interface{}) reflect.Value { + var v reflect.Value + if rv, isValue := t.(reflect.Value); isValue { + v = rv + } else { + v = reflect.ValueOf(t) + } + for v.Type().Kind() == reflect.Ptr { + v = v.Elem() + } + return v +} + +// UnsafeCopy will copy data from src to dst while ignoring type information. +// Both types need to be of the same size and dst and src have to be pointers. +// UnsafeCopy will panic if these requirements are not met. +func UnsafeCopy(dst, src interface{}) { + dstValue := reflect.ValueOf(dst) + srcValue := reflect.ValueOf(src) + UnsafeCopyValue(dstValue, srcValue) +} + +// UnsafeCopyValue will copy data from src to dst while ignoring type +// information. Both types need to be of the same size or this function will +// panic. Also both types must support dereferencing via reflect.Elem() +func UnsafeCopyValue(dstValue reflect.Value, srcValue reflect.Value) { + dstType := dstValue.Elem().Type() + srcType := srcValue.Type() + + var srcPtr uintptr + if srcValue.Kind() != reflect.Ptr { + // If we don't get a pointer to our source data we need to forcefully + // retrieve it by accessing the interface pointer. This is ok as we + // only read from it. + iface := srcValue.Interface() + srcPtr = reflect.ValueOf(&iface).Elem().InterfaceData()[1] // Pointer to data + } else { + srcType = srcValue.Elem().Type() + srcPtr = srcValue.Pointer() + } + + if dstType.Size() != srcType.Size() { + panic("Type size mismatch between " + dstType.String() + " and " + srcType.String()) + } + + dstAsSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: dstValue.Pointer(), + Len: int(dstType.Size()), + Cap: int(dstType.Size()), + })) + + srcAsSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: srcPtr, + Len: int(srcType.Size()), + Cap: int(srcType.Size()), + })) + + copy(dstAsSlice, srcAsSlice) +} + +// SetMemberByName sets member name of the given pointer-to-struct to the data +// passed to this function. The member may be private, too. +func SetMemberByName(ptrToStruct interface{}, name string, data interface{}) { + structVal := reflect.Indirect(reflect.ValueOf(ptrToStruct)) + member := structVal.FieldByName(name) + + SetValue(member, data) +} + +// SetMemberByIndex sets member idx of the given pointer-to-struct to the data +// passed to this function. The member may be private, too. +func SetMemberByIndex(ptrToStruct interface{}, idx int, data interface{}) { + structVal := reflect.Indirect(reflect.ValueOf(ptrToStruct)) + member := structVal.Field(idx) + + SetValue(member, data) +} + +// SetValue sets an addressable value to the data passed to this function. +// In contrast to golangs reflect package this will also work with private +// variables. Please note that this function may not support all types, yet. +func SetValue(member reflect.Value, data interface{}) { + if member.CanSet() { + member.Set(reflect.ValueOf(data).Convert(member.Type())) + return // ### return, easy way ### + } + + if !member.CanAddr() { + panic("SetValue requires addressable member type") + } + + ptrToMember := unsafe.Pointer(member.UnsafeAddr()) + dataValue := reflect.ValueOf(data) + + switch member.Kind() { + case reflect.Bool: + *(*bool)(ptrToMember) = dataValue.Bool() + + case reflect.Uint: + *(*uint)(ptrToMember) = uint(dataValue.Uint()) + + case reflect.Uint8: + *(*uint8)(ptrToMember) = uint8(dataValue.Uint()) + + case reflect.Uint16: + *(*uint16)(ptrToMember) = uint16(dataValue.Uint()) + + case reflect.Uint32: + *(*uint32)(ptrToMember) = uint32(dataValue.Uint()) + + case reflect.Uint64: + *(*uint64)(ptrToMember) = dataValue.Uint() + + case reflect.Int: + *(*int)(ptrToMember) = int(dataValue.Int()) + + case reflect.Int8: + *(*int8)(ptrToMember) = int8(dataValue.Int()) + + case reflect.Int16: + *(*int16)(ptrToMember) = int16(dataValue.Int()) + + case reflect.Int32: + *(*int32)(ptrToMember) = int32(dataValue.Int()) + + case reflect.Int64: + *(*int64)(ptrToMember) = dataValue.Int() + + case reflect.Float32: + *(*float32)(ptrToMember) = float32(dataValue.Float()) + + case reflect.Float64: + *(*float64)(ptrToMember) = dataValue.Float() + + case reflect.Complex64: + *(*complex64)(ptrToMember) = complex64(dataValue.Complex()) + + case reflect.Complex128: + *(*complex128)(ptrToMember) = dataValue.Complex() + + case reflect.String: + *(*string)(ptrToMember) = dataValue.String() + + case reflect.Map, reflect.Chan: + // Exploit the fact that "map" is actually "*runtime.hmap" and force + // overwrite that pointer in the passed struct. + // Same foes for "chan" which is actually "*runtime.hchan". + + // Note: Assigning a map or channel to another variable does NOT copy + // the contents so copying the pointer follows go's standard behavior. + dataAsPtr := unsafe.Pointer(dataValue.Pointer()) + *(**uintptr)(ptrToMember) = (*uintptr)(dataAsPtr) + + case reflect.Interface: + // Interfaces are basically two pointers, see runtime.iface. + // We want to modify exactly that data, which is returned by + // the InterfaceData() method. + + if dataValue.Kind() != reflect.Interface { + // A type reference was passed. In order to overwrite the memory + // Representation of an interface we need to generate it first. + // Reflect does not allow us to do that unless we use the + // InterfaceData method which exposes the internal representation + // of an interface. + interfaceData := reflect.ValueOf(&data).Elem().InterfaceData() + dataValue = reflect.ValueOf(interfaceData) + } + fallthrough + + default: + // Complex types are assigned memcpy style. + // Note: This should not break the garbage collector although we cannot + // be 100% sure on this. + UnsafeCopyValue(member.Addr(), dataValue) + } +} diff --git a/vendor/github.com/trivago/tgo/treflect/typeregistry.go b/vendor/github.com/trivago/tgo/treflect/typeregistry.go new file mode 100644 index 0000000..3299f27 --- /dev/null +++ b/vendor/github.com/trivago/tgo/treflect/typeregistry.go @@ -0,0 +1,97 @@ +// Copyright 2015-2016 trivago GmbH +// +// 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 treflect + +import ( + "fmt" + "reflect" + "strings" +) + +// TypeRegistry is a name to type registry used to create objects by name. +type TypeRegistry struct { + namedType map[string]reflect.Type +} + +// NewTypeRegistry creates a new TypeRegistry. Note that there is a global type +// registry available in the main tgo package (tgo.TypeRegistry). +func NewTypeRegistry() TypeRegistry { + return TypeRegistry{ + namedType: make(map[string]reflect.Type), + } +} + +// Register a plugin to the TypeRegistry by passing an uninitialized object. +func (registry TypeRegistry) Register(typeInstance interface{}) { + registry.RegisterWithDepth(typeInstance, 1) +} + +// RegisterWithDepth to register a plugin to the TypeRegistry by passing an uninitialized object. +func (registry TypeRegistry) RegisterWithDepth(typeInstance interface{}, depth int) { + structType := reflect.TypeOf(typeInstance) + packageName := structType.PkgPath() + typeName := structType.Name() + + pathTokens := strings.Split(packageName, "/") + maxDepth := 3 + if len(pathTokens) < maxDepth { + maxDepth = len(pathTokens) + } + + for n := depth; n <= maxDepth; n++ { + shortTypeName := strings.Join(pathTokens[len(pathTokens)-n:], ".") + "." + typeName + registry.namedType[shortTypeName] = structType + } +} + +// New creates an uninitialized object by class name. +// The class name has to be "package.class" or "package/subpackage.class". +// The gollum package is omitted from the package path. +func (registry TypeRegistry) New(typeName string) (interface{}, error) { + structType, exists := registry.namedType[typeName] + if exists { + return reflect.New(structType).Interface(), nil + } + return nil, fmt.Errorf("Unknown class: %s", typeName) +} + +// GetTypeOf returns only the type asscociated with the given name. +// If the name is not registered, nil is returned. +// The type returned will be a pointer type. +func (registry TypeRegistry) GetTypeOf(typeName string) reflect.Type { + if structType, exists := registry.namedType[typeName]; exists { + return reflect.PtrTo(structType) + } + return nil +} + +// IsTypeRegistered returns true if a type is registered to this registry. +// Note that GetTypeOf can do the same thing by checking for nil but also +// returns the type, so in many cases you will want to call this function. +func (registry TypeRegistry) IsTypeRegistered(typeName string) bool { + _, exists := registry.namedType[typeName] + return exists +} + +// GetRegistered returns the names of all registered types for a given package +func (registry TypeRegistry) GetRegistered(packageName string) []string { + var result []string + for key := range registry.namedType { + if strings.HasPrefix(key, packageName) { + result = append(result, key) + } + } + return result +}