diff --git a/.github/workflows/abstractions-go.yml b/.github/workflows/abstractions-go.yml index 5b9b9d65c6..2621c0278e 100644 --- a/.github/workflows/abstractions-go.yml +++ b/.github/workflows/abstractions-go.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2.3.5 - uses: actions/setup-go@v2 with: - go-version: '^1.16.6' + go-version: '^1.17.2' - name: Install dependencies run: go install working-directory: ${{ env.relativePath }} diff --git a/.github/workflows/authentication-dotnet-azure.yml b/.github/workflows/authentication-dotnet-azure.yml index b9b38f15d5..ea9ef9ed01 100644 --- a/.github/workflows/authentication-dotnet-azure.yml +++ b/.github/workflows/authentication-dotnet-azure.yml @@ -49,7 +49,7 @@ jobs: name: drop path: | ${{ env.relativePath }}/src/bin/Release/*.nupkg - - run: git checkout ${{ env.relativePath }}/nuget.config + - run: rm ${{ env.relativePath }}/nuget.config if: always() deploy: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/authentication-go-azure.yml b/.github/workflows/authentication-go-azure.yml new file mode 100644 index 0000000000..a4427505cf --- /dev/null +++ b/.github/workflows/authentication-go-azure.yml @@ -0,0 +1,26 @@ +name: Go Authentication Azure + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: ['authentication/go/azure/**', '.github/workflows/**'] + pull_request: + paths: ['authentication/go/azure/**', '.github/workflows/**'] + +jobs: + build: + runs-on: ubuntu-latest + env: + relativePath: ./authentication/go/azure + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-go@v2 + with: + go-version: '^1.17.2' + - name: Install dependencies + run: go install + working-directory: ${{ env.relativePath }} + - name: Build SDK project + run: go build + working-directory: ${{ env.relativePath }} diff --git a/.github/workflows/http-dotnet-httpclient.yml b/.github/workflows/http-dotnet-httpclient.yml index 1c757856cc..bd1ec3faa4 100644 --- a/.github/workflows/http-dotnet-httpclient.yml +++ b/.github/workflows/http-dotnet-httpclient.yml @@ -49,7 +49,7 @@ jobs: name: drop path: | ${{ env.relativePath }}/src/bin/Release/*.nupkg - - run: git checkout ${{ env.relativePath }}/nuget.config + - run: rm ${{ env.relativePath }}/nuget.config if: always() deploy: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/http-go-nethttp.yml b/.github/workflows/http-go-nethttp.yml new file mode 100644 index 0000000000..47dee859d4 --- /dev/null +++ b/.github/workflows/http-go-nethttp.yml @@ -0,0 +1,26 @@ +name: Go Http NetHttp + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: ['http/go/nethttp/**', '.github/workflows/**'] + pull_request: + paths: ['http/go/nethttp/**', '.github/workflows/**'] + +jobs: + build: + runs-on: ubuntu-latest + env: + relativePath: ./http/go/nethttp + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-go@v2 + with: + go-version: '^1.17.2' + - name: Install dependencies + run: go install + working-directory: ${{ env.relativePath }} + - name: Build SDK project + run: go build + working-directory: ${{ env.relativePath }} diff --git a/.github/workflows/serialization-dotnet-json.yml b/.github/workflows/serialization-dotnet-json.yml index 21054e178d..c3afcac048 100644 --- a/.github/workflows/serialization-dotnet-json.yml +++ b/.github/workflows/serialization-dotnet-json.yml @@ -49,7 +49,7 @@ jobs: name: drop path: | ${{ env.relativePath }}/src/bin/Release/*.nupkg - - run: git checkout ${{ env.relativePath }}/nuget.config + - run: rm ${{ env.relativePath }}/nuget.config if: always() deploy: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} diff --git a/.github/workflows/serialization-go-json.yml b/.github/workflows/serialization-go-json.yml new file mode 100644 index 0000000000..3ea6d39da5 --- /dev/null +++ b/.github/workflows/serialization-go-json.yml @@ -0,0 +1,29 @@ +name: Go Serialization Json + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: ['serialization/go/json/**', '.github/workflows/**'] + pull_request: + paths: ['serialization/go/json/**', '.github/workflows/**'] + +jobs: + build: + runs-on: ubuntu-latest + env: + relativePath: ./serialization/go/json + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-go@v2 + with: + go-version: '^1.17.2' + - name: Install dependencies + run: go install + working-directory: ${{ env.relativePath }} + - name: Build SDK project + run: go build + working-directory: ${{ env.relativePath }} + - name: Run unit tests + run: go test + working-directory: ${{ env.relativePath }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 13214f550f..0dcfae627f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Adds Go authentication, http and serialization libraries and finalizes the generation #716 + ### Changed ## [0.0.11] - 2021-10-27 diff --git a/README.md b/README.md index 1d4ab043f2..a24810d51e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The following table provides an overview of the languages supported by Kiota and | Language | Generation | Abstractions | Serialization | Authentication | HTTP | Required tools & dependencies | | -------- | ---------- | ------------ | ------------- | -------------- | ---- | -------------- | | CSharp | [✔](https://github.com/microsoft/kiota/projects/5) | [✔](./abstractions/dotnet) | [JSON](./serialization/dotnet/json) | [Anonymous](./abstractions/dotnet/src/authentication/AnonymousAuthenticationProvider.cs), [Azure](./authentication/dotnet/azure) | [✔](./http/dotnet/httpclient) | [link](./docs/requiredtools/dotnet.md) | -| Go | [✔](https://github.com/microsoft/kiota/projects/8) | [✔](./abstractions/go) | ❌ | ❌ | ❌ | [link](./docs/requiredtools/go.md) | +| Go | [✔](https://github.com/microsoft/kiota/projects/8) | [✔](./abstractions/go) | [Azure](./authentication/go/azure) | [✔](./http/go/nethttp) | [JSON](./serialization/go/json) | [link](./docs/requiredtools/go.md) | | Java | [✔](https://github.com/microsoft/kiota/projects/7) | [✔](./abstractions/java) | [JSON](./serialization/java/json) | [Anonymous](./abstractions/java/lib/src/main/java/com/microsoft/kiota/authentication/AnonymousAuthenticationProvider.java), [Azure](./authentication/java/azure) | [✔](./http/java/okhttp) | [link](./docs/requiredtools/java.md) | | PHP | [❌](https://github.com/microsoft/kiota/projects/4) | [▶](https://github.com/microsoft/kiota/pull/321) | ❌ | ❌ | ❌ | | | Python | [❌](https://github.com/microsoft/kiota/projects/3) | ❌ | ❌ | ❌ | ❌ | | diff --git a/abstractions/go/README.md b/abstractions/go/README.md index bcb0618aac..8f36e2f05e 100644 --- a/abstractions/go/README.md +++ b/abstractions/go/README.md @@ -3,6 +3,9 @@ ![Go](https://github.com/microsoft/kiota/actions/workflows/abstractions-go.yml/badge.svg) - [ ] unit tests +- [ ] move to its own repo and implement [the guidelines](https://golang.org/doc/#developing-modules) to make referencing the module easier +- [ ] add doc.go +- [ ] rename module name, update reference and remove the replace directive ## Using the abstractions diff --git a/abstractions/go/api_client_builder.go b/abstractions/go/api_client_builder.go index 8f110e5531..efc9f7849f 100644 --- a/abstractions/go/api_client_builder.go +++ b/abstractions/go/api_client_builder.go @@ -4,6 +4,9 @@ import ( s "github.com/microsoft/kiota/abstractions/go/serialization" ) +// Registers the default serializer to the registry. +// Parameters: +// metaFactory: the factory function that creates the serialization writer factory. func RegisterDefaultSerializer(metaFactory func() s.SerializationWriterFactory) { factory := metaFactory() contentType, err := factory.GetValidContentType() @@ -12,6 +15,9 @@ func RegisterDefaultSerializer(metaFactory func() s.SerializationWriterFactory) } } +// Registers the default deserializer to the registry. +// Parameters: +// metaFactory: the factory function that creates the serialization reader factory. func RegisterDefaultDeserializer(metaFactory func() s.ParseNodeFactory) { factory := metaFactory() contentType, err := factory.GetValidContentType() diff --git a/abstractions/go/authentication/anonymous_authentication_provider.go b/abstractions/go/authentication/anonymous_authentication_provider.go index de7e1c175b..cb67a3ff83 100644 --- a/abstractions/go/authentication/anonymous_authentication_provider.go +++ b/abstractions/go/authentication/anonymous_authentication_provider.go @@ -2,9 +2,15 @@ package authentication import abs "github.com/microsoft/kiota/abstractions/go" +// This authentication provider does not perform any authentication. type AnonymousAuthenticationProvider struct { } -func (provider *AnonymousAuthenticationProvider) Authenticate(request abs.RequestInformation) error { +// Authenticates the Request information instance +// Parameters: +// request: Request information instance +// Returns: +// error: nil if authentication is successful, otherwise an error +func (provider *AnonymousAuthenticationProvider) AuthenticateRequest(request abs.RequestInformation) error { return nil } diff --git a/abstractions/go/authentication/authentication_provider.go b/abstractions/go/authentication/authentication_provider.go index 17164e4f39..317c687e4e 100644 --- a/abstractions/go/authentication/authentication_provider.go +++ b/abstractions/go/authentication/authentication_provider.go @@ -4,6 +4,12 @@ import ( abs "github.com/microsoft/kiota/abstractions/go" ) +// Authenticates the application request. type AuthenticationProvider interface { - Authenticate(request abs.RequestInformation) error + // Authenticates the Request information instance + // Parameters: + // request: Request information instance + // Returns: + // error: nil if authentication is successful, otherwise an error + AuthenticateRequest(request abs.RequestInformation) error } diff --git a/abstractions/go/authentication/base_bearer_token_authentication_provider.go b/abstractions/go/authentication/base_bearer_token_authentication_provider.go index e29ceedd1e..f148eb525f 100644 --- a/abstractions/go/authentication/base_bearer_token_authentication_provider.go +++ b/abstractions/go/authentication/base_bearer_token_authentication_provider.go @@ -8,20 +8,37 @@ import ( const authorizationHeader = "Authorization" +// Provides a base class for implementing AuthenticationProvider for Bearer token scheme. type BaseBearerTokenAuthenticationProvider struct { + // This method is called by the BaseBearerTokenAuthenticationProvider class to authenticate the request via the returned access token. + // Parameters: + // request: Request information instance + // Returns: + // string: Access token + // error: nil if authentication is successful, otherwise an error getAuthorizationToken func(request abs.RequestInformation) (string, error) } +// Creates a new instance of the BaseBearerTokenAuthenticationProvider class. +// Parameters: +// getAuthorizationToken: This method is called by the BaseBearerTokenAuthenticationProvider class to authenticate the request via the returned access token. +// Returns: +// *BaseBearerTokenAuthenticationProvider: A new instance of the BaseBearerTokenAuthenticationProvider class. func NewBaseBearerTokenAuthenticationProvider(getAuthorizationToken func(request abs.RequestInformation) (string, error)) *BaseBearerTokenAuthenticationProvider { return &BaseBearerTokenAuthenticationProvider{getAuthorizationToken} } -func (provider *BaseBearerTokenAuthenticationProvider) Authenticate(request abs.RequestInformation) error { +// Authenticates the Request information instance +// Parameters: +// request: Request information instance +// Returns: +// error: nil if authentication is successful, otherwise an error +func (provider *BaseBearerTokenAuthenticationProvider) AuthenticateRequest(request abs.RequestInformation) error { if request.Headers == nil { request.Headers = make(map[string]string) } if provider.getAuthorizationToken == nil { - return errors.New("This class is abstract, you need to derive from it and implement the GetAuthorizationToken method.") + return errors.New("this class is abstract, you need to derive from it and implement the GetAuthorizationToken method.") } if request.Headers[authorizationHeader] == "" { token, err := provider.getAuthorizationToken(request) @@ -29,7 +46,7 @@ func (provider *BaseBearerTokenAuthenticationProvider) Authenticate(request abs. return err } if token == "" { - return errors.New("Could not get an authorization token") + return errors.New("could not get an authorization token") } request.Headers[authorizationHeader] = "Bearer " + token } diff --git a/abstractions/go/http_method.go b/abstractions/go/http_method.go index ce0f9b76c3..ec3c09b2ef 100644 --- a/abstractions/go/http_method.go +++ b/abstractions/go/http_method.go @@ -1,19 +1,30 @@ package abstractions +// Represents the HTTP method used by a request. type HttpMethod int const ( + // The HTTP GET method. GET HttpMethod = iota + // The HTTP POST method. POST + // The HTTP PATCH method. PATCH + // The HTTP DELETE method. DELETE + // The HTTP OPTIONS method. OPTIONS + // The HTTP CONNECT method. CONNECT + // The HTTP PUT method. PUT + // The HTTP TRACE method. TRACE + // The HTTP HEAD method. HEAD ) +// Returns the string representation of the HTTP method. func (m HttpMethod) String() string { return []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS", "CONNECT", "PUT", "TRACE", "HEAD"}[m] } diff --git a/abstractions/go/query_parameters_base.go b/abstractions/go/query_parameters_base.go index a3254344aa..5363c34481 100644 --- a/abstractions/go/query_parameters_base.go +++ b/abstractions/go/query_parameters_base.go @@ -5,9 +5,15 @@ import ( "reflect" ) +// The base implementation of the Query Parameters type QueryParametersBase struct { } +// Vanity method to add the query parameters to the request query parameters dictionary. +// Parameters: +// - target: The target map to add the query parameters to. +// Returns: +// - error: An error if the target is nil. func (p *QueryParametersBase) AddQueryParameters(target map[string]string) error { if target == nil { return errors.New("target cannot be nil") diff --git a/abstractions/go/request_adapter.go b/abstractions/go/request_adapter.go index da6ae3772e..4d028a6884 100644 --- a/abstractions/go/request_adapter.go +++ b/abstractions/go/request_adapter.go @@ -4,12 +4,55 @@ import ( s "github.com/microsoft/kiota/abstractions/go/serialization" ) +// Service responsible for translating abstract Request Info into concrete native HTTP requests. type RequestAdapter interface { - SendAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) func() (s.Parsable, error) - SendCollectionAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) func() ([]s.Parsable, error) - SendPrimitiveAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) func() (interface{}, error) - SendPrimitiveCollectionAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) func() ([]interface{}, error) - SendNoContentAsync(requestInfo RequestInformation, responseHandler ResponseHandler) func() error - GetSerializationWriterFactory() (s.SerializationWriterFactory, error) + // Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. + // Parameters: + // - requestInfo: The RequestInformation object to use for the HTTP request. + // - constuctor: The factory for the result Parsable object + // - responseHandler: The response handler to use for the HTTP request instead of the default handler. + // Returns: + // - The deserialized response model. + // - An error if any. + SendAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) (s.Parsable, error) + // Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. + // Parameters: + // - requestInfo: The RequestInformation object to use for the HTTP request. + // - constuctor: The factory for the result Parsable object + // - responseHandler: The response handler to use for the HTTP request instead of the default handler. + // Returns: + // - The deserialized response model collection. + // - An error if any. + SendCollectionAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) ([]s.Parsable, error) + // Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. + // Parameters: + // - requestInfo: The RequestInformation object to use for the HTTP request. + // - typeName: The type name of the response model. + // - responseHandler: The response handler to use for the HTTP request instead of the default handler. + // Returns: + // - The deserialized response model. + // - An error if any. + SendPrimitiveAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) (interface{}, error) + // Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. + // Parameters: + // - requestInfo: The RequestInformation object to use for the HTTP request. + // - typeName: The type name of the response model. + // - responseHandler: The response handler to use for the HTTP request instead of the default handler. + // Returns: + // - The deserialized response model collection. + // - An error if any. + SendPrimitiveCollectionAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) ([]interface{}, error) + // Executes the HTTP request specified by the given RequestInformation with no return content. + // Parameters: + // - requestInfo: The RequestInformation object to use for the HTTP request. + // - responseHandler: The response handler to use for the HTTP request instead of the default handler. + // Returns: + // - An error if any. + SendNoContentAsync(requestInfo RequestInformation, responseHandler ResponseHandler) error + // Gets the serialization writer factory currently in use for the request adapter service. + // Returns: + // - The serialization writer factory currently in use for the request adapter service. + GetSerializationWriterFactory() s.SerializationWriterFactory + // Enables the backing store proxies for the SerializationWriters and ParseNodes in use. EnableBackingStore() } diff --git a/abstractions/go/request_information.go b/abstractions/go/request_information.go index d1e325f70b..bc72bb69ad 100644 --- a/abstractions/go/request_information.go +++ b/abstractions/go/request_information.go @@ -2,7 +2,6 @@ package abstractions import ( "errors" - "reflect" u "net/url" @@ -10,16 +9,22 @@ import ( t "github.com/yosida95/uritemplate/v3" ) -/* This type represents an abstract HTTP request. */ +// This type represents an abstract HTTP request. type RequestInformation struct { - Method HttpMethod - uri *u.URL - Headers map[string]string + // The HTTP method of the request. + Method HttpMethod + uri *u.URL + // The Request Headers. + Headers map[string]string + // The Query Parameters of the request. QueryParameters map[string]string - Content []byte - PathParameters map[string]string - UrlTemplate string - options map[string]RequestOption + // The Request Body. + Content []byte + // The path parameters to use for the URL template when generating the URI. + PathParameters map[string]string + // The Url template for the current request. + UrlTemplate string + options map[string]RequestOption } const raw_url_key = "request-raw-url" @@ -33,6 +38,10 @@ func NewRequestInformation() *RequestInformation { } } +// Get the URI of the request. +// Returns: +// - The URI of the request. +// - An error if the URI cannot be retrieved. func (request *RequestInformation) GetUri() (*u.URL, error) { if request.uri != nil { return request.uri, nil @@ -47,10 +56,7 @@ func (request *RequestInformation) GetUri() (*u.URL, error) { if err != nil { return nil, err } - err = request.SetUri(*uri) - if err != nil { - return nil, err - } + request.SetUri(*uri) return request.uri, nil } else { uriTemplate, err := t.New(request.UrlTemplate) @@ -73,7 +79,10 @@ func (request *RequestInformation) GetUri() (*u.URL, error) { } } -func (request *RequestInformation) SetUri(url u.URL) error { +// Sets the URI for the request from a raw URL. +// Parameters: +// - url: The raw URL to set the URI to. +func (request *RequestInformation) SetUri(url u.URL) { request.uri = &url for k := range request.PathParameters { delete(request.PathParameters, k) @@ -81,9 +90,13 @@ func (request *RequestInformation) SetUri(url u.URL) error { for k := range request.QueryParameters { delete(request.QueryParameters, k) } - return nil } +// Adds an option to the request. +// Parameters: +// - option: The option to add to the request. +// Returns: +// - An error if the option cannot be added. func (request *RequestInformation) AddRequestOptions(options ...RequestOption) error { if options == nil { return errors.New("RequestOptions cannot be nil") @@ -92,20 +105,23 @@ func (request *RequestInformation) AddRequestOptions(options ...RequestOption) e request.options = make(map[string]RequestOption, len(options)) } for _, option := range options { - tp := reflect.TypeOf(option) - name := tp.Name() - request.options[name] = option + request.options[option.GetKey().Key] = option } return nil } +// Gets the options for this request. Options are unique by type. If an option of the same type is added twice, the last one wins. +// Returns: +// - The options for this request. func (request *RequestInformation) GetRequestOptions() []RequestOption { if request.options == nil { return []RequestOption{} } result := make([]RequestOption, len(request.options)) + idx := 0 for _, option := range request.options { - result = append(result, option) + result[idx] = option + idx++ } return result } @@ -113,10 +129,21 @@ func (request *RequestInformation) GetRequestOptions() []RequestOption { const contentTypeHeader = "Content-Type" const binaryContentType = "application/octet-steam" +// Sets the request body to a binary stream. +// Parameters: +// - content: The binary stream to set the request body to. func (request *RequestInformation) SetStreamContent(content []byte) { request.Content = content request.Headers[contentTypeHeader] = binaryContentType } + +// Sets the request body from a model with the specified content type. +// Parameters: +// - requestAdapter: The request adapter to use to get the request body from the model. +// - contentType: The content type to set the request body to. +// - item: The model to set the request body from. +// Returns: +// - An error if the request body cannot be set. func (request *RequestInformation) SetContentFromParsable(requestAdapter RequestAdapter, contentType string, items ...s.Parsable) error { if contentType == "" { return errors.New("content type cannot be empty") @@ -125,10 +152,8 @@ func (request *RequestInformation) SetContentFromParsable(requestAdapter Request } else if len(items) == 0 { return errors.New("items cannot be nil or empty") } - factory, err := requestAdapter.GetSerializationWriterFactory() - if err != nil { - return err - } else if factory == nil { + factory := requestAdapter.GetSerializationWriterFactory() + if factory == nil { return errors.New("factory cannot be nil") } writer, err := factory.GetSerializationWriter(contentType) diff --git a/abstractions/go/request_option.go b/abstractions/go/request_option.go index 8c005f800f..134c9422c5 100644 --- a/abstractions/go/request_option.go +++ b/abstractions/go/request_option.go @@ -1,4 +1,9 @@ package abstractions +// Represents a request option. type RequestOption interface { + GetKey() RequestOptionKey +} +type RequestOptionKey struct { + Key string } diff --git a/abstractions/go/response_handler.go b/abstractions/go/response_handler.go index 62599c621a..83e3a8d5db 100644 --- a/abstractions/go/response_handler.go +++ b/abstractions/go/response_handler.go @@ -1,5 +1,12 @@ package abstractions +// Defines the contract for a response handler. type ResponseHandler interface { + // Callback method that is invoked when a response is received. + // Parameters: + // - response: The response received. Native response type from the HTTP library + // Returns: + // - The deserialized object model + // - An error if any. HandleResponse(response interface{}) (interface{}, error) } diff --git a/abstractions/go/serialization/parsable.go b/abstractions/go/serialization/parsable.go index 4e92287192..8c5fa04548 100644 --- a/abstractions/go/serialization/parsable.go +++ b/abstractions/go/serialization/parsable.go @@ -1,6 +1,25 @@ package serialization +// Defines a serializable model object. type Parsable interface { + // Writes the objects properties to the current writer. + // Parameters: + // - writer: the writer to write the properties to. + // Returns: + // - error: the error that occurred while writing the properties. Serialize(writer SerializationWriter) error + // Gets the deserialization information for this object. + // Returns: + // The deserialization information for this object. GetFieldDeserializers() map[string]func(interface{}, ParseNode) error + // Sets additional data of the object that doesn't belong to a field. + // Parameters: + // - data: the data to set. + SetAdditionalData(value map[string]interface{}) + // Gets additional data of the object that doesn't belong to a field. + // Returns: + // - data: the data. + GetAdditionalData() map[string]interface{} + // Returns whether the current object is nil or not. + IsNil() bool } diff --git a/abstractions/go/serialization/parse_node.go b/abstractions/go/serialization/parse_node.go index 63173ef798..530972401c 100644 --- a/abstractions/go/serialization/parse_node.go +++ b/abstractions/go/serialization/parse_node.go @@ -6,20 +6,91 @@ import ( "github.com/google/uuid" ) +// Interface for a deserialization node in a parse tree. This interace provides an abstraction layer over serialiation formats, libararies and implementations. type ParseNode interface { + // Gets a new parse node for the given identifier. + // Parameters: + // - index: The identifier of the new node. + // Returns: + // - The new node. + // - An error if any GetChildNode(index string) (ParseNode, error) - GetCollectionOfObjectValues(func() interface{}) ([]Parsable, error) + // Gets the collection of Parsable values from the node. + // Parameters: + // - ctor: the factory for the target type of parsable + // Returns: + // - The collection of parsable values. + // - An error if any + GetCollectionOfObjectValues(ctor func() Parsable) ([]Parsable, error) + // Gets the collection of primitive values from the node. + // Parameters: + // - targetType: the target primitive type + // Returns: + // - The collection of primitive values. + // - An error if any GetCollectionOfPrimitiveValues(targetType string) ([]interface{}, error) - GetCollectionOfEnumValues(func(string) (interface{}, error)) ([]interface{}, error) - GetObjectValue(func() interface{}) (Parsable, error) + // Gets the collection of Enum values from the node. + // Parameters: + // - parser: the parser method for the target enum + // Returns: + // - The collection of enum values. + // - An error if any + GetCollectionOfEnumValues(parser func(string) (interface{}, error)) ([]interface{}, error) + // Gets the Parsable value from the node. + // Parameters: + // - ctor: the factory for the target type of parsable + // Returns: + // - The parsable values. + // - An error if any + GetObjectValue(ctor func() Parsable) (Parsable, error) + // Gets a String value from the nodes. + // Returns: + // - A String value when available + // - An error if any GetStringValue() (*string, error) + // Gets a Bool value from the nodes. + // Returns: + // - A Bool value when available + // - An error if any GetBoolValue() (*bool, error) + // Gets a Float32 value from the nodes. + // Returns: + // - A Float32 value when available + // - An error if any GetFloat32Value() (*float32, error) + // Gets a Float64 value from the nodes. + // Returns: + // - A Float64 value when available + // - An error if any GetFloat64Value() (*float64, error) + // Gets a Int32 value from the nodes. + // Returns: + // - A Int32 value when available + // - An error if any GetInt32Value() (*int32, error) + // Gets a Int64 value from the nodes. + // Returns: + // - A Int64 value when available + // - An error if any GetInt64Value() (*int64, error) + // Gets a Time value from the nodes. + // Returns: + // - A Time value when available + // - An error if any GetTimeValue() (*time.Time, error) + // Gets a UUID value from the nodes. + // Returns: + // - A UUID value when available + // - An error if any GetUUIDValue() (*uuid.UUID, error) - GetEnumValue(func(string) (interface{}, error)) (interface{}, error) + // Gets a Enum value from the nodes. + // Returns: + // - A Enum value when available + // - An error if any + GetEnumValue(parser func(string) (interface{}, error)) (interface{}, error) + // Gets a ByteArray value from the nodes. + // Returns: + // - A ByteArray value when available + // - An error if any GetByteArrayValue() ([]byte, error) } diff --git a/abstractions/go/serialization/parse_node_factory.go b/abstractions/go/serialization/parse_node_factory.go index 0c565ad0bc..545f9a95d7 100644 --- a/abstractions/go/serialization/parse_node_factory.go +++ b/abstractions/go/serialization/parse_node_factory.go @@ -1,6 +1,15 @@ package serialization +// Defines the contract for a factory that creates parse nodes. type ParseNodeFactory interface { + // Returns the content type this factory's parse nodes can deserialize. GetValidContentType() (string, error) + // Get the ParseNode instance that is the root of the content + // Parameters: + // contentType: The content type of the content to be parsed + // content: The content to be parsed + // Returns: + // - A ParseNode instance that is the root of the content + // - An error if the content type is not supported GetRootParseNode(contentType string, content []byte) (ParseNode, error) } diff --git a/abstractions/go/serialization/parse_node_factory_registry.go b/abstractions/go/serialization/parse_node_factory_registry.go index 7bbb3573d1..8aa852c250 100644 --- a/abstractions/go/serialization/parse_node_factory_registry.go +++ b/abstractions/go/serialization/parse_node_factory_registry.go @@ -2,19 +2,28 @@ package serialization import "errors" -// implements Parse Node Factory +// This factory holds a list of all the registered factories for the various types of nodes. type ParseNodeFactoryRegistry struct { ContentTypeAssociatedFactories map[string]ParseNodeFactory } -var DefaultParseNodeFactoryInstance = ParseNodeFactoryRegistry{ +// Default singleton instance of the registry to be used when registring new factories that should be available by default. +var DefaultParseNodeFactoryInstance = &ParseNodeFactoryRegistry{ ContentTypeAssociatedFactories: make(map[string]ParseNodeFactory), } +// Gets the valid content type for the ParseNodeFactoryRegistry func (m *ParseNodeFactoryRegistry) GetValidContentType() (string, error) { - return "", errors.New("The registry supports multiple content types. Get the registered factory instead.") + return "", errors.New("the registry supports multiple content types. Get the registered factory instead") } +// Get the ParseNode instance that is the root of the content +// Parameters: +// contentType: The content type of the content to be parsed +// content: The content to be parsed +// Returns: +// - A ParseNode instance that is the root of the content +// - An error if the content type is not supported func (m *ParseNodeFactoryRegistry) GetRootParseNode(contentType string, content []byte) (ParseNode, error) { if contentType == "" { return nil, errors.New("contentType is required") @@ -24,7 +33,7 @@ func (m *ParseNodeFactoryRegistry) GetRootParseNode(contentType string, content } factory, ok := m.ContentTypeAssociatedFactories[contentType] if !ok { - return nil, errors.New("Content type " + contentType + " does not have a factory registered to be parsed") + return nil, errors.New("content type " + contentType + " does not have a factory registered to be parsed") } else { return factory.GetRootParseNode(contentType, content) } diff --git a/abstractions/go/serialization/serialization_writer.go b/abstractions/go/serialization/serialization_writer.go index 3b1c0ac430..abf8f3725c 100644 --- a/abstractions/go/serialization/serialization_writer.go +++ b/abstractions/go/serialization/serialization_writer.go @@ -2,29 +2,156 @@ package serialization import ( i "io" + "time" + + "github.com/google/uuid" ) +// Defines an interface for serialization of objects to a byte array. type SerializationWriter interface { i.Closer - WritePrimitiveValue(key string, value interface{}) error + // Writes a String value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the String value to write. + // Returns: + // - An error if any. + WriteStringValue(key string, value *string) error + // Writes a Bool value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Bool value to write. + // Returns: + // - An error if any. + WriteBoolValue(key string, value *bool) error + // Writes a Int32 value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Int32 value to write. + // Returns: + // - An error if any. + WriteInt32Value(key string, value *int32) error + // Writes a Int64 value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Int64 value to write. + // Returns: + // - An error if any. + WriteInt64Value(key string, value *int64) error + // Writes a Float32 value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Float32 value to write. + // Returns: + // - An error if any. + WriteFloat32Value(key string, value *float32) error + // Writes a Float64 value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Float64 value to write. + // Returns: + // - An error if any. + WriteFloat64Value(key string, value *float64) error + // Writes a ByteArray value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the ByteArray value to write. + // Returns: + // - An error if any. + WriteByteArrayValue(key string, value []byte) error + // Writes a Time value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Time value to write. + // Returns: + // - An error if any. + WriteTimeValue(key string, value *time.Time) error + // Writes a UUID value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the UUID value to write. + // Returns: + // - An error if any. + WriteUUIDValue(key string, value *uuid.UUID) error + // Writes a Parsable value to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - value - the Parsable value to write. + // Returns: + // - An error if any. WriteObjectValue(key string, item Parsable) error + // Writes a collection of Parsable values to the byte array. + // Parameters: + // - key - the key of the value to write (optional). + // - collection - the collection of Parsable value to write. + // Returns: + // - An error if any. WriteCollectionOfObjectValues(key string, collection []Parsable) error - WriteCollectionOfPrimitiveValues(key string, collection []interface{}) error + // Writes a collection of String values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfStringValues(key string, collection []string) error + // Writes a collection of Bool values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfBoolValues(key string, collection []bool) error + // Writes a collection of Int32 values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfInt32Values(key string, collection []int32) error + // Writes a collection of Int64 values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfInt64Values(key string, collection []int64) error + // Writes a collection of Float32 values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfFloat32Values(key string, collection []float32) error + // Writes a collection of Float64 values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfFloat64Values(key string, collection []float64) error + // Writes a collection of Time values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfTimeValues(key string, collection []time.Time) error + // Writes a collection of UUID values to the byte array. + // Parameters: + // - key - the key to write (optional). + // - collection - the collection to write. + // Returns: + // - An error if any. + WriteCollectionOfUUIDValues(key string, collection []uuid.UUID) error + // Gets the resulting byte array from the serialization writer. + // Returns: + // - The resulting byte array. + // - An error if any. GetSerializedContent() ([]byte, error) - WriteAdditionalData(map[string]interface{}) error -} - -func ConvertToArrayOfParsable(params ...interface{}) []Parsable { - var result []Parsable - for _, param := range params { - result = append(result, param.(Parsable)) - } - return result -} -func ConvertToArrayOfPrimitives(params ...interface{}) []interface{} { - var result []interface{} - for _, param := range params { - result = append(result, param) - } - return result + // Writes additional data to the byte array. + // Parameters: + // - value - the data to write. + // Returns: + // - An error if any. + WriteAdditionalData(value map[string]interface{}) error } diff --git a/abstractions/go/serialization/serialization_writer_factory.go b/abstractions/go/serialization/serialization_writer_factory.go index 8a7039c413..fc8a14358b 100644 --- a/abstractions/go/serialization/serialization_writer_factory.go +++ b/abstractions/go/serialization/serialization_writer_factory.go @@ -1,6 +1,17 @@ package serialization +// Defines the contract for a factory that creates SerializationWriter instances. type SerializationWriterFactory interface { + // The valid content type for the SerializationWriterFactoryRegistry + // Returns: + // - the content type + // - nil if the content type is valid GetValidContentType() (string, error) + // Get the relevant SerializationWriter instance for the given content type + // Parameters: + // - contentType: the content type + // Returns: + // - the SerializationWriter instance + // - an error if any GetSerializationWriter(contentType string) (SerializationWriter, error) } diff --git a/abstractions/go/serialization/serialization_writer_factory_registry.go b/abstractions/go/serialization/serialization_writer_factory_registry.go index 6be4235e24..816fe723a2 100644 --- a/abstractions/go/serialization/serialization_writer_factory_registry.go +++ b/abstractions/go/serialization/serialization_writer_factory_registry.go @@ -2,21 +2,34 @@ package serialization import "errors" -// implements the SerializationWriterFactory +// This factory holds a list of all the registered factories for the various types of nodes. type SerializationWriterFactoryRegistry struct { + // List of factories that are registered by content type. ContentTypeAssociatedFactories map[string]SerializationWriterFactory } -var DefaultSerializationWriterFactoryInstance = SerializationWriterFactoryRegistry{ +// Default singleton instance of the registry to be used when registring new factories that should be available by default. +var DefaultSerializationWriterFactoryInstance = &SerializationWriterFactoryRegistry{ ContentTypeAssociatedFactories: make(map[string]SerializationWriterFactory), } +// The valid content type for the SerializationWriterFactoryRegistry +// Returns: +// - the content type +// - nil if the content type is valid func (m *SerializationWriterFactoryRegistry) GetValidContentType() (string, error) { - return "", errors.New("The registry supports multiple content types. Get the registered factory instead.") + return "", errors.New("the registry supports multiple content types. Get the registered factory instead") } + +// Get the relevant SerializationWriter instance for the given content type +// Parameters: +// - contentType: the content type +// Returns: +// - the SerializationWriter instance +// - an error if any func (m *SerializationWriterFactoryRegistry) GetSerializationWriter(contentType string) (SerializationWriter, error) { if contentType == "" { - return nil, errors.New("The content type is empty") + return nil, errors.New("the content type is empty") } factory := m.ContentTypeAssociatedFactories[contentType] if factory == nil { diff --git a/authentication/README.md b/authentication/README.md index 8c4f176367..d29a05979f 100644 --- a/authentication/README.md +++ b/authentication/README.md @@ -4,5 +4,6 @@ The Kiota Authentication libraries are language specific libraries implementing Your project will need a reference to the abstraction package to build and run, the following languages are currently supported: - [Dotnet](./dotnet/azure): relies on [Azure identity](https://www.nuget.org/packages/Azure.Identity). +- [Go](./go/azure): relies on [Azure identity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity) - [Java](./java/azure) : relies on [Azure identity](https://docs.microsoft.com/en-us/java/api/overview/azure/identity-readme?view=azure-java-stable). - [TypeScript](./typescript/azure) : relies on [Azure identity](https://www.npmjs.com/package/@azure/identity). diff --git a/authentication/dotnet/azure/nuget.config b/authentication/dotnet/azure/nuget.config deleted file mode 100644 index c47e1a68da..0000000000 --- a/authentication/dotnet/azure/nuget.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/authentication/go/azure/README.md b/authentication/go/azure/README.md new file mode 100644 index 0000000000..a881f7a8fe --- /dev/null +++ b/authentication/go/azure/README.md @@ -0,0 +1,24 @@ +# To-do + +![Go](https://github.com/microsoft/kiota/actions/workflows/authentication-go-azure.yml/badge.svg) + +- [ ] unit tests +- [ ] move to its own repo and implement [the guidelines](https://golang.org/doc/#developing-modules) to make referencing the module easier +- [ ] add doc.go +- [ ] rename module name, update reference and remove the replace directive + +## Using the Azure Authentication library + +1. Navigate to the directory where `go.mod` is located for your project. +1. Run the following command: + + ```Shell + go get github.com/microsoft/kiota/authentication/go/azure + ``` + +1. In the code + + ```Golang + cred, err := azidentity.NewDeviceCodeCredential(nil) + authProvider, err := kiotaazure.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"User.Read"}) + ``` diff --git a/authentication/go/azure/azure_identity_authentication_provider.go b/authentication/go/azure/azure_identity_authentication_provider.go new file mode 100644 index 0000000000..321963c1c9 --- /dev/null +++ b/authentication/go/azure/azure_identity_authentication_provider.go @@ -0,0 +1,63 @@ +package microsoft_kiota_authentication_azure + +import ( + "context" + "errors" + + azcore "github.com/Azure/azure-sdk-for-go/sdk/azcore" + azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + abs "github.com/microsoft/kiota/abstractions/go" + auth "github.com/microsoft/kiota/abstractions/go/authentication" +) + +// The BaseBearerTokenAuthenticationProvider implementation that supports implementations of TokenCredential from Azure.Identity. +type AzureIdentityAuthenticationProvider struct { + auth.BaseBearerTokenAuthenticationProvider + scopes []string + credential azcore.TokenCredential +} + +// Creates a new instance of the AzureIdentityAuthenticationProvider. +// Parameters: +// credential: The TokenCredential implementation that will be used to acquire tokens. +// Returns: +// - The new instance of the AzureIdentityAuthenticationProvider. +// - An error if any. +func NewAzureIdentityAuthenticationProvider(credential azcore.TokenCredential) (*AzureIdentityAuthenticationProvider, error) { + return NewAzureIdentityAuthenticationProviderWithScopes(credential, nil) +} + +// Creates a new instance of the AzureIdentityAuthenticationProvider. +// Parameters: +// credential: The TokenCredential implementation that will be used to acquire tokens. +// scopes: The list of scopes that will be used to acquire the token. +// Returns: +// - The new instance of the AzureIdentityAuthenticationProvider. +// - An error if any. +func NewAzureIdentityAuthenticationProviderWithScopes(credential azcore.TokenCredential, scopes []string) (*AzureIdentityAuthenticationProvider, error) { + if credential == nil { + return nil, errors.New("credential cannot be nil") + } + baseBearer := auth.NewBaseBearerTokenAuthenticationProvider( + func(request abs.RequestInformation) (string, error) { + options := azpolicy.TokenRequestOptions{ + Scopes: scopes, + } + token, err := credential.GetToken(context.Background(), options) + if err != nil { + return "", err + } + return token.Token, nil + }) + result := &AzureIdentityAuthenticationProvider{ + BaseBearerTokenAuthenticationProvider: *baseBearer, + credential: credential, + scopes: scopes, + } + + if result.scopes == nil || len(result.scopes) == 0 { + result.scopes = []string{"https://graph.microsoft.com/.default"} //TODO: init from the request URL host instead for national clouds + } + + return result, nil +} diff --git a/authentication/go/azure/go.mod b/authentication/go/azure/go.mod new file mode 100644 index 0000000000..81aae2b39a --- /dev/null +++ b/authentication/go/azure/go.mod @@ -0,0 +1,17 @@ +module github.com/microsoft/kiota/authentication/azure/go + +replace github.com/microsoft/kiota/abstractions/go => ../../../abstractions/go +//TODO remove this replace once we "publish" the package + +go 1.16 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.1 // indirect + github.com/microsoft/kiota/abstractions/go v0.0.0-20211013091133-b793efa27646 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect + golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/authentication/go/azure/go.sum b/authentication/go/azure/go.sum new file mode 100644 index 0000000000..5321d3dff7 --- /dev/null +++ b/authentication/go/azure/go.sum @@ -0,0 +1,57 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 h1:OYa9vmRX2XC5GXRAzeggG12sF/z5D9Ahtdm9EJ00WN4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.1 h1:8XSiy/LSvjtFwpguk7m6yGLgGkWocluo8hLM5vtcpcg= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/microsoft/kiota/abstractions/go v0.0.0-20211013091133-b793efa27646 h1:+rVzziUHhq8Cu4QgbDOFhC/KbiS7qS1HwiZOs9SSINs= +github.com/microsoft/kiota/abstractions/go v0.0.0-20211013091133-b793efa27646/go.mod h1:pkpvjhr4d5pLAaRTPLdJYpfP2pOaXdwHLyDv3hTz6O4= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yosida95/uritemplate/v3 v3.0.1 h1:+Fs//CsT+x231WmUQhMHWMxZizMvpnkOVWop02mVCfs= +github.com/yosida95/uritemplate/v3 v3.0.1/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 h1:qOfNqBm5gk93LjGZo1MJaKY6Bph39zOKz1Hz2ogHj1w= +golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/authentication/java/azure/README.md b/authentication/java/azure/README.md index 29cf7d38ff..d56a5e4e2f 100644 --- a/authentication/java/azure/README.md +++ b/authentication/java/azure/README.md @@ -8,7 +8,7 @@ - [ ] javadoc - [ ] cobertura -## Using the core implementations +## Using the Azure Authentication library 1. In `build.gradle` in the `repositories` section: diff --git a/http/README.md b/http/README.md index c61a7d2600..1c56649827 100644 --- a/http/README.md +++ b/http/README.md @@ -4,5 +4,6 @@ The Kiota HTTP libraries are language specific libraries implementing the interf Your project will need a reference to the abstraction package to build and run, the following languages are currently supported: - [Dotnet](./dotnet/httpclient): relies on [HttpClient](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0). +- [Go](./do/nethttp): relies on [NetHttp](https://pkg.go.dev/net/http). - [Java](./java/okhttp) : relies on [OkHttp](https://square.github.io/okhttp/). - [TypeScript](./typescript/fetch) : relies on [cross-fetch](https://www.npmjs.com/package/cross-fetch). diff --git a/http/dotnet/httpclient/nuget.config b/http/dotnet/httpclient/nuget.config deleted file mode 100644 index c47e1a68da..0000000000 --- a/http/dotnet/httpclient/nuget.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/http/go/nethttp/README.md b/http/go/nethttp/README.md new file mode 100644 index 0000000000..836c85839f --- /dev/null +++ b/http/go/nethttp/README.md @@ -0,0 +1,23 @@ +# To-do + +![Go](https://github.com/microsoft/kiota/actions/workflows/http-go-nethttp.yml/badge.svg) + +- [ ] unit tests +- [ ] move to its own repo and implement [the guidelines](https://golang.org/doc/#developing-modules) to make referencing the module easier +- [ ] add doc.go +- [ ] rename module name, update reference and remove the replace directive + +## Using the net http implementation + +1. Navigate to the directory where `go.mod` is located for your project. +1. Run the following command: + + ```Shell + go get github.com/microsoft/kiota/http/go/nethttp + ``` + +1. Add the following code + + ```Golang + httpAdapter, err := NewNetHttpRequestAdapter(authProvider) + ``` diff --git a/http/go/nethttp/go.mod b/http/go/nethttp/go.mod new file mode 100644 index 0000000000..cfed49f5f2 --- /dev/null +++ b/http/go/nethttp/go.mod @@ -0,0 +1,12 @@ +module github.com/microsoft/kiota/http/go/nethttp + +replace github.com/microsoft/kiota/abstractions/go => ../../../abstractions/go + +//TODO remove this replace once we "publish" the package + +go 1.16 + +require ( + github.com/microsoft/kiota/abstractions/go v0.0.0-20211013091133-b793efa27646 // indirect + github.com/stretchr/testify v1.7.0 // indirect +) diff --git a/http/go/nethttp/go.sum b/http/go/nethttp/go.sum new file mode 100644 index 0000000000..249483e221 --- /dev/null +++ b/http/go/nethttp/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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.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= +github.com/yosida95/uritemplate/v3 v3.0.1 h1:+Fs//CsT+x231WmUQhMHWMxZizMvpnkOVWop02mVCfs= +github.com/yosida95/uritemplate/v3 v3.0.1/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http/go/nethttp/kiota_client_factory.go b/http/go/nethttp/kiota_client_factory.go new file mode 100644 index 0000000000..09195c6140 --- /dev/null +++ b/http/go/nethttp/kiota_client_factory.go @@ -0,0 +1,36 @@ +package nethttplibrary + +import ( + nethttp "net/http" +) + +// Create a new default net/http client with the options configured for the Kiota request adapter +// Returns: +// the client +func GetDefaultClient(middleware ...Middleware) *nethttp.Client { + client := getDefaultClientWithoutMiddleware() + client.Transport = NewCustomTransport(middleware...) + return client +} + +// used for internal unit testing +func getDefaultClientWithoutMiddleware() *nethttp.Client { + client := nethttp.DefaultClient //TODO add default configuration and new up the client instead of using the default + client.CheckRedirect = func(req *nethttp.Request, via []*nethttp.Request) error { + return nethttp.ErrUseLastResponse + } + return client +} + +// Creates a new default set of middlewares for the Kiota request adapter +// Parameters: +// options - the options to use for the middlewares +// Returns: +// the middlewares +func GetDefaultMiddlewares() []Middleware { + return []Middleware{ + NewRetryHandler(), + NewRedirectHandler(), + //TODO add additional middlewares + } +} diff --git a/http/go/nethttp/middleware.go b/http/go/nethttp/middleware.go new file mode 100644 index 0000000000..add08c1300 --- /dev/null +++ b/http/go/nethttp/middleware.go @@ -0,0 +1,15 @@ +package nethttplibrary + +import nethttp "net/http" + +// Middleware interface for cross cutting concerns with HTTP requests and responses. +type Middleware interface { + // intercepts the request and returns the response. The implementer MUST call pipeline.Next() + // Parameters: + // - the pipeline to be executed after the middleware + // - the request to be processed + // Returns: + // - the response + // - error if any + Intercept(Pipeline, *nethttp.Request) (*nethttp.Response, error) +} diff --git a/http/go/nethttp/nethttp_request_adapter.go b/http/go/nethttp/nethttp_request_adapter.go new file mode 100644 index 0000000000..1dacf6ace3 --- /dev/null +++ b/http/go/nethttp/nethttp_request_adapter.go @@ -0,0 +1,274 @@ +package nethttplibrary + +import ( + "bytes" + "errors" + "io/ioutil" + "strings" + + ctx "context" + nethttp "net/http" + + abs "github.com/microsoft/kiota/abstractions/go" + absauth "github.com/microsoft/kiota/abstractions/go/authentication" + absser "github.com/microsoft/kiota/abstractions/go/serialization" +) + +// NetHttpRequestAdapter implements the RequestAdapter interface using net/http +type NetHttpRequestAdapter struct { + // serializationWriterFactory is the factory used to create serialization writers + serializationWriterFactory absser.SerializationWriterFactory + // parseNodeFactory is the factory used to create parse nodes + parseNodeFactory absser.ParseNodeFactory + // httpClient is the client used to send requests + httpClient *nethttp.Client + // authenticationProvider is the provider used to authenticate requests + authenticationProvider absauth.AuthenticationProvider +} + +// NewNetHttpRequestAdapter creates a new NetHttpRequestAdapter with the given parameters +// Parameters: +// authenticationProvider: the provider used to authenticate requests +// Returns: +// a new NetHttpRequestAdapter +func NewNetHttpRequestAdapter(authenticationProvider absauth.AuthenticationProvider) (*NetHttpRequestAdapter, error) { + return NewNetHttpRequestAdapterWithParseNodeFactory(authenticationProvider, nil) +} + +// NewNetHttpRequestAdapterWithParseNodeFactory creates a new NetHttpRequestAdapter with the given parameters +// Parameters: +// authenticationProvider: the provider used to authenticate requests +// parseNodeFactory: the factory used to create parse nodes +// Returns: +// a new NetHttpRequestAdapter +func NewNetHttpRequestAdapterWithParseNodeFactory(authenticationProvider absauth.AuthenticationProvider, parseNodeFactory absser.ParseNodeFactory) (*NetHttpRequestAdapter, error) { + return NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactory(authenticationProvider, parseNodeFactory, nil) +} + +// NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactory creates a new NetHttpRequestAdapter with the given parameters +// Parameters: +// authenticationProvider: the provider used to authenticate requests +// parseNodeFactory: the factory used to create parse nodes +// serializationWriterFactory: the factory used to create serialization writers +// Returns: +// a new NetHttpRequestAdapter +func NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactory(authenticationProvider absauth.AuthenticationProvider, parseNodeFactory absser.ParseNodeFactory, serializationWriterFactory absser.SerializationWriterFactory) (*NetHttpRequestAdapter, error) { + return NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(authenticationProvider, parseNodeFactory, serializationWriterFactory, nil) +} + +// NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient creates a new NetHttpRequestAdapter with the given parameters +// Parameters: +// authenticationProvider: the provider used to authenticate requests +// parseNodeFactory: the factory used to create parse nodes +// serializationWriterFactory: the factory used to create serialization writers +// httpClient: the client used to send requests +// Returns: +// a new NetHttpRequestAdapter +func NewNetHttpRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient(authenticationProvider absauth.AuthenticationProvider, parseNodeFactory absser.ParseNodeFactory, serializationWriterFactory absser.SerializationWriterFactory, httpClient *nethttp.Client) (*NetHttpRequestAdapter, error) { + if authenticationProvider == nil { + return nil, errors.New("authenticationProvider cannot be nil") + } + result := &NetHttpRequestAdapter{ + serializationWriterFactory: serializationWriterFactory, + parseNodeFactory: parseNodeFactory, + httpClient: httpClient, + authenticationProvider: authenticationProvider, + } + if result.httpClient == nil { + defaultClient := GetDefaultClient() + result.httpClient = defaultClient + } + if result.serializationWriterFactory == nil { + result.serializationWriterFactory = absser.DefaultSerializationWriterFactoryInstance + } + if result.parseNodeFactory == nil { + result.parseNodeFactory = absser.DefaultParseNodeFactoryInstance + } + return result, nil +} +func (a *NetHttpRequestAdapter) GetSerializationWriterFactory() absser.SerializationWriterFactory { + return a.serializationWriterFactory +} +func (a *NetHttpRequestAdapter) EnableBackingStore() { + //TODO implement when backing store is available for go +} +func (a *NetHttpRequestAdapter) getHttpResponseMessage(requestInfo abs.RequestInformation) (*nethttp.Response, error) { + err := a.authenticationProvider.AuthenticateRequest(requestInfo) + if err != nil { + return nil, err + } + request, err := a.getRequestFromRequestInformation(requestInfo) + if err != nil { + return nil, err + } + return (*a.httpClient).Do(request) +} +func (a *NetHttpRequestAdapter) getResponsePrimaryContentType(response *nethttp.Response) string { + if response.Header == nil { + return "" + } + rawType := response.Header.Get("Content-Type") + splat := strings.Split(rawType, ";") + return strings.ToLower(splat[0]) +} +func (a *NetHttpRequestAdapter) getRequestFromRequestInformation(requestInfo abs.RequestInformation) (*nethttp.Request, error) { + uri, err := requestInfo.GetUri() + if err != nil { + return nil, err + } + request, err := nethttp.NewRequest(requestInfo.Method.String(), uri.String(), nil) + if err != nil { + return nil, err + } + if len(requestInfo.Content) > 0 { + reader := bytes.NewReader(requestInfo.Content) + request.Body = ioutil.NopCloser(reader) + } + if request.Header == nil { + request.Header = make(nethttp.Header) + } + if requestInfo.Headers != nil { + for key, value := range requestInfo.Headers { + request.Header.Set(key, value) + } + } + for _, value := range requestInfo.GetRequestOptions() { + request = request.WithContext(ctx.WithValue(request.Context(), value.GetKey(), value)) + } + return request, nil +} +func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, constructor func() absser.Parsable, responseHandler abs.ResponseHandler) (absser.Parsable, error) { + response, err := a.getHttpResponseMessage(requestInfo) + if err != nil { + return nil, err + } + if responseHandler != nil { + result, err := responseHandler.HandleResponse(response) + if err != nil { + return nil, err + } + return result.(absser.Parsable), nil + } else if response != nil { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + if err != nil { + return nil, err + } + result, err := parseNode.GetObjectValue(constructor) + return result, err + } else { + return nil, errors.New("response is nil") + } +} +func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInformation, constructor func() absser.Parsable, responseHandler abs.ResponseHandler) ([]absser.Parsable, error) { + response, err := a.getHttpResponseMessage(requestInfo) + if err != nil { + return nil, err + } + if responseHandler != nil { + result, err := responseHandler.HandleResponse(response) + if err != nil { + return nil, err + } + return result.([]absser.Parsable), nil + } else if response != nil { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + if err != nil { + return nil, err + } + result, err := parseNode.GetCollectionOfObjectValues(constructor) + return result, err + } else { + return nil, errors.New("response is nil") + } +} +func (a *NetHttpRequestAdapter) SendPrimitiveAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler) (interface{}, error) { + response, err := a.getHttpResponseMessage(requestInfo) + if err != nil { + return nil, err + } + if responseHandler != nil { + result, err := responseHandler.HandleResponse(response) + if err != nil { + return nil, err + } + return result.(absser.Parsable), nil + } else if response != nil { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + if err != nil { + return nil, err + } + switch typeName { + case "string": + return parseNode.GetStringValue() + case "float32": + return parseNode.GetFloat32Value() + case "float64": + return parseNode.GetFloat64Value() + case "int32": + return parseNode.GetInt32Value() + case "int64": + return parseNode.GetInt64Value() + case "bool": + return parseNode.GetBoolValue() + case "Time": + return parseNode.GetTimeValue() + case "UUID": + return parseNode.GetUUIDValue() + default: + return nil, errors.New("unsupported type") + } + } else { + return nil, errors.New("response is nil") + } +} +func (a *NetHttpRequestAdapter) SendPrimitiveCollectionAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler) ([]interface{}, error) { + response, err := a.getHttpResponseMessage(requestInfo) + if err != nil { + return nil, err + } + if responseHandler != nil { + result, err := responseHandler.HandleResponse(response) + if err != nil { + return nil, err + } + return result.([]interface{}), nil + } else if response != nil { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + if err != nil { + return nil, err + } + return parseNode.GetCollectionOfPrimitiveValues(typeName) + } else { + return nil, errors.New("response is nil") + } +} +func (a *NetHttpRequestAdapter) SendNoContentAsync(requestInfo abs.RequestInformation, responseHandler abs.ResponseHandler) error { + response, err := a.getHttpResponseMessage(requestInfo) + if err != nil { + return err + } + if responseHandler != nil { + _, err := responseHandler.HandleResponse(response) + return err + } else if response != nil { + return nil + } else { + return errors.New("response is nil") + } +} diff --git a/http/go/nethttp/pipeline.go b/http/go/nethttp/pipeline.go new file mode 100644 index 0000000000..4358eb7882 --- /dev/null +++ b/http/go/nethttp/pipeline.go @@ -0,0 +1,73 @@ +package nethttplibrary + +import nethttp "net/http" + +// Pipeline contract for middleware infrastructure +type Pipeline interface { + // Next moves the request object through middlewares in the pipeline + // Parameters: + // req: the request object + // Returns: + // the response object + // error: any error that occurred + Next(req *nethttp.Request) (*nethttp.Response, error) +} + +// custom transport for net/http with a middleware pipeline +type customTransport struct { + nethttp.Transport + // middleware pipeline in use for the client + middlewarePipeline *middlewarePipeline +} + +// middleware pipeline implementation using a roundtripper from net/http +type middlewarePipeline struct { + // index of the middleware beeing executed + middlewareIndex int + // the round tripper to use to execute the request + transport nethttp.RoundTripper + // the middlewares to execute + middlewares []Middleware +} + +func newMiddlewarePipeline(middlewares []Middleware) *middlewarePipeline { + return &middlewarePipeline{ + middlewareIndex: 0, + transport: nethttp.DefaultTransport, + middlewares: middlewares, + } +} + +func (pipeline *middlewarePipeline) incrementMiddlewareIndex() { + pipeline.middlewareIndex++ +} + +// Next moves the request object through middlewares in the pipeline +func (pipeline *middlewarePipeline) Next(req *nethttp.Request) (*nethttp.Response, error) { + if pipeline.middlewareIndex < len(pipeline.middlewares) { + middleware := pipeline.middlewares[pipeline.middlewareIndex] + + pipeline.incrementMiddlewareIndex() + return middleware.Intercept(pipeline, req) + } + + return pipeline.transport.RoundTrip(req) +} + +func (transport *customTransport) RoundTrip(req *nethttp.Request) (*nethttp.Response, error) { + return transport.middlewarePipeline.Next(req) +} + +// Creates a new custom transport for http client with the provided set of middleware +// Parameters: +// middlewares: the middlewares to use +// Returns: +// the custom transport +func NewCustomTransport(middlewares ...Middleware) *customTransport { + if len(middlewares) == 0 { + middlewares = GetDefaultMiddlewares() + } + return &customTransport{ + middlewarePipeline: newMiddlewarePipeline(middlewares), + } +} diff --git a/http/go/nethttp/pipeline_test.go b/http/go/nethttp/pipeline_test.go new file mode 100644 index 0000000000..496bc8c6d8 --- /dev/null +++ b/http/go/nethttp/pipeline_test.go @@ -0,0 +1,27 @@ +package nethttplibrary + +import ( + "net/http" + "testing" +) + +type TestMiddleware struct{} + +func (middleware TestMiddleware) Intercept(pipeline Pipeline, req *http.Request) (*http.Response, error) { + req.Header.Add("test", "test-header") + + return pipeline.Next(req) +} + +func TestCanInterceptRequests(t *testing.T) { + transport := NewCustomTransport(&TestMiddleware{}) + client := &http.Client{Transport: transport} + resp, _ := client.Get("https://example.com") + + expect := "test-header" + got := resp.Request.Header.Get("test") + + if expect != got { + t.Errorf("Expected: %v, but received: %v", expect, got) + } +} diff --git a/http/go/nethttp/redirect_handler.go b/http/go/nethttp/redirect_handler.go new file mode 100644 index 0000000000..8ff223bab2 --- /dev/null +++ b/http/go/nethttp/redirect_handler.go @@ -0,0 +1,152 @@ +package nethttplibrary + +import ( + "errors" + nethttp "net/http" + "net/url" + "strings" + + abs "github.com/microsoft/kiota/abstractions/go" +) + +// The redirect handler handles redirect responses and follows them according to the options specified. +type RedirectHandler struct { + // options to use when evaluating whether to redirect or not + options RedirectHandlerOptions +} + +// Creates a new redirect handler with the default options. +func NewRedirectHandler() *RedirectHandler { + return NewRedirectHandlerWithOptions(RedirectHandlerOptions{ + MaxRedirects: defaultMaxRedirects, + ShouldRedirect: func(req *nethttp.Request, res *nethttp.Response) bool { + return true + }, + }) +} + +// Creates a new redirect handler with the specified options. +// Parameters: +// options - the options to use when evaluating whether to redirect or not +func NewRedirectHandlerWithOptions(options RedirectHandlerOptions) *RedirectHandler { + return &RedirectHandler{options: options} +} + +// Options to use when evaluating whether to redirect or not. +type RedirectHandlerOptions struct { + // A callback that determines whether to redirect or not. + ShouldRedirect func(req *nethttp.Request, res *nethttp.Response) bool + // The maximum number of redirects to follow. + MaxRedirects int +} + +var redirectKeyValue = abs.RequestOptionKey{ + Key: "RedirectHandler", +} + +type redirectHandlerOptionsInt interface { + abs.RequestOption + GetShouldRedirect() func(req *nethttp.Request, res *nethttp.Response) bool + GetMaxRedirect() int +} + +// Returns the key value to be used when the option is added to the request context +func (options *RedirectHandlerOptions) GetKey() abs.RequestOptionKey { + return redirectKeyValue +} + +// Returns the redirection evaluation function. +func (options *RedirectHandlerOptions) GetShouldRedirect() func(req *nethttp.Request, res *nethttp.Response) bool { + return options.ShouldRedirect +} + +// Returns the maximum number of redirects to follow. +func (options *RedirectHandlerOptions) GetMaxRedirect() int { + if options == nil || options.MaxRedirects < 1 { + return defaultMaxRedirects + } else if options.MaxRedirects > absoluteMaxRedirects { + return absoluteMaxRedirects + } else { + return options.MaxRedirects + } +} + +const defaultMaxRedirects = 5 +const absoluteMaxRedirects = 20 +const movedPermanently = 301 +const found = 302 +const seeOther = 303 +const temporaryRedirect = 307 +const permanentRedirect = 308 +const locationHeader = "Location" + +func (middleware RedirectHandler) Intercept(pipeline Pipeline, req *nethttp.Request) (*nethttp.Response, error) { + response, err := pipeline.Next(req) + if err != nil { + return response, err + } + reqOption, ok := req.Context().Value(redirectKeyValue).(redirectHandlerOptionsInt) + if !ok { + reqOption = &middleware.options + } + return middleware.redirectRequest(pipeline, reqOption, req, response, 0) +} + +func (middleware RedirectHandler) redirectRequest(pipeline Pipeline, reqOption redirectHandlerOptionsInt, req *nethttp.Request, response *nethttp.Response, redirectCount int) (*nethttp.Response, error) { + shouldRedirect := reqOption.GetShouldRedirect() != nil && reqOption.GetShouldRedirect()(req, response) || reqOption.GetShouldRedirect() == nil + if middleware.isRedirectResponse(response) && + redirectCount < reqOption.GetMaxRedirect() && + shouldRedirect { + redirectCount++ + redirectRequest, err := middleware.getRedirectRequest(req, response) + if err != nil { + return response, err + } + result, err := pipeline.Next(redirectRequest) + if err != nil { + return result, err + } + return middleware.redirectRequest(pipeline, reqOption, redirectRequest, result, redirectCount) + } + return response, nil +} + +func (middleware RedirectHandler) isRedirectResponse(response *nethttp.Response) bool { + if response == nil { + return false + } + locationHeader := response.Header.Get(locationHeader) + if locationHeader == "" { + return false + } + statusCode := response.StatusCode + return statusCode == movedPermanently || statusCode == found || statusCode == seeOther || statusCode == temporaryRedirect || statusCode == permanentRedirect +} + +func (middleware RedirectHandler) getRedirectRequest(request *nethttp.Request, response *nethttp.Response) (*nethttp.Request, error) { + if request == nil || response == nil { + return nil, errors.New("request or response is nil") + } + locationHeaderValue := response.Header.Get(locationHeader) + if locationHeaderValue[0] == '/' { + locationHeaderValue = request.URL.Scheme + "://" + request.URL.Host + locationHeaderValue + } + result := request.Clone(request.Context()) + targetUrl, err := url.Parse(locationHeaderValue) + if err != nil { + return nil, err + } + result.URL = targetUrl + sameHost := strings.EqualFold(targetUrl.Host, request.URL.Host) + sameScheme := strings.EqualFold(targetUrl.Scheme, request.URL.Scheme) + if !sameHost || !sameScheme { + result.Header.Del("Authorization") + } + if response.StatusCode == seeOther { + result.Method = nethttp.MethodGet + result.Header.Del("Content-Type") + result.Header.Del("Content-Length") + result.Body = nil + } + return result, nil +} diff --git a/http/go/nethttp/redirect_handler_test.go b/http/go/nethttp/redirect_handler_test.go new file mode 100644 index 0000000000..f9695f862e --- /dev/null +++ b/http/go/nethttp/redirect_handler_test.go @@ -0,0 +1,114 @@ +package nethttplibrary + +import ( + nethttp "net/http" + httptest "net/http/httptest" + testing "testing" + + "strconv" + + assert "github.com/stretchr/testify/assert" +) + +func TestItCreatesANewRedirectHandler(t *testing.T) { + handler := NewRedirectHandler() + if handler == nil { + t.Error("handler is nil") + } +} + +func TestItDoesntRedirectWithoutMiddleware(t *testing.T) { + requestCount := int64(0) + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + requestCount++ + res.Header().Set("Location", "/"+strconv.FormatInt(requestCount, 10)) + res.WriteHeader(301) + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + client := getDefaultClientWithoutMiddleware() + resp, err := client.Do(req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, int64(1), requestCount) +} + +func TestItHonoursShouldRedirect(t *testing.T) { + requestCount := int64(0) + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + requestCount++ + res.Header().Set("Location", "/"+strconv.FormatInt(requestCount, 10)) + res.WriteHeader(301) + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRedirectHandlerWithOptions(RedirectHandlerOptions{ + ShouldRedirect: func(req *nethttp.Request, res *nethttp.Response) bool { + return false + }, + }) + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, int64(1), requestCount) +} + +func TestItHonoursMaxRedirect(t *testing.T) { + requestCount := int64(0) + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + requestCount++ + res.Header().Set("Location", "/"+strconv.FormatInt(requestCount, 10)) + res.WriteHeader(301) + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRedirectHandler() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, int64(defaultMaxRedirects+1), requestCount) +} + +func TestItStripsAuthorizationHeaderOnDifferentHost(t *testing.T) { + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + res.Header().Set("Location", "https://www.bing.com/") + res.WriteHeader(301) + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRedirectHandler() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + req.Header.Set("Authorization", "Bearer 12345") + client := getDefaultClientWithoutMiddleware() + resp, err := client.Do(req) + if err != nil { + t.Error(err) + } + result, err := handler.getRedirectRequest(req, resp) + if err != nil { + t.Error(err) + } + assert.NotNil(t, result) + assert.Equal(t, "", result.Header.Get("Authorization")) +} diff --git a/http/go/nethttp/retry_handler.go b/http/go/nethttp/retry_handler.go new file mode 100644 index 0000000000..fcd1e76971 --- /dev/null +++ b/http/go/nethttp/retry_handler.go @@ -0,0 +1,151 @@ +package nethttplibrary + +import ( + "math" + nethttp "net/http" + "strconv" + "time" + + abs "github.com/microsoft/kiota/abstractions/go" +) + +// The retry handler handles transient HTTP responses and retries the request given the retry options +type RetryHandler struct { + // default options to use when evaluating the response + options RetryHandlerOptions +} + +// Creates a new RetryHandler with default options +func NewRetryHandler() *RetryHandler { + return NewRetryHandlerWithOptions(RetryHandlerOptions{ + ShouldRetry: func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool { + return true + }, + }) +} + +// Creates a new RetryHandler with the given options +// Parameters: +// options: the options to use for the RetryHandler during execution +func NewRetryHandlerWithOptions(options RetryHandlerOptions) *RetryHandler { + return &RetryHandler{options: options} +} + +const defaultMaxRetries = 3 +const absoluteMaxRetries = 10 +const defaultDelaySeconds = 3 +const absoluteMaxDelaySeconds = 180 + +// Options to apply when evaluating the response for retrial +type RetryHandlerOptions struct { + // Callback to determine if the request should be retried + ShouldRetry func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool + // The maximum number of times a request can be retried + MaxRetries int + // The delay in seconds between retries + DelaySeconds int +} + +type retryHandlerOptionsInt interface { + abs.RequestOption + GetShouldRetry() func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool + GetDelaySeconds() int + GetMaxRetries() int +} + +var retryKeyValue = abs.RequestOptionKey{ + Key: "RetryHandler", +} + +// Returns the key value to be used when the option is added to the request context +func (options *RetryHandlerOptions) GetKey() abs.RequestOptionKey { + return retryKeyValue +} + +// Returns the should retry callback function +func (options *RetryHandlerOptions) GetShouldRetry() func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool { + return options.ShouldRetry +} + +// Returns the delays in seconds between retries +func (options *RetryHandlerOptions) GetDelaySeconds() int { + if options.DelaySeconds < 1 { + return defaultDelaySeconds + } else if options.DelaySeconds > absoluteMaxDelaySeconds { + return absoluteMaxDelaySeconds + } else { + return options.DelaySeconds + } +} + +// Returns the maximum number of times a request can be retried +func (options *RetryHandlerOptions) GetMaxRetries() int { + if options.MaxRetries < 1 { + return defaultMaxRetries + } else if options.MaxRetries > absoluteMaxRetries { + return absoluteMaxRetries + } else { + return options.MaxRetries + } +} + +const retryAttemptHeader = "Retry-Attempt" +const retryAfterHeader = "Retry-After" + +const tooManyRequests = 429 +const serviceUnavailable = 503 +const gatewayTimeout = 504 + +func (middleware RetryHandler) Intercept(pipeline Pipeline, req *nethttp.Request) (*nethttp.Response, error) { + response, err := pipeline.Next(req) + if err != nil { + return response, err + } + reqOption, ok := req.Context().Value(retryKeyValue).(retryHandlerOptionsInt) + if !ok { + reqOption = &middleware.options + } + return middleware.retryRequest(pipeline, reqOption, req, response, 0, 0) +} + +func (middleware RetryHandler) retryRequest(pipeline Pipeline, options retryHandlerOptionsInt, req *nethttp.Request, resp *nethttp.Response, executionCount int, cummulativeDelay time.Duration) (*nethttp.Response, error) { + if middleware.isRetriableErrorCode(resp.StatusCode) && + middleware.isRetriableRequest(req) && + executionCount < options.GetMaxRetries() && + cummulativeDelay < time.Duration(absoluteMaxDelaySeconds)*time.Second && + options.GetShouldRetry()(cummulativeDelay, executionCount, req, resp) { + executionCount++ + delay := middleware.getRetryDelay(req, resp, options, executionCount) + cummulativeDelay += delay + req.Header.Set(retryAttemptHeader, strconv.Itoa(executionCount)) + time.Sleep(delay) + response, err := pipeline.Next(req) + if err != nil { + return response, err + } + return middleware.retryRequest(pipeline, options, req, response, executionCount, cummulativeDelay) + } + return resp, nil +} + +func (middleware RetryHandler) isRetriableErrorCode(code int) bool { + return code == tooManyRequests || code == serviceUnavailable || code == gatewayTimeout +} +func (middleware RetryHandler) isRetriableRequest(req *nethttp.Request) bool { + isBodiedMethod := req.Method == "POST" || req.Method == "PUT" || req.Method == "PATCH" + if isBodiedMethod && req.Body != nil { + return req.ContentLength != -1 + } + return true +} + +func (middleware RetryHandler) getRetryDelay(req *nethttp.Request, resp *nethttp.Response, options retryHandlerOptionsInt, executionCount int) time.Duration { + retryAfter := resp.Header.Get(retryAfterHeader) + if retryAfter != "" { + retryAfterDelay, err := strconv.ParseFloat(retryAfter, 64) + if err == nil { + return time.Duration(retryAfterDelay) * time.Second + } + } //TODO parse the header if it's a date + return time.Duration(math.Pow(float64(options.GetDelaySeconds()), float64(executionCount))) * time.Second +} diff --git a/http/go/nethttp/retry_handler_test.go b/http/go/nethttp/retry_handler_test.go new file mode 100644 index 0000000000..b328e4f998 --- /dev/null +++ b/http/go/nethttp/retry_handler_test.go @@ -0,0 +1,127 @@ +package nethttplibrary + +import ( + nethttp "net/http" + httptest "net/http/httptest" + testing "testing" + "time" + + "strconv" + + assert "github.com/stretchr/testify/assert" +) + +type NoopPipeline struct { + client *nethttp.Client +} + +func (pipeline *NoopPipeline) Next(req *nethttp.Request) (*nethttp.Response, error) { + return pipeline.client.Do(req) +} +func newNoopPipeline() *NoopPipeline { + return &NoopPipeline{ + client: getDefaultClientWithoutMiddleware(), + } +} +func TestItCreatesANewRetryHandler(t *testing.T) { + handler := NewRetryHandler() + if handler == nil { + t.Error("handler is nil") + } +} +func TestItAddsRetryAttemptHeaders(t *testing.T) { + retryAttemptInt := 0 + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + retryAttempt := req.Header.Get("Retry-Attempt") + if retryAttempt == "" { + res.WriteHeader(429) + } else { + res.WriteHeader(200) + retryAttemptInt, _ = strconv.Atoi(retryAttempt) + } + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRetryHandler() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, 1, retryAttemptInt) +} + +func TestItHonoursShouldRetry(t *testing.T) { + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + retryAttempt := req.Header.Get("Retry-Attempt") + if retryAttempt == "" { + res.WriteHeader(429) + } else { + res.WriteHeader(200) + } + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRetryHandlerWithOptions(RetryHandlerOptions{ + ShouldRetry: func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool { + return false + }, + }) + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, 429, resp.StatusCode) +} + +func TestItHonoursMaxRetries(t *testing.T) { + retryAttemptInt := -1 + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + res.WriteHeader(429) + retryAttemptInt++ + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRetryHandler() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, 429, resp.StatusCode) + assert.Equal(t, defaultMaxRetries, retryAttemptInt) +} + +func TestItDoesntRetryOnSuccess(t *testing.T) { + retryAttemptInt := -1 + testServer := httptest.NewServer(nethttp.HandlerFunc(func(res nethttp.ResponseWriter, req *nethttp.Request) { + res.WriteHeader(200) + retryAttemptInt++ + res.Write([]byte("body")) + })) + defer func() { testServer.Close() }() + handler := NewRetryHandler() + req, err := nethttp.NewRequest(nethttp.MethodGet, testServer.URL, nil) + if err != nil { + t.Error(err) + } + resp, err := handler.Intercept(newNoopPipeline(), req) + if err != nil { + t.Error(err) + } + assert.NotNil(t, resp) + assert.Equal(t, 0, retryAttemptInt) +} diff --git a/scripts/updateNugetCredentials.ps1 b/scripts/updateNugetCredentials.ps1 index 507138272b..3e9d2a7c4f 100644 --- a/scripts/updateNugetCredentials.ps1 +++ b/scripts/updateNugetCredentials.ps1 @@ -1,11 +1,8 @@ param([Parameter(Mandatory=$true)][string]$username, [Parameter(Mandatory=$true)][string]$apiToken, [Parameter(Mandatory=$true)][string]$nugetFileAbsolutePath) -if(Test-Path -Path $nugetFileAbsolutePath) { - [xml]$nugetConfigFileContent = Get-Content -Path $nugetFileAbsolutePath; - $userEntry = $nugetConfigFileContent.configuration.packageSourceCredentials.GitHub.add | ? {$_.key -eq "Username"} | select -First 1 - $userEntry.value = $username - $tokenEntry = $nugetConfigFileContent.configuration.packageSourceCredentials.GitHub.add | ? {$_.key -eq "ClearTextPassword"} | select -First 1 - $tokenEntry.value = $apiToken - $nugetConfigFileContent.Save($nugetFileAbsolutePath) -} else { - Write-Error -Message "Nuget config file not found, please check the path" -} \ No newline at end of file +$template = "" +[xml]$nugetConfigFileContent = [xml]$template +$userEntry = $nugetConfigFileContent.configuration.packageSourceCredentials.GitHub.add | Where-Object {$_.key -eq "Username"} | Select-Object -First 1 +$userEntry.value = $username +$tokenEntry = $nugetConfigFileContent.configuration.packageSourceCredentials.GitHub.add | Where-Object {$_.key -eq "ClearTextPassword"} | Select-Object -First 1 +$tokenEntry.value = $apiToken +Set-Content -Path $nugetFileAbsolutePath -Value $nugetConfigFileContent.InnerXml -Encoding UTF8 \ No newline at end of file diff --git a/serialization/README.md b/serialization/README.md index cad2cbfd78..6bbe04dd68 100644 --- a/serialization/README.md +++ b/serialization/README.md @@ -3,6 +3,7 @@ The Kiota Serialization libraries are language specific libraries implementing the serialization interfaces required by Kiota projects for diverse formats. Your project will need a reference to the abstraction package to build and run, the following languages are currently supported: -- [Dotnet](./dotnet/json): relies on [System.Text.Json](https://docs.microsoft.com/en-us/dotnet/api/system.text.json?view=net-5.0) for JSON serialization/deserialization +- [Dotnet](./dotnet/json): relies on [System.Text.Json](https://docs.microsoft.com/en-us/dotnet/api/system.text.json?view=net-5.0) for JSON serialization/deserialization. +- [Go](./go/json): relies on [encoding/json](https://pkg.go.dev/encoding/json) for JSON serialization/deserialization. - [Java](./java/json) : relies on [Gson](https://github.com/google/gson) for JSON serialization/deserialization. - [TypeScript](./typescript/json) : relies on the native JSON capabilities for JSON serialization/deserialization. diff --git a/serialization/dotnet/json/nuget.config b/serialization/dotnet/json/nuget.config deleted file mode 100644 index c47e1a68da..0000000000 --- a/serialization/dotnet/json/nuget.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/serialization/go/json/README.md b/serialization/go/json/README.md new file mode 100644 index 0000000000..38740a8414 --- /dev/null +++ b/serialization/go/json/README.md @@ -0,0 +1,17 @@ +# To-do + +![Go Serialization Json](https://github.com/microsoft/kiota/actions/workflows/serialization-go-json.yml/badge.svg) + +- [ ] coverage code +- [ ] analyzers +- [ ] unit test project +- [ ] docs comments + +## Using the JSON Serialization implementations + +1. Navigate to the directory where `go.mod` is located for your project. +1. Run the following command: + + ```Shell + go get github.com/microsoft/kiota/serialization/go/json + ``` diff --git a/serialization/go/json/go.mod b/serialization/go/json/go.mod index 21de24368c..33fa26dacc 100644 --- a/serialization/go/json/go.mod +++ b/serialization/go/json/go.mod @@ -1,3 +1,12 @@ -module github.com/microsoft/kiota/serialization/json/go +module github.com/microsoft/kiota/serialization/go/json + +replace github.com/microsoft/kiota/abstractions/go => ../../../abstractions/go + +//TODO remove this replace once we "publish" the package go 1.16 + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/microsoft/kiota/abstractions/go v0.0.0-20211013091133-b793efa27646 // indirect +) diff --git a/serialization/go/json/go.sum b/serialization/go/json/go.sum new file mode 100644 index 0000000000..cc0001fb7d --- /dev/null +++ b/serialization/go/json/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/yosida95/uritemplate/v3 v3.0.1 h1:+Fs//CsT+x231WmUQhMHWMxZizMvpnkOVWop02mVCfs= +github.com/yosida95/uritemplate/v3 v3.0.1/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= diff --git a/serialization/go/json/json_parse_node.go b/serialization/go/json/json_parse_node.go index 46289c48be..1b2b217d4b 100644 --- a/serialization/go/json/json_parse_node.go +++ b/serialization/go/json/json_parse_node.go @@ -1,4 +1,334 @@ -package json +package jsonserialization +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "io" + "time" + + "github.com/google/uuid" + absser "github.com/microsoft/kiota/abstractions/go/serialization" +) + +// ParseNode implementation for JSON. type JsonParseNode struct { + value interface{} +} + +// Creates a new JsonParseNode. +// Parameters: +// - content: the content of the node. +// Returns: +// - the new JsonParseNode. +// - An error if any. +func NewJsonParseNode(content []byte) (*JsonParseNode, error) { + if len(content) == 0 { + return nil, errors.New("content is empty") + } + decoder := json.NewDecoder(bytes.NewReader(content)) + value, err := loadJsonTree(decoder) + return value, err +} +func loadJsonTree(decoder *json.Decoder) (*JsonParseNode, error) { + for { + token, err := decoder.Token() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + switch token.(type) { + case json.Delim: + switch token.(json.Delim) { + case '{': + v := make(map[string]*JsonParseNode) + for decoder.More() { + key, err := decoder.Token() + if err != nil { + return nil, err + } + keyStr, ok := key.(string) + if !ok { + return nil, errors.New("key is not a string") + } + childNode, err := loadJsonTree(decoder) + if err != nil { + return nil, err + } + v[keyStr] = childNode + } + decoder.Token() // skip the closing curly + result := &JsonParseNode{value: v} + return result, nil + case '[': + v := make([]*JsonParseNode, 0) + for decoder.More() { + childNode, err := loadJsonTree(decoder) + if err != nil { + return nil, err + } + v = append(v, childNode) + } + decoder.Token() // skip the closing bracket + result := &JsonParseNode{value: v} + return result, nil + case ']': + case '}': + } + case json.Number: + number := token.(json.Number) + i, err := number.Int64() + c := &JsonParseNode{} + if err == nil { + c.SetValue(&i) + } else { + f, err := number.Float64() + if err == nil { + c.SetValue(&f) + } else { + return nil, err + } + } + return c, nil + case string: + v := token.(string) + c := &JsonParseNode{} + c.SetValue(&v) + return c, nil + case bool: + c := &JsonParseNode{} + v := token.(bool) + c.SetValue(&v) + return c, nil + case float64: + c := &JsonParseNode{} + v := token.(float64) + c.SetValue(&v) + return c, nil + case float32: + c := &JsonParseNode{} + v := token.(float32) + c.SetValue(&v) + return c, nil + case int32: + c := &JsonParseNode{} + v := token.(int32) + c.SetValue(&v) + return c, nil + case int64: + c := &JsonParseNode{} + v := token.(int64) + c.SetValue(&v) + return c, nil + case nil: + return nil, nil + default: + } + } + return nil, nil +} +func (n *JsonParseNode) SetValue(value interface{}) { + n.value = value +} +func (n *JsonParseNode) GetChildNode(index string) (absser.ParseNode, error) { + if index == "" { + return nil, errors.New("index is empty") + } + childNodes, ok := n.value.(map[string]*JsonParseNode) + if !ok || len(childNodes) == 0 { + return nil, errors.New("no child node available") + } + return childNodes[index], nil +} +func (n *JsonParseNode) GetObjectValue(ctor func() absser.Parsable) (absser.Parsable, error) { + if ctor == nil { + return nil, errors.New("constuctor is nil") + } + result := ctor() + //TODO onbefore when implementing backing store + properties, ok := n.value.(map[string]*JsonParseNode) + if !ok { + return nil, errors.New("value is not an object") + } + fields := result.GetFieldDeserializers() + if len(properties) != 0 { + for key, value := range properties { + field := fields[key] + if field == nil { + result.GetAdditionalData()[key] = value.value + } else { + err := field(result, value) + if err != nil { + return nil, err + } + } + } + } + //TODO on after when implmenting backing store + return result, nil +} +func (n *JsonParseNode) GetCollectionOfObjectValues(ctor func() absser.Parsable) ([]absser.Parsable, error) { + if n == nil || n.value == nil { + return nil, nil + } + if ctor == nil { + return nil, errors.New("ctor is nil") + } + nodes, ok := n.value.([]*JsonParseNode) + if !ok { + return nil, errors.New("value is not a collection") + } + result := make([]absser.Parsable, len(nodes)) + for i, v := range nodes { + val, err := (*v).GetObjectValue(ctor) + if err != nil { + return nil, err + } + result[i] = val + } + return result, nil +} +func (n *JsonParseNode) GetCollectionOfPrimitiveValues(targetType string) ([]interface{}, error) { + if n == nil || n.value == nil { + return nil, nil + } + if targetType == "" { + return nil, errors.New("targetType is empty") + } + nodes, ok := n.value.([]*JsonParseNode) + if !ok { + return nil, errors.New("value is not a collection") + } + result := make([]interface{}, len(nodes)) + for i, v := range nodes { + val, err := v.getPrimitiveValue(targetType) + if err != nil { + return nil, err + } + result[i] = val + } + return result, nil +} +func (n *JsonParseNode) getPrimitiveValue(targetType string) (interface{}, error) { + switch targetType { + case "string": + return n.GetStringValue() + case "bool": + return n.GetBoolValue() + case "float32": + return n.GetFloat32Value() + case "float64": + return n.GetFloat64Value() + case "int32": + return n.GetInt32Value() + case "int64": + return n.GetInt64Value() + case "time": + return n.GetTimeValue() + case "uuid": + return n.GetUUIDValue() + case "base64": + return n.GetByteArrayValue() + default: + return nil, errors.New("targetType is not supported") + } +} +func (n *JsonParseNode) GetCollectionOfEnumValues(parser func(string) (interface{}, error)) ([]interface{}, error) { + if n == nil || n.value == nil { + return nil, nil + } + if parser == nil { + return nil, errors.New("parser is nil") + } + nodes, ok := n.value.([]*JsonParseNode) + if !ok { + return nil, errors.New("value is not a collection") + } + result := make([]interface{}, len(nodes)) + for i, v := range nodes { + val, err := v.GetEnumValue(parser) + if err != nil { + return nil, err + } + result[i] = val + } + return result, nil +} +func (n *JsonParseNode) GetStringValue() (*string, error) { + if n == nil || n.value == nil { + return nil, nil + } + return n.value.(*string), nil +} +func (n *JsonParseNode) GetBoolValue() (*bool, error) { + if n == nil || n.value == nil { + return nil, nil + } + return n.value.(*bool), nil +} +func (n *JsonParseNode) GetFloat32Value() (*float32, error) { + v, err := n.GetFloat64Value() + if err != nil { + return nil, err + } + cast := float32(*v) + return &cast, nil +} +func (n *JsonParseNode) GetFloat64Value() (*float64, error) { + if n == nil || n.value == nil { + return nil, nil + } + return n.value.(*float64), nil +} +func (n *JsonParseNode) GetInt32Value() (*int32, error) { + v, err := n.GetFloat64Value() + if err != nil { + return nil, err + } + cast := int32(*v) + return &cast, nil +} +func (n *JsonParseNode) GetInt64Value() (*int64, error) { + v, err := n.GetFloat64Value() + if err != nil { + return nil, err + } + cast := int64(*v) + return &cast, nil +} +func (n *JsonParseNode) GetTimeValue() (*time.Time, error) { + v, err := n.GetStringValue() + if err != nil { + return nil, err + } + parsed, err := time.Parse(time.RFC3339, *v) + return &parsed, err +} +func (n *JsonParseNode) GetUUIDValue() (*uuid.UUID, error) { + v, err := n.GetStringValue() + if err != nil { + return nil, err + } + parsed, err := uuid.Parse(*v) + return &parsed, err +} +func (n *JsonParseNode) GetEnumValue(parser func(string) (interface{}, error)) (interface{}, error) { + if parser == nil { + return nil, errors.New("parser is nil") + } + s, err := n.GetStringValue() + if err != nil { + return nil, err + } + return parser(*s) +} +func (n *JsonParseNode) GetByteArrayValue() ([]byte, error) { + s, err := n.GetStringValue() + if err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(*s) } diff --git a/serialization/go/json/json_parse_node_factory.go b/serialization/go/json/json_parse_node_factory.go new file mode 100644 index 0000000000..c3aadc24d4 --- /dev/null +++ b/serialization/go/json/json_parse_node_factory.go @@ -0,0 +1,32 @@ +package jsonserialization + +import ( + "errors" + + absser "github.com/microsoft/kiota/abstractions/go/serialization" +) + +// ParseNodeFactory implementation for JSON +type JsonParseNodeFactory struct { +} + +// Creates a new JsonParseNodeFactory +func NewJsonParseNodeFactory() *JsonParseNodeFactory { + return &JsonParseNodeFactory{} +} + +func (f *JsonParseNodeFactory) GetValidContentType() (string, error) { + return "application/json", nil +} +func (f *JsonParseNodeFactory) GetRootParseNode(contentType string, content []byte) (absser.ParseNode, error) { + validType, err := f.GetValidContentType() + if err != nil { + return nil, err + } else if contentType == "" { + return nil, errors.New("contentType is empty") + } else if contentType != validType { + return nil, errors.New("contentType is not valid") + } else { + return NewJsonParseNode(content) + } +} diff --git a/serialization/go/json/json_parse_node_test.go b/serialization/go/json/json_parse_node_test.go new file mode 100644 index 0000000000..6e7773139c --- /dev/null +++ b/serialization/go/json/json_parse_node_test.go @@ -0,0 +1,126 @@ +package jsonserialization + +import ( + testing "testing" +) + +func TestTree(t *testing.T) { + source := "{\"someProp\": \"stringValue\",\"otherProp\": [1,2,3],\"objectProp\": {\"boolProp\": true}}" + sourceArray := []byte(source) + parseNode, err := NewJsonParseNode(sourceArray) + if err != nil { + t.Errorf("Error creating parse node: %s", err.Error()) + } + someProp, err := parseNode.GetChildNode("someProp") + if err != nil { + t.Errorf("Error getting child node: %s", err.Error()) + } + stringValue, err := someProp.GetStringValue() + if err != nil { + t.Errorf("Error getting string value: %s", err.Error()) + } + if *stringValue != "stringValue" { + t.Errorf("Expected value to be 'stringValue', got '%s'", *stringValue) + } + otherProp, err := parseNode.GetChildNode("otherProp") + if err != nil { + t.Errorf("Error getting child node: %s", err.Error()) + } + arrayValue, err := otherProp.GetCollectionOfPrimitiveValues("int32") + if err != nil { + t.Errorf("Error getting array value: %s", err.Error()) + } + if len(arrayValue) != 3 { + t.Errorf("Expected array to have 3 elements, got %d", len(arrayValue)) + } + if *(arrayValue[0].(*int32)) != 1 { + t.Errorf("Expected array element 0 to be 1, got %d", arrayValue[0]) + } + objectProp, err := parseNode.GetChildNode("objectProp") + if err != nil { + t.Errorf("Error getting child node: %s", err.Error()) + } + boolProp, err := objectProp.GetChildNode("boolProp") + if err != nil { + t.Errorf("Error getting child node: %s", err.Error()) + } + boolValue, err := boolProp.GetBoolValue() + if err != nil { + t.Errorf("Error getting boolean value: %s", err.Error()) + } + if !*boolValue { + t.Errorf("Expected value to be true, got false") + } +} + +func TestFunctional(t *testing.T) { + sourceArray := []byte(FunctionalTestSource) + parseNode, err := NewJsonParseNode(sourceArray) + if err != nil { + t.Errorf("Error creating parse node: %s", err.Error()) + } + if parseNode == nil { + t.Errorf("Expected parse node to be non-nil") + } +} + +const FunctionalTestSource = "{" + + "\"@odata.context\": \"https://graph.microsoft.com/v1.0/$metadata#users('vincent%40biret365.onmicrosoft.com')/messages\"," + + "\"@odata.nextLink\": \"https://graph.microsoft.com/v1.0/users/vincent@biret365.onmicrosoft.com/messages?$skip=10\"," + + "\"value\": [" + + "{" + + "\"@odata.etag\": \"W/\\\"CQAAABYAAAAs+XSiyjZdS4Rhtwk0v1pGAAA4Xv0v\\\"\"," + + "\"id\": \"AAMkAGNmMGZiNjM5LTZmMDgtNGU2OS1iYmUwLWYwZDc4M2ZkOGY1ZQBGAAAAAAAK20ulGawAT7z-yx90ohp-BwAs_XSiyjZdS4Rhtwk0v1pGAAAAAAEMAAAs_XSiyjZdS4Rhtwk0v1pGAAA4dw6TAAA=\"," + + "\"createdDateTime\": \"2021-10-14T09:19:01Z\"," + + "\"lastModifiedDateTime\": \"2021-10-14T09:19:03Z\"," + + "\"changeKey\": \"CQAAABYAAAAs+XSiyjZdS4Rhtwk0v1pGAAA4Xv0v\"," + + "\"categories\": []," + + "\"receivedDateTime\": \"2021-10-14T09:19:02Z\"," + + "\"sentDateTime\": \"2021-10-14T09:18:59Z\"," + + "\"hasAttachments\": false," + + "\"internetMessageId\": \"<608fed24166f421aa1e27a6c822074ba-JFBVALKQOJXWILKNK4YVA7CPGM3DKTLFONZWCZ3FINSW45DFOJ6E2ZLTONQWOZKDMVXHIZLSL5GUGMRZGEYDQOD4KNWXI4A=@microsoft.com>\"," + + "\"subject\": \"Major update from Message center\"," + + "\"bodyPreview\": \"(Updated) Microsoft 365 Compliance Center Core eDiscovery - Search by ID list retirementMC291088 · BIRET365Updated October 13, 2021: We have updated this message with additional details for clarity.We will be retiring the option to Search by ID,\"," + + "\"importance\": \"normal\"," + + "\"parentFolderId\": \"AQMkAGNmMGZiNjM5LTZmMDgtNGU2OS1iYgBlMC1mMGQ3ODNmZDhmNWUALgAAAwrbS6UZrABPvP-LH3SiGn8BACz5dKLKNl1LhGG3CTS-WkYAAAIBDAAAAA==\"," + + "\"conversationId\": \"AAQkAGNmMGZiNjM5LTZmMDgtNGU2OS1iYmUwLWYwZDc4M2ZkOGY1ZQAQANari86tqeZDsqpmA19AXLQ=\"," + + "\"conversationIndex\": \"AQHXwNyG1quLzq2p5kOyqmYDX0BctA==\"," + + "\"isDeliveryReceiptRequested\": null," + + "\"isReadReceiptRequested\": false," + + "\"isRead\": false," + + "\"isDraft\": false," + + "\"webLink\": \"https://outlook.office365.com/owa/?ItemID=AAMkAGNmMGZiNjM5LTZmMDgtNGU2OS1iYmUwLWYwZDc4M2ZkOGY1ZQBGAAAAAAAK20ulGawAT7z%2Fyx90ohp%2FBwAs%2BXSiyjZdS4Rhtwk0v1pGAAAAAAEMAAAs%2BXSiyjZdS4Rhtwk0v1pGAAA4dw6TAAA%3D&exvsurl=1&viewmodel=ReadMessageItem\"," + + "\"inferenceClassification\": \"other\"," + + "\"body\": {" + + "\"contentType\": \"html\"," + + "\"content\": \"
\\\"Microsoft\\\"
(Updated) Microsoft 365 Compliance Center Core eDiscovery - Search by ID list retirement
MC291088 · BIRET365

