Skip to content

Commit

Permalink
Stop using strings.Split in urn.go (#15669)
Browse files Browse the repository at this point in the history
# Description

`strings.Split` is being used to extract delimited components of the
urn, this is inefficient when only one component is being accessed.

This changes to scan the urn string to find the required component, this
is measurably more efficient.

<!--- 
Thanks so much for your contribution! If this is your first time
contributing, please ensure that you have read the
[CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md)
documentation.
-->

<!--- Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context. -->


## Checklist

- [X] I have run `make tidy` to update any new dependencies
- [X] I have run `make lint` to verify my code passes the lint check
  - [ ] I have formatted my code using `gofumpt`

<!--- Please provide details if the checkbox below is to be left
unchecked. -->
- [X] I have added tests that prove my fix is effective or that my
feature works
<!--- 
User-facing changes require a CHANGELOG entry.
-->
- [ ] I have run `make changelog` and committed the
`changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the
Pulumi Cloud,
then the service should honor older versions of the CLI where this
change would not exist.
You must then bump the API version in
/pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi
Cloud API version
<!-- @pulumi employees: If yes, you must submit corresponding changes in
the service repo. -->

---------

Co-authored-by: Paul Roberts <proberts@pulumi.com>
  • Loading branch information
PollRobots and Paul Roberts committed Mar 14, 2024
1 parent 72ce48a commit 7b791a6
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 11 deletions.
58 changes: 47 additions & 11 deletions sdk/go/common/resource/urn.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ func (urn URN) IsValid() bool {
return false
}

split := strings.SplitN(string(urn), URNNameDelimiter, 4)
return len(split) == 4
return strings.Count(string(urn), URNNameDelimiter) >= 3
// TODO: We should validate the stack, project and type tokens here, but currently those fields might not
// actually be "valid" (e.g. spaces in project names, custom component types, etc).
}
Expand All @@ -123,31 +122,68 @@ func (urn URN) URNName() string {

// Stack returns the resource stack part of a URN.
func (urn URN) Stack() tokens.QName {
return tokens.QName(strings.Split(urn.URNName(), URNNameDelimiter)[0])
return tokens.QName(getComponent(urn.URNName(), URNNameDelimiter, 0))
}

// Project returns the project name part of a URN.
func (urn URN) Project() tokens.PackageName {
return tokens.PackageName(strings.Split(urn.URNName(), URNNameDelimiter)[1])
return tokens.PackageName(getComponent(urn.URNName(), URNNameDelimiter, 1))
}

// QualifiedType returns the resource type part of a URN including the parent type
func (urn URN) QualifiedType() tokens.Type {
return tokens.Type(strings.Split(urn.URNName(), URNNameDelimiter)[2])
return tokens.Type(getComponent(urn.URNName(), URNNameDelimiter, 2))
}

// Gets the n'th delimited component of a string.
//
// This is used instead of the `strings.Split(string, delimiter)[index]` pattern which
// is inefficient.
func getComponent(input string, delimiter string, index int) string {
return getComponentN(input, delimiter, index, false)
}

// This gets the n'th delimited compnent of a string, and optionally the rest of the string
//
// If the *open* parameter is true, then this will return everything after the n-1th delimiter
func getComponentN(input string, delimiter string, index int, open bool) string {
if open && index == 0 {
return input
}
nameDelimiters := 0
partStart := 0
for i := 0; i < len(input); i++ {
if strings.HasPrefix(input[i:], delimiter) {
nameDelimiters++
if nameDelimiters == index {
i += len(delimiter)
partStart = i
if open {
return input[partStart:]
}
i--
} else if nameDelimiters > index {
return input[partStart:i]
} else {
i += len(delimiter) - 1
}
}
}
return input[partStart:]
}

// Type returns the resource type part of a URN
func (urn URN) Type() tokens.Type {
qualifiedType := strings.Split(urn.URNName(), URNNameDelimiter)[2]
types := strings.Split(qualifiedType, URNTypeDelimiter)
lastType := types[len(types)-1]
return tokens.Type(lastType)
name := urn.URNName()
qualifiedType := getComponent(name, URNNameDelimiter, 2)

lastTypeDelimiter := strings.LastIndex(qualifiedType, URNTypeDelimiter)
return tokens.Type(qualifiedType[lastTypeDelimiter+1:])
}

// Name returns the resource name part of a URN.
func (urn URN) Name() string {
split := strings.SplitN(urn.URNName(), URNNameDelimiter, 4)
return split[3]
return getComponentN(urn.URNName(), URNNameDelimiter, 3, true)
}

// Returns a new URN with an updated name part
Expand Down
251 changes: 251 additions & 0 deletions sdk/go/common/resource/urn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
package resource

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
Expand Down Expand Up @@ -85,3 +87,252 @@ func TestIsValid(t *testing.T) {
assert.True(t, urn.IsValid(), "IsValid expected to be true: %v", urn)
}
}

func TestComponentAccess(t *testing.T) {
t.Parallel()

type ComponentTestCase struct {
urn string
expected string
}

t.Run("Stack component", func(t *testing.T) {
t.Parallel()

cases := []ComponentTestCase{
{urn: "urn:pulumi:stack::test::pulumi:pulumi:Stack::test-test", expected: "stack"},
{urn: "urn:pulumi:stack::::::", expected: "stack"},
{urn: "urn:pulumi:::test::pulumi:pulumi:Stack::test-test", expected: ""},
{urn: "urn:pulumi:::::::", expected: ""},
}

for _, test := range cases {
urn, err := ParseURN(test.urn)
require.NoError(t, err)
require.Equal(t, test.urn, string(urn))

assert.Equalf(t, tokens.QName(test.expected), urn.Stack(),
"Expecting stack to be '%v' from urn '%v'", test.expected, test.urn)
}
})

t.Run("Project component", func(t *testing.T) {
t.Parallel()

cases := []ComponentTestCase{
{urn: "urn:pulumi:stack::proj::pulumi:pulumi:Stack::test-test", expected: "proj"},
{urn: "urn:pulumi:::proj::::", expected: "proj"},
{urn: "urn:pulumi:stack::::pulumi:pulumi:Stack::test-test", expected: ""},
{urn: "urn:pulumi:::::::", expected: ""},
}

for _, test := range cases {
urn, err := ParseURN(test.urn)
require.NoError(t, err)
require.Equal(t, test.urn, string(urn))

assert.Equalf(t, tokens.PackageName(test.expected), urn.Project(),
"Expecting project to be '%v' from urn '%v'", test.expected, test.urn)
}
})

t.Run("QualifiedType component", func(t *testing.T) {
t.Parallel()

cases := []ComponentTestCase{
{urn: "urn:pulumi:stack::proj::qualified$type::test-test", expected: "qualified$type"},
{urn: "urn:pulumi:::::qualified$type::", expected: "qualified$type"},
{urn: "urn:pulumi:stack::proj::::test-test", expected: ""},
{urn: "urn:pulumi:::::::", expected: ""},
}

for _, test := range cases {
urn, err := ParseURN(test.urn)
require.NoError(t, err)
require.Equal(t, test.urn, string(urn))

assert.Equalf(t, tokens.Type(test.expected), urn.QualifiedType(),
"Expecting qualified type to be '%v' from urn '%v'", test.expected, test.urn)
}
})

t.Run("Type component", func(t *testing.T) {
t.Parallel()

cases := []ComponentTestCase{
{urn: "urn:pulumi:stack::proj::very$qualified$type::test-test", expected: "type"},
{urn: "urn:pulumi:::::very$qualified$type::", expected: "type"},
{urn: "urn:pulumi:stack::proj::qualified$type::test-test", expected: "type"},
{urn: "urn:pulumi:::::qualified$type::", expected: "type"},
{urn: "urn:pulumi:stack::proj::qualified-type::test-test", expected: "qualified-type"},
{urn: "urn:pulumi:::::qualified-type::", expected: "qualified-type"},
{urn: "urn:pulumi:stack::proj::::test-test", expected: ""},
{urn: "urn:pulumi:::::::", expected: ""},
}

for _, test := range cases {
urn, err := ParseURN(test.urn)
require.NoError(t, err)
require.Equal(t, test.urn, string(urn))

assert.Equalf(t, tokens.Type(test.expected), urn.Type(),
"Expecting type to be '%v' from urn '%v'", test.expected, test.urn)
}
})

t.Run("Name component", func(t *testing.T) {
t.Parallel()

cases := []ComponentTestCase{
{urn: "urn:pulumi:stack::proj::qualified$type::name", expected: "name"},
{urn: "urn:pulumi:::::::name", expected: "name"},
{urn: "urn:pulumi:stack::proj::qualified$type::", expected: ""},
{urn: "urn:pulumi:::::::", expected: ""},
{
urn: "urn:pulumi::stack::proj::type::a-longer-name",
expected: "a-longer-name",
},
{
urn: "urn:pulumi::stack::proj::type::a name with spaces",
expected: "a name with spaces",
},
{
urn: "urn:pulumi::stack::proj::type::a-name-with::a-name-separator",
expected: "a-name-with::a-name-separator",
},
{
urn: "urn:pulumi::stack::proj::type::a-name-with::many::name::separators",
expected: "a-name-with::many::name::separators",
},
}

for _, test := range cases {
urn, err := ParseURN(test.urn)
require.NoError(t, err)
require.Equal(t, test.urn, string(urn))

assert.Equalf(t, test.expected, urn.Name(),
"Expecting name to be '%v' from urn '%v'", test.expected, test.urn)
}
})
}

func TestParseURN(t *testing.T) {
t.Parallel()

t.Run("Positive Tests", func(t *testing.T) {
t.Parallel()

goodUrns := []string{
"urn:pulumi:test::test::pulumi:pulumi:Stack::test-test",
"urn:pulumi:stack-name::project-name::my:customtype$aws:s3/bucket:Bucket::bob",
"urn:pulumi:stack::project::type::",
"urn:pulumi:stack::project::type::some really ::^&\n*():: crazy name",
"urn:pulumi:stack::project with whitespace::type::some name",
}
for _, str := range goodUrns {
urn, err := ParseURN(str)
assert.NoErrorf(t, err, "Expecting %v to parse as a good urn", str)
assert.Equal(t, str, string(urn), "A parsed URN should be the same as the string that it was parsed from")
}
})

t.Run("Negative Tests", func(t *testing.T) {
t.Parallel()

t.Run("Empty String", func(t *testing.T) {
t.Parallel()

urn, err := ParseURN("")
assert.ErrorContains(t, err, "missing required URN")
assert.Empty(t, urn)
})

t.Run("Invalid URNs", func(t *testing.T) {
t.Parallel()

invalidUrns := []string{
"URN:PULUMI:TEST::TEST::PULUMI:PULUMI:STACK::TEST-TEST",
"urn:not-pulumi:stack-name::project-name::my:customtype$aws:s3/bucket:Bucket::bob",
"The quick brown fox",
"urn:pulumi:stack::too-few-elements",
}
for _, str := range invalidUrns {
urn, err := ParseURN(str)
assert.ErrorContainsf(t, err, "invalid URN", "Expecting %v to parse as an invalid urn")
assert.Empty(t, urn)
}
})
})
}

func TestParseOptionalURN(t *testing.T) {
t.Parallel()

t.Run("Positive Tests", func(t *testing.T) {
t.Parallel()

goodUrns := []string{
"urn:pulumi:test::test::pulumi:pulumi:Stack::test-test",
"urn:pulumi:stack-name::project-name::my:customtype$aws:s3/bucket:Bucket::bob",
"urn:pulumi:stack::project::type::",
"urn:pulumi:stack::project::type::some really ::^&\n*():: crazy name",
"urn:pulumi:stack::project with whitespace::type::some name",
"",
}
for _, str := range goodUrns {
urn, err := ParseOptionalURN(str)
assert.NoErrorf(t, err, "Expecting '%v' to parse as a good urn", str)
assert.Equal(t, str, string(urn))
}
})

t.Run("Invalid URNs", func(t *testing.T) {
t.Parallel()

invalidUrns := []string{
"URN:PULUMI:TEST::TEST::PULUMI:PULUMI:STACK::TEST-TEST",
"urn:not-pulumi:stack-name::project-name::my:customtype$aws:s3/bucket:Bucket::bob",
"The quick brown fox",
"urn:pulumi:stack::too-few-elements",
}
for _, str := range invalidUrns {
urn, err := ParseOptionalURN(str)
assert.ErrorContainsf(t, err, "invalid URN", "Expecting %v to parse as an invalid urn")
assert.Empty(t, urn)
}
})
}

func TestQuote(t *testing.T) {
t.Parallel()

urn, err := ParseURN("urn:pulumi:test::test::pulumi:pulumi:Stack::test-test")
require.NoError(t, err)
require.NotEmpty(t, urn)

expected := "'urn:pulumi:test::test::pulumi:pulumi:Stack::test-test'"
if runtime.GOOS == "windows" {
expected = "\"urn:pulumi:test::test::pulumi:pulumi:Stack::test-test\""
}

assert.Equal(t, expected, urn.Quote())
}

func TestRename(t *testing.T) {
t.Parallel()

stack := tokens.QName("stack")
proj := tokens.PackageName("foo/bar/baz")
parentType := tokens.Type("parent$type")
typ := tokens.Type("bang:boom/fizzle:MajorResource")
name := "a-swell-resource"

urn := NewURN(stack, proj, parentType, typ, name)
renamed := urn.Rename("a-better-resource")

assert.NotEqual(t, urn, renamed)
assert.Equal(t,
NewURN(stack, proj, parentType, typ, "a-better-resource"),
renamed)
}

0 comments on commit 7b791a6

Please sign in to comment.