From 1cc95bfac8c2edc58ef43cd310c882263f3f2c30 Mon Sep 17 00:00:00 2001 From: Zhao Xiaojie Date: Wed, 18 Dec 2019 22:35:42 +0800 Subject: [PATCH] Enhance the searching Jenkins job (#284) * Add field filter for job search * Add support to output a common table by reflect way * Add output and filter as generic method * Fix the test error of casc cmd * Keep job path is compitable --- app/cmd/casc.go | 3 + app/cmd/casc_apply.go | 6 +- app/cmd/casc_export.go | 6 +- app/cmd/casc_open.go | 3 + app/cmd/casc_reload.go | 6 +- app/cmd/casc_schema.go | 6 +- app/cmd/casc_test.go | 26 +++--- app/cmd/common.go | 104 +++++++++++++++++++++- app/cmd/common_test.go | 168 +++++++++++++++++++++++++++++++++++ app/cmd/credential.go | 3 + app/cmd/credential_create.go | 6 +- app/cmd/credential_delete.go | 6 +- app/cmd/credential_list.go | 6 +- app/cmd/job_search.go | 34 ++----- app/cmd/job_search_test.go | 40 ++++----- app/cmd/plugin_create.go | 6 +- app/cmd/plugin_release.go | 6 +- client/job.go | 12 ++- util/reflect.go | 11 +++ 19 files changed, 386 insertions(+), 72 deletions(-) create mode 100644 app/cmd/common_test.go create mode 100644 util/reflect.go diff --git a/app/cmd/casc.go b/app/cmd/casc.go index 0ae251d..a86b421 100644 --- a/app/cmd/casc.go +++ b/app/cmd/casc.go @@ -31,4 +31,7 @@ var cascCmd = &cobra.Command{ Use: "casc", Short: i18n.T("Configuration as Code"), Long: i18n.T("Configuration as Code"), + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_apply.go b/app/cmd/casc_apply.go index 3cee99e..a720edd 100644 --- a/app/cmd/casc_apply.go +++ b/app/cmd/casc_apply.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/client" "net/http" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -33,4 +34,7 @@ var cascApplyCmd = &cobra.Command{ getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore)) return jClient.Apply() }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_export.go b/app/cmd/casc_export.go index dbc2d5e..084166f 100644 --- a/app/cmd/casc_export.go +++ b/app/cmd/casc_export.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/client" "net/http" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -38,4 +39,7 @@ var cascExportCmd = &cobra.Command{ } return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_open.go b/app/cmd/casc_open.go index d80db68..fa4689b 100644 --- a/app/cmd/casc_open.go +++ b/app/cmd/casc_open.go @@ -28,4 +28,7 @@ var cascOpenCmd = &cobra.Command{ jenkins := getCurrentJenkinsFromOptionsOrDie() return util.Open(fmt.Sprintf("%s/configuration-as-code", jenkins.URL), cascOpenOption.ExecContext) }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_reload.go b/app/cmd/casc_reload.go index c58dcb2..43de4f6 100644 --- a/app/cmd/casc_reload.go +++ b/app/cmd/casc_reload.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/client" "net/http" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -33,4 +34,7 @@ var cascReloadCmd = &cobra.Command{ getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore)) return jClient.Reload() }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_schema.go b/app/cmd/casc_schema.go index 81259ad..bff320d 100644 --- a/app/cmd/casc_schema.go +++ b/app/cmd/casc_schema.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/client" "net/http" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -38,4 +39,7 @@ var cascSchemaCmd = &cobra.Command{ } return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/casc_test.go b/app/cmd/casc_test.go index fa5c695..e33e347 100644 --- a/app/cmd/casc_test.go +++ b/app/cmd/casc_test.go @@ -1,7 +1,6 @@ package cmd import ( - "io/ioutil" "os" "github.com/jenkins-zh/jenkins-cli/client" @@ -31,40 +30,41 @@ var _ = Describe("casc command check", func() { rootURL = "http://localhost:8080/jenkins" user = "admin" password = "111e3a2f0231198855dceaff96f20540a9" + + config = &Config{ + Current: "fake", + JenkinsServers: []JenkinsServer{JenkinsServer{ + Name: "fake", + URL: rootURL, + UserName: user, + Token: password, + }}, + } }) AfterEach(func() { rootCmd.SetArgs([]string{}) os.Remove(rootOptions.ConfigFile) rootOptions.ConfigFile = "" + config = nil ctrl.Finish() }) Context("basic cases", func() { It("without casc plugin", func() { - data, err := generateSampleConfig() - Expect(err).To(BeNil()) - err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) - Expect(err).To(BeNil()) - req, _ := client.PrepareForOneInstalledPlugin(roundTripper, rootURL) req.SetBasicAuth(user, password) - err = cascOptions.Check() + err := cascOptions.Check() Expect(err).To(HaveOccurred()) }) It("with casc plugin", func() { - data, err := generateSampleConfig() - Expect(err).To(BeNil()) - err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) - Expect(err).To(BeNil()) - req, _ := client.PrepareForOneInstalledPluginWithPluginName(roundTripper, rootURL, "configuration-as-code") req.SetBasicAuth(user, password) - err = cascOptions.Check() + err := cascOptions.Check() Expect(err).NotTo(HaveOccurred()) }) }) diff --git a/app/cmd/common.go b/app/cmd/common.go index 99beba8..28691ec 100644 --- a/app/cmd/common.go +++ b/app/cmd/common.go @@ -3,14 +3,21 @@ package cmd import ( "encoding/json" "fmt" + "gopkg.in/yaml.v2" + "io" "net/http" + "reflect" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/jenkins-zh/jenkins-cli/client" "github.com/jenkins-zh/jenkins-cli/util" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" +) + +const ( + since = "since" ) // CommonOption contains the common options @@ -25,7 +32,11 @@ type CommonOption struct { type OutputOption struct { Format string + Columns string WithoutHeaders bool + Filter []string + + Writer io.Writer } // FormatOutput is the interface of format output @@ -43,6 +54,7 @@ const ( ) // Output print the object into byte array +// Deprecated see also OutputV2 func (o *OutputOption) Output(obj interface{}) (data []byte, err error) { switch o.Format { case JSONOutputFormat: @@ -54,6 +66,96 @@ func (o *OutputOption) Output(obj interface{}) (data []byte, err error) { return nil, fmt.Errorf("not support format %s", o.Format) } +// OutputV2 print the data line by line +func (o *OutputOption) OutputV2(obj interface{}) (err error) { + if o.Writer == nil { + err = fmt.Errorf("no writer found") + return + } + + if len(o.Columns) == 0 { + err = fmt.Errorf("no columns found") + return + } + + obj = o.ListFilter(obj) + + var data []byte + switch o.Format { + case JSONOutputFormat: + data, err = json.MarshalIndent(obj, "", " ") + case YAMLOutputFormat: + data, err = yaml.Marshal(obj) + case TableOutputFormat, "": + table := util.CreateTableWithHeader(o.Writer, o.WithoutHeaders) + table.AddHeader(strings.Split(o.Columns, ",")...) + items := reflect.ValueOf(obj) + for i := 0; i < items.Len(); i++ { + table.AddRow(o.GetLine(items.Index(i))...) + } + table.Render() + default: + err = fmt.Errorf("not support format %s", o.Format) + } + + if err == nil && len(data) > 0 { + _, err = o.Writer.Write(data) + } + return +} + +// ListFilter filter the data list by fields +func (o *OutputOption) ListFilter(obj interface{}) interface{} { + if len(o.Filter) == 0 { + return obj + } + + elemType := reflect.TypeOf(obj).Elem() + elemSlice := reflect.MakeSlice(reflect.SliceOf(elemType), 0, 10) + items := reflect.ValueOf(obj) + for i := 0; i < items.Len(); i++ { + item := items.Index(i) + if o.Match(item) { + elemSlice = reflect.Append(elemSlice, item) + } + } + return elemSlice.Interface() +} + +// Match filter an item +func (o *OutputOption) Match(item reflect.Value) bool { + if len(o.Filter) == 0 { + return true + } + + for _, f := range o.Filter { + arr := strings.Split(f, "=") + if len(arr) < 2 { + continue + } + + key := arr[0] + val := arr[1] + + if !strings.Contains(util.ReflectFieldValueAsString(item, key), val) { + continue + } else { + return true + } + } + return false +} + +// GetLine returns the line of a table +func (o *OutputOption) GetLine(obj reflect.Value) []string { + columns := strings.Split(o.Columns, ",") + values := make([]string, 0) + for _, col := range columns { + values = append(values, util.ReflectFieldValueAsString(obj, col)) + } + return values +} + // SetFlag set flag of output format func (o *OutputOption) SetFlag(cmd *cobra.Command) { cmd.Flags().StringVarP(&o.Format, "output", "o", TableOutputFormat, "Format the output, supported formats: table, json, yaml") diff --git a/app/cmd/common_test.go b/app/cmd/common_test.go new file mode 100644 index 0000000..3107579 --- /dev/null +++ b/app/cmd/common_test.go @@ -0,0 +1,168 @@ +package cmd_test + +import ( + "bytes" + "github.com/jenkins-zh/jenkins-cli/app/cmd" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "reflect" +) + +var _ = Describe("test OutputOption", func() { + var ( + outputOption cmd.OutputOption + fakeFoos []FakeFoo + ) + + BeforeEach(func() { + outputOption = cmd.OutputOption{} + + fakeFoos = []FakeFoo{{ + Name: "fake", + }, { + Name: "foo-1", + }} + }) + + Context("ListFilter test", func() { + var ( + result interface{} + ) + + JustBeforeEach(func() { + result = outputOption.ListFilter(fakeFoos) + }) + + It("without filter", func() { + Expect(result).To(Equal(fakeFoos)) + }) + + Context("with filter", func() { + BeforeEach(func() { + outputOption = cmd.OutputOption{ + Filter: []string{"Name=fake"}, + } + }) + + It("should success", func() { + Expect(result).NotTo(Equal(fakeFoos)) + }) + }) + }) + + Context("OutputV2 test", func() { + var ( + err error + ) + + JustBeforeEach(func() { + err = outputOption.OutputV2(fakeFoos) + }) + + It("without io writer", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("writer")) + }) + + Context("without columns", func() { + BeforeEach(func() { + outputOption.Writer = new(bytes.Buffer) + }) + + It("get no columns error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("columns")) + }) + }) + + Context("with io writer", func() { + var ( + buf *bytes.Buffer + ) + + BeforeEach(func() { + buf = new(bytes.Buffer) + outputOption.Writer = buf + outputOption.Columns = "Name" + }) + + It("with the default output format", func() { + Expect(buf.String()).To(Equal(`Name +fake +foo-1 +`)) + }) + + Context("with json format", func() { + BeforeEach(func() { + outputOption.Format = cmd.JSONOutputFormat + }) + + It("should get a json text", func() { + Expect(buf.String()).To(Equal(`[ + { + "Name": "fake" + }, + { + "Name": "foo-1" + } +]`)) + }) + }) + + Context("with yaml format", func() { + BeforeEach(func() { + outputOption.Format = cmd.YAMLOutputFormat + }) + + It("should get a yaml text", func() { + Expect(buf.String()).To(Equal(`- name: fake +- name: foo-1 +`)) + }) + }) + + Context("with a unknown format", func() { + BeforeEach(func() { + outputOption.Format = "fake" + }) + + It("should get an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not support format")) + }) + }) + }) + }) + + Context("Match test", func() { + var ( + result bool + ) + + JustBeforeEach(func() { + result = outputOption.Match(reflect.ValueOf(fakeFoos[0])) + }) + + It("without filter", func() { + Expect(result).To(BeTrue()) + }) + + Context("invalid filter", func() { + BeforeEach(func() { + outputOption = cmd.OutputOption{ + Filter: []string{"Name"}, + } + }) + + It("not matched", func() { + Expect(result).To(BeFalse()) + }) + }) + }) +}) + +// FakeFoo only for test +type FakeFoo struct { + Name string +} diff --git a/app/cmd/credential.go b/app/cmd/credential.go index 727d662..c5aa5d0 100644 --- a/app/cmd/credential.go +++ b/app/cmd/credential.go @@ -14,4 +14,7 @@ var credentialCmd = &cobra.Command{ Aliases: []string{"secret", "cred"}, Short: i18n.T("Manage the credentials of your Jenkins"), Long: i18n.T(`Manage the credentials of your Jenkins`), + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/credential_create.go b/app/cmd/credential_create.go index 40d5fa6..0fd00bc 100644 --- a/app/cmd/credential_create.go +++ b/app/cmd/credential_create.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" + "net/http" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/jenkins-zh/jenkins-cli/client" "github.com/spf13/cobra" - "net/http" ) // CredentialCreateOption option for credential delete command @@ -90,4 +91,7 @@ var credentialCreateCmd = &cobra.Command{ } return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/credential_delete.go b/app/cmd/credential_delete.go index 94a9baf..6291ec5 100644 --- a/app/cmd/credential_delete.go +++ b/app/cmd/credential_delete.go @@ -2,10 +2,11 @@ package cmd import ( "fmt" + "net/http" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/jenkins-zh/jenkins-cli/client" "github.com/spf13/cobra" - "net/http" ) // CredentialDeleteOption option for credential delete command @@ -63,4 +64,7 @@ var credentialDeleteCmd = &cobra.Command{ err = jClient.Delete(credentialDeleteOption.Store, credentialDeleteOption.ID) return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/app/cmd/credential_list.go b/app/cmd/credential_list.go index 3c83746..5289565 100644 --- a/app/cmd/credential_list.go +++ b/app/cmd/credential_list.go @@ -3,9 +3,10 @@ package cmd import ( "bytes" "fmt" - "github.com/jenkins-zh/jenkins-cli/client" "net/http" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/jenkins-zh/jenkins-cli/util" @@ -51,6 +52,9 @@ var credentialListCmd = &cobra.Command{ } return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } // Output render data into byte array as a table format diff --git a/app/cmd/job_search.go b/app/cmd/job_search.go index 2edaa5f..61a60cb 100644 --- a/app/cmd/job_search.go +++ b/app/cmd/job_search.go @@ -1,15 +1,12 @@ package cmd import ( - "bytes" "fmt" - "github.com/jenkins-zh/jenkins-cli/app/i18n" - "github.com/jenkins-zh/jenkins-cli/util" - "net/http" - "github.com/hashicorp/go-version" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/jenkins-zh/jenkins-cli/client" "github.com/spf13/cobra" + "net/http" ) // JobSearchOption is the options of job search command @@ -35,6 +32,10 @@ func init() { i18n.T("The name of plugin for search")) jobSearchCmd.Flags().StringVarP(&jobSearchOption.Type, "type", "", "", i18n.T("The type of plugin for search")) + jobSearchCmd.Flags().StringVarP(&jobSearchOption.Columns, "columns", "", "Name,DisplayName,Type,URL", + i18n.T("The columns of table")) + jobSearchCmd.Flags().StringArrayVarP(&jobSearchOption.Filter, "filter", "", []string{}, + i18n.T("Filter for the list")) jobSearchOption.SetFlag(jobSearchCmd) healthCheckRegister.Register(getCmdPath(jobSearchCmd), &jobSearchOption) @@ -61,32 +62,13 @@ var jobSearchCmd = &cobra.Command{ var items []client.JenkinsItem if items, err = jClient.Search(jobSearchOption.Name, jobSearchOption.Type, jobSearchOption.Start, jobSearchOption.Limit); err == nil { - var data []byte - if data, err = jobSearchOption.Output(items); err == nil { - cmd.Print(string(data)) - } + jobSearchOption.Writer = cmd.OutOrStdout() + err = jobSearchOption.OutputV2(items) } return }, } -// Output render data into byte array -func (o *JobSearchOption) Output(obj interface{}) (data []byte, err error) { - if data, err = o.OutputOption.Output(obj); err != nil { - items := obj.([]client.JenkinsItem) - buf := new(bytes.Buffer) - table := util.CreateTableWithHeader(buf, o.WithoutHeaders) - table.AddRow("number", "name", "displayname", "type", "url") - for i, item := range items { - table.AddRow(fmt.Sprintf("%d", i), item.Name, item.DisplayName, item.Type, item.URL) - } - table.Render() - data = buf.Bytes() - err = nil - } - return -} - // Check do the conditions check func (o *JobSearchOption) Check() (err error) { opt := PluginOptions{ diff --git a/app/cmd/job_search_test.go b/app/cmd/job_search_test.go index a6079cf..89d0358 100644 --- a/app/cmd/job_search_test.go +++ b/app/cmd/job_search_test.go @@ -58,8 +58,8 @@ var _ = Describe("job search command", func() { _, err = rootCmd.ExecuteC() Expect(err).To(BeNil()) - Expect(buf.String()).To(Equal(`number name displayname type url -0 fake fake WorkflowJob job/fake/ + Expect(buf.String()).To(Equal(`Name DisplayName Type URL +fake fake WorkflowJob job/fake/ `)) }) @@ -79,8 +79,8 @@ var _ = Describe("job search command", func() { _, err = rootCmd.ExecuteC() Expect(err).To(BeNil()) - Expect(buf.String()).To(Equal(`number name displayname type url -0 fake fake WorkflowJob job/fake/ + Expect(buf.String()).To(Equal(`Name DisplayName Type URL +fake fake WorkflowJob job/fake/ `)) }) }) @@ -104,9 +104,20 @@ var _ = Describe("job search command check", func() { rootURL = "http://localhost:8080/jenkins" user = "admin" password = "111e3a2f0231198855dceaff96f20540a9" + + config = &Config{ + Current: "fake", + JenkinsServers: []JenkinsServer{JenkinsServer{ + Name: "fake", + URL: rootURL, + UserName: user, + Token: password, + }}, + } }) AfterEach(func() { + config = nil rootCmd.SetArgs([]string{}) os.Remove(rootOptions.ConfigFile) rootOptions.ConfigFile = "" @@ -115,43 +126,28 @@ var _ = Describe("job search command check", func() { Context("basic cases", func() { It("without pipeline-restful-api plugin", func() { - data, err := generateSampleConfig() - Expect(err).To(BeNil()) - err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) - Expect(err).To(BeNil()) - req, _ := client.PrepareForOneInstalledPlugin(roundTripper, rootURL) req.SetBasicAuth(user, password) - err = jobSearchOption.Check() + err := jobSearchOption.Check() Expect(err).To(HaveOccurred()) }) It("with pipeline-restful-api 0.2+ plugin", func() { - data, err := generateSampleConfig() - Expect(err).To(BeNil()) - err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) - Expect(err).To(BeNil()) - req, _ := client.PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper, rootURL, "pipeline-restful-api", "1.0") req.SetBasicAuth(user, password) - err = jobSearchOption.Check() + err := jobSearchOption.Check() Expect(err).NotTo(HaveOccurred()) }) It("with pipeline-restful-api 0.2- plugin", func() { - data, err := generateSampleConfig() - Expect(err).To(BeNil()) - err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) - Expect(err).To(BeNil()) - req, _ := client.PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper, rootURL, "pipeline-restful-api", "0.1") req.SetBasicAuth(user, password) - err = jobSearchOption.Check() + err := jobSearchOption.Check() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("should be")) }) diff --git a/app/cmd/plugin_create.go b/app/cmd/plugin_create.go index 5098de7..18c383a 100644 --- a/app/cmd/plugin_create.go +++ b/app/cmd/plugin_create.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/util" "os" + "github.com/jenkins-zh/jenkins-cli/util" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -42,4 +43,7 @@ Plugin tutorial is here https://jenkins.io/doc/developer/tutorial/`), } return }, + Annotations: map[string]string{ + since: "v0.0.23", + }, } diff --git a/app/cmd/plugin_release.go b/app/cmd/plugin_release.go index 8a0348d..b9a1d41 100644 --- a/app/cmd/plugin_release.go +++ b/app/cmd/plugin_release.go @@ -1,9 +1,10 @@ package cmd import ( - "github.com/jenkins-zh/jenkins-cli/util" "os" + "github.com/jenkins-zh/jenkins-cli/util" + "github.com/jenkins-zh/jenkins-cli/app/i18n" "github.com/spf13/cobra" @@ -70,4 +71,7 @@ var pluginReleaseCmd = &cobra.Command{ } return }, + Annotations: map[string]string{ + since: "v0.0.24", + }, } diff --git a/client/job.go b/client/job.go index 9c81bb9..3bff4ea 100644 --- a/client/job.go +++ b/client/job.go @@ -287,7 +287,8 @@ func (q *JobClient) JobInputSubmit(jobName, inputID string, buildID int, abort b // ParseJobPath leads with slash func ParseJobPath(jobName string) (path string) { path = jobName - if jobName == "" || strings.HasPrefix(jobName, "/job") { + if jobName == "" || strings.HasPrefix(jobName, "/job/") || + strings.HasPrefix(jobName, "job/") { return } @@ -313,6 +314,15 @@ type JenkinsItem struct { URL string Description string Type string + + /** comes from Job */ + Buildable bool + Building bool + InQueue bool + + /** comes from ParameterizedJob */ + Parameterized bool + Disabled bool } // Job represents a job diff --git a/util/reflect.go b/util/reflect.go new file mode 100644 index 0000000..8a032a0 --- /dev/null +++ b/util/reflect.go @@ -0,0 +1,11 @@ +package util + +import ( + "fmt" + "reflect" +) + +// ReflectFieldValueAsString returns the value of a field +func ReflectFieldValueAsString(v reflect.Value, field string) string { + return fmt.Sprint(reflect.Indirect(v).FieldByName(field)) +} -- GitLab