diff --git a/internal/apidef/controller.go b/internal/apidef/controller.go deleted file mode 100644 index ed606da..0000000 --- a/internal/apidef/controller.go +++ /dev/null @@ -1,232 +0,0 @@ -package apidef - -import ( - "encoding/json" - "fmt" - "os" - "strings" -) - -// Parse builds a representation of the configuration -// The struct definition checks for most format errors -// -// path The path to the configuration -// -// @return The parsed configuration root controller -// @return The error if occurred -// -func Parse(path string) (*Controller, error) { - - /* (1) Extract data - ---------------------------------------------------------*/ - /* (1) Open file */ - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - /* (2) Init receiver dataset */ - receiver := &Controller{} - - /* (3) Decode json */ - decoder := json.NewDecoder(file) - err = decoder.Decode(receiver) - if err != nil { - return nil, err - } - - /* (4) Format result */ - err = receiver.format("/") - if err != nil { - return nil, err - } - - return receiver, nil - -} - -// Method returns a controller's method if exists -// -// @method The wanted method (case insensitive) -// -// @return<*Method> The requested method -// NIL if not found -// -func (c Controller) Method(method string) *Method { - method = strings.ToUpper(method) - - switch method { - - case "GET": - return c.GET - case "POST": - return c.POST - case "PUT": - return c.PUT - case "DELETE": - return c.DELETE - default: - return nil - - } - -} - -// Browse tries to browse the controller childtree and -// returns the farthest matching child -// -// @path the path to browse -// -// @return The index in 'path' used to find the controller -// @return<*Controller> The farthest match -func (c *Controller) Browse(path []string) (int, *Controller) { - - /* (1) initialise cursors */ - current := c - i := 0 // index in path - - /* (2) Browse while there is uri parts */ - for i < len(path) { - - // 1. Try to get child for this name - child, exists := current.Children[path[i]] - - // 2. Stop if no matching child - if !exists { - break - } - - // 3. Increment cursors - current = child - i++ - - } - - /* (3) Return matches */ - return i, current - -} - -// format checks for format errors and missing required fields -// it also sets default values to optional fields -func (c *Controller) format(controllerName string) error { - - /* (1) Check each method - ---------------------------------------------------------*/ - methods := []struct { - Name string - Ptr *Method - }{ - {"GET", c.GET}, - {"POST", c.POST}, - {"PUT", c.PUT}, - {"DELETE", c.DELETE}, - } - - for _, method := range methods { - - /* (1) ignore non-defined method */ - if method.Ptr == nil { - continue - } - - /* (2) Fail on missing description */ - if len(method.Ptr.Description) < 1 { - return fmt.Errorf("Missing %s.%s description", controllerName, method.Name) - } - - /* (3) stop if no parameter */ - if method.Ptr.Parameters == nil || len(method.Ptr.Parameters) < 1 { - method.Ptr.Parameters = make(map[string]*Parameter, 0) - continue - } - - /* check parameters */ - for pName, pData := range method.Ptr.Parameters { - - // check name - if strings.Trim(pName, "_") != pName { - return fmt.Errorf("Invalid name '%s' must not begin/end with '_'", pName) - } - - if len(pData.Rename) < 1 { - pData.Rename = pName - } - - /* (5) Check for name/rename conflict */ - for paramName, param := range method.Ptr.Parameters { - - // ignore self - if pName == paramName { - continue - } - - // 1. Same rename field - if pData.Rename == param.Rename { - return fmt.Errorf("Rename conflict for %s.%s parameter '%s'", controllerName, method.Name, pData.Rename) - } - - // 2. Not-renamed field matches a renamed field - if pName == param.Rename { - return fmt.Errorf("Name conflict for %s.%s parameter '%s'", controllerName, method.Name, pName) - } - - // 3. Renamed field matches name - if pData.Rename == paramName { - return fmt.Errorf("Name conflict for %s.%s parameter '%s'", controllerName, method.Name, pName) - } - - } - - /* (6) Manage invalid type */ - if len(pData.Type) < 1 { - return fmt.Errorf("Invalid type for %s.%s parameter '%s'", controllerName, method.Name, pName) - } - - /* (7) Fail on missing description */ - if len(pData.Description) < 1 { - return fmt.Errorf("Missing description for %s.%s parameter '%s'", controllerName, method.Name, pName) - } - - /* (8) Fail on missing type */ - if len(pData.Type) < 1 { - return fmt.Errorf("Missing type for %s.%s parameter '%s'", controllerName, method.Name, pName) - } - - /* (9) Set optional + type */ - if pData.Type[0] == '?' { - pData.Optional = true - pData.Type = pData.Type[1:] - } - - } - - } - - /* (2) Check child controllers - ---------------------------------------------------------*/ - /* (1) Stop if no child */ - if c.Children == nil || len(c.Children) < 1 { - return nil - } - - /* (2) For each controller */ - for ctlName, ctl := range c.Children { - - /* (3) Invalid name */ - if strings.ContainsAny(ctlName, "/-") { - return fmt.Errorf("Controller '%s' must not contain any slash '/' nor '-' symbols", ctlName) - } - - /* (4) Check recursively */ - err := ctl.format(ctlName) - if err != nil { - return err - } - - } - - return nil - -} diff --git a/internal/apidef/method.go b/internal/apidef/method.go deleted file mode 100644 index e400d75..0000000 --- a/internal/apidef/method.go +++ /dev/null @@ -1,46 +0,0 @@ -package apidef - -import ( - "git.xdrm.io/go/aicra/middleware" -) - -// CheckScope returns whether a given scope matches the -// method configuration -// -// format is: [ [a,b], [c], [d,e] ] -// > level 1 is OR -// > level 2 is AND -func (m *Method) CheckScope(scope middleware.Scope) bool { - - for _, OR := range m.Permission { - - granted := true - - for _, AND := range OR { - - if !scopeHasPermission(AND, scope) { - granted = false - break - } - - } - - // if one is valid -> grant - if granted { - return true - } - - } - - return false -} - -// scopeHasPermission returns whether @perm is present in a given @scope -func scopeHasPermission(perm string, scope []string) bool { - for _, s := range scope { - if perm == s { - return true - } - } - return false -} diff --git a/internal/apidef/types.go b/internal/apidef/types.go deleted file mode 100644 index 95020ff..0000000 --- a/internal/apidef/types.go +++ /dev/null @@ -1,30 +0,0 @@ -package apidef - -/* (1) Configuration ----------------------------------------------------------*/ -// Parameter represents a parameter definition (from api.json) -type Parameter struct { - Description string `json:"info"` - Type string `json:"type"` - Rename string `json:"name,omitempty"` - Optional bool - Default *interface{} `json:"default"` -} - -// Method represents a method definition (from api.json) -type Method struct { - Description string `json:"info"` - Permission [][]string `json:"scope"` - Parameters map[string]*Parameter `json:"in"` - Download *bool `json:"download"` -} - -// Controller represents a controller definition (from api.json) -type Controller struct { - GET *Method `json:"GET"` - POST *Method `json:"POST"` - PUT *Method `json:"PUT"` - DELETE *Method `json:"DELETE"` - - Children map[string]*Controller `json:"/"` -} diff --git a/internal/config/builder.go b/internal/config/builder.go deleted file mode 100644 index 4a76b57..0000000 --- a/internal/config/builder.go +++ /dev/null @@ -1,57 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - - "git.xdrm.io/go/aicra/driver" -) - -// InferFromFolder fills the 'Map' by browsing recursively the -// 'Folder' field -func (b *builder) InferFromFolder(_root string, _driver driver.Driver) { - - // init map - if b.Map == nil { - b.Map = make(map[string]string) - } - - // 1. ignore if no Folder - if len(b.Folder) < 1 { - return - } - - // 2. If relative Folder, join to root - rootpath := filepath.Join(_root, b.Folder) - - // 3. Walk - filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error { - - // ignore dir - if err != nil || info.IsDir() { - return nil - } - - // format path - path, err = filepath.Rel(rootpath, path) - if err != nil { - return nil - } - // extract universal path from the driver - upath := _driver.Path(_root, b.Folder, path) - - // format name - name := upath - if name == "/" { - name = "" - } - name = fmt.Sprintf("%s", name) - - // add to map - b.Map[name] = upath - - return nil - }) - -} diff --git a/internal/config/defaults.go b/internal/config/defaults.go deleted file mode 100644 index ae40e07..0000000 --- a/internal/config/defaults.go +++ /dev/null @@ -1,21 +0,0 @@ -package config - -// Default contains the default values when omitted in json -var Default = Schema{ - Root: ".", - Host: "0.0.0.0", - Port: 80, - DriverName: "", - Types: &builder{ - Default: true, - Folder: "type", - }, - Controllers: &builder{ - Default: false, - Folder: "controller", - }, - Middlewares: &builder{ - Default: false, - Folder: "middleware", - }, -} diff --git a/internal/config/method.go b/internal/config/method.go new file mode 100644 index 0000000..4f3ce7a --- /dev/null +++ b/internal/config/method.go @@ -0,0 +1,118 @@ +package config + +import ( + "fmt" + "strings" + + "git.xdrm.io/go/aicra/middleware" +) + +// checkAndFormat checks for errors and missing fields and sets default values for optional fields. +func (methodDef *Method) checkAndFormat(servicePath string, httpMethod string) error { + + // 1. fail on missing description + if len(methodDef.Description) < 1 { + return fmt.Errorf("missing %s.%s description", servicePath, httpMethod) + } + + // 2. stop if no parameter + if methodDef.Parameters == nil || len(methodDef.Parameters) < 1 { + methodDef.Parameters = make(map[string]*Parameter, 0) + return nil + } + + // 3. for each parameter + for pName, pData := range methodDef.Parameters { + + // check name + if strings.Trim(pName, "_") != pName { + return fmt.Errorf("invalid name '%s' must not begin/end with '_'", pName) + } + + if len(pData.Rename) < 1 { + pData.Rename = pName + } + + // 5. Check for name/rename conflict + for paramName, param := range methodDef.Parameters { + + // ignore self + if pName == paramName { + continue + } + + // 1. Same rename field + if pData.Rename == param.Rename { + return fmt.Errorf("rename conflict for %s.%s parameter '%s'", servicePath, httpMethod, pData.Rename) + } + + // 2. Not-renamed field matches a renamed field + if pName == param.Rename { + return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName) + } + + // 3. Renamed field matches name + if pData.Rename == paramName { + return fmt.Errorf("name conflict for %s.%s parameter '%s'", servicePath, httpMethod, pName) + } + + } + + // 6. Manage invalid type + if len(pData.Type) < 1 { + return fmt.Errorf("invalid type for %s.%s parameter '%s'", servicePath, httpMethod, pName) + } + + // 7. Fail on missing description + if len(pData.Description) < 1 { + return fmt.Errorf("missing description for %s.%s parameter '%s'", servicePath, httpMethod, pName) + } + + // 8. Fail on missing type + if len(pData.Type) < 1 { + return fmt.Errorf("missing type for %s.%s parameter '%s'", servicePath, httpMethod, pName) + } + + // 9. Set optional + type + if pData.Type[0] == '?' { + pData.Optional = true + pData.Type = pData.Type[1:] + } + + } + + return nil +} + +// CheckScope returns whether a given scope matches the method configuration. +// The scope format is: `[ [a,b], [c], [d,e] ]` where the first level is a bitwise `OR` and the second a bitwise `AND` +func (methodDef *Method) CheckScope(scope middleware.Scope) bool { + + for _, OR := range methodDef.Permission { + granted := true + + for _, AND := range OR { + if !scopeHasPermission(AND, scope) { + granted = false + break + } + } + + // if one is valid -> grant + if granted { + return true + } + } + + return false +} + +// scopeHasPermission returns whether the permission fulfills a given scope +func scopeHasPermission(permission string, scope []string) bool { + for _, s := range scope { + if permission == s { + return true + } + } + return false +} diff --git a/internal/config/parser.go b/internal/config/parser.go deleted file mode 100644 index 12a9acd..0000000 --- a/internal/config/parser.go +++ /dev/null @@ -1,108 +0,0 @@ -package config - -import ( - "encoding/json" - "errors" - "git.xdrm.io/go/aicra/driver" - "os" - "path/filepath" - "strings" -) - -// Parse extracts a Meta from a json config file (aicra.json) -func Parse(_path string) (*Schema, error) { - - /* 1. ppen file */ - file, err := os.Open(_path) - if err != nil { - return nil, errors.New("cannot open file") - } - defer file.Close() - - /* 2. Init receiver dataset */ - receiver := &Schema{} - - /* 3. Decode json */ - decoder := json.NewDecoder(file) - err = decoder.Decode(receiver) - if err != nil { - return nil, err - } - - /* 4. Error on invalid driver */ - receiver.DriverName = strings.ToLower(receiver.DriverName) - switch receiver.DriverName { - case "generic": - receiver.Driver = &driver.Generic{} - case "plugin": - receiver.Driver = &driver.Plugin{} - - default: - return nil, errors.New("invalid driver; choose from 'generic', 'plugin'") - } - - /* 5. Fail on absolute folders */ - if len(receiver.Types.Folder) > 0 && filepath.IsAbs(receiver.Types.Folder) { - return nil, errors.New("types folder must be relative to root") - } - if len(receiver.Controllers.Folder) > 0 && filepath.IsAbs(receiver.Controllers.Folder) { - return nil, errors.New("controllers folder must be relative to root") - } - if len(receiver.Middlewares.Folder) > 0 && filepath.IsAbs(receiver.Middlewares.Folder) { - return nil, errors.New("middlewares folder must be relative to root") - } - - /* 7. Format result (default values, etc) */ - receiver.setDefaults() - - return receiver, nil - -} - -// setDefaults sets defaults values and checks for missing data -func (m *Schema) setDefaults() { - - // 1. extract absolute root folder - absroot, err := filepath.Abs(m.Root) - if err == nil { - m.Root = absroot - } - - // 2. host - if len(m.Host) < 1 { - m.Host = Default.Host - } - - // 3. port - if m.Port == 0 { - m.Port = Default.Port - } - - // 4. Use default builders if not set - if m.Types == nil { - m.Types = Default.Types - } - if m.Controllers == nil { - m.Controllers = Default.Controllers - } - if m.Middlewares == nil { - m.Middlewares = Default.Middlewares - } - - // 5. Use default folders if not set - if m.Types.Folder == "" { - m.Types.Folder = Default.Types.Folder - } - if m.Controllers.Folder == "" { - m.Controllers.Folder = Default.Controllers.Folder - } - if m.Middlewares.Folder == "" { - m.Middlewares.Folder = Default.Middlewares.Folder - } - - // 6. Infer Maps from Folders - m.Types.InferFromFolder(m.Root, m.Driver) - m.Controllers.InferFromFolder(m.Root, m.Driver) - m.Middlewares.InferFromFolder(m.Root, m.Driver) - -} diff --git a/internal/config/service.go b/internal/config/service.go new file mode 100644 index 0000000..78ff407 --- /dev/null +++ b/internal/config/service.go @@ -0,0 +1,107 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// Parse builds a service from a json reader and checks for most format errors. +func Parse(r io.Reader) (*Service, error) { + + receiver := &Service{} + + err := json.NewDecoder(r).Decode(receiver) + if err != nil { + return nil, err + } + + err = receiver.checkAndFormat("/") + if err != nil { + return nil, err + } + + return receiver, nil + +} + +// Method returns the actual method from the http method. +func (svc *Service) Method(httpMethod string) *Method { + httpMethod = strings.ToUpper(httpMethod) + + switch httpMethod { + case http.MethodGet: + return svc.GET + case http.MethodPost: + return svc.POST + case http.MethodPut: + return svc.PUT + case http.MethodDelete: + return svc.DELETE + } + + return nil +} + +// Browse the service childtree and returns the farthest matching child. The `path` is a formatted URL split by '/' +func (svc *Service) Browse(path []string) (*Service, int) { + currentService := svc + var depth int + + // for each URI depth + for depth = 0; depth < len(path); depth++ { + currentPath := path[depth] + + child, exists := currentService.Children[currentPath] + if !exists { + break + } + currentService = child + + } + + return currentService, depth +} + +// checkAndFormat checks for errors and missing fields and sets default values for optional fields. +func (svc *Service) checkAndFormat(servicePath string) error { + + // 1. check and format every method + for _, httpMethod := range availableHTTPMethods { + methodDef := svc.Method(httpMethod) + if methodDef == nil { + continue + } + + err := methodDef.checkAndFormat(servicePath, httpMethod) + if err != nil { + return err + } + } + + // 1. stop if no child */ + if svc.Children == nil || len(svc.Children) < 1 { + return nil + } + + // 2. for each controller */ + for childService, ctl := range svc.Children { + + // 3. invalid name */ + if strings.ContainsAny(childService, "/-") { + return fmt.Errorf("Controller '%s' must not contain any slash '/' nor '-' symbols", childService) + } + + // 4. check recursively */ + err := ctl.checkAndFormat(childService) + if err != nil { + return err + } + + } + + return nil + +} diff --git a/internal/config/types.go b/internal/config/types.go index 19bae0b..d71b1c9 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -1,50 +1,32 @@ package config -import ( - "git.xdrm.io/go/aicra/driver" -) +import "net/http" -type builder struct { - // Default tells whether or not to ignore the built-in components - Default bool `json:"default,ommitempty"` +var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} - // Folder is used to infer the 'Map' object - Folder string `json:"folder,ommitempty"` +// Service represents a service definition (from api.json) +type Service struct { + GET *Method `json:"GET"` + POST *Method `json:"POST"` + PUT *Method `json:"PUT"` + DELETE *Method `json:"DELETE"` - // Map defines the association path=>file - Map map[string]string + Children map[string]*Service `json:"/"` } -// Schema represents an AICRA configuration (not the API, the server, drivers, etc) -type Schema struct { - // Root is root of the project structure default is "." (current directory) - Root string `json:"root,ommitempty"` - - // Host is the hostname to listen to (default is 0.0.0.0) - Host string `json:"host,ommitempty"` - // Port is the port to listen to (default is 80) - Port uint16 `json:"port,ommitempty"` - - // DriverName is the driver used to load the controllers and middlewares - DriverName string `json:"driver"` - Driver driver.Driver - - // Types defines : - // - the type folder - // - whether to load the built-in types - // - // types are omitted if not set (no default) - Types *builder `json:"types,ommitempty"` - - // Controllers defines : - // - the controller folder - // - // (default is .build/controller) - Controllers *builder `json:"controllers,ommitempty"` - - // Middlewares defines : - // - the middleware folder - // - // (default is .build/middleware) - Middlewares *builder `json:"middlewares,ommitempty"` +// Parameter represents a parameter definition (from api.json) +type Parameter struct { + Description string `json:"info"` + Type string `json:"type"` + Rename string `json:"name,omitempty"` + Optional bool + Default *interface{} `json:"default"` +} + +// Method represents a method definition (from api.json) +type Method struct { + Description string `json:"info"` + Permission [][]string `json:"scope"` + Parameters map[string]*Parameter `json:"in"` + Download *bool `json:"download"` }