Repository: PagerDuty/go-pagerduty
Pull request #499 (2023-10-12 21:12:51)

[CSGI-1827] Add Standards support
Add support for Standards APIs...

* [**List Standards**](https://developer.pagerduty.com/api-reference/dbed9a0ff9355-list-standards) - `GET: /standards`
* [**Update a standard**](https://developer.pagerduty.com/api-reference/4a18a8e4202aa-update-a-standard) - `PUT: /standards/{id}`
* [**List resources' standards scores**](https://developer.pagerduty.com/api-reference/2e832500ae129-list-resources-standards-scores) - `GET: /standards/scores/{resource_type}`
* [**List a resource's standards scores**](https://developer.pagerduty.com/api-reference/f339354b607d5-list-a-resource-s-standards-scores) - `GET: /standards/scores/{resource_type}/{id}`

## Test cases added...

```bash
$ go test -count=1 -v -run TestStandard
=== RUN   TestStandard_List
--- PASS: TestStandard_List (0.00s)
=== RUN   TestStandard_Update
--- PASS: TestStandard_Update (0.00s)
=== RUN   TestStandard_ListTechniServiceStandardScores
--- PASS: TestStandard_ListTechniServiceStandardScores (0.00s)
=== RUN   TestStandard_ListManyTechniServiceStandardScores
--- PASS: TestStandard_ListManyTechniServiceStandardScores (0.00s)
PASS
ok      github.com/PagerDuty/go-pagerduty       0.709s

```


Patch:
diff --git a/command/main.go b/command/main.go
index ce6215b..340d7b2 100644
--- a/command/main.go
+++ b/command/main.go
@@ -82,6 +82,11 @@ func loadCommands() map[string]cli.CommandFactory {
 		"service rule show":          ServiceRuleShowCommand,
 		"service rule update":        ServiceRuleUpdateCommand,
 
+		"standard list":                        StandardListCommand,
+		"standard update":                      StandardUpdateCommand,
+		"standard resource-scores list":        StandardListResourceScoresCommand,
+		"standard multi-resources-scores list": StandardListMultiResourcesScoresCommand,
+
 		"team list":                     TeamListCommand,
 		"team create":                   TeamShowCommand,
 		"team delete":                   TeamDeleteCommand,
diff --git a/command/standard_list.go b/command/standard_list.go
new file mode 100644
index 0000000..85dedda
--- /dev/null
+++ b/command/standard_list.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+)
+
+type StandardList struct {
+	Meta
+}
+
+func (c *StandardList) Help() string {
+	helpText := `
+	pd standard list List all of the existing standards
+
+	Options:
+
+    -active
+    -resource-type    Filter by resource type. Allowed values: technical_service
+
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *StandardList) Synopsis() string {
+	return "List all of the existing standards"
+}
+
+func StandardListCommand() (cli.Command, error) {
+	return &StandardList{}, nil
+}
+
+func (c *StandardList) Run(args []string) int {
+	var active bool
+	var resourceType string
+
+	flags := c.Meta.FlagSet("standard list")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	flags.BoolVar(&active, "active", false, "")
+	flags.StringVar(&resourceType, "resource-type", "", "Filter by resource type. Allowed values: technical_service")
+
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	client := c.Meta.Client()
+	opts := pagerduty.ListStandardsOptions{
+		Active:       active,
+		ResourceType: resourceType,
+	}
+	if res, err := client.ListStandards(context.Background(), opts); err != nil {
+		log.Error(err)
+		return -1
+	} else {
+		for i, r := range res.Standards {
+			if i > 0 {
+				fmt.Println("---")
+			}
+			data, err := yaml.Marshal(r)
+			if err != nil {
+				log.Error(err)
+				return -1
+			}
+			fmt.Println(string(data))
+		}
+	}
+	return 0
+}
diff --git a/command/standard_multi_resources_scores_list.go b/command/standard_multi_resources_scores_list.go
new file mode 100644
index 0000000..1e6359b
--- /dev/null
+++ b/command/standard_multi_resources_scores_list.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/PagerDuty/go-pagerduty"
+	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+)
+
+type StandardListMultiResourcesScores struct {
+	Meta
+}
+
+func (c *StandardListMultiResourcesScores) Help() string {
+	helpText := `
+	pd standard multi-resources-scores list List all of the existing standards scores for multiple resources
+
+	Options:
+
+    -id    List standard scores applied to resource (can be specified multiple times)
+    -resource-type    List scores applied to resource type (default: technical_services). Allowed values: technical_services
+
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *StandardListMultiResourcesScores) Synopsis() string {
+	return "List all of the existing standards scores applied to multiple resources"
+}
+
+func StandardListMultiResourcesScoresCommand() (cli.Command, error) {
+	return &StandardListMultiResourcesScores{}, nil
+}
+
+func (c *StandardListMultiResourcesScores) Run(args []string) int {
+	var resIDs []string
+	var resourceType string
+
+	flags := c.Meta.FlagSet("standard multi-resources-scores list")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	flags.Var((*ArrayFlags)(&resIDs), "id", "Display scores for standard applied to resource (can be specified multiple times)")
+	flags.StringVar(&resourceType, "resource-type", "technical_services", "Scores applied to resource type (default: technical_services). Allowed values: technical_services")
+
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	client := c.Meta.Client()
+	opts := pagerduty.ListMultiResourcesStandardScoresOptions{
+		IDs: resIDs,
+	}
+	if res, err := client.ListMultiResourcesStandardScores(context.Background(), resourceType, opts); err != nil {
+		log.Error(err)
+		return -1
+	} else {
+		for i, r := range res.Resources {
+			if i > 0 {
+				fmt.Println("---")
+			}
+			data, err := yaml.Marshal(r)
+			if err != nil {
+				log.Error(err)
+				return -1
+			}
+			fmt.Println(string(data))
+		}
+	}
+	return 0
+}
diff --git a/command/standard_resource_scores_list.go b/command/standard_resource_scores_list.go
new file mode 100644
index 0000000..2026665
--- /dev/null
+++ b/command/standard_resource_scores_list.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/mitchellh/cli"
+	log "github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v2"
+)
+
+type StandardListResourceScores struct {
+	Meta
+}
+
+func (c *StandardListResourceScores) Help() string {
+	helpText := `
+	pd standard resource-scores list List all of the existing standards scores for a specific resource
+
+	Options:
+
+    -id
+    -resource-type    List scores applied to resource type (default: technical_services). Allowed values: technical_services
+
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *StandardListResourceScores) Synopsis() string {
+	return "List all of the existing standards scores applied to a specific resource"
+}
+
+func StandardListResourceScoresCommand() (cli.Command, error) {
+	return &StandardListResourceScores{}, nil
+}
+
+func (c *StandardListResourceScores) Run(args []string) int {
+	var resID string
+	var resourceType string
+
+	flags := c.Meta.FlagSet("standard resource-scores list")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	flags.StringVar(&resID, "id", "", "Resource id")
+	flags.StringVar(&resourceType, "resource-type", "technical_services", "Scores applied to resource type (default: technical_services). Allowed values: technical_services")
+
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	client := c.Meta.Client()
+	if res, err := client.ListResourceStandardScores(context.Background(), resID, resourceType); err != nil {
+		log.Error(err)
+		return -1
+	} else {
+		data, err := yaml.Marshal(res)
+		if err != nil {
+			log.Error(err)
+			return -1
+		}
+		fmt.Println(string(data))
+	}
+	return 0
+}
diff --git a/command/standard_update.go b/command/standard_update.go
new file mode 100644
index 0000000..02e8171
--- /dev/null
+++ b/command/standard_update.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/PagerDuty/go-pagerduty"
+	log "github.com/sirupsen/logrus"
+
+	"github.com/mitchellh/cli"
+)
+
+type StandardUpdate struct {
+	Meta
+}
+
+func StandardUpdateCommand() (cli.Command, error) {
+	return &StandardUpdate{}, nil
+}
+
+func (c *StandardUpdate) Help() string {
+	helpText := `
+	pd standard update <FILE> Update an standard from json file
+	Options:
+
+		 -id
+
+	` + c.Meta.Help()
+	return strings.TrimSpace(helpText)
+}
+
+func (c *StandardUpdate) Synopsis() string {
+	return "Update an existing standard"
+}
+
+func (c *StandardUpdate) Run(args []string) int {
+	var standardID string
+
+	flags := c.Meta.FlagSet("standard update")
+	flags.Usage = func() { fmt.Println(c.Help()) }
+	flags.StringVar(&standardID, "id", "", "standard id")
+	if err := flags.Parse(args); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if err := c.Meta.Setup(); err != nil {
+		log.Error(err)
+		return -1
+	}
+	if standardID == "" {
+		log.Error("You must provide standard id")
+		return -1
+	}
+
+	client := c.Meta.Client()
+	var standard pagerduty.Standard
+	if len(flags.Args()) != 1 {
+		log.Error("Please specify input json file")
+		return -1
+	}
+
+	log.Info("Input file is:", flags.Arg(0))
+	f, err := os.ReadFile(flags.Arg(0))
+	if err != nil {
+		log.Error(err)
+		return -1
+	}
+
+	if err := json.Unmarshal(f, &standard); err != nil {
+		log.Errorln("Failed to decode json. Error:", err)
+		return -1
+	}
+
+	log.Debugf("%#v", standard)
+	if _, err := client.UpdateStandard(context.Background(), standardID, standard); err != nil {
+		log.Error(err)
+		return -1
+	}
+
+	return 0
+}
diff --git a/standard.go b/standard.go
new file mode 100644
index 0000000..792dde8
--- /dev/null
+++ b/standard.go
@@ -0,0 +1,148 @@
+package pagerduty
+
+import (
+	"context"
+
+	"github.com/google/go-querystring/query"
+)
+
+const (
+	standardPath = "/standards"
+)
+
+// Standard defines a PagerDuty resource standard.
+type Standard struct {
+	Active       bool                         `json:"active"`
+	Description  string                       `json:"description,omitempty"`
+	Exclusions   []StandardInclusionExclusion `json:"exclusions,omitempty"`
+	ID           string                       `json:"id,omitempty"`
+	Inclusions   []StandardInclusionExclusion `json:"inclusions,omitempty"`
+	Name         string                       `json:"name,omitempty"`
+	ResourceType string                       `json:"resource_type,omitempty"`
+	Type         string                       `json:"type,omitempty"`
+}
+
+type StandardInclusionExclusion struct {
+	Type string `json:"type,omitempty"`
+	ID   string `json:"id,omitempty"`
+}
+
+// ListStandardsResponse is the data structure returned from calling the ListStandards API endpoint.
+type ListStandardsResponse struct {
+	Standards []Standard `json:"standards"`
+}
+
+// ListStandardsOptions is the data structure used when calling the ListStandards API endpoint.
+type ListStandardsOptions struct {
+	Active bool `url:"active,omitempty"`
+
+	// ResourceType query for a specific resource type.
+	//  Allowed value: technical_service
+	ResourceType string `url:"resource_type,omitempty"`
+}
+
+type ResourceStandardScore struct {
+	ResourceID   string             `json:"resource_id,omitempty"`
+	ResourceType string             `json:"resource_type,omitempty"`
+	Score        *ResourceScore     `json:"score,omitempty"`
+	Standards    []ResourceStandard `json:"standards,omitempty"`
+}
+
+type ResourceScore struct {
+	Passing int `json:"passing,omitempty"`
+	Total   int `json:"total,omitempty"`
+}
+
+type ResourceStandard struct {
+	Active      bool   `json:"active"`
+	Description string `json:"description,omitempty"`
+	ID          string `json:"id,omitempty"`
+	Name        string `json:"name,omitempty"`
+	Pass        bool   `json:"pass"`
+	Type        string `json:"type,omitempty"`
+}
+
+type ListMultiResourcesStandardScoresResponse struct {
+	Resources []ResourceStandardScore `json:"resources,omitempty"`
+}
+
+type ListMultiResourcesStandardScoresOptions struct {
+	// Ids of resources to apply the standards. Maximum of 100 items
+	IDs []string `url:"ids,omitempty,brackets"`
+}
+
+// ListStandards lists all the existing standards.
+func (c *Client) ListStandards(ctx context.Context, o ListStandardsOptions) (*ListStandardsResponse, error) {
+	v, err := query.Values(o)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.get(ctx, standardPath+"?"+v.Encode())
+	if err != nil {
+		return nil, err
+	}
+
+	var result ListStandardsResponse
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// UpdateStandard updates an existing standard.
+func (c *Client) UpdateStandard(ctx context.Context, id string, s Standard) (*Standard, error) {
+	resp, err := c.put(ctx, standardPath+"/"+id, s, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result Standard
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// ListResourceStandardScores
+//
+//	rt - Resource type
+//	Allowed values: technical_services
+func (c *Client) ListResourceStandardScores(ctx context.Context, id string, rt string) (*ResourceStandardScore, error) {
+	resp, err := c.get(ctx, standardPath+"/scores/"+rt+"/"+id)
+	if err != nil {
+		return nil, err
+	}
+
+	var result ResourceStandardScore
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// ListMultiResourcesStandardScores
+//
+//	rt - Resource type
+//	Allowed values: technical_services
+func (c *Client) ListMultiResourcesStandardScores(ctx context.Context, rt string, o ListMultiResourcesStandardScoresOptions) (*ListMultiResourcesStandardScoresResponse, error) {
+	v, err := query.Values(o)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.get(ctx, standardPath+"/scores/"+rt+"?"+v.Encode())
+	if err != nil {
+		return nil, err
+	}
+
+	var result ListMultiResourcesStandardScoresResponse
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}


Test patch:
diff --git a/standard_test.go b/standard_test.go
new file mode 100644
index 0000000..f83a1f1
--- /dev/null
+++ b/standard_test.go
@@ -0,0 +1,158 @@
+package pagerduty
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestStandard_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/standards", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{"standards":[{"active":true,"description":"A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.","id":"1","inclusions":[{"type":"technical_service_reference","id":"1"}],"name":"Service has a description","resource_type":"technical_service","type":"has_technical_service_description"}]}`))
+	})
+
+	var opts ListStandardsOptions
+	client := defaultTestClient(server.URL, standardPath)
+
+	res, err := client.ListStandards(context.Background(), opts)
+
+	want := &ListStandardsResponse{
+		Standards: []Standard{
+			{
+				Active:      true,
+				ID:          "1",
+				Description: "A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.",
+				Inclusions: []StandardInclusionExclusion{
+					{
+						ID:   "1",
+						Type: "technical_service_reference",
+					},
+				},
+				Name:         "Service has a description",
+				ResourceType: "technical_service",
+				Type:         "has_technical_service_description",
+			},
+		},
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestStandard_Update(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/standards/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		_, _ = w.Write([]byte(`{"active":true,"description":"A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.","id":"1","inclusions":[{"type":"technical_service_reference","id":"1"}],"name":"Service has a description","resource_type":"technical_service","type":"has_technical_service_description"}`))
+	})
+
+	client := defaultTestClient(server.URL, "foo")
+	input := &Standard{Active: true}
+	want := &Standard{
+		Active:      true,
+		ID:          "1",
+		Description: "A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.",
+		Inclusions: []StandardInclusionExclusion{
+			{
+				ID:   "1",
+				Type: "technical_service_reference",
+			},
+		},
+		Name:         "Service has a description",
+		ResourceType: "technical_service",
+		Type:         "has_technical_service_description",
+	}
+	res, err := client.UpdateStandard(context.Background(), "1", *input)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestStandard_ListTechniServiceStandardScores(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/standards/scores/technical_services/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{"resource_id":"1","resource_type":"technical_service","score":{"passing":1,"total":1},"standards":[{"active":true,"description":"A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.","id":"01CXX38Q0U8XKHO4LNKXUJTBFG","pass":true,"name":"Service has a description","type":"has_technical_service_description"}]}`))
+	})
+
+	client := defaultTestClient(server.URL, standardPath)
+
+	res, err := client.ListResourceStandardScores(context.Background(), "1", "technical_services")
+
+	want := &ResourceStandardScore{
+		ResourceID:   "1",
+		ResourceType: "technical_service",
+		Score: &ResourceScore{
+			Passing: 1,
+			Total:   1,
+		},
+		Standards: []ResourceStandard{
+			{
+				Active:      true,
+				Description: "A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.",
+				ID:          "01CXX38Q0U8XKHO4LNKXUJTBFG",
+				Pass:        true,
+				Name:        "Service has a description",
+				Type:        "has_technical_service_description",
+			},
+		},
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestStandard_ListManyTechniServiceStandardScores(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/standards/scores/technical_services", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{"resources":[{"resource_id":"1","resource_type":"technical_service","score":{"passing":1,"total":1},"standards":[{"active":true,"description":"A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.","id":"01CXX38Q0U8XKHO4LNKXUJTBFG","pass":true,"name":"Service has a description","type":"has_technical_service_description"}]}]}`))
+	})
+
+	opts := ListMultiResourcesStandardScoresOptions{
+		IDs: []string{"1"},
+	}
+	client := defaultTestClient(server.URL, standardPath)
+
+	res, err := client.ListMultiResourcesStandardScores(context.Background(), "technical_services", opts)
+
+	want := &ListMultiResourcesStandardScoresResponse{
+		Resources: []ResourceStandardScore{
+			{
+				ResourceID:   "1",
+				ResourceType: "technical_service",
+				Score: &ResourceScore{
+					Passing: 1,
+					Total:   1,
+				},
+				Standards: []ResourceStandard{
+					{
+						Active:      true,
+						Description: "A description provides critical context about what a service represents or is used for to inform team members and responders. The description should be kept concise and understandable by those without deep knowledge of the service.",
+						ID:          "01CXX38Q0U8XKHO4LNKXUJTBFG",
+						Pass:        true,
+						Name:        "Service has a description",
+						Type:        "has_technical_service_description",
+					},
+				},
+			},
+		},
+	}
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}