Clean-up of various Go design guidelines (#4556)

Updates reflect the current state of the world for naming of options and
response envelope types.
Promote client constructors that use AAD token auth or the service
preferred authentication mechanism.
Paged and long-running operations now return their respective generic
types.
Resuming an LRO now uses the resume token in the Begin* method.
This commit is contained in:
Joel Hendrix 2022-07-20 11:30:10 -07:00 коммит произвёл GitHub
Родитель 1286f18c2d
Коммит 7d74c7afe1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 90 добавлений и 158 удалений

Просмотреть файл

@ -84,15 +84,11 @@ const (
WidgetColorRed WidgetColor = "red"
)
// WidgetColorValues returns a slice of possible values for WidgetColor.
func WidgetColorValues() []WidgetColor {
// PossibleWidgetColorValues returns a slice of possible values for WidgetColor.
func PossibleWidgetColorValues() []WidgetColor {
// ...
}
func (c WidgetColor) ToPtr() *WidgetColor {
return &c
}
```
## SDK Feature Implementation

Просмотреть файл

@ -84,33 +84,28 @@ type WidgetClient struct {
#### Service Client Constructors
{% include requirement/MUST id="golang-client-constructors" %} provide two constructors in the following format that return a new instance of a service client type. Constructors MUST return the client instance by reference.
{% include requirement/MUST id="golang-client-constructors" %} provide one or more constructors in the following format that return a new instance of a service client type. The "simple named" constructor MUST use an `azcore.TokenCredential`, assuming the service supports AAD authentication. If not, then the preferred credential type is used instead. Constructors MUST return the client instance by reference.
```go
// NewWidgetClient creates a new instance of WidgetClient with the specified values. It uses the default pipeline configuration.
// NewWidgetClient creates a new instance of WidgetClient with the specified values.
// endpoint - The URI of the Widget.
// cred - The credential used to authenticate with the Widget service.
// cred - The AAD token credential used to authenticate with the Widget service.
// options - Optional WidgetClient values. Pass nil to accept default values.
func NewWidgetClient(endpoint string, cred azcore.Credential, options *WidgetClientOptions) (*WidgetClient, error) {
func NewWidgetClient(endpoint string, cred azcore.TokenCredential, options *WidgetClientOptions) (*WidgetClient, error) {
// ...
}
// NewWidgetClientWithPipeline creates a new instance of WidgetClient with the specified values and custom pipeline.
// endpoint - The URI of the Widget.
// p - The pipeline used to process HTTP requests and responses for this WidgetClient.
func NewWidgetClientWithPipeline(endpoint string, p azcore.Pipeline) (*WidgetClient, error) {
// NewWidgetClientWithNoCredential creates a new instance of WidgetClient with the specified values.
// endpoint - The URI of the Widget that supports anonymous/SAS authentication
// options - Optional WidgetClient values. Pass nil to accept default values.
func NewWidgetClientWithNoCredential(endpoint string, options *WidgetClientOptions) (*WidgetClient, error) {
// ...
}
```
{% include requirement/MUST id="golang-client-constructors" %} provide a default constructor in the following format for services with a default endpoint (management plane is the most common example).
```go
// NewDefaultClient creates a new instance of WidgetClient with the specified values. It uses the default endpoint and pipeline configuration.
// cred - The credential used to authenticate with the Widget service.
// options - Optional Client values. Pass nil to accept default values.
func NewDefaultClient(cred azcore.Credential, options *ClientOptions) (*WidgetClient, error) {
// NewWidgetClientFromConnectionString creates a new instance of WidgetClient from the specified connection string.
// options - Optional WidgetClient values. Pass nil to accept default values.
func NewWidgetClientFromConnectionString(connectionString string, options *WidgetClientOptions) (*WidgetClient, error) {
// ...
}
```
@ -164,24 +159,21 @@ The Go idiom is to expose only synchronous methods. This allows callers to impl
Requests to the service fall into two basic groups: methods that make a single logical request, and methods that make a deterministic sequence of requests. An example of a _single logical request_ is a request that may be retried inside the operation. An example of a _deterministic sequence of requests_ is a paged operation.
The _response envelope_ is a protocol neutral representation of a response. The response envelope may combine data from headers, body, and the HTTP response. For example, you may expose an `ETag` header as a property on the response envelope. `<Resource>Response` is the response envelope. It contains HTTP headers, the object (a deserialized object created from the response body), and the raw HTTP response. Response envelopes MUST be returned by value.
The _response envelope_ is a protocol neutral representation of a response. The response envelope may combine data from headers and response body. For example, you may expose an `ETag` header as a property on the response envelope. `<Client><Method>Response` is the response envelope. It contains HTTP headers and the object (a deserialized object created from the response body). Response envelopes MUST be returned by value.
{% include requirement/MUST id="golang-response-logical-entity" %} return the response envelope for the normal form of a service method. The response envelope MUST represent the information needed in the 99%+ case.
```go
// WidgetResponse is the response envelope for operations that return a Widget type.
type WidgetResponse struct {
// WidgetClientGetResponse contains the response from method WidgetClient.Get.
type WidgetClientGetResponse struct {
// ETag contains the value from the ETag header.
ETag *string
ETag *azcore.ETag
// LastModified contains the value from the last-modified header.
LastModified *time.Time
// Widget contains the unmarshalled response body in Widget format.
Widget *Widget
// RawResponse contains the underlying HTTP response.
RawResponse *http.Response
Widget
}
type Widget struct {
@ -189,7 +181,7 @@ type Widget struct {
Color WidgetColor
}
func (c *WidgetClient) GetWidget(ctx context.Context, name string, options *GetWidgetOptions) (WidgetResponse, error) {
func (c *WidgetClient) Get(ctx context.Context, name string, options *WidgetClientGetOptions) (WidgetClientGetResponse, error) {
// ...
}
```
@ -197,11 +189,15 @@ func (c *WidgetClient) GetWidget(ctx context.Context, name string, options *GetW
{% include requirement/MUST id="golang-response-examples" %} provide examples on how to access the streamed response for a request, where exposed by the client library. We dont expect all methods to expose a streamed response.
```go
func (c *WidgetClient) GetBinaryResponse(ctx context.Context, name string, options *GetBinaryResponseOptions) (*http.Response, error) {
// ...
// WidgetClientGetBinaryResponse contains the response from method WidgetClient.GetBinaryResponse.
type WidgetClientGetBinaryResponse struct {
// Body contains the streaming response.
Body io.ReadCloser
}
// callers read from the io.ReadCloser Body field on the HTTP response.
func (c *WidgetClient) GetBinaryResponse(ctx context.Context, name string, options *WidgetClientGetBinaryResponseOptions) (WidgetClientGetBinaryResponse, error) {
// ...
}
```
{% include requirement/MUST id="golang-response-logical-paging" %} provide an idiomatic way to enumerate all logical entities for a paged operation, automatically fetching new pages as needed. For more information on what to return for List operations, refer to [Pagination](#pagination).
@ -236,31 +232,31 @@ Cancellation is handled via the `context.Context` paramater, which is _always_ t
##### Optional Parameters
{% include requirement/MUST id="golang-api-options-struct" %} define a `<MethodNameOptions>` structure for every method. This structure includes fields for all non-mandatory parameters. The structure can have fields added to it over time to simplify versioning. To disambiguate names, use the client type name for a prefix. If the method contains no optional parameters, the `options` struct should have a comment indicating it's a placeholder for future optional parameters.
{% include requirement/MUST id="golang-api-options-struct" %} define a `<Client><Method>Options` structure for every method. This structure includes fields for all non-mandatory parameters. The structure can have fields added to it over time to simplify versioning. To disambiguate names, use the client type name for a prefix. If the method contains no optional parameters, the `options` struct should have a comment indicating it's a placeholder for future optional parameters.
```go
// GetWidgetOptions contains the optional parameters for the Widget.Get method.
type GetWidgetOptions struct {
// WidgetClientGetOptions contains the optional parameters for the WidgetClient.Get method.
type WidgetClientGetOptions struct {
Tag *string
Length *int
}
// SetWidgetOptions contains the optional parameters for the Widget.Set method.
type SetWidgetOptions struct {
// WidgetClientSetOptions contains the optional parameters for the WidgetClient.Set method.
type WidgetClientSetOptions struct {
// placeholder for future optional parameters
}
```
{% include requirement/MUST id="golang-api-options-ptr" %} allow the user to pass a pointer to the structure as the last parameter. If the user passes `nil`, then the method should assume appropriate default values for all the structures fields. Note that `nil` and a zero-initialized `<MethodNameOptions>` structure are **NOT** required to be semantically equivalent.
{% include requirement/MUST id="golang-api-options-ptr" %} allow the user to pass a pointer to the structure as the last parameter. If the user passes `nil`, then the method should assume appropriate default values for all the structures fields. Note that `nil` and a zero-initialized `<Client><Method>Options` structure **are required** to be semantically equivalent.
{% include requirement/MUST id="golang-api-params" %} document all parameters as part of the method block comment.
```go
// GetWidget retrieves the specified Widget.
// Get retrieves the specified Widget.
// ctx - The context used to control the lifetime of the request.
// name - The name of the Widget to retrieve.
// options - Any optional parameters.
func (c *WidgetClient) GetWidget(ctx context.Context, name string, options *GetWidgetOptions) (WidgetResponse, error) {
func (c *WidgetClient) Get(ctx context.Context, name string, options *WidgetClientGetOptions) (WidgetClientGetResponse, error) {
// ...
}
```
@ -277,122 +273,66 @@ The service client will have several methods that perform requests on the servic
#### Methods Returning Collections (Paging)
{% include requirement/MUST id="golang-pagination" %} return a value that implements the Pager interface for operations that return pages. The Pager interface allows consumers to iterate over all pages as defined by the service.
{% include requirement/MUST id="golang-pagination" %} return an instance of `*runtime.Pager[T]` for operations that return pages. `runtime.Pager[T]` allows consumers to iterate over all pages as defined by the service.
{% include requirement/MUST id="golang-pagination-pagers" %} create Pager interface types with the name `<Resource>Pager` that are to be returned from their respective operations.
{% include requirement/MUST id="golang-pagination" %} name methods that return a `*runtime.Pager[T]` with pattern `New<Operation>Pager`.
{% include requirement/MUST id="golang-pagination-pagers-interface-page" %} expose methods `NextPage()`, `PageResponse()`, and `Err()` on the `<Resource>Pager` type.
{% include requirement/MUSTNOT id="golang-params-service-validation" %} perform any IO when creating the `runtime.Pager[T]`. This implies that the method does NOT take a context and does NOT return an error.
```go
// WidgetPager provides iteration over ListWidgets pages.
type WidgetPager interface {
// NextPage returns true if the pager advanced to the next page.
// Returns false if there are no more pages or an error occurred.
NextPage(context.Context) bool
// PageResponse returns the current WidgetsPage.
PageResponse() ListWidgetsResponse
// Err returns the last error encountered while paging.
Err() error
}
type ListWidgetsResponse struct {
RawResponse *http.Response
Widgets *[]Widget
}
```
{% include requirement/MUST id="golang-pagination-methods" %} use the prefix `List` in the method name for methods that return a Pager. The `List` method creates the Pager but does NOT perform an IO operation.
```go
func (c *WidgetClient) ListWidgets(options *ListWidgetOptions) WidgetPager {
func (c *WidgetClient) NewListPager(options *WidgetClientListOptions) *runtime.Pager[WidgetClientListResponse] {
// ...
}
pager := client.ListWidgets(options)
for pager.NextPage(ctx) {
for _, w := range pager.PageResponse().Widgets {
type WidgetClientListResponse struct {
WidgetsListResult
}
type WidgetsListResult struct {
Values []*Widget
NextLink *string
}
pager := client.NewListPager(nil)
for pager.More() {
page, err := pager.NextPage(context.Background())
if err != nil {
// handle error...
}
for _, w := range page.Value {
process(w)
}
}
if pager.Err() != nil {
// handle error...
}
```
{% include requirement/MUST id="golang-pagination-serialization" %} provide means to serialize and deserialize a Pager so that paging can pause and continue, potentially on another machine.
{% include requirement/MUST id="golang-pagination-serialization" %} provide means to serialize and deserialize a `runtime.Pager[T]` so that paging can pause and continue, potentially on another machine.
#### Methods Invoking Long Running Operations
{% include requirement/MUST id="golang-lro-poller" %} return a value that implements the Poller interface for long-running operation methods. The Poller interface encapsulates the polling and status of the long-running operation.
{% include requirement/MUST id="golang-lro-poller" %} return an instance of `*runtime.Poller[T]` for long-running operation methods. `runtime.Poller[T]` encapsulates the polling and status of the long-running operation.
{% include requirement/MUST id="golang-lro-poller-name" %} create Poller interface types with the name `<Resource>Poller` that are to be returned from their respective operations.
{% include requirement/MUST id="golang-lro-poller-def" %} provide the following methods on a `<Resource>Poller` type: `Done()`, `ResumeToken()`, `Poll()`, and `FinalResponse()`.
{% include requirement/MUST id="golang-lro-method-naming" %} prefix methods which return a `*runtime.Poller[T]` with `Begin`.
```go
// Poller provides operations for checking the state of a long-running operation.
// An LRO can be in either a non-terminal or terminal state. A non-terminal state
// indicates the LRO is still in progress. A terminal state indicates the LRO has
// completed successfully, failed, or was cancelled.
type WidgetPoller interface {
// Done returns true if the LRO has reached a terminal state.
Done() bool
// ResumeToken returns a value representing the poller that can be used to resume
// the LRO at a later time. ResumeTokens are unique per service operation.
ResumeToken() string
// Poll fetches the latest state of the LRO. It returns an HTTP response or error.
// If the LRO has completed successfully, the poller's state is update and the HTTP
// response is returned.
// If the LRO has completed with failure or was cancelled, the poller's state is
// updated and the error is returned.
// If the LRO has not reached a terminal state, the poller's state is updated and
// the latest HTTP response is returned.
// If Poll fails, the poller's state is unmodified and the error is returned.
// Calling Poll on an LRO that has reached a terminal state will return the final
// HTTP response or error.
Poll(context.Context) (*http.Response, error)
// FinalResponse performs a final GET to the service and returns the final response
// for the polling operation. If there is an error performing the final GET then an error is returned.
// If the final GET succeeded then the final WidgetResponse will be returned.
FinalResponse(context.Context) (WidgetResponse, error)
}
```
{% include requirement/MUST id="golang-lro-wait-method" %} accept a `pollingInterval` argument in the `PollUntilDone()` method to be used in the absence of relevant retry-after headers from the service.
{% include requirement/MUST id="golang-lro-method-naming" %} prefix methods which return a `<Resource>Poller` with `Begin`.
```go
// WidgetPollerResponse is the response envelope for operations that asynchronously return a Widget type.
type WidgetPollerResponse struct {
// PollUntilDone will poll the service endpoint until a terminal state is reached or an error is received.
PollUntilDone func(context.Context, time.Duration) (WidgetResponse, error)
// Poller contains an initialized WidgetPoller.
Poller WidgetPoller
// RawResponse contains the underlying HTTP response.
RawResponse *http.Response
}
// BeginCreate creates a new widget with the specified name.
func (c *WidgetClient) BeginCreate(ctx context.Context, name string, options *BeginCreateOptions) (WidgetPollerResponse, error) {
func (c *WidgetClient) BeginCreate(ctx context.Context, name string, options *WidgetClientBeginCreateOptions) (*runtime.Poller[WidgetClientCreateResponse], error) {
// ...
}
// WidgetClientCreateResponse contains the response from method WidgetClient.BeginCreate.
type WidgetClientCreateResponse struct {
Widget
}
```
{% include requirement/MUST id="golang-lro-resuming-operations" %} provide a method with the prefix `Resume` to instantiate a `<Resource>Poller` type with the `ResumeToken` from a previous call to `Poller.ResumeToken()`.
{% include requirement/MUST id="golang-lro-resuming-operations" %} provide a field named `ResumeToken` in the `<Client><Method>Options` type used to instantiate a `*runtime.Poller[T]` type with the `ResumeToken` from a previous call to `Poller[T].ResumeToken()`.
```go
// ResumeWidgetPoller creates a new WidgetPoller from the specified ResumtToken.
// resumeToken - The value must come from a previous call to WidgetPoller.ResumeToken().
func (c WidgetClient) ResumeWidgetPoller(resumeToken string) WidgetPoller {
// ...
// WidgetClientBeginCreateOptions contains the optional parameters for the WidgetClient.BeginCreate method.
type WidgetClientBeginCreateOptions struct {
// Resumes the LRO from the provided token.
ResumeToken string
}
```
@ -402,22 +342,24 @@ func (c WidgetClient) ResumeWidgetPoller(resumeToken string) WidgetPoller {
```go
// example #1, blocking call to PollUntilDone()
resp, err := client.BeginCreate(context.Background(), "blue_widget", nil)
poller, err := client.BeginCreate(context.Background(), "blue_widget", nil)
if err != nil {
// handle error...
}
w, err = resp.PollUntilDone(context.Background(), 5*time.Second)
w, err = poller.PollUntilDone(context.Background(), &runtime.PollUntilDoneOptions{Frequency: 5*time.Second})
if err != nil {
// handle error...
}
process(w)
// example #2, customized poll loop
resp, err := client.BeginCreate(context.Background(), "green_widget")
poller, err := client.BeginCreate(context.Background(), "green_widget")
if err != nil {
// handle error...
}
poller := resp.Poller
for {
resp, err := poller.Poll(context.Background())
if err != nil {
@ -426,13 +368,13 @@ for {
if poller.Done() {
break
}
if delay := azcore.RetryAfter(resp); delay > 0 {
if delay := runtime.RetryAfter(resp); delay > 0 {
time.Sleep(delay)
} else {
time.Sleep(frequency)
}
}
w, err := poller.FinalResponse(ctx)
w, err := poller.Result(context.Background())
if err != nil {
// handle error ...
}
@ -440,33 +382,27 @@ process(w)
// example #3, resuming from a previous operation
// getting the resume token from a previous poller instance
poller := resp.Poller
poller, err := client.BeginCreate(context.Background(), "blue_widget", nil)
if err != nil {
// handle error...
}
tk, err := poller.ResumeToken()
if err != nil {
// handle error ...
}
// resuming from the resume token that was previously saved
poller, err := client.ResumeWidgetPoller(tk)
poller, err = client.BeginCreate(context.Background(), "", &WidgetClientBeginCreateOptions{
ResumeToken: tk,
})
if err != nil {
// handle error ...
}
for {
resp, err := poller.Poll(context.Background())
if err != nil {
// handle error ...
}
if poller.Done() {
break
}
if delay := azcore.RetryAfter(resp); delay > 0 {
time.Sleep(delay)
} else {
time.Sleep(frequency)
}
}
w, err := poller.FinalResponse(ctx)
w, err = poller.PollUntilDone(context.Background(), nil)
if err != nil {
// handle error ...
// handle error...
}
process(w)
```