Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hand written prototype for updated client API design. #530

Closed
wants to merge 4 commits into from

Conversation

jasdel
Copy link
Contributor

@jasdel jasdel commented Apr 15, 2020

Generated API client design

Addresses issue #70, #438, #439

Operation generated types

The following are a set of types that must be generated by smithy for an operation, that are not defined in the model. These types are needed by the API client to wrap and encapsulate the modeled operation types. The Smithy generator must ensure that all novel generated type do not conflict with modeled shapes.

Operation input: Smithy must generate a unique input type for each operation, regardless if an input shape is modeled. The naming of the input shape will be Input, where is the exported name of the operation. The input shape is a renamed clone of the modeled operation’s input shape, if any. If the operation does not have a modeled input shape, the a new empty shape will be created and used as the input shape.

Operation output: Smithy must generate a unique output type for each operation, regardless if an output shape is modeled. The name of the output type will be Output, where is the exported name of the operation. The output shape is a renamed clone of the modeled operation’s output shape, if any. If the operation does not have a modeled output shape, the a new empty output shape will be created and used as the input shape.

Operation Paginator: Smithy must generate operation specific paginator helpers for operations with modeled paginators. The paginator is a novel type that provides pagination iterator behavior. The name of the type will be Paginator, where is the exported name of the operation.

API Client: Smithy must generate a type named Client for each API client package. There may only be one API client per package. The client type will have additional configuration option utility types and functions generated as well.

Client

Smithy will generate a Client type in the API client package. This type will have methods for each modeled operation. The client's constructor takes functional options for configuring how the client operates. If customers would like to modify the client's behavior for all operation they can use the Client's functional options to do this.

// Client provides the client for interacting with the Amazon Lex Runtime Service.
type Client struct {
    options Options
}

// New returns an initialized client with the client options used to specify the behavior
// of the client. The user has the ability to provide the options parameters
// directly without being required to load configuraiton.
func New(Options) *Client { ... }

// NewFromConfig returns an initialized Client based on the passed in config, and
// functional options. Provide additional function options to further configure the
// behavior of the client. Such as changing the client's region, endpoint, or
// adding custom middleware behavior.
func NewFromConfig(cfg aws.Config, opts ...func(*Options)) *Client {...}

// ServiceID returns the name of the identifier for the service API.
func (c *Client) ServiceID() string { return "ServiceID" }

// ServiceName returns the full service name.
func (c *Client) ServiceName() string { return "Service Name" }

Client options

A client options is a type unique to the Client, that defines the set of options that a customer can modify when they create a client via the New function.

If the SDK were to generate functional option helpers the New function's opts parameter could be defined as an exported type that all functional options return. This would ensure functional option helpers are grouped with the option type in API reference docs.

// Options provides the set of configurations that can be applied to the
// Client. Use functional options to modify this set when creating the client,
// or when invoking API operations.
type Options struct {
	// The AWS regional endpoint the client will make API operation calls to.
	// Required by the client.
	RegionID string

	// Behavior to resolve endpoints. Defaults to client's default endpoint
	// resolution if nil.
	EndpointResolver aws.EndpointResolver

	// The HTTP client to invoke API calls with. Defaults to client's default
	// HTTP implementation if nil.
	HTTPClient HTTPClient

	// Credentials will be used to create the client's default signer if one is
	// not provided. If signer is provided Credentials are ignored.
	Credentials aws.CredentialsProvider

	// If nil, will default to client's signer using provided credentials.
	// Credentials must also be set.
	Signer      HTTPSigner
	SigningName string

	// Set of options to modify how an operation is invoked. These apply to all
	// operations invoked for this client. Use functional option on operation
	// call to modify this list for per operation behavior.
	APIOptions []func(*middleware.Stack)

	Retryer  aws.Retryer
	LogLevel aws.LogLevel
	Logger   aws.Logger
}

Operations

