diff --git a/CHANGELOG.md b/CHANGELOG.md index fd3c69e..50aea68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +### Changed + +## [1.8.0] - 2024-07-09 + +- Remove native support for date in `std-uritemplate` [#1.8.0](https://github.com/microsoft/kiota-abstractions-go/issues/183) + ### Added ## [1.7.0] - 2024-07-09 diff --git a/go.mod b/go.mod index d8dbbd0..7a42830 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/cjlapao/common-go v0.0.39 github.com/google/uuid v1.6.0 - github.com/std-uritemplate/std-uritemplate/go v0.0.57 + github.com/std-uritemplate/std-uritemplate/go v1.0.6 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/trace v1.24.0 diff --git a/go.sum b/go.sum index c445b60..35020a1 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/std-uritemplate/std-uritemplate/go v0.0.57 h1:GHGjptrsmazP4IVDlUprssiEf9ESVkbjx15xQXXzvq4= -github.com/std-uritemplate/std-uritemplate/go v0.0.57/go.mod h1:rG/bqh/ThY4xE5de7Rap3vaDkYUT76B0GPJ0loYeTTc= +github.com/std-uritemplate/std-uritemplate/go v1.0.6 h1:XkCI5iBsbDxc9bYnFArKlgBkNvcw8St1UBJoNpYGCCo= +github.com/std-uritemplate/std-uritemplate/go v1.0.6/go.mod h1:rG/bqh/ThY4xE5de7Rap3vaDkYUT76B0GPJ0loYeTTc= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= diff --git a/request_information.go b/request_information.go index ceeaf02..9c0ca63 100644 --- a/request_information.go +++ b/request_information.go @@ -1,566 +1,636 @@ -package abstractions - -import ( - "context" - "errors" - "time" - - "reflect" - "strconv" - "strings" - - u "net/url" - - "github.com/google/uuid" - s "github.com/microsoft/kiota-abstractions-go/serialization" - stduritemplate "github.com/std-uritemplate/std-uritemplate/go" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// RequestInformation represents an abstract HTTP request. -type RequestInformation struct { - // The HTTP method of the request. - Method HttpMethod - uri *u.URL - // The Request Headers. - Headers *RequestHeaders - // The Query Parameters of the request. - // Deprecated: use QueryParametersAny instead - QueryParameters map[string]string - // The Query Parameters of the request. - QueryParametersAny map[string]any - // The Request Body. - Content []byte - // The path parameters to use for the URL template when generating the URI. - // Deprecated: use PathParametersAny instead - PathParameters map[string]string - // The path parameters to use for the URL template when generating the URI. - PathParametersAny map[string]any - // The Url template for the current request. - UrlTemplate string - options map[string]RequestOption -} - -const raw_url_key = "request-raw-url" - -// NewRequestInformation creates a new RequestInformation object with default values. -func NewRequestInformation() *RequestInformation { - return &RequestInformation{ - Headers: NewRequestHeaders(), - QueryParameters: make(map[string]string), - QueryParametersAny: make(map[string]any), - options: make(map[string]RequestOption), - PathParameters: make(map[string]string), - PathParametersAny: make(map[string]any), - } -} - -// NewRequestInformationWithMethodAndUrlTemplateAndPathParameters creates a new RequestInformation object with the specified method and URL template and path parameters. -func NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(method HttpMethod, urlTemplate string, pathParameters map[string]string) *RequestInformation { - value := NewRequestInformation() - value.Method = method - value.UrlTemplate = urlTemplate - value.PathParameters = pathParameters - return value -} -func ConfigureRequestInformation[T any](request *RequestInformation, config *RequestConfiguration[T]) { - if request == nil { - return - } - if config == nil { - return - } - if config.QueryParameters != nil { - request.AddQueryParameters(*(config.QueryParameters)) - } - request.Headers.AddAll(config.Headers) - request.AddRequestOptions(config.Options) -} - -// GetUri returns the URI of the request. -func (request *RequestInformation) GetUri() (*u.URL, error) { - if request.uri != nil { - return request.uri, nil - } else if request.UrlTemplate == "" { - return nil, errors.New("uri cannot be empty") - } else if request.PathParameters == nil { - return nil, errors.New("uri template parameters cannot be nil") - } else if request.QueryParameters == nil { - return nil, errors.New("uri query parameters cannot be nil") - } else if request.QueryParametersAny == nil { - return nil, errors.New("uri query parameters cannot be nil") - } else if request.PathParameters[raw_url_key] != "" { - uri, err := u.Parse(request.PathParameters[raw_url_key]) - if err != nil { - return nil, err - } - request.SetUri(*uri) - return request.uri, nil - } else { - _, baseurlExists := request.PathParameters["baseurl"] - if !baseurlExists && strings.Contains(strings.ToLower(request.UrlTemplate), "{+baseurl}") { - return nil, errors.New("pathParameters must contain a value for \"baseurl\" for the url to be built") - } - - substitutions := make(map[string]any) - for key, value := range request.PathParameters { - substitutions[key] = value - } - for key, value := range request.PathParametersAny { - substitutions[key] = request.normalizeParameters(reflect.ValueOf(value), value, false) - } - for key, value := range request.QueryParameters { - substitutions[key] = value - } - for key, value := range request.QueryParametersAny { - substitutions[key] = value - } - url, err := stduritemplate.Expand(request.UrlTemplate, substitutions) - if err != nil { - return nil, err - } - uri, err := u.Parse(url) - return uri, err - } -} - -// SetUri updates the URI for the request from a raw URL. -func (request *RequestInformation) SetUri(url u.URL) { - request.uri = &url - for k := range request.PathParameters { - delete(request.PathParameters, k) - } - for k := range request.QueryParameters { - delete(request.QueryParameters, k) - } - for k := range request.QueryParametersAny { - delete(request.QueryParametersAny, k) - } -} - -// AddRequestOptions adds an option to the request to be read by the middleware infrastructure. -func (request *RequestInformation) AddRequestOptions(options []RequestOption) { - if options == nil { - return - } - if request.options == nil { - request.options = make(map[string]RequestOption, len(options)) - } - for _, option := range options { - request.options[option.GetKey().Key] = option - } -} - -// GetRequestOptions returns the options for this request. Options are unique by type. If an option of the same type is added twice, the last one wins. -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[idx] = option - idx++ - } - return result -} - -const contentTypeHeader = "Content-Type" -const binaryContentType = "application/octet-stream" - -// SetStreamContent sets the request body to a binary stream. -// Deprecated: Use SetStreamContentAndContentType instead. -func (request *RequestInformation) SetStreamContent(content []byte) { - request.SetStreamContentAndContentType(content, binaryContentType) -} - -// SetStreamContentAndContentType sets the request body to a binary stream with the specified content type. -func (request *RequestInformation) SetStreamContentAndContentType(content []byte, contentType string) { - request.Content = content - if request.Headers != nil { - request.Headers.Add(contentTypeHeader, contentType) - } -} - -func (request *RequestInformation) setContentAndContentType(writer s.SerializationWriter, contentType string) error { - content, err := writer.GetSerializedContent() - if err != nil { - return err - } else if content == nil { - return errors.New("content cannot be nil") - } - request.Content = content - if request.Headers != nil { - request.Headers.TryAdd(contentTypeHeader, contentType) - } - return nil -} - -func (request *RequestInformation) getSerializationWriter(requestAdapter RequestAdapter, contentType string, items ...interface{}) (s.SerializationWriter, error) { - if contentType == "" { - return nil, errors.New("content type cannot be empty") - } else if requestAdapter == nil { - return nil, errors.New("requestAdapter cannot be nil") - } else if len(items) == 0 { - return nil, errors.New("items cannot be nil or empty") - } - factory := requestAdapter.GetSerializationWriterFactory() - if factory == nil { - return nil, errors.New("factory cannot be nil") - } - writer, err := factory.GetSerializationWriter(contentType) - if err != nil { - return nil, err - } else if writer == nil { - return nil, errors.New("writer cannot be nil") - } else { - return writer, nil - } -} - -func (r *RequestInformation) setRequestType(result interface{}, span trace.Span) { - if result != nil { - span.SetAttributes(attribute.String("com.microsoft.kiota.request.type", reflect.TypeOf(result).String())) - } -} - -const observabilityTracerName = "github.com/microsoft/kiota-abstractions-go" - -// SetContentFromParsable sets the request body from a model with the specified content type. -func (request *RequestInformation) SetContentFromParsable(ctx context.Context, requestAdapter RequestAdapter, contentType string, item s.Parsable) error { - _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromParsable") - defer span.End() - - writer, err := request.getSerializationWriter(requestAdapter, contentType, item) - if err != nil { - span.RecordError(err) - return err - } - defer writer.Close() - if multipartBody, ok := item.(MultipartBody); ok { - contentType += "; boundary=" + multipartBody.GetBoundary() - multipartBody.SetRequestAdapter(requestAdapter) - } - request.setRequestType(item, span) - err = writer.WriteObjectValue("", item) - if err != nil { - span.RecordError(err) - return err - } - err = request.setContentAndContentType(writer, contentType) - if err != nil { - span.RecordError(err) - return err - } - return nil -} - -// SetContentFromParsableCollection sets the request body from a model with the specified content type. -func (request *RequestInformation) SetContentFromParsableCollection(ctx context.Context, requestAdapter RequestAdapter, contentType string, items []s.Parsable) error { - _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromParsableCollection") - defer span.End() - - writer, err := request.getSerializationWriter(requestAdapter, contentType, items) - if err != nil { - span.RecordError(err) - return err - } - defer writer.Close() - if len(items) > 0 { - request.setRequestType(items[0], span) - } - err = writer.WriteCollectionOfObjectValues("", items) - if err != nil { - span.RecordError(err) - return err - } - err = request.setContentAndContentType(writer, contentType) - if err != nil { - span.RecordError(err) - return err - } - return nil -} - -// SetContentFromScalar sets the request body from a scalar value with the specified content type. -func (request *RequestInformation) SetContentFromScalar(ctx context.Context, requestAdapter RequestAdapter, contentType string, item interface{}) error { - _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromScalar") - defer span.End() - writer, err := request.getSerializationWriter(requestAdapter, contentType, item) - if err != nil { - span.RecordError(err) - return err - } - defer writer.Close() - request.setRequestType(item, span) - - if sv, ok := item.(*string); ok { - err = writer.WriteStringValue("", sv) - } else if bv, ok := item.(*bool); ok { - err = writer.WriteBoolValue("", bv) - } else if byv, ok := item.(*byte); ok { - err = writer.WriteByteValue("", byv) - } else if i8v, ok := item.(*int8); ok { - err = writer.WriteInt8Value("", i8v) - } else if i32v, ok := item.(*int32); ok { - err = writer.WriteInt32Value("", i32v) - } else if i64v, ok := item.(*int64); ok { - err = writer.WriteInt64Value("", i64v) - } else if f32v, ok := item.(*float32); ok { - err = writer.WriteFloat32Value("", f32v) - } else if f64v, ok := item.(*float64); ok { - err = writer.WriteFloat64Value("", f64v) - } else if uv, ok := item.(*uuid.UUID); ok { - err = writer.WriteUUIDValue("", uv) - } else if tv, ok := item.(*time.Time); ok { - err = writer.WriteTimeValue("", tv) - } else if dv, ok := item.(*s.ISODuration); ok { - err = writer.WriteISODurationValue("", dv) - } else if tov, ok := item.(*s.TimeOnly); ok { - err = writer.WriteTimeOnlyValue("", tov) - } else if dov, ok := item.(*s.DateOnly); ok { - err = writer.WriteDateOnlyValue("", dov) - } - if err != nil { - span.RecordError(err) - return err - } - err = request.setContentAndContentType(writer, contentType) - if err != nil { - span.RecordError(err) - return err - } - return nil -} - -// SetContentFromScalarCollection sets the request body from a scalar value with the specified content type. -func (request *RequestInformation) SetContentFromScalarCollection(ctx context.Context, requestAdapter RequestAdapter, contentType string, items []interface{}) error { - _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromScalarCollection") - defer span.End() - writer, err := request.getSerializationWriter(requestAdapter, contentType, items...) - if err != nil { - span.RecordError(err) - return err - } - defer writer.Close() - if len(items) > 0 { - value := items[0] - request.setRequestType(value, span) - if _, ok := value.(string); ok { - sc := make([]string, len(items)) - for i, v := range items { - if sv, ok := v.(string); ok { - sc[i] = sv - } - } - err = writer.WriteCollectionOfStringValues("", sc) - } else if _, ok := value.(bool); ok { - bc := make([]bool, len(items)) - for i, v := range items { - if sv, ok := v.(bool); ok { - bc[i] = sv - } - } - err = writer.WriteCollectionOfBoolValues("", bc) - } else if _, ok := value.(byte); ok { - byc := make([]byte, len(items)) - for i, v := range items { - if sv, ok := v.(byte); ok { - byc[i] = sv - } - } - err = writer.WriteCollectionOfByteValues("", byc) - } else if _, ok := value.(int8); ok { - i8c := make([]int8, len(items)) - for i, v := range items { - if sv, ok := v.(int8); ok { - i8c[i] = sv - } - } - err = writer.WriteCollectionOfInt8Values("", i8c) - } else if _, ok := value.(int32); ok { - i32c := make([]int32, len(items)) - for i, v := range items { - if sv, ok := v.(int32); ok { - i32c[i] = sv - } - } - err = writer.WriteCollectionOfInt32Values("", i32c) - } else if _, ok := value.(int64); ok { - i64c := make([]int64, len(items)) - for i, v := range items { - if sv, ok := v.(int64); ok { - i64c[i] = sv - } - } - err = writer.WriteCollectionOfInt64Values("", i64c) - } else if _, ok := value.(float32); ok { - f32c := make([]float32, len(items)) - for i, v := range items { - if sv, ok := v.(float32); ok { - f32c[i] = sv - } - } - err = writer.WriteCollectionOfFloat32Values("", f32c) - } else if _, ok := value.(float64); ok { - f64c := make([]float64, len(items)) - for i, v := range items { - if sv, ok := v.(float64); ok { - f64c[i] = sv - } - } - err = writer.WriteCollectionOfFloat64Values("", f64c) - } else if _, ok := value.(uuid.UUID); ok { - uc := make([]uuid.UUID, len(items)) - for i, v := range items { - if sv, ok := v.(uuid.UUID); ok { - uc[i] = sv - } - } - err = writer.WriteCollectionOfUUIDValues("", uc) - } else if _, ok := value.(time.Time); ok { - tc := make([]time.Time, len(items)) - for i, v := range items { - if sv, ok := v.(time.Time); ok { - tc[i] = sv - } - } - err = writer.WriteCollectionOfTimeValues("", tc) - } else if _, ok := value.(s.ISODuration); ok { - dc := make([]s.ISODuration, len(items)) - for i, v := range items { - if sv, ok := v.(s.ISODuration); ok { - dc[i] = sv - } - } - err = writer.WriteCollectionOfISODurationValues("", dc) - } else if _, ok := value.(s.TimeOnly); ok { - toc := make([]s.TimeOnly, len(items)) - for i, v := range items { - if sv, ok := v.(s.TimeOnly); ok { - toc[i] = sv - } - } - err = writer.WriteCollectionOfTimeOnlyValues("", toc) - } else if _, ok := value.(s.DateOnly); ok { - doc := make([]s.DateOnly, len(items)) - for i, v := range items { - if sv, ok := v.(s.DateOnly); ok { - doc[i] = sv - } - } - err = writer.WriteCollectionOfDateOnlyValues("", doc) - } else if _, ok := value.(byte); ok { - ba := make([]byte, len(items)) - for i, v := range items { - if sv, ok := v.(byte); ok { - ba[i] = sv - } - } - err = writer.WriteByteArrayValue("", ba) - } - } - if err != nil { - span.RecordError(err) - return err - } - err = request.setContentAndContentType(writer, contentType) - if err != nil { - span.RecordError(err) - return err - } - return nil -} - -// AddQueryParameters adds the query parameters to the request by reading the properties from the provided object. -func (request *RequestInformation) AddQueryParameters(source any) { - if source == nil || request == nil { - return - } - valOfP := reflect.ValueOf(source) - fields := reflect.TypeOf(source) - numOfFields := fields.NumField() - for i := 0; i < numOfFields; i++ { - field := fields.Field(i) - fieldName := field.Name - fieldValue := valOfP.Field(i) - tagValue := field.Tag.Get("uriparametername") - if tagValue != "" { - fieldName = tagValue - } - value := fieldValue.Interface() - valueOfValue := reflect.ValueOf(value) - if valueOfValue.IsNil() { - continue - } - str, ok := value.(*string) - if ok && str != nil { - request.QueryParameters[fieldName] = *str - } - bl, ok := value.(*bool) - if ok && bl != nil { - request.QueryParameters[fieldName] = strconv.FormatBool(*bl) - } - it, ok := value.(*int32) - if ok && it != nil { - request.QueryParameters[fieldName] = strconv.FormatInt(int64(*it), 10) - } - strArr, ok := value.([]string) - if ok && len(strArr) > 0 { - // populating both query parameter fields to avoid breaking compatibility with code reading this field - request.QueryParameters[fieldName] = strings.Join(strArr, ",") - - tmp := make([]any, len(strArr)) - for i, v := range strArr { - tmp[i] = v - } - request.QueryParametersAny[fieldName] = tmp - } - if arr, ok := value.([]any); ok && len(arr) > 0 { - request.QueryParametersAny[fieldName] = arr - } - normalizedValue := request.normalizeParameters(valueOfValue, value, true) - if normalizedValue != nil { - request.QueryParametersAny[fieldName] = normalizedValue - } - } -} - -// Normalize different types to values that can be rendered in an URL: -// enum -> string (name) -// []enum -> []string (containing names) -// []non_interface -> []any (like []int64 -> []any) -func (request *RequestInformation) normalizeParameters(valueOfValue reflect.Value, value any, returnNilIfNotNormalizable bool) any { - if valueOfValue.Kind() == reflect.Slice && valueOfValue.Len() > 0 { - //type assertions to "enums" don't work if you don't know the enum type in advance, we need to use reflection - enumArr := valueOfValue.Slice(0, valueOfValue.Len()) - if _, ok := enumArr.Index(0).Interface().(kiotaEnum); ok { - // testing the first value is an enum to avoid iterating over the whole array if it's not - strRepresentations := make([]string, valueOfValue.Len()) - for i := range strRepresentations { - strRepresentations[i] = enumArr.Index(i).Interface().(kiotaEnum).String() - } - return strRepresentations - } else { - anySlice := make([]any, valueOfValue.Len()) - for i := range anySlice { - anySlice[i] = enumArr.Index(i).Interface() - } - return anySlice - } - } else if enum, ok := value.(kiotaEnum); ok { - return enum.String() - } - - if returnNilIfNotNormalizable { - return nil - } else { - return value - } -} - -type kiotaEnum interface { - String() string -} +package abstractions + +import ( + "context" + "errors" + "time" + + "reflect" + "strconv" + "strings" + + u "net/url" + + "github.com/google/uuid" + s "github.com/microsoft/kiota-abstractions-go/serialization" + stduritemplate "github.com/std-uritemplate/std-uritemplate/go" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// RequestInformation represents an abstract HTTP request. +type RequestInformation struct { + // The HTTP method of the request. + Method HttpMethod + uri *u.URL + // The Request Headers. + Headers *RequestHeaders + // The Query Parameters of the request. + // Deprecated: use QueryParametersAny instead + QueryParameters map[string]string + // The Query Parameters of the request. + QueryParametersAny map[string]any + // The Request Body. + Content []byte + // The path parameters to use for the URL template when generating the URI. + // Deprecated: use PathParametersAny instead + PathParameters map[string]string + // The path parameters to use for the URL template when generating the URI. + PathParametersAny map[string]any + // The Url template for the current request. + UrlTemplate string + options map[string]RequestOption +} + +const raw_url_key = "request-raw-url" + +// NewRequestInformation creates a new RequestInformation object with default values. +func NewRequestInformation() *RequestInformation { + return &RequestInformation{ + Headers: NewRequestHeaders(), + QueryParameters: make(map[string]string), + QueryParametersAny: make(map[string]any), + options: make(map[string]RequestOption), + PathParameters: make(map[string]string), + PathParametersAny: make(map[string]any), + } +} + +// NewRequestInformationWithMethodAndUrlTemplateAndPathParameters creates a new RequestInformation object with the specified method and URL template and path parameters. +func NewRequestInformationWithMethodAndUrlTemplateAndPathParameters(method HttpMethod, urlTemplate string, pathParameters map[string]string) *RequestInformation { + value := NewRequestInformation() + value.Method = method + value.UrlTemplate = urlTemplate + value.PathParameters = pathParameters + return value +} +func ConfigureRequestInformation[T any](request *RequestInformation, config *RequestConfiguration[T]) { + if request == nil { + return + } + if config == nil { + return + } + if config.QueryParameters != nil { + request.AddQueryParameters(*(config.QueryParameters)) + } + request.Headers.AddAll(config.Headers) + request.AddRequestOptions(config.Options) +} + +// GetUri returns the URI of the request. +func (request *RequestInformation) GetUri() (*u.URL, error) { + if request.uri != nil { + return request.uri, nil + } else if request.UrlTemplate == "" { + return nil, errors.New("uri cannot be empty") + } else if request.PathParameters == nil { + return nil, errors.New("uri template parameters cannot be nil") + } else if request.QueryParameters == nil { + return nil, errors.New("uri query parameters cannot be nil") + } else if request.QueryParametersAny == nil { + return nil, errors.New("uri query parameters cannot be nil") + } else if request.PathParameters[raw_url_key] != "" { + uri, err := u.Parse(request.PathParameters[raw_url_key]) + if err != nil { + return nil, err + } + request.SetUri(*uri) + return request.uri, nil + } else { + _, baseurlExists := request.PathParameters["baseurl"] + if !baseurlExists && strings.Contains(strings.ToLower(request.UrlTemplate), "{+baseurl}") { + return nil, errors.New("pathParameters must contain a value for \"baseurl\" for the url to be built") + } + + substitutions := make(map[string]any) + for key, value := range request.PathParameters { + substitutions[key] = request.sanitizeValue(value) + } + for key, value := range request.PathParametersAny { + substitutions[key] = request.normalizeParameters(reflect.ValueOf(value), request.sanitizeValue(value), false) + } + for key, value := range request.QueryParameters { + substitutions[key] = request.sanitizeValue(value) + } + for key, value := range request.QueryParametersAny { + substitutions[key] = request.sanitizeValue(value) + } + url, err := stduritemplate.Expand(request.UrlTemplate, substitutions) + if err != nil { + return nil, err + } + uri, err := u.Parse(url) + return uri, err + } +} + +func castItem[T any, R interface{}](collection []T, mutator func(t T) R) []R { + if len(collection) > 0 { + cast := make([]R, len(collection)) + for i, v := range collection { + cast[i] = mutator(v) + } + return cast + } + return nil +} + +func (request *RequestInformation) sanitizeValue(value any) any { + if value == nil { + return nil + } + + switch v := value.(type) { + case *time.Time: + return v.Format(time.RFC3339) + case time.Time: + return v.Format(time.RFC3339) + case []*time.Time: + return castItem(v, func(t *time.Time) string { + return t.Format(time.RFC3339) + }) + case []time.Time: + return castItem(v, func(t time.Time) string { + return t.Format(time.RFC3339) + }) + case *s.ISODuration: + return v.String() + case s.ISODuration: + return v.String() + case []*s.ISODuration: + return castItem(v, func(v *s.ISODuration) string { + return v.String() + }) + case []s.ISODuration: + return castItem(v, func(v s.ISODuration) string { + return v.String() + }) + case *s.TimeOnly: + return v.String() + case s.TimeOnly: + return v.String() + case []*s.TimeOnly: + return castItem(v, func(v *s.TimeOnly) string { + return v.String() + }) + case []s.TimeOnly: + return castItem(v, func(v s.TimeOnly) string { + return v.String() + }) + case *s.DateOnly: + return v.String() + case s.DateOnly: + return v.String() + case []*s.DateOnly: + return castItem(v, func(v *s.DateOnly) string { + return v.String() + }) + case []s.DateOnly: + return castItem(v, func(v s.DateOnly) string { + return v.String() + }) + } + + return value +} + +// SetUri updates the URI for the request from a raw URL. +func (request *RequestInformation) SetUri(url u.URL) { + request.uri = &url + for k := range request.PathParameters { + delete(request.PathParameters, k) + } + for k := range request.QueryParameters { + delete(request.QueryParameters, k) + } + for k := range request.QueryParametersAny { + delete(request.QueryParametersAny, k) + } +} + +// AddRequestOptions adds an option to the request to be read by the middleware infrastructure. +func (request *RequestInformation) AddRequestOptions(options []RequestOption) { + if options == nil { + return + } + if request.options == nil { + request.options = make(map[string]RequestOption, len(options)) + } + for _, option := range options { + request.options[option.GetKey().Key] = option + } +} + +// GetRequestOptions returns the options for this request. Options are unique by type. If an option of the same type is added twice, the last one wins. +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[idx] = option + idx++ + } + return result +} + +const contentTypeHeader = "Content-Type" +const binaryContentType = "application/octet-stream" + +// SetStreamContent sets the request body to a binary stream. +// Deprecated: Use SetStreamContentAndContentType instead. +func (request *RequestInformation) SetStreamContent(content []byte) { + request.SetStreamContentAndContentType(content, binaryContentType) +} + +// SetStreamContentAndContentType sets the request body to a binary stream with the specified content type. +func (request *RequestInformation) SetStreamContentAndContentType(content []byte, contentType string) { + request.Content = content + if request.Headers != nil { + request.Headers.Add(contentTypeHeader, contentType) + } +} + +func (request *RequestInformation) setContentAndContentType(writer s.SerializationWriter, contentType string) error { + content, err := writer.GetSerializedContent() + if err != nil { + return err + } else if content == nil { + return errors.New("content cannot be nil") + } + request.Content = content + if request.Headers != nil { + request.Headers.TryAdd(contentTypeHeader, contentType) + } + return nil +} + +func (request *RequestInformation) getSerializationWriter(requestAdapter RequestAdapter, contentType string, items ...interface{}) (s.SerializationWriter, error) { + if contentType == "" { + return nil, errors.New("content type cannot be empty") + } else if requestAdapter == nil { + return nil, errors.New("requestAdapter cannot be nil") + } else if len(items) == 0 { + return nil, errors.New("items cannot be nil or empty") + } + factory := requestAdapter.GetSerializationWriterFactory() + if factory == nil { + return nil, errors.New("factory cannot be nil") + } + writer, err := factory.GetSerializationWriter(contentType) + if err != nil { + return nil, err + } else if writer == nil { + return nil, errors.New("writer cannot be nil") + } else { + return writer, nil + } +} + +func (r *RequestInformation) setRequestType(result interface{}, span trace.Span) { + if result != nil { + span.SetAttributes(attribute.String("com.microsoft.kiota.request.type", reflect.TypeOf(result).String())) + } +} + +const observabilityTracerName = "github.com/microsoft/kiota-abstractions-go" + +// SetContentFromParsable sets the request body from a model with the specified content type. +func (request *RequestInformation) SetContentFromParsable(ctx context.Context, requestAdapter RequestAdapter, contentType string, item s.Parsable) error { + _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromParsable") + defer span.End() + + writer, err := request.getSerializationWriter(requestAdapter, contentType, item) + if err != nil { + span.RecordError(err) + return err + } + defer writer.Close() + if multipartBody, ok := item.(MultipartBody); ok { + contentType += "; boundary=" + multipartBody.GetBoundary() + multipartBody.SetRequestAdapter(requestAdapter) + } + request.setRequestType(item, span) + err = writer.WriteObjectValue("", item) + if err != nil { + span.RecordError(err) + return err + } + err = request.setContentAndContentType(writer, contentType) + if err != nil { + span.RecordError(err) + return err + } + return nil +} + +// SetContentFromParsableCollection sets the request body from a model with the specified content type. +func (request *RequestInformation) SetContentFromParsableCollection(ctx context.Context, requestAdapter RequestAdapter, contentType string, items []s.Parsable) error { + _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromParsableCollection") + defer span.End() + + writer, err := request.getSerializationWriter(requestAdapter, contentType, items) + if err != nil { + span.RecordError(err) + return err + } + defer writer.Close() + if len(items) > 0 { + request.setRequestType(items[0], span) + } + err = writer.WriteCollectionOfObjectValues("", items) + if err != nil { + span.RecordError(err) + return err + } + err = request.setContentAndContentType(writer, contentType) + if err != nil { + span.RecordError(err) + return err + } + return nil +} + +// SetContentFromScalar sets the request body from a scalar value with the specified content type. +func (request *RequestInformation) SetContentFromScalar(ctx context.Context, requestAdapter RequestAdapter, contentType string, item interface{}) error { + _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromScalar") + defer span.End() + writer, err := request.getSerializationWriter(requestAdapter, contentType, item) + if err != nil { + span.RecordError(err) + return err + } + defer writer.Close() + request.setRequestType(item, span) + + if sv, ok := item.(*string); ok { + err = writer.WriteStringValue("", sv) + } else if bv, ok := item.(*bool); ok { + err = writer.WriteBoolValue("", bv) + } else if byv, ok := item.(*byte); ok { + err = writer.WriteByteValue("", byv) + } else if i8v, ok := item.(*int8); ok { + err = writer.WriteInt8Value("", i8v) + } else if i32v, ok := item.(*int32); ok { + err = writer.WriteInt32Value("", i32v) + } else if i64v, ok := item.(*int64); ok { + err = writer.WriteInt64Value("", i64v) + } else if f32v, ok := item.(*float32); ok { + err = writer.WriteFloat32Value("", f32v) + } else if f64v, ok := item.(*float64); ok { + err = writer.WriteFloat64Value("", f64v) + } else if uv, ok := item.(*uuid.UUID); ok { + err = writer.WriteUUIDValue("", uv) + } else if tv, ok := item.(*time.Time); ok { + err = writer.WriteTimeValue("", tv) + } else if dv, ok := item.(*s.ISODuration); ok { + err = writer.WriteISODurationValue("", dv) + } else if tov, ok := item.(*s.TimeOnly); ok { + err = writer.WriteTimeOnlyValue("", tov) + } else if dov, ok := item.(*s.DateOnly); ok { + err = writer.WriteDateOnlyValue("", dov) + } + if err != nil { + span.RecordError(err) + return err + } + err = request.setContentAndContentType(writer, contentType) + if err != nil { + span.RecordError(err) + return err + } + return nil +} + +// SetContentFromScalarCollection sets the request body from a scalar value with the specified content type. +func (request *RequestInformation) SetContentFromScalarCollection(ctx context.Context, requestAdapter RequestAdapter, contentType string, items []interface{}) error { + _, span := otel.GetTracerProvider().Tracer(observabilityTracerName).Start(ctx, "SetContentFromScalarCollection") + defer span.End() + writer, err := request.getSerializationWriter(requestAdapter, contentType, items...) + if err != nil { + span.RecordError(err) + return err + } + defer writer.Close() + if len(items) > 0 { + value := items[0] + request.setRequestType(value, span) + if _, ok := value.(string); ok { + sc := make([]string, len(items)) + for i, v := range items { + if sv, ok := v.(string); ok { + sc[i] = sv + } + } + err = writer.WriteCollectionOfStringValues("", sc) + } else if _, ok := value.(bool); ok { + bc := make([]bool, len(items)) + for i, v := range items { + if sv, ok := v.(bool); ok { + bc[i] = sv + } + } + err = writer.WriteCollectionOfBoolValues("", bc) + } else if _, ok := value.(byte); ok { + byc := make([]byte, len(items)) + for i, v := range items { + if sv, ok := v.(byte); ok { + byc[i] = sv + } + } + err = writer.WriteCollectionOfByteValues("", byc) + } else if _, ok := value.(int8); ok { + i8c := make([]int8, len(items)) + for i, v := range items { + if sv, ok := v.(int8); ok { + i8c[i] = sv + } + } + err = writer.WriteCollectionOfInt8Values("", i8c) + } else if _, ok := value.(int32); ok { + i32c := make([]int32, len(items)) + for i, v := range items { + if sv, ok := v.(int32); ok { + i32c[i] = sv + } + } + err = writer.WriteCollectionOfInt32Values("", i32c) + } else if _, ok := value.(int64); ok { + i64c := make([]int64, len(items)) + for i, v := range items { + if sv, ok := v.(int64); ok { + i64c[i] = sv + } + } + err = writer.WriteCollectionOfInt64Values("", i64c) + } else if _, ok := value.(float32); ok { + f32c := make([]float32, len(items)) + for i, v := range items { + if sv, ok := v.(float32); ok { + f32c[i] = sv + } + } + err = writer.WriteCollectionOfFloat32Values("", f32c) + } else if _, ok := value.(float64); ok { + f64c := make([]float64, len(items)) + for i, v := range items { + if sv, ok := v.(float64); ok { + f64c[i] = sv + } + } + err = writer.WriteCollectionOfFloat64Values("", f64c) + } else if _, ok := value.(uuid.UUID); ok { + uc := make([]uuid.UUID, len(items)) + for i, v := range items { + if sv, ok := v.(uuid.UUID); ok { + uc[i] = sv + } + } + err = writer.WriteCollectionOfUUIDValues("", uc) + } else if _, ok := value.(time.Time); ok { + tc := make([]time.Time, len(items)) + for i, v := range items { + if sv, ok := v.(time.Time); ok { + tc[i] = sv + } + } + err = writer.WriteCollectionOfTimeValues("", tc) + } else if _, ok := value.(s.ISODuration); ok { + dc := make([]s.ISODuration, len(items)) + for i, v := range items { + if sv, ok := v.(s.ISODuration); ok { + dc[i] = sv + } + } + err = writer.WriteCollectionOfISODurationValues("", dc) + } else if _, ok := value.(s.TimeOnly); ok { + toc := make([]s.TimeOnly, len(items)) + for i, v := range items { + if sv, ok := v.(s.TimeOnly); ok { + toc[i] = sv + } + } + err = writer.WriteCollectionOfTimeOnlyValues("", toc) + } else if _, ok := value.(s.DateOnly); ok { + doc := make([]s.DateOnly, len(items)) + for i, v := range items { + if sv, ok := v.(s.DateOnly); ok { + doc[i] = sv + } + } + err = writer.WriteCollectionOfDateOnlyValues("", doc) + } else if _, ok := value.(byte); ok { + ba := make([]byte, len(items)) + for i, v := range items { + if sv, ok := v.(byte); ok { + ba[i] = sv + } + } + err = writer.WriteByteArrayValue("", ba) + } + } + if err != nil { + span.RecordError(err) + return err + } + err = request.setContentAndContentType(writer, contentType) + if err != nil { + span.RecordError(err) + return err + } + return nil +} + +// AddQueryParameters adds the query parameters to the request by reading the properties from the provided object. +func (request *RequestInformation) AddQueryParameters(source any) { + if source == nil || request == nil { + return + } + valOfP := reflect.ValueOf(source) + fields := reflect.TypeOf(source) + numOfFields := fields.NumField() + for i := 0; i < numOfFields; i++ { + field := fields.Field(i) + fieldName := field.Name + fieldValue := valOfP.Field(i) + tagValue := field.Tag.Get("uriparametername") + if tagValue != "" { + fieldName = tagValue + } + value := request.sanitizeValue(fieldValue.Interface()) + valueOfValue := reflect.ValueOf(value) + if valueOfValue.IsNil() { + continue + } + str, ok := value.(*string) + if ok && str != nil { + request.QueryParameters[fieldName] = *str + } + bl, ok := value.(*bool) + if ok && bl != nil { + request.QueryParameters[fieldName] = strconv.FormatBool(*bl) + } + it, ok := value.(*int32) + if ok && it != nil { + request.QueryParameters[fieldName] = strconv.FormatInt(int64(*it), 10) + } + strArr, ok := value.([]string) + if ok && len(strArr) > 0 { + // populating both query parameter fields to avoid breaking compatibility with code reading this field + request.QueryParameters[fieldName] = strings.Join(strArr, ",") + + tmp := make([]any, len(strArr)) + for i, v := range strArr { + tmp[i] = v + } + request.QueryParametersAny[fieldName] = tmp + } + if arr, ok := value.([]any); ok && len(arr) > 0 { + request.QueryParametersAny[fieldName] = arr + } + normalizedValue := request.normalizeParameters(valueOfValue, value, true) + if normalizedValue != nil { + request.QueryParametersAny[fieldName] = normalizedValue + } + } +} + +// Normalize different types to values that can be rendered in an URL: +// enum -> string (name) +// []enum -> []string (containing names) +// []non_interface -> []any (like []int64 -> []any) +func (request *RequestInformation) normalizeParameters(valueOfValue reflect.Value, value any, returnNilIfNotNormalizable bool) any { + if valueOfValue.Kind() == reflect.Slice && valueOfValue.Len() > 0 { + //type assertions to "enums" don't work if you don't know the enum type in advance, we need to use reflection + enumArr := valueOfValue.Slice(0, valueOfValue.Len()) + if _, ok := enumArr.Index(0).Interface().(kiotaEnum); ok { + // testing the first value is an enum to avoid iterating over the whole array if it's not + strRepresentations := make([]string, valueOfValue.Len()) + for i := range strRepresentations { + strRepresentations[i] = enumArr.Index(i).Interface().(kiotaEnum).String() + } + return strRepresentations + } else { + anySlice := make([]any, valueOfValue.Len()) + for i := range anySlice { + anySlice[i] = enumArr.Index(i).Interface() + } + return anySlice + } + } else if enum, ok := value.(kiotaEnum); ok { + return enum.String() + } + + if returnNilIfNotNormalizable { + return nil + } else { + return value + } +} + +type kiotaEnum interface { + String() string +} diff --git a/request_information_test.go b/request_information_test.go index 0460e5c..10b5475 100644 --- a/request_information_test.go +++ b/request_information_test.go @@ -273,6 +273,112 @@ func TestItSetsEnumValuesInPathParameters(t *testing.T) { assert.Equal(t, "http://localhost/active,suspended", resultUri.String()) } +func prepareNormalizedStdTest(arrayValues any, singleValue any, referenceArray any, referenceValue any) *RequestInformation { + requestInformation := NewRequestInformation() + requestInformation.UrlTemplate = "{+baseurl}/array/{arrayValues}/single/{singleValue}/referenceArray/{referenceArray}/referenceValue/{referenceValue}" + + requestInformation.PathParameters["baseurl"] = "http://localhost" + requestInformation.PathParametersAny["arrayValues"] = arrayValues + requestInformation.PathParametersAny["singleValue"] = singleValue + requestInformation.PathParametersAny["referenceArray"] = referenceArray + requestInformation.PathParametersAny["referenceValue"] = referenceValue + + return requestInformation +} + +func TestItNormalizesOnStandardizedTimeParams(t *testing.T) { + arrayValues := []time.Time{ + time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC), + time.Date(2022, 8, 2, 0, 0, 0, 0, time.UTC), + } + + singleValue := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC) + + time1 := time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC) + time2 := time.Date(2022, 8, 2, 0, 0, 0, 0, time.UTC) + referenceArray := []*time.Time{ + &time1, + &time2, + } + + referenceValue := &time1 + + requestInformation := prepareNormalizedStdTest(arrayValues, singleValue, referenceArray, referenceValue) + resultUri, err := requestInformation.GetUri() + assert.Nil(t, err) + assert.Equal(t, "http://localhost/array/2022-08-01%2000%3A00%3A00%20%2B0000%20UTC,2022-08-02%2000%3A00%3A00%20%2B0000%20UTC/single/2022-08-01T00%3A00%3A00Z/referenceArray/2022-08-01%2000%3A00%3A00%20%2B0000%20UTC,2022-08-02%2000%3A00%3A00%20%2B0000%20UTC/referenceValue/2022-08-01T00%3A00%3A00Z", resultUri.String()) + +} + +func TestItNormalizesOnStandardizedDurationParams(t *testing.T) { + duration := s.NewDuration(0, 0, 1, 1, 0, 0, 0) + duration1 := s.NewDuration(0, 0, 1, 2, 0, 0, 0) + arrayValues := []s.ISODuration{ + *duration, + *duration1, + } + + value := s.NewDuration(0, 0, 2, 1, 0, 0, 0) + singleValue := *value + + duration3 := s.NewDuration(0, 0, 1, 2, 0, 0, 0) + referenceArray := []*s.ISODuration{ + duration3, + } + + referenceValue := s.NewDuration(0, 0, 1, 1, 0, 0, 0) + + requestInformation := prepareNormalizedStdTest(arrayValues, singleValue, referenceArray, referenceValue) + resultUri, err := requestInformation.GetUri() + assert.Nil(t, err) + assert.Equal(t, "http://localhost/array/P1DT1H,P1DT2H/single/P2DT1H/referenceArray/P1DT2H/referenceValue/P1DT1H", resultUri.String()) +} + +func TestItNormalizesOnStandardizedTimeOnlyParams(t *testing.T) { + time1, err := s.ParseTimeOnly("16:20:21.000") + assert.Nil(t, err) + arrayValues := []s.TimeOnly{ + *time1, + } + + value, err := s.ParseTimeOnly("16:20:21.000") + assert.Nil(t, err) + singleValue := *value + + time2, err := s.ParseTimeOnly("16:20:21.000") + referenceArray := []*s.TimeOnly{ + time2, + } + + referenceValue, err := s.ParseTimeOnly("16:20:21.000") + + requestInformation := prepareNormalizedStdTest(arrayValues, singleValue, referenceArray, referenceValue) + resultUri, err := requestInformation.GetUri() + assert.Nil(t, err) + assert.Equal(t, "http://localhost/array/16%3A20%3A21.000000000/single/16%3A20%3A21.000000000/referenceArray/16%3A20%3A21.000000000/referenceValue/16%3A20%3A21.000000000", resultUri.String()) +} + +func TestItNormalizesOnStandardizedDateOnlyParams(t *testing.T) { + dateOnly := s.NewDateOnly(time.Date(2020, 1, 4, 0, 0, 0, 0, time.UTC)) + arrayValues := []s.DateOnly{ + *dateOnly, + } + + date1 := s.NewDateOnly(time.Date(2020, 1, 4, 0, 0, 0, 0, time.UTC)) + singleValue := *date1 + + referenceArray := []*s.DateOnly{ + s.NewDateOnly(time.Date(2020, 1, 4, 0, 0, 0, 0, time.UTC)), + } + + referenceValue := s.NewDateOnly(time.Date(2020, 1, 4, 0, 0, 0, 0, time.UTC)) + + requestInformation := prepareNormalizedStdTest(arrayValues, singleValue, referenceArray, referenceValue) + resultUri, err := requestInformation.GetUri() + assert.Nil(t, err) + assert.Equal(t, "http://localhost/array/2020-01-04/single/2020-01-04/referenceArray/2020-01-04/referenceValue/2020-01-04", resultUri.String()) +} + func TestItSetsExplodedQueryParameters(t *testing.T) { value := true requestInformation := NewRequestInformation()