diff --git a/.chglog/config.yml b/.chglog/config.yml index abfce168..13bd904c 100755 --- a/.chglog/config.yml +++ b/.chglog/config.yml @@ -18,10 +18,21 @@ options: perf: Performance Improvements refactor: Code Refactoring header: - pattern: "^(\\w*)\\:\\s(.*)$" + pattern: "^(\\w*)[\\[(\\w*)\\]]\\:\\s(.*)$" pattern_maps: - Type + - JiraIssueId - Subject notes: keywords: - BREAKING CHANGE + jira: + info: + username: u + token: p + url: https://jira.com + issue: + type_maps: + Task: fix + Story: feat + description_pattern: "(.*)" diff --git a/Gopkg.lock b/Gopkg.lock index d07ec387..df90c648 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,96 +2,178 @@ [[projects]] + digest = "1:6b933ddaf9c30844438b732960021771842cd3a4a57d521e62de6e844bd55370" + name = "github.com/andygrunwald/go-jira" + packages = ["."] + pruneopts = "NUT" + revision = "7966e7f5ed3e453362a4deac213c0e6cf7563f4c" + version = "v1.10.0" + +[[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "NUT" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] + digest = "1:7c4b5e23bb01e5945b0df8a5f181efa2d90abd19e368be7c7bd55a5737db0739" name = "github.com/fatih/color" packages = ["."] + pruneopts = "NUT" revision = "507f6050b8568533fb3f5504de8e5205fa62a114" version = "v1.6.0" [[projects]] + digest = "1:aa3ed0a71c4e66e4ae6486bf97a3f4cab28edc78df2e50c5ad01dc7d91604b88" + name = "github.com/fatih/structs" + packages = ["."] + pruneopts = "NUT" + revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047" + version = "v1.1.0" + +[[projects]] + digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690" + name = "github.com/google/go-querystring" + packages = ["query"] + pruneopts = "NUT" + revision = "44c6ddd0a2342c386950e880b658017258da92fc" + version = "v1.0.0" + +[[projects]] + digest = "1:cf327083982a19eae01f70be6663a935a02bac3a2dc79efbc19668c8378bfbb3" name = "github.com/imdario/mergo" packages = ["."] + pruneopts = "NUT" revision = "163f41321a19dd09362d4c63cc2489db2015f1f4" version = "0.3.2" [[projects]] + digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "NUT" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "NUT" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] branch = "master" + digest = "1:063d55b87e200bced5e2be658cc70acafb4c5bbc4afa04d4b82f66298b73d089" name = "github.com/mgutz/ansi" packages = ["."] + pruneopts = "NUT" revision = "9520e82c474b0a04dd04f8a40959027271bab992" [[projects]] + digest = "1:14715f705ff5dfe0ffd6571d7d201dd8e921030f8070321a79380d8ca4ec1a24" + name = "github.com/pkg/errors" + packages = ["."] + pruneopts = "NUT" + revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" + version = "v0.8.1" + +[[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "NUT" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + digest = "1:a852b1ad03ca063d2c57866d9f94dcb1cb2e111415c5902ce0586fc2d207221b" name = "github.com/stretchr/testify" packages = ["assert"] + pruneopts = "NUT" revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" [[projects]] + digest = "1:5e59b9b6d0c389f8b691d5caa2211dea04ac95c5285c95b3b8e1187042df3f26" + name = "github.com/trivago/tgo" + packages = [ + "tcontainer", + "treflect", + ] + pruneopts = "NUT" + revision = "efdb64f40efe6e7cd3f50415710e7af6a7c316ad" + version = "v1.0.7" + +[[projects]] + digest = "1:bc6fc3b838718f65a91d63108426e69bbddf643c04f20e2bbbb0ea907d84b984" name = "github.com/tsuyoshiwada/go-gitcmd" packages = ["."] + pruneopts = "NUT" revision = "5f1f5f9475df211f8b48620d704284d7375229c3" version = "0.0.1" [[projects]] + digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58" name = "github.com/urfave/cli" packages = ["."] + pruneopts = "NUT" revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" version = "v1.20.0" [[projects]] branch = "master" + digest = "1:600bbc566231723dd6d13ccb6269a1c97235041281d796e03102ae4feab4d5d8" name = "golang.org/x/sys" packages = ["unix"] + pruneopts = "NUT" revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd" [[projects]] + digest = "1:8f2948d3a161e4ef9591a3a465ef8fb94e12f458115ed00f9aed6c2c0a1d3025" name = "gopkg.in/AlecAivazis/survey.v1" packages = [ ".", "core", - "terminal" + "terminal", ] + pruneopts = "NUT" revision = "9f89d9dd66613216993dc300f9f3dbae9c3c9bda" version = "v1.4.2" [[projects]] + digest = "1:e21bd8ae4721734a5b9a79c7345aa528f0cc2e0cc18265691c37566f1c445a19" name = "gopkg.in/kyokomi/emoji.v1" packages = ["."] + pruneopts = "NUT" revision = "7e06b236c489543f53868841f188a294e3383eab" version = "v1.5" [[projects]] branch = "v2" + digest = "1:13e704c08924325be00f96e47e7efe0bfddf0913cdfc237423c83f9b183ff590" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "NUT" revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "da532fb70b3049dffe6ae9f73b3f0a742452639096a325ac5d2400be6ad0559e" + input-imports = [ + "github.com/andygrunwald/go-jira", + "github.com/fatih/color", + "github.com/imdario/mergo", + "github.com/mattn/go-colorable", + "github.com/stretchr/testify/assert", + "github.com/tsuyoshiwada/go-gitcmd", + "github.com/urfave/cli", + "gopkg.in/AlecAivazis/survey.v1", + "gopkg.in/kyokomi/emoji.v1", + "gopkg.in/yaml.v2", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 2c69f0bc..98ff5637 100644 --- a/README.md +++ b/README.md @@ -480,6 +480,86 @@ See godoc [RenderData][doc-render-data] for available variables. > :memo: Even with styles that are not yet supported, it is possible to make ordinary CHANGELOG. +## Jira Integration + + +Jira is a popular project management tool. When a project uses Jira to track feature development and bug fixes, +it may also want to generate change log based information stored in Jira. With embedding a Jira story id in git +commit header, the git-chglog tool may automatically fetch data of the story from Jira, those data then can be +used to render the template. + +Take the following steps to add Jira integration: + +#### 1. Change the header parse pattern to recognize Jira issue id in the configure file. + +__Where Jira issue is identical Jira story.__ + +The following is a sample pattern: + + ```yaml + header: + pattern: "^(\\w*)[\\[(\\w*)\\]]\\:\\s(.*)$" + pattern_maps: + - Type + - JiraIssueId + - Subject + ``` + +This sample pattern can match both forms of commit headers: + +* `feat: new feature of something` +* `[JIRA-ID]: something` + +#### 2. Add Jira configuration to the configure file. + +The following is a sample: + + ```yaml + jira: + info: + username: u + token: p + url: https://jira.com + issue: + type_maps: + Task: fix + Story: feat + description_pattern: "(.*)" + ``` + +Here you need to define Jira URL, access username and token (password). If you don't want to +write your Jira access credential in configure file, you may define them with environment variables: +`JIRA_URL`, `JIRA_USERNAME` and `JIRA_TOKEN`. + +You also needs to define a issue type map. In above sample, Jira issue type `Task` will be +mapped to `fix` and `Story` will be mapped to `feat`. + +As a Jira story's description could be very long, you might not want to include the entire +description into change log. In that case, you may define `description_pattern` like above, +so that only content embraced with ` ... ` will be included. + +#### 3. Update the template to show Jira data. + +In the template, if a commit contains a Jira issue id, then you may show Jira data. For example: + +```markdown +{{ range .CommitGroups -}} +### {{ .Title }} +{{ range .Commits -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ if .JiraIssue }} {{ .JiraIssue.Description }} +{{ end }} +{{ end }} +{{ end -}} +``` + +Within a `Commit`, the following Jira data can be used in template: + +* `.JiraIssue.Summary` - Summary of the Jira story +* `.JiraIssue.Description` - Description of the Jira story +* `.JiraIssue.Type` - Original type of the Jira story, and `.Type` will be mapped type. +* `.JiraIssue.Labels` - A list of strings, each is a Jira label. + ## FAQ diff --git a/chglog.go b/chglog.go index a45f201f..d8b707dc 100644 --- a/chglog.go +++ b/chglog.go @@ -16,23 +16,28 @@ import ( // Options is an option used to process commits type Options struct { - Processor Processor - NextTag string // Treat unreleased commits as specified tags (EXPERIMENTAL) - TagFilterPattern string // Filter tag by regexp - CommitFilters map[string][]string // Filter by using `Commit` properties and values. Filtering is not done by specifying an empty value - CommitSortBy string // Property name to use for sorting `Commit` (e.g. `Scope`) - CommitGroupBy string // Property name of `Commit` to be grouped into `CommitGroup` (e.g. `Type`) - CommitGroupSortBy string // Property name to use for sorting `CommitGroup` (e.g. `Title`) - CommitGroupTitleMaps map[string]string // Map for `CommitGroup` title conversion - HeaderPattern string // A regular expression to use for parsing the commit header - HeaderPatternMaps []string // A rule for mapping the result of `HeaderPattern` to the property of `Commit` - IssuePrefix []string // Prefix used for issues (e.g. `#`, `gh-`) - RefActions []string // Word list of `Ref.Action` - MergePattern string // A regular expression to use for parsing the merge commit - MergePatternMaps []string // Similar to `HeaderPatternMaps` - RevertPattern string // A regular expression to use for parsing the revert commit - RevertPatternMaps []string // Similar to `HeaderPatternMaps` - NoteKeywords []string // Keyword list to find `Note`. A semicolon is a separator, like `:` (e.g. `BREAKING CHANGE`) + Processor Processor + NextTag string // Treat unreleased commits as specified tags (EXPERIMENTAL) + TagFilterPattern string // Filter tag by regexp + CommitFilters map[string][]string // Filter by using `Commit` properties and values. Filtering is not done by specifying an empty value + CommitSortBy string // Property name to use for sorting `Commit` (e.g. `Scope`) + CommitGroupBy string // Property name of `Commit` to be grouped into `CommitGroup` (e.g. `Type`) + CommitGroupSortBy string // Property name to use for sorting `CommitGroup` (e.g. `Title`) + CommitGroupTitleMaps map[string]string // Map for `CommitGroup` title conversion + HeaderPattern string // A regular expression to use for parsing the commit header + HeaderPatternMaps []string // A rule for mapping the result of `HeaderPattern` to the property of `Commit` + IssuePrefix []string // Prefix used for issues (e.g. `#`, `gh-`) + RefActions []string // Word list of `Ref.Action` + MergePattern string // A regular expression to use for parsing the merge commit + MergePatternMaps []string // Similar to `HeaderPatternMaps` + RevertPattern string // A regular expression to use for parsing the revert commit + RevertPatternMaps []string // Similar to `HeaderPatternMaps` + NoteKeywords []string // Keyword list to find `Note`. A semicolon is a separator, like `:` (e.g. `BREAKING CHANGE`) + JiraUsername string + JiraToken string + JiraUrl string + JiraTypeMaps map[string]string + JiraIssueDescriptionPattern string } // Info is metadata related to CHANGELOG @@ -100,6 +105,8 @@ func NewGenerator(config *Config) *Generator { Bin: config.Bin, }) + jiraClient := NewJiraClient(config) + if config.Options.Processor != nil { config.Options.Processor.Bootstrap(config) } @@ -111,7 +118,7 @@ func NewGenerator(config *Config) *Generator { config: config, tagReader: newTagReader(client, config.Options.TagFilterPattern), tagSelector: newTagSelector(), - commitParser: newCommitParser(client, config), + commitParser: newCommitParser(client, jiraClient, config), commitExtractor: newCommitExtractor(config.Options), } } diff --git a/cmd/git-chglog/config.go b/cmd/git-chglog/config.go index 6f13b723..e20fc93d 100644 --- a/cmd/git-chglog/config.go +++ b/cmd/git-chglog/config.go @@ -48,6 +48,24 @@ type NoteOptions struct { Keywords []string `yaml:"keywords"` } +type JiraClientInfoOptions struct { + Username string `yaml:"username"` + Token string `yaml:"token"` + URL string `yaml:"url"` +} + +// JiraIssueOptions ... +type JiraIssueOptions struct { + TypeMaps map[string]string `yaml:"type_maps"` + DescriptionPattern string `yaml:"description_pattern"` +} + +// JiraOptions ... +type JiraOptions struct { + ClintInfo JiraClientInfoOptions `yaml:"info"` + Issue JiraIssueOptions `yaml:"issue"` +} + // Options ... type Options struct { Commits CommitOptions `yaml:"commits"` @@ -58,6 +76,7 @@ type Options struct { Merges PatternOptions `yaml:"merges"` Reverts PatternOptions `yaml:"reverts"` Notes NoteOptions `yaml:"notes"` + Jira JiraOptions `yaml:"jira"` } // Config ... @@ -243,6 +262,13 @@ func (config *Config) normalizeStyleOfBitbucket() { config.Options = opts } +func or_value(str1 string, str2 string) string { + if str1 != "" { + return str1 + } + return str2 +} + // Convert ... func (config *Config) Convert(ctx *CLIContext) *chglog.Config { info := config.Info @@ -251,28 +277,33 @@ func (config *Config) Convert(ctx *CLIContext) *chglog.Config { return &chglog.Config{ Bin: config.Bin, WorkingDir: ctx.WorkingDir, - Template: config.Template, + Template: or_value(ctx.Template, config.Template), Info: &chglog.Info{ Title: info.Title, - RepositoryURL: info.RepositoryURL, + RepositoryURL: or_value(ctx.RepositoryUrl, info.RepositoryURL), }, Options: &chglog.Options{ - NextTag: ctx.NextTag, - TagFilterPattern: ctx.TagFilterPattern, - CommitFilters: opts.Commits.Filters, - CommitSortBy: opts.Commits.SortBy, - CommitGroupBy: opts.CommitGroups.GroupBy, - CommitGroupSortBy: opts.CommitGroups.SortBy, - CommitGroupTitleMaps: opts.CommitGroups.TitleMaps, - HeaderPattern: opts.Header.Pattern, - HeaderPatternMaps: opts.Header.PatternMaps, - IssuePrefix: opts.Issues.Prefix, - RefActions: opts.Refs.Actions, - MergePattern: opts.Merges.Pattern, - MergePatternMaps: opts.Merges.PatternMaps, - RevertPattern: opts.Reverts.Pattern, - RevertPatternMaps: opts.Reverts.PatternMaps, - NoteKeywords: opts.Notes.Keywords, + NextTag: ctx.NextTag, + TagFilterPattern: ctx.TagFilterPattern, + CommitFilters: opts.Commits.Filters, + CommitSortBy: opts.Commits.SortBy, + CommitGroupBy: opts.CommitGroups.GroupBy, + CommitGroupSortBy: opts.CommitGroups.SortBy, + CommitGroupTitleMaps: opts.CommitGroups.TitleMaps, + HeaderPattern: opts.Header.Pattern, + HeaderPatternMaps: opts.Header.PatternMaps, + IssuePrefix: opts.Issues.Prefix, + RefActions: opts.Refs.Actions, + MergePattern: opts.Merges.Pattern, + MergePatternMaps: opts.Merges.PatternMaps, + RevertPattern: opts.Reverts.Pattern, + RevertPatternMaps: opts.Reverts.PatternMaps, + NoteKeywords: opts.Notes.Keywords, + JiraUsername: or_value(ctx.JiraUsername, opts.Jira.ClintInfo.Username), + JiraToken: or_value(ctx.JiraToken, opts.Jira.ClintInfo.Token), + JiraUrl: or_value(ctx.JiraUrl, opts.Jira.ClintInfo.URL), + JiraTypeMaps: opts.Jira.Issue.TypeMaps, + JiraIssueDescriptionPattern: opts.Jira.Issue.DescriptionPattern, }, } } diff --git a/cmd/git-chglog/context.go b/cmd/git-chglog/context.go index 79b497e3..bcb110f9 100644 --- a/cmd/git-chglog/context.go +++ b/cmd/git-chglog/context.go @@ -10,6 +10,8 @@ type CLIContext struct { Stdout io.Writer Stderr io.Writer ConfigPath string + Template string + RepositoryUrl string OutputPath string Silent bool NoColor bool @@ -17,6 +19,9 @@ type CLIContext struct { Query string NextTag string TagFilterPattern string + JiraUsername string + JiraToken string + JiraUrl string } // InitContext ... diff --git a/cmd/git-chglog/main.go b/cmd/git-chglog/main.go index d4582ba8..efc6a474 100644 --- a/cmd/git-chglog/main.go +++ b/cmd/git-chglog/main.go @@ -83,6 +83,18 @@ func CreateApp(actionFunc cli.ActionFunc) *cli.App { Value: ".chglog/config.yml", }, + // template + cli.StringFlag{ + Name: "template, t", + Usage: "specifies a template file to pick up. If not specified, use the one in config", + }, + + // repository url + cli.StringFlag{ + Name: "repository-url", + Usage: "specifies git repo URL. If not specified, use 'repository_url' in config", + }, + // output cli.StringFlag{ Name: "output, o", @@ -120,6 +132,24 @@ func CreateApp(actionFunc cli.ActionFunc) *cli.App { Usage: "Regular expression of tag filter. Is specified, only matched tags will be picked", }, + cli.StringFlag{ + Name: "jira-url", + Usage: "Jira URL", + EnvVar: "JIRA_URL", + }, + + cli.StringFlag{ + Name: "jira-username", + Usage: "Jira username", + EnvVar: "JIRA_USERNAME", + }, + + cli.StringFlag{ + Name: "jira-token", + Usage: "Jira token", + EnvVar: "JIRA_TOKEN", + }, + // help & version cli.HelpFlag, cli.VersionFlag, @@ -162,17 +192,20 @@ func AppAction(c *cli.Context) error { // chglog chglogCLI := NewCLI( &CLIContext{ - WorkingDir: wd, - Stdout: colorable.NewColorableStdout(), - Stderr: colorable.NewColorableStderr(), - ConfigPath: c.String("config"), - OutputPath: c.String("output"), - Silent: c.Bool("silent"), - NoColor: c.Bool("no-color"), - NoEmoji: c.Bool("no-emoji"), - Query: c.Args().First(), - NextTag: c.String("next-tag"), + WorkingDir: wd, + Stdout: colorable.NewColorableStdout(), + Stderr: colorable.NewColorableStderr(), + ConfigPath: c.String("config"), + OutputPath: c.String("output"), + Silent: c.Bool("silent"), + NoColor: c.Bool("no-color"), + NoEmoji: c.Bool("no-emoji"), + Query: c.Args().First(), + NextTag: c.String("next-tag"), TagFilterPattern: c.String("tag-filter-pattern"), + JiraUsername: c.String("jira-username"), + JiraToken: c.String("jira-token"), + JiraUrl: c.String("jira-url"), }, fs, NewConfigLoader(), diff --git a/commit_parser.go b/commit_parser.go index 5054b227..a4942499 100644 --- a/commit_parser.go +++ b/commit_parser.go @@ -1,12 +1,14 @@ package chglog import ( + "fmt" + "os" "regexp" "strconv" "strings" "time" - gitcmd "github.com/tsuyoshiwada/go-gitcmd" + "github.com/tsuyoshiwada/go-gitcmd" ) var ( @@ -47,18 +49,20 @@ func joinAndQuoteMeta(list []string, sep string) string { } type commitParser struct { - client gitcmd.Client - config *Config - reHeader *regexp.Regexp - reMerge *regexp.Regexp - reRevert *regexp.Regexp - reRef *regexp.Regexp - reIssue *regexp.Regexp - reNotes *regexp.Regexp - reMention *regexp.Regexp + client gitcmd.Client + jiraClient JiraClient + config *Config + reHeader *regexp.Regexp + reMerge *regexp.Regexp + reRevert *regexp.Regexp + reRef *regexp.Regexp + reIssue *regexp.Regexp + reNotes *regexp.Regexp + reMention *regexp.Regexp + reJiraIssueDescription *regexp.Regexp } -func newCommitParser(client gitcmd.Client, config *Config) *commitParser { +func newCommitParser(client gitcmd.Client, jiraClient JiraClient, config *Config) *commitParser { opts := config.Options joinedRefActions := joinAndQuoteMeta(opts.RefActions, "|") @@ -66,15 +70,17 @@ func newCommitParser(client gitcmd.Client, config *Config) *commitParser { joinedNoteKeywords := joinAndQuoteMeta(opts.NoteKeywords, "|") return &commitParser{ - client: client, - config: config, - reHeader: regexp.MustCompile(opts.HeaderPattern), - reMerge: regexp.MustCompile(opts.MergePattern), - reRevert: regexp.MustCompile(opts.RevertPattern), - reRef: regexp.MustCompile("(?i)(" + joinedRefActions + ")\\s?([\\w/\\.\\-]+)?(?:" + joinedIssuePrefix + ")(\\d+)"), - reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"), - reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"), - reMention: regexp.MustCompile("@([\\w-]+)"), + client: client, + jiraClient: jiraClient, + config: config, + reHeader: regexp.MustCompile(opts.HeaderPattern), + reMerge: regexp.MustCompile(opts.MergePattern), + reRevert: regexp.MustCompile(opts.RevertPattern), + reRef: regexp.MustCompile("(?i)(" + joinedRefActions + ")\\s?([\\w/\\.\\-]+)?(?:" + joinedIssuePrefix + ")(\\d+)"), + reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"), + reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"), + reMention: regexp.MustCompile("@([\\w-]+)"), + reJiraIssueDescription: regexp.MustCompile(opts.JiraIssueDescriptionPattern), } } @@ -206,6 +212,11 @@ func (p *commitParser) processHeader(commit *Commit, input string) { // refs & mentions commit.Refs = p.parseRefs(input) commit.Mentions = p.parseMentions(input) + + // Jira + if commit.JiraIssueId != "" { + p.processJiraIssue(commit, commit.JiraIssueId) + } } func (p *commitParser) processBody(commit *Commit, input string) { @@ -344,6 +355,28 @@ func (p *commitParser) uniqMentions(mentions []string) []string { return arr } +func (p *commitParser) processJiraIssue(commit *Commit, issueId string) { + issue, err := p.jiraClient.GetJiraIssue(commit.JiraIssueId) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse Jira story %s: %s\n", issueId, err) + return + } + commit.Type = p.config.Options.JiraTypeMaps[issue.Fields.Type.Name] + commit.JiraIssue = &JiraIssue{ + Type: issue.Fields.Type.Name, + Summary: issue.Fields.Summary, + Description: issue.Fields.Description, + Labels: issue.Fields.Labels, + } + + if p.config.Options.JiraIssueDescriptionPattern != "" { + res := p.reJiraIssueDescription.FindStringSubmatch(commit.JiraIssue.Description) + if len(res) > 1 { + commit.JiraIssue.Description = res[1] + } + } +} + var ( fenceTypes = []string{ "```", diff --git a/commit_parser_test.go b/commit_parser_test.go index 2df79531..1a70c674 100644 --- a/commit_parser_test.go +++ b/commit_parser_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/stretchr/testify/assert" + agjira "github.com/andygrunwald/go-jira" ) func TestCommitParserParse(t *testing.T) { @@ -27,7 +28,7 @@ func TestCommitParserParse(t *testing.T) { }, } - parser := newCommitParser(mock, &Config{ + parser := newCommitParser(mock, nil, &Config{ Options: &Options{ CommitFilters: map[string][]string{ "Type": []string{ @@ -308,3 +309,102 @@ Closes username/repository#456`, "```", "```"), }, }, commits) } + + +type mockJiraClient struct { +} + +func (jira mockJiraClient) GetJiraIssue(id string) (*agjira.Issue, error) { + return &agjira.Issue { + ID: id, + Fields: &agjira.IssueFields{ + Expand: "", + Type: agjira.IssueType{Name: "Story"}, + Project: agjira.Project{}, + Resolution: nil, + Priority: nil, + Resolutiondate: agjira.Time{}, + Created: agjira.Time{}, + Duedate: agjira.Date{}, + Watches: nil, + Assignee: nil, + Updated: agjira.Time{}, + Description: fmt.Sprintf("description of %s", id), + Summary: fmt.Sprintf("summary of %s", id), + Creator: nil, + Reporter: nil, + Components: nil, + Status: nil, + Progress: nil, + AggregateProgress: nil, + TimeTracking: nil, + TimeSpent: 0, + TimeEstimate: 0, + TimeOriginalEstimate: 0, + Worklog: nil, + IssueLinks: nil, + Comments: nil, + FixVersions: nil, + AffectsVersions: nil, + Labels: []string{"GA"}, + Subtasks: nil, + Attachments: nil, + Epic: nil, + Sprint: nil, + Parent: nil, + AggregateTimeOriginalEstimate: 0, + AggregateTimeSpent: 0, + AggregateTimeEstimate: 0, + Unknowns: nil, + }, + }, nil +} + +func TestCommitParserParseWithJira(t *testing.T) { + assert := assert.New(t) + assert.True(true) + + mock := &mockClient{ + ReturnExec: func(subcmd string, args ...string) (string, error) { + if subcmd != "log" { + return "", errors.New("") + } + + bytes, _ := ioutil.ReadFile(filepath.Join("testdata", "gitlog_jira.txt")) + + return string(bytes), nil + }, + } + + parser := newCommitParser(mock, mockJiraClient{}, &Config{ + Options: &Options{ + CommitFilters: map[string][]string{ + "Type": []string{ + "feat", + "fix", + "perf", + "refactor", + }, + }, + HeaderPattern: "^(?:(\\w*)|(?:\\[(.*)\\])?)\\:\\s(.*)$", + HeaderPatternMaps: []string{ + "Type", + "JiraIssueId", + "Subject", + }, + JiraTypeMaps: map[string]string { + "Story": "feat", + }, + }, + }) + + commits, err := parser.Parse("HEAD") + assert.Nil(err) + commit := commits[0] + assert.Equal(commit.JiraIssueId, "JIRA-1111") + assert.Equal(commit.JiraIssue.Type, "Story") + assert.Equal(commit.JiraIssue.Summary, "summary of JIRA-1111") + assert.Equal(commit.JiraIssue.Description, "description of JIRA-1111") + assert.Equal(commit.JiraIssue.Labels, []string{"GA"}) + assert.Equal(commit.Type, "feat") +} diff --git a/fields.go b/fields.go index 46cf8cc3..123da6c7 100644 --- a/fields.go +++ b/fields.go @@ -52,21 +52,31 @@ type NoteGroup struct { Notes []*Note } +// JiraIssue +type JiraIssue struct { + Type string + Summary string + Description string + Labels []string +} + // Commit data type Commit struct { - Hash *Hash - Author *Author - Committer *Committer - Merge *Merge // If it is not a merge commit, `nil` is assigned - Revert *Revert // If it is not a revert commit, `nil` is assigned - Refs []*Ref - Notes []*Note - Mentions []string // Name of the user included in the commit header or body - Header string // (e.g. `feat(core): Add new feature`) - Type string // (e.g. `feat`) - Scope string // (e.g. `core`) - Subject string // (e.g. `Add new feature`) - Body string + Hash *Hash + Author *Author + Committer *Committer + Merge *Merge // If it is not a merge commit, `nil` is assigned + Revert *Revert // If it is not a revert commit, `nil` is assigned + Refs []*Ref + Notes []*Note + Mentions []string // Name of the user included in the commit header or body + JiraIssue *JiraIssue // If no issue id found in header, `nil` is assigned + Header string // (e.g. `feat(core)[RNWY-310]: Add new feature`) + Type string // (e.g. `feat`) + Scope string // (e.g. `core`) + Subject string // (e.g. `Add new feature`) + JiraIssueId string // (e.g. `RNWY-310`) + Body string } // CommitGroup is a collection of commits grouped according to the `CommitGroupBy` option diff --git a/jira.go b/jira.go new file mode 100644 index 00000000..78afbdd7 --- /dev/null +++ b/jira.go @@ -0,0 +1,36 @@ +package chglog + +import ( + agjira "github.com/andygrunwald/go-jira" +) + +type JiraClient interface { + GetJiraIssue(id string) (*agjira.Issue, error) +} + +type jiraClient struct { + username string + token string + url string +} + +func NewJiraClient(config *Config) JiraClient { + return jiraClient{ + username: config.Options.JiraUsername, + token: config.Options.JiraToken, + url: config.Options.JiraUrl, + } +} + +func (jira jiraClient) GetJiraIssue(id string) (*agjira.Issue, error) { + tp := agjira.BasicAuthTransport{ + Username: jira.username, + Password: jira.token, + } + client, err := agjira.NewClient(tp.Client(), jira.url) + if err != nil { + return nil, err + } + issue, _, err := client.Issue.Get(id, nil) + return issue, err +} diff --git a/jira_test.go b/jira_test.go new file mode 100644 index 00000000..30936e29 --- /dev/null +++ b/jira_test.go @@ -0,0 +1,42 @@ +package chglog + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestJira(t *testing.T) { + assert := assert.New(t) + + config := &Config { + Options: &Options{ + Processor: nil, + NextTag: "", + TagFilterPattern: "", + CommitFilters: nil, + CommitSortBy: "", + CommitGroupBy: "", + CommitGroupSortBy: "", + CommitGroupTitleMaps: nil, + HeaderPattern: "", + HeaderPatternMaps: nil, + IssuePrefix: nil, + RefActions: nil, + MergePattern: "", + MergePatternMaps: nil, + RevertPattern: "", + RevertPatternMaps: nil, + NoteKeywords: nil, + JiraUsername: "uuu", + JiraToken: "ppp", + JiraUrl: "http://jira.com", + JiraTypeMaps: nil, + JiraIssueDescriptionPattern: "", + }, + } + + jira := NewJiraClient(config) + issue, err := jira.GetJiraIssue("fake") + assert.Nil(issue) + assert.Error(err) +} diff --git a/testdata/gitlog_jira.txt b/testdata/gitlog_jira.txt new file mode 100644 index 00000000..f535f2e1 --- /dev/null +++ b/testdata/gitlog_jira.txt @@ -0,0 +1 @@ +@@__CHGLOG__@@HASH:65cf1add9735dcc4810dda3312b0792236c97c4e 65cf1add@@__CHGLOG_DELIMITER__@@AUTHOR:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@COMMITTER:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@SUBJECT:[JIRA-1111]: Add new feature #123@@__CHGLOG_DELIMITER__@@BODY: This is body message. diff --git a/vendor/github.com/andygrunwald/go-jira/LICENSE b/vendor/github.com/andygrunwald/go-jira/LICENSE new file mode 100644 index 00000000..692f6bea --- /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/authentication.go b/vendor/github.com/andygrunwald/go-jira/authentication.go new file mode 100644 index 00000000..f848a1d5 --- /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 00000000..a691672c --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/board.go @@ -0,0 +1,204 @@ +package jira + +import ( + "fmt" + "strconv" + "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:"type,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 +} + +// GetAllSprintsOptions specifies the optional parameters to the BoardService.GetList +type GetAllSprintsOptions struct { + // State filters results to sprints in the specified states, comma-separate list + State string `url:"state,omitempty"` + + SearchOptions +} + +// SprintsList reflects a list of agile sprints +type SprintsList 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 []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) + if err != nil { + return nil, nil, err + } + 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 return 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) { + id, err := strconv.Atoi(boardID) + if err != nil { + return nil, nil, err + } + + result, response, err := s.GetAllSprintsWithOptions(id, &GetAllSprintsOptions{}) + if err != nil { + return nil, nil, err + } + + return result.Values, response, nil +} + +// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options +// 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) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID) + url, err := addOptions(apiEndpoint, options) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + result := new(SprintsList) + resp, err := s.client.Do(req, result) + if err != nil { + err = NewJiraError(resp, err) + } + + return result, resp, err +} diff --git a/vendor/github.com/andygrunwald/go-jira/component.go b/vendor/github.com/andygrunwald/go-jira/component.go new file mode 100644 index 00000000..407ad36a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/component.go @@ -0,0 +1,38 @@ +package jira + +// ComponentService handles components for the JIRA instance / API. +// +// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component +type ComponentService struct { + client *Client +} + +// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component +type CreateComponentOptions struct { + Name string `json:"name,omitempty" structs:"name,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + Lead *User `json:"lead,omitempty" structs:"lead,omitempty"` + LeadUserName string `json:"leadUserName,omitempty" structs:"leadUserName,omitempty"` + AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"` + Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"` + Project string `json:"project,omitempty" structs:"project,omitempty"` + ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` +} + +// Create creates a new JIRA component based on the given options. +func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { + apiEndpoint := "rest/api/2/component" + req, err := s.client.NewRequest("POST", apiEndpoint, options) + if err != nil { + return nil, nil, err + } + + component := new(ProjectComponent) + resp, err := s.client.Do(req, component) + + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + + return component, resp, nil +} 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 00000000..bd1a5b9a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/error.go @@ -0,0 +1,90 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "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} + contentType := resp.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "application/json") { + 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()) + } + } else { + if httpError == nil { + return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body)) + } + return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body))) + } + + 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/field.go b/vendor/github.com/andygrunwald/go-jira/field.go new file mode 100644 index 00000000..257d4f99 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/field.go @@ -0,0 +1,43 @@ +package jira + +// FieldService handles fields for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field +type FieldService struct { + client *Client +} + +// Field represents a field of a JIRA issue. +type Field struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Custom bool `json:"custom,omitempty" structs:"custom,omitempty"` + Navigable bool `json:"navigable,omitempty" structs:"navigable,omitempty"` + Searchable bool `json:"searchable,omitempty" structs:"searchable,omitempty"` + ClauseNames []string `json:"clauseNames,omitempty" structs:"clauseNames,omitempty"` + Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"` +} + +type FieldSchema struct { + Type string `json:"type,omitempty" structs:"type,omitempty"` + System string `json:"system,omitempty" structs:"system,omitempty"` +} + +// GetList gets all fields from JIRA +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get +func (s *FieldService) GetList() ([]Field, *Response, error) { + apiEndpoint := "rest/api/2/field" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + fieldList := []Field{} + resp, err := s.client.Do(req, &fieldList) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return fieldList, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/filter.go b/vendor/github.com/andygrunwald/go-jira/filter.go new file mode 100644 index 00000000..209e63bd --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/filter.go @@ -0,0 +1,93 @@ +package jira + +import "github.com/google/go-querystring/query" +import "fmt" + +// FilterService handles fields for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter +type FilterService struct { + client *Client +} + +// Filter represents a Filter in Jira +type Filter struct { + Self string `json:"self"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Owner User `json:"owner"` + Jql string `json:"jql"` + ViewURL string `json:"viewUrl"` + SearchURL string `json:"searchUrl"` + Favourite bool `json:"favourite"` + FavouritedCount int `json:"favouritedCount"` + SharePermissions []interface{} `json:"sharePermissions"` + Subscriptions struct { + Size int `json:"size"` + Items []interface{} `json:"items"` + MaxResults int `json:"max-results"` + StartIndex int `json:"start-index"` + EndIndex int `json:"end-index"` + } `json:"subscriptions"` +} + +// GetList retrieves all filters from Jira +func (fs *FilterService) GetList() ([]*Filter, *Response, error) { + + options := &GetQueryOptions{} + apiEndpoint := "rest/api/2/filter" + req, err := fs.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() + } + + filters := []*Filter{} + resp, err := fs.client.Do(req, &filters) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + return filters, resp, err +} + +// GetFavouriteList retrieves the user's favourited filters from Jira +func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { + apiEndpoint := "rest/api/2/filter/favourite" + req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + filters := []*Filter{} + resp, err := fs.client.Do(req, &filters) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + return filters, resp, err +} + +// Get retrieves a single Filter from Jira +func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID) + req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + filter := new(Filter) + resp, err := fs.client.Do(req, filter) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return filter, resp, err +} 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 00000000..8ceadc9b --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/group.go @@ -0,0 +1,154 @@ +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"` +} + +// GroupSearchOptions specifies the optional parameters for the Get Group methods +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 +} + +// GetWithOptions 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 00000000..93212b1d --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/issue.go @@ -0,0 +1,1260 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "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 +} + +// UpdateQueryOptions specifies the optional parameters to the Edit issue +type UpdateQueryOptions struct { + NotifyUsers bool `url:"notifyUsers,omitempty"` + OverrideScreenSecurity bool `url:"overrideScreenSecurity,omitempty"` + OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"` +} + +// 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"` + RenderedFields *IssueRenderedFields `json:"renderedFields,omitempty" structs:"renderedFields,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 + // * "workratio": -1, + // * "lastViewed": 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"` + AffectsVersions []*AffectsVersion `json:"versions,omitempty" structs:"versions,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"` + AggregateTimeOriginalEstimate int `json:"aggregatetimeoriginalestimate,omitempty" structs:"aggregatetimeoriginalestimate,omitempty"` + AggregateTimeSpent int `json:"aggregatetimespent,omitempty" structs:"aggregatetimespent,omitempty"` + AggregateTimeEstimate int `json:"aggregatetimeestimate,omitempty" structs:"aggregatetimeestimate,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 + +} + +// IssueRenderedFields represents rendered fields of a JIRA issue. +// Not all IssueFields are rendered. +type IssueRenderedFields struct { + // TODO Missing fields + // * "aggregatetimespent": null, + // * "workratio": -1, + // * "lastViewed": null, + // * "aggregatetimeoriginalestimate": null, + // * "aggregatetimeestimate": null, + // * "environment": null, + Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Duedate string `json:"duedate,omitempty" structs:"duedate,omitempty"` + Updated string `json:"updated,omitempty" structs:"updated,omitempty"` + Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` +} + +// 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"` +} + +// 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"` +} + +// Progress represents the progress of a JIRA issue. +type Progress struct { + Progress int `json:"progress" structs:"progress"` + Total int `json:"total" structs:"total"` + Percent int `json:"percent" structs:"percent"` +} + +// 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 + +func (t Time) Equal(u Time) bool { + return time.Time(t).Equal(time.Time(u)) +} + +// 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 +} + +// MarshalJSON will transform the time.Time into a JIRA time +// during the creation of a JIRA request +func (t Time) MarshalJSON() ([]byte, error) { + return []byte(time.Time(t).Format("\"2006-01-02T15:04:05.999-0700\"")), 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"` + Properties []EntityProperty `json:"properties,omitempty"` +} + +type EntityProperty struct { + Key string `json:"key"` + Value interface{} `json:"value"` +} + +// 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 { + 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 + StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"` +} + +// AffectsVersion represents a software release which is affected by an issue. +type AffectsVersion Version + +// 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"` +} + +// GetWorklogsQueryOptions specifies the optional parameters for the Get Worklogs method +type GetWorklogsQueryOptions struct { + StartAt int64 `url:"startAt,omitempty"` + MaxResults int32 `url:"maxResults,omitempty"` + Expand string `url:"expand,omitempty"` +} + +type AddWorklogQueryOptions struct { + NotifyUsers bool `url:"notifyUsers,omitempty"` + AdjustEstimate string `url:"adjustEstimate,omitempty"` + NewEstimate string `url:"newEstimate,omitempty"` + ReduceBy string `url:"reduceBy,omitempty"` + Expand string `url:"expand,omitempty"` + OverrideEditableFlag bool `url:"overrideEditableFlag,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 +} + +// DeleteAttachment deletes an attachment of a given attachmentID +func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/attachment/%s", attachmentID) + + 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 +} + +// 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, options ...func(*http.Request) error) (*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 + } + + for _, option := range options { + err = option(req) + if err != nil { + return nil, nil, err + } + } + + v := new(Worklog) + resp, err := s.client.Do(req, v) + return v, resp, err +} + +// Applies query options to http request. +// This helper is meant to be used with all "QueryOptions" structs. +func WithQueryOptions(options interface{}) func(*http.Request) error { + q, err := query.Values(options) + if err != nil { + return func(*http.Request) error { + return err + } + } + + return func(r *http.Request) error { + r.URL.RawQuery = q.Encode() + return nil + } +} + +// 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 +} + +// UpdateWithOptions updates an issue from a JSON representation, +// while also specifiying query params. The issue is found by key. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue +func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key) + url, err := addOptions(apiEndpoint, opts) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("PUT", url, 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 +} + +// 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) { + return s.UpdateWithOptions(issue, 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("PUT", 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 +} + +// DeleteComment Deletes a comment from an issueID. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-comment-id-delete +func (s *IssueService) DeleteComment(issueID, commentID string) error { + apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, commentID) + req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + if err != nil { + return err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return jerr + } + + return 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, options ...func(*http.Request) error) (*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 + } + + for _, option := range options { + err = option(req) + 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 = "rest/api/2/search?jql=" + url.QueryEscape(jql) + if options.StartAt != 0 { + u += fmt.Sprintf("&startAt=%d", options.StartAt) + } + if options.MaxResults != 0 { + u += fmt.Sprintf("&maxResults=%d", options.MaxResults) + } + if options.Expand != "" { + u += fmt.Sprintf("&expand=%s", options.Expand) + } + if strings.Join(options.Fields, ",") != "" { + u += fmt.Sprintf("&fields=%s", strings.Join(options.Fields, ",")) + } + if options.ValidateQuery != "" { + u += fmt.Sprintf("&validateQuery=%s", 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 +} + +// UpdateAssignee updates the user assigned to work on the given issue +// +// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.2/#api/2/issue-assign +func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/assignee", issueID) + + req, err := s.client.NewRequest("PUT", apiEndPoint, assignee) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + err = NewJiraError(resp, err) + } + + return resp, err +} + +func (c ChangelogHistory) CreatedTime() (time.Time, error) { + var t time.Time + // Ignore null + if string(c.Created) == "null" { + return t, nil + } + t, err := time.Parse("2006-01-02T15:04:05.999-0700", c.Created) + return t, 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 00000000..4bd1c35c --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/jira.go @@ -0,0 +1,460 @@ +package jira + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "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 + Priority *PriorityService + Field *FieldService + Component *ComponentService + Resolution *ResolutionService + StatusCategory *StatusCategoryService + Filter *FilterService + Role *RoleService + PermissionScheme *PermissionSchemeService +} + +// 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 + } + + // ensure the baseURL contains a trailing slash so that all paths are preserved in later calls + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + + 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} + c.Priority = &PriorityService{client: c} + c.Field = &FieldService{client: c} + c.Component = &ComponentService{client: c} + c.Resolution = &ResolutionService{client: c} + c.StatusCategory = &StatusCategoryService{client: c} + c.Filter = &FilterService{client: c} + c.Role = &RoleService{client: c} + c.PermissionScheme = &PermissionSchemeService{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. +// 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 + } + // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash + rel.Path = strings.TrimLeft(rel.Path, "/") + + 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. +// 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 + } + // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash + rel.Path = strings.TrimLeft(rel.Path, "/") + + 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. +// 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 + } + // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash + rel.Path = strings.TrimLeft(rel.Path, "/") + + 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 { + // Don't add an empty value cookie to the request + if cookie.Value != "" { + 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 00000000..19813786 --- /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/permissionscheme.go b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go new file mode 100644 index 00000000..6a48d65f --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go @@ -0,0 +1,69 @@ +package jira + +import "fmt" + +// PermissionSchemeService handles permissionschemes for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme +type PermissionSchemeService struct { + client *Client +} +type PermissionSchemes struct { + PermissionSchemes []PermissionScheme `json:"permissionSchemes" structs:"permissionSchemes"` +} + +type Permission struct { + ID int `json:"id" structs:"id"` + Self string `json:"expand" structs:"expand"` + Holder Holder `json:"holder" structs:"holder"` + Name string `json:"permission" structs:"permission"` +} + +type Holder struct { + Type string `json:"type" structs:"type"` + Parameter string `json:"parameter" structs:"parameter"` + Expand string `json:"expand" structs:"expand"` +} + +// GetList returns a list of all permission schemes +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get +func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) { + apiEndpoint := "/rest/api/3/permissionscheme" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + pss := new(PermissionSchemes) + resp, err := s.client.Do(req, &pss) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return pss, resp, nil +} + +// Get returns a full representation of the permission scheme for the schemeID +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get +func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID) + 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 + } + if ps.Self == "" { + return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID) + } + + return ps, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/priority.go b/vendor/github.com/andygrunwald/go-jira/priority.go new file mode 100644 index 00000000..481f9592 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/priority.go @@ -0,0 +1,37 @@ +package jira + +// PriorityService handles priorities for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority +type PriorityService struct { + client *Client +} + +// 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"` + StatusColor string `json:"statusColor,omitempty" structs:"statusColor,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` +} + +// GetList gets all priorities from JIRA +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get +func (s *PriorityService) GetList() ([]Priority, *Response, error) { + apiEndpoint := "rest/api/2/priority" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + priorityList := []Priority{} + resp, err := s.client.Do(req, &priorityList) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return priorityList, resp, 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 00000000..24184224 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/project.go @@ -0,0 +1,161 @@ +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 map[string]string `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"` + Permissions []Permission `json:"permissions" structs:"permissions,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/resolution.go b/vendor/github.com/andygrunwald/go-jira/resolution.go new file mode 100644 index 00000000..36a651fb --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/resolution.go @@ -0,0 +1,35 @@ +package jira + +// ResolutionService handles resolutions for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution +type ResolutionService struct { + client *Client +} + +// 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"` +} + +// GetList gets all resolutions from JIRA +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get +func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { + apiEndpoint := "rest/api/2/resolution" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + resolutionList := []Resolution{} + resp, err := s.client.Do(req, &resolutionList) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return resolutionList, resp, nil +} diff --git a/vendor/github.com/andygrunwald/go-jira/role.go b/vendor/github.com/andygrunwald/go-jira/role.go new file mode 100644 index 00000000..f2535e4a --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/role.go @@ -0,0 +1,76 @@ +package jira + +import ( + "fmt" +) + +// RoleService handles roles for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role +type RoleService struct { + client *Client +} + +// Role represents a JIRA product role +type Role struct { + Self string `json:"self" structs:"self"` + Name string `json:"name" structs:"name"` + ID int `json:"id" structs:"id"` + Description string `json:"description" structs:"description"` + Actors []*Actor `json:"actors" structs:"actors"` +} + +// Actor represents a JIRA actor +type Actor struct { + ID int `json:"id" structs:"id"` + DisplayName string `json:"displayName" structs:"displayName"` + Type string `json:"type" structs:"type"` + Name string `json:"name" structs:"name"` + AvatarURL string `json:"avatarUrl" structs:"avatarUrl"` + ActorUser *ActorUser `json:"actorUser" structs:"actoruser"` +} + +// ActorUser contains the account id of the actor/user +type ActorUser struct { + AccountID string `json:"accountId" structs:"accountId"` +} + +// GetList returns a list of all available project roles +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get +func (s *RoleService) GetList() (*[]Role, *Response, error) { + apiEndpoint := "rest/api/3/role" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + roles := new([]Role) + resp, err := s.client.Do(req, roles) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + return roles, resp, err +} + +// Get retreives a single Role from Jira +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get +func (s *RoleService) Get(roleID int) (*Role, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID) + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + role := new(Role) + resp, err := s.client.Do(req, role) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + if role.Self == "" { + return nil, resp, fmt.Errorf("No role with ID %d found", roleID) + } + + return role, resp, err +} 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 00000000..7e8e697d --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/sprint.go @@ -0,0 +1,107 @@ +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 +} + +// GetIssue 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/statuscategory.go b/vendor/github.com/andygrunwald/go-jira/statuscategory.go new file mode 100644 index 00000000..05db4207 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/statuscategory.go @@ -0,0 +1,44 @@ +package jira + +// StatusCategoryService handles status categories for the JIRA instance / API. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory +type StatusCategoryService struct { + client *Client +} + +// 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"` +} + +// These constants are the keys of the default JIRA status categories +const ( + StatusCategoryComplete = "done" + StatusCategoryInProgress = "indeterminate" + StatusCategoryToDo = "new" + StatusCategoryUndefined = "undefined" +) + +// GetList gets all status categories from JIRA +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get +func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { + apiEndpoint := "rest/api/2/statuscategory" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + statusCategoryList := []StatusCategory{} + resp, err := s.client.Do(req, &statusCategoryList) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return statusCategoryList, 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 00000000..e14ebd83 --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/user.go @@ -0,0 +1,213 @@ +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"` + AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"` + // TODO: name & key are deprecated, see: + // https://developer.atlassian.com/cloud/jira/platform/api-changes-for-user-privacy-announcement/ + Name string `json:"name,omitempty" structs:"name,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Password string `json:"-"` + 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"` +} + +type userSearchParam struct { + name string + value string +} + +type userSearch []userSearchParam + +type userSearchF func(userSearch) userSearch + +// 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 +} + +// Delete deletes an user from JIRA. +// Returns http.StatusNoContent on success. +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete +func (s *UserService) Delete(username string) (*Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", 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 { + return resp, NewJiraError(resp, err) + } + return 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 +} + +// Get information about the current logged-in user +// +// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get +func (s *UserService) GetSelf() (*User, *Response, error) { + const apiEndpoint = "rest/api/2/myself" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + var user User + resp, err := s.client.Do(req, &user) + if err != nil { + return nil, resp, NewJiraError(resp, err) + } + return &user, resp, nil +} + +// WithMaxResults sets the max results to return +func WithMaxResults(maxResults int) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "maxResults", value: fmt.Sprintf("%d", maxResults)}) + return s + } +} + +// WithStartAt set the start pager +func WithStartAt(startAt int) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "startAt", value: fmt.Sprintf("%d", startAt)}) + return s + } +} + +// WithActive sets the active users lookup +func WithActive(active bool) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "includeActive", value: fmt.Sprintf("%t", active)}) + return s + } +} + +// WithInactive sets the inactive users lookup +func WithInactive(inactive bool) userSearchF { + return func(s userSearch) userSearch { + s = append(s, userSearchParam{name: "includeInactive", value: fmt.Sprintf("%t", inactive)}) + return s + } +} + +// 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, tweaks ...userSearchF) ([]User, *Response, error) { + search := []userSearchParam{ + { + name: "username", + value: property, + }, + } + for _, f := range tweaks { + search = f(search) + } + + var queryString = "" + for _, param := range search { + queryString += param.name + "=" + param.value + "&" + } + + apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1]) + 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 00000000..87bcde7b --- /dev/null +++ b/vendor/github.com/andygrunwald/go-jira/version.go @@ -0,0 +1,97 @@ +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 + StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"` +} + +// 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/LICENSE b/vendor/github.com/fatih/structs/LICENSE new file mode 100644 index 00000000..34504e4b --- /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/field.go b/vendor/github.com/fatih/structs/field.go new file mode 100644 index 00000000..e6978323 --- /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 00000000..3a877065 --- /dev/null +++ b/vendor/github.com/fatih/structs/structs.go @@ -0,0 +1,584 @@ +// 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 + t = append(t, Values(val.Interface())...) + } 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()) + 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 00000000..136a31eb --- /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 option 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/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE new file mode 100644 index 00000000..ae121a1e --- /dev/null +++ b/vendor/github.com/google/go-querystring/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Google. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go new file mode 100644 index 00000000..37080b19 --- /dev/null +++ b/vendor/github.com/google/go-querystring/query/encode.go @@ -0,0 +1,320 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package query implements encoding of structs into URL query parameters. +// +// As a simple example: +// +// type Options struct { +// Query string `url:"q"` +// ShowAll bool `url:"all"` +// Page int `url:"page"` +// } +// +// opt := Options{ "foo", true, 2 } +// v, _ := query.Values(opt) +// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" +// +// The exact mapping between Go values and url.Values is described in the +// documentation for the Values() function. +package query + +import ( + "bytes" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +var timeType = reflect.TypeOf(time.Time{}) + +var encoderType = reflect.TypeOf(new(Encoder)).Elem() + +// Encoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type Encoder interface { + EncodeValues(key string, v *url.Values) error +} + +// Values returns the url.Values encoding of v. +// +// Values expects to be passed a struct, and traverses it recursively using the +// following encoding rules. +// +// Each exported struct field is encoded as a URL parameter unless +// +// - the field's tag is "-", or +// - the field is empty and its tag specifies the "omitempty" option +// +// The empty values are false, 0, any nil pointer or interface value, any array +// slice, map, or string of length zero, and any time.Time that returns true +// for IsZero(). +// +// The URL parameter name defaults to the struct field name but can be +// specified in the struct field's tag value. The "url" key in the struct +// field's tag value is the key name, followed by an optional comma and +// options. For example: +// +// // Field is ignored by this package. +// Field int `url:"-"` +// +// // Field appears as URL parameter "myName". +// Field int `url:"myName"` +// +// // Field appears as URL parameter "myName" and the field is omitted if +// // its value is empty +// Field int `url:"myName,omitempty"` +// +// // Field appears as URL parameter "Field" (the default), but the field +// // is skipped if empty. Note the leading comma. +// Field int `url:",omitempty"` +// +// For encoding individual field values, the following type-dependent rules +// apply: +// +// Boolean values default to encoding as the strings "true" or "false". +// Including the "int" option signals that the field should be encoded as the +// strings "1" or "0". +// +// time.Time values default to encoding as RFC3339 timestamps. Including the +// "unix" option signals that the field should be encoded as a Unix time (see +// time.Unix()) +// +// Slice and Array values default to encoding as multiple URL values of the +// same name. Including the "comma" option signals that the field should be +// encoded as a single comma-delimited value. Including the "space" option +// similarly encodes the value as a single space-delimited string. Including +// the "semicolon" option will encode the value as a semicolon-delimited string. +// Including the "brackets" option signals that the multiple URL values should +// have "[]" appended to the value name. "numbered" will append a number to +// the end of each incidence of the value name, example: +// name0=value0&name1=value1, etc. +// +// Anonymous struct fields are usually encoded as if their inner exported +// fields were fields in the outer struct, subject to the standard Go +// visibility rules. An anonymous struct field with a name given in its URL +// tag is treated as having that name, rather than being anonymous. +// +// Non-nil pointer values are encoded as the value pointed to. +// +// Nested structs are encoded including parent fields in value names for +// scoping. e.g: +// +// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" +// +// All other values are encoded using their default string representation. +// +// Multiple fields that encode to the same URL parameter name will be included +// as multiple URL values of the same name. +func Values(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + var embedded []reflect.Value + + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "-" { + continue + } + name, opts := parseTag(tag) + if name == "" { + if sf.Anonymous && sv.Kind() == reflect.Struct { + // save embedded struct for later processing + embedded = append(embedded, sv) + continue + } + + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(encoderType) { + if !reflect.Indirect(sv).IsValid() { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(Encoder) + if err := m.EncodeValues(name, &values); err != nil { + return err + } + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + var del byte + if opts.Contains("comma") { + del = ',' + } else if opts.Contains("space") { + del = ' ' + } else if opts.Contains("semicolon") { + del = ';' + } else if opts.Contains("brackets") { + name = name + "[]" + } + + if del != 0 { + s := new(bytes.Buffer) + first := true + for i := 0; i < sv.Len(); i++ { + if first { + first = false + } else { + s.WriteByte(del) + } + s.WriteString(valueString(sv.Index(i), opts)) + } + values.Add(name, s.String()) + } else { + for i := 0; i < sv.Len(); i++ { + k := name + if opts.Contains("numbered") { + k = fmt.Sprintf("%s%d", name, i) + } + values.Add(k, valueString(sv.Index(i), opts)) + } + } + continue + } + + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == timeType { + values.Add(name, valueString(sv, opts)) + continue + } + + if sv.Kind() == reflect.Struct { + reflectValue(values, sv, name) + continue + } + + values.Add(name, valueString(sv, opts)) + } + + for _, f := range embedded { + if err := reflectValue(values, f, scope); err != nil { + return err + } + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Kind() == reflect.Bool && opts.Contains("int") { + if v.Bool() { + return "1" + } + return "0" + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if opts.Contains("unix") { + return strconv.FormatInt(t.Unix(), 10) + } + return t.Format(time.RFC3339) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + + if v.Type() == timeType { + return v.Interface().(time.Time).IsZero() + } + + return false +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 00000000..835ba3e7 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 00000000..7421f326 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,282 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which when applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// together with the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required, the errors.WithStack and +// errors.WithMessage functions destructure errors.Wrap into its component +// operations: annotating an error with a stack trace and with a message, +// respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error that does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// Although the causer interface is not exported by this package, it is +// considered a part of its stable public interface. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported: +// +// %s print the error. If the error has a Cause it will be +// printed recursively. +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface: +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// The returned errors.StackTrace type is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// Although the stackTracer interface is not exported by this package, it is +// considered a part of its stable public interface. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is called, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +// WithMessagef annotates err with the format specifier. +// If err is nil, WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 00000000..2874a048 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,147 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} diff --git a/vendor/github.com/trivago/tgo/LICENSE b/vendor/github.com/trivago/tgo/LICENSE new file mode 100644 index 00000000..8f71f43f --- /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 00000000..ede9aed3 --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/arrays.go @@ -0,0 +1,113 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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 00000000..c1290b76 --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/bytepool.go @@ -0,0 +1,157 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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 00000000..7564f3b5 --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go @@ -0,0 +1,565 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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" + "reflect" + "strconv" + "strings" + "time" + + "github.com/trivago/tgo/treflect" +) + +// 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") +} + +// Clone creates a copy of the given MarshalMap. +func (mmap MarshalMap) Clone() MarshalMap { + clone := cloneMap(reflect.ValueOf(mmap)) + return clone.Interface().(MarshalMap) +} + +func cloneMap(mapValue reflect.Value) reflect.Value { + clone := reflect.MakeMap(mapValue.Type()) + keys := mapValue.MapKeys() + + for _, k := range keys { + v := mapValue.MapIndex(k) + switch k.Kind() { + default: + clone.SetMapIndex(k, v) + + case reflect.Array, reflect.Slice: + if v.Type().Elem().Kind() == reflect.Map { + sliceCopy := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) + for i := 0; i < v.Len(); i++ { + element := v.Index(i) + sliceCopy.Index(i).Set(cloneMap(element)) + } + } else { + sliceCopy := reflect.MakeSlice(v.Type(), 0, v.Len()) + reflect.Copy(sliceCopy, v) + clone.SetMapIndex(k, sliceCopy) + } + + case reflect.Map: + vClone := cloneMap(v) + clone.SetMapIndex(k, vClone) + } + } + + return clone +} + +// 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 +} + +// Bytes returns a value at key that is expected to be a []byte +func (mmap MarshalMap) Bytes(key string) ([]byte, error) { + val, exists := mmap.Value(key) + if !exists { + return []byte{}, fmt.Errorf(`"%s" is not set`, key) + } + + bytesValue, isBytes := val.([]byte) + if !isBytes { + return []byte{}, fmt.Errorf(`"%s" is expected to be a []byte`, key) + } + return bytesValue, nil +} + +// Slice is an alias for Array +func (mmap MarshalMap) Slice(key string) ([]interface{}, error) { + return mmap.Array(key) +} + +// 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) + } +} + +// StringSlice is an alias for StringArray +func (mmap MarshalMap) StringSlice(key string) ([]string, error) { + return mmap.StringArray(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) + } +} + +// Int64Slice is an alias for Int64Array +func (mmap MarshalMap) Int64Slice(key string) ([]int64, error) { + return mmap.Int64Array(key) +} + +// Int64Array 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 + } +} + +// StringSliceMap is an alias for StringArrayMap +func (mmap MarshalMap) StringSliceMap(key string) (map[string][]string, error) { + return mmap.StringArrayMap(key) +} + +// 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) (val interface{}, exists bool) { + exists = mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) { + val = v + }) + return val, exists +} + +// Delete a value from a given path. +// The path must point to a map key. Deleting from arrays is not supported. +func (mmap MarshalMap) Delete(key string) { + mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) { + if v != nil { + p.SetMapIndex(k, reflect.Value{}) + } + }) +} + +// Set a value for a given path. +// The path must point to a map key. Setting array elements is not supported. +func (mmap MarshalMap) Set(key string, val interface{}) { + mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) { + p.SetMapIndex(k, reflect.ValueOf(val)) + }) +} + +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(k string, v interface{}, action func(p, k reflect.Value, v interface{})) bool { + if len(k) == 0 { + action(reflect.Value{}, reflect.ValueOf(k), v) // ### return, found requested value ### + return true + } + + vValue := reflect.ValueOf(v) + switch vValue.Kind() { + case reflect.Array, reflect.Slice: + startIdx := strings.IndexRune(k, MarshalMapArrayBegin) // Must be first char, otherwise malformed + endIdx := strings.IndexRune(k, MarshalMapArrayEnd) // Must be > startIdx, otherwise malformed + + if startIdx == -1 || endIdx == -1 { + return false + } + + if startIdx == 0 && endIdx > startIdx { + index, err := strconv.Atoi(k[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 < vValue.Len() { + item := vValue.Index(index).Interface() + key := k[endIdx+1:] + return mmap.resolvePath(key, item, action) // ### return, nested array ### + } + } + + case reflect.Map: + kValue := reflect.ValueOf(k) + if storedValue := vValue.MapIndex(kValue); storedValue.IsValid() { + action(vValue, kValue, storedValue.Interface()) + return true + } + + keyEnd, nextKeyStart := mmap.resolvePathKey(k) + if keyEnd == len(k) { + action(vValue, kValue, nil) // call action to support setting non-existing keys + return false // ### return, key not found ### + } + + nextKey := k[:keyEnd] + nkValue := reflect.ValueOf(nextKey) + + if storedValue := vValue.MapIndex(nkValue); storedValue.IsValid() { + remain := k[nextKeyStart:] + return mmap.resolvePath(remain, storedValue.Interface(), action) // ### return, nested map ### + } + } + + return 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 00000000..f7217093 --- /dev/null +++ b/vendor/github.com/trivago/tgo/tcontainer/trie.go @@ -0,0 +1,227 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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/clone.go b/vendor/github.com/trivago/tgo/treflect/clone.go new file mode 100644 index 00000000..b8871262 --- /dev/null +++ b/vendor/github.com/trivago/tgo/treflect/clone.go @@ -0,0 +1,80 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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 ( + "reflect" +) + +// Clone does a deep copy of the given value. +// Please note that field have to be public in order to be copied. +// Private fields will be ignored. +func Clone(v interface{}) interface{} { + value := reflect.ValueOf(v) + copy := clone(value) + return copy.Interface() +} + +func clone(v reflect.Value) reflect.Value { + switch v.Kind() { + case reflect.Struct: + copy := reflect.New(v.Type()) + + for i := 0; i < v.Type().NumField(); i++ { + field := v.Field(i) + targetField := copy.Elem().Field(i) + if !targetField.CanSet() { + continue // ignore private fields + } + fieldCopy := clone(field) + targetField.Set(fieldCopy) + } + return copy.Elem() + + case reflect.Chan: + copy := reflect.MakeChan(v.Type(), v.Len()) + return copy + + case reflect.Map: + copy := reflect.MakeMap(v.Type()) + keys := v.MapKeys() + for _, k := range keys { + fieldCopy := clone(v.MapIndex(k)) + copy.SetMapIndex(k, fieldCopy) + } + return copy + + case reflect.Slice: + copy := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) + for i := 0; i < v.Len(); i++ { + elementCopy := clone(v.Index(i)) + copy.Index(i).Set(elementCopy) + } + return copy + + case reflect.Array: + copy := reflect.New(v.Type()).Elem() + for i := 0; i < v.Len(); i++ { + elementCopy := clone(v.Index(i)) + copy.Index(i).Set(elementCopy) + } + return copy + + default: + copy := reflect.New(v.Type()) + copy.Elem().Set(v) + return copy.Elem() + } +} 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 00000000..48a11f47 --- /dev/null +++ b/vendor/github.com/trivago/tgo/treflect/reflection.go @@ -0,0 +1,371 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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 + } + + 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 00000000..d3e3d7ef --- /dev/null +++ b/vendor/github.com/trivago/tgo/treflect/typeregistry.go @@ -0,0 +1,97 @@ +// Copyright 2015-2018 trivago N.V. +// +// 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 +}