1
0
Fork 0
forked from goffee/core

initial commits 2

This commit is contained in:
Zeni Kim 2024-09-12 17:13:16 -05:00
parent 5475b7dd26
commit 7f38826b9c
39 changed files with 4525 additions and 0 deletions

22
LICENSE Normal file
View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2021 Harran Ali <harran.m@gmail.com>
Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

73
cache.go Normal file
View file

@ -0,0 +1,73 @@
package core
import (
"context"
"fmt"
"os"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
var ctx context.Context
type Cache struct {
redis *redis.Client
}
func NewCache(cacheConfig CacheConfig) *Cache {
ctx = context.Background()
dbStr := os.Getenv("REDIS_DB")
db64, err := strconv.ParseInt(dbStr, 10, 64)
if err != nil {
panic(fmt.Sprintf("error parsing redis db env var: %v", err))
}
db := int(db64)
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%v:%v", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")),
Password: os.Getenv("REDIS_PASSWORD"), // no password set
DB: db, // use default DB
})
_, err = rdb.Ping(ctx).Result()
if cacheConfig.EnableCache && err != nil {
panic(fmt.Sprintf("problem connecting to redis cache, (if it's not needed you can disable it in config/cache.go): %v", err))
}
return &Cache{
redis: rdb,
}
}
func (c *Cache) Set(key string, value string) error {
err := c.redis.Set(ctx, key, value, 0).Err()
if err != nil {
return err
}
return nil
}
func (c *Cache) SetWithExpiration(key string, value string, expiration time.Duration) error {
err := c.redis.Set(ctx, key, value, expiration).Err()
if err != nil {
return err
}
return nil
}
func (c *Cache) Get(key string) (string, error) {
result, err := c.redis.Get(ctx, key).Result()
if err != nil {
return "", err
}
return result, nil
}
func (c *Cache) Delete(key string) error {
err := c.redis.Del(ctx, key).Err()
if err != nil {
return err
}
return nil
}

27
config.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
type EnvFileConfig struct {
UseDotEnvFile bool
}
type RequestConfig struct {
MaxUploadFileSize int
}
type JWTConfig struct {
SecretKey string
Lifetime int
}
type GormConfig struct {
EnableGorm bool
}
type CacheConfig struct {
EnableCache bool
}

17
consts.go Normal file
View file

@ -0,0 +1,17 @@
package core
const GET string = "get"
const POST string = "post"
const DELETE string = "delete"
const PATCH string = "patch"
const PUT string = "put"
const OPTIONS string = "options"
const HEAD string = "head"
const CONTENT_TYPE string = "content-Type"
const CONTENT_TYPE_HTML string = "text/html; charset=utf-8"
const CONTENT_TYPE_JSON string = "application/json"
const CONTENT_TYPE_TEXT string = "text/plain"
const CONTENT_TYPE_MULTIPART_FORM_DATA string = "multipart/form-data;"
const LOCALHOST string = "http://localhost"
const TEST_STR string = "Testing!"
const PRODUCTION string = "production"

334
context.go Normal file
View file

@ -0,0 +1,334 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"syscall"
"git.smarteching.com/goffee/core/logger"
"gorm.io/gorm"
)
type Context struct {
Request *Request
Response *Response
GetValidator func() *Validator
GetJWT func() *JWT
GetGorm func() *gorm.DB
GetCache func() *Cache
GetHashing func() *Hashing
GetMailer func() *Mailer
GetEventsManager func() *EventsManager
GetLogger func() *logger.Logger
}
// TODO enhance
func (c *Context) DebugAny(variable interface{}) {
var formatted string
m := reflect.ValueOf(variable)
if m.Kind() == reflect.Pointer {
formatted = fmt.Sprintf("\n\nType: [pointer] %v (%v) \nMemory Address: %v \nValue: %v\n\n", m.Type(), m.Elem().Kind(), m, m.Elem())
} else {
formatted = fmt.Sprintf("\n\nType: %v (%v) \nValue: %v\n\n", m.Type(), m.Kind(), variable)
}
fmt.Println(formatted)
c.Response.HttpResponseWriter.Write([]byte(formatted))
}
func (c *Context) Next() {
ResolveApp().Next(c)
}
func (c *Context) prepare(ctx *Context) {
ctx.Request.httpRequest.ParseMultipartForm(int64(app.Config.Request.MaxUploadFileSize))
}
func (c *Context) GetPathParam(key string) interface{} {
return c.Request.httpPathParams.ByName(key)
}
func (c *Context) GetRequestParam(key string) interface{} {
return c.Request.httpRequest.FormValue(key)
}
func (c *Context) RequestParamExists(key string) bool {
return c.Request.httpRequest.Form.Has(key)
}
func (c *Context) GetHeader(key string) string {
return c.Request.httpRequest.Header.Get(key)
}
func (c *Context) GetUploadedFile(name string) *UploadedFileInfo {
file, fileHeader, err := c.Request.httpRequest.FormFile(name)
if err != nil {
panic(fmt.Sprintf("error with file,[%v]", err.Error()))
}
defer file.Close()
ext := strings.TrimPrefix(path.Ext(fileHeader.Filename), ".")
tmpFilePath := filepath.Join(os.TempDir(), fileHeader.Filename)
tmpFile, err := os.Create(tmpFilePath)
if err != nil {
panic(fmt.Sprintf("error with file,[%v]", err.Error()))
}
buff := make([]byte, 100)
for {
n, err := file.Read(buff)
if err != nil && err != io.EOF {
panic("error with uploaded file")
}
if n == 0 {
break
}
n, _ = tmpFile.Write(buff[:n])
}
tmpFileInfo, err := os.Stat(tmpFilePath)
if err != nil {
panic(fmt.Sprintf("error with file,[%v]", err.Error()))
}
defer tmpFile.Close()
uploadedFileInfo := &UploadedFileInfo{
FullPath: tmpFilePath,
Name: fileHeader.Filename,
NameWithoutExtension: strings.TrimSuffix(fileHeader.Filename, path.Ext(fileHeader.Filename)),
Extension: ext,
Size: int(tmpFileInfo.Size()),
}
return uploadedFileInfo
}
func (c *Context) MoveFile(sourceFilePath string, destFolderPath string) error {
o := syscall.Umask(0)
defer syscall.Umask(o)
newFileName := filepath.Base(sourceFilePath)
os.MkdirAll(destFolderPath, 766)
srcFileInfo, err := os.Stat(sourceFilePath)
if err != nil {
return err
}
if !srcFileInfo.Mode().IsRegular() {
return errors.New("can not move file, not in a regular mode")
}
srcFile, err := os.Open(sourceFilePath)
if err != nil {
return err
}
defer srcFile.Close()
destFilePath := filepath.Join(destFolderPath, newFileName)
destFile, err := os.Create(destFilePath)
if err != nil {
return err
}
buff := make([]byte, 1024*8)
for {
n, err := srcFile.Read(buff)
if err != nil && err != io.EOF {
panic(fmt.Sprintf("error moving file %v", sourceFilePath))
}
if n == 0 {
break
}
_, err = destFile.Write(buff[:n])
if err != nil {
return err
}
}
destFile.Close()
err = os.Remove(sourceFilePath)
if err != nil {
return err
}
return nil
}
func (c *Context) CopyFile(sourceFilePath string, destFolderPath string) error {
o := syscall.Umask(0)
defer syscall.Umask(o)
newFileName := filepath.Base(sourceFilePath)
os.MkdirAll(destFolderPath, 766)
srcFileInfo, err := os.Stat(sourceFilePath)
if err != nil {
return err
}
if !srcFileInfo.Mode().IsRegular() {
return errors.New("can not move file, not in a regular mode")
}
srcFile, err := os.Open(sourceFilePath)
if err != nil {
return err
}
defer srcFile.Close()
destFilePath := filepath.Join(destFolderPath, newFileName)
destFile, err := os.Create(destFilePath)
if err != nil {
return err
}
buff := make([]byte, 1024*8)
for {
n, err := srcFile.Read(buff)
if err != nil && err != io.EOF {
panic(fmt.Sprintf("error moving file %v", sourceFilePath))
}
if n == 0 {
break
}
_, err = destFile.Write(buff[:n])
if err != nil {
return err
}
}
destFile.Close()
return nil
}
func (c *Context) MapToJson(v any) string {
r := reflect.ValueOf(v)
if r.Kind() != reflect.Map {
panic("parameter is not a map")
}
j, err := json.Marshal(v)
if err != nil {
panic(err.Error())
}
return string(j)
}
type UploadedFileInfo struct {
FullPath string
Name string
NameWithoutExtension string
Extension string
Size int
}
func (c *Context) GetBaseDirPath() string {
return basePath
}
func (c *Context) CastToString(value interface{}) string {
if !basicType(value) {
panic("can not cast to string")
}
return fmt.Sprintf("%v", value)
}
func (c Context) GetUserAgent() string {
return c.Request.httpRequest.UserAgent()
}
func (c *Context) CastToInt(value interface{}) int {
var i int
if !basicType(value) {
panic("can not cast to int")
}
i, ok := value.(int)
if ok {
return i
}
_i, ok := value.(int32)
if ok {
i := int(_i)
return i
}
_ii, ok := value.(int64)
if ok {
i := int(_ii)
return i
}
f, ok := value.(float32)
if ok {
i := int(f)
return i
}
ff, ok := value.(float64)
if ok {
i := int(ff)
return i
}
s, ok := value.(string)
if ok {
fff, err := strconv.ParseFloat(s, 64)
if err != nil {
panic("error casting to int")
}
i = int(fff)
return i
}
panic("error casting to int")
}
func (c *Context) CastToFloat(value interface{}) float64 {
if !basicType(value) {
panic("can not cast to float")
}
v := reflect.ValueOf(value)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
var str string
var ok bool
if v.Kind() == reflect.Float64 {
f, ok := value.(float64)
if !ok {
panic("error casting to float")
}
return f
}
if v.Kind() == reflect.Float32 {
s := fmt.Sprintf("%v", value)
f, err := strconv.ParseFloat(s, 64)
if err != nil {
panic("error casting to float")
}
return f
}
if v.Kind() == reflect.String {
str, ok = value.(string)
if !ok {
panic("error casting to float")
}
}
if v.CanInt() {
i, ok := value.(int)
if !ok {
panic("error casting to float")
}
str = fmt.Sprintf("%v.0", i)
}
f, err := strconv.ParseFloat(str, 64)
if err != nil {
panic("error casting to float")
}
return f
}
func basicType(value interface{}) bool {
v := reflect.ValueOf(value)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
if !(v.Kind() == reflect.Array ||
v.Kind() == reflect.Slice ||
v.Kind() == reflect.Map ||
v.Kind() == reflect.Struct ||
v.Kind() == reflect.Interface ||
v.Kind() == reflect.Func) {
return true
}
return false
}

555
context_test.go Normal file
View file

