diff --git a/.fernignore b/.fernignore new file mode 100644 index 0000000..084a8eb --- /dev/null +++ b/.fernignore @@ -0,0 +1 @@ +# Specify files that shouldn't be modified by Fern diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4c0a5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Test + run: go test ./... diff --git a/.mock/definition/api.yml b/.mock/definition/api.yml new file mode 100644 index 0000000..bf7de50 --- /dev/null +++ b/.mock/definition/api.yml @@ -0,0 +1 @@ +name: generator-cli diff --git a/.mock/definition/feature.yml b/.mock/definition/feature.yml new file mode 100644 index 0000000..03bc978 --- /dev/null +++ b/.mock/definition/feature.yml @@ -0,0 +1,39 @@ +types: + FeatureId: + docs: | + A unique identifier for a feature (e.g. OPTIONALS). This is typed as a freeform string + to allow for arbitrary features, but callers are expected to use the FeatureType + string representation whenever possible. + type: string + + FeatureType: + docs: | + Unique identifiers for features that can be demonstrated with snippets. + enum: + - AUTHENTICATION + - ERRORS + - USAGE + - PAGINATION + - RETRIES + - REQUEST_OPTIONS + - STREAMING + - TIMEOUTS + + FeatureConfig: + docs: | + The configuration used to specify a generator's set of supported features. + This is static data associated with a particular version of a generator, and + is expected to be written as a static features.yml file in the generator's + repository. + properties: + features: list + + FeatureSpec: + docs: | + A specification for a feature supported by a generator. This includes the + feature's ID, a description, and any additional information that should be + included in the README.md. + properties: + id: FeatureId + description: optional + addendum: optional diff --git a/.mock/definition/readme.yml b/.mock/definition/readme.yml new file mode 100644 index 0000000..de3c025 --- /dev/null +++ b/.mock/definition/readme.yml @@ -0,0 +1,114 @@ +imports: + feature: feature.yml +types: + ReadmeConfig: + docs: | + The configuration used to generate a README.md file. + + The information described here is a combination of user-defined information + (i.e. specified in the generators.yml), and dynamically generated information + that comes from each generator (i.e. features, requirements, and more). + properties: + language: LanguageInfo + organization: string + bannerLink: optional + docsLink: optional + requirements: optional> + features: + docs: | + Specifies the list of features supported by a specific generator. + The features are rendered in the order they're specified. + type: optional> + + ReadmeFeature: + docs: | + A single feature supported by a generator (e.g. PAGINATION). + properties: + id: feature.FeatureId + description: optional + addendum: optional + snippets: optional> + snippetsAreOptional: + docs: | + If true, the feature block should be rendered even if we don't receive a snippet for it. + This is useful for features that are always supported, but might not require a snippet + to explain. + type: boolean + + LanguageInfo: + docs: | + The language and its associated publish information (if any). + + This is used to generate badges, the installation guide, and determine what language to + use when surrounding the snippets in a code block. + union: + typescript: TypescriptInfo + python: PythonInfo + go: GoInfo + java: JavaInfo + ruby: RubyInfo + csharp: CsharpInfo + + TypescriptInfo: + properties: + title: literal<"TypeScript"> + format: literal<"ts"> + publishInfo: optional + + PythonInfo: + properties: + title: literal<"Python"> + format: literal<"python"> + publishInfo: optional + + GoInfo: + properties: + title: literal<"Go"> + format: literal<"go"> + publishInfo: optional + + JavaInfo: + properties: + title: literal<"Java"> + format: literal<"java"> + publishInfo: optional + + RubyInfo: + properties: + title: literal<"Ruby"> + format: literal<"ruby"> + publishInfo: optional + + CsharpInfo: + properties: + title: literal<"C#"> + format: literal<"csharp"> + publishInfo: optional + + NpmPublishInfo: + properties: + packageName: string + + PypiPublishInfo: + properties: + packageName: string + + GoPublishInfo: + properties: + owner: string + repo: string + version: string + + MavenPublishInfo: + properties: + artifact: string + group: string + version: string + + RubyGemsPublishInfo: + properties: + packageName: string + + NugetPublishInfo: + properties: + packageName: string diff --git a/.mock/fern.config.json b/.mock/fern.config.json new file mode 100644 index 0000000..37807fe --- /dev/null +++ b/.mock/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization" : "fern", + "version" : "0.0.23" +} \ No newline at end of file diff --git a/.mock/generators.yml b/.mock/generators.yml new file mode 100644 index 0000000..1c685b5 --- /dev/null +++ b/.mock/generators.yml @@ -0,0 +1,35 @@ +default-group: local +groups: + local: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.12.8-rc0 + output: + location: local-file-system + path: ../../../packages/generator-cli/src/configuration/generated + config: + noSerdeLayer: true + outputSourceFiles: true + neverThrowErrors: true + timeoutInSeconds: infinity + outputEsm: true + + sdk: + generators: + - name: fernapi/fern-typescript-node-sdk + version: 0.12.8-rc0 + output: + location: npm + url: npm.buildwithfern.com + package-name: "@fern-fern/generator-cli-sdk" + config: + includeUtilsOnUnionMembers: true + useBrandedStringAliases: true + + - name: fernapi/fern-go-sdk + version: 0.22.0 + github: + repository: fern-api/generator-cli-go + config: + union: v1 + packageName: generatorcli diff --git a/README.md b/README.md deleted file mode 100644 index ac9b46d..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# generator-cli-go -The Go library used to call Fern's generator-cli diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..8243e72 --- /dev/null +++ b/client/client.go @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/fern-api/generator-cli-go/core" + option "github.com/fern-api/generator-cli-go/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..244be33 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + option "github.com/fern-api/generator-cli-go/option" + assert "github.com/stretchr/testify/assert" + http "net/http" + testing "testing" + time "time" +) + +func TestNewClient(t *testing.T) { + t.Run("default", func(t *testing.T) { + c := NewClient() + assert.Empty(t, c.baseURL) + }) + + t.Run("base url", func(t *testing.T) { + c := NewClient( + option.WithBaseURL("test.co"), + ) + assert.Equal(t, "test.co", c.baseURL) + }) + + t.Run("http client", func(t *testing.T) { + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + c := NewClient( + option.WithHTTPClient(httpClient), + ) + assert.Empty(t, c.baseURL) + }) + + t.Run("http header", func(t *testing.T) { + header := make(http.Header) + header.Set("X-API-Tenancy", "test") + c := NewClient( + option.WithHTTPHeader(header), + ) + assert.Empty(t, c.baseURL) + assert.Equal(t, "test", c.header.Get("X-API-Tenancy")) + }) +} diff --git a/core/core.go b/core/core.go new file mode 100644 index 0000000..3e28f90 --- /dev/null +++ b/core/core.go @@ -0,0 +1,280 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} + +// WriteMultipartJSON writes the given value as a JSON part. +// This is used to serialize non-primitive multipart properties +// (i.e. lists, objects, etc). +func WriteMultipartJSON(writer *multipart.Writer, field string, value interface{}) error { + bytes, err := json.Marshal(value) + if err != nil { + return err + } + return writer.WriteField(field, string(bytes)) +} + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, err error) *APIError { + return &APIError{ + err: err, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *APIError). +type ErrorDecoder func(statusCode int, body io.Reader) error + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient HTTPClient = http.DefaultClient + if params.Client != nil { + httpClient = params.Client + } + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + return &Caller{ + client: httpClient, + retrier: NewRetrier(retryOptions...), + } +} + +// CallParams represents the parameters used to issue an API call. +type CallParams struct { + URL string + Method string + MaxAttempts uint + Headers http.Header + Client HTTPClient + Request interface{} + Response interface{} + ResponseIsOptional bool + ErrorDecoder ErrorDecoder +} + +// Call issues an API call according to the given call parameters. +func (c *Caller) Call(ctx context.Context, params *CallParams) error { + req, err := newRequest(ctx, params.URL, params.Method, params.Headers, params.Request) + if err != nil { + return err + } + + // If the call has been cancelled, don't issue the request. + if err := ctx.Err(); err != nil { + return err + } + + client := c.client + if params.Client != nil { + // Use the HTTP client scoped to the request. + client = params.Client + } + + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + + resp, err := c.retrier.Run( + client.Do, + req, + params.ErrorDecoder, + retryOptions..., + ) + if err != nil { + return err + } + + // Close the response body after we're done. + defer resp.Body.Close() + + // Check if the call was cancelled before we return the error + // associated with the call and/or unmarshal the response data. + if err := ctx.Err(); err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return decodeError(resp, params.ErrorDecoder) + } + + // Mutate the response parameter in-place. + if params.Response != nil { + if writer, ok := params.Response.(io.Writer); ok { + _, err = io.Copy(writer, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(params.Response) + } + if err != nil { + if err == io.EOF { + if params.ResponseIsOptional { + // The response is optional, so we should ignore the + // io.EOF error + return nil + } + return fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response) + } + return err + } + } + + return nil +} + +// newRequest returns a new *http.Request with all of the fields +// required to issue the call. +func newRequest( + ctx context.Context, + url string, + method string, + endpointHeaders http.Header, + request interface{}, +) (*http.Request, error) { + requestBody, err := newRequestBody(request) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set(contentTypeHeader, contentType) + for name, values := range endpointHeaders { + req.Header[name] = values + } + return req, nil +} + +// newRequestBody returns a new io.Reader that represents the HTTP request body. +func newRequestBody(request interface{}) (io.Reader, error) { + var requestBody io.Reader + if request != nil { + if body, ok := request.(io.Reader); ok { + requestBody = body + } else { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + requestBody = bytes.NewReader(requestBytes) + } + } + return requestBody, nil +} + +// decodeError decodes the error from the given HTTP response. Note that +// it's the caller's responsibility to close the response body. +func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { + if errorDecoder != nil { + // This endpoint has custom errors, so we'll + // attempt to unmarshal the error into a structured + // type based on the status code. + return errorDecoder(response.StatusCode, response.Body) + } + // This endpoint doesn't have any custom error + // types, so we just read the body as-is, and + // put it into a normal error. + bytes, err := io.ReadAll(response.Body) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + // The error didn't have a response body, + // so all we can do is return an error + // with the status code. + return NewAPIError(response.StatusCode, nil) + } + return NewAPIError(response.StatusCode, errors.New(string(bytes))) +} diff --git a/core/core_test.go b/core/core_test.go new file mode 100644 index 0000000..f476f9e --- /dev/null +++ b/core/core_test.go @@ -0,0 +1,284 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCase represents a single test case. +type TestCase struct { + description string + + // Server-side assertions. + giveMethod string + giveResponseIsOptional bool + giveHeader http.Header + giveErrorDecoder ErrorDecoder + giveRequest *Request + + // Client-side assertions. + wantResponse *Response + wantError error +} + +// Request a simple request body. +type Request struct { + Id string `json:"id"` +} + +// Response a simple response body. +type Response struct { + Id string `json:"id"` +} + +// NotFoundError represents a 404. +type NotFoundError struct { + *APIError + + Message string `json:"message"` +} + +func TestCall(t *testing.T) { + tests := []*TestCase{ + { + description: "GET success", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + wantResponse: &Response{ + Id: "123", + }, + }, + { + description: "GET not found", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusNotFound), + }, + giveErrorDecoder: newTestErrorDecoder(t), + wantError: &NotFoundError{ + APIError: NewAPIError( + http.StatusNotFound, + errors.New(`{"message":"ID \"404\" not found"}`), + ), + }, + }, + { + description: "POST optional response", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + giveResponseIsOptional: true, + }, + { + description: "POST API error", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusInternalServerError), + }, + wantError: NewAPIError( + http.StatusInternalServerError, + errors.New("failed to process request"), + ), + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + var ( + server = newTestServer(t, test) + client = server.Client() + ) + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + var response *Response + err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: test.giveMethod, + Headers: test.giveHeader, + Request: test.giveRequest, + Response: &response, + ResponseIsOptional: test.giveResponseIsOptional, + ErrorDecoder: test.giveErrorDecoder, + }, + ) + if test.wantError != nil { + assert.EqualError(t, err, test.wantError.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +func TestMergeHeaders(t *testing.T) { + t.Run("both empty", func(t *testing.T) { + merged := MergeHeaders(make(http.Header), make(http.Header)) + assert.Empty(t, merged) + }) + + t.Run("empty left", func(t *testing.T) { + left := make(http.Header) + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("empty right", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.1") + + right := make(http.Header) + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("single value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.0") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) + + t.Run("multiple value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Versions", "0.0.0") + + right := make(http.Header) + right.Add("X-API-Versions", "0.0.1") + right.Add("X-API-Versions", "0.0.2") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions")) + }) + + t.Run("disjoint merge", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Tenancy", "test") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy")) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) +} + +// newTestServer returns a new *httptest.Server configured with the +// given test parameters. +func newTestServer(t *testing.T, tc *TestCase) *httptest.Server { + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tc.giveMethod, r.Method) + assert.Equal(t, contentType, r.Header.Get(contentTypeHeader)) + for header, value := range tc.giveHeader { + assert.Equal(t, value, r.Header.Values(header)) + } + + bytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + + request := new(Request) + require.NoError(t, json.Unmarshal(bytes, request)) + + switch request.Id { + case strconv.Itoa(http.StatusNotFound): + notFoundError := &NotFoundError{ + APIError: &APIError{ + StatusCode: http.StatusNotFound, + }, + Message: fmt.Sprintf("ID %q not found", request.Id), + } + bytes, err = json.Marshal(notFoundError) + require.NoError(t, err) + + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(bytes) + require.NoError(t, err) + return + + case strconv.Itoa(http.StatusInternalServerError): + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write([]byte("failed to process request")) + require.NoError(t, err) + return + } + + if tc.giveResponseIsOptional { + w.WriteHeader(http.StatusOK) + return + } + + response := &Response{ + Id: request.Id, + } + bytes, err = json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(bytes) + require.NoError(t, err) + }, + ), + ) +} + +// newTestErrorDecoder returns an error decoder suitable for tests. +func newTestErrorDecoder(t *testing.T) func(int, io.Reader) error { + return func(statusCode int, body io.Reader) error { + raw, err := io.ReadAll(body) + require.NoError(t, err) + + var ( + apiError = NewAPIError(statusCode, errors.New(string(raw))) + decoder = json.NewDecoder(bytes.NewReader(raw)) + ) + if statusCode == http.StatusNotFound { + value := new(NotFoundError) + value.APIError = apiError + require.NoError(t, decoder.Decode(value)) + + return value + } + return apiError + } +} diff --git a/core/extra_properties.go b/core/extra_properties.go new file mode 100644 index 0000000..a6af3e1 --- /dev/null +++ b/core/extra_properties.go @@ -0,0 +1,141 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/core/extra_properties_test.go b/core/extra_properties_test.go new file mode 100644 index 0000000..dc66fcc --- /dev/null +++ b/core/extra_properties_test.go @@ -0,0 +1,228 @@ +package core + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/core/query.go b/core/query.go new file mode 100644 index 0000000..3febdc7 --- /dev/null +++ b/core/query.go @@ -0,0 +1,219 @@ +package core + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google/uuid" +) + +var ( + bytesType = reflect.TypeOf([]byte{}) + queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() + timeType = reflect.TypeOf(time.Time{}) + uuidType = reflect.TypeOf(uuid.UUID{}) +) + +// QueryEncoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type QueryEncoder interface { + EncodeQueryValues(key string, v *url.Values) error +} + +// QueryValues encodes url.Values from request objects. +// +// Note: This type is inspired by Google's query encoding library, but +// supports far less customization and is tailored to fit this SDK's use case. +// +// Ref: https://github.com/google/go-querystring +func QueryValues(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Skip unexported fields. + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + + name, opts := parseTag(tag) + if name == "" { + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(queryEncoderType) { + // If sv is a nil pointer and the custom encoder is defined on a non-pointer + // method receiver, set sv to the zero value of the underlying type + if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(QueryEncoder) + if err := m.EncodeQueryValues(name, &values); err != nil { + return err + } + continue + } + + // Recursively dereference pointers, but stop at nil pointers. + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType { + values.Add(name, valueString(sv, opts, sf)) + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + if sv.Len() == 0 { + // Skip if slice or array is empty. + continue + } + for i := 0; i < sv.Len(); i++ { + values.Add(name, valueString(sv.Index(i), opts, sf)) + } + continue + } + + if sv.Kind() == reflect.Struct { + if err := reflectValue(values, sv, name); err != nil { + return err + } + continue + } + + values.Add(name, valueString(sv, opts, sf)) + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if format := sf.Tag.Get("format"); format == "date" { + return t.Format("2006-01-02") + } + return t.Format(time.RFC3339) + } + + if v.Type() == uuidType { + u := v.Interface().(uuid.UUID) + return u.String() + } + + if v.Type() == bytesType { + b := v.Interface().([]byte) + return base64.StdEncoding.EncodeToString(b) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + type zeroable interface { + IsZero() bool + } + + if !v.IsZero() { + if z, ok := v.Interface().(zeroable); ok { + return z.IsZero() + } + } + + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer: + return false + } + + return false +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/core/query_test.go b/core/query_test.go new file mode 100644 index 0000000..aad030a --- /dev/null +++ b/core/query_test.go @@ -0,0 +1,160 @@ +package core + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueryValues(t *testing.T) { + t.Run("empty optional", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Empty(t, values) + }) + + t.Run("empty required", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Equal(t, "required=", values.Encode()) + }) + + t.Run("allow multiple", func(t *testing.T) { + type example struct { + Values []string `json:"values" url:"values"` + } + + values, err := QueryValues( + &example{ + Values: []string{"foo", "bar", "baz"}, + }, + ) + require.NoError(t, err) + assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode()) + }) + + t.Run("nested object", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + nestedValue := "nestedValue" + values, err := QueryValues( + &example{ + Required: "requiredValue", + Nested: &nested{ + Value: &nestedValue, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode()) + }) + + t.Run("url unspecified", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("url ignored", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound" url:"-"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("datetime", func(t *testing.T) { + type example struct { + DateTime time.Time `json:"dateTime" url:"dateTime"` + } + + values, err := QueryValues( + &example{ + DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode()) + }) + + t.Run("date", func(t *testing.T) { + type example struct { + Date time.Time `json:"date" url:"date" format:"date"` + } + + values, err := QueryValues( + &example{ + Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "date=1994-03-16", values.Encode()) + }) + + t.Run("optional time", func(t *testing.T) { + type example struct { + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("omitempty with non-pointer zero value", func(t *testing.T) { + type enum string + + type example struct { + Enum enum `json:"enum,omitempty" url:"enum,omitempty"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) +} diff --git a/core/request_option.go b/core/request_option.go new file mode 100644 index 0000000..41e2eb6 --- /dev/null +++ b/core/request_option.go @@ -0,0 +1,85 @@ +// This file was auto-generated by Fern from our API Definition. + +package core + +import ( + http "net/http" +) + +// RequestOption adapts the behavior of the client or an individual request. +type RequestOption interface { + applyRequestOptions(*RequestOptions) +} + +// RequestOptions defines all of the possible request options. +// +// This type is primarily used by the generated code and is not meant +// to be used directly; use the option package instead. +type RequestOptions struct { + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + MaxAttempts uint +} + +// NewRequestOptions returns a new *RequestOptions value. +// +// This function is primarily used by the generated code and is not meant +// to be used directly; use RequestOption instead. +func NewRequestOptions(opts ...RequestOption) *RequestOptions { + options := &RequestOptions{ + HTTPHeader: make(http.Header), + } + for _, opt := range opts { + opt.applyRequestOptions(options) + } + return options +} + +// ToHeader maps the configured request options into a http.Header used +// for the request(s). +func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() } + +func (r *RequestOptions) cloneHeader() http.Header { + headers := r.HTTPHeader.Clone() + headers.Set("X-Fern-Language", "Go") + headers.Set("X-Fern-SDK-Name", "github.com/fern-api/generator-cli-go") + headers.Set("X-Fern-SDK-Version", "v0.0.23") + return headers +} + +// BaseURLOption implements the RequestOption interface. +type BaseURLOption struct { + BaseURL string +} + +func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) { + opts.BaseURL = b.BaseURL +} + +// HTTPClientOption implements the RequestOption interface. +type HTTPClientOption struct { + HTTPClient HTTPClient +} + +func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPClient = h.HTTPClient +} + +// HTTPHeaderOption implements the RequestOption interface. +type HTTPHeaderOption struct { + HTTPHeader http.Header +} + +func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPHeader = h.HTTPHeader +} + +// MaxAttemptsOption implements the RequestOption interface. +type MaxAttemptsOption struct { + MaxAttempts uint +} + +func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxAttempts = m.MaxAttempts +} diff --git a/core/retrier.go b/core/retrier.go new file mode 100644 index 0000000..ea24916 --- /dev/null +++ b/core/retrier.go @@ -0,0 +1,166 @@ +package core + +import ( + "crypto/rand" + "math/big" + "net/http" + "time" +) + +const ( + defaultRetryAttempts = 2 + minRetryDelay = 500 * time.Millisecond + maxRetryDelay = 5000 * time.Millisecond +) + +// RetryOption adapts the behavior the *Retrier. +type RetryOption func(*retryOptions) + +// RetryFunc is a retriable HTTP function call (i.e. *http.Client.Do). +type RetryFunc func(*http.Request) (*http.Response, error) + +// WithMaxAttempts configures the maximum number of attempts +// of the *Retrier. +func WithMaxAttempts(attempts uint) RetryOption { + return func(opts *retryOptions) { + opts.attempts = attempts + } +} + +// Retrier retries failed requests a configurable number of times with an +// exponential back-off between each retry. +type Retrier struct { + attempts uint +} + +// NewRetrier constructs a new *Retrier with the given options, if any. +func NewRetrier(opts ...RetryOption) *Retrier { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + attempts := uint(defaultRetryAttempts) + if options.attempts > 0 { + attempts = options.attempts + } + return &Retrier{ + attempts: attempts, + } +} + +// Run issues the request and, upon failure, retries the request if possible. +// +// The request will be retried as long as the request is deemed retriable and the +// number of retry attempts has not grown larger than the configured retry limit. +func (r *Retrier) Run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + opts ...RetryOption, +) (*http.Response, error) { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + maxRetryAttempts := r.attempts + if options.attempts > 0 { + maxRetryAttempts = options.attempts + } + var ( + retryAttempt uint + previousError error + ) + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt, + previousError, + ) +} + +func (r *Retrier) run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + maxRetryAttempts uint, + retryAttempt uint, + previousError error, +) (*http.Response, error) { + if retryAttempt >= maxRetryAttempts { + return nil, previousError + } + + // If the call has been cancelled, don't issue the request. + if err := request.Context().Err(); err != nil { + return nil, err + } + + response, err := fn(request) + if err != nil { + return nil, err + } + + if r.shouldRetry(response) { + defer response.Body.Close() + + delay, err := r.retryDelay(retryAttempt) + if err != nil { + return nil, err + } + + time.Sleep(delay) + + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt+1, + decodeError(response, errorDecoder), + ) + } + + return response, nil +} + +// shouldRetry returns true if the request should be retried based on the given +// response status code. +func (r *Retrier) shouldRetry(response *http.Response) bool { + return response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusRequestTimeout || + response.StatusCode == http.StatusConflict || + response.StatusCode >= http.StatusInternalServerError +} + +// retryDelay calculates the delay time in milliseconds based on the retry attempt. +func (r *Retrier) retryDelay(retryAttempt uint) (time.Duration, error) { + // Apply exponential backoff. + delay := minRetryDelay + minRetryDelay*time.Duration(retryAttempt*retryAttempt) + + // Do not allow the number to exceed maxRetryDelay. + if delay > maxRetryDelay { + delay = maxRetryDelay + } + + // Apply some itter by randomizing the value in the range of 75%-100%. + max := big.NewInt(int64(delay / 4)) + jitter, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + + delay -= time.Duration(jitter.Int64()) + + // Never sleep less than the base sleep seconds. + if delay < minRetryDelay { + delay = minRetryDelay + } + + return delay, nil +} + +type retryOptions struct { + attempts uint +} diff --git a/core/stringer.go b/core/stringer.go new file mode 100644 index 0000000..000cf44 --- /dev/null +++ b/core/stringer.go @@ -0,0 +1,13 @@ +package core + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/core/time.go b/core/time.go new file mode 100644 index 0000000..d009ab3 --- /dev/null +++ b/core/time.go @@ -0,0 +1,137 @@ +package core + +import ( + "encoding/json" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + + *d = DateTime{t: &parsedTime} + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d2e8bbb --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/fern-api/generator-cli-go + +go 1.13 + +require ( + github.com/google/uuid v1.4.0 + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b3766d4 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/option/request_option.go b/option/request_option.go new file mode 100644 index 0000000..36d7f68 --- /dev/null +++ b/option/request_option.go @@ -0,0 +1,41 @@ +// This file was auto-generated by Fern from our API Definition. + +package option + +import ( + core "github.com/fern-api/generator-cli-go/core" + http "net/http" +) + +// RequestOption adapts the behavior of an indivdual request. +type RequestOption = core.RequestOption + +// WithBaseURL sets the base URL, overriding the default +// environment, if any. +func WithBaseURL(baseURL string) *core.BaseURLOption { + return &core.BaseURLOption{ + BaseURL: baseURL, + } +} + +// WithHTTPClient uses the given HTTPClient to issue the request. +func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption { + return &core.HTTPClientOption{ + HTTPClient: httpClient, + } +} + +// WithHTTPHeader adds the given http.Header to the request. +func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption { + return &core.HTTPHeaderOption{ + // Clone the headers so they can't be modified after the option call. + HTTPHeader: httpHeader.Clone(), + } +} + +// WithMaxAttempts configures the maximum number of retry attempts. +func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { + return &core.MaxAttemptsOption{ + MaxAttempts: attempts, + } +} diff --git a/pointer.go b/pointer.go new file mode 100644 index 0000000..81b9ff4 --- /dev/null +++ b/pointer.go @@ -0,0 +1,132 @@ +package generatorcli + +import ( + "time" + + "github.com/google/uuid" +) + +// Bool returns a pointer to the given bool value. +func Bool(b bool) *bool { + return &b +} + +// Byte returns a pointer to the given byte value. +func Byte(b byte) *byte { + return &b +} + +// Complex64 returns a pointer to the given complex64 value. +func Complex64(c complex64) *complex64 { + return &c +} + +// Complex128 returns a pointer to the given complex128 value. +func Complex128(c complex128) *complex128 { + return &c +} + +// Float32 returns a pointer to the given float32 value. +func Float32(f float32) *float32 { + return &f +} + +// Float64 returns a pointer to the given float64 value. +func Float64(f float64) *float64 { + return &f +} + +// Int returns a pointer to the given int value. +func Int(i int) *int { + return &i +} + +// Int8 returns a pointer to the given int8 value. +func Int8(i int8) *int8 { + return &i +} + +// Int16 returns a pointer to the given int16 value. +func Int16(i int16) *int16 { + return &i +} + +// Int32 returns a pointer to the given int32 value. +func Int32(i int32) *int32 { + return &i +} + +// Int64 returns a pointer to the given int64 value. +func Int64(i int64) *int64 { + return &i +} + +// Rune returns a pointer to the given rune value. +func Rune(r rune) *rune { + return &r +} + +// String returns a pointer to the given string value. +func String(s string) *string { + return &s +} + +// Uint returns a pointer to the given uint value. +func Uint(u uint) *uint { + return &u +} + +// Uint8 returns a pointer to the given uint8 value. +func Uint8(u uint8) *uint8 { + return &u +} + +// Uint16 returns a pointer to the given uint16 value. +func Uint16(u uint16) *uint16 { + return &u +} + +// Uint32 returns a pointer to the given uint32 value. +func Uint32(u uint32) *uint32 { + return &u +} + +// Uint64 returns a pointer to the given uint64 value. +func Uint64(u uint64) *uint64 { + return &u +} + +// Uintptr returns a pointer to the given uintptr value. +func Uintptr(u uintptr) *uintptr { + return &u +} + +// UUID returns a pointer to the given uuid.UUID value. +func UUID(u uuid.UUID) *uuid.UUID { + return &u +} + +// Time returns a pointer to the given time.Time value. +func Time(t time.Time) *time.Time { + return &t +} + +// MustParseDate attempts to parse the given string as a +// date time.Time, and panics upon failure. +func MustParseDate(date string) time.Time { + t, err := time.Parse("2006-01-02", date) + if err != nil { + panic(err) + } + return t +} + +// MustParseDateTime attempts to parse the given string as a +// datetime time.Time, and panics upon failure. +func MustParseDateTime(datetime string) time.Time { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + panic(err) + } + return t +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..0e8a69e --- /dev/null +++ b/types.go @@ -0,0 +1,1040 @@ +// This file was auto-generated by Fern from our API Definition. + +package generatorcli + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/fern-api/generator-cli-go/core" +) + +// The configuration used to specify a generator's set of supported features. +// This is static data associated with a particular version of a generator, and +// is expected to be written as a static features.yml file in the generator's +// repository. +type FeatureConfig struct { + Features []*FeatureSpec `json:"features,omitempty" url:"features,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (f *FeatureConfig) GetExtraProperties() map[string]interface{} { + return f.extraProperties +} + +func (f *FeatureConfig) UnmarshalJSON(data []byte) error { + type unmarshaler FeatureConfig + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *f = FeatureConfig(value) + + extraProperties, err := core.ExtractExtraProperties(data, *f) + if err != nil { + return err + } + f.extraProperties = extraProperties + + f._rawJSON = json.RawMessage(data) + return nil +} + +func (f *FeatureConfig) String() string { + if len(f._rawJSON) > 0 { + if value, err := core.StringifyJSON(f._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(f); err == nil { + return value + } + return fmt.Sprintf("%#v", f) +} + +// A unique identifier for a feature (e.g. OPTIONALS). This is typed as a freeform string +// to allow for arbitrary features, but callers are expected to use the FeatureType +// string representation whenever possible. +type FeatureId = string + +// A specification for a feature supported by a generator. This includes the +// feature's ID, a description, and any additional information that should be +// included in the README.md. +type FeatureSpec struct { + Id FeatureId `json:"id" url:"id"` + Description *string `json:"description,omitempty" url:"description,omitempty"` + Addendum *string `json:"addendum,omitempty" url:"addendum,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (f *FeatureSpec) GetExtraProperties() map[string]interface{} { + return f.extraProperties +} + +func (f *FeatureSpec) UnmarshalJSON(data []byte) error { + type unmarshaler FeatureSpec + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *f = FeatureSpec(value) + + extraProperties, err := core.ExtractExtraProperties(data, *f) + if err != nil { + return err + } + f.extraProperties = extraProperties + + f._rawJSON = json.RawMessage(data) + return nil +} + +func (f *FeatureSpec) String() string { + if len(f._rawJSON) > 0 { + if value, err := core.StringifyJSON(f._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(f); err == nil { + return value + } + return fmt.Sprintf("%#v", f) +} + +// Unique identifiers for features that can be demonstrated with snippets. +type FeatureType string + +const ( + FeatureTypeAuthentication FeatureType = "AUTHENTICATION" + FeatureTypeErrors FeatureType = "ERRORS" + FeatureTypeUsage FeatureType = "USAGE" + FeatureTypePagination FeatureType = "PAGINATION" + FeatureTypeRetries FeatureType = "RETRIES" + FeatureTypeRequestOptions FeatureType = "REQUEST_OPTIONS" + FeatureTypeStreaming FeatureType = "STREAMING" + FeatureTypeTimeouts FeatureType = "TIMEOUTS" +) + +func NewFeatureTypeFromString(s string) (FeatureType, error) { + switch s { + case "AUTHENTICATION": + return FeatureTypeAuthentication, nil + case "ERRORS": + return FeatureTypeErrors, nil + case "USAGE": + return FeatureTypeUsage, nil + case "PAGINATION": + return FeatureTypePagination, nil + case "RETRIES": + return FeatureTypeRetries, nil + case "REQUEST_OPTIONS": + return FeatureTypeRequestOptions, nil + case "STREAMING": + return FeatureTypeStreaming, nil + case "TIMEOUTS": + return FeatureTypeTimeouts, nil + } + var t FeatureType + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (f FeatureType) Ptr() *FeatureType { + return &f +} + +type CsharpInfo struct { + PublishInfo *NugetPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (c *CsharpInfo) GetExtraProperties() map[string]interface{} { + return c.extraProperties +} + +func (c *CsharpInfo) Title() string { + return c.title +} + +func (c *CsharpInfo) Format() string { + return c.format +} + +func (c *CsharpInfo) UnmarshalJSON(data []byte) error { + type embed CsharpInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*c), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *c = CsharpInfo(unmarshaler.embed) + c.title = "C#" + c.format = "csharp" + + extraProperties, err := core.ExtractExtraProperties(data, *c, "title", "format") + if err != nil { + return err + } + c.extraProperties = extraProperties + + c._rawJSON = json.RawMessage(data) + return nil +} + +func (c *CsharpInfo) MarshalJSON() ([]byte, error) { + type embed CsharpInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*c), + Title: "C#", + Format: "csharp", + } + return json.Marshal(marshaler) +} + +func (c *CsharpInfo) String() string { + if len(c._rawJSON) > 0 { + if value, err := core.StringifyJSON(c._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(c); err == nil { + return value + } + return fmt.Sprintf("%#v", c) +} + +type GoInfo struct { + PublishInfo *GoPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (g *GoInfo) GetExtraProperties() map[string]interface{} { + return g.extraProperties +} + +func (g *GoInfo) Title() string { + return g.title +} + +func (g *GoInfo) Format() string { + return g.format +} + +func (g *GoInfo) UnmarshalJSON(data []byte) error { + type embed GoInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*g), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *g = GoInfo(unmarshaler.embed) + g.title = "Go" + g.format = "go" + + extraProperties, err := core.ExtractExtraProperties(data, *g, "title", "format") + if err != nil { + return err + } + g.extraProperties = extraProperties + + g._rawJSON = json.RawMessage(data) + return nil +} + +func (g *GoInfo) MarshalJSON() ([]byte, error) { + type embed GoInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*g), + Title: "Go", + Format: "go", + } + return json.Marshal(marshaler) +} + +func (g *GoInfo) String() string { + if len(g._rawJSON) > 0 { + if value, err := core.StringifyJSON(g._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(g); err == nil { + return value + } + return fmt.Sprintf("%#v", g) +} + +type GoPublishInfo struct { + Owner string `json:"owner" url:"owner"` + Repo string `json:"repo" url:"repo"` + Version string `json:"version" url:"version"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (g *GoPublishInfo) GetExtraProperties() map[string]interface{} { + return g.extraProperties +} + +func (g *GoPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler GoPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *g = GoPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *g) + if err != nil { + return err + } + g.extraProperties = extraProperties + + g._rawJSON = json.RawMessage(data) + return nil +} + +func (g *GoPublishInfo) String() string { + if len(g._rawJSON) > 0 { + if value, err := core.StringifyJSON(g._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(g); err == nil { + return value + } + return fmt.Sprintf("%#v", g) +} + +type JavaInfo struct { + PublishInfo *MavenPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (j *JavaInfo) GetExtraProperties() map[string]interface{} { + return j.extraProperties +} + +func (j *JavaInfo) Title() string { + return j.title +} + +func (j *JavaInfo) Format() string { + return j.format +} + +func (j *JavaInfo) UnmarshalJSON(data []byte) error { + type embed JavaInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*j), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *j = JavaInfo(unmarshaler.embed) + j.title = "Java" + j.format = "java" + + extraProperties, err := core.ExtractExtraProperties(data, *j, "title", "format") + if err != nil { + return err + } + j.extraProperties = extraProperties + + j._rawJSON = json.RawMessage(data) + return nil +} + +func (j *JavaInfo) MarshalJSON() ([]byte, error) { + type embed JavaInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*j), + Title: "Java", + Format: "java", + } + return json.Marshal(marshaler) +} + +func (j *JavaInfo) String() string { + if len(j._rawJSON) > 0 { + if value, err := core.StringifyJSON(j._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(j); err == nil { + return value + } + return fmt.Sprintf("%#v", j) +} + +// The language and its associated publish information (if any). +// +// This is used to generate badges, the installation guide, and determine what language to +// use when surrounding the snippets in a code block. +type LanguageInfo struct { + Type string + Typescript *TypescriptInfo + Python *PythonInfo + Go *GoInfo + Java *JavaInfo + Ruby *RubyInfo + Csharp *CsharpInfo +} + +func (l *LanguageInfo) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + l.Type = unmarshaler.Type + switch unmarshaler.Type { + case "typescript": + value := new(TypescriptInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Typescript = value + case "python": + value := new(PythonInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Python = value + case "go": + value := new(GoInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Go = value + case "java": + value := new(JavaInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Java = value + case "ruby": + value := new(RubyInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Ruby = value + case "csharp": + value := new(CsharpInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + l.Csharp = value + } + return nil +} + +func (l LanguageInfo) MarshalJSON() ([]byte, error) { + if l.Typescript != nil { + return core.MarshalJSONWithExtraProperty(l.Typescript, "type", "typescript") + } + if l.Python != nil { + return core.MarshalJSONWithExtraProperty(l.Python, "type", "python") + } + if l.Go != nil { + return core.MarshalJSONWithExtraProperty(l.Go, "type", "go") + } + if l.Java != nil { + return core.MarshalJSONWithExtraProperty(l.Java, "type", "java") + } + if l.Ruby != nil { + return core.MarshalJSONWithExtraProperty(l.Ruby, "type", "ruby") + } + if l.Csharp != nil { + return core.MarshalJSONWithExtraProperty(l.Csharp, "type", "csharp") + } + return nil, fmt.Errorf("type %T does not define a non-empty union type", l) +} + +type LanguageInfoVisitor interface { + VisitTypescript(*TypescriptInfo) error + VisitPython(*PythonInfo) error + VisitGo(*GoInfo) error + VisitJava(*JavaInfo) error + VisitRuby(*RubyInfo) error + VisitCsharp(*CsharpInfo) error +} + +func (l *LanguageInfo) Accept(visitor LanguageInfoVisitor) error { + if l.Typescript != nil { + return visitor.VisitTypescript(l.Typescript) + } + if l.Python != nil { + return visitor.VisitPython(l.Python) + } + if l.Go != nil { + return visitor.VisitGo(l.Go) + } + if l.Java != nil { + return visitor.VisitJava(l.Java) + } + if l.Ruby != nil { + return visitor.VisitRuby(l.Ruby) + } + if l.Csharp != nil { + return visitor.VisitCsharp(l.Csharp) + } + return fmt.Errorf("type %T does not define a non-empty union type", l) +} + +type MavenPublishInfo struct { + Artifact string `json:"artifact" url:"artifact"` + Group string `json:"group" url:"group"` + Version string `json:"version" url:"version"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *MavenPublishInfo) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *MavenPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler MavenPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = MavenPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *MavenPublishInfo) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type NpmPublishInfo struct { + PackageName string `json:"packageName" url:"packageName"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (n *NpmPublishInfo) GetExtraProperties() map[string]interface{} { + return n.extraProperties +} + +func (n *NpmPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler NpmPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NpmPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + + n._rawJSON = json.RawMessage(data) + return nil +} + +func (n *NpmPublishInfo) String() string { + if len(n._rawJSON) > 0 { + if value, err := core.StringifyJSON(n._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +type NugetPublishInfo struct { + PackageName string `json:"packageName" url:"packageName"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (n *NugetPublishInfo) GetExtraProperties() map[string]interface{} { + return n.extraProperties +} + +func (n *NugetPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler NugetPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = NugetPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + + n._rawJSON = json.RawMessage(data) + return nil +} + +func (n *NugetPublishInfo) String() string { + if len(n._rawJSON) > 0 { + if value, err := core.StringifyJSON(n._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +type PypiPublishInfo struct { + PackageName string `json:"packageName" url:"packageName"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (p *PypiPublishInfo) GetExtraProperties() map[string]interface{} { + return p.extraProperties +} + +func (p *PypiPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler PypiPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *p = PypiPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *p) + if err != nil { + return err + } + p.extraProperties = extraProperties + + p._rawJSON = json.RawMessage(data) + return nil +} + +func (p *PypiPublishInfo) String() string { + if len(p._rawJSON) > 0 { + if value, err := core.StringifyJSON(p._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} + +type PythonInfo struct { + PublishInfo *PypiPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (p *PythonInfo) GetExtraProperties() map[string]interface{} { + return p.extraProperties +} + +func (p *PythonInfo) Title() string { + return p.title +} + +func (p *PythonInfo) Format() string { + return p.format +} + +func (p *PythonInfo) UnmarshalJSON(data []byte) error { + type embed PythonInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*p), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *p = PythonInfo(unmarshaler.embed) + p.title = "Python" + p.format = "python" + + extraProperties, err := core.ExtractExtraProperties(data, *p, "title", "format") + if err != nil { + return err + } + p.extraProperties = extraProperties + + p._rawJSON = json.RawMessage(data) + return nil +} + +func (p *PythonInfo) MarshalJSON() ([]byte, error) { + type embed PythonInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*p), + Title: "Python", + Format: "python", + } + return json.Marshal(marshaler) +} + +func (p *PythonInfo) String() string { + if len(p._rawJSON) > 0 { + if value, err := core.StringifyJSON(p._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(p); err == nil { + return value + } + return fmt.Sprintf("%#v", p) +} + +// The configuration used to generate a README.md file. +// +// The information described here is a combination of user-defined information +// (i.e. specified in the generators.yml), and dynamically generated information +// that comes from each generator (i.e. features, requirements, and more). +type ReadmeConfig struct { + Language *LanguageInfo `json:"language,omitempty" url:"language,omitempty"` + Organization string `json:"organization" url:"organization"` + BannerLink *string `json:"bannerLink,omitempty" url:"bannerLink,omitempty"` + DocsLink *string `json:"docsLink,omitempty" url:"docsLink,omitempty"` + Requirements []string `json:"requirements,omitempty" url:"requirements,omitempty"` + // Specifies the list of features supported by a specific generator. + // The features are rendered in the order they're specified. + Features []*ReadmeFeature `json:"features,omitempty" url:"features,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *ReadmeConfig) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *ReadmeConfig) UnmarshalJSON(data []byte) error { + type unmarshaler ReadmeConfig + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = ReadmeConfig(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *ReadmeConfig) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +// A single feature supported by a generator (e.g. PAGINATION). +type ReadmeFeature struct { + Id FeatureId `json:"id" url:"id"` + Description *string `json:"description,omitempty" url:"description,omitempty"` + Addendum *string `json:"addendum,omitempty" url:"addendum,omitempty"` + Snippets []string `json:"snippets,omitempty" url:"snippets,omitempty"` + // If true, the feature block should be rendered even if we don't receive a snippet for it. + // This is useful for features that are always supported, but might not require a snippet + // to explain. + SnippetsAreOptional bool `json:"snippetsAreOptional" url:"snippetsAreOptional"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *ReadmeFeature) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *ReadmeFeature) UnmarshalJSON(data []byte) error { + type unmarshaler ReadmeFeature + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = ReadmeFeature(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *ReadmeFeature) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type RubyGemsPublishInfo struct { + PackageName string `json:"packageName" url:"packageName"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *RubyGemsPublishInfo) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *RubyGemsPublishInfo) UnmarshalJSON(data []byte) error { + type unmarshaler RubyGemsPublishInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = RubyGemsPublishInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *RubyGemsPublishInfo) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type RubyInfo struct { + PublishInfo *RubyGemsPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *RubyInfo) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *RubyInfo) Title() string { + return r.title +} + +func (r *RubyInfo) Format() string { + return r.format +} + +func (r *RubyInfo) UnmarshalJSON(data []byte) error { + type embed RubyInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*r), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *r = RubyInfo(unmarshaler.embed) + r.title = "Ruby" + r.format = "ruby" + + extraProperties, err := core.ExtractExtraProperties(data, *r, "title", "format") + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *RubyInfo) MarshalJSON() ([]byte, error) { + type embed RubyInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*r), + Title: "Ruby", + Format: "ruby", + } + return json.Marshal(marshaler) +} + +func (r *RubyInfo) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type TypescriptInfo struct { + PublishInfo *NpmPublishInfo `json:"publishInfo,omitempty" url:"publishInfo,omitempty"` + title string + format string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (t *TypescriptInfo) GetExtraProperties() map[string]interface{} { + return t.extraProperties +} + +func (t *TypescriptInfo) Title() string { + return t.title +} + +func (t *TypescriptInfo) Format() string { + return t.format +} + +func (t *TypescriptInfo) UnmarshalJSON(data []byte) error { + type embed TypescriptInfo + var unmarshaler = struct { + embed + }{ + embed: embed(*t), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *t = TypescriptInfo(unmarshaler.embed) + t.title = "TypeScript" + t.format = "ts" + + extraProperties, err := core.ExtractExtraProperties(data, *t, "title", "format") + if err != nil { + return err + } + t.extraProperties = extraProperties + + t._rawJSON = json.RawMessage(data) + return nil +} + +func (t *TypescriptInfo) MarshalJSON() ([]byte, error) { + type embed TypescriptInfo + var marshaler = struct { + embed + Title string `json:"title"` + Format string `json:"format"` + }{ + embed: embed(*t), + Title: "TypeScript", + Format: "ts", + } + return json.Marshal(marshaler) +} + +func (t *TypescriptInfo) String() string { + if len(t._rawJSON) > 0 { + if value, err := core.StringifyJSON(t._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(t); err == nil { + return value + } + return fmt.Sprintf("%#v", t) +}