From 38b01dafc8ba7f581757eb5804a0ab532bcb9c5c Mon Sep 17 00:00:00 2001 From: vnazarov Date: Thu, 11 Jul 2019 13:22:54 +0300 Subject: [PATCH] Task #509: Otto scripting --- api/interfaces.go | 3 + api/rest/decl.go | 4 + api/rest/i_restservice.go | 49 ++++--- api/session/decl.go | 4 +- api/session/service.go | 4 +- api/session/session.go | 10 +- app/models/helpers.go | 5 +- app/models/manager.go | 1 - app/models/seo/helpers.go | 15 ++ basebuild/build.go | 3 +- env/interfaces.go | 17 +++ env/manager.go | 25 ++++ env/otto/api.go | 51 +++++++ env/otto/decl.go | 48 +++++++ env/otto/doc.go | 11 ++ env/otto/i_application_context.go | 219 ++++++++++++++++++++++++++++++ env/otto/i_script.go | 34 +++++ env/otto/i_scriptengine.go | 55 ++++++++ env/otto/init.go | 212 +++++++++++++++++++++++++++++ main.go | 7 + utils/datatypes.go | 17 +++ utils/generic.go | 65 +++++++++ utils/generic_test.go | 30 ++++ 23 files changed, 858 insertions(+), 31 deletions(-) create mode 100644 env/otto/api.go create mode 100644 env/otto/decl.go create mode 100644 env/otto/doc.go create mode 100644 env/otto/i_application_context.go create mode 100644 env/otto/i_script.go create mode 100644 env/otto/i_scriptengine.go create mode 100644 env/otto/init.go diff --git a/api/interfaces.go b/api/interfaces.go index e00bf402..554f53c3 100644 --- a/api/interfaces.go +++ b/api/interfaces.go @@ -41,6 +41,9 @@ type InterfaceRestService interface { GetName() string Run() error + + GetHandler(method string, resource string) FuncAPIHandler + GET(resource string, handler FuncAPIHandler) PUT(resource string, handler FuncAPIHandler) POST(resource string, handler FuncAPIHandler) diff --git a/api/rest/decl.go b/api/rest/decl.go index b06148a7..901aeaf2 100644 --- a/api/rest/decl.go +++ b/api/rest/decl.go @@ -3,6 +3,7 @@ package rest import ( "io" "net/http" + "sync" "github.com/julienschmidt/httprouter" "github.com/ottemo/commerce/api" @@ -28,6 +29,9 @@ type DefaultRestService struct { ListenOn string Router *httprouter.Router Handlers []string + + RawHandler api.FuncAPIHandler + RawHandlerMutex sync.RWMutex } // DefaultRestApplicationContext is a structure to hold API request related information diff --git a/api/rest/i_restservice.go b/api/rest/i_restservice.go index f7f5bf3b..590ed6bf 100644 --- a/api/rest/i_restservice.go +++ b/api/rest/i_restservice.go @@ -35,6 +35,10 @@ func (it *DefaultRestService) GetName() string { func (it *DefaultRestService) wrappedHandler(handler api.FuncAPIHandler) httprouter.Handle { // httprouter supposes other format of handler than we use, so we need wrapper wrappedHandler := func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + if resp == nil && req == nil && params == nil { + it.RawHandler = handler + return + } // catching API handler fails defer func() { @@ -130,9 +134,6 @@ func (it *DefaultRestService) wrappedHandler(handler api.FuncAPIHandler) httprou content = newContent - // request contains POST text - case strings.Contains(contentType, "text/plain"): - fallthrough default: var body []byte @@ -348,34 +349,46 @@ func (it *DefaultRestService) wrappedHandler(handler api.FuncAPIHandler) httprou // GET is a wrapper for the HTTP GET verb func (it *DefaultRestService) GET(resource string, handler api.FuncAPIHandler) { - path := "/" + resource - it.Router.GET(path, it.wrappedHandler(handler)) - - it.Handlers = append(it.Handlers, path+" {GET}") + it.RegisterAPI("GET", resource, handler) } // PUT is a wrapper for the HTTP PUT verb func (it *DefaultRestService) PUT(resource string, handler api.FuncAPIHandler) { - path := "/" + resource - it.Router.PUT(path, it.wrappedHandler(handler)) - - it.Handlers = append(it.Handlers, path+" {PUT}") + it.RegisterAPI("PUT", resource, handler) } // POST is a wrapper for the HTTP POST verb func (it *DefaultRestService) POST(resource string, handler api.FuncAPIHandler) { - path := "/" + resource - it.Router.POST(path, it.wrappedHandler(handler)) - - it.Handlers = append(it.Handlers, path+" {POST}") + it.RegisterAPI("POST", resource, handler) } // DELETE is a wrapper for the HTTP DELETE verb func (it *DefaultRestService) DELETE(resource string, handler api.FuncAPIHandler) { - path := "/" + resource - it.Router.DELETE(path, it.wrappedHandler(handler)) + it.RegisterAPI("DELETE", resource, handler) +} + +// RegisterAPI registers API ahndler for a given resource +func (it *DefaultRestService) RegisterAPI(method, resource string, handler api.FuncAPIHandler) { + path := resource + if !strings.HasPrefix("/", resource) { + path = "/" + resource + } + + wrappedHandler := it.wrappedHandler(handler) + it.Router.Handle(method, path, wrappedHandler) + it.Handlers = append(it.Handlers, fmt.Sprintf("%s {%s}", path, method)) +} + +// GetHandler returns original handler function (before wrapping for httprouter.Handle) +func (it *DefaultRestService) GetHandler(method string, resource string) api.FuncAPIHandler { + it.RawHandlerMutex.Lock() + defer it.RawHandlerMutex.Unlock() + + if handle, _, _ := it.Router.Lookup(method, resource); handle != nil { + handle(nil, nil, nil) + } - it.Handlers = append(it.Handlers, path+" {DELETE}") + return it.RawHandler } // ServeHTTP is an entry point for HTTP request, it takes control before request handled diff --git a/api/session/decl.go b/api/session/decl.go index 0753204f..64448ed6 100644 --- a/api/session/decl.go +++ b/api/session/decl.go @@ -30,7 +30,9 @@ var ( // DefaultSession is a default implementer of InterfaceSession declared in // "github.com/ottemo/commerce/api" package -type DefaultSession string +type DefaultSession struct { + id string +} // DefaultSessionService is a basic implementer of InterfaceSessionService declared in // "github.com/ottemo/commerce/api" package diff --git a/api/session/service.go b/api/session/service.go index ad846ed8..6af4a0f4 100644 --- a/api/session/service.go +++ b/api/session/service.go @@ -253,7 +253,7 @@ func (it *DefaultSessionService) Get(sessionID string, create bool) (api.Interfa } if sessionInstance != nil { - resultSession = DefaultSession(sessionInstance.GetID()) + resultSession = &DefaultSession {id: sessionInstance.GetID()} } return resultSession, resultError @@ -279,7 +279,7 @@ func (it *DefaultSessionService) New() (api.InterfaceSession, error) { return nil, env.ErrorDispatch(err) } - return DefaultSession(sessionID), nil + return &DefaultSession {id: sessionID}, nil } // Touch updates session last modification time to current moment diff --git a/api/session/session.go b/api/session/session.go index 534422b3..e0414fa8 100644 --- a/api/session/session.go +++ b/api/session/session.go @@ -5,17 +5,17 @@ package session // GetID returns current session id func (it DefaultSession) GetID() string { - return string(it) + return it.id } // Get returns session value by a given key or nil - if not set func (it DefaultSession) Get(key string) interface{} { - return SessionService.GetKey(string(it), key) + return SessionService.GetKey(it.id, key) } // Set assigns value to session key func (it DefaultSession) Set(key string, value interface{}) { - SessionService.SetKey(string(it), key, value) + SessionService.SetKey(it.id, key, value) } // IsEmpty checks if session contains data @@ -25,10 +25,10 @@ func (it DefaultSession) IsEmpty() bool { // Touch updates session last modification time to current moment func (it DefaultSession) Touch() error { - return SessionService.Touch(string(it)) + return SessionService.Touch(it.id) } // Close makes current session instance expired func (it DefaultSession) Close() error { - return SessionService.Close(string(it)) + return SessionService.Close(it.id) } diff --git a/app/models/helpers.go b/app/models/helpers.go index c6aacf01..4fcddef9 100644 --- a/app/models/helpers.go +++ b/app/models/helpers.go @@ -142,9 +142,8 @@ func ApplyFilters(context api.InterfaceApplicationContext, collection db.Interfa if attributeType != db.ConstTypeText && attributeType != db.ConstTypeID && !strings.Contains(attributeType, db.ConstTypeVarchar) && filterOperator == "like" { - - filterOperator = "=" - } + filterOperator = "=" + } if typedValue, err := utils.StringToType(attributeValue, attributeType); err == nil { // fix for NULL db boolean values filter (perhaps should be part of DB adapter) diff --git a/app/models/manager.go b/app/models/manager.go index 771bb1cd..d954369e 100644 --- a/app/models/manager.go +++ b/app/models/manager.go @@ -10,7 +10,6 @@ func RegisterModel(ModelName string, Model InterfaceModel) error { return env.ErrorNew(ConstErrorModule, ConstErrorLevel, "0300eb6b-08b8-497e-afd0-eda0ee358596", "The model with name '"+ModelName+"' has already been registered") } declaredModels[ModelName] = Model - return nil } diff --git a/app/models/seo/helpers.go b/app/models/seo/helpers.go index 6a33c44b..00432c27 100644 --- a/app/models/seo/helpers.go +++ b/app/models/seo/helpers.go @@ -59,3 +59,18 @@ func LoadSEOItemByID(SEOItemID string) (InterfaceSEOItem, error) { return SEOItemModel, nil } + +// GetProductCollectionModel retrieves current InterfaceProductCollection model implementation +func GetSEOItemCollectionModel() (InterfaceSEOCollection, error) { + model, err := models.GetModel(ConstModelNameSEOItemCollection) + if err != nil { + return nil, env.ErrorDispatch(err) + } + + stockModel, ok := model.(InterfaceSEOCollection) + if !ok { + return nil, env.ErrorNew(ConstErrorModule, ConstErrorLevel, "bc30efc8-fcad-4fe6-961d-b97208732376", "model "+model.GetImplementationName()+" is not 'InterfaceSEOCollection' capable") + } + + return stockModel, nil +} diff --git a/basebuild/build.go b/basebuild/build.go index 3ae87001..0dcd2107 100644 --- a/basebuild/build.go +++ b/basebuild/build.go @@ -13,6 +13,7 @@ import ( _ "github.com/ottemo/commerce/api/session" // Session Management service _ "github.com/ottemo/commerce/impex" // Import/Export service _ "github.com/ottemo/commerce/media/fsmedia" // Media Storage service + _ "github.com/ottemo/commerce/env/otto" // Otto - JS like scripting language _ "github.com/ottemo/commerce/app/actors/category" // Category module _ "github.com/ottemo/commerce/app/actors/cms" // CMS Page/Block module @@ -34,7 +35,7 @@ import ( // _ "github.com/ottemo/commerce/app/actors/payment/braintree" // Braintree payment method _ "github.com/ottemo/commerce/app/actors/payment/checkmo" // "Check Money Order" payment method _ "github.com/ottemo/commerce/app/actors/payment/paypal" // PayPal payment method - _ "github.com/ottemo/commerce/app/actors/payment/stripe" // Stripe payment method + // _ "github.com/ottemo/commerce/app/actors/payment/stripe" // Stripe payment method _ "github.com/ottemo/commerce/app/actors/shipping/fedex" // FedEx _ "github.com/ottemo/commerce/app/actors/shipping/flatrate" // Flat Rate diff --git a/env/interfaces.go b/env/interfaces.go index 385c2b43..46022d17 100644 --- a/env/interfaces.go +++ b/env/interfaces.go @@ -173,3 +173,20 @@ type StructConfigItem struct { Image string } + +// InterfaceScript is an interface to interact with the scripting language +type InterfaceScript interface { + GetID() string + Interact() error + Execute(code string) (interface{}, error) + Get(name string) (interface{}, error) + Set(name string, value interface{}) error +} + +// InterfaceScriptEngine is an interface to interact the scripting engine +type InterfaceScriptEngine interface { + GetScriptName() string + GetScriptInstance(in string) InterfaceScript + Get(name string) (interface{}, error) + Set(name string, value interface{}) error +} diff --git a/env/manager.go b/env/manager.go index d9590017..c190bd7c 100644 --- a/env/manager.go +++ b/env/manager.go @@ -17,6 +17,8 @@ var ( // variables to hold callback functions on configuration services startup callbacksOnConfigStart = []func() error{} callbacksOnConfigIniStart = []func() error{} + + declaredScripEngines = map[string]InterfaceScriptEngine{} ) // RegisterOnConfigStart registers new callback on configuration service start @@ -149,3 +151,26 @@ func GetScheduler() InterfaceScheduler { func ConfigEmptyValueValidator(val interface{}) (interface{}, bool) { return val, true } + +// GetModel returns registered in system model +func GetScriptEngine(EngineName string) (InterfaceScriptEngine, error) { + if engine, present := declaredScripEngines[EngineName]; present { + return engine, nil + } + return nil, ErrorNew(ConstErrorModule, ConstErrorLevel, "5d49fd0d-1fed-47dc-8e72-2346f1e778c3", "Unable to find script engine with name '"+EngineName+"'") +} + + +// GetDeclaredScriptEngines returns all currently registered in system script engines +func GetDeclaredScriptEngines() map[string]InterfaceScriptEngine { + return declaredScripEngines +} + +// RegisterModel registers new model to system +func RegisterScriptEngine(EngineName string, ScriptEngine InterfaceScriptEngine) error { + if _, present := declaredScripEngines[EngineName]; present { + return ErrorNew(ConstErrorModule, ConstErrorLevel, "278b6595-29cc-45d8-b599-0e03dae52a46", "Script engine with name '"+EngineName+"' has been already registered") + } + declaredScripEngines[EngineName] = ScriptEngine + return nil +} diff --git a/env/otto/api.go b/env/otto/api.go new file mode 100644 index 00000000..ab31899e --- /dev/null +++ b/env/otto/api.go @@ -0,0 +1,51 @@ +package otto + +import ( + "github.com/ottemo/commerce/api" + "github.com/ottemo/commerce/env" + "github.com/ottemo/commerce/utils" +) + +// setupAPI setups package related API endpoints +func setupAPI() error { + service := api.GetRestService() + service.POST("otto", restOtto) + + return nil +} + +// WEB REST API used to execute Otto script +func restOtto(context api.InterfaceApplicationContext) (interface{}, error) { + if !api.IsAdminSession(context) { + return nil, env.ErrorNew(ConstErrorModule, env.ConstErrorLevelAPI, "edabecda-5a46-4745-a8fa-bfd3cb913cb0", "Operation not allowed.") + } + + scriptID := "" + script := "" + content := context.GetRequestContent() + if dict, ok := content.(map[string]interface{}); ok { + if value, present := dict["value"]; present { + script = utils.InterfaceToString(value) + } + } else { + script = utils.InterfaceToString(content) + } + + session := context.GetSession() + + if value := session.Get(ConstSessionKey); value != nil { + scriptID = utils.InterfaceToString(value) + } else { + scriptID = utils.MakeUUID() + session.Set(ConstSessionKey, scriptID) + } + + vm := engine.GetScriptInstance(scriptID) + + result, err := vm.Execute(script) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/env/otto/decl.go b/env/otto/decl.go new file mode 100644 index 00000000..27d32a52 --- /dev/null +++ b/env/otto/decl.go @@ -0,0 +1,48 @@ +package otto + +import ( + "bytes" + "github.com/ottemo/commerce/api" + "github.com/ottemo/commerce/env" + "github.com/robertkrimen/otto" + "io" + "sync" +) + +// Package global constants +const ( + ConstSessionKey = "script_id" + + ConstErrorModule = "env/otto" + ConstErrorLevel = env.ConstErrorLevelService +) + +var engine *ScriptEngine + +// ScriptEngine is an implementer of InterfaceScriptEngine +type ScriptEngine struct { + mutex sync.RWMutex + mappings map[string]interface{} + instances map[string]*Script +} + +// Script is an implementer of InterfaceScriptEngine +type Script struct { + id string + vm *otto.Otto +} + +// ApplicationContext is an implementor of api.InterfaceApplicationContext +type ApplicationContext struct { + RequestParameters map[string]string + RequestSettings map[string]interface{} + RequestArguments map[string]string + RequestContent interface{} + RequestFiles map[string]io.Reader + + Session api.InterfaceSession + ContextValues map[string]interface{} + Result interface{} + Response *bytes.Buffer + ResponseSettings map[string]interface{} +} diff --git a/env/otto/doc.go b/env/otto/doc.go new file mode 100644 index 00000000..322a245b --- /dev/null +++ b/env/otto/doc.go @@ -0,0 +1,11 @@ +// Copyright 2019 Ottemo. All rights reserved. + +/* + +Package otto represents Ottemo scripting engine implementation. + +That package build based on an existing script engine Otto (https://github.com/robertkrimen/otto) +which is the java script language implementation for Go lang. + +*/ +package otto diff --git a/env/otto/i_application_context.go b/env/otto/i_application_context.go new file mode 100644 index 00000000..09d716ee --- /dev/null +++ b/env/otto/i_application_context.go @@ -0,0 +1,219 @@ +package otto + +import ( + "bytes" + "github.com/ottemo/commerce/env" + "io" + "net/http" + + "github.com/ottemo/commerce/api" + "github.com/ottemo/commerce/utils" +) + +// makeApplicationContext creates new InterfaceApplicationContext instance +func makeApplicationContext() api.InterfaceApplicationContext { + applicationContext := new(ApplicationContext) + applicationContext.Response = bytes.NewBufferString("") + applicationContext.Result = applicationContext.Response + applicationContext.ResponseSettings = make(map[string]interface{}) + + applicationContext.RequestArguments = make(map[string]string) + applicationContext.RequestSettings = make(map[string]interface{}) + applicationContext.RequestParameters = make(map[string]string) + applicationContext.RequestContent = nil + applicationContext.RequestFiles = make(map[string]io.Reader) + applicationContext.ContextValues = make(map[string]interface{}) + + if session, err := api.NewSession(); err == nil { + applicationContext.Session = session + } + + return applicationContext +} + +// apiHandler returns API handler for a giver resource +func apiHandler(method string, resource string) (api.FuncAPIHandler, error) { + if service := api.GetRestService(); service != nil { + if handler := service.GetHandler(method, resource); handler != nil { + return handler, nil + } + return nil, env.ErrorNew(ConstErrorModule, ConstErrorLevel, "f0e17e01-b0a7-43a0-a7e1-90646cd5c309", "Handler not found") + } + return nil, env.ErrorNew(ConstErrorModule, ConstErrorLevel, "e5c20766-e3bc-4020-a918-c5d396b321df", "API service is not available") +} + +// apiCall performs API call for a given resource +func apiCall(method string, resource string, context api.InterfaceApplicationContext) (interface{}, error) { + if context == nil { + context = makeApplicationContext() + } + + handler, err := apiHandler(method, resource) + if err != nil { + return nil, err + } + return handler(context) +} + +// GetRequest returns raw request object +func (it *ApplicationContext) GetRequest() interface{} { + return nil +} + +// GetResponse returns raw response object +func (it *ApplicationContext) GetResponse() interface{} { + return it.Result +} + +// GetResponseWriter returns io.Writes for response (for ApplicationContext it is clone to GetResponse) +func (it *ApplicationContext) GetResponseWriter() io.Writer { + return it.Response +} + +// GetRequestArguments returns all arguments provided to API function +// - for REST API it is URI parameters "http://localhost/myfunc/:param1/get/:param2/:param3/" +func (it *ApplicationContext) GetRequestArguments() map[string]string { + return it.RequestArguments +} + +// GetRequestArgument returns particular argument provided to API function or "" +func (it *ApplicationContext) GetRequestArgument(name string) string { + if value, present := it.RequestArguments[name]; present { + return value + } + return "" +} + +// GetRequestContent returns request contents of nil if not specified (HTTP request body) +func (it *ApplicationContext) GetRequestContent() interface{} { + return it.RequestContent +} + +// GetRequestFiles returns files were attached to request +func (it *ApplicationContext) GetRequestFiles() map[string]io.Reader { + return it.RequestFiles +} + +// GetRequestFile returns particular file attached to request or nil +func (it *ApplicationContext) GetRequestFile(name string) io.Reader { + if file, present := it.RequestFiles[name]; present { + return file + } + return nil +} + +// GetRequestSettings returns request related settings +// - for REST API settings are HTTP headers and COOKIES +func (it *ApplicationContext) GetRequestSettings() map[string]interface{} { + return it.RequestSettings +} + +// GetRequestSetting returns particular request related setting or nil +func (it *ApplicationContext) GetRequestSetting(name string) interface{} { + if value, present := it.RequestSettings[name]; present { + return value + } + return nil +} + +// GetRequestContentType returns MIME type of request content +func (it *ApplicationContext) GetRequestContentType() string { + if value, present := it.RequestSettings["Content-Type"]; present { + return utils.InterfaceToString(value) + } + return "" +} + +// GetResponseContentType returns MIME type of supposed response content +func (it *ApplicationContext) GetResponseContentType() string { + if value, present := it.ResponseSettings["Content-Type"]; present { + return utils.InterfaceToString(value) + } + return "" +} + +// SetResponseContentType changes response content type, returns error if not possible +func (it *ApplicationContext) SetResponseContentType(mimeType string) error { + it.ResponseSettings["Content-Type"] = mimeType + return nil +} + +// GetResponseSetting returns specified setting value (for REST API returns header as settings) +func (it *ApplicationContext) GetResponseSetting(name string) interface{} { + if value, present := it.RequestSettings[name]; present { + return value + } + return nil +} + +// SetResponseSetting specifies response setting (for REST API it just sets additional header) +func (it *ApplicationContext) SetResponseSetting(name string, value interface{}) error { + it.ResponseSettings[name] = value + return nil +} + +// SetResponseStatus will set an HTTP response code +// - code is an integer correlating to HTTP response codes +func (it *ApplicationContext) SetResponseStatus(code int) { + it.ResponseSettings["status"] = code +} + +// SetResponseStatusBadRequest will set the ResponseWriter to StatusBadRequest (400) +func (it *ApplicationContext) SetResponseStatusBadRequest() { + it.ResponseSettings["status"] = http.StatusBadRequest +} + +// SetResponseStatusForbidden will set the ResponseWriter to StatusForbidden (403) +func (it *ApplicationContext) SetResponseStatusForbidden() { + it.ResponseSettings["status"] = http.StatusForbidden +} + +// SetResponseStatusNotFound will set the ResponseWriter to StatusNotFound (404) +func (it *ApplicationContext) SetResponseStatusNotFound() { + it.ResponseSettings["status"] = http.StatusNotFound +} + +// SetResponseStatusInternalServerError will set the ResponseWriter to StatusInternalServerError (500) +func (it *ApplicationContext) SetResponseStatusInternalServerError() { + it.ResponseSettings["status"] = http.StatusInternalServerError +} + +// GetResponseResult returns result going to be written to response writer +func (it *ApplicationContext) GetResponseResult() interface{} { + return it.Result +} + +// SetResponseResult changes result going to be written to response writer +func (it *ApplicationContext) SetResponseResult(value interface{}) error { + it.Result = value + return nil +} + +// GetContextValues returns current context related values map +func (it *ApplicationContext) GetContextValues() map[string]interface{} { + return it.ContextValues +} + +// GetContextValue returns particular context related value or nil if not set +func (it *ApplicationContext) GetContextValue(key string) interface{} { + if value, present := it.ContextValues[key]; present { + return value + } + return nil +} + +// SetContextValue stores specified value in current context +func (it *ApplicationContext) SetContextValue(key string, value interface{}) { + it.ContextValues[key] = value +} + +// SetSession assigns given session to current context +func (it *ApplicationContext) SetSession(session api.InterfaceSession) error { + it.Session = session + return nil +} + +// GetSession returns session assigned to current context or nil if nothing was assigned +func (it *ApplicationContext) GetSession() api.InterfaceSession { + return it.Session +} diff --git a/env/otto/i_script.go b/env/otto/i_script.go new file mode 100644 index 00000000..78d316ef --- /dev/null +++ b/env/otto/i_script.go @@ -0,0 +1,34 @@ +package otto + +import ( + "github.com/robertkrimen/otto/repl" +) + +// GetID returns the script identifier +func (it *Script) GetID() string { + return it.id +} + +// Execute executing given script +func (it *Script) Execute(code string) (interface{}, error) { + value, err := it.vm.Run(code) + if err != nil { + return nil, err + } + return value.Export() +} + +// Get returns the script context variable +func (it *Script) Get(name string) (interface{}, error) { + return it.vm.Get(name) +} + +// Set specifies script context variable +func (it *Script) Set(name string, value interface{}) error { + return it.vm.Set(name, value) +} + +// Interact run script instance in interaction mode +func (it *Script) Interact() error { + return repl.RunWithOptions(it.vm, repl.Options{Prompt: "otto> ", Autocomplete: true}) +} diff --git a/env/otto/i_scriptengine.go b/env/otto/i_scriptengine.go new file mode 100644 index 00000000..4228de68 --- /dev/null +++ b/env/otto/i_scriptengine.go @@ -0,0 +1,55 @@ +package otto + +import ( + "github.com/ottemo/commerce/env" + "github.com/ottemo/commerce/utils" + "github.com/robertkrimen/otto" +) + +// GetScriptName returns ScriptEngine instance name +func (it *ScriptEngine) GetScriptName() string { + return "Otto" +} + +// GetScriptInstance returns new instance for scripting +func (it *ScriptEngine) GetScriptInstance(id string) env.InterfaceScript { + + if instance, present := it.instances[id]; present { + return instance + } + + script := new(Script) + script.vm = otto.New() + script.id = id + + it.mutex.Lock() + defer it.mutex.Unlock() + + for key, value := range it.mappings { + script.vm.Set(key, value) + } + + // TODO: implement instances cleanup (lifetime based) + it.instances[script.id] = script + + return script +} + +// Get returns value which is available for a new script instances +func (it *ScriptEngine) Get(path string) (interface{}, error) { + it.mutex.Lock() + defer it.mutex.Unlock() + + return utils.MapGetPathValue(it.mappings, path) +} + +// Set specifies a value for all new script instances +func (it *ScriptEngine) Set(path string, value interface{}) error { + it.mutex.Lock() + defer it.mutex.Unlock() + + if value == nil { + return utils.MapSetPathValue(it.mappings, path, value, true) + } + return utils.MapSetPathValue(it.mappings, path, value, false) +} diff --git a/env/otto/init.go b/env/otto/init.go new file mode 100644 index 00000000..94a60bfb --- /dev/null +++ b/env/otto/init.go @@ -0,0 +1,212 @@ +package otto + +import ( + "fmt" + "github.com/ottemo/commerce/api" + "github.com/ottemo/commerce/app" + "github.com/ottemo/commerce/app/models" + "github.com/ottemo/commerce/app/models/cart" + "github.com/ottemo/commerce/app/models/category" + "github.com/ottemo/commerce/app/models/checkout" + "github.com/ottemo/commerce/app/models/cms" + "github.com/ottemo/commerce/app/models/order" + "github.com/ottemo/commerce/app/models/product" + "github.com/ottemo/commerce/app/models/seo" + "github.com/ottemo/commerce/app/models/stock" + "github.com/ottemo/commerce/app/models/subscription" + "github.com/ottemo/commerce/app/models/visitor" + "github.com/ottemo/commerce/db" + "github.com/ottemo/commerce/media" + + "github.com/ottemo/commerce/env" + "github.com/ottemo/commerce/impex" + "github.com/ottemo/commerce/utils" +) + +// init performs package self-initialization +func init() { + api.RegisterOnRestServiceStart(setupAPI) + + engine = new(ScriptEngine) + engine.instances = make(map[string]*Script) + engine.mappings = make(map[string]interface{}) + + engine.Set("printf", fmt.Sprintf) + engine.Set("print", fmt.Sprint) + + engine.Set("api.makeContext", makeApplicationContext) + engine.Set("api.getHandler", apiHandler) + engine.Set("api.call", apiCall) + + engine.Set("app.getVersion", app.GetVerboseVersion) + engine.Set("app.getStorefrontURL", app.GetStorefrontURL) + engine.Set("app.getDashboardURL", app.GetDashboardURL) + engine.Set("app.getCommerceURL", app.GetcommerceURL) + + engine.Set("app.sendMail", app.SendMail) + engine.Set("app.sendMailEx", app.SendMailEx) + + engine.Set("model.get", models.LoadModelByID) + engine.Set("model.getModel", models.GetModel) + engine.Set("model.getModels", models.GetDeclaredModels) + + engine.Set("model.test", func(x interface{ GetName() string }) string { return x.GetName() }) + + engine.Set("model.product.get", product.LoadProductByID) + engine.Set("model.product.getModel", product.GetProductModel) + engine.Set("model.product.getCollection", product.GetProductCollectionModel) + engine.Set("model.product.getStock", product.GetRegisteredStock()) + + engine.Set("model.stock.getModel", stock.GetStockModel) + engine.Set("model.stock.getCollection", stock.GetStockCollectionModel) + + engine.Set("model.visitor.get", visitor.LoadVisitorByID) + engine.Set("model.visitor.getModel", visitor.GetVisitorModel) + engine.Set("model.visitor.getCollection", visitor.GetVisitorCollectionModel) + engine.Set("model.visitor.getAddressModel", visitor.GetVisitorAddressModel) + engine.Set("model.visitor.getCardModel", visitor.GetVisitorCardModel) + + engine.Set("model.subscription.get", subscription.LoadSubscriptionByID) + engine.Set("model.subscription.getModel", subscription.GetSubscriptionModel) + engine.Set("model.subscription.getCollection", subscription.GetSubscriptionCollectionModel) + engine.Set("model.subscription.getOptionValues", subscription.GetSubscriptionOptionValues) + engine.Set("model.subscription.getCronExpr", subscription.GetSubscriptionCronExpr) + engine.Set("model.subscription.getPeriodValue", subscription.GetSubscriptionPeriodValue) + engine.Set("model.subscription.isEnabled", subscription.IsSubscriptionEnabled) + + engine.Set("model.seo.get", seo.LoadSEOItemByID) + engine.Set("model.seo.getModel", seo.GetSEOItemModel) + engine.Set("model.seo.getCollection", seo.GetSEOItemCollectionModel) + engine.Set("model.seo.getEngine", seo.GetRegisteredSEOEngine) + + engine.Set("model.order.get", order.LoadOrderByID) + engine.Set("model.order.getModel", order.GetOrderModel) + engine.Set("model.order.getCollection", order.GetOrderCollectionModel) + engine.Set("model.order.getItemCollection", order.GetOrderItemCollectionModel) + engine.Set("model.order.getItemsForOrders", order.GetItemsForOrders) + engine.Set("model.order.getOrdersCreatedBetween", order.GetOrdersCreatedBetween) + engine.Set("model.order.getOrdersUpdatedBetween", order.GetFullOrdersUpdatedBetween) + + engine.Set("model.cms.getBlockById", cms.LoadCMSBlockByID) + engine.Set("model.cms.getBlockByIdentifier", cms.LoadCMSBlockByIdentifier) + engine.Set("model.cms.getBlockModel", cms.GetCMSBlockModel) + engine.Set("model.cms.getBlockCollection", cms.GetCMSBlockCollectionModel) + engine.Set("model.cms.getPageById", cms.LoadCMSPageByID) + engine.Set("model.cms.getPageByIdentifier", cms.LoadCMSPageByIdentifier) + engine.Set("model.cms.getPageModel", cms.GetCMSPageModel) + engine.Set("model.cms.getPageCollection", cms.GetCMSPageCollectionModel) + + engine.Set("model.category.get", category.LoadCategoryByID) + engine.Set("model.category.getModel", category.GetCategoryModel) + engine.Set("model.category.getCollection", category.GetCategoryCollectionModel) + + engine.Set("model.checkout.getModel", checkout.GetCheckoutModel) + engine.Set("model.checkout.getPaymentMethodByCode", checkout.GetPaymentMethodByCode) + engine.Set("model.checkout.getShippingMethodByCode", checkout.GetShippingMethodByCode) + engine.Set("model.checkout.getPaymentMethods", checkout.GetRegisteredPaymentMethods) + engine.Set("model.checkout.getShippingMethods", checkout.GetRegisteredShippingMethods) + engine.Set("model.checkout.validateAddress", checkout.ValidateAddress) + + engine.Set("model.cart.get", cart.LoadCartByID) + engine.Set("model.cart.getModel", cart.GetCartModel) + engine.Set("model.cart.getCartForVisitor", cart.GetCartForVisitor) + + engine.Set("db.getEngine", db.GetDBEngine) + engine.Set("db.getCollection", db.GetCollection) + + engine.Set("env.getConfig", env.GetConfig) + engine.Set("env.getIniConfig", env.GetIniConfig) + engine.Set("env.getLogger", env.GetLogger) + engine.Set("env.getScheduler", env.GetScheduler) + engine.Set("env.getErrorBus", env.GetErrorBus) + engine.Set("env.getEventBus", env.GetEventBus) + engine.Set("env.getScriptEngine", env.GetScriptEngine) + + engine.Set("env.configValue", env.ConfigGetValue) + engine.Set("env.iniValue", env.IniValue) + engine.Set("env.log", env.Log) + engine.Set("env.logError", env.LogError) + engine.Set("env.logEvent", env.LogEvent) + engine.Set("env.getErrorLevel", env.ErrorLevel) + engine.Set("env.getErrorCode", env.ErrorCode) + engine.Set("env.getErrorMessage", env.ErrorMessage) + engine.Set("env.error.registerListener", env.ErrorRegisterListener) + engine.Set("env.error.dispatch", env.ErrorRegisterListener) + engine.Set("env.error.registerListener", env.ErrorRegisterListener) + engine.Set("env.error.dispatch", env.ErrorRegisterListener) + engine.Set("env.error.new", env.ErrorNew) + engine.Set("env.event.registerListener", env.EventRegisterListener) + engine.Set("env.event.dispatch", env.Event) + + engine.Set("media.get", media.GetMediaStorage) + + engine.Set("impex.importCSV", impex.ImportCSVData) + engine.Set("impex.importCSVScript", impex.ImportCSVScript) + engine.Set("impex.mapToCSV", impex.MapToCSV) + engine.Set("impex.CSVToMap", impex.CSVToMap) + + engine.Set("utils.cryptToURLString", utils.CryptToURLString) + engine.Set("utils.decryptURLString", utils.DecryptURLString) + engine.Set("utils.passwordEncode", utils.PasswordEncode) + engine.Set("utils.passwordCheck", utils.PasswordCheck) + engine.Set("utils.encryptData", utils.EncryptData) + engine.Set("utils.decryptData", utils.DecryptData) + + engine.Set("utils.isZeroTime", utils.IsZeroTime) + engine.Set("utils.isMD5", utils.IsMD5) + engine.Set("utils.isAmongStr", utils.IsAmongStr) + engine.Set("utils.isInArray", utils.IsInArray) + engine.Set("utils.isInListStr", utils.IsInListStr) + engine.Set("utils.isBlank", utils.CheckIsBlank) + engine.Set("utils.stringToFloat", utils.StringToFloat) + engine.Set("utils.stringToInteger", utils.StringToInteger) + engine.Set("utils.stringToType", utils.StringToType) + engine.Set("utils.interfaceToBool", utils.InterfaceToBool) + engine.Set("utils.interfaceToFloat64", utils.InterfaceToFloat64) + engine.Set("utils.interfaceToInt", utils.InterfaceToInt) + engine.Set("utils.interfaceToMap", utils.InterfaceToMap) + engine.Set("utils.interfaceToString", utils.InterfaceToString) + engine.Set("utils.interfaceToStringArray", utils.InterfaceToStringArray) + engine.Set("utils.interfaceToTime", utils.InterfaceToTime) + engine.Set("utils.interfaceToMap", utils.InterfaceToMap) + + engine.Set("utils.getTemplateFunctions", utils.GetTemplateFunctions) + engine.Set("utils.registerTemplateFunction", utils.RegisterTemplateFunction) + engine.Set("utils.textTemplate", utils.TextTemplate) + + engine.Set("utils.timezones", utils.TimeZones) + engine.Set("utils.parseTimeZone", utils.ParseTimeZone) + engine.Set("utils.makeTZTime", utils.MakeTZTime) + engine.Set("utils.timeToUTCTime", utils.TimeToUTCTime) + + engine.Set("utils.getPointer", utils.GetPointer) + engine.Set("utils.syncGet", utils.SyncGet) + engine.Set("utils.syncSet", utils.SyncSet) + engine.Set("utils.syncMutex", utils.SyncMutex) + engine.Set("utils.syncLock", utils.SyncLock) + engine.Set("utils.syncUnlock", utils.SyncUnlock) + + engine.Set("utils.sortMapByKeys", utils.SortMapByKeys) + + engine.Set("utils.encodeToJSONString", utils.EncodeToJSONString) + engine.Set("utils.decodeJSONToArray", utils.DecodeJSONToArray) + engine.Set("utils.decodeJSONToInterface", utils.DecodeJSONToInterface) + engine.Set("utils.DecodeJSONToStringKeyMap", utils.DecodeJSONToStringKeyMap) + + engine.Set("utils.KeysInMapAndNotBlank", utils.KeysInMapAndNotBlank) + engine.Set("utils.GetFirstMapValue", utils.GetFirstMapValue) + engine.Set("utils.Explode", utils.Explode) + engine.Set("utils.Round", utils.Round) + engine.Set("utils.RoundPrice", utils.RoundPrice) + engine.Set("utils.SplitQuotedStringBy", utils.SplitQuotedStringBy) + engine.Set("utils.MatchMapAValuesToMapB", utils.MatchMapAValuesToMapB) + engine.Set("utils.EscapeRegexSpecials", utils.EscapeRegexSpecials) + engine.Set("utils.ValidEmailAddress", utils.ValidEmailAddress) + engine.Set("utils.Clone", utils.Clone) + engine.Set("utils.StrToSnakeCase", utils.StrToSnakeCase) + engine.Set("utils.StrToCamelCase", utils.StrToCamelCase) + engine.Set("utils.MapGetPathValue", utils.MapGetPathValue) + engine.Set("utils.MapSetPathValue", utils.MapSetPathValue) + + env.RegisterScriptEngine("Otto", engine) +} diff --git a/main.go b/main.go index a91e177b..81014f82 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,13 @@ func main() { fmt.Println("Ottemo " + app.GetVerboseVersion()) + go func() { + for _, engine := range env.GetDeclaredScriptEngines() { + engine.GetScriptInstance("main").Interact() + break + } + }() + // starting HTTP server if err := app.Serve(); err != nil { fmt.Println(err.Error()) diff --git a/utils/datatypes.go b/utils/datatypes.go index 9bad6c22..efd46b31 100644 --- a/utils/datatypes.go +++ b/utils/datatypes.go @@ -1,6 +1,8 @@ package utils import ( + "crypto/rand" + "encoding/hex" "errors" "reflect" "regexp" @@ -620,3 +622,18 @@ func StringToInterface(value string) interface{} { return value } + +// MakeUUID returns unique identifier based on a moment of call +func MakeUUID() string { + timeStamp := strconv.FormatInt(time.Now().Unix(), 16) + + randomBytes := make([]byte, 8) + if _, err := rand.Reader.Read(randomBytes); err != nil { + return timeStamp + } + + randomHex := make([]byte, 16) + hex.Encode(randomHex, randomBytes) + + return timeStamp + string(randomHex) +} diff --git a/utils/generic.go b/utils/generic.go index be26716a..e5d5c594 100644 --- a/utils/generic.go +++ b/utils/generic.go @@ -1,6 +1,8 @@ package utils import ( + "errors" + "fmt" "math" "reflect" "regexp" @@ -337,3 +339,66 @@ func StrToCamelCase(str string) string { return str } + +// MapGetPathValue returns the '.' separated 'path' key value within map[string]interface{} +// (i.e. MapGetPathValue(x, "a.b.c") equals to expression x["a"]["b"]["c"]) +func MapGetPathValue(subject map[string]interface{}, key string) (interface{}, error) { + path := strings.Split(key, ".") + for i, x := range path { + if value, present := subject[x]; present { + if i == len(path) -1 { + return value, nil + } + + if value, ok := value.(map[string]interface{}); ok { + subject = value + } else { + return nil, errors.New(fmt.Sprintf("path %s have incompatible type %T", path[0:i], value)) + } + } else { + return nil, errors.New(fmt.Sprintf("path %v does not exist", path[0:i])) + } + } + return nil, nil +} + +// MapSetPathValue puts the '.' separated 'path' key value into map[string]interface{} +// (i.e. MapSetPathValue(x, "a.b.c", 10) equals to expression x["a"]["b"]["c"]=10) +// if remove=true it removes the path keys within given map +// (i.e. delete(x["a"]["b"], "c"); delete(x["a"], "b"); delete(x, "a")) +func MapSetPathValue(subject map[string]interface{}, key string, value interface{}, remove bool) error { + var stack []map[string]interface{} + path := strings.Split(key, ".") + + for i, x := range path { + stack = append(stack, subject) + + if i == len(path) - 1 { + if remove { + delete(subject, x) + for i:=len(stack)-1; i > 0; i-- { + if len(stack[i]) == 0 { + delete(stack[i-1], path[i-1]) + } + } + } else { + subject[x] = value + } + } else { + newSubject := map[string]interface{} {} + + if value, present := subject[x]; present { + if value, ok := value.(map[string]interface{}); ok { + newSubject = value + } else { + newSubject["value"] = value + return errors.New(fmt.Sprintf("path %s have incompatible type %T", path[0:i], value)) + } + } + + subject[x] = newSubject + subject = newSubject + } + } + return nil +} diff --git a/utils/generic_test.go b/utils/generic_test.go index 22427379..9e88b2fb 100644 --- a/utils/generic_test.go +++ b/utils/generic_test.go @@ -2,6 +2,7 @@ package utils import ( "testing" + "fmt" ) func TestMatchMapAValuesToMapB(t *testing.T) { @@ -148,3 +149,32 @@ func TestStrToCamelCase(t *testing.T) { t.Error("case 2 fail") } } + +func TestMapSetPathValue(t *testing.T) { + x := map[string]interface{} { } + + if e := MapSetPathValue(x, "a.b.c.d.e.f", "something", false); e != nil { + t.Error(e) + } + + if e := MapSetPathValue(x, "a.g", "something else", false); e != nil { + t.Error(e) + } + + + if value, _ := MapGetPathValue(x, "a.b.c.d.e.f"); value != "something" { + t.Error(fmt.Sprintf("test case 1 fail: %v != 'something'", value)) + } + + if e := MapSetPathValue(x, "a.b.c.d.e.f", nil, true); e != nil { + t.Error(e) + } + + if value, _ := MapGetPathValue(x, "a.b.c.d.e.f"); value != nil { + t.Error(fmt.Sprintf("test case 2 fail: %v != nil", value)) + } + + if value, _ := MapGetPathValue(x, "a.g"); value != "something else" { + t.Error(fmt.Sprintf("test case 3 fail: %v != 'something else'", value)) + } +}