Repository: twmb/franz-go (go)

Issue:
Add `Client.OptValue` and `Client.OptValues` to `sr` package
Similar to the `Client` type in the `kgo` package, it would be useful to have `Client.OptValue` and `Client.OptValues` functions for the `Client` type in the `sr` package. This would allow to retrieve values used in options, which can be useful for testing or option value retrieval in existing client objects.

If you agree with this request, I can give it a try to implement it and send a PR. 👍 

Pull request description:
sr: add `Client.OptValue` and `Client.OptValues` functions
This PR adds `Client:OptValue` and `Client.OptValues` functions to the `sr` package.
These are heavily based on the functions of the same name in the `kgo` package.

~~Note that I did not add a case and unit test for the `DialTLSConfig` option because this option creates a new `http.Client` object with TLS preconfigured instead of storing the TLS config in the client struct.~~ EDIT: Added now after merged #978.

Fixes #954 


Patch:
diff --git a/pkg/sr/client.go b/pkg/sr/client.go
index 4a893aea..f947f241 100644
--- a/pkg/sr/client.go
+++ b/pkg/sr/client.go
@@ -26,6 +26,9 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"reflect"
+	"runtime"
+	"strings"
 	"time"
 )
 
@@ -115,6 +118,19 @@ func NewClient(opts ...ClientOpt) (*Client, error) {
 	return cl, nil
 }
 
+func namefn(fn any) string {
+	v := reflect.ValueOf(fn)
+	if v.Type().Kind() != reflect.Func {
+		return ""
+	}
+	name := runtime.FuncForPC(v.Pointer()).Name()
+	dot := strings.LastIndexByte(name, '.')
+	if dot >= 0 {
+		return name[dot+1:]
+	}
+	return name
+}
+
 // Opts returns the options that were used to create this client. This can be
 // as a base to generate a new client, where you can add override options to
 // the end of the original input list.
@@ -122,6 +138,79 @@ func (cl *Client) Opts() []ClientOpt {
 	return cl.opts
 }
 
+// OptValue returns the value for the given configuration option. If the
+// given option does not exist, this returns nil. This function takes either a
+// raw ClientOpt, or an Opt function name.
+//
+// If a configuration option has multiple inputs, this function returns only
+// the first input. Variadic option inputs are returned as a single slice.
+// Options that are internally stored as a pointer are returned as their
+// string input; you can see if the option is internally nil by looking at
+// the second value returned from OptValues.
+//
+//		var (
+//	 		cl, _ := NewClient(
+//	 			URLs("foo", "bar"),
+//				UserAgent("baz"),
+//	 		)
+//	 		urls = cl.OptValue("URLs")     // urls is []string{"foo", "bar"}; string lookup for the option works
+//	 		ua   = cl.OptValue(UserAgent)  // ua is "baz"
+//	 		unk  = cl.OptValue("Unknown"), // unk is nil
+//		)
+func (cl *Client) OptValue(opt any) any {
+	vs := cl.OptValues(opt)
+	if len(vs) > 0 {
+		return vs[0]
+	}
+	return nil
+}
+
+// OptValues returns all values for options. This method is useful for
+// options that have multiple inputs (notably, BasicAuth). This is also useful
+// for options that are internally stored as a pointer -- this function will
+// return the string value of the option but also whether the option is non-nil.
+// Boolean options are returned as a single-element slice with the bool value.
+// Variadic inputs are returned as a signle slice. If the input option does not
+// exist, this returns nil.
+//
+//	     var (
+//		 		cl, _ := NewClient(
+//		 			URLs("foo", "bar"),
+//					UserAgent("baz"),
+//		 		)
+//		 		urls = cl.OptValues("URLs")     // urls is []any{[]string{"foo", "bar"}}
+//		 		ua   = cl.OptValues(UserAgent)  // ua is []any{"baz"}
+//		 		ba   = cl.OptValues(BasicAuth)  // ba is []any{"user", "pass"}
+//		 		unk  = cl.OptValues("Unknown"), // unk is nil
+//	     )
+func (cl *Client) OptValues(opt any) []any {
+	name := namefn(opt)
+	if s, ok := opt.(string); ok {
+		name = s
+	}
+
+	switch name {
+	case namefn(HTTPClient):
+		return []any{cl.httpcl}
+	case namefn(UserAgent):
+		return []any{cl.ua}
+	case namefn(URLs):
+		return []any{cl.urls}
+	case namefn(DialTLSConfig):
+		return []any{cl.dialTLS}
+	case namefn(BasicAuth):
+		return []any{cl.basicAuth.user, cl.basicAuth.pass}
+	case namefn(BearerToken):
+		return []any{cl.bearerToken}
+	case namefn(PreReq):
+		return []any{cl.preReq}
+	case namefn(DefaultParams):
+		return []any{cl.defParams}
+	default:
+		return nil
+	}
+}
+
 func (cl *Client) get(ctx context.Context, path string, into any) error {
 	return cl.do(ctx, http.MethodGet, path, nil, into)
 }


