0x4a52466c696e74 před 6 měsíci
revize
0462aabc10
13 změnil soubory, kde provedl 1039 přidání a 0 odebrání
  1. 0 0
      README.md
  2. 39 0
      case.go
  3. 68 0
      command.go
  4. 42 0
      command_store.go
  5. 11 0
      errors.go
  6. 88 0
      fielder.go
  7. 8 0
      go.mod
  8. 4 0
      go.sum
  9. 65 0
      request.go
  10. 146 0
      response.go
  11. 180 0
      rest.go
  12. 271 0
      z_test.go
  13. 117 0
      z_test_api.go

+ 0 - 0
README.md


+ 39 - 0
case.go

@@ -0,0 +1,39 @@
+package rest
+
+import (
+	"bytes"
+	"strings"
+	"unicode"
+)
+
+// CamelToSnake преобразует строку из CamelCase в snake_case.
+func CamelToSnake(s string) string {
+	var result bytes.Buffer
+	var lastIndex int = 0
+
+	for i, char := range s {
+		if i > 0 && unicode.IsUpper(char) && ((i+1 < len(s) && unicode.IsLower(rune(s[i+1]))) || unicode.IsLower(rune(s[i-1]))) {
+			result.WriteString(strings.ToLower(s[lastIndex:i]))
+			if lastIndex != i {
+				result.WriteString("_")
+			}
+			lastIndex = i
+		}
+	}
+	result.WriteString(strings.ToLower(s[lastIndex:]))
+
+	return result.String()
+}
+
+// SnakeToCamel преобразует строку из snake_case в CamelCase.
+func SnakeToCamel(s string) string {
+	var result string
+	words := strings.Split(s, "_")
+
+	for _, word := range words {
+		w := strings.Title(word)
+		result += w
+	}
+
+	return result
+}

+ 68 - 0
command.go

@@ -0,0 +1,68 @@
+package rest
+
+type IValidator interface {
+	Validate(r *Request) *Response
+}
+
+type IExecuter interface {
+	Execute(r *Request) *Response
+}
+
+type ICommand interface {
+	IValidator
+	IExecuter
+}
+
+///////////////////////////////////////////////
+
+func NewCommand(validator IValidator, executer IExecuter) *Command {
+	return &Command{
+		validator,
+		executer,
+	}
+}
+
+type Command struct {
+	validator IValidator
+	executer  IExecuter
+}
+
+func (s *Command) Validate(r *Request) *Response {
+	return s.validator.Validate(r)
+}
+
+func (s *Command) Execute(r *Request) *Response {
+	return s.executer.Execute(r)
+}
+
+///////////////////////////////////////////////
+
+func NewValidator(method func(r *Request) *Response) *Validator {
+	return &Validator{
+		method,
+	}
+}
+
+type Validator struct {
+	method func(r *Request) *Response
+}
+
+func (s *Validator) Validate(r *Request) *Response {
+	return s.method(r)
+}
+
+///////////////////////////////////////////////
+
+func NewExecuter(method func(r *Request) *Response) *Executer {
+	return &Executer{
+		method,
+	}
+}
+
+type Executer struct {
+	method func(r *Request) *Response
+}
+
+func (s *Executer) Execute(r *Request) *Response {
+	return s.method(r)
+}

+ 42 - 0
command_store.go

@@ -0,0 +1,42 @@
+package rest
+
+import "sync"
+
+func NewCommandStore() *CommandStore {
+	return &CommandStore{
+		commands: make(map[string]ICommand),
+		locker:   &sync.RWMutex{},
+	}
+}
+
+type CommandStore struct {
+	commands map[string]ICommand
+	locker   *sync.RWMutex
+}
+
+func (s *CommandStore) AddCommand(uri string, command ICommand) {
+	s.locker.Lock()
+	s.commands[uri] = command
+	s.locker.Unlock()
+}
+
+func (s *CommandStore) AddCommandObjects(uri string, validator IValidator, executor IExecuter) {
+	s.AddCommand(uri, NewCommand(validator, executor))
+}
+
+func (s *CommandStore) GetCommand(uri string) (ICommand, bool) {
+	s.locker.RLock()
+	command, check := s.commands[uri]
+	s.locker.RUnlock()
+	return command, check
+}
+
+func (s *CommandStore) AddCommandMethods(uri string, validate func(*Request) *Response, execute func(*Request) *Response) {
+	s.AddCommand(
+		uri,
+		NewCommand(
+			NewValidator(validate),
+			NewExecuter(execute),
+		),
+	)
+}

