diff --git a/resourcemanager/commonids/composite_resource_id.go b/resourcemanager/commonids/composite_resource_id.go new file mode 100644 index 0000000..64dc91a --- /dev/null +++ b/resourcemanager/commonids/composite_resource_id.go @@ -0,0 +1,88 @@ +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 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 := "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 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 +} diff --git a/resourcemanager/commonids/composite_resource_id_test.go b/resourcemanager/commonids/composite_resource_id_test.go new file mode 100644 index 0000000..e4e279a --- /dev/null +++ b/resourcemanager/commonids/composite_resource_id_test.go @@ -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("Composite Resource ID (%s | %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("Composite Resource ID (%s | %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") + } +}