diff --git a/service/middleware.go b/service/middleware.go index ca966193..fea51030 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/sui/api" ) // Middlewares the middlewares @@ -49,8 +50,21 @@ func withStaticFileServer(c *gin.Context) { // Sui file server if strings.HasSuffix(c.Request.URL.Path, ".sui") { - data := []byte(`SUI Server: ` + c.Request.URL.Path) - c.Data(200, "text/html; charset=utf-8", data) + + r, code, err := api.NewRequestContext(c) + if err != nil { + c.AbortWithError(code, err) + return + } + + html, code, err := r.Render() + if err != nil { + c.AbortWithError(code, err) + return + } + + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(200, html) c.Done() return } diff --git a/sui/api/request.go b/sui/api/request.go new file mode 100644 index 00000000..f2a134d1 --- /dev/null +++ b/sui/api/request.go @@ -0,0 +1,172 @@ +package api + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/gou/application" + "github.com/yaoapp/yao/sui/core" +) + +// Request is the request for the page API. +type Request struct { + File string + *core.Request +} + +// NewRequestContext is the constructor for Request. +func NewRequestContext(c *gin.Context) (*Request, int, error) { + + file, params, err := parserPath(c) + if err != nil { + return nil, 404, err + } + + payload, body, err := payload(c) + if err != nil { + return nil, 500, err + } + + return &Request{ + File: file, + Request: &core.Request{ + Method: c.Request.Method, + Query: c.Request.URL.Query(), + Body: body, + Payload: payload, + Referer: c.Request.Referer(), + Headers: c.Request.Header, + Params: params, + }, + }, 200, nil +} + +// Render is the response for the page API. +func (r *Request) Render() (string, int, error) { + return r.File, 200, nil +} + +func parserPath(c *gin.Context) (string, map[string]string, error) { + + params := map[string]string{} + + parts := strings.Split(strings.TrimSuffix(c.Request.URL.Path, ".sui"), "/")[1:] + if len(parts) < 1 { + return "", nil, fmt.Errorf("no route matchers") + } + + fileParts := []string{application.App.Root(), "public"} + + // Match the sui + matchers := core.RouteExactMatchers[parts[0]] + if matchers == nil { + for matcher, reMatchers := range core.RouteMatchers { + matched := matcher.FindStringSubmatch(parts[0]) + if len(matched) > 0 { + matchers = reMatchers + fileParts = append(fileParts, matched[0]) + break + } + } + } + + if matchers == nil { + return "", nil, fmt.Errorf("no route matchers") + } + + // Match the page parts + for i, part := range parts[1:] { + if len(matchers) < i+1 { + return "", nil, fmt.Errorf("no route matchers") + } + + parent := "" + if i > 0 { + parent = parts[i] + } + matched := false + for _, matcher := range matchers[i] { + + // Filter the parent + if matcher.Parent != "" && matcher.Parent != parent { + continue + } + + if matcher.Exact == part { + fileParts = append(fileParts, matcher.Exact) + matched = true + break + + } else if matcher.Regex != nil { + if matcher.Regex.MatchString(part) { + file := matcher.Ref.(string) + key := strings.TrimRight(strings.TrimLeft(file, "["), "]") + params[key] = part + fileParts = append(fileParts, file) + matched = true + break + } + } + } + + if !matched { + return "", nil, fmt.Errorf("no route matchers") + } + } + return filepath.Join(fileParts...) + ".sui", params, nil +} + +func params(c *gin.Context) map[string]string { + return nil +} + +func payload(c *gin.Context) (map[string]interface{}, interface{}, error) { + contentType := c.Request.Header.Get("Content-Type") + var payload map[string]interface{} + var body interface{} + + switch contentType { + case "application/x-www-form-urlencoded": + c.Request.ParseForm() + payload = make(map[string]interface{}) + for key, value := range c.Request.Form { + payload[key] = value + } + body = nil + break + + case "multipart/form-data": + c.Request.ParseMultipartForm(32 << 20) + payload = make(map[string]interface{}) + for key, value := range c.Request.MultipartForm.Value { + payload[key] = value + } + body = nil + break + + case "application/json": + if c.Request.Body == nil { + return nil, nil, nil + } + + c.Bind(&payload) + body = nil + break + + default: + if c.Request.Body == nil { + return nil, nil, nil + } + + var data []byte + _, err := c.Request.Body.Read(data) + if err != nil && err.Error() != "EOF" { + return nil, nil, err + } + body = data + } + + return payload, body, nil +} diff --git a/sui/api/sui.go b/sui/api/sui.go index 5186b2a3..d6606ccc 100644 --- a/sui/api/sui.go +++ b/sui/api/sui.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "regexp" "strings" "github.com/yaoapp/gou/application" @@ -45,7 +46,7 @@ func Load(cfg config.Config) error { _, err := loadFile(file, id) if err != nil { log.Error("[sui] Load sui %s error: %s", id, err.Error()) - return err + return nil } return nil }, exts...) @@ -54,6 +55,7 @@ func Load(cfg config.Config) error { return err } + buildRouteMatchers() return registerAPI() } @@ -72,3 +74,71 @@ func loadFile(file string, id string) (core.SUI, error) { core.SUIs[id] = sui return core.SUIs[id], nil } + +func buildRouteMatchers() (map[*regexp.Regexp][][]*core.Matcher, map[string][][]*core.Matcher) { + matchers := map[*regexp.Regexp][][]*core.Matcher{} + exactMatchers := map[string][][]*core.Matcher{} + for id, sui := range core.SUIs { + suiMatcher := sui.PublicRootMatcher() + if suiMatcher.Regex != nil { + matchers[suiMatcher.Regex] = [][]*core.Matcher{} + + } else if suiMatcher.Exact != "" { + exactMatchers[suiMatcher.Exact] = [][]*core.Matcher{} + + } else { + log.Error("[sui] Load sui %s error: %s", id, "the public root is empty") + continue + } + + tmpls, err := sui.GetTemplates() + if err != nil { + log.Error("[sui] Load sui %s error: %s", id, err.Error()) + continue + } + + for _, tmpl := range tmpls { + pages, err := tmpl.Pages() + if err != nil { + log.Error("[sui] Load sui %s error: %s", id, err.Error()) + continue + } + + for _, page := range pages { + route := page.Get().Route + parts := strings.Split(route, "/")[1:] + + for i, part := range parts { + parent := "" + if i > 0 { + parent = parts[i-1] + } + matcher := &core.Matcher{Ref: part, Parent: parent} + if strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { + matcher.Regex = core.RouteRegexp + } else { + matcher.Exact = part + } + + if suiMatcher.Regex != nil { + if len(matchers[suiMatcher.Regex]) < i+1 { + matchers[suiMatcher.Regex] = append(matchers[suiMatcher.Regex], []*core.Matcher{}) + } + matchers[suiMatcher.Regex][i] = append(matchers[suiMatcher.Regex][i], matcher) + } + + if suiMatcher.Exact != "" { + if len(exactMatchers[suiMatcher.Exact]) < i+1 { + exactMatchers[suiMatcher.Exact] = append(exactMatchers[suiMatcher.Exact], []*core.Matcher{}) + } + exactMatchers[suiMatcher.Exact][i] = append(exactMatchers[suiMatcher.Exact][i], matcher) + } + } + } + } + } + + core.RouteMatchers = matchers + core.RouteExactMatchers = exactMatchers + return matchers, exactMatchers +} diff --git a/sui/api/sui_test.go b/sui/api/sui_test.go index d9b52239..ff7c8a15 100644 --- a/sui/api/sui_test.go +++ b/sui/api/sui_test.go @@ -26,7 +26,7 @@ func check(t *testing.T) { for id := range core.SUIs { ids[id] = true } - assert.True(t, ids["azure"]) + assert.False(t, ids["azure"]) assert.True(t, ids["demo"]) assert.True(t, ids["screen"]) } diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index cc69999f..2a03f8f6 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -3,11 +3,21 @@ package core import ( "io" "net/url" + "regexp" ) // SUIs the loaded SUI instances var SUIs = map[string]SUI{} +// RouteMatchers the route matchers for the SUI instance +var RouteMatchers = map[*regexp.Regexp][][]*Matcher{} + +// RouteExactMatchers the route exact matchers for the SUI instance +var RouteExactMatchers = map[string][][]*Matcher{} + +// RouteRegexp the regexp for the route +var RouteRegexp = regexp.MustCompile(`([a-z0-9A-Z_\-]+)`) + // SUI is the interface for the SUI type SUI interface { Setting() (*Setting, error) @@ -15,6 +25,8 @@ type SUI interface { GetTemplate(name string) (ITemplate, error) UploadTemplate(src string, dst string) (ITemplate, error) WithSid(sid string) + PublicRootMatcher() *Matcher + GetPublic() *Public } // ITemplate is the interface for the ITemplate diff --git a/sui/core/render.go b/sui/core/parser.go similarity index 100% rename from sui/core/render.go rename to sui/core/parser.go diff --git a/sui/core/render_test.go b/sui/core/parser_test.go similarity index 100% rename from sui/core/render_test.go rename to sui/core/parser_test.go diff --git a/sui/core/sui.go b/sui/core/sui.go index 10cfa8f9..139aa222 100644 --- a/sui/core/sui.go +++ b/sui/core/sui.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/yaoapp/gou/session" + "github.com/yaoapp/kun/log" "github.com/yaoapp/kun/maps" ) @@ -27,6 +28,24 @@ func (sui *DSL) WithSid(sid string) { sui.Sid = sid } +// PublicRootMatcher returns the public root matcher +func (sui *DSL) PublicRootMatcher() *Matcher { + var ref SUI = sui + pub := sui.GetPublic() + if varRe.MatchString(pub.Root) { + if pub.Matcher != "" { + re, err := regexp.Compile(pub.Matcher) + if err != nil { + log.Error("[sui] %s matcher error %s, use the default matcher", sui.ID, err.Error()) + return &Matcher{Regex: RouteRegexp} + } + return &Matcher{Regex: re} + } + return &Matcher{Regex: RouteRegexp, Ref: ref} + } + return &Matcher{Exact: pub.Root, Ref: ref} +} + // PublicRoot returns the public root path func (sui *DSL) PublicRoot() (string, error) { // Cache the public root @@ -55,3 +74,23 @@ func (sui *DSL) PublicRoot() (string, error) { sui.publicRoot = output return output, nil } + +// GetTemplate returns the template +func (sui *DSL) GetTemplate(name string) (ITemplate, error) { + return nil, nil +} + +// GetTemplates returns the templates +func (sui *DSL) GetTemplates() ([]ITemplate, error) { + return nil, nil +} + +// UploadTemplate upload the template +func (sui *DSL) UploadTemplate(src string, dst string) (ITemplate, error) { + return nil, nil +} + +// GetPublic returns the public +func (sui *DSL) GetPublic() *Public { + return sui.Public +} diff --git a/sui/core/types.go b/sui/core/types.go index 203b8028..e797afc3 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -1,5 +1,10 @@ package core +import ( + "net/url" + "regexp" +) + // DSL the struct for the DSL type DSL struct { ID string `json:"-"` @@ -140,7 +145,7 @@ type Request struct { AssetRoot string `json:"asset_root,omitempty"` Referer string `json:"referer,omitempty"` Payload map[string]interface{} `json:"payload,omitempty"` - Query map[string][]string `json:"query,omitempty"` + Query url.Values `json:"query,omitempty"` Params map[string]string `json:"params,omitempty"` Headers map[string][]string `json:"headers,omitempty"` Body interface{} `json:"body,omitempty"` @@ -244,9 +249,10 @@ type Source struct { // Public is the struct for the static type Public struct { - Host string `json:"host,omitempty"` - Root string `json:"root,omitempty"` - Index string `json:"index,omitempty"` + Host string `json:"host,omitempty"` + Root string `json:"root,omitempty"` + Index string `json:"index,omitempty"` + Matcher string `json:"matcher,omitempty"` } // Storage is the struct for the storage @@ -255,6 +261,14 @@ type Storage struct { Option map[string]interface{} `json:"option,omitempty"` } +// Matcher the struct for the matcher +type Matcher struct { + Regex *regexp.Regexp `json:"regex,omitempty"` + Exact string `json:"exact,omitempty"` + Parent string `json:"-"` + Ref interface{} `json:"-"` +} + // DocumentDefault is the default document var DocumentDefault = []byte(` diff --git a/sui/storages/azure/azure.go b/sui/storages/azure/azure.go index ad5fccf9..a93784ba 100644 --- a/sui/storages/azure/azure.go +++ b/sui/storages/azure/azure.go @@ -1,6 +1,7 @@ package azure import ( + "fmt" "net/url" "github.com/yaoapp/yao/sui/core" @@ -14,7 +15,7 @@ type Azure struct { // new create a new azure sui func new() (*Azure, error) { - return &Azure{}, nil + return nil, fmt.Errorf("Azure does not support yet") } // New create a new azure sui diff --git a/sui/storages/local/local.go b/sui/storages/local/local.go index 033a4bf4..3e7c64f5 100644 --- a/sui/storages/local/local.go +++ b/sui/storages/local/local.go @@ -24,6 +24,7 @@ func New(dsl *sui.DSL) (*Local, error) { root := "/" host := "/" index := "/index" + matcher := "" if dsl.Public != nil { if dsl.Public.Root != "" { root = dsl.Public.Root @@ -36,6 +37,10 @@ func New(dsl *sui.DSL) (*Local, error) { if dsl.Public.Index != "" { index = dsl.Public.Index } + + if dsl.Public.Matcher != "" { + matcher = dsl.Public.Matcher + } } dataFS, err := fs.Get("system") @@ -44,9 +49,10 @@ func New(dsl *sui.DSL) (*Local, error) { } dsl.Public = &sui.Public{ - Host: host, - Root: root, - Index: index, + Host: host, + Root: root, + Index: index, + Matcher: matcher, } return &Local{