+ 11 - 0
errors.go

@@ -0,0 +1,11 @@
+package rest
+
+import "errors"
+
+var (
+	ErrAlreadyOpened  = errors.New("AlreadyOpened")
+	ErrNotOpened      = errors.New("NotOpened")
+	ErrKeyNotExists   = errors.New("KeyNotExists")
+	ErrKeyInvalidType = errors.New("KeyInvalidType")
+	ErrNotFound       = errors.New("ErrNotFound")
+)

+ 88 - 0
fielder.go

@@ -0,0 +1,88 @@
+package rest
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+type IFielder interface {
+	Fields(...any) (json.Map, error)
+}
+
+type IFieldChecker interface {
+	FieldCheck(string) bool
+}
+
+/////////////////////////////////////////////////
+
+func NewFielder(source any) *Fielder {
+	res := &Fielder{
+		source: source,
+		fields: make(map[string]reflect.Value),
+	}
+	res._Prepare()
+	return res
+}
+
+type Fielder struct {
+	source  any
+	fields  map[string]reflect.Value
+	checker IFieldChecker
+}
+
+func (s *Fielder) _Prepare() {
+	s.checker, _ = s.source.(IFieldChecker)
+	val := reflect.ValueOf(s.source).Elem()
+	for i := 0; i < val.NumField(); i++ {
+		fName := val.Type().Field(i)
+		field := val.Field(i)
+		s.fields[CamelToSnake(fName.Name)] = field
+	}
+}
+
+func (s *Fielder) Fields(names ...any) (json.Map, error) {
+	res := make(json.Map)
+	for _, rName := range names {
+		var name string
+		var fields []any
+		if sName, check := rName.(string); check {
+			name = sName
+		} else if sMap, check := rName.(map[string]any); check {
+			name = fmt.Sprint(sMap["name"])
+			if rFields, check := sMap["fields"]; check {
+				if rxFields, check := rFields.([]any); check {
+					fields = rxFields
+				}
+			}
+		}
+		permission := true
+		if s.checker != nil {
+			permission = s.checker.FieldCheck(name)
+		}
+		val, check := s.fields[name]
+		if check && permission {
+			iface := val.Interface()
+			if val.Kind() == reflect.Ptr {
+				if val.IsNil() {
+					res[name] = nil
+				} else {
+					if fielder, check := iface.(IFielder); check {
+						xVal, err := fielder.Fields(fields...)
+						if err != nil {
+							return nil, err
+						}
+						res[name] = xVal
+					}
+				}
+			} else {
+				res[name] = val.Interface()
+			}
+		} else {
+			return nil, errors.New(name)
+		}
+	}
+	return res, nil
+}

+ 8 - 0
go.mod

@@ -0,0 +1,8 @@
+module git.ali33.ru/fcg-xvii/rest
+
+go 1.20
+
+require (
+	git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91 // indirect
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+)

+ 4 - 0
go.sum

@@ -0,0 +1,4 @@
+git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91 h1:8N3j1V1Yx24uHwCp+LPOOdzdoRq3ad9tEIchHd6CZUI=
+git.ali33.ru/fcg-xvii/go-tools v0.0.0-20230529104008-2552c5121c91/go.mod h1:YbBhWFFNNQIKcRisQFnpVaN5KA+XHGImSU1Z/MuntqU=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=

