0x4a52466c696e74 1 рік тому
батько
коміт
4c7dd25806
9 змінених файлів з 412 додано та 113 видалено
  1. 3 1
      command.go
  2. 72 0
      doc.go
  3. 13 7
      errors.go
  4. 10 2
      request.go
  5. 7 1
      response.go
  6. 35 37
      rest.go
  7. 117 59
      serialize.go
  8. 10 6
      tests/app_avatar_test.go
  9. 145 0
      z_test.go

+ 3 - 1
command.go

@@ -1,6 +1,8 @@
 package rest
 
-type IApp interface {
+type IApplication interface {
+	Addr() string
+	Secret() []byte
 	Executer(r *Request) (IExecuter, bool)
 }
 

+ 72 - 0
doc.go

@@ -0,0 +1,72 @@
+package rest
+
+import (
+	"log"
+	"sync"
+	"time"
+)
+
+type ExampleGroup struct {
+	ID   int64
+	Name string
+}
+
+type ExamplePermission struct {
+	ID   int64
+	Name string
+}
+
+type ExampleUser struct {
+	ID          int64
+	Name        string
+	Token       string
+	Group       *ExampleGroup
+	Permissions []*ExamplePermission
+}
+
+// ExapmleApp реализует интерфейс IApplication, который отдает серверу параметры подключения,
+// секретный ключ шифрования токена авторизации,
+// а так же обрабатывает поступающие запросы
+type ExampleApp struct {
+	addr        string
+	secret      []byte
+	users       *sync.Map
+	groups      *sync.Map
+	permissions *sync.Map
+}
+
+func (s *ExampleApp) Addr() string {
+	return s.addr
+}
+
+func (s *ExampleApp) Secret() []byte {
+	return s.secret
+}
+
+func (s *ExampleApp) Executer(r *Request) (IExecuter, bool) {
+	return nil, false
+}
+
+// ExampleNew показывает пример создания и запуска сервера с RestAPI
+func ExampleNew() {
+	// создаем новый сервер с использованием приложения ExampleApp
+	restServ := New(&ExampleApp{})
+	// пробуем запустить его. Если через секунду запуск не удался, будет возвращена ошибка
+	if err := restServ.Listen(time.Second); err != nil {
+		// ошибка запуска
+		log.Fatal(err)
+	}
+}
+
+type ExampleRequestRegister struct {
+	ID      int64   `rest:"id"`
+	Name    string  `rest:"name"`
+	RoleIDS []int64 `rest:"role_ids"`
+	GroupID int64
+}
+
+func (s *ExampleRequestRegister) Validate(req *Request) *Response {
+	// вилидируем группу
+	//group, check := req.
+	return nil
+}

+ 13 - 7
errors.go

@@ -37,15 +37,21 @@ func NewErrFieldRequired(field string) IErrorArgs {
 	}
 }
 