@ -0,0 +1,555 @@
package core
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"testing"
"git.smarteching.com/goffee/core/logger"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
)
func TestDebugAny(t *testing.T) {
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
c := &Context{
Request: &Request{
httpRequest: r,
httpPathParams: nil,
},
Response: &Response{
headers: []header{},
body: nil,
HttpResponseWriter: w,
},
GetValidator: nil,
GetJWT: nil,
}
h := func(c *Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var msg interface{}
msg = "test-debug-pointer"
c.DebugAny(&msg)
c.DebugAny("test-debug-msg")
}
}(c)
h(w, r)
b, err := io.ReadAll(w.Body)
if err != nil {
t.Errorf("failed testing debug any")
}
if !strings.Contains(string(b), "test-debug-msg") {
t.Errorf("failed testing debug any")
}
if !strings.Contains(string(b), "test-debug-pointer") {
t.Errorf("failed testing debug any")
}
}
func TestLogInfo(t *testing.T) {
tmpF := filepath.Join(t.TempDir(), uuid.NewString())
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
msg := "test-log-info"
c := makeCTXLogTestCTX(t, w, r, tmpF)
h := func(c *Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
c.GetLogger().Info(msg)
}
}(c)
h(w, r)
fc, err := os.ReadFile(tmpF)
if err != nil {
t.Errorf("failed testing log info")
}
if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "info:")) {
t.Errorf("failed testing log info")
}
}
func TestLogWarning(t *testing.T) {
tmpF := filepath.Join(t.TempDir(), uuid.NewString())
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
msg := "test-log-warning"
c := makeCTXLogTestCTX(t, w, r, tmpF)
h := func(c *Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
c.GetLogger().Warning(msg)
}
}(c)
h(w, r)
fc, err := os.ReadFile(tmpF)
if err != nil {
t.Errorf("failed testing log warning")
}
if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "warning:")) {
t.Errorf("failed testing log warning")
}
}
func TestLogDebug(t *testing.T) {
tmpF := filepath.Join(t.TempDir(), uuid.NewString())
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
msg := "test-log-debug"
c := makeCTXLogTestCTX(t, w, r, tmpF)
h := func(c *Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
c.GetLogger().Debug(msg)
}
}(c)
h(w, r)
fc, err := os.ReadFile(tmpF)
if err != nil {
t.Errorf("failed testing log debug")
}
if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "debug:")) {
t.Errorf("failed testing log debug")
}
}
func TestLogError(t *testing.T) {
tmpF := filepath.Join(t.TempDir(), uuid.NewString())
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
msg := "test-log-error"
c := makeCTXLogTestCTX(t, w, r, tmpF)
h := func(c *Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
c.GetLogger().Error(msg)
}
}(c)
h(w, r)
fc, err := os.ReadFile(tmpF)
if err != nil {
t.Errorf("failed testing log error")
}
if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "error:")) {
t.Errorf("failed testing log error")
}
}
func TestGetPathParams(t *testing.T) {
NewEventsManager() //TODO removing require refactoring makeHTTPRouterHandlerFunc()
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
pathParams := httprouter.Params{
{
Key: "param1",
Value: "param1val",
},
{
Key: "param2",
Value: "param2val",
},
}
a := New()
h := a.makeHTTPRouterHandlerFunc(
Handler(func(c *Context) *Response {
rsp := fmt.Sprintf("param1: %v | param2: %v", c.GetPathParam("param1"), c.GetPathParam("param2"))
return c.Response.Text(rsp)
}), nil)
h(w, r, pathParams)
b, err := io.ReadAll(w.Body)
if err != nil {
t.Log("failed testing get path params")
}
bStr := string(b)
if !(strings.Contains(bStr, "param1val") || strings.Contains(bStr, "param2val")) {
t.Errorf("failed testing get path params")
}
}
func TestGetRequestParams(t *testing.T) {
pwd, _ := os.Getwd()
app := New()
app.SetBasePath(pwd)
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/pt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Get("/gt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
client := s.Client()
defer s.Close()
rsp, err := client.PostForm(s.URL+"/pt", url.Values{"param": {"paramValPost"}})
if err != nil {
t.Errorf("failed test get request params: %v", err)
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Logf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "paramValPost" {
t.Errorf("failed test get request params")
}
rsp.Body.Close()
rsp, err = http.Get(s.URL + "/gt?param=paramValGet")
b, err = io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "paramValGet" {
t.Errorf("failed test get request param")
}
rsp.Body.Close()
}
func TestRequestParamsExists(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/pt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
return nil
}))
gcr.Get("/gt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
rsp, err := http.PostForm(s.URL+"/pt", url.Values{"param": {"paramValPost"}})
if err != nil {
t.Logf("failed test get request params")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Logf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "true" {
t.Errorf("failed test get request params")
}
rsp.Body.Close()
rsp, err = http.Get(s.URL + "/gt?param=paramValGet")
b, err = io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "true" {
t.Errorf("failed test get request param")
}
rsp.Body.Close()
}
func TestGetHeader(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/pt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
return nil
}))
gcr.Get("/gt", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("POST", s.URL+"/pt", nil)
if err != nil {
t.Errorf("failed test get header")
}
req.Header.Add("headerkey", "headerPostVal")
rsp, err := clt.Do(req)
if err != nil {
t.Logf("failed test get request params")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Logf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "headerPostVal" {
t.Errorf("failed test get request params")
}
req, err = http.NewRequest("GET", s.URL+"/gt", nil)
if err != nil {
t.Errorf("failed test get header")
}
req.Header.Add("headerkey", "headerGetVal")
rsp, err = clt.Do(req)
if err != nil {
t.Logf("failed test get request params")
}
b, err = io.ReadAll(rsp.Body)
if err != nil {
t.Logf("failed test get request params")
}
if strings.TrimSpace(string(b)) != "headerGetVal" {
t.Errorf("failed test get request params")
}
}
func TestGetUploadedFile(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/pt", Handler(func(c *Context) *Response {
uploadedFile := c.GetUploadedFile("myfile")
rs := fmt.Sprintf("file name: %v | size: %v", uploadedFile.Name, uploadedFile.Size)
fmt.Fprintln(c.Response.HttpResponseWriter, rs)
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
wd, _ := os.Getwd()
tfp := filepath.Join(wd, "testingdata/testdata.json")
file, err := os.Open(tfp)
if err != nil {
t.Error("failed test get upload file")
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("myfile", filepath.Base(file.Name()))
io.Copy(part, file)
writer.Close()
clt := &http.Client{}
req, err := http.NewRequest("POST", s.URL+"/pt", body)
if err != nil {
t.Errorf("failed test get uploaded file")
}
req.Header.Add(CONTENT_TYPE, writer.FormDataContentType())
rsp, err := clt.Do(req)
if err != nil {
t.Logf("failed test get get uploaded file")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Logf("failed test get get uploaded file")
}
rsp.Body.Close()
asrtFile, err := os.Stat(tfp)
if err != nil {
t.Logf("failed test get get uploaded file")
}
if !strings.Contains(string(b), fmt.Sprintf("size: %v", asrtFile.Size())) {
t.Errorf("failed test get uploaded file")
}
if !strings.Contains(string(b), "testdata.json") {
t.Errorf("failed test get uploaded file")
}
}
func TestMoveFile(t *testing.T) {
o := syscall.Umask(0)
defer syscall.Umask(o)
pwd, _ := os.Getwd()
var tmpDir string
tmpDir = path.Join(pwd, "testingdata/tmp")
_, err := os.Stat(path.Join("./testingdata", "totestmovefile.md"))
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
c := makeCTX(t)
err = c.MoveFile("./testingdata/totestmovefile.md", tmpDir)
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
_, err = os.Stat(path.Join(tmpDir, "totestmovefile.md"))
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
t.Cleanup(func() {
c.MoveFile(filepath.Join(tmpDir, "totestmovefile.md"), "./testingdata")
})
}
func TestCopyFile(t *testing.T) {
o := syscall.Umask(0)
defer syscall.Umask(o)
pwd, _ := os.Getwd()
var tmpDir string
tmpDir = path.Join(pwd, "testingdata/tmp")
_, err := os.Stat(path.Join("./testingdata", "totestcopyfile.md"))
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
c := makeCTX(t)
err = c.CopyFile("./testingdata/totestcopyfile.md", tmpDir)
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
_, err = os.Stat(path.Join(tmpDir, "totestcopyfile.md"))
if err != nil {
t.Errorf("failed test move file: %v", err.Error())
}
t.Cleanup(func() {
os.Remove(filepath.Join(tmpDir, "totestcopyfile.md"))
})
}
func TestCastToString(t *testing.T) {
c := makeCTX(t)
s := c.CastToString(25)
if fmt.Sprintf("%T", s) != "string" {
t.Errorf("failed test cast to string")
}
s = c.CastToString(25.54)
if fmt.Sprintf("%T", s) != "string" {
t.Errorf("failed test cast to string")
}
var v interface{} = "434"
s = c.CastToString(v)
if fmt.Sprintf("%T", s) != "string" {
t.Errorf("failed test cast to string")
}
var vs interface{} = "this is a string"
s = c.CastToString(vs)
if fmt.Sprintf("%T", s) != "string" {
t.Errorf("failed test cast to string")
}
}
func TestCastToInt(t *testing.T) {
c := makeCTX(t)
i := c.CastToInt(4)
if !(i == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
ii := c.CastToInt(4.434)
if !(ii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
iii := c.CastToInt("4")
if !(iii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
iiii := c.CastToInt("4.434")
if !(iiii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
var iInterface interface{}
iInterface = 4
i = c.CastToInt(iInterface)
if !(i == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
iInterface = 4.545
ii = c.CastToInt(iInterface)
if !(ii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
iInterface = "4"
iii = c.CastToInt(iInterface)
if !(iii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
iInterface = "4.434"
iiii = c.CastToInt(iInterface)
if !(iiii == 4 && fmt.Sprintf("%T", i) == "int") {
t.Errorf("failed test cast to int")
}
}
func TestCastToFloat(t *testing.T) {
c := makeCTX(t)
f := c.CastToFloat(4)
if !(f == 4 && fmt.Sprintf("%T", f) == "float64") {
t.Errorf("failed test cast to float")
}
var varf32 float32 = 4.434
ff32 := c.CastToFloat(varf32)
if !(ff32 == 4.434 && fmt.Sprintf("%T", ff32) == "float64") {
t.Errorf("failed test cast to float")
}
var varf64 float64 = 4.434
ff64 := c.CastToFloat(varf64)
if !(ff64 == 4.434 && fmt.Sprintf("%T", ff64) == "float64") {
t.Errorf("failed test cast to float")
}
fff := c.CastToFloat("4")
if !(fff == 4 && fmt.Sprintf("%T", fff) == "float64") {
t.Errorf("failed test cast to float")
}
ffff := c.CastToFloat("4.434")
if !(ffff == 4.434 && fmt.Sprintf("%T", ffff) == "float64") {
t.Errorf("failed test cast to float")
}
var iInterface interface{}
iInterface = 4
f = c.CastToFloat(iInterface)
if !(f == 4 && fmt.Sprintf("%T", f) == "float64") {
t.Errorf("failed test cast to float")
}
iInterface = 4.434
iff := c.CastToFloat(iInterface)
if !(iff == 4.434 && fmt.Sprintf("%T", iff) == "float64") {
t.Errorf("failed test cast to float")
}
iInterface = "4"
fff = c.CastToFloat(iInterface)
if !(fff == 4 && fmt.Sprintf("%T", fff) == "float64") {
t.Errorf("failed test cast to float")
}
iInterface = "4.434"
ffff = c.CastToFloat(iInterface)
if !(ffff == 4.434 && fmt.Sprintf("%T", ffff) == "float64") {
t.Errorf("failed test cast to float")
}
}
func TestGetBaseDirPath(t *testing.T) {
c := makeCTX(t)
p := c.GetBaseDirPath()
pwd, err := os.Getwd()
if err != nil {
t.Errorf("failed test get base dir path")
}
if p != pwd {
t.Errorf("failed test get base dir path")
}
}
func makeCTXLogTestCTX(t *testing.T, w http.ResponseWriter, r *http.Request, tmpFilePath string) *Context {
t.Helper()
return &Context{
Request: &Request{
httpRequest: r,
httpPathParams: nil,
},
Response: &Response{
headers: []header{},
body: nil,
HttpResponseWriter: w,
},
GetValidator: nil,
GetJWT: nil,
GetLogger: func() *logger.Logger {
return logger.NewLogger(logger.LogFileDriver{FilePath: tmpFilePath})
},
}
}

541
core.go Normal file
View file

@ -0,0 +1,541 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"fmt"
"log"
"net/http"
"os"
"path"
"path/filepath"
"runtime/debug"
"strconv"
"syscall"
"git.smarteching.com/goffee/core/env"
"git.smarteching.com/goffee/core/logger"
"github.com/julienschmidt/httprouter"
"golang.org/x/crypto/acme/autocert"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var loggr *logger.Logger
var logsDriver *logger.LogsDriver
var requestC RequestConfig
var jwtC JWTConfig
var gormC GormConfig
var cacheC CacheConfig
var db *gorm.DB
var mailer *Mailer
var basePath string
var disableEvents bool = false
type configContainer struct {
Request RequestConfig
}
type App struct {
t int // for trancking middlewares
chain *chain
middlewares *Middlewares
Config *configContainer
}
var app *App
func New() *App {
app = &App{
chain: &chain{},
middlewares: NewMiddlewares(),
Config: &configContainer{
Request: requestC,
},
}
return app
}
func ResolveApp() *App {
return app
}
func (app *App) SetLogsDriver(d logger.LogsDriver) {
logsDriver = &d
}
func (app *App) Bootstrap() {
loggr = logger.NewLogger(*logsDriver)
NewRouter()
NewEventsManager()
}
func (app *App) Run(router *httprouter.Router) {
portNumber := os.Getenv("App_HTTP_PORT")
if portNumber == "" {
portNumber = "80"
}
router = app.RegisterRoutes(ResolveRouter().GetRoutes(), router)
useHttpsStr := os.Getenv("App_USE_HTTPS")
if useHttpsStr == "" {
useHttpsStr = "false"
}
useHttps, _ := strconv.ParseBool(useHttpsStr)
fmt.Printf("Welcome to Goffee\n")
if useHttps {
fmt.Printf("Listening on https \nWaiting for requests...\n")
} else {
fmt.Printf("Listening on port %s\nWaiting for requests...\n", portNumber)
}
UseLetsEncryptStr := os.Getenv("App_USE_LETSENCRYPT")
if UseLetsEncryptStr == "" {
UseLetsEncryptStr = "false"
}
UseLetsEncrypt, _ := strconv.ParseBool(UseLetsEncryptStr)
if useHttps && UseLetsEncrypt {
m := &autocert.Manager{
Cache: autocert.DirCache("letsencrypt-certs-dir"),
Prompt: autocert.AcceptTOS,
}
LetsEncryptEmail := os.Getenv("APP_LETSENCRYPT_EMAIL")
if LetsEncryptEmail != "" {
m.Email = LetsEncryptEmail
}
HttpsHosts := os.Getenv("App_HTTPS_HOSTS")
if HttpsHosts != "" {
m.HostPolicy = autocert.HostWhitelist(HttpsHosts)
}
log.Fatal(http.Serve(m.Listener(), router))
return
}
if useHttps && !UseLetsEncrypt {
CertFile := os.Getenv("App_CERT_FILE_PATH")
if CertFile == "" {
CertFile = "tls/server.crt"
}
KeyFile := os.Getenv("App_KEY_FILE_PATH")
if KeyFile == "" {
KeyFile = "tls/server.key"
}
certFilePath := filepath.Join(basePath, CertFile)
KeyFilePath := filepath.Join(basePath, KeyFile)
log.Fatal(http.ListenAndServeTLS(":443", certFilePath, KeyFilePath, router))
return
}
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", portNumber), router))
}
func (app *App) RegisterRoutes(routes []Route, router *httprouter.Router) *httprouter.Router {
router.PanicHandler = panicHandler
router.NotFound = notFoundHandler{}
router.MethodNotAllowed = methodNotAllowed{}
for _, route := range routes {
switch route.Method {
case GET:
router.GET(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case POST:
router.POST(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case DELETE:
router.DELETE(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case PATCH:
router.PATCH(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case PUT:
router.PUT(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case OPTIONS:
router.OPTIONS(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
case HEAD:
router.HEAD(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares))
}
}
return router
}
func (app *App) makeHTTPRouterHandlerFunc(h Handler, ms []Middleware) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ctx := &Context{
Request: &Request{
httpRequest: r,
httpPathParams: ps,
},
Response: &Response{
headers: []header{},
body: nil,
contentType: "",
overrideContentType: "",
HttpResponseWriter: w,
isTerminated: false,
redirectTo: "",
},
GetValidator: getValidator(),
GetJWT: getJWT(),
GetGorm: getGormFunc(),
GetCache: resolveCache(),
GetHashing: resloveHashing(),
GetMailer: resolveMailer(),
GetEventsManager: resolveEventsManager(),
GetLogger: resolveLogger(),
}
ctx.prepare(ctx)
rhs := app.combHandlers(h, ms)
app.prepareChain(rhs)
app.t = 0
app.chain.execute(ctx)
for _, header := range ctx.Response.headers {
w.Header().Add(header.key, header.val)
}
logger.CloseLogsFile()
var ct string
if ctx.Response.overrideContentType != "" {
ct = ctx.Response.overrideContentType
} else if ctx.Response.contentType != "" {
ct = ctx.Response.contentType
} else {
ct = CONTENT_TYPE_HTML
}
w.Header().Add(CONTENT_TYPE, ct)
if ctx.Response.statusCode != 0 {
w.WriteHeader(ctx.Response.statusCode)
}
if ctx.Response.redirectTo != "" {
http.Redirect(w, r, ctx.Response.redirectTo, http.StatusPermanentRedirect)
} else {
w.Write(ctx.Response.body)
}
e := ResolveEventsManager()
if e != nil {
e.setContext(ctx).processFiredEvents()
}
app.t = 0
ctx.Response.reset()
app.chain.reset()
}
}
type notFoundHandler struct{}
type methodNotAllowed struct{}
func (n notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
res := "{\"message\": \"Not Found\"}"
loggr.Error("Not Found")
loggr.Error(debug.Stack())
w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON)
w.Write([]byte(res))
}
func (n methodNotAllowed) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
res := "{\"message\": \"Method not allowed\"}"
loggr.Error("Method not allowed")
loggr.Error(debug.Stack())
w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON)
w.Write([]byte(res))
}
var panicHandler = func(w http.ResponseWriter, r *http.Request, e interface{}) {
isDebugModeStr := os.Getenv("APP_DEBUG_MODE")
isDebugMode, err := strconv.ParseBool(isDebugModeStr)
if err != nil {
errStr := "error parsing env var APP_DEBUG_MODE"
loggr.Error(errStr)
fmt.Sprintln(errStr)
w.Write([]byte(errStr))
return
}
if !isDebugMode {
errStr := "internal error"
loggr.Error(errStr)
fmt.Sprintln(errStr)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON)
w.Write([]byte(fmt.Sprintf("{\"message\": \"%v\"}", errStr)))
return
}
shrtMsg := fmt.Sprintf("%v", e)
loggr.Error(shrtMsg)
fmt.Println(shrtMsg)
loggr.Error(string(debug.Stack()))
var res string
if env.GetVarOtherwiseDefault("APP_ENV", "local") == PRODUCTION {
res = "{\"message\": \"internal error\"}"
} else {
res = fmt.Sprintf("{\"message\": \"%v\", \"stack trace\": \"%v\"}", e, string(debug.Stack()))
}
w.WriteHeader(http.StatusInternalServerError)
w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON)
w.Write([]byte(res))
}
func UseMiddleware(mw Middleware) {
ResolveMiddlewares().Attach(mw)
}
func (app *App) Next(c *Context) {
app.t = app.t + 1
n := app.chain.getByIndex(app.t)
if n != nil {
f, ok := n.(Middleware)
if ok {
f(c)
} else {
ff, ok := n.(Handler)
if ok {
ff(c)
}
}
}
}
type chain struct {
nodes []interface{}
}
func (cn *chain) reset() {
cn.nodes = []interface{}{}
}
func (c *chain) getByIndex(i int) interface{} {
for k := range c.nodes {
if k == i {
return c.nodes[i]
}
}
return nil
}
func (app *App) prepareChain(hs []interface{}) {
mw := app.middlewares.GetMiddlewares()
for _, v := range mw {
app.chain.nodes = append(app.chain.nodes, v)
}
for _, v := range hs {
app.chain.nodes = append(app.chain.nodes, v)
}
}
func (cn *chain) execute(ctx *Context) {
i := cn.getByIndex(0)
if i != nil {
f, ok := i.(Middleware)
if ok {
f(ctx)
} else {
ff, ok := i.(Handler)
if ok {
ff(ctx)
}
}
}
}
func (app *App) combHandlers(h Handler, mw []Middleware) []interface{} {
var rev []interface{}
for _, k := range mw {
rev = append(rev, k)
}
rev = append(rev, h)
return rev
}
func getGormFunc() func() *gorm.DB {
f := func() *gorm.DB {
if !gormC.EnableGorm {
panic("you are trying to use gorm but it's not enabled, you can enable it in the file config/gorm.go")
}
return ResolveGorm()
}
return f
}
func NewGorm() *gorm.DB {
var err error
switch os.Getenv("DB_DRIVER") {
case "mysql":
db, err = mysqlConnect()
case "postgres":
db, err = postgresConnect()
case "sqlite":
sqlitePath := os.Getenv("SQLITE_DB_PATH")
fullSqlitePath := path.Join(basePath, sqlitePath)
_, err := os.Stat(fullSqlitePath)
if err != nil {
panic(fmt.Sprintf("error locating sqlite file: %v", err.Error()))
}
db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{})
default:
panic("database driver not selected")
}
if gormC.EnableGorm && err != nil {
panic(fmt.Sprintf("gorm has problem connecting to %v, (if it's not needed you can disable it in config/gorm.go): %v", os.Getenv("DB_DRIVER"), err))
}
return db
}
func ResolveGorm() *gorm.DB {
if db != nil {
return db
}
db = NewGorm()
return db
}
func resolveCache() func() *Cache {
f := func() *Cache {
if !cacheC.EnableCache {
panic("you are trying to use cache but it's not enabled, you can enable it in the file config/cache.go")
}
return NewCache(cacheC)
}
return f
}
func postgresConnect() (*gorm.DB, error) {
dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=%v TimeZone=%v",
os.Getenv("POSTGRES_HOST"),
os.Getenv("POSTGRES_USER"),
os.Getenv("POSTGRES_PASSWORD"),
os.Getenv("POSTGRES_DB_NAME"),
os.Getenv("POSTGRES_PORT"),
os.Getenv("POSTGRES_SSL_MODE"),
os.Getenv("POSTGRES_TIMEZONE"),
)
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
func mysqlConnect() (*gorm.DB, error) {
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=%v&parseTime=True&loc=Local",
os.Getenv("MYSQL_USERNAME"),
os.Getenv("MYSQL_PASSWORD"),
os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_PORT"),
os.Getenv("MYSQL_DB_NAME"),
os.Getenv("MYSQL_CHARSET"),
)
return gorm.Open(mysql.New(mysql.Config{
DSN: dsn, // data source name
DefaultStringSize: 256, // default size for string fields
DisableDatetimePrecision: true, // disable datetime precision, which not supported before MySQL 5.6
DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB
DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB
SkipInitializeWithVersion: false, // auto configure based on currently MySQL version
}), &gorm.Config{})
}
func getJWT() func() *JWT {
f := func() *JWT {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
panic("jwt secret key is not set")
}
lifetimeStr := os.Getenv("JWT_LIFESPAN_MINUTES")
if lifetimeStr == "" {
lifetimeStr = "10080" // 7 days
}
lifetime64, err := strconv.ParseInt(lifetimeStr, 10, 32)
if err != nil {
panic(err)
}
lifetime := int(lifetime64)
return newJWT(JWTOptions{
SigningKey: secret,
LifetimeMinutes: lifetime,
})
}
return f
}
func getValidator() func() *Validator {
f := func() *Validator {
return &Validator{}
}
return f
}
func resloveHashing() func() *Hashing {
f := func() *Hashing {
return &Hashing{}
}
return f
}
func resolveMailer() func() *Mailer {
f := func() *Mailer {
if mailer != nil {
return mailer
}
var m *Mailer
var emailsDriver string
if os.Getenv("EMAILS_DRIVER") == "" {
emailsDriver = "SMTP"
}
switch emailsDriver {
case "SMTP":
m = initiateMailerWithSMTP()
case "sparkpost":
m = initiateMailerWithSparkPost()
case "sendgrid":
m = initiateMailerWithSendGrid()
case "mailgun":
return initiateMailerWithMailGun()
default:
m = initiateMailerWithSMTP()
}
mailer = m
return mailer
}
return f
}
func resolveEventsManager() func() *EventsManager {
f := func() *EventsManager {
return ResolveEventsManager()
}
return f
}
func resolveLogger() func() *logger.Logger {
f := func() *logger.Logger {
return loggr
}
return f
}
func (app *App) MakeDirs(dirs ...string) {
o := syscall.Umask(0)
defer syscall.Umask(o)
for _, dir := range dirs {
os.MkdirAll(path.Join(basePath, dir), 0766)
}
}
func (app *App) SetRequestConfig(r RequestConfig) {
requestC = r
}
func (app *App) SetGormConfig(g GormConfig) {
gormC = g
}
func (app *App) SetCacheConfig(c CacheConfig) {
cacheC = c
}
func (app *App) SetBasePath(path string) {
basePath = path
}
func DisableEvents() {
disableEvents = true
}
func EnableEvents() {
disableEvents = false
}

525
core_test.go Normal file
View file

@ -0,0 +1,525 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"git.smarteching.com/goffee/core/env"
"git.smarteching.com/goffee/core/logger"
"github.com/google/uuid"
"github.com/joho/godotenv"
"github.com/julienschmidt/httprouter"
)
func TestNew(t *testing.T) {
app := createNewApp(t)
if fmt.Sprintf("%T", app) != "*core.App" {
t.Errorf("failed testing new core")
}
}
func TestSetEnv(t *testing.T) {
envVars, err := godotenv.Read("./testingdata/.env")
if err != nil {
t.Errorf("failed reading .env file")
}
env.SetEnvVars(envVars)
if os.Getenv("KEY_ONE") != "VAL_ONE" || os.Getenv("KEY_TWO") != "VAL_TWO" {
t.Errorf("failed to set env vars")
}
}
func TestMakeHTTPHandlerFunc(t *testing.T) {
app := createNewApp(t)
tmpFile := filepath.Join(t.TempDir(), uuid.NewString())
app.SetLogsDriver(&logger.LogFileDriver{
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
})
hdlr := Handler(func(c *Context) *Response {
f, _ := os.Create(tmpFile)
f.WriteString("DFT2V56H")
c.Response.SetHeader("header-key", "header-val")
return c.Response.Text("DFT2V56H")
})
h := app.makeHTTPRouterHandlerFunc(hdlr, nil)
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
h(w, r, []httprouter.Param{{Key: "tkey", Value: "tvalue"}})
rsp := w.Result()
if rsp.StatusCode != http.StatusOK {
t.Errorf("failed testing make http handler func")
}
s, _ := os.ReadFile(tmpFile)
if string(s) != "DFT2V56H" {
t.Errorf("failed testing make http handler func")
}
}
func TestMakeHTTPHandlerFuncVerifyJson(t *testing.T) {
app := createNewApp(t)
tmpFile := filepath.Join(t.TempDir(), uuid.NewString())
app.SetLogsDriver(&logger.LogFileDriver{
FilePath: filepath.Join(t.TempDir(), uuid.NewString()),
})
hdlr := Handler(func(c *Context) *Response {
f, _ := os.Create(tmpFile)
f.WriteString("DFT2V56H")
c.Response.SetHeader("header-key", "header-val")
return c.Response.Json("{\"testKey\": \"testVal\"}")
})
h := app.makeHTTPRouterHandlerFunc(hdlr, nil)
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
h(w, r, []httprouter.Param{{Key: "tkey", Value: "tvalue"}})
rsp := w.Result()
if rsp.StatusCode != http.StatusOK {
t.Errorf("failed testing make http handler func with json verify")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed testing make http handler func with json verify")
}
var j map[string]interface{}
err = json.Unmarshal(b, &j)
if err != nil {
t.Errorf("failed testing make http handler func with json verify: %v", err)
}
if j["testKey"] != "testVal" {
t.Errorf("failed testing make http handler func with json verify")
}
}
func TestMethodNotAllowedHandler(t *testing.T) {
app := createNewApp(t)
app.SetLogsDriver(logger.LogNullDriver{})
app.Bootstrap()
m := &methodNotAllowed{}
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
m.ServeHTTP(w, r)
rsp := w.Result()
if rsp.StatusCode != 405 {
t.Errorf("failed testing method not allowed")
}
}
func TestNotFoundHandler(t *testing.T) {
n := &notFoundHandler{}
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
n.ServeHTTP(w, r)
rsp := w.Result()
if rsp.StatusCode != 404 {
t.Errorf("failed testing not found handler")
}
}
func TestUseMiddleware(t *testing.T) {
app := createNewApp(t)
UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") }))
if len(app.middlewares.GetMiddlewares()) != 1 {
t.Errorf("failed testing use middleware")
}
}
func TestChainReset(t *testing.T) {
c := &chain{}
c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing1!") }))
c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") }))
c.reset()
if len(c.nodes) != 0 {
t.Errorf("failed testing reset chain")
}
}
func TestNext(t *testing.T) {
app := createNewApp(t)
app.t = 0
tfPath := filepath.Join(t.TempDir(), uuid.NewString())
var hs []interface{}
hs = append(hs, Middleware(func(c *Context) { c.Next() }))
hs = append(hs, Handler(func(c *Context) *Response {
f, _ := os.Create(tfPath)
f.WriteString("DFT2V56H")
return nil
}))
app.prepareChain(hs)
app.chain.execute(makeCTX(t))
cnt, _ := os.ReadFile(tfPath)
if string(cnt) != "DFT2V56H" {
// t.Errorf("failed testing next")
}
}
func TestChainGetByIndex(t *testing.T) {
c := &chain{}
tf := filepath.Join(t.TempDir(), uuid.NewString())
var hs []interface{}
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing!") }))
hs = append(hs, Middleware(func(c *Context) {
f, _ := os.Create(tf)
f.WriteString("DFT2V56H")
}))
c.nodes = hs
pf := c.getByIndex(1)
f, ok := pf.(Middleware)
if ok {
f(makeCTX(t))
}
d, _ := os.ReadFile(tf)
if string(d) != "DFT2V56H" {
t.Errorf("failed testing chain get by index")
}
}
func TestPrepareChain(t *testing.T) {
app := createNewApp(t)
UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") }))
var hs []interface{}
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing1!") }))
hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing2!") }))
app.prepareChain(hs)
if len(app.chain.nodes) != 3 {
t.Errorf("failed preparing chain")
}
}
func TestChainExecute(t *testing.T) {
tmpDir := t.TempDir()
f1Path := filepath.Join(tmpDir, uuid.NewString())
c := &chain{}
c.nodes = []interface{}{
Handler(func(c *Context) *Response {
tf, _ := os.Create(f1Path)
defer tf.Close()
tf.WriteString("DFT2V56H")
return nil
}),
}
ctx := makeCTX(t)
c.execute(ctx)
cnt, _ := os.ReadFile(f1Path)
if string(cnt) != "DFT2V56H" {
t.Errorf("failed testing execute chain")
}
}
func makeCTX(t *testing.T) *Context {
t.Helper()
return &Context{
Request: &Request{
httpRequest: httptest.NewRequest(GET, LOCALHOST, nil),
httpPathParams: nil,
},
Response: &Response{
headers: []header{},
body: nil,
HttpResponseWriter: httptest.NewRecorder(),
},
GetValidator: nil,
GetJWT: nil,
}
}
func TestcombHndlers(t *testing.T) {
app := createNewApp(t)
t1 := Handler(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil })
t2 := Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") })
mw := []Middleware{t2}
comb := app.combHandlers(t1, mw)
if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[0]).Pointer() {
t.Errorf("failed testing reverse handlers")
}
if reflect.ValueOf(t2).Pointer() != reflect.ValueOf(comb[1]).Pointer() {
t.Errorf("failed testing reverse handlers")
}
}
func TestRegisterGetRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Get("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Post("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Delete("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Patch("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Put("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Options("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
gcr.Head("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("GET", s.URL+"?param=valget", nil)
if err != nil {
t.Errorf("failed test register routes")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register routes")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register routes")
}
if strings.TrimSpace(string(b)) != "valget" {
t.Errorf("failed test register routes")
}
rsp.Body.Close()
}
func TestRegisterPostRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Post("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("POST", s.URL+"?param=valpost", nil)
if err != nil {
t.Errorf("failed test register post route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register post route")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register post route")
}
if strings.TrimSpace(string(b)) != "valpost" {
t.Errorf("failed test register post route")
}
rsp.Body.Close()
}
func TestRegisterDeleteRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Delete("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("DELETE", s.URL+"?param=valdelete", nil)
if err != nil {
t.Errorf("failed test register delete route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register delete route")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register delete route")
}
if strings.TrimSpace(string(b)) != "valdelete" {
t.Errorf("failed test register delete route")
}
rsp.Body.Close()
}
func TestRegisterPatchRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Patch("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("PATCH", s.URL+"?param=valpatch", nil)
if err != nil {
t.Errorf("failed test register patch route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register patch route")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register patch route")
}
if strings.TrimSpace(string(b)) != "valpatch" {
t.Errorf("failed test register patch route")
}
rsp.Body.Close()
}
func TestRegisterPutRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Put("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("PUT", s.URL+"?param=valput", nil)
if err != nil {
t.Errorf("failed test register put route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register put route")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register put route")
}
if strings.TrimSpace(string(b)) != "valput" {
t.Errorf("failed test register put route")
}
rsp.Body.Close()
}
func TestRegisterOptionsRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
gcr.Options("/", Handler(func(c *Context) *Response {
fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param"))
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("OPTIONS", s.URL+"?param=valoptions", nil)
if err != nil {
t.Errorf("failed test register options route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register options route")
}
b, err := io.ReadAll(rsp.Body)
if err != nil {
t.Errorf("failed test register options route")
}
if strings.TrimSpace(string(b)) != "valoptions" {
t.Errorf("failed test register options route")
}
rsp.Body.Close()
}
func TestRegisterHeadRoute(t *testing.T) {
app := New()
hr := httprouter.New()
gcr := NewRouter()
tfp := filepath.Join(t.TempDir(), uuid.NewString())
gcr.Head("/", Handler(func(c *Context) *Response {
param := c.GetRequestParam("param")
p, _ := param.(string)
f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 777)
if err != nil {
fmt.Println(err.Error())
}
defer f.Close()
f.WriteString("fromhead")
return nil
}))
hr = app.RegisterRoutes(gcr.GetRoutes(), hr)
s := httptest.NewServer(hr)
defer s.Close()
clt := &http.Client{}
req, err := http.NewRequest("HEAD", s.URL+"?param="+tfp, nil)
if err != nil {
t.Errorf("failed test register head route")
}
rsp, err := clt.Do(req)
if err != nil {
t.Errorf("failed test register head route")
}
f, err := os.Open(tfp)
if err != nil {
t.Errorf("failed test register head route: %v", err.Error())
}
b, err := io.ReadAll(f)
if err != nil {
t.Errorf("failed test register head route: %v", err.Error())
}
f.Close()
if strings.TrimSpace(string(b)) != "fromhead" {
t.Errorf("failed test register head route")
}
rsp.Body.Close()
}
func TestPanicHandler(t *testing.T) {
os.Setenv("APP_DEBUG_MODE", "true")
loggr = logger.NewLogger(&logger.LogNullDriver{})
r := httptest.NewRequest(GET, LOCALHOST, nil)
w := httptest.NewRecorder()
panicHandler(w, r, "")
rsp := w.Result()
b, _ := io.ReadAll(rsp.Body)
if !strings.Contains(string(b), "stack trace") {
t.Errorf("failed test panic handler")
}
}
func createNewApp(t *testing.T) *App {
t.Helper()
a := New()
a.SetLogsDriver(&logger.LogNullDriver{})
a.SetRequestConfig(testingRequestC)
return a
}

32
env/env.go vendored Normal file
View file

@ -0,0 +1,32 @@
package env
import (
"os"
"strings"
)
func GetVar(varName string) string {
return os.Getenv(varName)
}
func GetVarOtherwiseDefault(varName string, defaultValue string) string {
v, p := os.LookupEnv(varName)
if p {
return v
}
return defaultValue
}
func IsSet(varName string) bool {
_, p := os.LookupEnv(varName)
if p {
return true
}
return false
}
func SetEnvVars(envVars map[string]string) {
for key, val := range envVars {
os.Setenv(strings.TrimSpace(key), strings.TrimSpace(val))
}
}

53
env/env_test.go vendored Normal file
View file

@ -0,0 +1,53 @@
package env
import (
"os"
"testing"
)
func TestGetVar(t *testing.T) {
os.Setenv("testKey11", "testVal")
v := GetVar("testKey11")
if v != "testVal" {
t.Error("failed testing get var")
}
}
func TestGetVarOtherwiseDefault(t *testing.T) {
v := GetVarOtherwiseDefault("testKey12", "defaultVal")
if v != "defaultVal" {
t.Error("failed testing get default")
}
os.Setenv("testKey12", "testVal")
v = GetVarOtherwiseDefault("testKey12", "defaultVal")
if v != "testVal" {
t.Error("failed testing get default val")
}
}
func TestIsSet(t *testing.T) {
i := IsSet("testKey13")
if i == true {
t.Error("failed testing is set")
}
os.Setenv("testKey13", "testVal")
i = IsSet("testKey13")
if i == false {
t.Error("filed testing is set")
}
}
func TestSetEnvVars(t *testing.T) {
envVars := map[string]string{
"key14": "testVal14",
"key15": "testVal15",
}
SetEnvVars(envVars)
if GetVar("key14") != "testVal14" {
t.Error("failed testing set vars")
}
if GetVar("key15") != "testVal15" {
t.Error("failed testing set vars")
}
}

3
event-job.go Normal file
View file

@ -0,0 +1,3 @@
package core
type EventJob func(event *Event, requestContext *Context)

6
event.go Normal file
View file

@ -0,0 +1,6 @@
package core
type Event struct {
Name string
Payload map[string]interface{}
}

89
events-manager.go Normal file
View file

@ -0,0 +1,89 @@
package core
import (
"errors"
"fmt"
)
type EventsManager struct {
eventsJobsList map[string][]EventJob
firedEvents []*Event
}
var manager *EventsManager
var rqc *Context
func NewEventsManager() *EventsManager {
manager = &EventsManager{
eventsJobsList: map[string][]EventJob{},
}
return manager
}
func ResolveEventsManager() *EventsManager {
return manager
}
func (m *EventsManager) setContext(requestContext *Context) *EventsManager {
rqc = requestContext
return m
}
func (m *EventsManager) Fire(e *Event) error {
if disableEvents {
return nil
}
if e.Name == "" {
return errors.New("event name is empty")
}
_, exists := m.eventsJobsList[e.Name]
if !exists {
return errors.New(fmt.Sprintf("event %v is not registered", e.Name))
}
m.firedEvents = append(m.firedEvents, e)
return nil
}
func (m *EventsManager) Register(eName string, job EventJob) {
if disableEvents {
return
}
if eName == "" {
panic("event name is empty")
}
_, exists := m.eventsJobsList[eName]
if !exists {
m.eventsJobsList[eName] = []EventJob{job}
return
}
for key, jobs := range m.eventsJobsList {
if key == eName {
jobs = append(jobs, job)
m.eventsJobsList[key] = jobs
}
}
}
func (m *EventsManager) processFiredEvents() {
if disableEvents {
return
}
for _, event := range m.firedEvents {
m.executeEventJobs(event)
}
m.firedEvents = []*Event{}
}
func (m *EventsManager) executeEventJobs(event *Event) {
for key, jobs := range m.eventsJobsList {
if key == event.Name {
for _, job := range jobs {
job(event, rqc)
}
}
}
}

103
events-manager_test.go Normal file
View file

@ -0,0 +1,103 @@
package core
import (
"fmt"
"testing"
)
func TestNewEventsManager(t *testing.T) {
m := NewEventsManager()
if fmt.Sprintf("%T", m) != "*core.EventsManager" {
t.Errorf("failed testing new events manager")
}
}
// func TestResolveEventsManager(t *testing.T) {
// NewEventsManager()
// m := ResolveEventsManager()
// if fmt.Sprintf("%T", m) != "*core.EventsManager" {
// t.Errorf("failed testing new events manager")
// }
// }
// func TestEvents(t *testing.T) {
// pwd, _ := os.Getwd()
// const eventName1 string = "test-event-name1"
// const eventName2 string = "test-event-name2"
// var tmpDir string
// if runtime.GOOS == "linux" {
// tmpDir = t.TempDir()
// } else {
// tmpDir = filepath.Join(pwd, "/testingdata/tmp")
// }
// tmpFile1 := filepath.Join(tmpDir, uuid.NewString())
// tmpFile2 := filepath.Join(tmpDir, uuid.NewString())
// tmpFile3 := filepath.Join(tmpDir, uuid.NewString())
// m := NewEventsManager()
// m.Register(eventName1, func(event *Event, requestContext *Context) {
// os.Create(tmpFile1)
// f, err := os.Create(tmpFile1)
// if err != nil {
// t.Errorf("error testing register event: %v", err.Error())
// }
// f.WriteString(event.Name)
// f.Close()
// })
// m.Register(eventName1, func(event *Event, requestContext *Context) {
// os.Create(tmpFile3)
// f, err := os.Create(tmpFile3)
// if err != nil {
// t.Errorf("error testing register event: %v", err.Error())
// }
// f.WriteString(event.Name)
// f.Close()
// })
// m.Fire(&Event{Name: eventName1})
// m.processFiredEvents()
// ff, err := os.Open(tmpFile1)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
// d, err := io.ReadAll(ff)
// if string(d) != eventName1 {
// t.Error("faild testing events")
// }
// ff.Close()
// os.Remove(tmpFile1)
// ff, err = os.Open(tmpFile3)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
// d, err = io.ReadAll(ff)
// if string(d) != eventName1 {
// t.Error("faild testing events")
// }
// ff.Close()
// os.Remove(tmpFile3)
// m.Register(eventName2, func(event *Event, requestContext *Context) {
// f, err := os.Create(tmpFile2)
// if err != nil {
// t.Errorf("error testing register event: %v", err.Error())
// }
// f.WriteString(event.Name)
// f.Close()
// })
// m.Fire(&Event{Name: eventName2})
// m.processFiredEvents()
// ff, err = os.Open(tmpFile2)
// if err != nil {
// t.Errorf("error testing register event : %v", err.Error())
// }
// d, err = io.ReadAll(ff)
// if string(d) != eventName2 {
// t.Error("faild testing events")
// }
// ff.Close()
// }

50
go.mod Normal file
View file

@ -0,0 +1,50 @@
module git.smarteching.com/goffee/core
replace git.smarteching.com/goffee/core/logger => ./logger
replace git.smarteching.com/goffee/core/env => ./env
go 1.20
require (
github.com/brianvoe/gofakeit/v6 v6.21.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/uuid v1.3.0
github.com/harranali/mailing v1.2.0
github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0
github.com/redis/go-redis/v9 v9.0.5
golang.org/x/crypto v0.11.0
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2
)
require (
github.com/SparkPost/gosparkpost v0.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/json-iterator/go v1.1.10 // indirect
github.com/mailgun/mailgun-go/v4 v4.10.0 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
)
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
)

109
go.sum Normal file
View file

@ -0,0 +1,109 @@
github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s=
github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY=
github.com/brianvoe/gofakeit/v6 v6.21.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY=
github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailgun/mailgun-go/v4 v4.10.0 h1:e5LVsxpqjOYRyaOWifrJORoLQZTYDP+g4ljfmf9G2zE=
github.com/mailgun/mailgun-go/v4 v4.10.0/go.mod h1:L9s941Lgk7iB3TgywTPz074pK2Ekkg4kgbnAaAyJ2z8=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg=
github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=

8
handler.go Normal file
View file

@ -0,0 +1,8 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
type Handler func(c *Context) *Response

37
hashing.go Normal file
View file

@ -0,0 +1,37 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
)
type Hashing struct{}
func (h *Hashing) HashPassword(password string) (string, error) {
res, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
return "", err
}
return string(res), nil
}
func (h *Hashing) CheckPasswordHash(hashedPassword string, originalPassowrd string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(originalPassowrd))
if err != nil && errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return false, nil
}
if err != nil {
fmt.Print(err)
loggr.Debug(err.Error())
return false, errors.New("failed checking password hash")
}
return true, nil
}