+ 65 - 0
request.go

@@ -0,0 +1,65 @@
+package rest
+
+import (
+	"io"
+	"net/http"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+type Request struct {
+	*http.Request
+	auth           json.Map
+	data           json.Map
+	files          map[string]io.ReadCloser
+	tokenGenerator func(json.Map, int64) (string, error)
+}
+
+func (s *Request) GenerateToken(data json.Map, expire int64) (string, error) {
+	return s.tokenGenerator(data, expire)
+}
+
+func (s *Request) RPath() string {
+	return s.URL.Path
+}
+
+func (s *Request) Data() json.Map {
+	return s.data
+}
+
+// file keys
+func (s *Request) FileKeys() []string {
+	res := make([]string, 0, len(s.files))
+	for k := range s.files {
+		res = append(res, k)
+	}
+	return res
+}
+
+func (s *Request) File(name string) (io.Reader, bool) {
+	r, check := s.files[name]
+	return r, check
+}
+
+// Auth returns auth data
+func (s *Request) Auth() json.Map {
+	return s.auth
+}
+
+func (s *Request) IsAuth() bool {
+	return s.auth != nil
+}
+
+func (s *Request) IsJSON() bool {
+	return s.Header.Get("Content-Type") == "application/json"
+}
+
+func (s *Request) IsForm() bool {
+	return s.Header.Get("Content-Type") == "application/x-www-form-urlencoded"
+}
+
+func (s *Request) Close() {
+	for _, file := range s.files {
+		file.Close()
+	}
+}

+ 146 - 0
response.go

@@ -0,0 +1,146 @@
+package rest
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+
+	mjson "git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+func NewResponse() *Response {
+	return &Response{
+		data:  make(mjson.Map),
+		files: make(map[string]io.ReadCloser),
+	}
+}
+
+type Response struct {
+	data    mjson.Map
+	files   map[string]io.ReadCloser
+	err     error
+	errArgs mjson.Map
+	code    int
+}
+
+func (s *Response) KeySet(key string, val any) {
+	s.data[key] = val
+}
+
+func (s *Response) FileSet(name string, file io.ReadCloser) {
+	s.files[name] = file
+}
+
+func (s *Response) SetError(code int, err error) {
+	s.code = code
+	s.err = err
+}
+
+func (s *Response) Close() {
+	for _, file := range s.files {
+		file.Close()
+	}
+}
+
+func (s *Response) Send(w http.ResponseWriter) error {
+	log.Println("SEND...")
+	defer s.Close()
+	if s.err != nil {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(s.code)
+		body := mjson.Map{
+			"error": s.err.Error(),
+			"args":  s.errArgs,
+		}
+		w.Write(body.JSON())
+		return nil
+	}
+	// Если есть файлы, то используем multipart
+	if len(s.files) > 0 {
+		var b bytes.Buffer
+		writer := multipart.NewWriter(&b)
+
+		// Добавляем JSON-данные как часть
+		partHeader := make(textproto.MIMEHeader)
+		partHeader.Set("Content-Type", "application/json")
+		partHeader.Set("Content-Disposition", `form-data; name="data"`)
+		dataPart, err := writer.CreatePart(partHeader)
+		if err != nil {
+			return err
+		}
+		if err := json.NewEncoder(dataPart).Encode(s.data); err != nil {
+			return err
+		}
+
+		// Добавляем файлы
+		for filename, file := range s.files {
+			part, err := writer.CreateFormFile("file", filename)
+			if err != nil {
+				return err
+			}
+			if _, err := io.Copy(part, file); err != nil {
+				return err
+			}
+		}
+
+		// Закрываем multipart writer
+		if err := writer.Close(); err != nil {
+			return err
+		}
+
+		// Отправляем multipart response
+		w.Header().Set("Content-Type", writer.FormDataContentType())
+		w.Write(b.Bytes())
+	} else {
+		// Если нет файлов, просто отправляем JSON
+		w.Header().Set("Content-Type", "application/json")
+		return json.NewEncoder(w).Encode(s.data)
+	}
+
+	return nil
+}
+
+///////////////////////////////////////////
+
+func ResponseError(err error, args mjson.Map, code int) *Response {
+	return &Response{
+		code:    code,
+		err:     err,
+		errArgs: args,
+	}
+}
+
+///////////////////////////////////////////
+
+func ResponseErrKeyNotExists(key string) *Response {
+	return ResponseError(
+		ErrKeyNotExists,
+		mjson.Map{
+			"key": key,
+		},
+		500,
+	)
+}
+
+func ResponseErrKeyInvalidType(key, kType string) *Response {
+	return ResponseError(
+		ErrKeyInvalidType,
+		mjson.Map{
+			"key":  key,
+			"type": kType,
+		},
+		500,
+	)
+}
+
+func ResponseNotFound(data mjson.Map) *Response {
+	return &Response{
+		code:    404,
+		err:     ErrNotFound,
+		errArgs: data,
+	}
+}

