Skip to content

Commit bc04cbf

Browse files
committed
feat: add alias generation and deduplication utilities
- GenerateUniqueAlias: Handle duplicate aliases with smart numbering - Extract base name from aliases with existing suffixes (e.g., "123_1" -> "123") - Find highest suffix and increment (e.g., "123_1" -> "123_2", not "123_1_1") - GenerateSmartAlias: Create intelligent aliases from host/user/port - Simplify domain names (remove www, extract meaningful parts) - Handle IP addresses appropriately - Skip common usernames (root, ubuntu, ec2-user, centos, azureuser, etc.) - Append non-standard ports to alias - Add comprehensive test coverage for edge cases
1 parent 70138d1 commit bc04cbf

File tree

3 files changed

+465
-0
lines changed

3 files changed

+465
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2025.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ui
16+
17+
import (
18+
"fmt"
19+
"strconv"
20+
"strings"
21+
)
22+
23+
// GenerateUniqueAlias generates a unique alias by appending a suffix if necessary
24+
func GenerateUniqueAlias(baseAlias string, existingAliases []string) string {
25+
// Build a map for faster lookup
26+
aliasMap := make(map[string]bool, len(existingAliases))
27+
for _, alias := range existingAliases {
28+
aliasMap[alias] = true
29+
}
30+
31+
// If the base alias is unique, return it
32+
if !aliasMap[baseAlias] {
33+
return baseAlias
34+
}
35+
36+
// Extract the base name and current suffix (if any)
37+
// e.g., "server_1" -> base: "server", suffix: 1
38+
// e.g., "server" -> base: "server", suffix: 0
39+
baseName := baseAlias
40+
41+
// Check if the alias already has a numeric suffix
42+
if lastUnderscore := strings.LastIndex(baseAlias, "_"); lastUnderscore != -1 {
43+
possibleSuffix := baseAlias[lastUnderscore+1:]
44+
if _, err := strconv.Atoi(possibleSuffix); err == nil {
45+
// It has a valid numeric suffix
46+
baseName = baseAlias[:lastUnderscore]
47+
}
48+
}
49+
50+
// Find the highest suffix number for this base name
51+
maxSuffix := 0
52+
basePattern := baseName + "_"
53+
54+
// Check all existing aliases to find the max suffix
55+
for _, alias := range existingAliases {
56+
if alias == baseName {
57+
// The base name without suffix exists, so we need at least _1
58+
if maxSuffix < 0 {
59+
maxSuffix = 0
60+
}
61+
} else if strings.HasPrefix(alias, basePattern) {
62+
suffixStr := strings.TrimPrefix(alias, basePattern)
63+
if suffix, err := strconv.Atoi(suffixStr); err == nil && suffix > maxSuffix {
64+
maxSuffix = suffix
65+
}
66+
}
67+
}
68+
69+
// Return the base name with the next available suffix
70+
return fmt.Sprintf("%s_%d", baseName, maxSuffix+1)
71+
}
72+
73+
// GenerateSmartAlias generates a smart alias from host, user, and port
74+
// It simplifies domain names and handles IP addresses intelligently
75+
func GenerateSmartAlias(host, user string, port int) string {
76+
alias := host
77+
78+
// Simplify common domain patterns
79+
// Remove www. prefix
80+
alias = strings.TrimPrefix(alias, "www.")
81+
82+
// For FQDN, extract the meaningful part
83+
// e.g., server.example.com -> server or example
84+
if strings.Contains(alias, ".") && !IsIPAddress(alias) {
85+
parts := strings.Split(alias, ".")
86+
// If it has subdomain, use the subdomain
87+
if len(parts) > 2 {
88+
// e.g., api.github.com -> api.github
89+
// or dev.server.example.com -> dev.server
90+
if parts[0] != "www" {
91+
alias = strings.Join(parts[:2], ".")
92+
} else {
93+
alias = parts[1] // Skip www
94+
}
95+
} else if len(parts) == 2 {
96+
// Simple domain like example.com -> example
97+
alias = parts[0]
98+
}
99+
}
100+
101+
// Optionally prepend user if it's not a common one
102+
if user != "" && !isCommonUser(user) {
103+
alias = fmt.Sprintf("%s@%s", user, alias)
104+
}
105+
106+
// Append port if non-standard
107+
if port != 0 && port != 22 {
108+
alias = fmt.Sprintf("%s:%d", alias, port)
109+
}
110+
111+
return alias
112+
}
113+
114+
// isCommonUser checks if a username is a common default username
115+
func isCommonUser(user string) bool {
116+
// Common usernames that don't need to be included in alias
117+
// These are default users for various cloud providers and Linux distributions
118+
commonUsers := map[string]bool{
119+
// Common administrative users
120+
"root": true,
121+
"admin": true,
122+
"administrator": true,
123+
124+
// Linux distribution default users
125+
"ubuntu": true, // Ubuntu
126+
"debian": true, // Debian
127+
"centos": true, // CentOS
128+
"fedora": true, // Fedora
129+
"alpine": true, // Alpine Linux
130+
"arch": true, // Arch Linux
131+
132+
// Cloud provider default users
133+
"ec2-user": true, // AWS Amazon Linux
134+
"azureuser": true, // Azure
135+
"opc": true, // Oracle Cloud
136+
"cloud-user": true, // Various cloud images
137+
"cloud_user": true, // Alternative format
138+
139+
// Container/orchestration platforms
140+
"core": true, // CoreOS
141+
"rancher": true, // RancherOS
142+
"docker": true, // Docker
143+
144+
// Other common defaults
145+
"user": true, // Generic
146+
"guest": true, // Guest account
147+
"vagrant": true, // Vagrant boxes
148+
}
149+
150+
return commonUsers[strings.ToLower(user)]
151+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright 2025.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ui
16+
17+
import "testing"
18+
19+
func TestGenerateUniqueAlias(t *testing.T) {
20+
tests := []struct {
21+
name string
22+
baseAlias string
23+
existingAliases []string
24+
want string
25+
}{
26+
{
27+
name: "no duplicates",
28+
baseAlias: "server",
29+
existingAliases: []string{"other", "another"},
30+
want: "server",
31+
},
32+
{
33+
name: "one duplicate",
34+
baseAlias: "server",
35+
existingAliases: []string{"server", "other"},
36+
want: "server_1",
37+
},
38+
{
39+
name: "multiple duplicates in sequence",
40+
baseAlias: "server",
41+
existingAliases: []string{"server", "server_1", "server_2"},
42+
want: "server_3",
43+
},
44+
{
45+
name: "duplicates with gap",
46+
baseAlias: "server",
47+
existingAliases: []string{"server", "server_1", "server_5"},
48+
want: "server_6",
49+
},
50+
{
51+
name: "empty existing aliases",
52+
baseAlias: "server",
53+
existingAliases: []string{},
54+
want: "server",
55+
},
56+
{
57+
name: "nil existing aliases",
58+
baseAlias: "server",
59+
existingAliases: nil,
60+
want: "server",
61+
},
62+
{
63+
name: "copying alias with suffix",
64+
baseAlias: "123_1",
65+
existingAliases: []string{"123", "123_1"},
66+
want: "123_2",
67+
},
68+
{
69+
name: "copying alias with suffix and gap",
70+
baseAlias: "server_2",
71+
existingAliases: []string{"server", "server_1", "server_2", "server_5"},
72+
want: "server_6",
73+
},
74+
{
75+
name: "alias with underscore but not numeric suffix",
76+
baseAlias: "my_server",
77+
existingAliases: []string{"my_server"},
78+
want: "my_server_1",
79+
},
80+
{
81+
name: "complex case with numeric alias",
82+
baseAlias: "123",
83+
existingAliases: []string{"123"},
84+
want: "123_1",
85+
},
86+
{
87+
name: "copy already suffixed numeric alias",
88+
baseAlias: "123_1",
89+
existingAliases: []string{"123", "123_1", "123_2"},
90+
want: "123_3",
91+
},
92+
}
93+
94+
for _, tt := range tests {
95+
t.Run(tt.name, func(t *testing.T) {
96+
got := GenerateUniqueAlias(tt.baseAlias, tt.existingAliases)
97+
if got != tt.want {
98+
t.Errorf("GenerateUniqueAlias() = %v, want %v", got, tt.want)
99+
}
100+
})
101+
}
102+
}
103+
104+
func TestGenerateSmartAlias(t *testing.T) {
105+
tests := []struct {
106+
name string
107+
host string
108+
user string
109+
port int
110+
want string
111+
}{
112+
{
113+
name: "simple domain",
114+
host: "example.com",
115+
user: "",
116+
port: 22,
117+
want: "example",
118+
},
119+
{
120+
name: "www prefix",
121+
host: "www.example.com",
122+
user: "",
123+
port: 22,
124+
want: "example",
125+
},
126+
{
127+
name: "subdomain",
128+
host: "api.github.com",
129+
user: "",
130+
port: 22,
131+
want: "api.github",
132+
},
133+
{
134+
name: "complex subdomain",
135+
host: "dev.server.example.com",
136+
user: "",
137+
port: 22,
138+
want: "dev.server",
139+
},
140+
{
141+
name: "IP address",
142+
host: "192.168.1.1",
143+
user: "",
144+
port: 22,
145+
want: "192.168.1.1",
146+
},
147+
{
148+
name: "with non-standard port",
149+
host: "example.com",
150+
user: "",
151+
port: 2222,
152+
want: "example:2222",
153+
},
154+
{
155+
name: "with custom user",
156+
host: "example.com",
157+
user: "developer",
158+
port: 22,
159+
want: "developer@example",
160+
},
161+
{
162+
name: "with common user (root)",
163+
host: "example.com",
164+
user: "root",
165+
port: 22,
166+
want: "example",
167+
},
168+
{
169+
name: "with common user (ubuntu)",
170+
host: "example.com",
171+
user: "ubuntu",
172+
port: 22,
173+
want: "example",
174+
},
175+
{
176+
name: "with common user (ec2-user)",
177+
host: "example.com",
178+
user: "ec2-user",
179+
port: 22,
180+
want: "example",
181+
},
182+
{
183+
name: "with common user (centos)",
184+
host: "example.com",
185+
user: "centos",
186+
port: 22,
187+
want: "example",
188+
},
189+
{
190+
name: "with common user (azureuser)",
191+
host: "example.com",
192+
user: "azureuser",
193+
port: 22,
194+
want: "example",
195+
},
196+
{
197+
name: "full combination",
198+
host: "api.github.com",
199+
user: "developer",
200+
port: 2222,
201+
want: "developer@api.github:2222",
202+
},
203+
{
204+
name: "IPv6 address",
205+
host: "2001:db8::1",
206+
user: "",
207+
port: 22,
208+
want: "2001:db8::1",
209+
},
210+
}
211+
212+
for _, tt := range tests {
213+
t.Run(tt.name, func(t *testing.T) {
214+
got := GenerateSmartAlias(tt.host, tt.user, tt.port)
215+
if got != tt.want {
216+
t.Errorf("GenerateSmartAlias() = %v, want %v", got, tt.want)
217+
}
218+
})
219+
}
220+
}

0 commit comments

Comments
 (0)