Skip to content

Commit

Permalink
resourcemanager/resourceids: adding a new SegmentNotSpecifiedError to…
Browse files Browse the repository at this point in the history
… allow providing better error messages when segments are missing during parsing

This means that the error messages will change from:

```
parsing segment "staticMicrosoftServiceBus": expected the segment "microsoft.servicebus" to be "Microsoft.ServiceBus"
```

to:

```
parsing the Queue ID: the segment "staticMicrosoftServiceBus" was not found

Expected a Queue ID that matched:

> /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.ServiceBus/namespaces/namespaceValue/queues/queueValue

However this value was provided:

> /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/my-resource-group/providers/microsoft.servicebus/namespaces/my-service-bus/queues/my-test-queue

However the parsed Resource ID was missing a value for the "staticMicrosoftServiceBus" segment
(which should be the name of the Resource Provider [for example 'Microsoft.ServiceBus']).
```

Which should help better explain the issue that occurred when parsing the Resource ID
  • Loading branch information
tombuildsstuff committed May 8, 2023
1 parent 90d1467 commit 7b399a1
Show file tree
Hide file tree
Showing 5 changed files with 589 additions and 38 deletions.
143 changes: 143 additions & 0 deletions resourcemanager/resourceids/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package resourceids

import (
"fmt"
"reflect"
"strings"
)

var _ error = SegmentNotSpecifiedError{}

type SegmentNotSpecifiedError struct {
parseResult ParseResult
resourceId ResourceId
resourceIdName string
segmentName string
}

// NewSegmentNotSpecifiedError returns a SegmentNotSpecifiedError for the provided Resource ID, segment and parseResult combination
func NewSegmentNotSpecifiedError(id ResourceId, segmentName string, parseResult ParseResult) SegmentNotSpecifiedError {
// Resource ID types must be in the format {Name}Id
resourceIdName := strings.TrimSuffix(reflect.ValueOf(id).Type().Name(), "Id")
return SegmentNotSpecifiedError{
resourceIdName: resourceIdName,
resourceId: id,
segmentName: segmentName,
parseResult: parseResult,
}
}

// Error returns a detailed error message highlighting the issues found when parsing this Resource ID Segment.
func (e SegmentNotSpecifiedError) Error() string {
expectedId := e.buildExpectedResourceId()
description, err := e.descriptionForSegment(e.segmentName)
if err != nil {
// this isn't user recoverable, and will be caught at unit/acceptance test time, so `panic` is reasonable here
return fmt.Sprintf("internal-error: building description for segment: %+v", err)
}

return fmt.Sprintf(`parsing the %[1]s ID: the segment %[2]q was not found
Expected a %[1]s ID that matched:
> %[3]s
However this value was provided:
> %[4]s
However the parsed Resource ID was missing a value for the %[2]q segment
(%[5]s).
`, e.resourceIdName, e.segmentName, expectedId, e.parseResult.RawInput, *description)
}

// descriptionForSegment returns a friendly description for the Segment specified in segmentName
func (e SegmentNotSpecifiedError) descriptionForSegment(segmentName string) (*string, error) {
// NOTE: do not use round brackets within these error messages, since this description can be contained within one
for _, segment := range e.resourceId.Segments() {
if segment.Name != segmentName {
continue
}

switch segment.Type {
case ConstantSegmentType:
{
if segment.PossibleValues == nil {
return nil, fmt.Errorf("the Segment %q defined a Constant with no PossibleValues", segment.Name)
}

// intentionally format these as quoted string values
values := make([]string, 0)
for _, v := range *segment.PossibleValues {
values = append(values, fmt.Sprintf("%q", v))
}

msg := fmt.Sprintf("which should be a Constant with one of the following values [%s]", strings.Join(values, ", "))
return &msg, nil
}

case ResourceGroupSegmentType:
{
msg := "which should be the name of the Resource Group"
return &msg, nil
}

case ResourceProviderSegmentType:
{
msg := fmt.Sprintf("which should be the name of the Resource Provider [for example '%s']", segment.ExampleValue)
return &msg, nil
}

case ScopeSegmentType:
{
msg := fmt.Sprintf("which specifies the Resource ID that should be used as a Scope [for example '%s']", segment.ExampleValue)
return &msg, nil
}

case StaticSegmentType:
{
if segment.FixedValue == nil {
return nil, fmt.Errorf("the Segment %q defined a Static Segment with no FixedValue", segment.Name)
}
msg := fmt.Sprintf("which should be the literal value %q", *segment.FixedValue)
return &msg, nil
}

case SubscriptionIdSegmentType:
{
msg := "which should be the UUID of the Azure Subscription"
return &msg, nil
}

case UserSpecifiedSegmentType:
{
name := strings.TrimSuffix(segment.Name, "Name")
msg := fmt.Sprintf("which should be the user specified value for this %s [for example %q]", name, segment.ExampleValue)
return &msg, nil
}

default:
{
return nil, fmt.Errorf("internal-error: the Segment Type %q was not implemented for Segment %q", string(segment.Type), segment.Name)
}
}
}

return nil, fmt.Errorf("the segment %q was not defined for this Resource ID", segmentName)
}

// buildExpectedResourceId iterates over the Resource ID to build up the "expected" value for the Resource ID
// this is done using the example segment values for each segment type.
func (e SegmentNotSpecifiedError) buildExpectedResourceId() string {
segments := make([]string, 0)
for _, v := range e.resourceId.Segments() {
segments = append(segments, strings.TrimPrefix(v.ExampleValue, "/"))
}

out := strings.Join(segments, "/")
return fmt.Sprintf("/%s", out)
}
Loading

0 comments on commit 7b399a1

Please sign in to comment.