+ 180 - 0
rest.go

@@ -0,0 +1,180 @@
+package rest
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	mjson "git.ali33.ru/fcg-xvii/go-tools/json"
+	jwt "github.com/dgrijalva/jwt-go"
+)
+
+func NewRest(addr string, secret []byte, commands *CommandStore) *Rest {
+	return &Rest{
+		commands: commands,
+		secret:   secret,
+		addr:     addr,
+	}
+}
+
+type Rest struct {
+	secret   []byte
+	addr     string
+	opened   atomic.Bool
+	server   *http.Server
+	commands *CommandStore
+}
+
+func (s *Rest) TokenGenerate(m mjson.Map, expire int64) (string, error) {
+	token := jwt.New(jwt.SigningMethodHS256)
+
+	claims := token.Claims.(jwt.MapClaims)
+	for key, val := range m {
+		claims[key] = val
+	}
+	if expire > 0 {
+		claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
+	}
+
+	tokenString, err := token.SignedString(s.secret)
+	return tokenString, err
+}
+
+// Listen start server in other goroutine
+func (s *Rest) Listen(timeout time.Duration) (err error) {
+	if s.opened.Swap(true) {
+		return ErrAlreadyOpened
+	}
+	ctx, _ := context.WithTimeout(context.Background(), timeout)
+	go func() {
+		mux := http.NewServeMux()
+		mux.HandleFunc("/", s.handle)
+		s.server = &http.Server{
+			Addr:    s.addr,
+			Handler: mux,
+		}
+		err = s.server.ListenAndServe()
+		s.opened.Store(false)
+	}()
+	<-ctx.Done()
+	return
+}
+
+// Close
+func (s *Rest) Close() error {
+	if !s.opened.Load() {
+		return ErrNotOpened
+	}
+	return s.server.Close()
+}
+
+func responseNotFound(w http.ResponseWriter) {
+	w.WriteHeader(404)
+}
+
+func responseError(w http.ResponseWriter, err error, code int) {
+	w.WriteHeader(code)
+	w.Write([]byte(err.Error()))
+}
+
+// handle
+func (s *Rest) handle(w http.ResponseWriter, r *http.Request) {
+	log.Println("handle", r.URL.Path)
+	// Инициализация restRequest
+	rr := &Request{
+		Request:        r,
+		data:           mjson.Map{},
+		files:          make(map[string]io.ReadCloser),
+		tokenGenerator: s.TokenGenerate,
+	}
+
+	// Парсим Bearer токен и извлекаем claims
+	authHeader := r.Header.Get("Authorization")
+	if authHeader != "" {
+		if parts := strings.Split(authHeader, " "); len(parts) == 2 && parts[0] == "Bearer" {
+			tokenString := parts[1]
+			token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+					return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+				}
+				return s.secret, nil
+			})
+
+			if err != nil {
+				log.Printf("Failed to parse JWT: %s", err)
+				http.Error(w, "Invalid token", http.StatusUnauthorized)
+				return
+			}
+
+			if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+				rr.auth = mjson.Map(claims)
+			}
+		}
+	}
+
+	// Если это многокомпонентный запрос, обрабатываем файлы
+	if strings.Index(r.Header.Get("Content-Type"), "multipart/form-data") == 0 {
+		err := r.ParseMultipartForm(32 << 20) // max memory 32MB, после этого файлы будут сохранены во временных файлах
+		if err != nil {
+			responseError(w, fmt.Errorf("failed to parse multipart form: %w", err), 500)
+			return
+		}
+		multiPartForm := r.MultipartForm
+
+		data, check := multiPartForm.Value["data"]
+		if check {
+			err := json.NewDecoder(bytes.NewBuffer([]byte(data[0]))).Decode(&rr.data)
+			if err != nil {
+				responseError(w, fmt.Errorf("failed to decode JSON: %w", err), 500)
+				return
+			}
+		}
+
+		for filename, headers := range multiPartForm.File {
+			for _, header := range headers {
+				file, err := header.Open()
+				if err != nil {
+					responseError(w, fmt.Errorf("failed to open file %s: %w", filename, err), 500)
+				}
+				rr.files[filename] = file
+			}
+		}
+		defer rr.Close()
+	} else {
+		err := json.NewDecoder(r.Body).Decode(&rr.data)
+		if err != nil {
+			responseError(w, fmt.Errorf("failed to decode JSON: %w", err), 500)
+			return
+		}
+	}
+
+	// get command
+	command, check := s.commands.GetCommand(r.URL.Path)
+	if !check {
+		responseNotFound(w)
+		return
+	}
+
+	// validate
+	resp := command.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()
+}

