diff --git a/gateway/router/route.go b/gateway/router/route.go index 96d5874c..19421b24 100644 --- a/gateway/router/route.go +++ b/gateway/router/route.go @@ -79,6 +79,10 @@ func (r *Route) UnmarshalFunc(request *http.Request) shared.Unmarshal { contentType := request.Header.Get(HeaderContentType) setter.SetStringIfEmpty(&contentType, request.Header.Get(strings.ToLower(HeaderContentType))) switch contentType { + case content.XLSContentType: + return func(data []byte, dest interface{}) error { + return shared.DecodeXLS(request.Context(), data, dest) + } case content.XMLContentType: return r.Marshaller.XML.Unmarshal case content.CSVContentType: diff --git a/repository/component.go b/repository/component.go index 0aba879c..f9cc7f9a 100644 --- a/repository/component.go +++ b/repository/component.go @@ -417,6 +417,18 @@ func (c *Component) UnmarshalFor(opts ...UnmarshalOption) shared.Unmarshal { } switch contentType { + case content.XLSContentType: + if c.Content.Marshaller.XLS.CanUnmarshal() { + return c.Content.Marshaller.XLS.Unmarshal + } + req := options.request + return func(data []byte, dest interface{}) error { + ctx := context.Background() + if req != nil { + ctx = req.Context() + } + return shared.DecodeXLS(ctx, data, dest) + } case content.XMLContentType: return c.Content.Marshaller.XML.Unmarshal case content.CSVContentType: diff --git a/repository/contract/input.go b/repository/contract/input.go index 07819664..c0ebf33c 100644 --- a/repository/contract/input.go +++ b/repository/contract/input.go @@ -3,8 +3,10 @@ package contract import ( "context" "fmt" + "github.com/viant/datly/shared" "github.com/viant/datly/view" "github.com/viant/datly/view/state" + "reflect" ) type Input struct { @@ -41,6 +43,9 @@ func (i *Input) Init(ctx context.Context, aView *view.View) error { } } } + if i.Body.Schema == nil && len(i.Type.Parameters.FilterByKind(state.KindRequestBody)) == 0 && implementsXLSUnmarshaller(i.Type.Schema) { + i.Body.Schema = i.Type.Schema.Clone() + } pkg := pkgPath if i.Type.Schema != nil && i.Type.Package != "" { @@ -77,3 +82,19 @@ func (i *Input) Init(ctx context.Context, aView *view.View) error { return nil } + +var xlsUnmarshallerType = reflect.TypeOf((*shared.XLSUnmarshaller)(nil)).Elem() + +func implementsXLSUnmarshaller(schema *state.Schema) bool { + if schema == nil { + return false + } + rType := schema.Type() + if rType == nil { + return false + } + if rType.Implements(xlsUnmarshallerType) { + return true + } + return rType.Kind() != reflect.Ptr && reflect.PtrTo(rType).Implements(xlsUnmarshallerType) +} diff --git a/service/session/stater.go b/service/session/stater.go index 02b48002..fcf7116c 100644 --- a/service/session/stater.go +++ b/service/session/stater.go @@ -7,9 +7,12 @@ import ( "os" "reflect" "runtime/debug" + "strings" "embed" + "github.com/viant/datly/repository/content" + "github.com/viant/datly/shared" "github.com/viant/datly/utils/types" "github.com/viant/datly/view" "github.com/viant/datly/view/state" @@ -170,6 +173,10 @@ func (s *Session) Bind(ctx context.Context, dest interface{}, opts ...hstate.Opt options := s.Indirect(true, stateOptions...) options.scope = hOptions.Scope() + if err = s.populateTopLevelXLSBody(ctx, dest, stateType, options); err != nil { + return err + } + if err = s.SetState(ctx, stateType.Parameters, aState, options); err != nil { return err } @@ -180,6 +187,82 @@ func (s *Session) Bind(ctx context.Context, dest interface{}, opts ...hstate.Opt return err } +func (s *Session) populateTopLevelXLSBody(ctx context.Context, dest interface{}, stateType *state.Type, opts *Options) error { + if dest == nil || stateType == nil || opts == nil || opts.kindLocator == nil { + return nil + } + if len(stateType.Parameters.FilterByKind(state.KindRequestBody)) > 0 { + return nil + } + + destType := reflect.TypeOf(dest) + if !implementsSessionXLSUnmarshaller(destType) { + return nil + } + + request, err := s.HttpRequest(ctx, opts) + if err != nil || request == nil { + return err + } + if !isXLSContentType(request.Header.Get(content.HeaderContentType)) { + return nil + } + + bodyLocator, err := opts.kindLocator.Lookup(state.KindRequestBody) + if err != nil { + return nil + } + value, has, err := bodyLocator.Value(ctx, destType, "") + if err != nil || !has || value == nil { + return err + } + return assignBoundBody(dest, value) +} + +func assignBoundBody(dest interface{}, value interface{}) error { + dst := reflect.ValueOf(dest) + if dst.Kind() != reflect.Ptr || dst.IsNil() { + return fmt.Errorf("destination must be a non-nil pointer, but had %T", dest) + } + src := reflect.ValueOf(value) + if !src.IsValid() { + return nil + } + if src.Type() == dst.Type() { + dst.Elem().Set(src.Elem()) + return nil + } + if src.Type().AssignableTo(dst.Elem().Type()) { + dst.Elem().Set(src) + return nil + } + if src.Kind() == reflect.Ptr && !src.IsNil() && src.Elem().Type().AssignableTo(dst.Elem().Type()) { + dst.Elem().Set(src.Elem()) + return nil + } + return fmt.Errorf("unable to assign request body value of type %T into %T", value, dest) +} + +var sessionXLSUnmarshallerType = reflect.TypeOf((*shared.XLSUnmarshaller)(nil)).Elem() + +func implementsSessionXLSUnmarshaller(rType reflect.Type) bool { + if rType == nil { + return false + } + if rType.Implements(sessionXLSUnmarshallerType) { + return true + } + return rType.Kind() != reflect.Ptr && reflect.PtrTo(rType).Implements(sessionXLSUnmarshallerType) +} + +func isXLSContentType(contentType string) bool { + if contentType == "" { + return false + } + mediaType := strings.TrimSpace(strings.SplitN(contentType, ";", 2)[0]) + return mediaType == content.XLSContentType +} + func (s *Session) handleInputState(ctx context.Context, hOptions *hstate.Options, embedFs *embed.FS) error { // Handle WithInput: preload cache from provided input data input := hOptions.Input() diff --git a/shared/marshaller.go b/shared/marshaller.go index b8fb7208..6e82df2a 100644 --- a/shared/marshaller.go +++ b/shared/marshaller.go @@ -1,7 +1,27 @@ package shared +import ( + "context" + "fmt" +) + // Unmarshal converts data to destination, destination has to be a pointer to desired output type type Unmarshal func(data []byte, destination interface{}) error // Marshal converts source to byte array type Marshal func(src interface{}) ([]byte, error) + +// XLSUnmarshaller decodes an XLS/XLSX request body into the receiver. +type XLSUnmarshaller interface { + UnmarshalXLS(ctx context.Context, data []byte) error +} + +// DecodeXLS dispatches to an XLS/XLSX-aware request-body decoder on dest. +func DecodeXLS(ctx context.Context, data []byte, dest interface{}) error { + switch actual := dest.(type) { + case XLSUnmarshaller: + return actual.UnmarshalXLS(ctx, data) + default: + return fmt.Errorf("xlsx request body is not supported for %T", dest) + } +}