diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0bc3e2a..6ff7dcd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -80,7 +80,8 @@ func TestLegalServiceName(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -134,7 +135,8 @@ func TestAvailableMethods(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("service.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if test.ValidMethod && err != nil { t.Errorf("unexpected error: '%s'", err.Error()) @@ -150,20 +152,22 @@ func TestAvailableMethods(t *testing.T) { } func TestParseEmpty(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[]`) - _, err := Parse(reader) + r := strings.NewReader(`[]`) + srv := &Server{} + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error (got '%s')", err) t.FailNow() } } func TestParseJsonError(t *testing.T) { - reader := strings.NewReader(`{ + r := strings.NewReader(`{ "GET": { "info": "info }, }`) // trailing ',' is invalid JSON - _, err := Parse(reader) + srv := &Server{} + err := srv.Parse(r) if err == nil { t.Errorf("expected error") t.FailNow() @@ -205,7 +209,8 @@ func TestParseMissingMethodDescription(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw)) + srv := &Server{} + err := srv.Parse(strings.NewReader(test.Raw)) if test.ValidDescription && err != nil { t.Errorf("unexpected error: '%s'", err) @@ -223,7 +228,7 @@ func TestParseMissingMethodDescription(t *testing.T) { func TestParamEmptyRenameNoRename(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[ + r := strings.NewReader(`[ { "method": "GET", "path": "/", @@ -233,7 +238,9 @@ func TestParamEmptyRenameNoRename(t *testing.T) { } } ]`) - srv, err := Parse(reader, builtin.AnyDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error: '%s'", err) t.FailNow() @@ -254,7 +261,7 @@ func TestParamEmptyRenameNoRename(t *testing.T) { } func TestOptionalParam(t *testing.T) { t.Parallel() - reader := strings.NewReader(`[ + r := strings.NewReader(`[ { "method": "GET", "path": "/", @@ -267,7 +274,10 @@ func TestOptionalParam(t *testing.T) { } } ]`) - srv, err := Parse(reader, builtin.AnyDataType{}, builtin.BoolDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + srv.Types = append(srv.Types, builtin.BoolDataType{}) + err := srv.Parse(r) if err != nil { t.Errorf("unexpected error: '%s'", err) t.FailNow() @@ -577,7 +587,9 @@ func TestParseParameters(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Raw), builtin.AnyDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.AnyDataType{}) + err := srv.Parse(strings.NewReader(test.Raw)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -814,7 +826,10 @@ func TestServiceCollision(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - _, err := Parse(strings.NewReader(test.Config), builtin.StringDataType{}, builtin.UintDataType{}) + srv := &Server{} + srv.Types = append(srv.Types, builtin.StringDataType{}) + srv.Types = append(srv.Types, builtin.UintDataType{}) + err := srv.Parse(strings.NewReader(test.Config)) if err == nil && test.Error != nil { t.Errorf("expected an error: '%s'", test.Error.Error()) @@ -951,7 +966,11 @@ func TestMatchSimple(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("method.%d", i), func(t *testing.T) { - srv, err := Parse(strings.NewReader(test.Config), builtin.AnyDataType{}, builtin.IntDataType{}, builtin.BoolDataType{}) + 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) diff --git a/internal/config/parameter.go b/internal/config/parameter.go index 1f8be92..ca36a1c 100644 --- a/internal/config/parameter.go +++ b/internal/config/parameter.go @@ -1,11 +1,26 @@ package config import ( + "reflect" + "git.xdrm.io/go/aicra/datatype" ) -// Validate implements the validator interface -func (param *Parameter) Validate(datatypes ...datatype.T) error { +// Parameter represents a parameter definition (from api.json) +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 + ExtractType reflect.Type + // Optional is set to true when the type is prefixed with '?' + Optional bool + + // Validator is inferred from @Type + Validator datatype.Validator +} + +func (param *Parameter) validate(datatypes ...datatype.T) error { // missing description if len(param.Description) < 1 { return ErrMissingParamDesc diff --git a/internal/config/server.go b/internal/config/server.go index d2c7e3a..192d98d 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -9,34 +9,30 @@ import ( "git.xdrm.io/go/aicra/datatype" ) -// Parse builds a server configuration from a json reader and checks for most format errors. -// you can provide additional DataTypes as variadic arguments -func Parse(r io.Reader, dtypes ...datatype.T) (*Server, error) { - server := &Server{ - Types: make([]datatype.T, 0), - Services: make([]*Service, 0), - } - - // add data types - for _, dtype := range dtypes { - server.Types = append(server.Types, dtype) - } - - if err := json.NewDecoder(r).Decode(&server.Services); err != nil { - return nil, fmt.Errorf("%s: %w", ErrRead, err) - } - - if err := server.Validate(); err != nil { - return nil, fmt.Errorf("%s: %w", ErrFormat, err) - } - - return server, nil +// Server definition +type Server struct { + Types []datatype.T + Services []*Service } -// Validate implements the validator interface -func (server Server) Validate(datatypes ...datatype.T) error { +// 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...) + err := service.validate(server.Types...) if err != nil { return fmt.Errorf("%s '%s': %w", service.Method, service.Pattern, err) } diff --git a/internal/config/service.go b/internal/config/service.go index 87815d7..0f79764 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -11,6 +11,35 @@ import ( var braceRegex = regexp.MustCompile(`^{([a-z_-]+)}$`) var queryRegex = regexp.MustCompile(`^GET@([a-z_-]+)$`) +var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} + +// Service definition +type Service struct { + Method string `json:"method"` + Pattern string `json:"path"` + Scope [][]string `json:"scope"` + Description string `json:"info"` + Input map[string]*Parameter `json:"in"` + Output map[string]*Parameter `json:"out"` + + // references to url parameters + // format: '/uri/{param}' + Captures []*BraceCapture + + // references to Query parameters + // format: 'GET@paranName' + Query map[string]*Parameter + + // references for form parameters (all but Captures and Query) + Form map[string]*Parameter +} + +// BraceCapture links to the related URI parameter +type BraceCapture struct { + Name string + Index int + Ref *Parameter +} // Match returns if this service would handle this HTTP request func (svc *Service) Match(req *http.Request) bool { @@ -24,9 +53,6 @@ func (svc *Service) Match(req *http.Request) bool { return false } - // check and extract input - // todo: check if input match and extract models - return true } @@ -76,7 +102,7 @@ func (svc *Service) matchPattern(uri string) bool { } // Validate implements the validator interface -func (svc *Service) Validate(datatypes ...datatype.T) error { +func (svc *Service) validate(datatypes ...datatype.T) error { // check method err := svc.isMethodAvailable() if err != nil { @@ -233,7 +259,7 @@ func (svc *Service) validateInput(types []datatype.T) error { param.Rename = paramName } - err := param.Validate(types...) + err := param.validate(types...) if err != nil { return fmt.Errorf("%s: %w", paramName, err) } @@ -283,7 +309,7 @@ func (svc *Service) validateOutput(types []datatype.T) error { param.Rename = paramName } - err := param.Validate(types...) + err := param.validate(types...) if err != nil { return fmt.Errorf("%s: %w", paramName, err) } diff --git a/internal/config/types.go b/internal/config/types.go deleted file mode 100644 index f3532a4..0000000 --- a/internal/config/types.go +++ /dev/null @@ -1,63 +0,0 @@ -package config - -import ( - "net/http" - "reflect" - - "git.xdrm.io/go/aicra/datatype" -) - -var availableHTTPMethods = []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete} - -// validator unifies the check and format routine -type validator interface { - Validate(...datatype.T) error -} - -// Server represents a full server configuration -type Server struct { - Types []datatype.T - Services []*Service -} - -// Service represents a service definition (from api.json) -type Service struct { - Method string `json:"method"` - Pattern string `json:"path"` - Scope [][]string `json:"scope"` - Description string `json:"info"` - Input map[string]*Parameter `json:"in"` - Output map[string]*Parameter `json:"out"` - - // references to url parameters - // format: '/uri/{param}' - Captures []*BraceCapture - - // references to Query parameters - // format: 'GET@paranName' - Query map[string]*Parameter - - // references for form parameters (all but Captures and Query) - Form map[string]*Parameter -} - -// Parameter represents a parameter definition (from api.json) -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 - ExtractType reflect.Type - // Optional is set to true when the type is prefixed with '?' - Optional bool - - // Validator is inferred from @Type - Validator datatype.Validator -} - -// BraceCapture links to the related URI parameter -type BraceCapture struct { - Name string - Index int - Ref *Parameter -}