The SDK will generate a method on the API Client for each supported operation. The operation will need to take three parameters, context.Context the modeled input shape, and a variadic argument of functional options to modify operation being invoked.

The functional options allow customers to modify how the individual operation is invoked. If the customer wants to modify all operations of a client, they can use the Options.APIOptions member.

// GetSession invokes the GetSession S3 API operation with the input parameters,
// and context provided. The passed in functional options can be used to modify
// how the operation is invoked. A result will be returned on success, or error
// if the operation invoke fails.
func (c *Client) GetSession(
    ctx context.Context, params *GetSessionInput, opts ...func(*Options),
) (
    *GetSessionOutput, error,
) {
    stack := middleware.NewStack("GetSession stack", smithyhttp.NewStackRequst)
    // op specific middleware
    
    // client options applied to stack
    // user func options applied to stack
    // decorate client handler with stack
    
    result, err := handler.Handle(ctx, params)
    if err != nil {
        return nil, err
    }
    return res.(*GetSessionOutput), nil
}

Operation input and output shape

Since Go does not support method overloading, and an operation can be updated to have a modeled input or output shape, all generated operation methods must have a stable input and output type generated. In order to ensure a stable name, this types must be <Operation>Input and <Operation>Output, where <Operation> is the name of the generated operation method.

If a modeled input or output shape in an orphan and is not used by any other operation, or nested within a shape, the original shape should not be generated.

If a modeled input or output shape is reused by another operation, or nested, the original shape should be generated. The generator should also generate copy utilities to copy the cloned input/output shape to and from the original modeled shape. This allows customers to easily convert the input or output shape into its original shape for round tripping.

For example, the Lex Runtime GetSession operation is modeled with GetSessionRequest input shape. Smithy will generated the GetSessionInput type with all the same parameters as the modeled GetSessionRequest shape. If the GetSessionRequest shape is not used by any other shape or operation, it would not be generated. The reason to not generate these unused types is, because it would cause noise, and be confusion.

Operation output type result metadata

The operation output generated type will have additional members and methods generated. These member's provide access to result metadata such as retry attempts and access to the raw protocol response message.

The operation output type will also have metadata helper methods generated on it such as ServiceRequestID(), and HTTPResponse().

For example the Lex Runtime GetSession operation has a modeled output shape of GetSessionResponse, which becomes the operation output type of GetSessionOutput. The GetSessionOutput type will have the additional member ResultMetadata generated. This member will be used to store result metadata of invoking the operation.

type GetSessionOutput struct {
	// Members from modeled output shape, GetSessionResponse.
	DialogAction            *types.DialogAction
	RecentIntentSummaryView []types.IntentSummary
	SessionAttributes       map[string]string
	SessionId               *string

	// Additional member for result metadata
	ResultMetadata middleware.Metadata
}

Mocking API Client operations

The Smithy generator will not generate interfaces for the API client. Customers will need to write these interfaces them selves. Depending on customer demand this decision might be changed, but this is a two-way door decision to not generate the API client interfaces. The exception to this are operation clients that are needed for Waiters and Paginators.

Customers are able to define their own interfaces for operations they use. These interfaces can be composed, adding multiple operation methods to the same interface as they need. This style is the idiomatic style of not defining interfaces for behavior not used by the package.

type GetSessionAPIClient interface {
    GetSession(context.Context, *lexruntimeservice.GetSessionInput,
          ...func(*Options))
}

Paginated operations

Smithy must generate operation specific paginator helpers for operations with modeled paginators. The paginator is a novel type that provides pagination iterator behavior. The name of the type will be <OperationName>Paginator.

When a paginator or other utility that needs a client operation, an interface in the client package should be generated for that operation. The interface should be named <OperationName>APIClient.

// ListSessionsClient provides the interface for a client that implements the
// ListSessions API operation. Implemented by the package's Client type.
type ListSessionsAPIClient interface {
    ListSessions(context.Context, *ListSessionsInput, ...func(*lexruntimeservice.Options)) (
        *ListSessionsOutput, error,
    )
}

