diff --git a/README.md b/README.md index dfb8937..242be55 100644 --- a/README.md +++ b/README.md @@ -7,40 +7,34 @@ [![Build Status](https://drone.xdrm.io/api/badges/go/aicra/status.svg)](https://drone.xdrm.io/go/aicra) -**Aicra** is a *configuration-driven* **web framework** written in Go that allows you to create a fully featured REST API. +Aicra is a *configuration-driven* REST API engine written in Go. -The whole management is done for you from a configuration file describing your API, you're left with implementing : +Most of the management is done for you using a configuration file describing your API. you're left with implementing : - handlers - optionnally middle-wares (_e.g. authentication, csrf_) - and optionnally your custom type checkers to check input parameters +> A example project is available [here](https://git.xdrm.io/go/articles-api) -The aicra server fulfills the `net/http` [Server interface](https://golang.org/pkg/net/http/#Server). - - - -> A example project is available [here](https://git.xdrm.io/go/tiny-url-ex) - - -### Table of contents +## Table of contents - [I/ Installation](#i-installation) -- [II/ Development](#ii-development) - * [1) Main executable](#1-main-executable) - * [2) API Configuration](#2-api-configuration) - - [Definition](#definition) - + [Input Arguments](#input-arguments) - - [1. Input types](#1-input-types) - - [2. Global Format](#2-global-format) +- [II/ Usage](#ii-usage) + * [1) Build a server](#1-build-a-server) + * [2) API Configuration](#2-api-configuration) + - [Definition](#definition) + + [Input Arguments](#input-arguments) + - [1. Input types](#1-input-types) + - [2. Global Format](#2-global-format) - [III/ Change Log](#iii-change-log) -### I/ Installation +## I/ Installation -You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.10**. +You need a recent machine with `go` [installed](https://golang.org/doc/install). This package has not been tested under the version **1.14**. ```bash @@ -50,95 +44,112 @@ go get -u git.xdrm.io/go/aicra/cmd/aicra The library should now be available as `git.xdrm.io/go/aicra` in your imports. -### II/ Development +## II/ Usage -#### 1) Main executable +### 1) Build a server -Your main executable will declare and run the aicra server, it might look quite like the code below. +Here is some sample code that builds and sets up an aicra server using your api configuration file. ```go package main import ( - "log" - "net/http" + "log" + "net/http" + "os" - "git.xdrm.io/go/aicra" - "git.xdrm.io/go/aicra/datatype" - "git.xdrm.io/go/aicra/datatype/builtin" + "git.xdrm.io/go/aicra" + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/datatype/builtin" ) func main() { - // 1. select your datatypes (builtin, custom) - var dtypes []datatype.T - dtypes = append(dtypes, builtin.AnyDataType{}) - dtypes = append(dtypes, builtin.BoolDataType{}) - dtypes = append(dtypes, builtin.UintDataType{}) - dtypes = append(dtypes, builtin.StringDataType{}) + builder := &aicra.Builder{} - // 2. create the server from the configuration file - server, err := aicra.New("path/to/your/api/definition.json", dtypes...) + // add datatypes your api uses + builder.AddType(builtin.BoolDataType{}) + builder.AddType(builtin.UintDataType{}) + builder.AddType(builtin.StringDataType{}) + + config, err := os.Open("./api.json") if err != nil { - log.Fatalf("cannot built aicra server: %s\n", err) + log.Fatalf("cannot open config: %s", err) } - // 3. bind your implementations - server.HandleFunc(http.MethodGet, "/path", func(req api.Request, res *api.Response){ - // ... process stuff ... - res.SetError(api.ErrorSuccess()); - }) - - // 4. extract to http server - httpServer, err := server.ToHTTPServer() + // pass your configuration + err = builder.Setup(config) + config.Close() if err != nil { - log.Fatalf("cannot get to http server: %s", err) + log.Fatalf("invalid config: %s", err) } - // 4. launch server - log.Fatal( http.ListenAndServe("localhost:8080", server) ) + // bind your handlers + builder.Bind(http.MethodGet, "/user/{id}", getUserById) + builder.Bind(http.MethodGet, "/user/{id}/username", getUsernameByID) + + // build the server and start listening + server, err := builder.Build() + if err != nil { + log.Fatalf("cannot build server: %s", err) + } + http.ListenAndServe("localhost:8080", server) } ``` +Here is an example handler +```go +type req struct{ + Param1 int + Param3 *string // optional are pointers +} +type res struct{ + Output1 string + Output2 bool +} -#### 2) API Configuration +func myHandler(r req) (*res, api.Error) { + err := doSomething() + if err != nil { + return nil, api.ErrorFailure + } + return &res{}, api.ErrorSuccess +} +``` -The whole project behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [template](https://git.xdrm.io/go/tiny-url-ex/src/master/api.json). This file defines : + +### 2) API Configuration + +The whole api behavior is described inside a json file (_e.g. usually api.json_). For a better understanding of the format, take a look at this working [template](https://git.xdrm.io/go/articles-api/src/master/api.json). This file defines : - routes and their methods - every input for each method (called *argument*) - every output for each method - scope permissions (list of permissions needed by clients) - input policy : - - type of argument (_i.e. for data types_) + - type of argument (_c.f. data types_) - required/optional - variable renaming +#### Format - -###### Definition - -The root of the json file must be an array containing your requests definitions. - -For each, you will have to create fields described in the table above. +The root of the json file must be an array containing your requests definitions. For each, you will have to create fields described in the table above. | field path | description | example | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | `info` | A short human-readable description of what the method does | `create a new user` | | `scope` | A 2-dimensional array of permissions. The first dimension can be translated to a **or** operator, the second dimension as a **and**. It allows you to combine permissions in complex ways. | `[["A", "B"], ["C", "D"]]` can be translated to : this method needs users to have permissions (A **and** B) **or** (C **and** D) | -| `in` | The list of arguments that the clients will have to provide. See [here](#input-arguments) for details. | | -| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but is only use for readability purpose and documentation. | | +| `in` | The list of arguments that the clients will have to provide. [Read more](#input-arguments). | | +| `out` | The list of output data that will be returned by your controllers. It has the same syntax as the `in` field but optional parameters are not allowed | -##### Input Arguments - -###### 1. Input types +### Input Arguments Input arguments defines what data from the HTTP request the method needs. Aicra is able to extract 3 types of data : -- **URI** - Curly Braces enclosed strings inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part. +- **URI** - data from inside the request path. For instance, if your controller is bound to the `/user/{id}` URI, you can set the input argument `{id}` matching this uri part. - **Query** - data formatted at the end of the URL following the standard [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax. - **URL encoded** - data send inside the body of the request but following the [HTTP Query](https://tools.ietf.org/html/rfc3986#section-3.4) syntax. - **Multipart** - data send inside the body of the request with a dedicated [format](https://tools.ietf.org/html/rfc2388#section-3). This format is not very lightweight but allows you to receive data as well as files. @@ -146,7 +157,7 @@ Input arguments defines what data from the HTTP request the method needs. Aicra -###### 2. Global Format +#### Format The `in` field in each method contains as list of arguments where the key is the argument name, and the value defines how to manage the variable. @@ -159,10 +170,6 @@ The `in` field in each method contains as list of arguments where the key is the In this example we want 3 arguments : -- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler. -- the 2^nd^ one is send in the query (_e.g. [http://host/uri?get-var=value](http://host/uri?get-var=value)_). It must be a valid `string` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `title`. -- the 3^rd^ can be send with a **JSON** body, in **multipart** or **URL encoded** it makes no difference and only give clients a choice over the technology to use. If not renamed, the variable will be given to the handler with the name `content`. - ```json [ { @@ -184,32 +191,6 @@ In this example we want 3 arguments : ] ``` - - -### III/ Change Log - -- [x] human-readable json configuration -- [x] nested routes (*i.e. `/user/:id:` and `/user/post/:id:`*) -- [x] nested URL arguments (*i.e. `/user/:id:` and `/user/:id:/post/​:id:​`*) -- [x] useful http methods: GET, POST, PUT, DELETE -- [x] manage URL, query and body arguments: - - [x] multipart/form-data (variables and file uploads) - - [x] application/x-www-form-urlencoded - - [x] application/json -- [x] required vs. optional parameters with a default value -- [x] parameter renaming -- [x] generic type check (*i.e. implement custom types alongside built-in ones*) -- [ ] built-in types - - [x] `any` - wildcard matching all values - - [x] `int` - see go types - - [x] `uint` - see go types - - [x] `float` - see go types - - [x] `string` - any text - - [x] `string(min, max)` - any string with a length between `min` and `max` - - [ ] `[a]` - array containing **only** elements matching `a` type - - [ ] `[a:b]` - map containing **only** keys of type `a` and values of type `b` (*a or b can be ommited*) -- [x] generic controllers implementation (shared objects) -- [x] response interface -- [x] log bound resources when building the aicra server -- [x] fail on check for unimplemented resources at server boot. -- [x] fail on check for unavailable types in api.json at server boot. +- the 1^st^ one is send at the end of the URI and is a number compliant with the `int` type checker. It is renamed `article_id`, this new name will be sent to the handler. +- the 2^nd^ one is send in the query (_e.g. [http://host/uri?get-var=value](http://host/uri?get-var=value)_). It must be a valid `string` or not given at all (the `?` at the beginning of the type tells that the argument is **optional**) ; it will be named `title`. +- the 3^rd^ can be send with a **JSON** body, in **multipart** or **URL encoded** it makes no difference and only give clients a choice over the technology to use. If not renamed, the variable will be given to the handler with the name `content`. diff --git a/api/error.defaults.go b/api/error.defaults.go index 4907ba5..583126f 100644 --- a/api/error.defaults.go +++ b/api/error.defaults.go @@ -1,5 +1,7 @@ package api +import "net/http" + var ( // ErrorUnknown represents any error which cause is unknown. // It might also be used for debug purposes as this error @@ -19,20 +21,17 @@ var ( // unique fields already exists ErrorAlreadyExists Error = 3 - // ErrorConfig has to be set when there is a configuration error - ErrorConfig Error = 4 - // ErrorCreation has to be set when there is a creation/insert error - ErrorCreation Error = 5 + ErrorCreation Error = 4 // ErrorModification has to be set when there is an update/modification error - ErrorModification Error = 6 + ErrorModification Error = 5 // ErrorDeletion has to be set when there is a deletion/removal error - ErrorDeletion Error = 7 + ErrorDeletion Error = 6 // ErrorTransaction has to be set when there is a transactional error - ErrorTransaction Error = 8 + ErrorTransaction Error = 7 // ErrorUpload has to be set when a file upload failed ErrorUpload Error = 100 @@ -90,7 +89,6 @@ var errorReasons = map[Error]string{ ErrorFailure: "it failed", ErrorNoMatchFound: "resource not found", ErrorAlreadyExists: "already exists", - ErrorConfig: "configuration error", ErrorCreation: "create error", ErrorModification: "update error", ErrorDeletion: "delete error", @@ -108,3 +106,26 @@ var errorReasons = map[Error]string{ ErrorInvalidParam: "invalid parameter", ErrorInvalidDefaultParam: "invalid default param", } +var errorStatus = map[Error]int{ + ErrorUnknown: http.StatusOK, + ErrorSuccess: http.StatusOK, + ErrorFailure: http.StatusInternalServerError, + ErrorNoMatchFound: http.StatusOK, + ErrorAlreadyExists: http.StatusOK, + ErrorCreation: http.StatusOK, + ErrorModification: http.StatusOK, + ErrorDeletion: http.StatusOK, + ErrorTransaction: http.StatusOK, + ErrorUpload: http.StatusInternalServerError, + ErrorDownload: http.StatusInternalServerError, + MissingDownloadHeaders: http.StatusBadRequest, + ErrorMissingDownloadBody: http.StatusBadRequest, + ErrorUnknownService: http.StatusServiceUnavailable, + ErrorUncallableService: http.StatusServiceUnavailable, + ErrorNotImplemented: http.StatusNotImplemented, + ErrorPermission: http.StatusUnauthorized, + ErrorToken: http.StatusForbidden, + ErrorMissingParam: http.StatusBadRequest, + ErrorInvalidParam: http.StatusBadRequest, + ErrorInvalidDefaultParam: http.StatusBadRequest, +} diff --git a/api/error.go b/api/error.go index ef6c9fd..52f97f7 100644 --- a/api/error.go +++ b/api/error.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "net/http" ) // Error represents an http response error following the api format. @@ -10,17 +11,23 @@ import ( // directly into the response as JSON alongside response output fields. type Error int -// Error implements the error interface func (e Error) Error() string { - // use unknown error if no reason reason, ok := errorReasons[e] if !ok { return ErrorUnknown.Error() } - return fmt.Sprintf("[%d] %s", e, reason) } +// Status returns the associated HTTP status code +func (e Error) Status() int { + status, ok := errorStatus[e] + if !ok { + return http.StatusOK + } + return status +} + // MarshalJSON implements encoding/json.Marshaler interface func (e Error) MarshalJSON() ([]byte, error) { // use unknown error if no reason diff --git a/api/request.go b/api/request.go deleted file mode 100644 index 8259836..0000000 --- a/api/request.go +++ /dev/null @@ -1,54 +0,0 @@ -package api - -import ( - "net/http" - "strings" -) - -// Request represents an API request i.e. HTTP -type Request struct { - // corresponds to the list of uri components - // featured in the request URI - URI []string - - // Scope from the configuration file of the current service - Scope [][]string - - // original HTTP request - Request *http.Request - - // input parameters - Param RequestParam -} - -// NewRequest builds an interface request from a http.Request -func NewRequest(req *http.Request) *Request { - uri := normaliseURI(req.URL.Path) - uriparts := strings.Split(uri, "/") - - return &Request{ - URI: uriparts, - Scope: nil, - Request: req, - Param: make(RequestParam), - } -} - -// normaliseURI removes the trailing '/' to always -// have the same Uri format for later processing -func normaliseURI(uri string) string { - - if len(uri) < 1 { - return uri - } - - if uri[0] == '/' { - uri = uri[1:] - } - - if len(uri) > 1 && uri[len(uri)-1] == '/' { - uri = uri[0 : len(uri)-1] - } - - return uri -} diff --git a/api/request.param.go b/api/request.param.go deleted file mode 100644 index f0891ee..0000000 --- a/api/request.param.go +++ /dev/null @@ -1,162 +0,0 @@ -package api - -import ( - "fmt" -) - -// cerr allows you to create constant "const" error with type boxing. -type cerr string - -// Error implements the error builtin interface. -func (err cerr) Error() string { - return string(err) -} - -// ErrReqParamNotFound is thrown when a request parameter is not found -const ErrReqParamNotFound = cerr("request parameter not found") - -// ErrReqParamNotType is thrown when a request parameter is not asked with the right type -const ErrReqParamNotType = cerr("request parameter does not fulfills type") - -// RequestParam defines input parameters of an api request -type RequestParam map[string]interface{} - -// Get returns the raw value (not typed) and an error if not found -func (rp RequestParam) Get(key string) (interface{}, error) { - rawValue, found := rp[key] - if !found { - return "", ErrReqParamNotFound - } - return rawValue, nil -} - -// GetString returns a string and an error if not found or invalid type -func (rp RequestParam) GetString(key string) (string, error) { - rawValue, err := rp.Get(key) - if err != nil { - return "", err - } - - switch cast := rawValue.(type) { - case fmt.Stringer: - return cast.String(), nil - case []byte: - return string(cast), nil - case string: - return cast, nil - default: - return "", ErrReqParamNotType - } -} - -// GetFloat returns a float64 and an error if not found or invalid type -func (rp RequestParam) GetFloat(key string) (float64, error) { - rawValue, err := rp.Get(key) - if err != nil { - return 0, err - } - - switch cast := rawValue.(type) { - case float32: - return float64(cast), nil - case float64: - return cast, nil - case int, int8, int16, int32, int64: - intVal, ok := cast.(int) - if !ok || intVal != int(float64(intVal)) { - return 0, ErrReqParamNotType - } - return float64(intVal), nil - case uint, uint8, uint16, uint32, uint64: - uintVal, ok := cast.(uint) - if !ok || uintVal != uint(float64(uintVal)) { - return 0, ErrReqParamNotType - } - return float64(uintVal), nil - default: - return 0, ErrReqParamNotType - } -} - -// GetInt returns an int and an error if not found or invalid type -func (rp RequestParam) GetInt(key string) (int, error) { - rawValue, err := rp.Get(key) - if err != nil { - return 0, err - } - - switch cast := rawValue.(type) { - case float32, float64: - floatVal, ok := cast.(float64) - if !ok || floatVal < 0 || floatVal != float64(int(floatVal)) { - return 0, ErrReqParamNotType - } - return int(floatVal), nil - case int, int8, int16, int32, int64: - intVal, ok := cast.(int) - if !ok || intVal != int(int(intVal)) { - return 0, ErrReqParamNotType - } - return int(intVal), nil - default: - return 0, ErrReqParamNotType - } -} - -// GetUint returns an uint and an error if not found or invalid type -func (rp RequestParam) GetUint(key string) (uint, error) { - rawValue, err := rp.Get(key) - if err != nil { - return 0, err - } - - switch cast := rawValue.(type) { - case float32, float64: - floatVal, ok := cast.(float64) - if !ok || floatVal < 0 || floatVal != float64(uint(floatVal)) { - return 0, ErrReqParamNotType - } - return uint(floatVal), nil - case int, int8, int16, int32, int64: - intVal, ok := cast.(int) - if !ok || intVal != int(uint(intVal)) { - return 0, ErrReqParamNotType - } - return uint(intVal), nil - case uint, uint8, uint16, uint32, uint64: - uintVal, ok := cast.(uint) - if !ok { - return 0, ErrReqParamNotType - } - return uintVal, nil - default: - return 0, ErrReqParamNotType - } -} - -// GetStrings returns an []slice and an error if not found or invalid type -func (rp RequestParam) GetStrings(key string) ([]string, error) { - rawValue, err := rp.Get(key) - if err != nil { - return nil, err - } - - switch cast := rawValue.(type) { - case []fmt.Stringer: - strings := make([]string, len(cast)) - for i, stringer := range cast { - strings[i] = stringer.String() - } - return strings, nil - case [][]byte: - strings := make([]string, len(cast)) - for i, bytes := range cast { - strings[i] = string(bytes) - } - return strings, nil - case []string: - return cast, nil - default: - return nil, ErrReqParamNotType - } -} diff --git a/api/response.go b/api/response.go index 242be3d..a7ce08c 100644 --- a/api/response.go +++ b/api/response.go @@ -26,13 +26,12 @@ func EmptyResponse() *Response { } } -// WithError sets the error from a base error with error arguments. +// WithError sets the error func (res *Response) WithError(err Error) *Response { res.err = err return res } -// Error implements the error interface and dispatches to internal error. func (res *Response) Error() string { return res.err.Error() } @@ -42,36 +41,23 @@ func (res *Response) SetData(name string, value interface{}) { res.Data[name] = value } -// GetData gets a response field -func (res *Response) GetData(name string) interface{} { - value, _ := res.Data[name] - - return value -} - // MarshalJSON implements the 'json.Marshaler' interface and is used // to generate the JSON representation of the response func (res *Response) MarshalJSON() ([]byte, error) { fmt := make(map[string]interface{}) - for k, v := range res.Data { fmt[k] = v } - fmt["error"] = res.err - return json.Marshal(fmt) } -// ServeHTTP implements http.Handler and writes the API response. func (res *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) error { - w.WriteHeader(res.Status) - + w.WriteHeader(res.err.Status()) encoded, err := json.Marshal(res) if err != nil { return err } w.Write(encoded) - return nil } diff --git a/builder.go b/builder.go index 2d14067..4e38818 100644 --- a/builder.go +++ b/builder.go @@ -29,7 +29,7 @@ func (b *Builder) AddType(t datatype.T) { b.conf = &config.Server{} } if b.conf.Services != nil { - panic(ErrLateType) + panic(errLateType) } b.conf.Types = append(b.conf.Types, t) } @@ -41,7 +41,7 @@ func (b *Builder) Setup(r io.Reader) error { b.conf = &config.Server{} } if b.conf.Services != nil { - panic(ErrAlreadySetup) + panic(errAlreadySetup) } return b.conf.Parse(r) } @@ -49,7 +49,7 @@ func (b *Builder) Setup(r io.Reader) error { // Bind a dynamic handler to a REST service func (b *Builder) Bind(method, path string, fn interface{}) error { if b.conf.Services == nil { - return ErrNotSetup + return errNotSetup } // find associated service @@ -62,7 +62,7 @@ func (b *Builder) Bind(method, path string, fn interface{}) error { } if service == nil { - return fmt.Errorf("%s '%s': %w", method, path, ErrUnknownService) + return fmt.Errorf("%s '%s': %w", method, path, errUnknownService) } dyn, err := dynfunc.Build(fn, *service) @@ -91,7 +91,7 @@ func (b Builder) Build() (http.Handler, error) { } } if !hasAssociatedHandler { - return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, ErrMissingHandler) + return nil, fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, errMissingHandler) } } diff --git a/datatype/datatype.go b/datatype/datatype.go new file mode 100644 index 0000000..2e0a264 --- /dev/null +++ b/datatype/datatype.go @@ -0,0 +1,23 @@ +package datatype + +import ( + "reflect" +) + +// Validator returns whether a given value fulfills the datatype +// and casts the value into a common go type. +// +// for example, if a validator checks for upper case strings, +// whether the value is a []byte, a string or a []rune, if the +// value matches the validator's checks, it will be cast it into +// a common go type, say, string. +type Validator func(value interface{}) (cast interface{}, valid bool) + +// T represents a datatype. The Build function returns a Validator if +// it manages types with the name `typeDefinition` (from the configuration field "type"); else it or returns NIL if the type +// definition does not match this datatype; the registry is passed to allow recursive datatypes (e.g. slices, structs, etc) +// The datatype's validator (when input is valid) must return a cast's go type matching the `Type() reflect.Type` +type T interface { + Type() reflect.Type + Build(typeDefinition string, registry ...T) Validator +} diff --git a/datatype/types.go b/datatype/types.go deleted file mode 100644 index c258b2f..0000000 --- a/datatype/types.go +++ /dev/null @@ -1,15 +0,0 @@ -package datatype - -import "reflect" - -// Validator returns whether a given value fulfills a datatype -// and casts the value into a compatible type -type Validator func(value interface{}) (cast interface{}, valid bool) - -// T builds a T from the type definition (from the configuration field "type") and returns NIL if the type -// definition does not match this T ; the registry is passed for recursive datatypes (e.g. slices, structs, etc) -// to be able to access other datatypes -type T interface { - Type() reflect.Type - Build(typeDefinition string, registry ...T) Validator -} diff --git a/errors.go b/errors.go index f3c7145..bb863b7 100644 --- a/errors.go +++ b/errors.go @@ -3,22 +3,21 @@ package aicra // cerr allows you to create constant "const" error with type boxing. type cerr string -// Error implements the error builtin interface. func (err cerr) Error() string { return string(err) } -// ErrLateType - cannot add datatype after setting up the definition -const ErrLateType = cerr("types cannot be added after Setup") +// errLateType - cannot add datatype after setting up the definition +const errLateType = cerr("types cannot be added after Setup") -// ErrNotSetup - not set up yet -const ErrNotSetup = cerr("not set up") +// errNotSetup - not set up yet +const errNotSetup = cerr("not set up") -// ErrAlreadySetup - already set up -const ErrAlreadySetup = cerr("already set up") +// errAlreadySetup - already set up +const errAlreadySetup = cerr("already set up") -// ErrUnknownService - no service matching this handler -const ErrUnknownService = cerr("unknown service") +// errUnknownService - no service matching this handler +const errUnknownService = cerr("unknown service") -// ErrMissingHandler - missing handler -const ErrMissingHandler = cerr("missing handler") +// errMissingHandler - missing handler +const errMissingHandler = cerr("missing handler") diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..bfda9df --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,182 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "git.xdrm.io/go/aicra/datatype" +) + +// Server definition +type Server struct { + Types []datatype.T + Services []*Service +} + +// Parse a configuration into a server. Server.Types must be set beforehand to +// make datatypes available when checking and formatting the read configuration. +func (srv *Server) Parse(r io.Reader) error { + err := json.NewDecoder(r).Decode(&srv.Services) + if err != nil { + return fmt.Errorf("%s: %w", errRead, err) + } + + err = srv.validate() + if err != nil { + return fmt.Errorf("%s: %w", errFormat, err) + } + return nil +} + +// validate implements the validator interface +func (server Server) validate(datatypes ...datatype.T) error { + for _, service := range server.Services { + err := service.validate(server.Types...) + if err != nil { + return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err) + } + } + + if err := server.collide(); err != nil { + return fmt.Errorf("%s: %w", errFormat, err) + } + return nil +} + +// Find a service matching an incoming HTTP request +func (server Server) Find(r *http.Request) *Service { + for _, service := range server.Services { + if matches := service.Match(r); matches { + return service + } + } + + return nil +} + +// collide returns if there is collision between any service for the same method and colliding paths. +// Note that service path collision detection relies on datatypes: +// - example 1: `/user/{id}` and `/user/articles` will not collide as {id} is an int and "articles" is not +// - example 2: `/user/{name}` and `/user/articles` will collide as {name} is a string so as "articles" +// - example 3: `/user/{name}` and `/user/{id}` will collide as {name} and {id} cannot be checked against their potential values +func (server *Server) collide() error { + length := len(server.Services) + + // for each service combination + for a := 0; a < length; a++ { + for b := a + 1; b < length; b++ { + aService := server.Services[a] + bService := server.Services[b] + + if aService.Method != bService.Method { + continue + } + + aURIParts := SplitURL(aService.Pattern) + bURIParts := SplitURL(bService.Pattern) + if len(aURIParts) != len(bURIParts) { + continue + } + + err := checkURICollision(aURIParts, bURIParts, aService.Input, bService.Input) + if err != nil { + return fmt.Errorf("(%s '%s') vs (%s '%s'): %w", aService.Method, aService.Pattern, bService.Method, bService.Pattern, err) + } + } + } + + return nil +} + +// check if uri of services A and B collide +func checkURICollision(uriA, uriB []string, inputA, inputB map[string]*Parameter) error { + var errors = []error{} + + // for each part + for pi, aPart := range uriA { + bPart := uriB[pi] + + // no need for further check as it has been done earlier in the validation process + aIsCapture := len(aPart) > 1 && aPart[0] == '{' + bIsCapture := len(bPart) > 1 && bPart[0] == '{' + + // both captures -> as we cannot check, consider a collision + if aIsCapture && bIsCapture { + errors = append(errors, fmt.Errorf("%w (path %s and %s)", errPatternCollision, aPart, bPart)) + continue + } + + // no capture -> check strict equality + if !aIsCapture && !bIsCapture { + if aPart == bPart { + errors = append(errors, fmt.Errorf("%w (same path '%s')", errPatternCollision, aPart)) + continue + } + } + + // A captures B -> check type (B is A ?) + if aIsCapture { + input, exists := inputA[aPart] + + // fail if no type or no validator + if !exists || input.Validator == nil { + errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, aPart)) + continue + } + + // fail if not valid + if _, valid := input.Validator(bPart); valid { + errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, aPart, bPart)) + continue + } + + // B captures A -> check type (A is B ?) + } else if bIsCapture { + input, exists := inputB[bPart] + + // fail if no type or no validator + if !exists || input.Validator == nil { + errors = append(errors, fmt.Errorf("%w (invalid type for %s)", errPatternCollision, bPart)) + continue + } + + // fail if not valid + if _, valid := input.Validator(aPart); valid { + errors = append(errors, fmt.Errorf("%w (%s captures '%s')", errPatternCollision, bPart, aPart)) + continue + } + } + + errors = append(errors, nil) + + } + + // at least 1 URI part not matching -> no collision + var firstError error + for _, err := range errors { + if err != nil && firstError == nil { + firstError = err + } + + if err == nil { + return nil + } + } + + return firstError +} + +// SplitURL without empty sets +func SplitURL(url string) []string { + trimmed := strings.Trim(url, " /\t\r\n") + split := strings.Split(trimmed, "/") + + // remove empty set when empty url + if len(split) == 1 && len(split[0]) == 0 { + return []string{} + } + return split +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6ff7dcd..831691e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -21,15 +21,15 @@ func TestLegalServiceName(t *testing.T) { // empty { `[ { "method": "GET", "info": "a", "path": "" } ]`, - ErrInvalidPattern, + errInvalidPattern, }, { `[ { "method": "GET", "info": "a", "path": "no-starting-slash" } ]`, - ErrInvalidPattern, + errInvalidPattern, }, { `[ { "method": "GET", "info": "a", "path": "ending-slash/" } ]`, - ErrInvalidPattern, + errInvalidPattern, }, { `[ { "method": "GET", "info": "a", "path": "/" } ]`, @@ -45,35 +45,35 @@ func TestLegalServiceName(t *testing.T) { }, { `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}a" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}" } ]`, - ErrUndefinedBraceCapture, + errUndefinedBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/s{braces}/abc" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}s/abc" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/abc" } ]`, - ErrUndefinedBraceCapture, + errUndefinedBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{b{races}s/abc" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, { `[ { "method": "GET", "info": "a", "path": "/invalid/{braces}/}abc" } ]`, - ErrInvalidPatternBraceCapture, + errInvalidPatternBraceCapture, }, } @@ -143,8 +143,8 @@ func TestAvailableMethods(t *testing.T) { t.FailNow() } - if !test.ValidMethod && !errors.Is(err, ErrUnknownMethod) { - t.Errorf("expected error <%s> got <%s>", ErrUnknownMethod, err) + if !test.ValidMethod && !errors.Is(err, errUnknownMethod) { + t.Errorf("expected error <%s> got <%s>", errUnknownMethod, err) t.FailNow() } }) @@ -217,8 +217,8 @@ func TestParseMissingMethodDescription(t *testing.T) { t.FailNow() } - if !test.ValidDescription && !errors.Is(err, ErrMissingDescription) { - t.Errorf("expected error <%s> got <%s>", ErrMissingDescription, err) + if !test.ValidDescription && !errors.Is(err, errMissingDescription) { + t.Errorf("expected error <%s> got <%s>", errMissingDescription, err) t.FailNow() } }) @@ -321,7 +321,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamDesc, + errMissingParamDesc, }, { // invalid param name suffix `[ @@ -334,7 +334,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamDesc, + errMissingParamDesc, }, { // missing param description @@ -348,7 +348,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamDesc, + errMissingParamDesc, }, { // empty param description `[ @@ -361,7 +361,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamDesc, + errMissingParamDesc, }, { // missing param type @@ -375,7 +375,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamType, + errMissingParamType, }, { // empty param type `[ @@ -388,7 +388,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMissingParamType, + errMissingParamType, }, { // invalid type (optional mark only) `[ @@ -402,7 +402,7 @@ func TestParseParameters(t *testing.T) { } ]`, - ErrMissingParamType, + errMissingParamType, }, { // valid description + valid type `[ @@ -444,7 +444,7 @@ func TestParseParameters(t *testing.T) { } ]`, // 2 possible errors as map order is not deterministic - ErrParamNameConflict, + errParamNameConflict, }, { // rename conflict with name `[ @@ -459,7 +459,7 @@ func TestParseParameters(t *testing.T) { } ]`, // 2 possible errors as map order is not deterministic - ErrParamNameConflict, + errParamNameConflict, }, { // rename conflict with rename `[ @@ -474,7 +474,7 @@ func TestParseParameters(t *testing.T) { } ]`, // 2 possible errors as map order is not deterministic - ErrParamNameConflict, + errParamNameConflict, }, { // both renamed with no conflict @@ -503,7 +503,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMandatoryRename, + errMandatoryRename, }, { `[ @@ -516,7 +516,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrMandatoryRename, + errMandatoryRename, }, { `[ @@ -556,7 +556,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrIllegalOptionalURIParam, + errIllegalOptionalURIParam, }, { // URI parameter not specified `[ @@ -569,7 +569,7 @@ func TestParseParameters(t *testing.T) { } } ]`, - ErrUnspecifiedBraceCapture, + errUnspecifiedBraceCapture, }, { // URI parameter not defined `[ @@ -580,7 +580,7 @@ func TestParseParameters(t *testing.T) { "in": { } } ]`, - ErrUndefinedBraceCapture, + errUndefinedBraceCapture, }, } @@ -637,7 +637,7 @@ func TestServiceCollision(t *testing.T) { "info": "info", "in": {} } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -672,7 +672,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -698,7 +698,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -711,7 +711,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -750,7 +750,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -789,7 +789,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -804,7 +804,7 @@ func TestServiceCollision(t *testing.T) { } } ]`, - ErrPatternCollision, + errPatternCollision, }, { `[ @@ -877,6 +877,36 @@ func TestMatchSimple(t *testing.T) { "/a", false, }, + { // root url + `[ { + "method": "GET", + "path": "/a", + "info": "info", + "in": {} + } ]`, + "/", + false, + }, + { + `[ { + "method": "GET", + "path": "/a", + "info": "info", + "in": {} + } ]`, + "/", + false, + }, + { + `[ { + "method": "GET", + "path": "/", + "info": "info", + "in": {} + } ]`, + "/", + true, + }, { `[ { "method": "GET", @@ -997,3 +1027,80 @@ func TestMatchSimple(t *testing.T) { } } + +func TestFindPriority(t *testing.T) { + t.Parallel() + tests := []struct { + Config string + URL string + MatchingDesc string + }{ + { + `[ + { "method": "GET", "path": "/a", "info": "s1" }, + { "method": "GET", "path": "/", "info": "s2" } + ]`, + "/", + "s2", + }, + { + `[ + { "method": "GET", "path": "/", "info": "s2" }, + { "method": "GET", "path": "/a", "info": "s1" } + ]`, + "/", + "s2", + }, + { + `[ + { "method": "GET", "path": "/a", "info": "s1" }, + { "method": "GET", "path": "/", "info": "s2" } + ]`, + "/a", + "s1", + }, + { + `[ + { "method": "GET", "path": "/a/b/c", "info": "s1" }, + { "method": "GET", "path": "/a/b", "info": "s2" } + ]`, + "/a/b/c", + "s1", + }, + { + `[ + { "method": "GET", "path": "/a/b/c", "info": "s1" }, + { "method": "GET", "path": "/a/b", "info": "s2" } + ]`, + "/a/b/", + "s2", + }, + } + + for i, test := range tests { + + t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + srv.Types = append(srv.Types, builtin.IntDataType{}) + srv.Types = append(srv.Types, builtin.BoolDataType{}) + err := srv.Parse(strings.NewReader(test.Config)) + + if err != nil { + t.Errorf("unexpected error: '%s'", err) + t.FailNow() + } + + req := httptest.NewRequest(http.MethodGet, test.URL, nil) + service := srv.Find(req) + if service == nil { + t.Errorf("expected to find a service") + t.FailNow() + } + if service.Description != test.MatchingDesc { + t.Errorf("expected description '%s', got '%s'", test.MatchingDesc, service.Description) + t.FailNow() + } + }) + } +} diff --git a/internal/config/errors.go b/internal/config/errors.go index 2a2df17..91f4997 100644 --- a/internal/config/errors.go +++ b/internal/config/errors.go @@ -3,58 +3,57 @@ package config // cerr allows you to create constant "const" error with type boxing. type cerr string -// Error implements the error builtin interface. func (err cerr) Error() string { return string(err) } -// ErrRead - a problem ocurred when trying to read the configuration file -const ErrRead = cerr("cannot read config") +// errRead - read error +const errRead = cerr("cannot read config") -// ErrUnknownMethod - invalid http method -const ErrUnknownMethod = cerr("unknown HTTP method") +// errUnknownMethod - unknown http method +const errUnknownMethod = cerr("unknown HTTP method") -// ErrFormat - a invalid format has been detected -const ErrFormat = cerr("invalid config format") +// errFormat - invalid format +const errFormat = cerr("invalid config format") -// ErrPatternCollision - there is a collision between 2 services' patterns (same method) -const ErrPatternCollision = cerr("pattern collision") +// errPatternCollision - collision between 2 services' patterns +const errPatternCollision = cerr("pattern collision") -// ErrInvalidPattern - a service pattern is malformed -const ErrInvalidPattern = cerr("must begin with a '/' and not end with") +// errInvalidPattern - malformed service pattern +const errInvalidPattern = cerr("malformed service path: must begin with a '/' and not end with") -// ErrInvalidPatternBraceCapture - a service pattern brace capture is invalid -const ErrInvalidPatternBraceCapture = cerr("invalid uri capturing braces") +// errInvalidPatternBraceCapture - invalid brace capture +const errInvalidPatternBraceCapture = cerr("invalid uri parameter") -// ErrUnspecifiedBraceCapture - a parameter brace capture is not specified in the pattern -const ErrUnspecifiedBraceCapture = cerr("capturing brace missing in the path") +// errUnspecifiedBraceCapture - missing path brace capture +const errUnspecifiedBraceCapture = cerr("missing uri parameter") -// ErrMandatoryRename - capture/query parameters must have a rename -const ErrMandatoryRename = cerr("capture and query parameters must have a 'name'") +// errUndefinedBraceCapture - missing capturing brace definition +const errUndefinedBraceCapture = cerr("missing uri parameter definition") -// ErrUndefinedBraceCapture - a parameter brace capture in the pattern is not defined in parameters -const ErrUndefinedBraceCapture = cerr("capturing brace missing input definition") +// errMandatoryRename - capture/query parameters must be renamed +const errMandatoryRename = cerr("uri and query parameters must be renamed") -// ErrMissingDescription - a service is missing its description -const ErrMissingDescription = cerr("missing description") +// errMissingDescription - a service is missing its description +const errMissingDescription = cerr("missing description") -// ErrIllegalOptionalURIParam - an URI parameter cannot be optional -const ErrIllegalOptionalURIParam = cerr("URI parameter cannot be optional") +// errIllegalOptionalURIParam - uri parameter cannot optional +const errIllegalOptionalURIParam = cerr("uri parameter cannot be optional") -// ErrOptionalOption - an output is optional -const ErrOptionalOption = cerr("output cannot be optional") +// errOptionalOption - cannot have optional output +const errOptionalOption = cerr("output cannot be optional") -// ErrMissingParamDesc - a parameter is missing its description -const ErrMissingParamDesc = cerr("missing parameter description") +// errMissingParamDesc - missing parameter description +const errMissingParamDesc = cerr("missing parameter description") -// ErrUnknownDataType - a parameter has an unknown datatype name -const ErrUnknownDataType = cerr("unknown data type") +// errUnknownDataType - unknown parameter datatype +const errUnknownDataType = cerr("unknown parameter datatype") -// ErrIllegalParamName - a parameter has an illegal name -const ErrIllegalParamName = cerr("illegal parameter name") +// errIllegalParamName - illegal parameter name +const errIllegalParamName = cerr("illegal parameter name") -// ErrMissingParamType - a parameter has an illegal type -const ErrMissingParamType = cerr("missing parameter type") +// errMissingParamType - missing parameter type +const errMissingParamType = cerr("missing parameter type") -// ErrParamNameConflict - a parameter has a conflict with its name/rename field -const ErrParamNameConflict = cerr("name conflict for parameter") +// errParamNameConflict - name/rename conflict +const errParamNameConflict = cerr("parameter name conflict") diff --git a/internal/config/func.go b/internal/config/func.go deleted file mode 100644 index 674030c..0000000 --- a/internal/config/func.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import "strings" - -// SplitURL without empty sets -func SplitURL(url string) []string { - trimmed := strings.Trim(url, " /\t\r\n") - split := strings.Split(trimmed, "/") - - // remove empty set when empty url - if len(split) == 1 && len(split[0]) == 0 { - return []string{} - } - return split -} diff --git a/internal/config/parameter.go b/internal/config/parameter.go index ca36a1c..23bfd3d 100644 --- a/internal/config/parameter.go +++ b/internal/config/parameter.go @@ -11,33 +11,29 @@ type Parameter struct { Description string `json:"info"` Type string `json:"type"` Rename string `json:"name,omitempty"` - // ExtractType is the type of data the datatype returns + Optional bool + // ExtractType is the type the Validator will cast into ExtractType reflect.Type - // Optional is set to true when the type is prefixed with '?' - Optional bool - - // Validator is inferred from @Type + // Validator is inferred from the "type" property Validator datatype.Validator } func (param *Parameter) validate(datatypes ...datatype.T) error { - // missing description if len(param.Description) < 1 { - return ErrMissingParamDesc + return errMissingParamDesc } - // invalid type if len(param.Type) < 1 || param.Type == "?" { - return ErrMissingParamType + return errMissingParamType } - // optional type transform + // optional type if param.Type[0] == '?' { param.Optional = true param.Type = param.Type[1:] } - // assign the datatype + // find validator for _, dtype := range datatypes { param.Validator = dtype.Build(param.Type, datatypes...) param.ExtractType = dtype.Type() @@ -46,8 +42,7 @@ func (param *Parameter) validate(datatypes ...datatype.T) error { } } if param.Validator == nil { - return ErrUnknownDataType + return errUnknownDataType } - return nil } diff --git a/internal/config/server.go b/internal/config/server.go deleted file mode 100644 index 192d98d..0000000 --- a/internal/config/server.go +++ /dev/null @@ -1,165 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "git.xdrm.io/go/aicra/datatype" -) - -// Server definition -type Server struct { - Types []datatype.T - Services []*Service -} - -// Parse a reader into a server. Server.Types must be set beforehand to -// make datatypes available when checking and formatting the read configuration. -func (srv *Server) Parse(r io.Reader) error { - if err := json.NewDecoder(r).Decode(&srv.Services); err != nil { - return fmt.Errorf("%s: %w", ErrRead, err) - } - - if err := srv.validate(); err != nil { - return fmt.Errorf("%s: %w", ErrFormat, err) - } - - return nil -} - -// validate implements the validator interface -func (server Server) validate(datatypes ...datatype.T) error { - for _, service := range server.Services { - err := service.validate(server.Types...) - if err != nil { - return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err) - } - } - - // check for collisions - if err := server.collide(); err != nil { - return fmt.Errorf("%s: %w", ErrFormat, err) - } - - return nil -} - -// Find a service matching an incoming HTTP request -func (server Server) Find(r *http.Request) *Service { - for _, service := range server.Services { - if matches := service.Match(r); matches { - return service - } - } - - return nil -} - -// collide returns if there is collision between services -func (server *Server) collide() error { - length := len(server.Services) - - // for each service combination - for a := 0; a < length; a++ { - for b := a + 1; b < length; b++ { - aService := server.Services[a] - bService := server.Services[b] - - // ignore different method - if aService.Method != bService.Method { - continue - } - - aParts := SplitURL(aService.Pattern) - bParts := SplitURL(bService.Pattern) - - // not same size - if len(aParts) != len(bParts) { - continue - } - - partErrors := make([]error, 0) - - // for each part - for pi, aPart := range aParts { - bPart := bParts[pi] - - aIsCapture := len(aPart) > 1 && aPart[0] == '{' - bIsCapture := len(bPart) > 1 && bPart[0] == '{' - - // both captures -> as we cannot check, consider a collision - if aIsCapture && bIsCapture { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (path %s and %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart, bPart)) - continue - } - - // no capture -> check equal - if !aIsCapture && !bIsCapture { - if aPart == bPart { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (same path '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart)) - continue - } - } - - // A captures B -> check type (B is A ?) - if aIsCapture { - input, exists := aService.Input[aPart] - - // fail if no type or no validator - if !exists || input.Validator == nil { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart)) - continue - } - - // fail if not valid - if _, valid := input.Validator(bPart); valid { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, aPart, bPart)) - continue - } - - // B captures A -> check type (A is B ?) - } else if bIsCapture { - input, exists := bService.Input[bPart] - - // fail if no type or no validator - if !exists || input.Validator == nil { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (invalid type for %s)", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, bPart)) - continue - } - - // fail if not valid - if _, valid := input.Validator(aPart); valid { - partErrors = append(partErrors, fmt.Errorf("(%s '%s') vs (%s '%s'): %w (%s captures '%s')", aService.Method, aService.Pattern, bService.Method, bService.Pattern, ErrPatternCollision, bPart, aPart)) - continue - } - } - - partErrors = append(partErrors, nil) - - } - - // if at least 1 url part does not match -> ok - var firstError error - oneMismatch := false - for _, err := range partErrors { - if err != nil && firstError == nil { - firstError = err - } - - if err == nil { - oneMismatch = true - continue - } - } - - if !oneMismatch { - return firstError - } - - } - } - - return nil -} diff --git a/internal/config/service.go b/internal/config/service.go index 0f79764..c713db2 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -22,15 +22,15 @@ type Service struct { Input map[string]*Parameter `json:"in"` Output map[string]*Parameter `json:"out"` - // references to url parameters - // format: '/uri/{param}' + // Captures contains references to URI parameters from the `Input` map. The format + // of these parameter names is "{paramName}" Captures []*BraceCapture - // references to Query parameters - // format: 'GET@paranName' + // Query contains references to HTTP Query parameters from the `Input` map. + // Query parameters names are "GET@paramName", this map contains escaped names (e.g. "paramName") Query map[string]*Parameter - // references for form parameters (all but Captures and Query) + // Form references form parameters from the `Input` map (all but Captures and Query). Form map[string]*Parameter } @@ -43,16 +43,12 @@ type BraceCapture struct { // Match returns if this service would handle this HTTP request func (svc *Service) Match(req *http.Request) bool { - // method if req.Method != svc.Method { return false } - - // check path if !svc.matchPattern(req.RequestURI) { return false } - return true } @@ -61,13 +57,12 @@ func (svc *Service) matchPattern(uri string) bool { uriparts := SplitURL(uri) parts := SplitURL(svc.Pattern) - // fail if size differ if len(uriparts) != len(parts) { return false } // root url '/' - if len(parts) == 0 { + if len(parts) == 0 && len(uriparts) == 0 { return true } @@ -118,7 +113,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error { // check description if len(strings.Trim(svc.Description, " \t\r\n")) < 1 { - return fmt.Errorf("field 'description': %w", ErrMissingDescription) + return fmt.Errorf("field 'description': %w", errMissingDescription) } // check input parameters @@ -130,7 +125,7 @@ func (svc *Service) validate(datatypes ...datatype.T) error { // fail if a brace capture remains undefined for _, capture := range svc.Captures { if capture.Ref == nil { - return fmt.Errorf("field 'in': %s: %w", capture.Name, ErrUndefinedBraceCapture) + return fmt.Errorf("field 'in': %s: %w", capture.Name, errUndefinedBraceCapture) } } @@ -149,7 +144,7 @@ func (svc *Service) isMethodAvailable() error { return nil } } - return ErrUnknownMethod + return errUnknownMethod } func (svc *Service) isPatternValid() error { @@ -157,13 +152,13 @@ func (svc *Service) isPatternValid() error { // empty pattern if length < 1 { - return ErrInvalidPattern + return errInvalidPattern } if length > 1 { // pattern not starting with '/' or ending with '/' if svc.Pattern[0] != '/' || svc.Pattern[length-1] == '/' { - return ErrInvalidPattern + return errInvalidPattern } } @@ -171,7 +166,7 @@ func (svc *Service) isPatternValid() error { parts := SplitURL(svc.Pattern) for i, part := range parts { if len(part) < 1 { - return ErrInvalidPattern + return errInvalidPattern } // if brace capture @@ -192,7 +187,7 @@ func (svc *Service) isPatternValid() error { // fail on invalid format if strings.ContainsAny(part, "{}") { - return ErrInvalidPatternBraceCapture + return errInvalidPatternBraceCapture } } @@ -211,7 +206,7 @@ func (svc *Service) validateInput(types []datatype.T) error { // for each parameter for paramName, param := range svc.Input { if len(paramName) < 1 { - return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName) + return fmt.Errorf("%s: %w", paramName, errIllegalParamName) } // fail if brace capture does not exists in pattern @@ -228,7 +223,7 @@ func (svc *Service) validateInput(types []datatype.T) error { } } if !found { - return fmt.Errorf("%s: %w", paramName, ErrUnspecifiedBraceCapture) + return fmt.Errorf("%s: %w", paramName, errUnspecifiedBraceCapture) } iscapture = true @@ -251,7 +246,7 @@ func (svc *Service) validateInput(types []datatype.T) error { // fail if capture or query without rename if len(param.Rename) < 1 && (iscapture || isquery) { - return fmt.Errorf("%s: %w", paramName, ErrMandatoryRename) + return fmt.Errorf("%s: %w", paramName, errMandatoryRename) } // use param name if no rename @@ -266,7 +261,7 @@ func (svc *Service) validateInput(types []datatype.T) error { // capture parameter cannot be optional if iscapture && param.Optional { - return fmt.Errorf("%s: %w", paramName, ErrIllegalOptionalURIParam) + return fmt.Errorf("%s: %w", paramName, errIllegalOptionalURIParam) } // fail on name/rename conflict @@ -280,7 +275,7 @@ func (svc *Service) validateInput(types []datatype.T) error { // 3.2.2. Not-renamed field matches a renamed field // 3.2.3. Renamed field matches name if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename { - return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict) + return fmt.Errorf("%s: %w", paramName, errParamNameConflict) } } @@ -301,7 +296,7 @@ func (svc *Service) validateOutput(types []datatype.T) error { // for each parameter for paramName, param := range svc.Output { if len(paramName) < 1 { - return fmt.Errorf("%s: %w", paramName, ErrIllegalParamName) + return fmt.Errorf("%s: %w", paramName, errIllegalParamName) } // use param name if no rename @@ -315,7 +310,7 @@ func (svc *Service) validateOutput(types []datatype.T) error { } if param.Optional { - return fmt.Errorf("%s: %w", paramName, ErrOptionalOption) + return fmt.Errorf("%s: %w", paramName, errOptionalOption) } // fail on name/rename conflict @@ -329,7 +324,7 @@ func (svc *Service) validateOutput(types []datatype.T) error { // 3.2.2. Not-renamed field matches a renamed field // 3.2.3. Renamed field matches name if param.Rename == param2.Rename || paramName == param2.Rename || paramName2 == param.Rename { - return fmt.Errorf("%s: %w", paramName, ErrParamNameConflict) + return fmt.Errorf("%s: %w", paramName, errParamNameConflict) } } diff --git a/internal/dynfunc/errors.go b/internal/dynfunc/errors.go index c038171..0d2a4b2 100644 --- a/internal/dynfunc/errors.go +++ b/internal/dynfunc/errors.go @@ -3,49 +3,48 @@ package dynfunc // cerr allows you to create constant "const" error with type boxing. type cerr string -// Error implements the error builtin interface. func (err cerr) Error() string { return string(err) } -// ErrHandlerNotFunc - handler is not a func -const ErrHandlerNotFunc = cerr("handler must be a func") +// errHandlerNotFunc - handler is not a func +const errHandlerNotFunc = cerr("handler must be a func") -// ErrNoServiceForHandler - no service matching this handler -const ErrNoServiceForHandler = cerr("no service found for this handler") +// errNoServiceForHandler - no service matching this handler +const errNoServiceForHandler = cerr("no service found for this handler") -// ErrMissingHandlerArgumentParam - missing params arguments for handler -const ErrMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct") +// errMissingHandlerArgumentParam - missing params arguments for handler +const errMissingHandlerArgumentParam = cerr("missing handler argument : parameter struct") -// ErrUnexpectedInput - input argument is not expected -const ErrUnexpectedInput = cerr("unexpected input struct") +// errUnexpectedInput - input argument is not expected +const errUnexpectedInput = cerr("unexpected input struct") -// ErrMissingHandlerOutput - missing output for handler -const ErrMissingHandlerOutput = cerr("handler must have at least 1 output") +// errMissingHandlerOutput - missing output for handler +const errMissingHandlerOutput = cerr("handler must have at least 1 output") -// ErrMissingHandlerOutputError - missing error output for handler -const ErrMissingHandlerOutputError = cerr("handler must have its last output of type api.Error") +// errMissingHandlerOutputError - missing error output for handler +const errMissingHandlerOutputError = cerr("handler must have its last output of type api.Error") -// ErrMissingRequestArgument - missing request argument for handler -const ErrMissingRequestArgument = cerr("handler first argument must be of type api.Request") +// errMissingRequestArgument - missing request argument for handler +const errMissingRequestArgument = cerr("handler first argument must be of type api.Request") -// ErrMissingParamArgument - missing parameters argument for handler -const ErrMissingParamArgument = cerr("handler second argument must be a struct") +// errMissingParamArgument - missing parameters argument for handler +const errMissingParamArgument = cerr("handler second argument must be a struct") -// ErrUnexportedName - argument is unexported in struct -const ErrUnexportedName = cerr("unexported name") +// errUnexportedName - argument is unexported in struct +const errUnexportedName = cerr("unexported name") -// ErrMissingParamOutput - missing output argument for handler -const ErrMissingParamOutput = cerr("handler first output must be a *struct") +// errMissingParamOutput - missing output argument for handler +const errMissingParamOutput = cerr("handler first output must be a *struct") -// ErrMissingParamFromConfig - missing a parameter in handler struct -const ErrMissingParamFromConfig = cerr("missing a parameter from configuration") +// errMissingParamFromConfig - missing a parameter in handler struct +const errMissingParamFromConfig = cerr("missing a parameter from configuration") -// ErrMissingOutputFromConfig - missing a parameter in handler struct -const ErrMissingOutputFromConfig = cerr("missing a parameter from configuration") +// errMissingOutputFromConfig - missing a parameter in handler struct +const errMissingOutputFromConfig = cerr("missing a parameter from configuration") -// ErrWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct -const ErrWrongParamTypeFromConfig = cerr("invalid struct field type") +// errWrongParamTypeFromConfig - a configuration parameter type is invalid in the handler param struct +const errWrongParamTypeFromConfig = cerr("invalid struct field type") -// ErrMissingHandlerErrorOutput - missing handler output error -const ErrMissingHandlerErrorOutput = cerr("last output must be of type api.Error") +// errMissingHandlerErrorOutput - missing handler output error +const errMissingHandlerErrorOutput = cerr("last output must be of type api.Error") diff --git a/internal/dynfunc/handler.go b/internal/dynfunc/handler.go index ad00224..f932a89 100644 --- a/internal/dynfunc/handler.go +++ b/internal/dynfunc/handler.go @@ -8,6 +8,12 @@ import ( "git.xdrm.io/go/aicra/internal/config" ) +// Handler represents a dynamic api handler +type Handler struct { + spec spec + fn interface{} +} + // Build a handler from a service configuration and a dynamic function // // @fn must have as a signature : `func(inputStruct) (*outputStruct, api.Error)` @@ -26,7 +32,7 @@ func Build(fn interface{}, service config.Service) (*Handler, error) { fnv := reflect.ValueOf(fn) if fnv.Type().Kind() != reflect.Func { - return nil, ErrHandlerNotFunc + return nil, errHandlerNotFunc } if err := h.spec.checkInput(fnv); err != nil { diff --git a/internal/dynfunc/spec.go b/internal/dynfunc/spec.go index 7b3c4f6..2449a85 100644 --- a/internal/dynfunc/spec.go +++ b/internal/dynfunc/spec.go @@ -9,6 +9,11 @@ import ( "git.xdrm.io/go/aicra/internal/config" ) +type spec struct { + Input map[string]reflect.Type + Output map[string]reflect.Type +} + // builds a spec from the configuration service func makeSpec(service config.Service) spec { spec := spec{ @@ -45,34 +50,34 @@ func (s spec) checkInput(fnv reflect.Value) error { // no input -> ok if len(s.Input) == 0 { if fnt.NumIn() > 0 { - return ErrUnexpectedInput + return errUnexpectedInput } return nil } if fnt.NumIn() != 1 { - return ErrMissingHandlerArgumentParam + return errMissingHandlerArgumentParam } // arg must be a struct structArg := fnt.In(0) if structArg.Kind() != reflect.Struct { - return ErrMissingParamArgument + return errMissingParamArgument } // check for invalid param for name, ptype := range s.Input { if name[0] == strings.ToLower(name)[0] { - return fmt.Errorf("%s: %w", name, ErrUnexportedName) + return fmt.Errorf("%s: %w", name, errUnexportedName) } field, exists := structArg.FieldByName(name) if !exists { - return fmt.Errorf("%s: %w", name, ErrMissingParamFromConfig) + return fmt.Errorf("%s: %w", name, errMissingParamFromConfig) } if !ptype.AssignableTo(field.Type) { - return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype) + return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype) } } @@ -83,13 +88,13 @@ func (s spec) checkInput(fnv reflect.Value) error { func (s spec) checkOutput(fnv reflect.Value) error { fnt := fnv.Type() if fnt.NumOut() < 1 { - return ErrMissingHandlerOutput + return errMissingHandlerOutput } // last output must be api.Error errOutput := fnt.Out(fnt.NumOut() - 1) if !errOutput.AssignableTo(reflect.TypeOf(api.ErrorUnknown)) { - return ErrMissingHandlerErrorOutput + return errMissingHandlerErrorOutput } // no output -> ok @@ -98,29 +103,29 @@ func (s spec) checkOutput(fnv reflect.Value) error { } if fnt.NumOut() != 2 { - return ErrMissingParamOutput + return errMissingParamOutput } // fail if first output is not a pointer to struct structOutputPtr := fnt.Out(0) if structOutputPtr.Kind() != reflect.Ptr { - return ErrMissingParamOutput + return errMissingParamOutput } structOutput := structOutputPtr.Elem() if structOutput.Kind() != reflect.Struct { - return ErrMissingParamOutput + return errMissingParamOutput } // fail on invalid output for name, ptype := range s.Output { if name[0] == strings.ToLower(name)[0] { - return fmt.Errorf("%s: %w", name, ErrUnexportedName) + return fmt.Errorf("%s: %w", name, errUnexportedName) } field, exists := structOutput.FieldByName(name) if !exists { - return fmt.Errorf("%s: %w", name, ErrMissingOutputFromConfig) + return fmt.Errorf("%s: %w", name, errMissingOutputFromConfig) } // ignore types evalutating to nil @@ -129,7 +134,7 @@ func (s spec) checkOutput(fnv reflect.Value) error { } if !field.Type.ConvertibleTo(ptype) { - return fmt.Errorf("%s: %w (%s instead of %s)", name, ErrWrongParamTypeFromConfig, field.Type, ptype) + return fmt.Errorf("%s: %w (%s instead of %s)", name, errWrongParamTypeFromConfig, field.Type, ptype) } } diff --git a/internal/dynfunc/spec_test.go b/internal/dynfunc/spec_test.go index 79ccfe3..a9ad5e9 100644 --- a/internal/dynfunc/spec_test.go +++ b/internal/dynfunc/spec_test.go @@ -25,7 +25,7 @@ func TestInputCheck(t *testing.T) { { Input: map[string]reflect.Type{}, Fn: func(int, string) {}, - Err: ErrUnexpectedInput, + Err: errUnexpectedInput, }, // missing input struct in func { @@ -33,7 +33,7 @@ func TestInputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() {}, - Err: ErrMissingHandlerArgumentParam, + Err: errMissingHandlerArgumentParam, }, // input not a struct { @@ -41,7 +41,7 @@ func TestInputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func(int) {}, - Err: ErrMissingParamArgument, + Err: errMissingParamArgument, }, // unexported param name { @@ -49,7 +49,7 @@ func TestInputCheck(t *testing.T) { "test1": reflect.TypeOf(int(0)), }, Fn: func(struct{}) {}, - Err: ErrUnexportedName, + Err: errUnexportedName, }, // input field missing { @@ -57,7 +57,7 @@ func TestInputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func(struct{}) {}, - Err: ErrMissingParamFromConfig, + Err: errMissingParamFromConfig, }, // input field invalid type { @@ -65,7 +65,7 @@ func TestInputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func(struct{ Test1 string }) {}, - Err: ErrWrongParamTypeFromConfig, + Err: errWrongParamTypeFromConfig, }, // input field valid type { @@ -115,13 +115,13 @@ func TestOutputCheck(t *testing.T) { { Output: map[string]reflect.Type{}, Fn: func() {}, - Err: ErrMissingHandlerOutput, + Err: errMissingHandlerOutput, }, // no input -> with last type not api.Error { Output: map[string]reflect.Type{}, Fn: func() bool { return true }, - Err: ErrMissingHandlerErrorOutput, + Err: errMissingHandlerErrorOutput, }, // no input -> with api.Error { @@ -141,7 +141,7 @@ func TestOutputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() api.Error { return api.ErrorSuccess }, - Err: ErrMissingParamOutput, + Err: errMissingParamOutput, }, // output not a pointer { @@ -149,7 +149,7 @@ func TestOutputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() (int, api.Error) { return 0, api.ErrorSuccess }, - Err: ErrMissingParamOutput, + Err: errMissingParamOutput, }, // output not a pointer to struct { @@ -157,7 +157,7 @@ func TestOutputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() (*int, api.Error) { return nil, api.ErrorSuccess }, - Err: ErrMissingParamOutput, + Err: errMissingParamOutput, }, // unexported param name { @@ -165,7 +165,7 @@ func TestOutputCheck(t *testing.T) { "test1": reflect.TypeOf(int(0)), }, Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, - Err: ErrUnexportedName, + Err: errUnexportedName, }, // output field missing { @@ -173,7 +173,7 @@ func TestOutputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() (*struct{}, api.Error) { return nil, api.ErrorSuccess }, - Err: ErrMissingParamFromConfig, + Err: errMissingParamFromConfig, }, // output field invalid type { @@ -181,7 +181,7 @@ func TestOutputCheck(t *testing.T) { "Test1": reflect.TypeOf(int(0)), }, Fn: func() (*struct{ Test1 string }, api.Error) { return nil, api.ErrorSuccess }, - Err: ErrWrongParamTypeFromConfig, + Err: errWrongParamTypeFromConfig, }, // output field valid type { diff --git a/internal/dynfunc/types.go b/internal/dynfunc/types.go deleted file mode 100644 index 3b0aac8..0000000 --- a/internal/dynfunc/types.go +++ /dev/null @@ -1,14 +0,0 @@ -package dynfunc - -import "reflect" - -// Handler represents a dynamic api handler -type Handler struct { - spec spec - fn interface{} -} - -type spec struct { - Input map[string]reflect.Type - Output map[string]reflect.Type -} diff --git a/internal/multipart/component.go b/internal/multipart/component.go index df4d8bf..0080bd2 100644 --- a/internal/multipart/component.go +++ b/internal/multipart/component.go @@ -13,19 +13,19 @@ func (comp *Component) parseHeaders(_raw []byte) error { // 1. Extract lines _lines := strings.Split(string(_raw), "\n") if len(_lines) < 2 { - return ErrNoHeader + return errNoHeader } // 2. trim each line + remove 'Content-Disposition' prefix header := strings.Trim(_lines[0], " \t\r") if !strings.HasPrefix(header, "Content-Disposition: form-data;") { - return ErrNoHeader + return errNoHeader } header = strings.Trim(header[len("Content-Disposition: form-data;"):], " \t\r") if len(header) < 1 { - return ErrNoHeader + return errNoHeader } // 3. Extract each key-value pair diff --git a/internal/multipart/types.go b/internal/multipart/multipart.go similarity index 52% rename from internal/multipart/types.go rename to internal/multipart/multipart.go index a14fd61..970cca8 100644 --- a/internal/multipart/types.go +++ b/internal/multipart/multipart.go @@ -3,19 +3,18 @@ package multipart // cerr allows you to create constant "const" error with type boxing. type cerr string -// Error implements the error builtin interface. func (err cerr) Error() string { return string(err) } -// ErrMissingDataName is set when a multipart variable/file has no name="..." -const ErrMissingDataName = cerr("data has no name") +// errMissingDataName is set when a multipart variable/file has no name="..." +const errMissingDataName = cerr("data has no name") -// ErrDataNameConflict is set when a multipart variable/file name is already used -const ErrDataNameConflict = cerr("data name conflict") +// errDataNameConflict is set when a multipart variable/file name is already used +const errDataNameConflict = cerr("data name conflict") -// ErrNoHeader is set when a multipart variable/file has no (valid) header -const ErrNoHeader = cerr("data has no header") +// errNoHeader is set when a multipart variable/file has no (valid) header +const errNoHeader = cerr("data has no header") // Component represents a multipart variable/file type Component struct { diff --git a/internal/multipart/reader.go b/internal/multipart/reader.go index affb80a..eaa8449 100644 --- a/internal/multipart/reader.go +++ b/internal/multipart/reader.go @@ -71,11 +71,11 @@ func (reader *Reader) Parse() error { name := comp.GetHeader("name") if len(name) < 1 { - return ErrMissingDataName + return errMissingDataName } if _, nameUsed := reader.Data[name]; nameUsed { - return ErrDataNameConflict + return errDataNameConflict } reader.Data[name] = comp diff --git a/internal/multipart/reader_test.go b/internal/multipart/reader_test.go index 0c4cc02..fa3dfdb 100644 --- a/internal/multipart/reader_test.go +++ b/internal/multipart/reader_test.go @@ -196,8 +196,8 @@ func TestNoName(t *testing.T) { return } - if err = mpr.Parse(); err != ErrMissingDataName { - t.Errorf("expected the error <%s>, got <%s>", ErrMissingDataName, err) + if err = mpr.Parse(); err != errMissingDataName { + t.Errorf("expected the error <%s>, got <%s>", errMissingDataName, err) return } }) @@ -238,8 +238,8 @@ func TestNoHeader(t *testing.T) { return } - if err = mpr.Parse(); err != ErrNoHeader { - t.Errorf("expected the error <%s>, got <%s>", ErrNoHeader, err) + if err = mpr.Parse(); err != errNoHeader { + t.Errorf("expected the error <%s>, got <%s>", errNoHeader, err) return } }) @@ -274,8 +274,8 @@ facebook.com t.Fatalf("unexpected error <%s>", err) } - if err = mpr.Parse(); err != ErrDataNameConflict { - t.Fatalf("expected the error <%s>, got <%s>", ErrDataNameConflict, err) + if err = mpr.Parse(); err != errDataNameConflict { + t.Fatalf("expected the error <%s>, got <%s>", errDataNameConflict, err) } } diff --git a/internal/reqdata/set.go b/internal/reqdata/set.go index 3930927..c8aac46 100644 --- a/internal/reqdata/set.go +++ b/internal/reqdata/set.go @@ -13,33 +13,29 @@ import ( "strings" ) -// Set represents all data that can be caught: +// T represents all data that can be caught from an http request for a specific +// configuration Service; it features: // - URI (from the URI) -// - GET (default url data) +// - GET (standard url data) // - POST (from json, form-data, url-encoded) // - 'application/json' => key-value pair is parsed as json into the map // - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters // - 'multipart/form-data' => parse form-data format -type Set struct { +type T struct { service *config.Service - - // contains URL+GET+FORM data with prefixes: - // - FORM: no prefix - // - URL: '{uri_var}' - // - GET: 'GET@' followed by the key in GET - Data map[string]interface{} + Data map[string]interface{} } // New creates a new empty store. -func New(service *config.Service) *Set { - return &Set{ +func New(service *config.Service) *T { + return &T{ service: service, - Data: make(map[string]interface{}), + Data: map[string]interface{}{}, } } -// ExtractURI fills 'Set' with creating pointers inside 'Url' -func (i *Set) ExtractURI(req http.Request) error { +// GetURI parameters +func (i *T) GetURI(req http.Request) error { uriparts := config.SplitURL(req.URL.RequestURI()) for _, capture := range i.service.Captures { @@ -54,122 +50,97 @@ func (i *Set) ExtractURI(req http.Request) error { return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType) } - // parse parameter parsed := parseParameter(value) - - // check type cast, valid := capture.Ref.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType) } - - // store cast value in 'Set' i.Data[capture.Ref.Rename] = cast } return nil } -// ExtractQuery data from the url query parameters -func (i *Set) ExtractQuery(req http.Request) error { +// GetQuery data from the url query parameters +func (i *T) GetQuery(req http.Request) error { query := req.URL.Query() for name, param := range i.service.Query { value, exist := query[name] - // fail on missing required if !exist && !param.Optional { return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) } - // optional if !exist { continue } - // parse parameter parsed := parseParameter(value) - - // check type cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - - // store cast value i.Data[param.Rename] = cast } return nil } -// ExtractForm data from request -// +// GetForm parameters the from request // - parse 'form-data' if not supported for non-POST requests // - parse 'x-www-form-urlencoded' // - parse 'application/json' -func (i *Set) ExtractForm(req http.Request) error { - - // ignore GET method +func (i *T) GetForm(req http.Request) error { if req.Method == http.MethodGet { return nil } - contentType := req.Header.Get("Content-Type") - - // parse json - if strings.HasPrefix(contentType, "application/json") { + ct := req.Header.Get("Content-Type") + switch { + case strings.HasPrefix(ct, "application/json"): return i.parseJSON(req) - } - // parse urlencoded - if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + case strings.HasPrefix(ct, "application/x-www-form-urlencoded"): return i.parseUrlencoded(req) - } - // parse multipart - if strings.HasPrefix(contentType, "multipart/form-data; boundary=") { + case strings.HasPrefix(ct, "multipart/form-data; boundary="): return i.parseMultipart(req) - } - // nothing to parse - return nil + default: + return nil + } } // parseJSON parses JSON from the request body inside 'Form' // and 'Set' -func (i *Set) parseJSON(req http.Request) error { - - parsed := make(map[string]interface{}, 0) +func (i *T) parseJSON(req http.Request) error { + var parsed map[string]interface{} decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(&parsed); err != nil { - if err == io.EOF { - return nil - } + err := decoder.Decode(&parsed) + if err == io.EOF { + return nil + } + if err != nil { return fmt.Errorf("%s: %w", err, ErrInvalidJSON) } for name, param := range i.service.Form { value, exist := parsed[name] - // fail on missing required if !exist && !param.Optional { return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) } - // optional if !exist { continue } - // fail on invalid type cast, valid := param.Validator(value) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - - // store cast value i.Data[param.Rename] = cast } @@ -178,8 +149,7 @@ func (i *Set) parseJSON(req http.Request) error { // parseUrlencoded parses urlencoded from the request body inside 'Form' // and 'Set' -func (i *Set) parseUrlencoded(req http.Request) error { - // use http.Request interface +func (i *T) parseUrlencoded(req http.Request) error { if err := req.ParseForm(); err != nil { return err } @@ -187,26 +157,19 @@ func (i *Set) parseUrlencoded(req http.Request) error { for name, param := range i.service.Form { value, exist := req.PostForm[name] - // fail on missing required if !exist && !param.Optional { return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) } - // optional if !exist { continue } - // parse parameter parsed := parseParameter(value) - - // check type cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - - // store cast value i.Data[param.Rename] = cast } @@ -215,46 +178,37 @@ func (i *Set) parseUrlencoded(req http.Request) error { // parseMultipart parses multi-part from the request body inside 'Form' // and 'Set' -func (i *Set) parseMultipart(req http.Request) error { - - // 1. create reader +func (i *T) parseMultipart(req http.Request) error { boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] mpr, err := multipart.NewReader(req.Body, boundary) + if err == io.EOF { + return nil + } if err != nil { - if err == io.EOF { - return nil - } return err } - // 2. parse multipart - if err = mpr.Parse(); err != nil { + err = mpr.Parse() + if err != nil { return fmt.Errorf("%s: %w", err, ErrInvalidMultipart) } for name, param := range i.service.Form { component, exist := mpr.Data[name] - // fail on missing required if !exist && !param.Optional { return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) } - // optional if !exist { continue } - // parse parameter parsed := parseParameter(string(component.Data)) - - // fail on invalid type cast, valid := param.Validator(parsed) if !valid { return fmt.Errorf("%s: %w", name, ErrInvalidType) } - - // store cast value i.Data[param.Rename] = cast } @@ -266,58 +220,47 @@ func (i *Set) parseMultipart(req http.Request) error { // - []string : return array of json elements // - string : return json if valid, else return raw string func parseParameter(data interface{}) interface{} { - dtype := reflect.TypeOf(data) - dvalue := reflect.ValueOf(data) + rt := reflect.TypeOf(data) + rv := reflect.ValueOf(data) - switch dtype.Kind() { + switch rt.Kind() { - /* (1) []string -> recursive */ + // []string -> recursive case reflect.Slice: - - // 1. ignore empty - if dvalue.Len() == 0 { + if rv.Len() == 0 { return data } - // 2. parse each element recursively - result := make([]interface{}, dvalue.Len()) - - for i, l := 0, dvalue.Len(); i < l; i++ { - element := dvalue.Index(i) - result[i] = parseParameter(element.Interface()) + slice := make([]interface{}, rv.Len()) + for i, l := 0, rv.Len(); i < l; i++ { + element := rv.Index(i) + slice[i] = parseParameter(element.Interface()) } - return result + return slice - /* (2) string -> parse */ + // string -> parse as json + // keep as string if invalid json case reflect.String: - - // build json wrapper - wrapper := fmt.Sprintf("{\"wrapped\":%s}", dvalue.String()) - - // try to parse as json - var result interface{} - err := json.Unmarshal([]byte(wrapper), &result) - - // return if success + var cast interface{} + wrapper := fmt.Sprintf("{\"wrapped\":%s}", rv.String()) + err := json.Unmarshal([]byte(wrapper), &cast) if err != nil { - return dvalue.String() + return rv.String() } - mapval, ok := result.(map[string]interface{}) + mapval, ok := cast.(map[string]interface{}) if !ok { - return dvalue.String() + return rv.String() } wrapped, ok := mapval["wrapped"] if !ok { - return dvalue.String() + return rv.String() } - return wrapped + // any type -> unchanged + default: + return rv.Interface() } - - /* (3) NIL if unknown type */ - return dvalue.Interface() - } diff --git a/internal/reqdata/set_test.go b/internal/reqdata/set_test.go index 7496327..771d863 100644 --- a/internal/reqdata/set_test.go +++ b/internal/reqdata/set_test.go @@ -131,7 +131,7 @@ func TestStoreWithUri(t *testing.T) { store := New(service) req := httptest.NewRequest(http.MethodGet, "http://host.com"+test.URI, nil) - err := store.ExtractURI(*req) + err := store.GetURI(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -242,7 +242,7 @@ func TestExtractQuery(t *testing.T) { store := New(getServiceWithQuery(test.ServiceParam...)) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://host.com?%s", test.Query), nil) - err := store.ExtractQuery(*req) + err := store.GetQuery(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -324,7 +324,7 @@ func TestStoreWithUrlEncodedFormParseError(t *testing.T) { // defer req.Body.Close() store := New(nil) - err := store.ExtractForm(*req) + err := store.GetForm(*req) if err == nil { t.Errorf("expected malformed urlencoded to have FailNow being parsed (got %d elements)", len(store.Data)) t.FailNow() @@ -420,7 +420,7 @@ func TestExtractFormUrlEncoded(t *testing.T) { defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(*req) + err := store.GetForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -563,7 +563,7 @@ func TestJsonParameters(t *testing.T) { defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(*req) + err := store.GetForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { @@ -720,7 +720,7 @@ x defer req.Body.Close() store := New(getServiceWithForm(test.ServiceParams...)) - err := store.ExtractForm(*req) + err := store.GetForm(*req) if err != nil { if test.Err != nil { if !errors.Is(err, test.Err) { diff --git a/server.go b/server.go index 15fd030..dbc2074 100644 --- a/server.go +++ b/server.go @@ -18,14 +18,14 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { // 1. find a matching service in the config service := server.conf.Find(req) if service == nil { - errorHandler(api.ErrorUnknownService) + errorHandler(api.ErrorUnknownService).ServeHTTP(res, req) return } // 2. extract request data dataset, err := extractRequestData(service, *req) if err != nil { - errorHandler(api.ErrorMissingParam) + errorHandler(api.ErrorMissingParam).ServeHTTP(res, req) return } @@ -39,7 +39,7 @@ func (server Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { // 4. fail if found no handler if handler == nil { - errorHandler(api.ErrorUncallableService) + errorHandler(api.ErrorUncallableService).ServeHTTP(res, req) return } @@ -76,23 +76,23 @@ func errorHandler(err api.Error) http.HandlerFunc { } } -func extractRequestData(service *config.Service, req http.Request) (*reqdata.Set, error) { +func extractRequestData(service *config.Service, req http.Request) (*reqdata.T, error) { dataset := reqdata.New(service) // 3. extract URI data - err := dataset.ExtractURI(req) + err := dataset.GetURI(req) if err != nil { return nil, err } // 4. extract query data - err = dataset.ExtractQuery(req) + err = dataset.GetQuery(req) if err != nil { return nil, err } // 5. extract form/json data - err = dataset.ExtractForm(req) + err = dataset.GetForm(req) if err != nil { return nil, err }