100
jwt.go Normal file
View file

@ -0,0 +1,100 @@
package core
import (
"encoding/json"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWT struct {
signingKey []byte
expiresAt time.Time
}
type JWTOptions struct {
SigningKey string
LifetimeMinutes int
}
var j *JWT
func newJWT(opts JWTOptions) *JWT {
d := time.Duration(opts.LifetimeMinutes)
j = &JWT{
signingKey: []byte(opts.SigningKey),
expiresAt: time.Now().Add(d * time.Minute),
}
return j
}
func resolveJWT() *JWT {
return j
}
type claims struct {
J []byte
jwt.RegisteredClaims
}
func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) {
claims, err := mapClaims(payload, j.expiresAt)
if err != nil {
return "", err
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := t.SignedString(j.signingKey)
if err != nil {
return "", err
}
return token, nil
}
func (j *JWT) DecodeToken(token string) (payload map[string]interface{}, err error) {
t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
return j.signingKey, nil
})
if err != nil {
return nil, err
}
c, ok := t.Claims.(*claims)
if !ok {
return nil, errors.New("error decoding token")
}
expiresAt := time.Unix(c.ExpiresAt.Unix(), 0)
et := time.Now().Compare(expiresAt)
if et != -1 {
return nil, errors.New("token has expired")
}
err = json.Unmarshal(c.J, &payload)
if err != nil {
return nil, err
}
return payload, nil
}
func (j *JWT) HasExpired(token string) (bool, error) {
_, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) {
return j.signingKey, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return true, nil
}
return true, err
}
return false, nil
}
func mapClaims(data map[string]interface{}, expiresAt time.Time) (jwt.Claims, error) {
j, err := json.Marshal(data)
if err != nil {
return claims{}, err
}
r := claims{
j,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
return r, nil
}

114
jwt_test.go Normal file
View file

@ -0,0 +1,114 @@
package core
import (
"fmt"
"testing"
"time"
)
func TestNewJWT(t *testing.T) {
j := newJWT(JWTOptions{
SigningKey: "testsigning",
LifetimeMinutes: 2,
})
if fmt.Sprintf("%T", j) != "*core.JWT" {
t.Errorf("failed testing new jwt")
}
}
func TestResolveJWT(t *testing.T) {
initiateJWTHelper(t)
j := resolveJWT()
if fmt.Sprintf("%T", j) != "*core.JWT" {
t.Errorf("failed testing resolve jwt")
}
}
func TestGenerateToken(t *testing.T) {
j := initiateJWTHelper(t)
token, err := j.GenerateToken(map[string]interface{}{
"testKey": "testVal",
})
if err != nil || token == "" {
t.Errorf("error testing generate jwt token")
}
d, err := j.DecodeToken(token)
if err != nil {
t.Errorf("error testing generate jwt token: %v", err.Error())
}
if d["testKey"] != "testVal" {
t.Errorf("error testing generate jwt token: %v", err.Error())
}
}
func TestDecodeToken(t *testing.T) {
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag"
j := initiateJWTHelper(t)
_, err := j.DecodeToken(expiredToken)
if err == nil {
t.Errorf("failed test decode token")
}
token, err := j.GenerateToken(map[string]interface{}{
"testKey": "testVal",
})
if err != nil || token == "" {
t.Errorf("failed testing decode token")
}
d, err := j.DecodeToken(token)
if err != nil {
t.Errorf("error testing decode jwt token")
}
if d["testKey"] != "testVal" {
t.Errorf("error testing decode jwt token")
}
d, err = j.DecodeToken("test-token")
if err == nil {
t.Errorf("error testing decode jwt token")
}
}
func TestHasExpired(t *testing.T) {
expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag"
j := initiateJWTHelper(t)
tokenHasExpired, err := j.HasExpired(expiredToken)
if !tokenHasExpired {
t.Errorf("failed test token has expired check")
}
if tokenHasExpired && err != nil {
t.Errorf("failed test decode token: %v", err.Error())
}
token, err := j.GenerateToken(map[string]interface{}{
"testKey": "testVal",
})
if err != nil || token == "" {
t.Errorf("failed testing decode token")
}
_, err = j.HasExpired(token)
if err != nil {
t.Errorf("error testing decode jwt token")
}
}
func TestMapClaims(t *testing.T) {
c, err := mapClaims(map[string]interface{}{
"testKey": "testVal",
}, time.Now())
if err != nil {
t.Errorf("failed testing map claims")
}
if fmt.Sprintf("%T", c) != "core.claims" {
t.Errorf("failed testing map claims")
}
}
func initiateJWTHelper(t *testing.T) *JWT {
t.Helper()
j := newJWT(JWTOptions{
SigningKey: "testsigning",
LifetimeMinutes: 2,
})
return j
}

94
logger/logger.go Normal file
View file

@ -0,0 +1,94 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package logger
import (
"io"
"log"
"os"
)
// logs file
var logsFile *os.File
type Logger struct {
infoLogger *log.Logger
warningLogger *log.Logger
errorLogger *log.Logger
debugLogger *log.Logger
}
var l *Logger
type LogsDriver interface {
GetTarget() interface{}
}
type LogFileDriver struct {
FilePath string
}
type LogNullDriver struct{}
func (n LogNullDriver) GetTarget() interface{} {
return nil
}
func (f LogFileDriver) GetTarget() interface{} {
return f.FilePath
}
func NewLogger(driver LogsDriver) *Logger {
if driver.GetTarget() == nil {
l = &Logger{
infoLogger: log.New(io.Discard, "info: ", log.LstdFlags),
warningLogger: log.New(io.Discard, "warning: ", log.LstdFlags),
errorLogger: log.New(io.Discard, "error: ", log.LstdFlags),
debugLogger: log.New(io.Discard, "debug: ", log.LstdFlags),
}
return l
}
path, ok := driver.GetTarget().(string)
if !ok {
panic("something wrong with the file path")
}
logsFile, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
panic(err)
}
l = &Logger{
infoLogger: log.New(logsFile, "info: ", log.LstdFlags),
warningLogger: log.New(logsFile, "warning: ", log.LstdFlags),
errorLogger: log.New(logsFile, "error: ", log.LstdFlags),
debugLogger: log.New(logsFile, "debug: ", log.LstdFlags),
}
return l
}
func ResolveLogger() *Logger {
return l
}
func (l *Logger) Info(msg interface{}) {
l.infoLogger.Println(msg)
}
func (l *Logger) Debug(msg interface{}) {
l.debugLogger.Println(msg)
}
func (l *Logger) Warning(msg interface{}) {
l.warningLogger.Println(msg)
}
func (l *Logger) Error(msg interface{}) {
l.errorLogger.Println(msg)
}
func CloseLogsFile() {
if logsFile != nil {
defer logsFile.Close()
}
}