+ 271 - 0
z_test.go

@@ -0,0 +1,271 @@
+package rest
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"net/textproto"
+	"testing"
+	"time"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+var (
+	AddrCorrect   = "localhost:7000"
+	AddrIncorrect = "1.1.1.1:7000"
+	Secret        = []byte("itsMyWay")
+)
+
+func URL(path string) string {
+	return fmt.Sprintf("http://%s%s", AddrCorrect, path)
+}
+
+func sendRequest(uri string, data json.Map, files map[string]io.ReadCloser, auth string) ([]byte, error) {
+	log.Println(uri)
+	body := &bytes.Buffer{}
+	var contentType string
+
+	if len(files) == 0 {
+		// Если файлов нет, используем application/json
+		contentType = "application/json"
+		jsonData, err := json.Marshal(data)
+		if err != nil {
+			return nil, err
+		}
+		body.Write(jsonData)
+	} else {
+		// Если есть файлы, используем multipart/form-data
+		writer := multipart.NewWriter(body)
+
+		// Прикрепление файлов
+		for fieldName, file := range files {
+			part, err := writer.CreateFormFile(fieldName, fieldName)
+			if err != nil {
+				return nil, err
+			}
+			_, err = io.Copy(part, file)
+			if err != nil {
+				return nil, err
+			}
+			file.Close()
+		}
+
+		// Прикрепление JSON как части с Content-Type application/json
+		jsonHeader := make(textproto.MIMEHeader)
+		jsonHeader.Set("Content-Disposition", `form-data; name="data"`)
+		jsonHeader.Set("Content-Type", "application/json")
+		jsonPart, err := writer.CreatePart(jsonHeader)
+		if err != nil {
+			return nil, err
+		}
+		jsonPart.Write(data.JSON())
+
+		err = writer.Close()
+		if err != nil {
+			return nil, err
+		}
+
+		contentType = writer.FormDataContentType()
+	}
+
+	req, err := http.NewRequest("POST", uri, body)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(auth) > 0 {
+		req.Header.Add("Authorization", "Bearer "+auth)
+	}
+
+	req.Header.Add("Content-Type", contentType)
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	rBody, _ := ioutil.ReadAll(resp.Body)
+
+	if resp.StatusCode != http.StatusOK {
+		err = fmt.Errorf("failed sending request: %s", resp.Status)
+	}
+
+	return rBody, err
+}
+
+func initFile(data []byte) io.ReadCloser {
+	buf := bytes.NewBuffer(data)
+	return io.NopCloser(buf)
+}
+
+func TestListen(t *testing.T) {
+	commands := NewCommandStore()
+	rest := NewRest(AddrCorrect, Secret, commands)
+	err := rest.Listen(time.Second)
+	if err != nil {
+		t.Log("open", err, rest.opened.Load())
+		return
+	}
+	t.Log("close", rest.Close(), rest.opened.Load())
+}
+
+func TestRequest(t *testing.T) {
+	command := new(ObjectDemo)
+	commands := NewCommandStore()
+	commands.AddCommand("/demo", command)
+	commands.AddCommand("/json-debug", &JSONDebug{})
+	rest := NewRest(AddrCorrect, Secret, commands)
+	err := rest.Listen(time.Second)
+	if err != nil {
+		t.Log("open", err, rest.opened.Load())
+		return
+	}
+	defer rest.Close()
+	body, err := sendRequest(
+		URL("/demo"),
+		json.Map{
+			"get-one": true,
+			"get-two": false,
+		},
+		map[string]io.ReadCloser{
+			"test-file.txt": initFile([]byte("test file contents...")),
+		},
+		"",
+	)
+	t.Log(string(body), err)
+}
+
+func TestToken(t *testing.T) {
+	commands := NewCommandStore()
+	auth := &AuthDebug{}
+	commands.AddCommand("/register", auth)
+	commands.AddCommand("/login", auth)
+	rest := NewRest(AddrCorrect, Secret, commands)
+	err := rest.Listen(time.Second)
+	if err != nil {
+		t.Log("open", err, rest.opened.Load())
+		return
+	}
+	defer rest.Close()
+
+	// register
+	body, err := sendRequest(
+		URL("/register"),
+		json.Map{
+			"login": "test",
+		},
+		nil,
+		"",
+	)
+	if err != nil {
+		t.Fatal(err, string(body))
+	}
+	var jm json.Map
+	json.Unmarshal(body, &jm)
+	token := jm.StringVal("token", "")
+	t.Log("token", token)
+
+	// auth
+	body, err = sendRequest(
+		URL("/login"),
+		json.Map{},
+		nil,
+		token,
+	)
+	if err != nil {
+		t.Fatal(err, string(body))
+	}
+}
+
+func NewFielderTest(id, parentID int64, name string, price float64, child *FielderTest) *FielderTest {
+	f := &FielderTest{
+		ID:       id,
+		Password: "top-secret",
+		ParentID: parentID,
+		Name:     name,
+		Price:    price,
+	}
+	f.Fielder = NewFielder(f)
+	return f
+}
+
+type FielderTest struct {
+	*Fielder
+	ID       int64
+	Password string
+	ParentID int64
+	Name     string
+	Price    float64
+	Child    *FielderTest
+}
+
+func (s *FielderTest) FieldCheck(name string) bool {
+	if name == "password" {
+		return false
+	}
+	return true
+}
+
+func TestFielder(t *testing.T) {
+	f := NewFielderTest(1, 10, "VTBR", 100, nil)
+	f1 := NewFielderTest(10, 100, "SBER", 1000, nil)
+	f.Child = f1
+
+	fm, err := f.Fields(
+		"id",
+		"name",
+		"parent_id",
+		"price",
+		"password",
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	fm.LogPretty()
+
+	f.ID = 11
+
+	fm, err = f.Fields(
+		"id",
+		"name",
+		"parent_id",
+		"price",
+		map[string]any{
+			"name": "child",
+			"fields": []any{
+				"id",
+				"name",
+				"parent_id",
+				map[string]any{
+					"name": "child",
+					"fields": []any{
+						"id",
+					},
+				},
+			},
+		},
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	fm.LogPretty()
+}
+
+func TestCase(t *testing.T) {
+	// Тестирование функций
+	testCamel := "ParentID"
+	testSnake := "parent_id"
+
+	snake := CamelToSnake(testCamel)
+	camel := SnakeToCamel(testSnake)
+
+	println(testCamel, "->", snake)
+	println(testSnake, "->", camel)
+}

+ 117 - 0
z_test_api.go

@@ -0,0 +1,117 @@
+package rest
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"log"
+
+	"git.ali33.ru/fcg-xvii/go-tools/json"
+)
+
+type ObjectDemo struct{}
+
+func (s *ObjectDemo) Validate(r *Request) *Response {
+	key := r.data.KeysExists([]string{"get-one", "get-two"})
+	if key != "" {
+		return ResponseErrKeyNotExists(key)
+	}
+	if !r.data.IsBoolean("get-one") {
+		return ResponseErrKeyInvalidType("get-one", "boolean")
+	}
+	if !r.data.IsBoolean("get-two") {
+		return ResponseErrKeyInvalidType("get-two", "boolean")
+	}
+	return nil
+}
+
+func (s *ObjectDemo) Execute(r *Request) *Response {
+	if fKeys := r.FileKeys(); len(fKeys) > 0 {
+		log.Println("FILES", fKeys)
+		tFile, _ := r.File(fKeys[0])
+		fContent, _ := io.ReadAll(tFile)
+		log.Println("FILE CONTENT", string(fContent))
+	}
+	resp := NewResponse()
+	var fileNames []string
+	if r.data.Bool("get-one", true) {
+		fileNames = append(fileNames, "get-one.txt")
+		buf := bytes.NewBuffer([]byte("get-one txt file :)"))
+		resp.FileSet("get-one.txt", io.NopCloser(buf))
+	}
+	if r.data.Bool("get-two", true) {
+		fileNames = append(fileNames, "get-two.txt")
+		buf := bytes.NewBuffer([]byte("get-two txt file :)"))
+		resp.FileSet("get-two.txt", io.NopCloser(buf))
+	}
+	resp.KeySet("file_names", fileNames)
+	return resp
+}
+
+////////////////////////////////////////////////////
+
+type JSONDebug struct{}
+
+func (s *JSONDebug) Validate(r *Request) *Response {
+	return nil
+}
+
+func (s *JSONDebug) Execute(r *Request) *Response {
+	resp := NewResponse()
+	resp.KeySet("completed", true)
+	return resp
+}
+
+////////////////////////////////////////////////////
+
+type AuthDebug struct{}
+
+func (s *AuthDebug) Validate(r *Request) *Response {
+	if r.RPath() == "/register" {
+		data := r.Data()
+		if !data.KeyExists("login") {
+			return ResponseErrKeyNotExists("login")
+		}
+		return nil
+	} else if r.RPath() == "/login" {
+		if !r.IsAuth() {
+			return ResponseError(
+				errors.New("NotAuth"),
+				nil,
+				500,
+			)
+		}
+		return nil
+	}
+	return ResponseNotFound(nil)
+}
+
+func (s *AuthDebug) Execute(r *Request) *Response {
+	switch r.RPath() {
+	case "/register":
+		return s.execRegister(r)
+	case "/login":
+		return s.execLogin(r)
+	}
+	return nil
+}
+
+func (s *AuthDebug) execRegister(r *Request) *Response {
+	log.Println("REDISTER")
+	data := r.Data()
+	token, err := r.GenerateToken(json.Map{
+		"login": data.StringVal("login", ""),
+	}, 0)
+	if err != nil {
+		return ResponseError(err, nil, 500)
+	}
+	resp := NewResponse()
+	resp.KeySet("token", token)
+	return resp
+}
+
+func (s *AuthDebug) execLogin(r *Request) *Response {
+	log.Println("login", r.Auth())
+	resp := NewResponse()
+	return resp
+}