Test patch:
diff --git a/pkg/sr/client_test.go b/pkg/sr/client_test.go
index d1de02ef..5941c43e 100644
--- a/pkg/sr/client_test.go
+++ b/pkg/sr/client_test.go
@@ -2,6 +2,7 @@ package sr
 
 import (
 	"context"
+	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -284,3 +285,106 @@ func TestSchemaRegistryAPI(t *testing.T) {
 		})
 	}
 }
+
+func TestOptValue(t *testing.T) {
+	httpcl := &http.Client{}
+	tlscfg := &tls.Config{}
+
+	c, err := NewClient(
+		HTTPClient(httpcl),
+		URLs("localhost:8081", "localhost2:8082"),
+		UserAgent("some-ua"),
+		DialTLSConfig(tlscfg),
+		BasicAuth("some-user", "some-pass"),
+		BearerToken("some-bearer"),
+		PreReq(func(req *http.Request) error {
+			req.Header.Set("Authorization", "foobar")
+			return nil
+		}),
+		DefaultParams(Normalize, Verbose),
+	)
+	if err != nil {
+		t.Fatalf("unable to create client: %s", err)
+	}
+
+	tests := []struct {
+		name     string
+		opt      any
+		assertFn func(v any) bool
+	}{
+		{
+			name:     "HTTPClient",
+			opt:      "HTTPClient",
+			assertFn: func(v any) bool { return v == httpcl },
+		},
+		{
+			name: "URLs",
+			opt:  "URLs",
+			assertFn: func(v any) bool {
+				vs := v.([]string)
+				return vs[0] == "http://localhost:8081" && vs[1] == "http://localhost2:8082"
+			},
+		},
+		{
+			name:     "UserAgent",
+			opt:      "UserAgent",
+			assertFn: func(v any) bool { return v.(string) == "some-ua" },
+		},
+		{
+			name:     "DialTLSConfig",
+			opt:      "DialTLSConfig",
+			assertFn: func(v any) bool { return v == tlscfg },
+		},
+		{
+			name:     "BasicAuth",
+			opt:      BasicAuth,
+			assertFn: func(v any) bool { return v.(string) == "some-user" },
+		},
+		{
+			name:     "BearerToken",
+			opt:      BearerToken,
+			assertFn: func(v any) bool { return v.(string) == "some-bearer" },
+		},
+		{
+			name:     "PreReq",
+			opt:      "PreReq",
+			assertFn: func(v any) bool { return v != nil },
+		},
+		{
+			name: "DefaultParams",
+			opt:  DefaultParams,
+			assertFn: func(v any) bool {
+				vp := v.(Param)
+				return vp.normalize == true && vp.verbose == true
+			},
+		},
+		{
+			name:     "unknown option name",
+			opt:      "Unknown",
+			assertFn: func(v any) bool { return v == nil },
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got := c.OptValue(test.opt)
+			if !test.assertFn(got) {
+				t.Errorf("assertion failed: %q", got)
+			}
+		})
+	}
+
+	t.Run("BasicAuthValues", func(t *testing.T) {
+		got := c.OptValues(BasicAuth)
+		if len(got) != 2 {
+			t.Errorf("number of return values: expected 2, got %d", len(got))
+		}
+		expected := "some-user"
+		if got[0] != expected {
+			t.Errorf("username: expected %q, got %q", expected, got[0])
+		}
+		expected = "some-pass"
+		if got[1] != expected {
+			t.Errorf("password: expected %q, got %q", expected, got[1])
+		}
+	})
+}