133
logger/logger_test.go Normal file
View file

@ -0,0 +1,133 @@
package logger
import (
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
)
func TestNewLogger(t *testing.T) {
fp := filepath.Join(t.TempDir(), uuid.NewString())
f, err := os.Create(fp)
if err != nil {
t.Errorf("failed test new logger")
}
f.Close()
l := NewLogger(&LogNullDriver{})
l.Info("testing")
fdrv := &LogFileDriver{
fp,
}
trgt := fdrv.GetTarget()
ts, ok := trgt.(string)
if !ok {
t.Errorf("failed test new logger")
}
if ts != fp {
t.Errorf("failed test new logger")
}
l = NewLogger(fdrv)
l.Error("test-err")
f, err = os.Open(fp)
if err != nil {
t.Errorf("failed test new logger")
}
defer f.Close()
b, err := io.ReadAll(f)
if !strings.Contains(string(b), "test-err") {
t.Errorf("failed test new logger")
}
}
func TestInfo(t *testing.T) {
path := filepath.Join(t.TempDir(), uuid.NewString())
l := NewLogger(&LogFileDriver{
FilePath: path,
})
l.Info("DFT2V56H")
lf, err := os.Open(path)
if err != nil {
t.Error("failed testing info")
}
d, err := io.ReadAll(lf)
if err != nil {
t.Error("error testing info")
}
if !strings.Contains(string(d), "DFT2V56H") {
t.Error("error testing info")
}
t.Cleanup(func() {
CloseLogsFile()
})
}
func TestWarning(t *testing.T) {
path := filepath.Join(t.TempDir(), uuid.NewString())
l := NewLogger(&LogFileDriver{
FilePath: path,
})
l.Warning("DFT2V56H")
lf, err := os.Open(path)
if err != nil {
t.Error("failed testing warning")
}
d, err := io.ReadAll(lf)
if err != nil {
t.Error("failed testing warning")
}
if !strings.Contains(string(d), "DFT2V56H") {
t.Error("failed testing warning")
}
t.Cleanup(func() {
CloseLogsFile()
})
}
func TestDebug(t *testing.T) {
path := filepath.Join(t.TempDir(), uuid.NewString())
l := NewLogger(&LogFileDriver{
FilePath: path,
})
l.Debug("DFT2V56H")
lf, err := os.Open(path)
if err != nil {
t.Error("failed testing debug")
}
d, err := io.ReadAll(lf)
if err != nil {
t.Error("error testing debug")
}
if !strings.Contains(string(d), "DFT2V56H") {
t.Error("error testing debug")
}
t.Cleanup(func() {
CloseLogsFile()
})
}
func TestError(t *testing.T) {
path := filepath.Join(t.TempDir(), uuid.NewString())
l := NewLogger(&LogFileDriver{
FilePath: path,
})
l.Error("DFT2V56H")
lf, err := os.Open(path)
if err != nil {
t.Error("failed testing error")
}
d, err := io.ReadAll(lf)
if err != nil {
t.Error("failed testing error")
}
if !strings.Contains(string(d), "DFT2V56H") {
t.Error("failed testing error")
}
t.Cleanup(func() {
CloseLogsFile()
})
}

