Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

commonids: add composite resource id #208

Merged
merged 4 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions resourcemanager/commonids/composite_resource_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package commonids

import (
"fmt"
"strings"

"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids"
)

// CompositeResourceID is a struct representing the Resource ID for a Composite Resource Id
type CompositeResourceID[T1 resourceids.ResourceId, T2 resourceids.ResourceId] struct {
First T1
Second T2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor but could we document these fields so that it's clearer what these are?

Suggested change
First T1
Second T2
// First specifies the first component of this Resource ID.
// This is in the format `{first}|{second}`.
First T1
// Second specifies the second component of this Resource ID
// This is in the format `{first}|{second}`.
Second T2

}

// ID returns the formatted Composite Resource Id
func (id CompositeResourceID[T1, T2]) ID() string {
fmtString := "%s|%s"
return fmt.Sprintf(fmtString, id.First.ID(), id.Second.ID())
}

// String returns a human-readable description of this Composite Resource Id
func (id CompositeResourceID[T1, T2]) String() string {
fmtString := "%s\n%s"
return fmt.Sprintf(fmtString, id.First.String(), id.Second.String())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call out that this is a Composite Resource ID:

Suggested change
// String returns a human-readable description of this Composite Resource Id
func (id CompositeResourceID[T1, T2]) String() string {
fmtString := "%s\n%s"
return fmt.Sprintf(fmtString, id.First.String(), id.Second.String())
}
// String returns a human-readable description of this Composite Resource Id
func (id CompositeResourceID[T1, T2]) String() string {
fmtString := "Composite Resource ID (%s | %s)"
return fmt.Sprintf(fmtString, id.First.String(), id.Second.String())
}


// ParseCompositeResourceID parses 'input' and two ResourceIds (first,second) into a CompositeResourceID
// The 'input' should be a string containing 2 resource ids separated by "|"
// eg: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Sql/servers/serverValue"
// The first and second ResourceIds should match the types in the 'input' string in the order in which they appear
// eg:
//
// input := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Sql/servers/serverValue"
// first := ResourceGroupId{}
// second := SqlServerId{}
// id, err := ParseCompositeResourceID(input, &first, &second)
func ParseCompositeResourceID[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2) (*CompositeResourceID[T1, T2], error) {
return parse(input, first, second, false)
}

// ParseCompositeResourceIDInsensitively parses 'input' and two ResourceIds (first,second) case-insensitively into a CompositeResourceID
// note: this method should only be used for API response data and not user input
func ParseCompositeResourceIDInsensitively[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2) (*CompositeResourceID[T1, T2], error) {
return parse(input, first, second, true)
}

func parse[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2, insensitively bool) (*CompositeResourceID[T1, T2], error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor but could we update parse to be parseCompositeResourceID so that this isn't used unintentionally?

Suggested change
return parse(input, first, second, false)
}
// ParseCompositeResourceIDInsensitively parses 'input' and two ResourceIds (first,second) case-insensitively into a CompositeResourceID
// note: this method should only be used for API response data and not user input
func ParseCompositeResourceIDInsensitively[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2) (*CompositeResourceID[T1, T2], error) {
return parse(input, first, second, true)
}
func parse[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2, insensitively bool) (*CompositeResourceID[T1, T2], error) {
return parseCompositeResourceID(input, first, second, false)
}
// ParseCompositeResourceIDInsensitively parses 'input' and two ResourceIds (first,second) case-insensitively into a CompositeResourceID
// note: this method should only be used for API response data and not user input
func ParseCompositeResourceIDInsensitively[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2) (*CompositeResourceID[T1, T2], error) {
return parseCompositeResourceID(input, first, second, true)
}
func parseCompositeResourceID[T1 resourceids.ResourceId, T2 resourceids.ResourceId](input string, first T1, second T2, insensitively bool) (*CompositeResourceID[T1, T2], error) {


components := strings.Split(input, "|")
if len(components) != 2 {
return nil, fmt.Errorf("expected 2 resourceids but got %d", len(components))
}

output := CompositeResourceID[T1, T2]{
First: first,
Second: second,
}

// Parse the first of the two Resource IDs from the components
firstParser := resourceids.NewParserFromResourceIdType(output.First)
firstParseResult, err := firstParser.Parse(components[0], insensitively)
if err != nil {
return nil, fmt.Errorf("parsing first id part %q of CompositeResourceID: %v", components[0], err)
}
err = output.First.FromParseResult(*firstParseResult)
if err != nil {
return nil, fmt.Errorf("populating first id part %q of CompositeResourceID: %v", components[0], err)
}

// Parse the second of the two Resource IDs from the components
secondParser := resourceids.NewParserFromResourceIdType(output.Second)
secondParseResult, err := secondParser.Parse(components[1], insensitively)
if err != nil {
return nil, fmt.Errorf("parsing second id part %q of CompositeResourceID: %v", components[1], err)
}
err = output.Second.FromParseResult(*secondParseResult)
if err != nil {
return nil, fmt.Errorf("populating second id part %q of CompositeResourceID: %v", components[1], err)
}

return &output, nil
}
155 changes: 155 additions & 0 deletions resourcemanager/commonids/composite_resource_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package commonids

import (
"fmt"
"testing"
)

func TestCompositeResourceID(t *testing.T) {
idString := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/sites/siteValue"

id1 := BotServiceId{}
id2 := AppServiceId{}
id, err := ParseCompositeResourceID(idString, &id1, &id2)
if err != nil {
t.Fatalf("Expected CompositeResourceID to parse successfully but got Error: %q", err)
}

expectedId1 := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue"
actualId1 := id.First.ID()
if expectedId1 != actualId1 {
t.Fatalf("Expected the First ID to be %q but got %q", expectedId1, actualId1)
}

expectedId2 := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/sites/siteValue"
actualId2 := id.Second.ID()
if expectedId2 != actualId2 {
t.Fatalf("Expected the Second ID to be %q but got %q", expectedId2, actualId2)
}

if idString != id.ID() {
t.Fatalf("Expected the Composite ID to be %q but got %q", idString, id.ID())
}

expectedIdString1 := `Bot Service (Subscription: "12345678-1234-9876-4563-123456789012"
Resource Group Name: "example-resource-group"
Bot Service Name: "botServiceValue")`
actualIdString1 := id.First.String()
if expectedIdString1 != actualIdString1 {
t.Fatalf("Expected the First ID String to be %q but got %q", expectedIdString1, actualIdString1)
}

expectedIdString2 := `App Service (Subscription: "12345678-1234-9876-4563-123456789012"
Resource Group Name: "example-resource-group"
Site Name: "siteValue")`
actualIdString2 := id.Second.String()
if expectedIdString2 != actualIdString2 {
t.Fatalf("Expected the Second ID String to be %q but got %q", expectedIdString2, actualIdString2)
}

expectedCompositeString := fmt.Sprintf("%s\n%s", expectedIdString1, expectedIdString2)
actualCompositeString := id.String()
if expectedCompositeString != actualCompositeString {
t.Fatalf("Expected the Composite ID String to be %q but got %q", expectedCompositeString, actualCompositeString)
}
}

func TestParseCompositeResourceIDInsensitively(t *testing.T) {
idIncorrectCasing := "/subscriptions/12345678-1234-9876-4563-123456789012/ReSoUrCeGrOuPs/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/ReSoUrCeGrOuPs/example-resource-group/providers/Microsoft.Web/sites/siteValue"
idCorrectCasing := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/sites/siteValue"

id1 := BotServiceId{}
id2 := AppServiceId{}
id, err := ParseCompositeResourceIDInsensitively(idIncorrectCasing, &id1, &id2)
if err != nil {
t.Fatalf("Expected CompositeResourceID to parse successfully but got Error: %q", err)
}

expectedId1 := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue"
actualId1 := id.First.ID()
if expectedId1 != actualId1 {
t.Fatalf("Expected the First ID to be %q but got %q", expectedId1, actualId1)
}

expectedId2 := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/sites/siteValue"
actualId2 := id.Second.ID()
if expectedId2 != actualId2 {
t.Fatalf("Expected the Second ID to be %q but got %q", expectedId2, actualId2)
}

if idCorrectCasing != id.ID() {
t.Fatalf("Expected the Composite ID to be %q but got %q", idIncorrectCasing, id.ID())
}

expectedIdString1 := `Bot Service (Subscription: "12345678-1234-9876-4563-123456789012"
Resource Group Name: "example-resource-group"
Bot Service Name: "botServiceValue")`
actualIdString1 := id.First.String()
if expectedIdString1 != actualIdString1 {
t.Fatalf("Expected the First ID String to be %q but got %q", expectedIdString1, actualIdString1)
}

expectedIdString2 := `App Service (Subscription: "12345678-1234-9876-4563-123456789012"
Resource Group Name: "example-resource-group"
Site Name: "siteValue")`
actualIdString2 := id.Second.String()
if expectedIdString2 != actualIdString2 {
t.Fatalf("Expected the Second ID String to be %q but got %q", expectedIdString2, actualIdString2)
}

expectedCompositeString := fmt.Sprintf("%s\n%s", expectedIdString1, expectedIdString2)
actualCompositeString := id.String()
if expectedCompositeString != actualCompositeString {
t.Fatalf("Expected the Composite ID String to be %q but got %q", expectedCompositeString, actualCompositeString)
}
}

func TestCompositeResourceIDNumberOfIDsErrors(t *testing.T) {
id1 := BotServiceId{}
id2 := AppServiceId{}
testData := []struct {
Input string
ExpectedError string
}{
{
// 1 ID
Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue",
ExpectedError: "expected 2 resourceids but got 1",
},
{
// 3 IDs
Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue",
ExpectedError: "expected 2 resourceids but got 3",
},
}

for _, v := range testData {
t.Logf("[DEBUG] Testing %q", v.Input)

_, err := ParseCompositeResourceID(v.Input, &id1, &id2)
if err == nil {
t.Fatalf("Expected an error but didn't get one")
}
if err.Error() != v.ExpectedError {
t.Fatalf("Expected error %q but got %q", v.ExpectedError, err.Error())
}
}
}

func TestCompositeResourceIDInvalidIDs(t *testing.T) {

id1 := BotServiceId{}
id2 := AppServiceId{}

idStringFirstInvalid := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/sites/siteValue"
_, err := ParseCompositeResourceID(idStringFirstInvalid, &id1, &id2)
if err == nil {
t.Fatalf("Expected error but didn't get one")
}

idStringSecondInvalid := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.BotService/botServices/botServiceValue|/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web"
_, err = ParseCompositeResourceID(idStringSecondInvalid, &id1, &id2)
if err == nil {
t.Fatalf("Expected error but didn't get one")
}
}