choksi2212 commented on code in PR #4497:
URL: https://github.com/apache/iggy/pull/4497#discussion_r4374700784


##########
foreign/go/client/connection_string.go:
##########
@@ -0,0 +1,466 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package client
+
+import (
+       "fmt"
+       "math/big"
+       "net"
+       "strconv"
+       "strings"
+
+       "github.com/apache/iggy/foreign/go/client/tcp"
+       ierror "github.com/apache/iggy/foreign/go/errors"
+)
+
+const (
+       defaultProtocol = "iggy"
+       protocolPrefix  = "iggy+"
+       tcpProtocol     = "tcp"
+)
+
+// ParseConnectionString parses an Iggy TCP connection string into the TCP
+// client options it describes, so the result can be passed to WithTcp:
+//
+//     client.NewIggyClient(client.WithTcp(opts...))
+//
+// with opts from 
client.ParseConnectionString("iggy://iggy:iggy@localhost:8090").
+//
+// The accepted shape is:
+//
+//     iggy://[username:password@ | personal_access_token@]host:port[?options]
+//     iggy+tcp://[username:password@ | 
personal_access_token@]host:port[?options]
+//
+// The scheme iggy:// means TCP, the same as the explicit iggy+tcp://.
+// Other iggy+<transport> schemes such as iggy+quic:// are rejected,
+// because the Go SDK implements the TCP transport only, which covers both
+// plain TCP and TLS. Credentials are username:password or a single
+// personal access token before the @; either way auto-login is configured,
+// matching what the Rust and Node SDKs do with a connection string. The
+// host may be a bracketed IPv6 literal, as in [::1]:8090.
+//
+// Query options follow the grammar the other SDKs share:
+//
+//     tls=true|false              enable TLS
+//     tls_domain=<name>           TLS server name indication (SNI)
+//     tls_ca_file=<path>          CA certificate file for verification
+//     reconnection_retries=<n|unlimited>
+//     reconnection_interval=<non-zero duration>
+//     reestablish_after=<duration>
+//     heartbeat_interval=<non-zero duration>
+//     nodelay=true|false
+//
+// Durations take the units the other SDKs share, from nanos to years,
+// including the hr, hrs, wk, wks, month and µs spellings, with optional
+// fractions and parts run together or spaced, as in 1h30m, 1h 30m or 1 s.
+// The reconnection_interval and heartbeat_interval spellings must be
+// non-zero and no shorter than one microsecond, because the Rust SDK
+// types them as non-zero durations whose wire format carries whole
+// microseconds; reestablish_after takes the zero spellings, which Rust
+// accepts there.
+// The Go client keeps its own reconnect and heartbeat pacing, so those
+// three spellings are validated for compatibility and then ignored, and
+// a value the other SDKs would reject is still rejected here, so one
+// connection string is either valid everywhere or nowhere. The nodelay
+// spelling is applied, because the Go client carries a matching
+// TCP_NODELAY option; it defaults to true, the Go client default, so
+// only nodelay=false ever turns Nagle back on.
+func ParseConnectionString(connectionString string) ([]tcp.Option, error) {
+       protocolParts := strings.Split(connectionString, "://")
+       if len(protocolParts) != 2 {
+               return nil, ierror.InvalidConnectionString{}
+       }
+
+       protocol, err := parseProtocol(protocolParts[0])
+       if err != nil {
+               return nil, err
+       }
+       if protocol != tcpProtocol {
+               // Only a plain transport name is echoed, because a malformed
+               // scheme may carry credentials, as the C# SDK notes on the same
+               // check.
+               if !isASCIILetters(protocol) {
+                       return nil, ierror.InvalidConnectionString{}
+               }
+               return nil, fmt.Errorf("%w: unsupported transport %q, Go SDK 
supports tcp only",
+                       ierror.InvalidConnectionString{}, protocol)
+       }
+
+       parts := strings.Split(protocolParts[1], "@")
+       if len(parts) != 2 {
+               return nil, ierror.InvalidConnectionString{}
+       }
+
+       credentials, err := parseCredentials(parts[0])
+       if err != nil {
+               return nil, err
+       }
+
+       serverAndOptions := strings.Split(parts[1], "?")
+       if len(serverAndOptions) > 2 {
+               return nil, ierror.InvalidConnectionString{}
+       }
+
+       // The no-delay default lives here for the no-options path and in
+       // parseConnectionOptions for queries, so a query that leaves the
+       // spelling out keeps Nagle off.
+       var tlsConfig parsedTLS
+       tlsConfig.noDelay = true
+       if len(serverAndOptions) == 2 {
+               tlsConfig, err = parseConnectionOptions(serverAndOptions[1])
+               if err != nil {
+                       return nil, err
+               }
+       }
+
+       serverAddress := serverAndOptions[0]
+       if serverAddress == "" {
+               return nil, ierror.InvalidConnectionString{}
+       }
+       host, portStr, err := net.SplitHostPort(serverAddress)
+       if err != nil || host == "" {
+               return nil, ierror.InvalidConnectionString{}
+       }
+       // Node and Rust accept any u16 port, 0 included; the dial fails later
+       // instead of the parse. A bad port reports the same error as every 
other
+       // parse failure, which is what the Rust SDK returns here.
+       if _, err := strconv.ParseUint(portStr, 10, 16); err != nil {
+               return nil, ierror.InvalidConnectionString{}
+       }
+
+       options := []tcp.Option{tcp.WithServerAddress(serverAddress)}
+       options = append(options, tcp.WithAutoLogin(credentials))
+       if tlsConfig.enabled {
+               var tlsOpts []tcp.TLSOption
+               if tlsConfig.domain != "" {
+                       tlsOpts = append(tlsOpts, 
tcp.WithTLSDomain(tlsConfig.domain))
+               }
+               if tlsConfig.caFile != "" {
+                       tlsOpts = append(tlsOpts, 
tcp.WithTLSCAFile(tlsConfig.caFile))
+               }
+               options = append(options, tcp.WithTLS(tlsOpts...))
+       }
+       options = append(options, tcp.WithNoDelay(tlsConfig.noDelay))
+       return options, nil
+}
+
+// parseProtocol extracts the transport from the scheme, which is either the
+// bare default protocol or an explicit iggy+<transport> spelling.
+func parseProtocol(scheme string) (string, error) {
+       if scheme == defaultProtocol {
+               return tcpProtocol, nil
+       }
+       if !strings.HasPrefix(scheme, protocolPrefix) {
+               return "", ierror.InvalidConnectionString{}
+       }
+       return strings.TrimPrefix(scheme, protocolPrefix), nil
+}
+
+// parseCredentials turns the userinfo before the @ into auto-login
+// credentials: username:password or a single personal access token.
+func parseCredentials(userinfo string) (tcp.Credentials, error) {
+       username, password, hasSeparator := strings.Cut(userinfo, ":")
+       if !hasSeparator {
+               if username == "" {
+                       return tcp.Credentials{}, 
ierror.InvalidConnectionString{}
+               }
+               return tcp.NewPersonalAccessTokenCredentials(username), nil
+       }
+       if username == "" || password == "" {
+               return tcp.Credentials{}, ierror.InvalidConnectionString{}
+       }
+       return tcp.NewUsernamePasswordCredentials(username, password), nil
+}
+
+type parsedTLS struct {
+       enabled bool
+       domain  string
+       caFile  string
+       noDelay bool
+}
+
+// parseConnectionOptions validates the shared connection-string grammar,
+// keeping the tls options and ignoring the reconnect spellings the Go
+// client does not take from a connection string.
+func parseConnectionOptions(optionsString string) (parsedTLS, error) {
+       parsed := parsedTLS{noDelay: true}
+       if optionsString == "" {
+               return parsed, ierror.InvalidConnectionString{}
+       }
+       for _, option := range strings.Split(optionsString, "&") {
+               name, value, found := strings.Cut(option, "=")
+               if !found {
+                       return parsed, ierror.InvalidConnectionString{}
+               }
+               switch name {
+               case "tls":
+                       if value != "true" && value != "false" {
+                               return parsed, ierror.InvalidConnectionString{}
+                       }
+                       parsed.enabled = value == "true"
+               case "tls_domain":
+                       parsed.domain = value
+               case "tls_ca_file":
+                       parsed.caFile = value
+               case "reconnection_retries":
+                       if value != "unlimited" {
+                               if _, err := strconv.ParseUint(value, 10, 32); 
err != nil {
+                                       return parsed, 
ierror.InvalidNumberValue{}
+                               }
+                       }
+               case "reconnection_interval", "heartbeat_interval":

Review Comment:
   Done in d82966e — the parsed interval is now passed to 
`tcp.WithHeartbeatInterval` after a checked `time.Duration` conversion (values 
above `math.MaxInt64` nanoseconds fail the parse like any other spelling the 
string cannot honor). 
`TestParseConnectionString_HeartbeatIntervalLandsOnTheClient` pins the 5s 
default, `1s`, and the fractional `250ms` form.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to