183
mailer.go Normal file
View file

@ -0,0 +1,183 @@
// Copyright 2023 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"crypto/tls"
"fmt"
"net/mail"
"os"
"strconv"
"github.com/harranali/mailing"
)
type Mailer struct {
mailer *mailing.Mailer
sender mail.Address
receiver mail.Address
cc []mail.Address
bcc []mail.Address
subject string
htmlBody string
plainText string
attachment string
}
type EmailAddress struct {
Name string // the name can be empty
Address string // ex: john@example.com
}
type EmailAttachment struct {
Name string // name of the file
Path string // full path to the file
}
func initiateMailerWithSMTP() *Mailer {
portStr := os.Getenv("SMTP_PORT")
if portStr == "" {
panic("error reading smtp port env var")
}
port, err := strconv.ParseInt(portStr, 10, 64)
if err != nil {
panic(fmt.Sprintf("error parsing smtp port env var: %v", err))
}
skipTlsVerifyStr := os.Getenv("SMTP_TLS_SKIP_VERIFY_HOST")
if skipTlsVerifyStr == "" {
panic("error reading smtp tls verify env var")
}
skipTlsVerify, err := strconv.ParseBool(skipTlsVerifyStr)
if err != nil {
panic(fmt.Sprintf("error parsing smtp tls verify env var: %v", err))
}
return &Mailer{
mailer: mailing.NewMailerWithSMTP(&mailing.SMTPConfig{
Host: os.Getenv("SMTP_HOST"),
Port: int(port),
Username: os.Getenv("SMTP_USERNAME"),
Password: os.Getenv("SMTP_PASSWORD"),
TLSConfig: tls.Config{
ServerName: os.Getenv("SMTP_HOST"),
InsecureSkipVerify: skipTlsVerify,
},
}),
}
}
func initiateMailerWithSparkPost() *Mailer {
apiVersionStr := os.Getenv("SPARKPOST_API_VERSION")
if apiVersionStr == "" {
panic("error reading sparkpost base url env var")
}
apiVersion, err := strconv.ParseInt(apiVersionStr, 10, 64)
if err != nil {
panic(fmt.Sprintf("error parsing sparkpost base url env var: %v", apiVersion))
}
return &Mailer{
mailer: mailing.NewMailerWithSparkPost(&mailing.SparkPostConfig{
BaseUrl: os.Getenv("SPARKPOST_BASE_URL"),
ApiKey: os.Getenv("SPARKPOST_API_KEY"),
ApiVersion: int(apiVersion),
}),
}
}
func initiateMailerWithSendGrid() *Mailer {
return &Mailer{
mailer: mailing.NewMailerWithSendGrid(&mailing.SendGridConfig{
Host: os.Getenv("SENDGRID_HOST"),
Endpoint: os.Getenv("SENDGRID_ENDPOINT"),
ApiKey: os.Getenv("SENDGRID_API_KEY"),
}),
}
}
func initiateMailerWithMailGun() *Mailer {
skipTlsVerifyStr := os.Getenv("MAILGUN_TLS_SKIP_VERIFY_HOST")
if skipTlsVerifyStr == "" {
panic("error reading mailgun tls verify env var")
}
skipTlsVerify, err := strconv.ParseBool(skipTlsVerifyStr)
if err != nil {
panic(fmt.Sprintf("error parsing mailgun tls verify env var: %v", err))
}
return &Mailer{
mailer: mailing.NewMailerWithMailGun(&mailing.MailGunConfig{
Domain: os.Getenv("MAILGUN_DOMAIN"),
APIKey: os.Getenv("MAILGUN_API_KEY"),
SkipTLSVerification: skipTlsVerify,
}),
}
}
func (m *Mailer) SetFrom(emailAddresses EmailAddress) *Mailer {
e := mailing.EmailAddress{
Name: emailAddresses.Name,
Address: emailAddresses.Address,
}
m.mailer.SetFrom(e)
return m
}
func (m *Mailer) SetTo(emailAddresses []EmailAddress) *Mailer {
var addressesList []mailing.EmailAddress
for _, v := range emailAddresses {
addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address})
}
m.mailer.SetTo(addressesList)
return m
}
func (m *Mailer) SetCC(emailAddresses []EmailAddress) *Mailer {
var addressesList []mailing.EmailAddress
for _, v := range emailAddresses {
addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address})
}
m.mailer.SetCC(addressesList)
return m
}
func (m *Mailer) SetBCC(emailAddresses []EmailAddress) *Mailer {
var addressesList []mailing.EmailAddress
for _, v := range emailAddresses {
addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address})
}
m.mailer.SetBCC(addressesList)
return m
}
func (m *Mailer) SetSubject(subject string) *Mailer {
m.mailer.SetSubject(subject)
return m
}
func (m *Mailer) SetHTMLBody(body string) *Mailer {
m.mailer.SetHTMLBody(body)
return m
}
func (m *Mailer) SetPlainTextBody(body string) *Mailer {
m.mailer.SetPlainTextBody(body)
return m
}
func (m *Mailer) SetAttachments(attachments []EmailAttachment) *Mailer {
var aList []mailing.Attachment
for _, v := range attachments {
aList = append(aList, mailing.Attachment{Name: v.Name, Path: v.Path})
}
m.mailer.SetAttachments(aList)
return m
}
func (m *Mailer) Send() error {
return m.mailer.Send()
}