The generated paginator utility will take the client operation interface, and input parameters as input.

// ListSessionsPaginator provides the paginator for the ListSessions API operation.
type ListSessionsPaginator struct {
    client ListSessionsAPIClient
    input *ListSessionsInput
    
    // members for pagination state
}

// NewListSessionsPaginator returns a ListSessionsPaginator configured for the
// API operation client, and input parameters.
func NewListSessionsPaginator(
    ListSessionsAPIClient, *ListSessionsInput, ...func(*Options),
) *ListSessionsPaginator

// HasMorePages returns if there may be more pages to retrieve.
func (p *ListSessionsPaginator) HasMorePages() bool

// NextPage returns the next page from the API, or error.
func (p *ListSessionsPaginator) NextPage(context.Context, ...func(*Options)) (
    *ListSessionsOutput, error,
)

The following is an example of the paginator being used to iterate over pages.

func ExampleClient_ListSessions_pagination() {
    cfg, err := external.LoadDefaultAWSConfig()
    if err != nil {
        log.Fatalf("unable to load configuration, %v", err)
    }

    client := lexruntimeservice.NewClient(cfg)

    // Create a paginator with the client and input parameters.
    p := lexruntime.NewListSessionsPaginator(client, &lexruntime.ListSessions{
        BotAlias: aws.String("botAlias"),
        BotName:  aws.String("botName"),
        UserId:   aws.String("userID"),
    })

    // Use the paginator to iterate over pages returned by the API. HasMorePages
    // will return false when there are no more pages, or NextPage failed.
    for p.HasMorePages() {
        o, err := p.NextPage(context.TODO())
        if err != nil {
            log.Fatalf("failed to get next page, %v", err)
        }
        fmt.Println("Page:", o)
    }
}

Outstanding Question: Should HasMorePages continue to return true even if NextPage fails"? The reasoning behind this would be so that in the case of temporary server side error. This would give the user the ability to recover mid pagination, without needing to restart from the beginning, or rebuilding the input parameters with the current token.

@skmcgrail
Copy link
Member

skmcgrail commented Apr 15, 2020

   // This service uses AWS Sigv4 signing, and needs Credentials values.
   Credentials aws.Credentials

   // Signer is the signer for the client.
   Signer HTTPSigner

May need to rethink the signer some, as today a signer is constructed with the aws.Credentials, maybe these should be passed to the SignHTTP/PresignHTTP functions instead?

@skmcgrail
Copy link
Member

If the GetSessionRequest shape is not used by any other shape, it must not be generated, because it would be unusable, cause noise, and be confusion.

May want to enumerate the keywords we will strip off when adding the Input suffix.

For example Request => Input

@skmcgrail
Copy link
Member

Note: An alternative would be to copy the output shape just like the SDK does for the input shape. The metadata would a novel member generated into the output shape. Metadata helpers would need some prefix, (e.g. SDK) to reduce collision potential.

My thought was that Metadata access helpers would be provided by the packages where middleware that produces said artifacts resides. Thoughts? Or were you thinking that we would have more specific metadata for services specifically?

@jasdel jasdel requested a review from skmcgrail April 21, 2020 20:02
@jasdel jasdel marked this pull request as ready for review April 24, 2020 17:35
@jasdel jasdel requested review from skotambkar and skmcgrail and removed request for skmcgrail April 24, 2020 18:29
@jasdel jasdel changed the base branch from master to feature/SmithyAPIClient April 27, 2020 19:01
@jasdel
Copy link
Contributor Author

jasdel commented Jun 12, 2020

Closing this since prototype is no longer needed. Using smithy to generate based on this design

@jasdel jasdel closed this Jun 12, 2020
@jasdel jasdel deleted the proto/APIClient branch June 12, 2020 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants