ref 0: internal.apidef becomes internal.config + refactoe

This commit is contained in:
Adrien Marquès 2019-05-01 10:29:02 +02:00
parent a63e227538
commit 8109f57d15
9 changed files with 249 additions and 536 deletions

View File

@ -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<string> The path to the configuration
//
// @return<controller> The parsed configuration root controller
// @return<err> 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<string> 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<int> 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
}

View File

@ -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
}

View File

@ -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:"/"`
}

View File

@ -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
})
}

View File

@ -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",
},
}

118
internal/config/method.go Normal file
View File

@ -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
}

View File

@ -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)
}

107
internal/config/service.go Normal file
View File

@ -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
}

View File

@ -1,50 +1,32 @@
package config package config
import ( import "net/http"
"git.xdrm.io/go/aicra/driver"
)
type builder struct { var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}
// Default tells whether or not to ignore the built-in components
Default bool `json:"default,ommitempty"`
// Folder is used to infer the 'Map' object // Service represents a service definition (from api.json)
Folder string `json:"folder,ommitempty"` 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 Children map[string]*Service `json:"/"`
Map map[string]string
} }
// Schema represents an AICRA configuration (not the API, the server, drivers, etc) // Parameter represents a parameter definition (from api.json)
type Schema struct { type Parameter struct {
// Root is root of the project structure default is "." (current directory) Description string `json:"info"`
Root string `json:"root,ommitempty"` Type string `json:"type"`
Rename string `json:"name,omitempty"`
// Host is the hostname to listen to (default is 0.0.0.0) Optional bool
Host string `json:"host,ommitempty"` Default *interface{} `json:"default"`
// Port is the port to listen to (default is 80) }
Port uint16 `json:"port,ommitempty"`
// Method represents a method definition (from api.json)
// DriverName is the driver used to load the controllers and middlewares type Method struct {
DriverName string `json:"driver"` Description string `json:"info"`
Driver driver.Driver Permission [][]string `json:"scope"`
Parameters map[string]*Parameter `json:"in"`
// Types defines : Download *bool `json:"download"`
// - 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"`
} }