8
middleware.go Normal file
View file

@ -0,0 +1,8 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
type Middleware func(c *Context)

40
middlewares.go Normal file
View file

@ -0,0 +1,40 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
type Middlewares struct {
middlewares []Middleware
}
var m *Middlewares
func NewMiddlewares() *Middlewares {
m = &Middlewares{}
return m
}
func ResolveMiddlewares() *Middlewares {
return m
}
func (m *Middlewares) Attach(mw Middleware) *Middlewares {
m.middlewares = append(m.middlewares, mw)
return m
}
func (m *Middlewares) GetMiddlewares() []Middleware {
return m.middlewares
}
func (m *Middlewares) getByIndex(i int) Middleware {
for k := range m.middlewares {
if k == i {
return m.middlewares[i]
}
}
return nil
}

65
middlewares_test.go Normal file
View file

@ -0,0 +1,65 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"fmt"
"reflect"
"testing"
)
func TestNewMiddlewares(t *testing.T) {
mw := NewMiddlewares()
if fmt.Sprintf("%T", mw) != "*core.Middlewares" {
t.Errorf("failed testing new middleware")
}
}
func TestResloveMiddleWares(t *testing.T) {
NewMiddlewares()
mw := ResolveMiddlewares()
if fmt.Sprintf("%T", mw) != "*core.Middlewares" {
t.Errorf("failed resolve middlewares")
}
}
func TestAttach(t *testing.T) {
mw := NewMiddlewares()
tmw := Middleware(func(c *Context) {
c.GetLogger().Info("Testing!")
})
mw.Attach(tmw)
mws := mw.getByIndex(0)
if reflect.ValueOf(tmw).Pointer() != reflect.ValueOf(mws).Pointer() {
t.Errorf("Failed testing attach middleware")
}
}
func TestGetMiddleWares(t *testing.T) {
mw := NewMiddlewares()
t1 := Middleware(func(c *Context) {
c.GetLogger().Info("testing1!")
})
t2 := Middleware(func(c *Context) {
c.GetLogger().Info("testing2!")
})
mw.Attach(t1)
mw.Attach(t2)
if len(mw.GetMiddlewares()) != 2 {
t.Errorf("failed testing get middlewares")
}
}
func TestMiddlewareGetByIndex(t *testing.T) {
mw := NewMiddlewares()
t1 := Middleware(func(c *Context) {
c.GetLogger().Info("testing!")
})
mw.Attach(t1)
if reflect.ValueOf(mw.getByIndex(0)).Pointer() != reflect.ValueOf(t1).Pointer() {
t.Errorf("failed testing get by index")
}
}