Updated October 13, 2021: We have updated this message with additional details for clarity.

We will be retiring the option to Search by ID list, as it is not functioning to an adequate level and creates significant challenges for organizations who depend on consistent and repeatable results for eDiscovery workflows.

When will this happen:

We will begin making this change in mid-November and expect to complete by the end of November.

How this will affect your organization:

You are receiving this message because our reporting indicates your organization may be using Search by ID list.

Once this change is made, the option to Search by ID list will be removed. We suggest focusing on search by query, condition and/or locations rather that ID.

What you need to do to prepare:

To fix this problem you need to review your eDiscovery search process, and update the workflow to focus on search by Subjects and dates rather than Search by ID list. Upon export from Core eDiscovery you can explore options to refine to only the messages of interest.

Click Additional Information to find out more.

Additional Information
You're subscribed to this email using vincent@biret365.onmicrosoft.com. If you're an IT admin, you're subscribed by default, but you can unsubscribe at any time. If you're not an IT admin, ask your admin to remove your email address from Microsoft 365 message center preferences.

How to view translated messages
This is a mandatory service communication. To set your contact preferences or to unsubcribe from other communications, visit the Promotional Communications Manager. Privacy statement.

Il s’agit de communications obligatoires. Pour configurer vos préférences de contact pour d’autres communications, accédez au gestionnaire de communications promotionnelles. Déclaration de confidentialité.
Microsoft Corporation, One Microsoft Way, Redmond WA 98052 USA
\\\"Microsoft\\\"
\\\"\\\" \"" + + "}," + + "\"sender\": {" + + "\"emailAddress\": {" + + "\"name\": \"Microsoft 365 Message center\"," + + "\"address\": \"o365mc@microsoft.com\"" + + "}" + + "}," + + "\"from\": {" + + "\"emailAddress\": {" + + "\"name\": \"Microsoft 365 Message center\"," + + "\"address\": \"o365mc@microsoft.com\"" + + "}" + + "}," + + "\"toRecipients\": [" + + "{" + + "\"emailAddress\": {" + + "\"name\": \"Vincent BIRET\"," + + "\"address\": \"vincent@biret365.onmicrosoft.com\"" + + "}" + + "}" + + "]," + + "\"ccRecipients\": []," + + "\"bccRecipients\": []," + + "\"replyTo\": []," + + "\"flag\": {" + + "\"flagStatus\": \"notFlagged\"" + + "}" + + "}" + + "]" + + "}" diff --git a/serialization/go/json/json_serialization_writer.go b/serialization/go/json/json_serialization_writer.go new file mode 100644 index 0000000000..3c306c5b64 --- /dev/null +++ b/serialization/go/json/json_serialization_writer.go @@ -0,0 +1,522 @@ +package jsonserialization + +import ( + "encoding/base64" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + + absser "github.com/microsoft/kiota/abstractions/go/serialization" +) + +// The SerializationWriter implementation for JSON. +type JsonSerializationWriter struct { + writer []string +} + +// Creates a new instance of the JsonSerializationWriter. +func NewJsonSerializationWriter() *JsonSerializationWriter { + return &JsonSerializationWriter{ + writer: make([]string, 0), + } +} +func (w *JsonSerializationWriter) writeRawValue(value string) { + w.writer = append(w.writer, value) +} +func (w *JsonSerializationWriter) writeStringValue(value string) { + w.writeRawValue("\"" + value + "\"") +} +func (w *JsonSerializationWriter) writePropertyName(key string) { + w.writeRawValue("\"" + key + "\":") +} +func (w *JsonSerializationWriter) writePropertySeparator() { + w.writeRawValue(",") +} +func (w *JsonSerializationWriter) trimLastPropertySeparator() { + writerLen := len(w.writer) + if writerLen > 0 && w.writer[writerLen-1] == "," { + w.writer = w.writer[:writerLen-1] + } +} +func (w *JsonSerializationWriter) writeArrayStart() { + w.writeRawValue("[") +} +func (w *JsonSerializationWriter) writeArrayEnd() { + w.writeRawValue("]") +} +func (w *JsonSerializationWriter) writeObjectStart() { + w.writeRawValue("{") +} +func (w *JsonSerializationWriter) writeObjectEnd() { + w.writeRawValue("}") +} + +func (w *JsonSerializationWriter) WriteStringValue(key string, value *string) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeStringValue(*value) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteBoolValue(key string, value *bool) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeRawValue(strconv.FormatBool(*value)) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteInt32Value(key string, value *int32) error { + if value != nil { + cast := int64(*value) + return w.WriteInt64Value(key, &cast) + } + return nil +} +func (w *JsonSerializationWriter) WriteInt64Value(key string, value *int64) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeRawValue(strconv.FormatInt(*value, 10)) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteFloat32Value(key string, value *float32) error { + if value != nil { + cast := float64(*value) + return w.WriteFloat64Value(key, &cast) + } + return nil +} +func (w *JsonSerializationWriter) WriteFloat64Value(key string, value *float64) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeRawValue(strconv.FormatFloat(*value, 'f', -1, 64)) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteTimeValue(key string, value *time.Time) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeRawValue((*value).String()) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteUUIDValue(key string, value *uuid.UUID) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeStringValue((*value).String()) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteByteArrayValue(key string, value []byte) error { + if key != "" && value != nil { + w.writePropertyName(key) + } + if value != nil { + w.writeStringValue(base64.StdEncoding.EncodeToString(value)) + } + if key != "" && value != nil { + w.writePropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) WriteObjectValue(key string, item absser.Parsable) error { + if !item.IsNil() { + if key != "" { + w.writePropertyName(key) + } + //TODO onBefore for backing store + w.writeObjectStart() + //TODO onStart for backing store + err := item.Serialize(w) + //TODO onAfter for backing store + if err != nil { + return err + } + w.trimLastPropertySeparator() + w.writeObjectEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfObjectValues(key string, collection []absser.Parsable) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteObjectValue("", item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfStringValues(key string, collection []string) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteStringValue("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfInt32Values(key string, collection []int32) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteInt32Value("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfInt64Values(key string, collection []int64) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteInt64Value("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfFloat32Values(key string, collection []float32) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteFloat32Value("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfFloat64Values(key string, collection []float64) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteFloat64Value("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfTimeValues(key string, collection []time.Time) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteTimeValue("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfUUIDValues(key string, collection []uuid.UUID) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteUUIDValue("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) WriteCollectionOfBoolValues(key string, collection []bool) error { + if len(collection) > 0 { + if key != "" { + w.writePropertyName(key) + } + w.writeArrayStart() + for _, item := range collection { + err := w.WriteBoolValue("", &item) + if err != nil { + return err + } + w.writePropertySeparator() + } + w.trimLastPropertySeparator() + w.writeArrayEnd() + if key != "" { + w.writePropertySeparator() + } + } + return nil +} +func (w *JsonSerializationWriter) GetSerializedContent() ([]byte, error) { + resultStr := strings.Join(w.writer, "") + return []byte(resultStr), nil +} +func (w *JsonSerializationWriter) WriteAdditionalData(value map[string]interface{}) error { + if value != nil { + for key, value := range value { + p, ok := value.(absser.Parsable) + if ok { + err := w.WriteObjectValue(key, p) + if err != nil { + return err + } + continue + } + c, ok := value.([]absser.Parsable) + if ok { + err := w.WriteCollectionOfObjectValues(key, c) + if err != nil { + return err + } + continue + } + sc, ok := value.([]string) + if ok { + err := w.WriteCollectionOfStringValues(key, sc) + if err != nil { + return err + } + continue + } + bc, ok := value.([]bool) + if ok { + err := w.WriteCollectionOfBoolValues(key, bc) + if err != nil { + return err + } + continue + } + i32c, ok := value.([]int32) + if ok { + err := w.WriteCollectionOfInt32Values(key, i32c) + if err != nil { + return err + } + continue + } + i64c, ok := value.([]int64) + if ok { + err := w.WriteCollectionOfInt64Values(key, i64c) + if err != nil { + return err + } + continue + } + f32c, ok := value.([]float32) + if ok { + err := w.WriteCollectionOfFloat32Values(key, f32c) + if err != nil { + return err + } + continue + } + f64c, ok := value.([]float64) + if ok { + err := w.WriteCollectionOfFloat64Values(key, f64c) + if err != nil { + return err + } + continue + } + uc, ok := value.([]uuid.UUID) + if ok { + err := w.WriteCollectionOfUUIDValues(key, uc) + if err != nil { + return err + } + continue + } + sv, ok := value.(*string) + if ok { + err := w.WriteStringValue(key, sv) + if err != nil { + return err + } + continue + } + bv, ok := value.(*bool) + if ok { + err := w.WriteBoolValue(key, bv) + if err != nil { + return err + } + continue + } + i32v, ok := value.(*int32) + if ok { + err := w.WriteInt32Value(key, i32v) + if err != nil { + return err + } + continue + } + i64v, ok := value.(*int64) + if ok { + err := w.WriteInt64Value(key, i64v) + if err != nil { + return err + } + continue + } + f32v, ok := value.(*float32) + if ok { + err := w.WriteFloat32Value(key, f32v) + if err != nil { + return err + } + continue + } + f64v, ok := value.(*float64) + if ok { + err := w.WriteFloat64Value(key, f64v) + if err != nil { + return err + } + continue + } + uv, ok := value.(*uuid.UUID) + if ok { + err := w.WriteUUIDValue(key, uv) + if err != nil { + return err + } + continue + } + tv, ok := value.(*time.Time) + if ok { + err := w.WriteTimeValue(key, tv) + if err != nil { + return err + } + continue + } + ba, ok := value.([]byte) + if ok { + err := w.WriteByteArrayValue(key, ba) + if err != nil { + return err + } + continue + } + } + w.trimLastPropertySeparator() + } + return nil +} +func (w *JsonSerializationWriter) Close() error { + return nil +} diff --git a/serialization/go/json/json_serialization_writer_factory.go b/serialization/go/json/json_serialization_writer_factory.go new file mode 100644 index 0000000000..b3623bb96d --- /dev/null +++ b/serialization/go/json/json_serialization_writer_factory.go @@ -0,0 +1,32 @@ +package jsonserialization + +import ( + "errors" + + absser "github.com/microsoft/kiota/abstractions/go/serialization" +) + +// The SerializationWriterFactory implementation for JSON serialization. +type JsonSerializationWriterFactory struct { +} + +// Creates a new instance of the JsonSerializationWriterFactory. +func NewJsonSerializationWriterFactory() *JsonSerializationWriterFactory { + return &JsonSerializationWriterFactory{} +} + +func (f *JsonSerializationWriterFactory) GetValidContentType() (string, error) { + return "application/json", nil +} +func (f *JsonSerializationWriterFactory) GetSerializationWriter(contentType string) (absser.SerializationWriter, error) { + validType, err := f.GetValidContentType() + if err != nil { + return nil, err + } else if contentType == "" { + return nil, errors.New("contentType is empty") + } else if contentType != validType { + return nil, errors.New("contentType is not valid") + } else { + return NewJsonSerializationWriter(), nil + } +} diff --git a/serialization/typescript/json/src/jsonSerializationWriter.ts b/serialization/typescript/json/src/jsonSerializationWriter.ts index 36a16a24d3..65105ceae9 100644 --- a/serialization/typescript/json/src/jsonSerializationWriter.ts +++ b/serialization/typescript/json/src/jsonSerializationWriter.ts @@ -82,6 +82,7 @@ export class JsonSerializationWriter implements SerializationWriter { this.writer.pop(); } this.writer.push(`}`); + key && this.writer.push(JsonSerializationWriter.propertySeparator); } } public writeEnumValue = (key?: string | undefined, ...values: (T | undefined)[]): void => { diff --git a/src/Kiota.Builder/CodeDOM/CodeClass.cs b/src/Kiota.Builder/CodeDOM/CodeClass.cs index 1c90b1fe0d..9b9ed2a998 100644 --- a/src/Kiota.Builder/CodeDOM/CodeClass.cs +++ b/src/Kiota.Builder/CodeDOM/CodeClass.cs @@ -9,6 +9,11 @@ public enum CodeClassKind { RequestBuilder, Model, QueryParameters, + /// + /// A single parameter to be provided by the SDK user which will contain query parameters, request body, options, etc. + /// Only used for languages that do not support overloads or optional parameters like go. + /// + ParameterSet, } /// /// CodeClass represents an instance of a Class to be generated in source code diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 961992dca4..4e518a7e7c 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -18,7 +18,8 @@ public enum CodeMethodKind ClientConstructor, RequestBuilderBackwardCompatibility, RequestBuilderWithParameters, - RawUrlConstructor + RawUrlConstructor, + NullCheck, } public enum HttpMethod { Get, diff --git a/src/Kiota.Builder/CodeDOM/CodeParameter.cs b/src/Kiota.Builder/CodeDOM/CodeParameter.cs index b021c9fbd5..a6fe7cdca9 100644 --- a/src/Kiota.Builder/CodeDOM/CodeParameter.cs +++ b/src/Kiota.Builder/CodeDOM/CodeParameter.cs @@ -23,7 +23,12 @@ public enum CodeParameterKind /// A single parameter to be provided by the SDK user which will be added to the path parameters. /// Path, - RawUrl + RawUrl, + /// + /// A single parameter to be provided by the SDK user which will contain query parameters, request body, options, etc. + /// Only used for languages that do not support overloads or optional parameters like go. + /// + ParameterSet, } public class CodeParameter : CodeTerminal, ICloneable, IDocumentedElement diff --git a/src/Kiota.Builder/CodeDOM/CodeProperty.cs b/src/Kiota.Builder/CodeDOM/CodeProperty.cs index 66c2f03182..acb0f7ef89 100644 --- a/src/Kiota.Builder/CodeDOM/CodeProperty.cs +++ b/src/Kiota.Builder/CodeDOM/CodeProperty.cs @@ -15,6 +15,26 @@ public enum CodePropertyKind /// PathParameters, RequestAdapter, + /// + /// The request body. Used when request parameters are wrapped in a classs. + /// + RequestBody, + /// + /// The request query parameters. Used when request parameters are wrapped in a classs. + /// + QueryParameter, + /// + /// The request headers. Used when request parameters are wrapped in a classs. + /// + Headers, + /// + /// The request middleware options. Used when request parameters are wrapped in a classs. + /// + Options, + /// + /// The request response handler. Used when request parameters are wrapped in a classs. + /// + ResponseHandler, } public class CodeProperty : CodeTerminal, IDocumentedElement diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 9adb824035..62cd1f9803 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -526,7 +526,8 @@ private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childT typeName = "binary"; } else if ("double".Equals(format, StringComparison.OrdinalIgnoreCase) || "float".Equals(format, StringComparison.OrdinalIgnoreCase) || - "int64".Equals(format, StringComparison.OrdinalIgnoreCase)) { + "int64".Equals(format, StringComparison.OrdinalIgnoreCase) || + "decimal".Equals(format, StringComparison.OrdinalIgnoreCase)) { isExternal = true; typeName = format.ToLowerInvariant(); } else if ("boolean".Equals(typeName, StringComparison.OrdinalIgnoreCase) || diff --git a/src/Kiota.Builder/Refiners/GoRefiner.cs b/src/Kiota.Builder/Refiners/GoRefiner.cs index 098be8b9e1..7e30d84250 100644 --- a/src/Kiota.Builder/Refiners/GoRefiner.cs +++ b/src/Kiota.Builder/Refiners/GoRefiner.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Kiota.Builder.Extensions; +using Kiota.Builder.Writers.Extensions; using Kiota.Builder.Writers.Go; namespace Kiota.Builder.Refiners { @@ -23,6 +25,9 @@ public override void Refine(CodeNamespace generatedCode) generatedCode, _configuration.UsesBackingStore ); + AddNullCheckMethods( + generatedCode + ); AddRawUrlConstructorOverload( generatedCode ); @@ -32,7 +37,7 @@ public override void Refine(CodeNamespace generatedCode) ReplaceReservedNames( generatedCode, new GoReservedNamesProvider(), - x => $"{x}_escpaped"); + x => $"{x}_escaped"); AddPropertiesAndMethodTypesImports( generatedCode, true, @@ -73,6 +78,80 @@ public override void Refine(CodeNamespace generatedCode) generatedCode, new string[] {"github.com/microsoft/kiota/abstractions/go/serialization.SerializationWriterFactory", "github.com/microsoft/kiota/abstractions/go.RegisterDefaultSerializer"}, new string[] {"github.com/microsoft/kiota/abstractions/go/serialization.ParseNodeFactory", "github.com/microsoft/kiota/abstractions/go.RegisterDefaultDeserializer"}); + ReplaceExecutorAndGeneratorParametersByParameterSets( + generatedCode); + } + private static void ReplaceExecutorAndGeneratorParametersByParameterSets(CodeElement currentElement) { + if (currentElement is CodeMethod currentMethod && + currentMethod.IsOfKind(CodeMethodKind.RequestExecutor) && + currentMethod.Parameters.Any() && + currentElement.Parent is CodeClass parentClass) { + var parameterSetClass = parentClass.AddInnerClass(new CodeClass{ + Name = $"{parentClass.Name.ToFirstCharacterUpperCase()}{currentMethod.HttpMethod}Options", + ClassKind = CodeClassKind.ParameterSet, + Description = $"Options for {currentMethod.Name}", + }).First(); + parameterSetClass.AddProperty( + currentMethod.Parameters.Select(x => new CodeProperty{ + Name = x.Name, + Type = x.Type, + Description = x.Description, + Access = AccessModifier.Public, + PropertyKind = x.ParameterKind switch { + CodeParameterKind.RequestBody => CodePropertyKind.RequestBody, + CodeParameterKind.QueryParameter => CodePropertyKind.QueryParameter, + CodeParameterKind.Headers => CodePropertyKind.Headers, + CodeParameterKind.Options => CodePropertyKind.Options, + CodeParameterKind.ResponseHandler => CodePropertyKind.ResponseHandler, + _ => CodePropertyKind.Custom + }, + }).ToArray()); + parameterSetClass.Properties.ToList().ForEach(x => {x.Type.ActionOf = false;}); + currentMethod.RemoveParametersByKind(CodeParameterKind.RequestBody, + CodeParameterKind.QueryParameter, + CodeParameterKind.Headers, + CodeParameterKind.Options, + CodeParameterKind.ResponseHandler); + var parameterSetParameter = new CodeParameter{ + Name = "options", + Type = new CodeType { + Name = parameterSetClass.Name, + ActionOf = false, + IsNullable = true, + TypeDefinition = parameterSetClass, + IsExternal = false, + }, + Optional = false, + Description = "Options for the request", + ParameterKind = CodeParameterKind.ParameterSet, + }; + currentMethod.AddParameter(parameterSetParameter); + var generatorMethod = parentClass.GetMethodsOffKind(CodeMethodKind.RequestGenerator) + .FirstOrDefault(x => x.HttpMethod == currentMethod.HttpMethod); + if(!(generatorMethod?.Parameters.Any(x => x.IsOfKind(CodeParameterKind.ParameterSet)) ?? true)) { + generatorMethod.RemoveParametersByKind(CodeParameterKind.RequestBody, + CodeParameterKind.QueryParameter, + CodeParameterKind.Headers, + CodeParameterKind.Options); + generatorMethod.AddParameter(parameterSetParameter); + } + } + CrawlTree(currentElement, ReplaceExecutorAndGeneratorParametersByParameterSets); + } + private static void AddNullCheckMethods(CodeElement currentElement) { + if(currentElement is CodeClass currentClass && currentClass.IsOfKind(CodeClassKind.Model)) { + currentClass.AddMethod(new CodeMethod { + Name = "IsNil", + IsAsync = false, + MethodKind = CodeMethodKind.NullCheck, + ReturnType = new CodeType { + Name = "boolean", + IsExternal = true, + IsNullable = false, + }, + }); + } + CrawlTree(currentElement, AddNullCheckMethods); } private static void MoveAllModelsToTopLevel(CodeElement currentElement, CodeNamespace targetNamespace = null) { if(currentElement is CodeNamespace currentNamespace) { @@ -166,16 +245,20 @@ private static void AddErrorImportForEnums(CodeElement currentElement) { new (x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.Serializer), "github.com/microsoft/kiota/abstractions/go/serialization", "SerializationWriter"), new (x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.Deserializer), - "github.com/microsoft/kiota/abstractions/go/serialization", "ParseNode", "ConvertToArrayOfParsable", "ConvertToArrayOfPrimitives"), + "github.com/microsoft/kiota/abstractions/go/serialization", "ParseNode", "Parsable"), new (x => x is CodeMethod method && method.Parameters.Any(x => x.IsOfKind(CodeParameterKind.Path) && "DateTimeOffset".Equals(x.Type.Name, StringComparison.OrdinalIgnoreCase)), "time", "Time"), + new (x => x is CodeEnum num, "ToUpper", "strings"), };//TODO add backing store types once we have them defined private static void CorrectMethodType(CodeMethod currentMethod) { if(currentMethod.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator) && currentMethod.Parent is CodeClass parentClass) { if(currentMethod.IsOfKind(CodeMethodKind.RequestExecutor)) - currentMethod.Parameters.Where(x => x.Type.Name.Equals("IResponseHandler")).ToList().ForEach(x => x.Type.Name = "ResponseHandler"); + currentMethod.Parameters.Where(x => x.Type.Name.Equals("IResponseHandler")).ToList().ForEach(x => { + x.Type.Name = "ResponseHandler"; + x.Type.IsNullable = false; //no pointers + }); else if(currentMethod.IsOfKind(CodeMethodKind.RequestGenerator)) currentMethod.ReturnType.IsNullable = true; currentMethod.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Options)).ToList().ForEach(x => { @@ -188,7 +271,7 @@ private static void CorrectMethodType(CodeMethod currentMethod) { else if(currentMethod.IsOfKind(CodeMethodKind.Serializer)) currentMethod.Parameters.Where(x => x.Type.Name.Equals("ISerializationWriter")).ToList().ForEach(x => x.Type.Name = "SerializationWriter"); else if(currentMethod.IsOfKind(CodeMethodKind.Deserializer)) { - currentMethod.ReturnType.Name = "map[string]func(interface{}, i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55.ParseNode)(error)"; + currentMethod.ReturnType.Name = $"map[string]func(interface{{}}, {conventions.SerializationHash}.ParseNode)(error)"; currentMethod.Name = "getFieldDeserializers"; } else if(currentMethod.IsOfKind(CodeMethodKind.ClientConstructor, CodeMethodKind.Constructor, CodeMethodKind.RawUrlConstructor)) { var rawUrlParam = currentMethod.Parameters.OfKind(CodeParameterKind.RawUrl); @@ -198,8 +281,9 @@ private static void CorrectMethodType(CodeMethod currentMethod) { .Where(x => x.Type.Name.StartsWith("I", StringComparison.InvariantCultureIgnoreCase)) .ToList() .ForEach(x => x.Type.Name = x.Type.Name[1..]); // removing the "I" - } else if(currentMethod.IsOfKind(CodeMethodKind.RequestGenerator)) + } else if(currentMethod.IsOfKind(CodeMethodKind.IndexerBackwardCompatibility, CodeMethodKind.RequestBuilderWithParameters, CodeMethodKind.RequestBuilderBackwardCompatibility)) { currentMethod.ReturnType.IsNullable = true; + } } private static void CorrectPropertyType(CodeProperty currentProperty) { if (currentProperty.Type != null) { diff --git a/src/Kiota.Builder/Refiners/GoReservedNamesProvider.cs b/src/Kiota.Builder/Refiners/GoReservedNamesProvider.cs index a2de915972..bda89c8400 100644 --- a/src/Kiota.Builder/Refiners/GoReservedNamesProvider.cs +++ b/src/Kiota.Builder/Refiners/GoReservedNamesProvider.cs @@ -29,7 +29,8 @@ public class GoReservedNamesProvider : IReservedNamesProvider "struct", "switch", "type", - "var" + "var", + "vendor" // cannot be used as a package name }); public HashSet ReservedNames => _reservedNames.Value; } diff --git a/src/Kiota.Builder/Writers/CodeClassExtensions.cs b/src/Kiota.Builder/Writers/CodeClassExtensions.cs index 998f65c872..261362ad8e 100644 --- a/src/Kiota.Builder/Writers/CodeClassExtensions.cs +++ b/src/Kiota.Builder/Writers/CodeClassExtensions.cs @@ -18,6 +18,16 @@ public static IEnumerable GetPropertiesOfKind(this CodeClass paren .Distinct() .OrderBy(x => x.Name); } + public static IEnumerable GetMethodsOffKind(this CodeClass parentClass, params CodeMethodKind[] kinds) { + if(parentClass == null) + return Enumerable.Empty(); + if(kinds == null || !kinds.Any()) + throw new ArgumentOutOfRangeException(nameof(kinds)); + return parentClass.Methods + .Where(x => x.IsOfKind(kinds)) + .Distinct() + .OrderBy(x => x.Name); + } public static CodeProperty GetBackingStoreProperty(this CodeClass parentClass) { if(parentClass == null) return null; return (parentClass.GetGreatestGrandparent(parentClass) ?? parentClass) // the backing store is always on the uppermost class diff --git a/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs b/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs index c8a0b3efc3..f93014fe20 100644 --- a/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs @@ -10,7 +10,7 @@ public CodeClassDeclarationWriter(GoConventionService conventionService) : base( public override void WriteCodeElement(CodeClass.Declaration codeElement, LanguageWriter writer) { if(codeElement?.Parent?.Parent is CodeNamespace ns) - writer.WriteLine($"package {ns.Name.GetLastNamespaceSegment()}"); + writer.WriteLine($"package {ns.Name.GetLastNamespaceSegment().Replace("-", string.Empty)}"); var importSegments = codeElement .Usings .Where(x => !x.Declaration.IsExternal) @@ -32,6 +32,7 @@ public override void WriteCodeElement(CodeClass.Declaration codeElement, Languag writer.DecreaseIndent(); writer.WriteLines(")", string.Empty); } + conventions.WriteShortDescription((codeElement.Parent as CodeClass).Description, writer); writer.WriteLine($"type {codeElement.Name.ToFirstCharacterUpperCase()} struct {{"); writer.IncreaseIndent(); if(codeElement.Inherits?.AllTypes?.Any() ?? false) { diff --git a/src/Kiota.Builder/Writers/Go/CodeEnumWriter.cs b/src/Kiota.Builder/Writers/Go/CodeEnumWriter.cs index 18900e576c..99f27fd681 100644 --- a/src/Kiota.Builder/Writers/Go/CodeEnumWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeEnumWriter.cs @@ -8,7 +8,7 @@ public CodeEnumWriter(GoConventionService conventionService) : base(conventionSe public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter writer) { if(!codeElement.Options.Any()) return; if(codeElement?.Parent is CodeNamespace ns) - writer.WriteLine($"package {ns.Name.GetLastNamespaceSegment()}"); + writer.WriteLine($"package {ns.Name.GetLastNamespaceSegment().Replace("-", string.Empty)}"); writer.WriteLine("import ("); writer.IncreaseIndent(); @@ -17,6 +17,7 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write writer.DecreaseIndent(); writer.WriteLine(")"); var typeName = codeElement.Name.ToFirstCharacterUpperCase(); + conventions.WriteShortDescription(codeElement.Description, writer); writer.WriteLines($"type {typeName} int", string.Empty, "const ("); @@ -40,7 +41,7 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write writer.WriteLines("}", $"func Parse{typeName}(v string) (interface{{}}, error) {{"); writer.IncreaseIndent(); - writer.WriteLine($"switch v {{"); + writer.WriteLine($"switch strings.ToUpper(v) {{"); writer.IncreaseIndent(); foreach (var item in codeElement.Options) { writer.WriteLine($"case \"{item.ToUpperInvariant()}\":"); @@ -51,8 +52,16 @@ public override void WriteCodeElement(CodeEnum codeElement, LanguageWriter write writer.DecreaseIndent(); writer.WriteLines("}", $"return 0, errors.New(\"Unknown {typeName} value: \" + v)"); - writer.DecreaseIndent(); - writer.WriteLine("}"); + writer.CloseBlock(); + writer.WriteLine($"func Serialize{typeName}(values []{typeName}) []string {{"); + writer.IncreaseIndent(); + writer.WriteLines("result := make([]string, len(values))", + "for i, v := range values {"); + writer.IncreaseIndent(); + writer.WriteLine("result[i] = v.String()"); + writer.CloseBlock(); + writer.WriteLine("return result"); + writer.CloseBlock(); } } } diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index d15fbdb5e3..3136c68c2c 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -18,13 +18,17 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri var parentClass = codeElement.Parent as CodeClass; var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; var returnType = conventions.GetTypeString(codeElement.ReturnType, parentClass); + WriteMethodDocumentation(codeElement, writer); WriteMethodPrototype(codeElement, writer, returnType, parentClass); writer.IncreaseIndent(); - var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); - var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); - var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); - var optionsParam = codeElement.Parameters.OfKind(CodeParameterKind.Options); - var requestParams = new RequestParams(requestBodyParam, queryStringParam, headersParam, optionsParam); + var requestOptionsParam = codeElement.Parameters.OfKind(CodeParameterKind.ParameterSet); + var requestParamSetDefinition = requestOptionsParam != null && requestOptionsParam.Type is CodeType rpsType && + rpsType.TypeDefinition is CodeClass rpsTypeDef ? rpsTypeDef : null; + var requestBodyParam = requestParamSetDefinition?.GetPropertiesOfKind(CodePropertyKind.RequestBody).FirstOrDefault(); + var queryStringParam = requestParamSetDefinition?.GetPropertiesOfKind(CodePropertyKind.QueryParameter).FirstOrDefault(); + var headersParam = requestParamSetDefinition?.GetPropertiesOfKind(CodePropertyKind.Headers).FirstOrDefault(); + var optionsParam = requestParamSetDefinition?.GetPropertiesOfKind(CodePropertyKind.Options).FirstOrDefault(); + var requestParams = new RequestProperties(requestOptionsParam, requestBodyParam, queryStringParam, headersParam, optionsParam); switch(codeElement.MethodKind) { case CodeMethodKind.Serializer: WriteSerializerBody(parentClass, writer); @@ -65,12 +69,29 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.RequestBuilderWithParameters: WriteRequestBuilderBody(parentClass, codeElement, writer); break; + case CodeMethodKind.NullCheck: + WriteNullCheckBody(writer); + break; default: writer.WriteLine("return nil"); break; } writer.CloseBlock(); } + private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) { + var parametersWithDescription = code.Parameters.Where(x => !string.IsNullOrEmpty(code.Description)); + if(!string.IsNullOrEmpty(code.Description)) + conventions.WriteShortDescription(code.Description, writer); + if (parametersWithDescription.Any()) { + writer.WriteLine($"{conventions.DocCommentPrefix}Parameters:"); + foreach(var paramWithDescription in parametersWithDescription.OrderBy(x => x.Name)) + writer.WriteLine($"{conventions.DocCommentPrefix} - {paramWithDescription.Name} : {paramWithDescription.Description}"); + } + } + private static void WriteNullCheckBody(LanguageWriter writer) + { + writer.WriteLine("return m == nil"); + } private const string TempParamsVarName = "urlParams"; private static void WriteRawUrlConstructorBody(CodeClass parentClass, CodeMethod codeElement, LanguageWriter writer) { @@ -110,8 +131,7 @@ private void WriteSerializerBody(CodeClass parentClass, LanguageWriter writer) { private static string errorVarDeclaration(bool shouldDeclareErrorVar) => shouldDeclareErrorVar ? ":" : string.Empty; private static readonly CodeParameterOrderComparer parameterOrderComparer = new(); private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, CodeClass parentClass) { - var returnTypeAsyncPrefix = code.IsAsync ? "func() (" : string.Empty; - var returnTypeAsyncSuffix = code.IsAsync ? "error)" : string.Empty; + var returnTypeAsyncSuffix = code.IsAsync ? "error" : string.Empty; if(!string.IsNullOrEmpty(returnType) && code.IsAsync) returnTypeAsyncSuffix = $", {returnTypeAsyncSuffix}"; var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); @@ -126,7 +146,7 @@ CodeMethodKind.Constructor when parentClass.IsOfKind(CodeClassKind.RequestBuilde var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p => conventions.GetParameterSignature(p, parentClass)).ToList()); var classType = conventions.GetTypeString(new CodeType { Name = parentClass.Name, TypeDefinition = parentClass }, parentClass); var associatedTypePrefix = isConstructor ? string.Empty : $" (m {classType})"; - var finalReturnType = isConstructor ? classType : $"{returnTypeAsyncPrefix}{returnType}{returnTypeAsyncSuffix}"; + var finalReturnType = isConstructor ? classType : $"{returnType}{returnTypeAsyncSuffix}"; var errorDeclaration = code.IsOfKind(CodeMethodKind.ClientConstructor, CodeMethodKind.Constructor, CodeMethodKind.Getter, @@ -135,7 +155,8 @@ CodeMethodKind.Constructor when parentClass.IsOfKind(CodeClassKind.RequestBuilde CodeMethodKind.Deserializer, CodeMethodKind.RequestBuilderWithParameters, CodeMethodKind.RequestBuilderBackwardCompatibility, - CodeMethodKind.RawUrlConstructor) || code.IsAsync ? + CodeMethodKind.RawUrlConstructor, + CodeMethodKind.NullCheck) || code.IsAsync ? string.Empty : "error"; if(!string.IsNullOrEmpty(finalReturnType) && !string.IsNullOrEmpty(errorDeclaration)) @@ -144,6 +165,12 @@ CodeMethodKind.Constructor when parentClass.IsOfKind(CodeClassKind.RequestBuilde } private void WriteGetterBody(CodeMethod codeElement, LanguageWriter writer, CodeClass parentClass) { var backingStore = parentClass.GetBackingStoreProperty(); + writer.WriteLine("if m == nil {"); + writer.IncreaseIndent(); + writer.WriteLine("return nil"); + writer.DecreaseIndent(); + writer.WriteLine("} else {"); + writer.IncreaseIndent(); if(backingStore == null || (codeElement.AccessedProperty?.IsOfKind(CodePropertyKind.BackingStore) ?? false)) writer.WriteLine($"return m.{codeElement.AccessedProperty?.Name?.ToFirstCharacterLowerCase()}"); else @@ -159,6 +186,7 @@ private void WriteGetterBody(CodeMethod codeElement, LanguageWriter writer, Code writer.WriteLine("return value;"); } else writer.WriteLine($"return m.Get{backingStore.Name.ToFirstCharacterUpperCase()}().Get(\"{codeElement.AccessedProperty?.Name?.ToFirstCharacterLowerCase()}\");"); + writer.CloseBlock(); } private void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { @@ -267,19 +295,21 @@ private void WriteFieldDeserializer(CodeProperty property, LanguageWriter writer var deserializationMethodName = GetDeserializationMethodName(property.Type, parentClass); writer.WriteLine($"val, err := {deserializationMethodName}"); WriteReturnError(writer); - var valueCast = property.Type.AllTypes.First().TypeDefinition is CodeClass || - property.Type.AllTypes.First().TypeDefinition is CodeEnum ? - $".(*{propertyTypeImportName})": - string.Empty; - var valueArgument = $"val{valueCast}"; + if (!property.Type.IsCollection && property.Type.AllTypes.First().TypeDefinition is CodeEnum) + writer.WriteLine($"cast := val.({propertyTypeImportName})"); + var valueArgument = property.Type.AllTypes.First().TypeDefinition switch { + CodeClass when !property.Type.IsCollection => $"val.(*{propertyTypeImportName})", + CodeEnum when !property.Type.IsCollection => $"&cast", + _ when property.Type.IsCollection => "res", + _ => "val", + }; if(property.Type.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None) { var isTargetTypeObject = property.Type is CodeType currentType && (currentType.TypeDefinition is CodeEnum || currentType.TypeDefinition is CodeClass); WriteCollectionCast(propertyTypeImportName, isTargetTypeObject, "val", "res", writer); - valueArgument = "res"; } - writer.WriteLines($"o.(*{parentClass.Name.ToFirstCharacterUpperCase()}).Set{property.Name.ToFirstCharacterUpperCase()}({valueArgument})", + writer.WriteLines($"m.Set{property.Name.ToFirstCharacterUpperCase()}({valueArgument})", "return nil"); writer.CloseBlock(); } @@ -293,7 +323,7 @@ private static void WriteCollectionCast(string propertyTypeImportName, bool isTa writer.WriteLine($"{targetVarName}[i] = {castingExpression}"); writer.CloseBlock(); } - private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, string returnType, CodeClass parentClass, LanguageWriter writer) { + private void WriteRequestExecutorBody(CodeMethod codeElement, RequestProperties requestParams, string returnType, CodeClass parentClass, LanguageWriter writer) { if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); if(returnType == null) throw new InvalidOperationException("return type cannot be null"); // string.Empty is a valid return type var isPrimitive = conventions.IsPrimitiveType(returnType); @@ -310,25 +340,22 @@ _ when string.IsNullOrEmpty(returnType) => "SendNoContentAsync", var typeShortName = returnType.Split('.').Last().ToFirstCharacterUpperCase(); var isVoid = string.IsNullOrEmpty(typeShortName); WriteGeneratorMethodCall(codeElement, requestParams, writer, $"{RequestInfoVarName}, err := "); - WriteAsyncReturnError(writer, returnType); + WriteReturnError(writer, returnType); var parsableImportSymbol = GetConversionHelperMethodImport(codeElement.Parent as CodeClass, "Parsable"); + var typeString = conventions.GetTypeString(codeElement.ReturnType, codeElement.Parent, false, false)?.Split(dot); + var importSymbol = typeString == null || typeString.Length < 2 ? string.Empty : typeString.First() + dot; var constructorFunction = returnType switch { _ when isVoid => string.Empty, _ when isPrimitive || isBinary => $"\"{returnType.TrimCollectionAndPointerSymbols()}\", ", - _ => $"func () {parsableImportSymbol} {{ return new({conventions.GetTypeString(codeElement.ReturnType, codeElement.Parent, false, false)}) }}, ", + _ => $"func () {parsableImportSymbol} {{ return {importSymbol}New{typeString.Last()}() }}, ", }; - var returnTypeDeclaration = isVoid ? - string.Empty : - $"{returnType}, "; - writer.WriteLine($"return func() ({returnTypeDeclaration}error) {{"); - writer.IncreaseIndent(); var assignmentPrefix = isVoid ? - string.Empty : - "res, "; + "err =" : + "res, err :="; if(responseHandlerParam != null) - writer.WriteLine($"{assignmentPrefix}err := m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}*{responseHandlerParam.Name})()"); + writer.WriteLine($"{assignmentPrefix} m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}{responseHandlerParam.Name})"); else - writer.WriteLine($"{assignmentPrefix}err := m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}nil)()"); + writer.WriteLine($"{assignmentPrefix} m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}nil)"); WriteReturnError(writer, returnType); var valueVarName = string.Empty; if(codeElement.ReturnType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None) { @@ -343,15 +370,14 @@ _ when string.IsNullOrEmpty(returnType) => "SendNoContentAsync", _ => $"res.({returnType}), " }; writer.WriteLine($"return {resultReturnCast}nil"); - writer.CloseBlock(); } - private static void WriteGeneratorMethodCall(CodeMethod codeElement, RequestParams requestParams, LanguageWriter writer, string prefix) { + private static void WriteGeneratorMethodCall(CodeMethod codeElement, RequestProperties requestParams, LanguageWriter writer, string prefix) { var generatorMethodName = (codeElement.Parent as CodeClass) .Methods .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod) ?.Name ?.ToFirstCharacterUpperCase(); - var paramsList = new List { requestParams.requestBody, requestParams.queryString, requestParams.headers, requestParams.options }; + var paramsList = new List { requestParams.paramSet }; var requestInfoParameters = paramsList.Where(x => x != null) .Select(x => x.Name) .ToList(); @@ -364,7 +390,7 @@ private static void WriteGeneratorMethodCall(CodeMethod codeElement, RequestPara writer.WriteLine($"{prefix}m.{generatorMethodName}({paramsCall});"); } private const string RequestInfoVarName = "requestInfo"; - private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, LanguageWriter writer, CodeClass parentClass, string returnType) { + private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestProperties requestParams, LanguageWriter writer, CodeClass parentClass, string returnType) { if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); var urlTemplateParamsProperty = parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); @@ -374,33 +400,33 @@ private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams req writer.WriteLines($"{RequestInfoVarName}.UrlTemplate = {GetPropertyCall(urlTemplateProperty, "\"\"")}", $"{RequestInfoVarName}.PathParameters = {GetPropertyCall(urlTemplateParamsProperty, "\"\"")}", $"{RequestInfoVarName}.Method = {conventions.AbstractionsHash}.{codeElement.HttpMethod?.ToString().ToUpperInvariant()}"); - if(requestParams.requestBody != null) + if(requestParams.requestBody != null) { + var bodyParamReference = $"{requestParams.paramSet.Name}.{requestParams.requestBody.Name.ToFirstCharacterUpperCase()}"; if(requestParams.requestBody.Type.Name.Equals("binary", StringComparison.OrdinalIgnoreCase)) - writer.WriteLine($"{RequestInfoVarName}.SetStreamContent({requestParams.requestBody.Name})"); + writer.WriteLine($"{RequestInfoVarName}.SetStreamContent({bodyParamReference})"); else - writer.WriteLine($"{RequestInfoVarName}.SetContentFromParsable(m.{requestAdapterProperty.Name.ToFirstCharacterLowerCase()}, \"{codeElement.ContentType}\", {requestParams.requestBody.Name})"); + writer.WriteLine($"{RequestInfoVarName}.SetContentFromParsable(m.{requestAdapterProperty.Name.ToFirstCharacterLowerCase()}, \"{codeElement.ContentType}\", {bodyParamReference})"); + } if(requestParams.queryString != null) { - var httpMethodPrefix = codeElement.HttpMethod.ToString().ToFirstCharacterUpperCase(); - writer.WriteLine($"if {requestParams.queryString.Name} != nil {{"); + var queryStringName = requestParams.queryString.Name.ToFirstCharacterUpperCase(); + writer.WriteLine($"if {requestParams.paramSet.Name} != nil && {requestParams.paramSet.Name}.{queryStringName} != nil {{"); writer.IncreaseIndent(); - writer.WriteLines($"qParams := new({parentClass.Name.ToFirstCharacterUpperCase()}{httpMethodPrefix}QueryParameters)", - $"err := {requestParams.queryString.Name}(qParams)"); - WriteReturnError(writer, returnType); - writer.WriteLine("err = qParams.AddQueryParameters(requestInfo.QueryParameters)"); + writer.WriteLine($"err := {requestParams.paramSet.Name}.{queryStringName}.AddQueryParameters(requestInfo.QueryParameters)"); WriteReturnError(writer, returnType); writer.CloseBlock(); } if(requestParams.headers != null) { - writer.WriteLine($"if {requestParams.headers.Name} != nil {{"); + var headersName = requestParams.headers.Name.ToFirstCharacterUpperCase(); + writer.WriteLine($"if {requestParams.paramSet.Name} != nil && {requestParams.paramSet.Name}.{headersName} != nil {{"); writer.IncreaseIndent(); - writer.WriteLine($"err := {requestParams.headers.Name}({RequestInfoVarName}.Headers)"); - WriteReturnError(writer, returnType); + writer.WriteLine($"{RequestInfoVarName}.Headers = {requestParams.paramSet.Name}.{headersName}"); writer.CloseBlock(); } if(requestParams.options != null) { - writer.WriteLine($"if {requestParams.options.Name} != nil {{"); + var optionsName = requestParams.options.Name.ToFirstCharacterUpperCase(); + writer.WriteLine($"if {requestParams.paramSet.Name} != nil && len({requestParams.paramSet.Name}.{optionsName}) != 0 {{"); writer.IncreaseIndent(); - writer.WriteLine($"err := {RequestInfoVarName}.AddRequestOptions({requestParams.options.Name})"); + writer.WriteLine($"err := {RequestInfoVarName}.AddRequestOptions({requestParams.paramSet.Name}.{optionsName}...)"); WriteReturnError(writer, returnType); writer.CloseBlock(); } @@ -421,17 +447,6 @@ private static string GetNilsErrorPrefix(params string[] returnTypes) { string.Empty : sanitizedTypes.Select(_ => "nil").Aggregate((x,y) => $"{x}, {y}") + ", "; } - private static void WriteAsyncReturnError(LanguageWriter writer, params string[] returnTypes) { - writer.WriteLine("if err != nil {"); - writer.IncreaseIndent(); - var sanitizedTypes = returnTypes.Where(x => !string.IsNullOrEmpty(x)); - var typeDeclarationPrefix = !sanitizedTypes.Any() ? - string.Empty : - sanitizedTypes.Aggregate((x,y) => $"{x}, {y}") + ", "; - var nilsPrefix = GetNilsErrorPrefix(returnTypes); - writer.WriteLine($"return func() ({typeDeclarationPrefix}error) {{ return {nilsPrefix}err }}"); - writer.CloseBlock(); - } private string GetDeserializationMethodName(CodeTypeBase propType, CodeClass parentClass) { var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; var propertyTypeName = conventions.GetTypeString(propType, parentClass, false, false); @@ -462,46 +477,50 @@ private string GetTypeFactory(CodeTypeBase propTypeBase, CodeClass parentClass, if(propTypeBase is CodeType propType) { var importSymbol = conventions.GetTypeString(propType, parentClass, false, false); var importNS = importSymbol.Contains(dot) ? importSymbol.Split(dot).First() + dot : string.Empty; - return $"func () interface{{}} {{ return {importNS}New{propertyTypeName.ToFirstCharacterUpperCase()}() }}"; + var parsableSymbol = GetConversionHelperMethodImport(parentClass, "Parsable"); + return $"func () {parsableSymbol} {{ return {importNS}New{propertyTypeName.ToFirstCharacterUpperCase()}() }}"; } else return GetTypeFactory(propTypeBase.AllTypes.First(), parentClass, propertyTypeName); } - - private const string ParsableConversionMethodName = "ConvertToArrayOfParsable"; - private const string PrimitiveConversionMethodName = "ConvertToArrayOfPrimitives"; private void WriteSerializationMethodCall(CodeTypeBase propType, CodeClass parentClass, string serializationKey, string valueGet, bool shouldDeclareErrorVar, LanguageWriter writer) { serializationKey = $"\"{serializationKey}\""; - var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; var errorPrefix = $"err {errorVarDeclaration(shouldDeclareErrorVar)}= writer."; - writer.WriteLine("{");// so the err var scope is limited + var isEnum = propType is CodeType eType && eType.TypeDefinition is CodeEnum; + var isClass = propType is CodeType cType && cType.TypeDefinition is CodeClass; + if(isEnum && !propType.IsCollection) + writer.WriteLine($"if {valueGet} != nil {{"); + else + writer.WriteLine("{");// so the err var scope is limited writer.IncreaseIndent(); - if(propType is CodeType currentType) { - if (isCollection) { - if(currentType.TypeDefinition == null) { - var conversionMethodImport = GetConversionHelperMethodImport(parentClass, PrimitiveConversionMethodName); - writer.WriteLine($"{errorPrefix}WriteCollectionOfPrimitiveValues({serializationKey}, {conversionMethodImport}({valueGet}))"); - WriteReturnError(writer); - } else { - var conversionMethodImport = GetConversionHelperMethodImport(parentClass, ParsableConversionMethodName); - writer.WriteLine($"{errorPrefix}WriteCollectionOfObjectValues({serializationKey}, {conversionMethodImport}({valueGet}))"); - WriteReturnError(writer); - } - writer.CloseBlock(); //closing the scope for the err var - return; - } else if (currentType.TypeDefinition is CodeEnum) { - writer.WriteLine($"if {valueGet} != nil {{"); - writer.IncreaseIndent(); - writer.WriteLine($"{errorPrefix}WritePrimitiveValue({serializationKey}, {valueGet}.String())"); - WriteReturnError(writer); - writer.CloseBlock(); - writer.CloseBlock(); //closing the scope for the err var - return; - } + if(isEnum && !propType.IsCollection) + writer.WriteLine($"cast := {valueGet}.String()"); + else if(isClass && propType.IsCollection) { + var parsableSymbol = GetConversionHelperMethodImport(parentClass, "Parsable"); + writer.WriteLines($"cast := make([]{parsableSymbol}, len({valueGet}))", + $"for i, v := range {valueGet} {{"); + writer.IncreaseIndent(); + writer.WriteLines($"temp := v", // temporary creating a new reference to avoid pointers to the same object + $"cast[i] = {parsableSymbol}(&temp)"); + writer.CloseBlock(); } - var propertyType = conventions.TranslateType(propType, false); - if(conventions.IsPrimitiveType(propertyType) || conventions.IsScalarType(propertyType)) - writer.WriteLine($"{errorPrefix}WritePrimitiveValue({serializationKey}, {valueGet})"); - else - writer.WriteLine($"{errorPrefix}WriteObjectValue({serializationKey}, {valueGet})"); + var collectionPrefix = propType.IsCollection ? "CollectionOf" : string.Empty; + var collectionSuffix = propType.IsCollection ? "s" : string.Empty; + var propertyTypeName = conventions.GetTypeString(propType, parentClass, false, false) + .Split('.') + .Last() + .ToFirstCharacterUpperCase(); + var reference = (isEnum, isClass, propType.IsCollection) switch { + (true, false, false) => $"&cast", + (true, false, true) => $"{conventions.GetTypeString(propType, parentClass, false, false).Replace(propertyTypeName, "Serialize" + propertyTypeName)}({valueGet})", //importSymbol.SerializeEnumName + (false, true, true) => $"cast", + (_, _, _) => valueGet, + }; + if(isClass) + propertyTypeName = "Object"; + else if(isEnum) + propertyTypeName = "String"; + else if (propertyTypeName.Equals("[]byte", StringComparison.OrdinalIgnoreCase)) + propertyTypeName = "ByteArray"; + writer.WriteLine($"{errorPrefix}Write{collectionPrefix}{propertyTypeName}Value{collectionSuffix}({serializationKey}, {reference})"); WriteReturnError(writer); writer.CloseBlock(); } diff --git a/src/Kiota.Builder/Writers/Go/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/Go/CodePropertyWriter.cs index a8a92f1488..4d5a435bd7 100644 --- a/src/Kiota.Builder/Writers/Go/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodePropertyWriter.cs @@ -13,6 +13,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w case CodePropertyKind.RequestBuilder: throw new InvalidOperationException("RequestBuilders are as properties are not supported in Go and should be replaced by methods by the refiner."); default: + conventions.WriteShortDescription(codeElement.Description, writer); writer.WriteLine($"{propertyName} {returnType};"); break; } diff --git a/src/Kiota.Builder/Writers/Go/GoConventionService.cs b/src/Kiota.Builder/Writers/Go/GoConventionService.cs index 83772df9da..a75fd5839c 100644 --- a/src/Kiota.Builder/Writers/Go/GoConventionService.cs +++ b/src/Kiota.Builder/Writers/Go/GoConventionService.cs @@ -11,10 +11,11 @@ public class GoConventionService : CommonLanguageConventionService public override string VoidTypeName => string.Empty; - public override string DocCommentPrefix => string.Empty; + public override string DocCommentPrefix => "// "; public override string ParseNodeInterfaceName => "ParseNode"; #pragma warning disable CA1822 // Method should be static public string AbstractionsHash => "ida96af0f171bb75f894a4013a6b3146a4397c58f11adb81a2b7cbea9314783a9"; + public string SerializationHash => "i04eb5309aeaafadd28374d79c8471df9b267510b4dc2e3144c378c50f6fd7b55"; #pragma warning restore CA1822 // Method should be static public override string GetAccessModifier(AccessModifier access) { @@ -63,7 +64,7 @@ public string TranslateType(CodeTypeBase type, bool includeImportSymbol) "float" => "float32", "integer" => "int32", "long" => "int64", - "double" => "float64", + "double" or "decimal" => "float64", "boolean" => "bool", "guid" when includeImportSymbol => "uuid.UUID", "guid" when !includeImportSymbol => "UUID", @@ -78,7 +79,7 @@ public string TranslateType(CodeTypeBase type, bool includeImportSymbol) public bool IsPrimitiveType(string typeName) { return typeName.TrimCollectionAndPointerSymbols() switch { "void" or "string" or "float" or "integer" or "long" or "double" or "boolean" or "guid" or "DateTimeOffset" - or "bool" or "int32" or "int64" or "float32" or "float64" or "UUID" or "Time" => true, + or "bool" or "int32" or "int64" or "float32" or "float64" or "UUID" or "Time" or "decimal" => true, _ => false, }; } @@ -116,7 +117,7 @@ currentEnumDefinition.Parent is CodeNamespace enumNS && public override void WriteShortDescription(string description, LanguageWriter writer) { - throw new NotImplementedException(); + writer.WriteLine($"{DocCommentPrefix}{description}"); } #pragma warning disable CA1822 // Method should be static internal void AddRequestBuilderBody(CodeClass parentClass, string returnType, LanguageWriter writer, string urlTemplateVarName = default, IEnumerable pathParameters = default) @@ -126,22 +127,19 @@ internal void AddRequestBuilderBody(CodeClass parentClass, string returnType, La var urlTemplateParams = urlTemplateVarName ?? $"m.{urlTemplateParamsProp.Name}"; var splatImport = returnType.Split('.'); var constructorName = splatImport.Last().TrimCollectionAndPointerSymbols().ToFirstCharacterUpperCase(); - var moduleName = splatImport.Length > 1 ? $"{splatImport.First()}." : string.Empty; + var moduleName = splatImport.Length > 1 ? $"{splatImport.First().TrimStart('*')}." : string.Empty; var pathParametersSuffix = !(pathParameters?.Any() ?? false) ? string.Empty : $", {string.Join(", ", pathParameters.Select(x => $"{x.Name.ToFirstCharacterLowerCase()}"))}"; - writer.WriteLines($"return *{moduleName}New{constructorName}Internal({urlTemplateParams}, m.{requestAdapterProp.Name}{pathParametersSuffix});"); + writer.WriteLines($"return {moduleName}New{constructorName}Internal({urlTemplateParams}, m.{requestAdapterProp.Name}{pathParametersSuffix});"); } public override string TempDictionaryVarName => "urlTplParams"; internal void AddParametersAssignment(LanguageWriter writer, CodeTypeBase pathParametersType, string pathParametersReference, params (CodeTypeBase, string, string)[] parameters) { if(pathParametersType == null) return; var mapTypeName = pathParametersType.Name; - writer.WriteLines($"{TempDictionaryVarName} := make({mapTypeName})", - $"if {pathParametersReference} != nil {{"); - writer.IncreaseIndent(); + writer.WriteLine($"{TempDictionaryVarName} := make({mapTypeName})"); writer.WriteLine($"for idx, item := range {pathParametersReference} {{"); writer.IncreaseIndent(); writer.WriteLine($"{TempDictionaryVarName}[idx] = item"); writer.CloseBlock(); - writer.CloseBlock(); if(parameters.Any()) foreach(var p in parameters) { var isStringStruct = !p.Item1.IsNullable && p.Item1.Name.Equals("string", StringComparison.OrdinalIgnoreCase); @@ -158,9 +156,10 @@ internal void AddParametersAssignment(LanguageWriter writer, CodeTypeBase pathPa private static string GetValueStringConversion(string typeName, string reference) { return typeName switch { "boolean" => $"{StrConvHash}.FormatBool({reference})", - "integer" => $"{StrConvHash}.FormatInt(int64({reference}), 10)", + "int64" => $"{StrConvHash}.FormatInt({reference}, 10)", + "integer" or "int32" => $"{StrConvHash}.FormatInt(int64({reference}), 10)", "long" => $"{StrConvHash}.FormatInt({reference}, 10)", - "float" or "double" => $"{StrConvHash}.FormatFloat({reference}, 'E', -1, 64)", + "float" or "double" or "decimal" or "float64" or "float32" => $"{StrConvHash}.FormatFloat({reference}, 'E', -1, 64)", "DateTimeOffset" or "Time" => $"({reference}).String()", _ => reference, }; diff --git a/src/Kiota.Builder/Writers/RequestParams.cs b/src/Kiota.Builder/Writers/RequestParams.cs index ddf80757e2..ae4b081fee 100644 --- a/src/Kiota.Builder/Writers/RequestParams.cs +++ b/src/Kiota.Builder/Writers/RequestParams.cs @@ -1,3 +1,4 @@ namespace Kiota.Builder.Writers { public record RequestParams(CodeParameter requestBody, CodeParameter queryString, CodeParameter headers, CodeParameter options); + public record RequestProperties(CodeParameter paramSet, CodeProperty requestBody, CodeProperty queryString, CodeProperty headers, CodeProperty options); } diff --git a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs index 4a43732d8b..2222de60f1 100644 --- a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs @@ -33,7 +33,7 @@ public void AddsErrorImportForEnums() { }).First(); ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); - Assert.Single(testEnum.Usings); + Assert.Single(testEnum.Usings.Where(x => x.Name == "errors")); } [Fact] public void CorrectsCoreType() { diff --git a/tests/Kiota.Builder.Tests/Writers/Go/CodeEnumWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Go/CodeEnumWriterTests.cs index e746e7f367..696681f393 100644 --- a/tests/Kiota.Builder.Tests/Writers/Go/CodeEnumWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Go/CodeEnumWriterTests.cs @@ -41,7 +41,7 @@ public void WritesEnum() { Assert.Contains($"[i]", result); Assert.Contains($"func Parse", result); Assert.Contains($"(v string) (interface{{}}, error)", result); - Assert.Contains($"switch v", result); + Assert.Contains($"switch strings.ToUpper(v)", result); Assert.Contains($"return 0, errors.New(\"Unknown ", result); AssertExtensions.CurlyBracesAreClosed(result); Assert.Contains(optionName.ToUpperInvariant(), result); diff --git a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs index c20cb80304..737673358e 100644 --- a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs @@ -2,7 +2,9 @@ using System.IO; using System.Linq; using Kiota.Builder.Extensions; +using Kiota.Builder.Refiners; using Kiota.Builder.Tests; +using Moq; using Xunit; namespace Kiota.Builder.Writers.Go.Tests { @@ -104,31 +106,32 @@ private void AddInheritanceClass() { Name = "someParentClass" }; } - private void AddRequestBodyParameters() { + private void AddRequestBodyParameters(CodeMethod target = default) { var stringType = new CodeType { Name = "string", }; - method.AddParameter(new CodeParameter { + target ??= method; + target.AddParameter(new CodeParameter { Name = "h", ParameterKind = CodeParameterKind.Headers, Type = stringType, }); - method.AddParameter(new CodeParameter{ + target.AddParameter(new CodeParameter{ Name = "q", ParameterKind = CodeParameterKind.QueryParameter, Type = stringType, }); - method.AddParameter(new CodeParameter{ + target.AddParameter(new CodeParameter{ Name = "b", ParameterKind = CodeParameterKind.RequestBody, Type = stringType, }); - method.AddParameter(new CodeParameter{ + target.AddParameter(new CodeParameter{ Name = "r", ParameterKind = CodeParameterKind.ResponseHandler, Type = stringType, }); - method.AddParameter(new CodeParameter { + target.AddParameter(new CodeParameter { Name = "o", ParameterKind = CodeParameterKind.Options, Type = stringType, @@ -143,7 +146,7 @@ public void WritesNullableVoidTypeForExecutor(){ }; writer.Write(method); var result = tw.ToString(); - Assert.Contains("(func() (error))", result); + Assert.Contains("(error)", result); AssertExtensions.CurlyBracesAreClosed(result); } [Fact] @@ -176,16 +179,28 @@ public void WritesRequestExecutorBody() { Assert.Contains("m.requestAdapter.SendAsync", result); Assert.Contains("return res.(", result); Assert.Contains("err != nil", result); - Assert.Contains("return func() (", result); AssertExtensions.CurlyBracesAreClosed(result); } private const string AbstractionsPackageHash = "ida96af0f171bb75f894a4013a6b3146a4397c58f11adb81a2b7cbea9314783a9"; [Fact] public void WritesRequestGeneratorBody() { + var configurationMock = new Mock(); + var refiner = new GoRefiner(configurationMock.Object); method.MethodKind = CodeMethodKind.RequestGenerator; method.HttpMethod = HttpMethod.Get; + var executor = parentClass.AddMethod(new CodeMethod { + Name = "executor", + HttpMethod = HttpMethod.Get, + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string", + IsExternal = true, + } + }).First(); + AddRequestBodyParameters(executor); AddRequestBodyParameters(); AddRequestProperties(); + refiner.Refine(parentClass.Parent as CodeNamespace); writer.Write(method); var result = tw.ToString(); Assert.Contains($"requestInfo := {AbstractionsPackageHash}.NewRequestInformation()", result); @@ -193,12 +208,12 @@ public void WritesRequestGeneratorBody() { Assert.Contains("requestInfo.PathParameters", result); Assert.Contains($"Method = {AbstractionsPackageHash}.GET", result); Assert.Contains("err != nil", result); - Assert.Contains("h != nil", result); - Assert.Contains("h(requestInfo.Headers)", result); - Assert.Contains("q != nil", result); - Assert.Contains("qParams.AddQueryParameters(requestInfo.QueryParameters)", result); - Assert.Contains("o != nil", result); - Assert.Contains("requestInfo.AddRequestOptions(o)", result); + Assert.Contains("H != nil", result); + Assert.Contains("requestInfo.Headers =", result); + Assert.Contains("Q != nil", result); + Assert.Contains("Q.AddQueryParameters(requestInfo.QueryParameters)", result); + Assert.Contains("O) != 0", result); + Assert.Contains("requestInfo.AddRequestOptions(", result); Assert.Contains("requestInfo.SetContentFromParsable(m.requestAdapter", result); Assert.Contains("return requestInfo, nil", result); AssertExtensions.CurlyBracesAreClosed(result); @@ -259,10 +274,9 @@ public void WritesSerializerBody() { AddSerializationProperties(); writer.Write(method); var result = tw.ToString(); - Assert.Contains("WritePrimitiveValue", result); - Assert.Contains("WriteCollectionOfPrimitiveValues", result); + Assert.Contains("WriteStringValue", result); + Assert.Contains("WriteCollectionOfStringValues", result); Assert.Contains("WriteCollectionOfObjectValues", result); - // Assert.Contains("WriteEnumValue", result); update when implementing enum serialization Assert.Contains("WriteAdditionalData(m.GetAdditionalData())", result); AssertExtensions.CurlyBracesAreClosed(result); } @@ -310,7 +324,7 @@ public void ThrowsIfReturnTypeIsMissing() { public void WritesReturnType() { writer.Write(method); var result = tw.ToString(); - Assert.Contains($"{MethodName.ToFirstCharacterUpperCase()}()({TaskPrefix}*{ReturnTypeName}, error)", result);// async default + Assert.Contains($"{MethodName.ToFirstCharacterUpperCase()}()(*{ReturnTypeName}, error)", result);// async default AssertExtensions.CurlyBracesAreClosed(result); } [Fact] @@ -389,7 +403,7 @@ public void WritesPathParameterRequestBuilder() { Assert.Contains("m.requestAdapter", result); Assert.Contains("m.pathParameters", result); Assert.Contains("pathParam", result); - Assert.Contains("return *New", result); + Assert.Contains("return New", result); } [Fact] public void WritesGetterToBackingStore() {