feat: provide better `NewClient` methods with `baseURL`/`basePath`
Hi there,
Could we make a pull request to add an essential part which is missing on instantiating a client from the Goa generated code:
The client takes the following signature:
func NewClient(
scheme string,
host string,
doer goahttp.Doer,
enc func(*http.Request) goahttp.Encoder,
dec func(*http.Response) goahttp.Decoder,
restoreBody bool,
) *Client
which leaves no room to set the base url like schema: https , baseURL: domain.com/a/b/c and all endpoints will start with https://domain.com/a/b/c/..... The host parameter cannot be misused as its used in url.URL{...} which has different semantics.
We would like to add another non-breaking function NewClientWithOpts(opts ... ClientOption) which lets us chose
all relevant options, and defaulting the ones not set.
NewClientWithOpts(
WithSchema("https"),
WithHost("domain.com:8080"),
WithURL(url.Parse("https://domain.com/a/b/c?a=asdf), // here only taking host and schema and path.
WithDoer(...),
WithDecoder(...),
WithEncoder(...),
WithRestoreBody(true))
etc.
The old function NewClient can then forward to
NewClientsWithOpts(WithBaseURL(url.URL{Scheme: scheme, Host: host})).
We really need this somewhat soon.
Implementation
type Client struct {
...
scheme string
host string
basePath string // this is probably enough to add (prefix with `/` if not given)
encoder func(*http.Request) goahttp.Encoder
decoder func(*http.Response) goahttp.Decoder
}
Have you tried Path DSL?
https://pkg.go.dev/goa.design/goa/v3/dsl#Path
Didnt know about that, but it does not help that hardcodes after generation time.
What we are looking for is a runtime changeable base path for instanciating the client which is not only "hostname" (which is too limiting), also I have never seen a client constructor which cannot take a full URL.
We have to following:
A Software ComponentA wants to define what exact external endpoints (client) it will contact when running. When starting (at deploy time) ComponentA we want to configure this client URL of course: http://componentB.com/a/b/c.
This ComponentA specifies that external endpoints in a Goa design (Go module) and generates client code once.
A software ComponentB of course needs to provide the server implementation for ComponentA to work. It will generate server code with its own Goa where it reuses some common Goa function (defining the GET method e.g.) from ComponentA's Go packages.
The problem: ComponentA has client code generated which does not allow to set a proper host URL, only hostname. Also we do not want and cant regenerate and compile ComponentA on the fly with dsl.Path.
client.NewClient is too limiting and not modular enough (no matter the Goa design), thats why I suggest to add proper options. Does that make sense or maybe there is a better way.
You can use a path parameter in the Path DSL. The base path cannot be included in the client, but can be dynamically provided via the request payload. See Param DSL for an example.
https://pkg.go.dev/goa.design/goa/v3/dsl#Param
request := foo.GetPayload{
BasePath: basePath, // <-
// ...
}
client := fooclient.NewClient(
scheme,
host,
doer,
encoder,
decoder,
restoreBody,
)
endpoint := client.Get()
response, err := endpoint(ctx, request)
@tchssk : Thanks for the pointer, thats neat 👍
Just to clarify: do I understand correctly that would be dsl.Path(/{basePath}) like:
var GetPayload = Type("GetPayload", func() {
Attribute("id", string, "id")
Attribute("basePath", string, "base path for the request")
})
var _ = Service("componentA", func() {
HTTP(func() {
Path("/{basePath}")
})
Method("Get", func() { // default response type.
Payload(GetPayload)
HTTP(func() {
GET("/get/{id}")
Params(func() {
Param("basePath:base")
})
})
})
})
would let me: choose basePath on the client as you say:
request := foo.GetPayload{
BasePath: basePath,
Id: "myid"
}
client := fooclient.NewClient(
scheme,
host,
doer,
encoder,
decoder,
restoreBody,
)
endpoint := client.Get()
response, err := endpoint(ctx, request)
Yes, that's correct.