17
request.go Normal file
View file

@ -0,0 +1,17 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
type Request struct {
httpRequest *http.Request
httpPathParams httprouter.Params
}

183
response.go Normal file
View file

@ -0,0 +1,183 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"fmt"
"net/http"
)
type Response struct {
headers []header
body []byte
statusCode int
contentType string
overrideContentType string
isTerminated bool
redirectTo string
HttpResponseWriter http.ResponseWriter
}
type header struct {
key string
val string
}
// TODO add doc
func (rs *Response) Any(body any) *Response {
if rs.isTerminated == false {
rs.contentType = CONTENT_TYPE_HTML
rs.body = []byte(rs.castBasicVarsToString(body))
}
return rs
}
// TODO add doc
func (rs *Response) Json(body string) *Response {
if rs.isTerminated == false {
rs.contentType = CONTENT_TYPE_JSON
rs.body = []byte(body)
}
return rs
}
// TODO add doc
func (rs *Response) Text(body string) *Response {
if rs.isTerminated == false {
rs.contentType = CONTENT_TYPE_TEXT
rs.body = []byte(body)
}
return rs
}
// TODO add doc
func (rs *Response) HTML(body string) *Response {
if rs.isTerminated == false {
rs.contentType = CONTENT_TYPE_HTML
rs.body = []byte(body)
}
return rs
}
// TODO add doc
func (rs *Response) SetStatusCode(code int) *Response {
if rs.isTerminated == false {
rs.statusCode = code
}
return rs
}
// TODO add doc
func (rs *Response) SetContentType(c string) *Response {
if rs.isTerminated == false {
rs.overrideContentType = c
}
return rs
}
// TODO add doc
func (rs *Response) SetHeader(key string, val string) *Response {
if rs.isTerminated == false {
h := header{
key: key,
val: val,
}
rs.headers = append(rs.headers, h)
}
return rs
}
func (rs *Response) ForceSendResponse() {
rs.isTerminated = true
}
func (rs *Response) Redirect(url string) *Response {
validator := resolveValidator()
v := validator.Validate(map[string]interface{}{
"url": url,
}, map[string]interface{}{
"url": "url",
})
if v.Failed() {
if url[0:1] != "/" {
rs.redirectTo = "/" + url
} else {
rs.redirectTo = url
}
return rs
}
rs.redirectTo = url
return rs
}
func (rs *Response) castBasicVarsToString(data interface{}) string {
switch dataType := data.(type) {
case string:
return fmt.Sprintf("%v", data)
case []byte:
d := data.(string)
return fmt.Sprintf("%v", d)
case int:
intVar, _ := data.(int)
return fmt.Sprintf("%v", intVar)
case int8:
int8Var := data.(int8)
return fmt.Sprintf("%v", int8Var)
case int16:
int16Var := data.(int16)
return fmt.Sprintf("%v", int16Var)
case int32:
int32Var := data.(int32)
return fmt.Sprintf("%v", int32Var)
case int64:
int64Var := data.(int64)
return fmt.Sprintf("%v", int64Var)
case uint:
uintVar, _ := data.(uint)
return fmt.Sprintf("%v", uintVar)
case uint8:
uint8Var := data.(uint8)
return fmt.Sprintf("%v", uint8Var)
case uint16:
uint16Var := data.(uint16)
return fmt.Sprintf("%v", uint16Var)
case uint32:
uint32Var := data.(uint32)
return fmt.Sprintf("%v", uint32Var)
case uint64:
uint64Var := data.(uint64)
return fmt.Sprintf("%v", uint64Var)
case float32:
float32Var := data.(float32)
return fmt.Sprintf("%v", float32Var)
case float64:
float64Var := data.(float64)
return fmt.Sprintf("%v", float64Var)
case complex64:
complex64Var := data.(complex64)
return fmt.Sprintf("%v", complex64Var)
case complex128:
complex128Var := data.(complex128)
return fmt.Sprintf("%v", complex128Var)
case bool:
boolVar := data.(bool)
return fmt.Sprintf("%v", boolVar)
default:
panic(fmt.Sprintf("unsupported response data type %v!", dataType))
}
}
func (rs *Response) reset() {
rs.body = nil
rs.statusCode = http.StatusOK
rs.contentType = CONTENT_TYPE_HTML
rs.overrideContentType = ""
rs.isTerminated = false
rs.redirectTo = ""
}

154
response_test.go Normal file
View file

@ -0,0 +1,154 @@
package core
import (
"fmt"
"testing"
)
func TestWrite(t *testing.T) {
res := Response{}
v := "test-text"
res.Any(v)
if string(res.body) != v {
t.Errorf("failed writing text")
}
}
func TestWriteJson(t *testing.T) {
res := Response{}
j := "{\"name\": \"test\"}"
res.Json(j)
if string(res.body) != j {
t.Errorf("failed wrting jsom")
}
}
func TestSetHeaders(t *testing.T) {
res := Response{}
res.SetHeader("testkey", "testval")
headers := res.headers
if len(headers) < 1 {
t.Errorf("testing set header failed")
}
}
func TestReset(t *testing.T) {
res := Response{}
res.Any("test text")
if res.body == nil {
t.Errorf("expecting body to not be empty, found empty")
}
j := "{\"name\": \"test\"}"
res.Json(j)
if string(res.body) == "" {
t.Errorf("expecting JsonBody to not be empty, found empty")
}
res.reset()
if !(res.body == nil && string(res.body) == "") {
t.Errorf("failed testing response reset()")
}
}
func TestCastBasicVarToString(t *testing.T) {
s := "test str"
r := Response{}
c := r.castBasicVarsToString(s)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var i int = 3
r = Response{}
c = r.castBasicVarsToString(i)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var i8 int8 = 3
r = Response{}
c = r.castBasicVarsToString(i8)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var i16 int16 = 3
r = Response{}
c = r.castBasicVarsToString(i16)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var i32 int32 = 3
r = Response{}
c = r.castBasicVarsToString(i32)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var i64 int64 = 3
r = Response{}
c = r.castBasicVarsToString(i64)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var ui uint = 3
r = Response{}
c = r.castBasicVarsToString(ui)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var ui8 uint8 = 3
r = Response{}
c = r.castBasicVarsToString(ui8)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var ui16 uint16 = 3
r = Response{}
c = r.castBasicVarsToString(ui16)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var ui32 uint32 = 3
r = Response{}
c = r.castBasicVarsToString(ui32)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var ui64 uint64 = 3
r = Response{}
c = r.castBasicVarsToString(ui64)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var f32 float32 = 3
r = Response{}
c = r.castBasicVarsToString(f32)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var f64 float64 = 3
r = Response{}
c = r.castBasicVarsToString(f64)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var c64 complex64 = 3
r = Response{}
c = r.castBasicVarsToString(c64)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var c128 complex128 = 3
r = Response{}
c = r.castBasicVarsToString(c128)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
var b bool = true
r = Response{}
c = r.castBasicVarsToString(b)
if fmt.Sprintf("%T", c) != "string" {
t.Errorf("failed test cast basic var to string")
}
}

104
router.go Normal file
View file

@ -0,0 +1,104 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
type Route struct {
Method string
Path string
Handler Handler
Middlewares []Middleware
}
type Router struct {
Routes []Route
}
var router *Router
func NewRouter() *Router {
router = &Router{
[]Route{},
}
return router
}
func ResolveRouter() *Router {
return router
}
func (r *Router) Get(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: GET,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Post(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: POST,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Delete(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: DELETE,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Patch(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: PATCH,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Put(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: PUT,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Options(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: OPTIONS,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) Head(path string, handler Handler, middlewares ...Middleware) *Router {
r.Routes = append(r.Routes, Route{
Method: HEAD,
Path: path,
Handler: handler,
Middlewares: middlewares,
})
return r
}
func (r *Router) GetRoutes() []Route {
return r.Routes
}

126
router_test.go Normal file
View file

@ -0,0 +1,126 @@
// Copyright 2021 Harran Ali <harran.m@gmail.com>. All rights reserved.
// Copyright (c) 2024 Zeni Kim <zenik@smarteching.com>
// Use of this source code is governed by MIT-style
// license that can be found in the LICENSE file.
package core
import (
"fmt"
"testing"
)
func TestNewRouter(t *testing.T) {
r := NewRouter()
if fmt.Sprintf("%T", r) != "*core.Router" {
t.Error("failed asserting initiation of new router")
}
}
func TestResolveRouter(t *testing.T) {
r := ResolveRouter()
if fmt.Sprintf("%T", r) != "*core.Router" {
t.Error("failed resolve router variable")
}
}
func TestGetRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Get("/", handler)
route := r.GetRoutes()[0]
if route.Method != "get" || route.Path != "/" {
t.Errorf("failed adding route with get http method")
}
}
func TestPostRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Post("/", handler)
route := r.GetRoutes()[0]
if route.Method != "post" || route.Path != "/" {
t.Errorf("failed adding route with post http method")
}
}
func TestDeleteRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Delete("/", handler)
route := r.GetRoutes()[0]
if route.Method != "delete" || route.Path != "/" {
t.Errorf("failed adding route with delete http method")
}
}
func TestPutRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Put("/", handler)
route := r.GetRoutes()[0]
if route.Method != "put" || route.Path != "/" {
t.Errorf("failed adding route with put http method")
}
}
func TestOptionsRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Options("/", handler)
route := r.GetRoutes()[0]
if route.Method != "options" || route.Path != "/" {
t.Errorf("failed adding route with options http method")
}
}
func TestHeadRequest(t *testing.T) {
r := NewRouter()
handler := Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
})
r.Head("/", handler)
route := r.GetRoutes()[0]
if route.Method != "head" || route.Path != "/" {
t.Errorf("failed adding route with head http method")
}
}
func TestAddMultipleRoutes(t *testing.T) {
r := NewRouter()
r.Get("/", Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
}))
r.Post("/", Handler(func(c *Context) *Response {
c.GetLogger().Info(TEST_STR)
return nil
}))
if len(r.GetRoutes()) != 2 {
t.Errorf("failed getting added routes")
}
}

5
testing-config.go Normal file
View file

@ -0,0 +1,5 @@
package core
var testingRequestC = RequestConfig{
MaxUploadFileSize: 20000000, // 20MB
}

10
testingdata/.env Normal file
View file

@ -0,0 +1,10 @@
#################################
### Env Vars ###
#################################
KEY_ONE=VAL_ONE
KEY_TWO=VAL_TWO
# SQLITE
SQLITE_DB=testingdata/db.sqlite

0
testingdata/db.sqlite Normal file
View file

View file

@ -0,0 +1,3 @@
{
"testKey": "testVal"
}

View file

@ -0,0 +1 @@
test copy file

View file

@ -0,0 +1 @@
test move file

242
validator.go Normal file
View file