-func NewErrFieldType(field, fType, gType, message string) IErrorArgs {
+func NewErrFieldType(field, expectedType, receivedType, message string, index int) IErrorArgs {
+	args := json.Map{
+		"field":    field,
+		"expected": expectedType,
+		"received": receivedType,
+	}
+	if len(message) > 0 {
+		args["message"] = message
+	}
+	if index >= 0 {
+		args["index"] = index
+	}
 	return &Error{
 		name: "FieldType",
-		args: json.Map{
-			"field":    field,
-			"expected": fType,
-			"received": gType,
-			"message":  message,
-		},
+		args: args,
 	}
 }
 

+ 10 - 2
request.go

@@ -16,19 +16,22 @@ type Request struct {
 	tokenGenerator func(json.Map, int64) (string, error)
 }
 
+// GenerateToken создает новый токен авторизации. expire - timestamp даты, после которой токен не будет действителен (если указан 0 - токен бессрочный)
 func (s *Request) GenerateToken(data json.Map, expire int64) (string, error) {
 	return s.tokenGenerator(data, expire)
 }
 
+// Возвращает путь (краткий url) запроса
 func (s *Request) RPath() string {
 	return s.URL.Path
 }
 
+// Data возвращает словерь json запроса
 func (s *Request) Data() json.Map {
 	return s.data
 }
 
-// file keys
+// FileKeys возвращает все имена файлы, которые доступны в запросе
 func (s *Request) FileKeys() []string {
 	res := make([]string, 0, len(s.files))
 	for k := range s.files {
@@ -37,28 +40,33 @@ func (s *Request) FileKeys() []string {
 	return res
 }
 
+// File возвращает файл, принятый в запросе multipart/form-data, если он существует
 func (s *Request) File(name string) (io.Reader, bool) {
 	r, check := s.files[name]
 	return r, check
 }
 
-// Auth returns auth data
+// Auth возвращает словарь с данными об авторизации (при условии, что в заголовке Bearer указан токен и его удалось успешно расшифровать и распарсить)
 func (s *Request) Auth() json.Map {
 	return s.auth
 }
 
+// IsAuth указывает, есть ли словарь авторизации или нет
 func (s *Request) IsAuth() bool {
 	return s.auth != nil
 }
 
+// IsJSON возарвщвет true, если в заголовке ContentType application/json
 func (s *Request) IsJSON() bool {
 	return s.Header.Get("Content-Type") == "application/json"
 }
 
+// // IsForm возарвщвет true, если в заголовке ContentType multipart/form-data
 func (s *Request) IsForm() bool {
 	return s.Header.Get("Content-Type") == "application/x-www-form-urlencoded"
 }
 
+// Close закрывает все открытые ресурсы запоса (файлы)
 func (s *Request) Close() {
 	for _, file := range s.files {
 		file.Close()

+ 7 - 1
response.go

@@ -20,6 +20,7 @@ func NewResponse() *Response {
 	}
 }
 
+// Response реализует объект ответа
 type Response struct {
 	data    mjson.Map
 	files   map[string]io.ReadCloser
@@ -28,25 +29,30 @@ type Response struct {
 	code    int
 }
 
+// KeySet устанавливает значение в словаре ответа по ключу
 func (s *Response) KeySet(key string, val any) {
 	s.data[key] = val
 }
 
+// FileSet устанавливает файл в словаре файлов по ключу
 func (s *Response) FileSet(name string, file io.ReadCloser) {
 	s.files[name] = file
 }
 
+// SetError устанавливает ошибку в ответе (http код и текст ошибки)
 func (s *Response) SetError(code int, err error) {
 	s.code = code
 	s.err = err
 }
 
+// Close закрывает ресурсы ответа после завершения отдачи серверу
 func (s *Response) Close() {
 	for _, file := range s.files {
 		file.Close()
 	}
 }
 
+// Send отправляет запрос серверу
 func (s *Response) Send(w http.ResponseWriter) error {
 	log.Println("SEND...")
 	defer s.Close()
@@ -105,7 +111,7 @@ func (s *Response) Send(w http.ResponseWriter) error {
 	return nil
 }
 
-///////////////////////////////////////////
+// Конструкторы ошибок /////////////////////////////////////////
 
 func ResponseError(err string, args mjson.Map, code int) *Response {
 	return &Response{

+ 35 - 37
rest.go

@@ -17,20 +17,16 @@ import (
 	jwt "github.com/dgrijalva/jwt-go"
 )
 
-func New(addr string, secret []byte, app IApp) *Rest {
+func New(app IApplication) *Rest {
 	return &Rest{
-		app:    app,
-		secret: secret,
-		addr:   addr,
+		app: app,
 	}
 }
 
 type Rest struct {
-	secret []byte
-	addr   string
 	opened atomic.Bool
 	server *http.Server
-	app    IApp
+	app    IApplication
 }
 
 func (s *Rest) TokenGenerate(m mjson.Map, expire int64) (string, error) {
@@ -44,7 +40,7 @@ func (s *Rest) TokenGenerate(m mjson.Map, expire int64) (string, error) {
 		claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
 	}
 
-	tokenString, err := token.SignedString(s.secret)
+	tokenString, err := token.SignedString(s.app.Secret())
 	return tokenString, err
 }
 
@@ -58,7 +54,7 @@ func (s *Rest) Listen(timeout time.Duration) (err error) {
 		mux := http.NewServeMux()
 		mux.HandleFunc("/", s.handle)
 		s.server = &http.Server{
-			Addr:    s.addr,
+			Addr:    s.app.Addr(),
 			Handler: mux,
 		}
 		err = s.server.ListenAndServe()
@@ -105,7 +101,7 @@ func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
 				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 					return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 				}
-				return s.secret, nil
+				return s.app.Secret(), nil
 			})
 
 			if err != nil {
@@ -158,34 +154,36 @@ func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
 
 	// get command
 
-	//command, check := s.commands.GetCommand(r.URL.Path)
-	command, check := s.app.Executer(rr)
-	if !check {
-		responseNotFound(w)
-		return
-	}
-
-	// serialize
-	if err := serializeRequest(command, rr.data, rr.files, ""); err != nil {
-		responseError(w, err, 500)
-		return
-	}
-
-	// validate
-	if validator, check := command.(IValidator); check {
-		resp := validator.Validate(rr)
-		if resp != nil {
-			if err := resp.Send(w); err != nil {
-				responseError(w, err, 500)
-			}
+	/*
+		//command, check := s.commands.GetCommand(r.URL.Path)
+		command, check := s.app.Executer(rr)
+		if !check {
+			responseNotFound(w)
 			return
 		}
-	}
 
-	// execute
-	resp := command.Execute(rr)
-	if err := resp.Send(w); err != nil {
-		responseError(w, err, 500)
-	}
-	resp.Close()
+				// serialize
+				if err := serializeRequest(command, rr.data, rr.files, ""); err != nil {
+					responseError(w, err, 500)
+					return
+				}
+
+				// validate
+				if validator, check := command.(IValidator); check {
+					resp := validator.Validate(rr)
+					if resp != nil {
+						if err := resp.Send(w); err != nil {
+							responseError(w, err, 500)
+						}
+						return
+					}
+				}
+
+				// execute
+				resp := command.Execute(rr)
+				if err := resp.Send(w); err != nil {
+					responseError(w, err, 500)
+				}
+			resp.Close()
+	*/
 }

+ 117 - 59
serialize.go

@@ -1,84 +1,142 @@
 package rest
 
 import (
-	"io"
-	"log"
+	"fmt"
 	"reflect"
 	"strings"
-
-	"git.ali33.ru/fcg-xvii/go-tools/json"
 )
 
-func findFileKey(m map[string]io.ReadCloser, key string) (io.Reader, bool) {
-	key = camelToSnake(key)
-	for fKey, val := range m {
-		fKey = strings.Replace(fKey, ".", "_", -1)
-		if key == fKey {
-			return val, true
-		}
+func parseVal(from, to reflect.Value, fieldName string) IErrorArgs {
+	if from.Kind() == reflect.Interface {
+		from = from.Elem()
+	}
+	if !from.CanConvert(to.Type()) {
+		return NewErrFieldType(fieldName, from.Type().String(), to.Type().String(), "unexpected type", -1)
+	}
+	to.Set(from.Convert(to.Type()))
+	return nil
+}
+
+func parseType(from, to reflect.Value, fieldName string) IErrorArgs {
+	from = realValue(from)
+	to = realValue(to)
+	switch to.Kind() {
+	case reflect.Interface:
+		to.Set(from)
+		return nil
+	case reflect.Struct:
+		return parseStruct(from, to, fieldName)
+	case reflect.Slice:
+		return parseSlice(from, to, fieldName)
+	case reflect.Map:
+		return parseMap(from, to, fieldName)
+	default:
+		return parseVal(from, to, fieldName)
 	}
-	return nil, false
 }
 
-func findKey(m map[string]any, key string) (val any, check bool) {
-	if val, check = m[key]; !check {
-		if !check {
-			key = camelToSnake(key)
-			val, check = m[key]
+func realValue(val reflect.Value) reflect.Value {
+	//log.Println(val.IsNil())
+	if val.Kind() == reflect.Ptr {
+		if val.IsNil() {
+			val.Set(reflect.New(val.Type().Elem()))
+		}
+		val = val.Elem()
+		if val.Kind() == reflect.Ptr {
+			return realValue(val)
 		}
 	}
-	return val, check
+	return val
 }
 
-func serializeRequest(obj any, data json.Map, files map[string]io.ReadCloser, parent string) IErrorArgs {
-	data.LogPretty()
-	val := reflect.ValueOf(obj)
-	if val.Kind() != reflect.Ptr {
-		return NewErrPrtexpected(parent)
+func parseSlice(from, to reflect.Value, fieldName string) IErrorArgs {
+	if to.IsNil() {
+		to.Set(reflect.MakeSlice(to.Type(), 0, from.Cap()))
+	}
+	eType := to.Type().Elem()
+	// Перебираем элементы среза, откуда копируем.
+	for i := 0; i < from.Len(); i++ {
+		iFieldName := fmt.Sprintf("%v[%v]", fieldName, i)
+		fromVal := from.Index(i)
+		toVal := reflect.New(eType).Elem()
+		if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
+			return err
+		}
+		to.Set(reflect.Append(to, toVal))
 	}
-	el := val.Elem()
-	for i := 0; i < el.NumField(); i++ {
-		required, isFile, arrived := false, false, false
-		field := el.Type().Field(i)
-		log.Println("------>", field)
-		xField := el.Field(i)
-		tag := field.Tag.Get("rest")
+	return nil
+}
+
+func parseStruct(from, to reflect.Value, fieldName string) IErrorArgs {
+	for i := 0; i < to.NumField(); i++ {
+		required := false
+		// устанавливаем тип и указатель поля объекта, куда копируем
+		fType := to.Type().Field(i)
+		fVal := to.Field(i)
+		// проверяем тег, если он есть
+		tag := fType.Tag.Get("rest")
 		if len(tag) > 0 {
 			required = strings.Contains(tag, "required")
-			isFile = strings.Contains(tag, "file")
 		}
-		if isFile {
-			if f, check := findFileKey(files, field.Name); check {
-				xField.Set(reflect.ValueOf(f))
-				arrived = true
+		var rVal reflect.Value
+		switch from.Kind() {
+		case reflect.Struct:
+			if rVal = from.FieldByName(fType.Name); !rVal.IsValid() {
+				rVal = from.FieldByName(camelToSnake(fType.Name))
 			}
-		} else {
-			if cval, check := findKey(data, field.Name); check {
-				rVal := reflect.ValueOf(cval)
-				switch {
-				case xField.Kind() == reflect.Ptr:
-					if rVal.Kind() != reflect.Map {
-						return NewErrFieldType(field.Name, xField.Type().String(), rVal.String(), "expected json object")
-					}
-					rv := reflect.New(xField.Type().Elem())
-					m := rVal.Convert(reflect.TypeOf(json.Map{})).Interface()
-					if err := serializeRequest(rv.Interface(), m.(json.Map), files, field.Name); err != nil {
-						return err
-					}
-					xField.Set(rv)
-				default:
-					if rVal.CanConvert(xField.Type()) {
-						xField.Set(rVal.Convert(xField.Type()))
-					} else {
-						return NewErrFieldType(field.Name, xField.Type().String(), rVal.Kind().String(), "convert error")
-					}
-				}
-				arrived = true
+		case reflect.Map:
+			if rVal = from.MapIndex(reflect.ValueOf(fType.Name)); !rVal.IsValid() {
+				rVal = from.MapIndex(reflect.ValueOf(camelToSnake(fType.Name)))
 			}
+		default:
+			return NewErrFieldType(fieldName, from.Type().String(), to.Type().String(), "unexpected type", -1)
+
+		}
+		if !rVal.IsValid() {
+			if required {
+				return NewErrorMessage("RequiredField", fType.Name)
+			}
+			continue
+		}
+		if rVal.Kind() == reflect.Interface {
+			rVal = rVal.Elem()
 		}
-		if required && !arrived {
-			return NewErrFieldRequired(field.Name)
+		iFieldName := fmt.Sprintf("%v.%v", fieldName, fType.Name)
+		if err := parseType(rVal, fVal.Addr(), iFieldName); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func parseMap(from, to reflect.Value, fieldName string) IErrorArgs {
+	if to.IsNil() {
+		to.Set(reflect.MakeMap(to.Type()))
+	}
+	switch from.Kind() {
+	case reflect.Map:
+		for _, key := range from.MapKeys() {
+			iFieldName := fmt.Sprintf("%v[%v]", fieldName, key)
+			fromVal := from.MapIndex(key)
+			toVal := reflect.New(to.Type().Elem()).Elem()
+			if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
+				return err
+			}
+			to.SetMapIndex(key, toVal)
+		}
+	case reflect.Struct:
+		for i := 0; i < from.NumField(); i++ {
+			iFieldName := fmt.Sprintf("%v.%v", fieldName, from.Type().Field(i).Name)
+			fromVal := from.Field(i)
+			toVal := reflect.New(to.Type().Elem()).Elem()
+			if err := parseType(fromVal, toVal.Addr(), iFieldName); err != nil {
+				return err
+			}
+			fName := camelToSnake(from.Type().Field(i).Name)
+			to.SetMapIndex(reflect.ValueOf(fName), toVal)
 		}
+	default:
+		return NewErrFieldType(fieldName, from.Type().String(), to.Type().String(), "unexpected type", -1)
 	}
 	return nil
 }

+ 10 - 6
tests/app_avatar_test.go

@@ -19,6 +19,14 @@ type AvatarApp struct {
 	imagePath string
 }
 
+func (s *AvatarApp) Addr() string {
+	return ":7000"
+}
+
+func (s *AvatarApp) Secret() []byte {
+	return []byte("my-top-secret-123456-$$")
+}
+
 // ImagePath формирует путь к файлу аватара пользователя по его хэшу
 func (s *AvatarApp) ImagePath(hash string) string {
 	return fmt.Sprintf("%s/%s.jpg", s.imagePath, hash)
@@ -134,15 +142,11 @@ func (s *RequestUpload) Execute(r *rest.Request) *rest.Response {
 /////////////////////////////////////////////////////////////////
 
 func RestNew() {
-	// адрес и порт веб сервера
-	addr := ":7000"
-	// ключ шифрования токена авторизации
-	secret := []byte("top-secret")
-	// люъект приложения (rest.IApp)
+	// объект приложения (rest.IApp)
 	app := &AvatarApp{}
 
 	// создание сервера
-	rest := rest.New(addr, secret, app)
+	rest := rest.New(app)
 
 	// запуск
 	if err := rest.Listen(time.Second); err != nil {

+ 145 - 0
z_test.go

@@ -0,0 +1,145 @@
+package rest
+
+import (
+	"fmt"
+	"log"
+	"reflect"
+	"testing"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+type Child struct {
+	ID   int64
+	Name string
+}
+
+func NewTFielder() *TFielder {
+	res := &TFielder{}
+	res.Fielder = NewFielder(res)
+	return res
+}
+
+type TFielder struct {
+	*Fielder
+	ID       int64
+	Name     string
+	NumArr   []int64
+	FloatArr []float64
+	StrArr   []string
+	AnyArr   []any
+	ChildArr []*Child
+	//MapInt   map[string]int64
+}
+
+func TestParse(t *testing.T) {
+	// int8 в int64
+	lInt, rInt := byte(10), int64(0)
+	if err := parseVal(reflect.ValueOf(lInt), reflect.ValueOf(&rInt), "int"); err != nil {
+		t.Fatal(err)
+	}
+	// из строки в строку
+	lStr, rStr := "copy message", ""
+	if err := parseVal(reflect.ValueOf(lStr), reflect.ValueOf(&rStr), "string"); err != nil {
+		t.Fatal(err)
+	}
+	// массивы
+	lArr := []any{"one", "two"}
+	rArr := []string{}
+	if err := parseSlice(reflect.ValueOf(lArr), reflect.ValueOf(&rArr), "slice"); err != nil {
+		t.Fatal(err)
+	}
+	// map -> объект
+	lMap := json.Map{
+		"id":      10,
+		"name":    "Fielder-Name",
+		"num_arr": []any{1, 2, 3, 4, 5},
+		"any_arr": []any{"one", "two", 3, 4},
+		"child_arr": []json.Map{
+			{
+				"id":   11,
+				"name": "Name-77",
+			},
+		},
+	}
+	var rObj TFielder
+	if err := parseStruct(reflect.ValueOf(lMap), reflect.ValueOf(&rObj), "struct"); err != nil {
+		t.Fatal(err)
+	}
+	log.Println(rObj)
+}
+
+func TestParse2(t *testing.T) {
+	//var val []int
+	//val = 20
+	val := []any{1, 2, 3, 4, 5}
+	log.Println(val)
+	var val2 []*float64
+	//val2 := []int{1, 2, 3}
+	if err := parseType(reflect.ValueOf(&val), reflect.ValueOf(&val2), "val"); err != nil {
+		t.Fatal(err)
+	}
+	log.Println("KK", val2, val)
+	for _, p := range val2 {
+		t.Log(*p)
+	}
+}
+
+func TestParseMap(t *testing.T) {
+	//var val map[string]int
+	//val := map[string]int{"a": 1, "b": 2, "c": 3}
+	val := struct {
+		Name   string
+		AgeNum int
+	}{
+		Name:   "Иван",
+		AgeNum: 30,
+	}
+	var val2 map[string]any
+	if err := parseType(reflect.ValueOf(&val), reflect.ValueOf(&val2), "val"); err != nil {
+		t.Fatal(err)
+	}
+	log.Println("KK", val2, val)
+}
+
+type Person struct {
+	Name   *string
+	AgeNum int
+	Child  *Person
+	Childs []*Person
+}
+
+func (s *Person) String() string {
+	return fmt.Sprintf("Person{Name: %v, AgeNum: %d, Child: %v}", *s.Name, s.AgeNum, s.Child)
+}
+
+func TestParseStruct(t *testing.T) {
+	val := json.Map{
+		"name":    "Иван",
+		"age_num": 30,
+		"child": json.Map{
+			"name":    "OOO",
+			"age_num": 10,
+		},
+		"childs": []json.Map{
+			{
+				"name":    "OOO",
+				"age_num": 10,
+			},
+			{
+				"name":    "OOO1",
+				"age_num": 10,
+			},
+			{
+				"name":    "OOO2",
+				"age_num": 10,
+			},
+		},
+	}
+	var val2 Person
+	if err := parseType(reflect.ValueOf(&val), reflect.ValueOf(&val2), "val"); err != nil {
+		t.Fatal(err)
+	}
+	t.Log(val2)
+	t.Log(*val2.Name)
+}