Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task #509: Otto scripting #510

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions api/rest/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rest
import (
"io"
"net/http"
"sync"

"github.com/julienschmidt/httprouter"
"github.com/ottemo/commerce/api"
Expand All @@ -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
Expand Down
49 changes: 31 additions & 18 deletions api/rest/i_restservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion api/session/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions api/session/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions api/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
5 changes: 2 additions & 3 deletions app/models/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion app/models/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
15 changes: 15 additions & 0 deletions app/models/seo/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion basebuild/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions env/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
25 changes: 25 additions & 0 deletions env/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
51 changes: 51 additions & 0 deletions env/otto/api.go
Original file line number Diff line number Diff line change
@@ -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
}
48 changes: 48 additions & 0 deletions env/otto/decl.go
Original file line number Diff line number Diff line change
@@ -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{}
}
11 changes: 11 additions & 0 deletions env/otto/doc.go
Original file line number Diff line number Diff line change
@@ -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
Loading