@ -0,0 +1,242 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/go-ozzo/ozzo-validation/is"
)
type Validator struct{}
type validationResult struct {
hasFailed bool
errorMessages map[string]string
}
var vr validationResult
var v *Validator
func newValidator() *Validator {
v := &Validator{}
return v
}
func resolveValidator() *Validator {
return v
}
func (v *Validator) Validate(data map[string]interface{}, rules map[string]interface{}) validationResult {
vr = validationResult{}
vr.hasFailed = false
res := map[string]string{}
for key, val := range data {
_, ok := rules[key]
if !ok {
continue
}
rls, err := parseRules(rules[key])
if err != nil {
panic(err.Error())
}
err = validation.Validate(val, rls...)
if err != nil {
res[key] = fmt.Sprintf("%v: %v", key, err.Error())
}
}
if len(res) != 0 {
vr.hasFailed = true
vr.errorMessages = res
}
return vr
}
func (vr *validationResult) Failed() bool {
return vr.hasFailed
}
func (vr *validationResult) GetErrorMessagesMap() map[string]string {
return vr.errorMessages
}
func (vr *validationResult) GetErrorMessagesJson() string {
j, err := json.Marshal(vr.GetErrorMessagesMap())
if err != nil {
panic("error converting validation error messages to json")
}
return string(j)
}
func parseRules(rawRules interface{}) ([]validation.Rule, error) {
var res []validation.Rule
rulesStr, ok := rawRules.(string)
if !ok {
return nil, errors.New("invalid validation rule")
}
rules := strings.Split(rulesStr, "|")
for _, rule := range rules {
rule = strings.TrimSpace(rule)
r, err := getRule(rule)
if err != nil {
return nil, err
}
res = append(res, r)
}
return res, nil
}
func getRule(rule string) (validation.Rule, error) {
switch {
case strings.Contains(rule, "max:"):
return getRuleMax(rule)
case strings.Contains(rule, "min:"):
return getRuleMin(rule)
case strings.Contains(rule, "in:"):
return getRuleIn(rule)
case strings.Contains(rule, "dateLayout:"):
return getRuleDateLayout(rule)
case strings.Contains(rule, "length:"):
return getRuleLength(rule)
}
switch rule {
case "required":
return validation.Required, nil
case "email":
return is.Email, nil
case "url":
return is.URL, nil
case "alpha":
return is.Alpha, nil
case "digit":
return is.Digit, nil
case "alphaNumeric":
return is.Alphanumeric, nil
case "lowerCase":
return is.LowerCase, nil
case "upperCase":
return is.UpperCase, nil
case "int":
return is.Int, nil
case "float":
return is.Float, nil
case "uuid":
return is.UUID, nil
case "creditCard":
return is.CreditCard, nil
case "json":
return is.JSON, nil
case "base64":
return is.Base64, nil
case "countryCode2":
return is.CountryCode2, nil
case "countryCode3":
return is.CountryCode3, nil
case "isoCurrencyCode":
return is.CurrencyCode, nil
case "mac":
return is.MAC, nil
case "ip":
return is.IP, nil
case "ipv4":
return is.IPv4, nil
case "ipv6":
return is.IPv6, nil
case "subdomain":
return is.Subdomain, nil
case "domain":
return is.Domain, nil
case "dnsName":
return is.DNSName, nil
case "host":
return is.Host, nil
case "port":
return is.Port, nil
case "mongoID":
return is.MongoID, nil
case "latitude":
return is.Latitude, nil
case "longitude":
return is.Longitude, nil
case "ssn":
return is.SSN, nil
case "semver":
return is.Semver, nil
default:
err := errors.New(fmt.Sprintf("invalid validation rule: %v", rule))
return nil, err
}
}
func getRuleMax(rule string) (validation.Rule, error) {
// max: 44
rr := strings.ReplaceAll(rule, "max:", "")
m := strings.TrimSpace(rr)
n, err := strconv.ParseInt(m, 10, 64)
if err != nil {
err := errors.New("invalid value for validation rule 'max'")
return nil, err
}
return validation.Max(n), err
}
func getRuleMin(rule string) (validation.Rule, error) {
// min: 33
rr := strings.ReplaceAll(rule, "min:", "")
m := strings.TrimSpace(rr)
n, err := strconv.ParseInt(m, 10, 64)
if err != nil {
err := errors.New("invalid value for validation rule 'min'")
return nil, err
}
return validation.Min(n), nil
}
func getRuleIn(rule string) (validation.Rule, error) {
// in: first, second, third
var readyElms []interface{}
rr := strings.ReplaceAll(rule, "in:", "")
elms := strings.Split(rr, ",")
for _, elm := range elms {
readyElms = append(readyElms, strings.TrimSpace(elm))
}
return validation.In(readyElms...), nil
}
// example date layouts: https://programming.guide/go/format-parse-string-time-date-example.html
func getRuleDateLayout(rule string) (validation.Rule, error) {
// dateLayout: 02 January 2006
rr := rule
rr = strings.TrimSpace(strings.Replace(rr, "dateLayout:", "", -1))
return validation.Date(rr), nil
}
func getRuleLength(rule string) (validation.Rule, error) {
// length: 3, 7
rr := rule
rr = strings.Replace(rr, "length:", "", -1)
lengthRange := strings.Split(rr, ",")
if len(lengthRange) < 0 {
err := errors.New("min value is not set for validation rule 'length'")
return nil, err
}
min, err := strconv.Atoi(strings.TrimSpace(lengthRange[0]))
if err != nil {
err := errors.New("min value is not set for validation rule 'length'")
return nil, err
}
if len(lengthRange) < 1 {
err := errors.New("max value is not set for validation rule 'length'")
return nil, err
}
max, err := strconv.Atoi(strings.TrimSpace(lengthRange[1]))
if err != nil {
err := errors.New("max value is not set for validation rule 'length'")
return nil, err
}
return validation.Length(min, max), nil
}

358
validator_test.go Normal file
View file

@ -0,0 +1,358 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/google/uuid"
)
type ruleTestData struct {
ruleName string
correctValue interface{}
correctValueExpectedResult interface{}
incorrectValue interface{}
incorrectValueExpectedResult error
}
type rulesTestData []ruleTestData
var rulesTestDataList = []ruleTestData{
{
ruleName: "required",
correctValue: gofakeit.Name(),
correctValueExpectedResult: nil,
incorrectValue: "",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "email",
correctValue: gofakeit.Email(),
correctValueExpectedResult: nil,
incorrectValue: "test@mailcom",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "url",
correctValue: gofakeit.URL(),
correctValueExpectedResult: nil,
incorrectValue: "http:/githubcom/goffe/ggoffeor",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "alpha",
correctValue: gofakeit.LoremIpsumWord(),
correctValueExpectedResult: nil,
incorrectValue: "test232",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "digit",
correctValue: gofakeit.Digit(),
correctValueExpectedResult: nil,
incorrectValue: "d",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "alphaNumeric",
correctValue: "abc3",
correctValueExpectedResult: nil,
incorrectValue: "!",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "lowerCase",
correctValue: "abc",
correctValueExpectedResult: nil,
incorrectValue: "ABC",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "upperCase",
correctValue: "ABC",
correctValueExpectedResult: nil,
incorrectValue: "abc",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "int",
correctValue: "343",
correctValueExpectedResult: nil,
incorrectValue: "342.3",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "float",
correctValue: "433.5",
correctValueExpectedResult: nil,
incorrectValue: gofakeit.LoremIpsumWord(),
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "uuid",
correctValue: uuid.NewString(),
correctValueExpectedResult: nil,
incorrectValue: gofakeit.LoremIpsumWord(),
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "creditCard",
correctValue: "4242 4242 4242 4242",
correctValueExpectedResult: nil,
incorrectValue: "dd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "json",
correctValue: "{\"testKEy\": \"testVal\"}",
correctValueExpectedResult: nil,
incorrectValue: "dd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "base64",
correctValue: "+rxVsR0pD0DU4XO4MZbXXg==",
correctValueExpectedResult: nil,
incorrectValue: "dd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "countryCode2",
correctValue: "SD",
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "countryCode3",
correctValue: "SDN",
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "isoCurrencyCode",
correctValue: "USD",
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "mac",
correctValue: gofakeit.MacAddress(),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "ip",
correctValue: gofakeit.IPv4Address(),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "ipv4",
correctValue: gofakeit.IPv4Address(),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "ipv6",
correctValue: gofakeit.IPv6Address(),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "domain",
correctValue: "site.com",
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "latitude",
correctValue: fmt.Sprintf("%v", gofakeit.Latitude()),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "longitude",
correctValue: fmt.Sprintf("%v", gofakeit.Longitude()),
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
{
ruleName: "semver",
correctValue: "3.2.4",
correctValueExpectedResult: nil,
incorrectValue: "ddd",
incorrectValueExpectedResult: errors.New("mock error"),
},
}
func TestValidatorValidate(t *testing.T) {
validator := newValidator()
v := validator.Validate(map[string]interface{}{
"name": gofakeit.LoremIpsumWord(),
"link": gofakeit.URL(),
},
map[string]interface{}{
"name": "required|alphaNumeric",
"link": "required|url",
},
)
if v.Failed() {
t.Errorf("erro testing validator validate: '%v'", v.GetErrorMessagesJson())
}
v = validator.Validate(map[string]interface{}{
"name": "",
"link": gofakeit.URL(),
},
map[string]interface{}{
"name": "required|alphaNumeric",
"link": "required|url",
},
)
if !v.Failed() {
t.Errorf("erro testing validator validate")
}
msgsMap := v.GetErrorMessagesMap()
if !strings.Contains(msgsMap["name"], "cannot be blank") {
t.Errorf("erro testing validator validate")
}
msgsJson := v.GetErrorMessagesJson()
var masgsMapOfJ map[string]interface{}
json.Unmarshal([]byte(msgsJson), &masgsMapOfJ)
sval, _ := masgsMapOfJ["name"].(string)
if !strings.Contains(sval, "cannot be blank") {
t.Errorf("erro testing validator validate")
}
}
func TestValidatorParseRules(t *testing.T) {
_, err := parseRules("344")
if err == nil {
t.Errorf("failed testing validation parse rules")
}
rules, err := parseRules("required| min: 3|length: 3, 5")
if err != nil {
t.Errorf("failed testing validation parse rules: '%v'", err.Error())
}
if len(rules) != 3 {
t.Errorf("failed testing validation parse rules")
}
}
func TestValidatorGetRule(t *testing.T) {
for _, td := range rulesTestDataList {
r, err := getRule(td.ruleName)
if err != nil {
t.Errorf("failed testing validation rule '%v'", td.ruleName)
}
err = r.Validate(td.correctValue)
if err != nil {
t.Errorf("failed testing validation rule '%v'", td.ruleName)
}
err = r.Validate(td.incorrectValue)
if err == nil {
t.Errorf("failed testing validation rule %v", td.ruleName)
}
}
_, err := getRule("unknownrule")
if err == nil {
t.Errorf("failed testing validation rule")
}
}
func TestValidatorGetRuleMax(t *testing.T) {
r, err := getRuleMax("max: 33")
if err != nil {
t.Errorf("failed testing validation rule 'max'")
}
err = r.Validate(30)
if err != nil {
t.Errorf("failed testing validation rule 'max': %v", err.Error())
}
err = r.Validate(40)
if err == nil {
t.Errorf("failed testing validation rule 'max'")
}
}
func TestValidatorGetRuleMin(t *testing.T) {
r, err := getRuleMin("min: 33")
if err != nil {
t.Errorf("failed testing validation rule 'min'")
}
err = r.Validate(34)
if err != nil {
t.Errorf("failed testing validation rule 'min': %v", err.Error())
}
err = r.Validate(3)
if err == nil {
t.Errorf("failed testing validation rule 'min'")
}
}
func TestValidatorGetRuleIn(t *testing.T) {
r, err := getRuleIn("in: a, b, c")
if err != nil {
t.Errorf("failed testing validation rule 'in'")
}
err = r.Validate("a")
if err != nil {
t.Errorf("failed testing validation rule 'in': %v", err)
}
}
func TestGetValidationRuleDateLayout(t *testing.T) {
r, err := getRuleDateLayout("dateLayout: 02 January 2006")
if err != nil {
t.Errorf("failed testing validation rule 'dateLayout'")
}
err = r.Validate("02 May 2023")
if err != nil {
t.Errorf("failed testing validation rule 'dateLayout': %v", err.Error())
}
err = r.Validate("02-04-2023")
if err == nil {
t.Errorf("failed testing validation rule 'dateLayout'")
}
}
func TestValidatorGetRuleLenth(t *testing.T) {
r, err := getRuleLength("length: 3, 5")
if err != nil {
t.Errorf("failed test validation rule 'length': %v", err.Error())
}
err = r.Validate("123")
if err != nil {
t.Errorf("failed test validation rule 'length': %v", err.Error())
}
err = r.Validate("12")
if err == nil {
t.Errorf("failed test validation rule 'length'")
}
err = r.Validate("123456")
if err == nil {
t.Errorf("failed test validation rule 'length'")
}
r, err = getRuleLength("length: 3dd, 5")
if err == nil {
t.Errorf("failed test validation rule 'length'")
}
r, err = getRuleLength("length: 3, 5dd")
if err == nil {
t.Errorf("failed test validation rule 'length'")
}
}