Cleaning up unused packages
This commit is contained in:
@@ -16,6 +16,7 @@ require (
|
||||
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d
|
||||
github.com/crusttech/go-oidc v0.0.0-20180918092017-982855dad3e1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b
|
||||
@@ -43,7 +44,6 @@ require (
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
|
||||
github.com/lib/pq v1.1.0
|
||||
github.com/markbates/goth v1.50.0
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.0
|
||||
github.com/minio/minio-go/v6 v6.0.39
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
@@ -52,7 +52,6 @@ require (
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v0.9.3
|
||||
github.com/rakyll/gotest v0.0.5 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009
|
||||
@@ -63,7 +62,6 @@ require (
|
||||
github.com/steinfletcher/apitest v1.3.8
|
||||
github.com/steinfletcher/apitest-jsonpath v1.3.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f
|
||||
go.uber.org/atomic v1.6.0
|
||||
go.uber.org/zap v1.15.0
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
|
||||
|
||||
@@ -63,8 +63,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v0.3.17 h1:NGWgggJJqTofUcTV1E7hkk2zVjZ54EfJa1z5O3z6By4=
|
||||
@@ -83,8 +81,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -180,15 +176,6 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.50.0 h1:KCAErbDdHh11gQAJs/GV73LCv4NwA7Z6wNZAU32ggMc=
|
||||
github.com/markbates/goth v1.50.0/go.mod h1:zZmAw0Es0Dpm7TT/4AdN14QrkiWLMrrU9Xei1o+/mdA=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
@@ -238,8 +225,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rakyll/gotest v0.0.5 h1:+BrdqPxKPDaxvhtIiVzfiYXLhi4BrSOqdwaEiA7qjpk=
|
||||
github.com/rakyll/gotest v0.0.5/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
@@ -275,8 +260,6 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f h1:sTgdEUlmmNSU3QKe8n20jR26zYEKw6ciSuj919XobV4=
|
||||
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f/go.mod h1:VFd2XRrQZoX9cOxpeZezKpOlXDwU/dbRejKLjwP+xY8=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
@@ -340,7 +323,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -350,9 +332,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
|
||||
@@ -408,8 +387,6 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
workspace:
|
||||
base: /factory
|
||||
|
||||
kind: pipeline
|
||||
name: crust
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:1.12-alpine
|
||||
pull: always
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
GOPROXY: https://proxy.golang.org
|
||||
CI: travis
|
||||
commands:
|
||||
- go fmt ./...
|
||||
- go test ./... -v -cover --tags="integration"
|
||||
|
||||
services:
|
||||
- name: factory-db
|
||||
pull: always
|
||||
image: percona:8.0
|
||||
command: [ "--innodb_lock_wait_timeout=2" ]
|
||||
ports:
|
||||
- 3306
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: bRxJ37sJ6Qu4
|
||||
MYSQL_DATABASE: factory
|
||||
MYSQL_USER: factory
|
||||
MYSQL_PASSWORD: factory
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
/vendor
|
||||
/coverage.*
|
||||
/Gopkg.*
|
||||
/.env
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2017+ Tit Petric <black@scene-si.org>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
.PHONY: all
|
||||
|
||||
all:
|
||||
drone exec
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
# factory
|
||||
|
||||
Go factories for some common things which I use.
|
||||
|
||||
- Database (sqlx + mysql & pq) + utility functions
|
||||
- Database query profiler for Stdout or Memory ([]string)
|
||||
- Sonyflake ID generator for uint64 ids
|
||||
|
||||
Written by [@TitPetric](https://twitter.com/TitPetric) and licensed under the permissive [WTFPL](http://www.wtfpl.net/txt/copying/).
|
||||
-578
@@ -1,578 +0,0 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"database/sql"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/titpetric/factory/logger"
|
||||
)
|
||||
|
||||
// DatabaseCredential is a configuration struct for a database connection
|
||||
type DatabaseCredential struct {
|
||||
DSN string
|
||||
DriverName string
|
||||
Connector func() (*sql.DB, error)
|
||||
}
|
||||
|
||||
// DatabaseFactory contains all database credentials and instances
|
||||
type DatabaseFactory struct {
|
||||
credentials map[string]DatabaseCredential
|
||||
instances map[string]*DB
|
||||
}
|
||||
|
||||
// The default database factory
|
||||
var Database *DatabaseFactory
|
||||
|
||||
func init() {
|
||||
Database = &DatabaseFactory{}
|
||||
Database.credentials = make(map[string]DatabaseCredential)
|
||||
Database.instances = make(map[string]*DB)
|
||||
}
|
||||
|
||||
// Add a new named database credential to the database factory
|
||||
//
|
||||
// The function will store a named database credential for use with
|
||||
// the Get function. Generally you'd need to call Add at least once.
|
||||
// In order to set the default database credential, use "default" as
|
||||
// the first parameter.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// factory.Database.Add("default", factory.DatabaseCredential{"mysql", "sqlapi:sqlapi@tcp(db1:3306)/sqlapi?collation=utf8mb4_general_ci"})
|
||||
// ```
|
||||
//
|
||||
// By default, additional options will be added to the credentials DSN:
|
||||
//
|
||||
// - collation will be set to `utf8_general_ci`
|
||||
// - parseTime will be set to `true`
|
||||
// - loc will be set to `Local`
|
||||
//
|
||||
// If your passed DSN will include any of these options, the default values will not
|
||||
// be applied, and your custom settings will be honored.
|
||||
func (r *DatabaseFactory) Add(name string, credentials DatabaseCredential) {
|
||||
credentials.DSN = r.CleanDSN(credentials.DriverName, credentials.DSN)
|
||||
r.credentials[name] = credentials
|
||||
}
|
||||
|
||||
func (r *DatabaseFactory) CleanDSN(driverName string, dsn string) string {
|
||||
addOption := func(s, match, option string) string {
|
||||
if !strings.Contains(s, match) {
|
||||
s += option
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
dsn = addOption(dsn, "?", "?")
|
||||
dsn = addOption(dsn, "collation=", "&collation=utf8_general_ci")
|
||||
dsn = addOption(dsn, "parseTime=", "&parseTime=true")
|
||||
dsn = addOption(dsn, "loc=", "&loc=Local")
|
||||
dsn = strings.Replace(dsn, "?&", "?", 1)
|
||||
return dsn
|
||||
}
|
||||
|
||||
// getCredentials returns the DatabaseCredential{} for a given db name
|
||||
func (r *DatabaseFactory) getCredentials(name string) (*DatabaseCredential, error) {
|
||||
if value, ok := r.credentials[name]; ok {
|
||||
return &value, nil
|
||||
}
|
||||
return nil, errors.New("No configuration found for database: " + name)
|
||||
}
|
||||
|
||||
// Get returns a database connection
|
||||
//
|
||||
// If you don't request a database connection by name, `factory.Database.Get()` will
|
||||
// return the connection with the name "default". If you supply one or more names as
|
||||
// the parameter, the first successful connection will be returned.
|
||||
//
|
||||
// It is fine to call this function per request, as a singleton instance is returned
|
||||
// for each call with the same parameters.
|
||||
//
|
||||
// This behavior enables sharding workloads between hosts. One could randomize the
|
||||
// parameters using [math/rand#Shuffle](https://tip.golang.org/pkg/math/rand/#Shuffle), or
|
||||
// provide a consistent hashing method based on server hostname/IP, or even go so far
|
||||
// to retrieve the hosts to connect to from some sort of inventory like etcd.
|
||||
//
|
||||
// The most general use case is that you will only call `factory.Database.Get()` once,
|
||||
// and then pass the resulting `*DB` forward in your application. Requesting custom named
|
||||
// connections also provides a way to access different parts of the database, or different
|
||||
// database altogether, depending on your microservice data distribution.
|
||||
func (r *DatabaseFactory) Get(dbName ...string) (*DB, error) {
|
||||
names := dbName
|
||||
if len(names) == 0 {
|
||||
names = []string{"default"}
|
||||
}
|
||||
for _, name := range names {
|
||||
if value, ok := r.instances[name]; ok {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
credentials, err := r.getCredentials(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if credentials.DriverName == "" {
|
||||
return nil, errors.New("Credentials missing DriverName")
|
||||
}
|
||||
|
||||
var handle *sqlx.DB
|
||||
|
||||
switch true {
|
||||
case credentials.Connector != nil:
|
||||
db, err := credentials.Connector()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
handle = sqlx.NewDb(db, credentials.DriverName)
|
||||
default:
|
||||
handle, err = sqlx.Connect(credentials.DriverName, credentials.DSN)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
r.instances[name] = &DB{
|
||||
handle,
|
||||
context.Background(),
|
||||
0,
|
||||
nil,
|
||||
&sql.TxOptions{
|
||||
ReadOnly: false,
|
||||
},
|
||||
logger.Silent{},
|
||||
}
|
||||
return r.instances[name], nil
|
||||
}
|
||||
return nil, fmt.Errorf("No configuration found for database: %v", names)
|
||||
}
|
||||
|
||||
// MustGet is the same as Get, except it will panic on connection error
|
||||
func (r *DatabaseFactory) MustGet(dbName ...string) *DB {
|
||||
db, err := r.Get(dbName...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// DB struct encapsulates sqlx.DB to add new functions
|
||||
type DB struct {
|
||||
*sqlx.DB
|
||||
|
||||
ctx context.Context
|
||||
|
||||
inTx int32
|
||||
Tx *sqlx.Tx
|
||||
TxOpts *sql.TxOptions
|
||||
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// Quiet will return a DB handle without a logger
|
||||
func (r *DB) Quiet() *DB {
|
||||
return &DB{
|
||||
r.DB,
|
||||
r.ctx,
|
||||
r.inTx,
|
||||
r.Tx,
|
||||
r.TxOpts,
|
||||
logger.Silent{},
|
||||
}
|
||||
}
|
||||
|
||||
// With will return a DB handle with a bound context (throw-away)
|
||||
func (r *DB) With(ctx context.Context) *DB {
|
||||
return &DB{
|
||||
r.DB,
|
||||
ctx,
|
||||
r.inTx,
|
||||
r.Tx,
|
||||
r.TxOpts,
|
||||
r.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Set a custom logger
|
||||
func (r *DB) SetLogger(logger logger.Logger) {
|
||||
r.logger = logger
|
||||
}
|
||||
|
||||
// Begin will create a transaction in the DB with a context
|
||||
func (r *DB) Begin() (err error) {
|
||||
if r.inTx > 0 {
|
||||
_, err = r.Exec(fmt.Sprintf("SAVEPOINT sp_%d", r.inTx))
|
||||
}
|
||||
if r.inTx == 0 {
|
||||
if r.ctx == nil {
|
||||
r.Log(func() {
|
||||
r.Tx, err = r.DB.Beginx()
|
||||
}, "BEGIN;")
|
||||
} else {
|
||||
r.Log(func() {
|
||||
r.Tx, err = r.DB.BeginTxx(r.ctx, r.TxOpts)
|
||||
}, "BEGIN; -- with context")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
r.inTx++
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transaction will create a transaction and invoke a callback
|
||||
func (r *DB) Transaction(callback func() error) (err error) {
|
||||
var try int
|
||||
|
||||
// Perform transaction statements
|
||||
try = 0
|
||||
for {
|
||||
// Start transaction
|
||||
if err = r.Begin(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = callback(); err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
try++
|
||||
if try > 3 {
|
||||
r.logger.Log(r.ctx, fmt.Sprintf("Retried transaction %d times, aborting", try-1))
|
||||
break
|
||||
}
|
||||
|
||||
// Break out if the causer is not a MySQL error
|
||||
cause, ok := (errors.Cause(err)).(*mysql.MySQLError)
|
||||
if !ok {
|
||||
r.logger.Log(r.ctx, "Returned error cause is not a MySQLError", logger.NewField("err", err))
|
||||
break
|
||||
}
|
||||
|
||||
// restart transaction:
|
||||
// - 1205: lock within transaction (unit tested),
|
||||
// - 1213: deadlock found
|
||||
if cause.Number == 1205 || cause.Number == 1213 {
|
||||
r.logger.Log(r.ctx, "Retrying transaction", logger.NewField("try", try), logger.NewField("cause", cause))
|
||||
r.Rollback()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
r.logger.Log(r.ctx, "Can't handle transaction error, aborting")
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.Rollback()
|
||||
return err
|
||||
}
|
||||
return r.Commit()
|
||||
}
|
||||
|
||||
func (r *DB) Commit() (err error) {
|
||||
if r.Tx != nil {
|
||||
if r.inTx <= 1 {
|
||||
r.Log(func() {
|
||||
err = r.Tx.Commit()
|
||||
}, "COMMIT;")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Tx = nil
|
||||
r.inTx = 0
|
||||
return nil
|
||||
}
|
||||
if _, err = r.Exec(fmt.Sprintf("RELEASE SAVEPOINT sp_%d", r.inTx-1)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.inTx--
|
||||
return nil
|
||||
}
|
||||
return errors.WithStack(sql.ErrTxDone)
|
||||
}
|
||||
|
||||
func (r *DB) Rollback() (err error) {
|
||||
if r.Tx != nil {
|
||||
if r.inTx <= 1 {
|
||||
r.Log(func() {
|
||||
err = r.Tx.Rollback()
|
||||
}, "ROLLBACK;")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.Tx = nil
|
||||
r.inTx = 0
|
||||
return nil
|
||||
}
|
||||
if _, err = r.Exec(fmt.Sprintf("ROLLBACK SAVEPOINT sp_%d", r.inTx-1)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
r.inTx--
|
||||
return nil
|
||||
}
|
||||
return errors.WithStack(sql.ErrTxDone)
|
||||
}
|
||||
|
||||
// SetFields will provide a string with SQL named bindings from a string slice
|
||||
func (r *DB) SetFields(fields []string) string {
|
||||
idx := 0
|
||||
sql := ""
|
||||
for _, field := range fields {
|
||||
if idx > 0 {
|
||||
sql = sql + ", "
|
||||
}
|
||||
idx++
|
||||
sql = sql + field + "=:" + field
|
||||
}
|
||||
return sql
|
||||
}
|
||||
|
||||
// NamedExec adds profiling on top of the parent DB.NameExec
|
||||
func (r *DB) NamedExec(query string, arg interface{}) (res sql.Result, err error) {
|
||||
exec := func() (sql.Result, error) {
|
||||
if r.Tx != nil {
|
||||
return r.Tx.NamedExecContext(r.ctx, query, arg)
|
||||
}
|
||||
return r.DB.NamedExecContext(r.ctx, query, arg)
|
||||
}
|
||||
|
||||
r.Log(func() {
|
||||
res, err = exec()
|
||||
}, query, arg)
|
||||
|
||||
return res, errors.Wrap(err, "exec query failed")
|
||||
}
|
||||
|
||||
// Exec adds profiling on top of the parent DB.Exec
|
||||
func (r *DB) Exec(query string, args ...interface{}) (res sql.Result, err error) {
|
||||
exec := func() (sql.Result, error) {
|
||||
if r.Tx != nil {
|
||||
return r.Tx.ExecContext(r.ctx, query, args...)
|
||||
}
|
||||
return r.DB.ExecContext(r.ctx, query, args...)
|
||||
}
|
||||
|
||||
r.Log(func() {
|
||||
res, err = exec()
|
||||
}, query, args...)
|
||||
|
||||
return res, errors.Wrap(err, "exec query failed")
|
||||
}
|
||||
|
||||
// Select is a helper function that will ignore sql.ErrNoRows
|
||||
func (r *DB) Select(dest interface{}, query string, args ...interface{}) error {
|
||||
var err error
|
||||
exec := func() error {
|
||||
if r.Tx != nil {
|
||||
return r.Tx.SelectContext(r.ctx, dest, query, args...)
|
||||
}
|
||||
return r.DB.SelectContext(r.ctx, dest, query, args...)
|
||||
}
|
||||
|
||||
r.Log(func() {
|
||||
err = exec()
|
||||
}, query, args...)
|
||||
|
||||
// clear no rows returned error
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "select query failed")
|
||||
}
|
||||
|
||||
// Get is a helper function that will ignore sql.ErrNoRows
|
||||
func (r *DB) Get(dest interface{}, query string, args ...interface{}) error {
|
||||
var err error
|
||||
exec := func() error {
|
||||
if r.Tx != nil {
|
||||
return r.Tx.GetContext(r.ctx, dest, query, args...)
|
||||
}
|
||||
return r.DB.GetContext(r.ctx, dest, query, args...)
|
||||
}
|
||||
|
||||
r.Log(func() {
|
||||
err = exec()
|
||||
}, query, args...)
|
||||
|
||||
// clear no rows returned error
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "get query failed")
|
||||
}
|
||||
|
||||
// set uses reflection to iterate over struct fields tags, producing bindings for struct values
|
||||
func (r *DB) set(data interface{}, allowed ...string) string {
|
||||
set := r.setMap(data, allowed...)
|
||||
return r.setImplode(", ", set)
|
||||
}
|
||||
|
||||
func (r *DB) tag(tag string) string {
|
||||
if tag != "" && tag != "-" {
|
||||
return strings.Split(tag, ",")[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *DB) setMap(data interface{}, allowed ...string) map[string]string {
|
||||
message_value := reflect.ValueOf(data)
|
||||
if message_value.Kind() == reflect.Ptr {
|
||||
message_value = message_value.Elem()
|
||||
}
|
||||
|
||||
set := make(map[string]string)
|
||||
length := message_value.NumField()
|
||||
for i := 0; i < length; i++ {
|
||||
fieldType := message_value.Type().Field(i)
|
||||
if tag := r.tag(fieldType.Tag.Get("db")); tag != "" {
|
||||
set[tag] = ":" + tag
|
||||
}
|
||||
}
|
||||
|
||||
// limit only to allowed fields
|
||||
if len(allowed) > 0 {
|
||||
for tag, _ := range set {
|
||||
canDelete := true
|
||||
for _, key := range allowed {
|
||||
if tag == key {
|
||||
canDelete = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if canDelete {
|
||||
delete(set, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (r *DB) setImplode(delimiter string, set map[string]string) string {
|
||||
result := ""
|
||||
count := 0
|
||||
for key, value := range set {
|
||||
if count > 0 {
|
||||
result = result + delimiter
|
||||
}
|
||||
result = result + key + "=" + value
|
||||
count++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Update is a helper function which will issue an `update` statement to the db
|
||||
func (r *DB) Update(table string, args interface{}, keys ...string) error {
|
||||
var err error
|
||||
if len(keys) == 0 {
|
||||
return errors.New("Full-table update not supported")
|
||||
}
|
||||
set := r.setMap(args)
|
||||
setWhere := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
value, ok := set[key]
|
||||
if !ok {
|
||||
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
|
||||
}
|
||||
delete(set, key)
|
||||
setWhere[key] = value
|
||||
}
|
||||
if len(set) == 0 {
|
||||
return errors.New("Encountered update struct with no fields")
|
||||
}
|
||||
query := "update " + table + " set " + r.setImplode(", ", set) + " where " + r.setImplode(" AND ", setWhere)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdatePartial is a helper function which will issue an `update` statement to the db
|
||||
func (r *DB) UpdatePartial(table string, args interface{}, allowed []string, keys ...string) error {
|
||||
var err error
|
||||
if len(keys) == 0 {
|
||||
return errors.New("Full-table update not supported")
|
||||
}
|
||||
set := r.setMap(args, allowed...)
|
||||
setWhere := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
value, ok := set[key]
|
||||
if !ok {
|
||||
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
|
||||
}
|
||||
delete(set, key)
|
||||
setWhere[key] = value
|
||||
}
|
||||
if len(set) == 0 {
|
||||
return errors.New("Encountered update struct with no fields")
|
||||
}
|
||||
query := "update " + table + " set " + r.setImplode(", ", set) + " where " + r.setImplode(" AND ", setWhere)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete is a helper function which will issue an `delete` statement to the db
|
||||
func (r *DB) Delete(table string, args interface{}, keys ...string) error {
|
||||
var err error
|
||||
if len(keys) == 0 {
|
||||
return errors.New("Full-table delete not supported")
|
||||
}
|
||||
set := r.setMap(args)
|
||||
setWhere := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
value, ok := set[key]
|
||||
if !ok {
|
||||
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
|
||||
}
|
||||
delete(set, key)
|
||||
setWhere[key] = value
|
||||
}
|
||||
query := "delete from " + table + " where " + r.setImplode(" AND ", setWhere)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
// Replace is a helper function which will issue an `replace` statement to the database
|
||||
func (r *DB) Replace(table string, args interface{}) error {
|
||||
var err error
|
||||
query := "replace into " + table + " set " + r.set(args)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert is a helper function which will issue an `insert` statement to the database
|
||||
func (r *DB) Insert(table string, args interface{}) error {
|
||||
var err error
|
||||
query := "insert into " + table + " set " + r.set(args)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertIgnore is a helper function which will issue an `insert ignore` statement to the database
|
||||
func (r *DB) InsertIgnore(table string, args interface{}) error {
|
||||
var err error
|
||||
query := "insert ignore into " + table + " set " + r.set(args)
|
||||
_, err = r.NamedExec(query, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DB) Log(callback func(), query string, args ...interface{}) {
|
||||
start := time.Now()
|
||||
callback()
|
||||
duration := time.Since(start).Seconds()
|
||||
r.logger.Log(r.ctx, query, logger.NewField("duration", duration), logger.NewField("args", args))
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/logger"
|
||||
)
|
||||
|
||||
// DatabaseConnectionOptions is a configuration struct for connection retry
|
||||
type DatabaseConnectionOptions struct {
|
||||
DSN string
|
||||
DriverName string
|
||||
Logger string
|
||||
|
||||
Retries int
|
||||
RetryTimeout time.Duration
|
||||
ConnectTimeout time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
dsnMasker = regexp.MustCompile("(.)(?:.*)(.):(.)(?:.*)(.)@")
|
||||
)
|
||||
|
||||
func (df *DatabaseFactory) TryToConnect(ctx context.Context, name string, options *DatabaseConnectionOptions) (db *DB, err error) {
|
||||
df.Add(name, DatabaseCredential{
|
||||
DSN: options.DSN,
|
||||
DriverName: options.DriverName,
|
||||
})
|
||||
|
||||
var (
|
||||
connErrCh = make(chan error, 1)
|
||||
)
|
||||
|
||||
// We'll not add this to the general log because we do not want to carry it with us for every query.
|
||||
dsnField := fmt.Sprintf("dsn=%s", dsnMasker.ReplaceAllString(options.DSN, "$1****$2:$3****$4@"))
|
||||
|
||||
defer close(connErrCh)
|
||||
|
||||
log.Println("connecting to database", dsnField)
|
||||
|
||||
go func() {
|
||||
var (
|
||||
try = 0
|
||||
)
|
||||
|
||||
for {
|
||||
try++
|
||||
|
||||
if options.Retries <= try {
|
||||
err = errors.Errorf("could not connect to %q, in %d tries", name, try)
|
||||
return
|
||||
}
|
||||
|
||||
db, err = df.Get(name)
|
||||
if err != nil {
|
||||
log.Println(
|
||||
"could not connect to the database",
|
||||
err,
|
||||
dsnField,
|
||||
fmt.Sprintf("try=%d", try),
|
||||
)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Forced break
|
||||
break
|
||||
case <-time.After(options.RetryTimeout):
|
||||
// Wait before next try
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
connErrCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err = <-connErrCh:
|
||||
break
|
||||
case <-time.After(options.ConnectTimeout):
|
||||
// Wait before next try
|
||||
return nil, errors.Errorf("db init for %q timed out", name)
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Errorf("db connection for %q cancelled", name)
|
||||
}
|
||||
|
||||
switch options.Logger {
|
||||
case "stdout":
|
||||
db.SetLogger(logger.Default{})
|
||||
default:
|
||||
db.SetLogger(logger.Silent{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
module github.com/titpetric/factory
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.4.1
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/lib/pq v1.0.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009
|
||||
)
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009 h1:3wBL/e/qjpSYaXacpbIV+Bsj/nwQ4UO1llG/av54zzw=
|
||||
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009/go.mod h1:dVvZuWJd174umvm5g8CmZD6S2GWwHKtpK/0ZPHswuNo=
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
)
|
||||
|
||||
type (
|
||||
Default struct{}
|
||||
)
|
||||
|
||||
func (Default) Log(ctx context.Context, msg string, fields ...Field) {
|
||||
args := make([]interface{}, len(fields)+1)
|
||||
args[0] = msg
|
||||
for key, value := range fields {
|
||||
args[key+1] = value
|
||||
}
|
||||
// fields satisfy fmt.Stringer as well
|
||||
log.Println(args...)
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
field struct {
|
||||
name string
|
||||
value interface{}
|
||||
}
|
||||
)
|
||||
|
||||
func NewField(name string, value interface{}) Field {
|
||||
return field{name, value}
|
||||
}
|
||||
|
||||
func (f field) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f field) Value() interface{} {
|
||||
return f.value
|
||||
}
|
||||
|
||||
func (f field) String() string {
|
||||
return fmt.Sprintf("%s=%v", f.name, f.value)
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
Logger interface {
|
||||
Log(ctx context.Context, message string, fields ...Field)
|
||||
}
|
||||
|
||||
Field interface {
|
||||
Name() string
|
||||
Value() interface{}
|
||||
|
||||
fmt.Stringer
|
||||
}
|
||||
)
|
||||
|
||||
func New() Logger {
|
||||
return Default{}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type (
|
||||
Silent struct{}
|
||||
)
|
||||
|
||||
func (Silent) Log(ctx context.Context, msg string, fields ...Field) {
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
# resputil
|
||||
|
||||
The package provides utilities to respond with some structured JSON payloads.
|
||||
By default, anything you pass to `JSON` will be encapsulated depending on the type.
|
||||
|
||||
~~~go
|
||||
func JSON(w http.ResponseWriter, responses ...interface{}) {
|
||||
~~~
|
||||
|
||||
The `response` variadic parameter may be any of the following:
|
||||
|
||||
- string
|
||||
- error
|
||||
- int
|
||||
- struct
|
||||
- nil
|
||||
- interface
|
||||
- func() error
|
||||
- func() (interface{}, error)
|
||||
- func() ([]byte, error)
|
||||
|
||||
It will output the first non-empty value. In the case where the result is taken
|
||||
from a `interface{}, error` return, it will first output the error if it's not
|
||||
empty, and then output the value, *even if empty*. For all other cases it will
|
||||
skip to the next item in the responses parameter.
|
||||
|
||||
A parameter of the type of `func() ([]byte, error)` will not return a wrapped structure.
|
||||
It's assumed the `[]byte` result is an as-is payload which should be returned. A typical
|
||||
use case is to return the output of `json.Marshal` which matches this signature.
|
||||
|
||||
The motivation behind it is to provide more reasonable error handling, when you
|
||||
want to break out of your function with less code. It's in part an attempt to get
|
||||
rid of all the `if err != nil {` checks in your code, but at the same time it's
|
||||
also something that changes how your code might be laid in order to fully take
|
||||
advantage of what it gives you.
|
||||
|
||||
For example, when writing APIs, you might structure your API call into several
|
||||
logical units, that have different responsibilities:
|
||||
|
||||
- request validation (parameters)
|
||||
- request processing (issuing SQL queries based on parameters)
|
||||
- this one may be significantly broken down into many stages
|
||||
- the actual response payload
|
||||
|
||||
Taking advantage of scope, this may look like this:
|
||||
|
||||
~~~go
|
||||
mux.HandleFunc("/api/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
owner := login.Decode(r)
|
||||
call := chi.URLParam(r, "*")
|
||||
|
||||
// validate request
|
||||
validate := func() error {
|
||||
if owner == "" {
|
||||
return errors.New("Missing login info. Try to relogin")
|
||||
}
|
||||
if call == "" {
|
||||
return errors.New("Unknown API call")
|
||||
}
|
||||
if r.Method == "POST" {
|
||||
return errors.Wrap(r.ParseForm(), "Error parsing POST data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// process request
|
||||
process := func() (interface{}, error) {
|
||||
params := map[string]interface{}{
|
||||
"owner": owner,
|
||||
}
|
||||
urlQuery := r.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
params[name] = param[0]
|
||||
}
|
||||
postVars := r.Form
|
||||
for name, param := range postVars {
|
||||
params[name] = param[0]
|
||||
}
|
||||
return sqlAPI(call, params)
|
||||
}
|
||||
|
||||
// process request
|
||||
resputil.JSON(w, validate, process)
|
||||
})
|
||||
~~~
|
||||
|
||||
Since `validate` and `process` are closures, they may access anything within the scope of
|
||||
their parent function. This means that you can have a `RequestParameters` struct, a response
|
||||
struct, and actually extend the logic of this further. This would be one possible way:
|
||||
|
||||
~~~go
|
||||
// Parameters
|
||||
params := &CommentListThread{
|
||||
CommentList: &CommentList{
|
||||
NewsID: chi.URLParam(r, "id"),
|
||||
SessionID: r.URL.Query().Get("session_id"),
|
||||
Sort: r.URL.Query().Get("sort"),
|
||||
Order: r.URL.Query().Get("order"),
|
||||
PageNumber: parseInt64(r.URL.Query().Get("pageNumber")),
|
||||
PageSize: parseInt64(r.URL.Query().Get("pageSize")),
|
||||
},
|
||||
SelfID: parseInt64(r.URL.Query().Get("self_id")),
|
||||
}
|
||||
|
||||
/* steps:
|
||||
|
||||
0. validate inputs
|
||||
1. with self_id=0 parameters:
|
||||
a. get comments with self_id 0 in the pagenumber/pagesize range,
|
||||
b. get all child comments with parent comment IDs,
|
||||
c. add 5 comments with date/asc to parent comments
|
||||
d. return comments data
|
||||
2. with self_id>0 parameters:
|
||||
a. get comments with self_id X in the pagenumber/pagesize range,
|
||||
b. return comments data
|
||||
|
||||
*/
|
||||
|
||||
// Parameters are included in the response
|
||||
result := params
|
||||
|
||||
validate := func() error {
|
||||
if !is(params.Sort, "date", "rating") {
|
||||
params.Sort = "date"
|
||||
}
|
||||
if !is(params.Order, "asc", "desc") {
|
||||
params.Order = "asc"
|
||||
}
|
||||
if params.PageNumber < 0 {
|
||||
params.PageNumber = 0
|
||||
}
|
||||
if params.PageSize < 10 {
|
||||
params.PageSize = 10
|
||||
}
|
||||
if params.PageSize > 100 {
|
||||
params.PageSize = 100
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// more code here ...
|
||||
|
||||
resputil.JSON(w, validate, process, addReplies, result)
|
||||
~~~
|
||||
|
||||
The parent function is broken down into closures, that represent some stage of the issued
|
||||
request. Depending on what works for you, each stage individually can produce an error using
|
||||
a `func() error` declaration like shown here. If a non-empty value is returned, it will
|
||||
be encoded into JSON and written to the HTTP output.
|
||||
|
||||
This pattern of use also allows a more functional approach to what you're responding with. For example,
|
||||
if you favor something closer to an ORM approach, then you could do something more similar to this:
|
||||
|
||||
~~~go
|
||||
func (p *ProjectHTTP) create(w http.ResponseWriter, r *http.Request) {
|
||||
project := Project{}.New()
|
||||
resputil.JSON(
|
||||
w,
|
||||
project.SetName(r.PostFormValue("name")),
|
||||
project.Save(),
|
||||
project,
|
||||
)
|
||||
}
|
||||
~~~
|
||||
|
||||
In this case, both `SetName` and `Save` will be invoked, regardless of the fact if `SetName` returned
|
||||
an error. Care should be taken between mixing `error` and `func() error` parameters, due to the order
|
||||
in which they will be invoked. In the above case, both SetName and Save are invoked before `JSON()`, but
|
||||
if we would omit `()` from `Save()`, the Save function would be invoked by `JSON()`.
|
||||
|
||||
## Tests
|
||||
|
||||
The package has 100% code coverage, but errors are possible. Due to the fact that the implementation is
|
||||
aimed at generic "take anything" use, errors may occur at runtime.
|
||||
|
||||
## Other notes
|
||||
|
||||
- The package makes use of `pkg/errors`, returning a stack trace in the JSON response if configured with `SetConfig`
|
||||
- There are helper functions `OK()` and `Success(string)` to format successful messages (`{ "success": { "message": "..." } }`)
|
||||
- Errors will be formatted as `{ "error": { "message": "..." } }` [according to some Google conventions](https://cloud.google.com/storage/docs/json_api/v1/status-codes) which may or may not have a RFC
|
||||
- HTTP response codes are not honored, you will always get a 200 OK response and a descriptive JSON payload
|
||||
- valid responses are nested within `{ "response": ... }`
|
||||
- if the set of all passed data to respond with it empty or all it's values are empty, `{ "response": false }` will be returned
|
||||
|
||||
## License
|
||||
|
||||
Written by [@TitPetric](https://twitter.com/TitPetric) and licensed under the permissive [WTFPL](http://www.wtfpl.net/txt/copying/).
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
package resputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Exported error messages
|
||||
const (
|
||||
E_EMPTY_TRACE = "no stack trace available"
|
||||
)
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
type successMessage struct {
|
||||
Success struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"success"`
|
||||
}
|
||||
|
||||
type errorMessage struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Trace string `json:"trace,omitempty"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// Options struct / configuration parameters
|
||||
type Options struct {
|
||||
Pretty bool // formats JSON output with indentation
|
||||
Trace bool // prints a stack backtrace if exists (pkg/errors)
|
||||
Logger func(error)
|
||||
}
|
||||
|
||||
var config Options
|
||||
|
||||
// SetConfig updates package options in use
|
||||
func SetConfig(options Options) {
|
||||
config = options
|
||||
}
|
||||
|
||||
// getTrace prints the first available stack trace if any
|
||||
func getTrace(errs ...error) string {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
terr, ok := err.(stackTracer)
|
||||
if ok {
|
||||
return fmt.Sprintf("%+v", terr.StackTrace())
|
||||
}
|
||||
}
|
||||
}
|
||||
return E_EMPTY_TRACE
|
||||
}
|
||||
|
||||
// error returns a structured error for API responses
|
||||
func errorResponse(err ...error) errorMessage {
|
||||
response := errorMessage{}
|
||||
// add stack trace to the response if available and enabled
|
||||
response.Error.Message = "Unknown error"
|
||||
if len(err) > 0 {
|
||||
if config.Trace {
|
||||
response.Error.Trace = getTrace(errors.Cause(err[0]), err[0])
|
||||
}
|
||||
response.Error.Message = err[0].Error()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// Success returns a structured success message for API responses
|
||||
func Success(success ...string) successMessage {
|
||||
response := successMessage{}
|
||||
response.Success.Message = "OK"
|
||||
if len(success) > 0 {
|
||||
response.Success.Message = success[0]
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// OK returns the default Success message
|
||||
func OK() successMessage {
|
||||
return Success()
|
||||
}
|
||||
|
||||
// JSON responds with the first non-nil payload, formats error messages
|
||||
func JSON(w http.ResponseWriter, responses ...interface{}) {
|
||||
respond := func(payload interface{}) {
|
||||
var result []byte
|
||||
var err error
|
||||
encode := func(payload interface{}) ([]byte, error) {
|
||||
if config.Pretty {
|
||||
return json.MarshalIndent(payload, "", "\t")
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
switch value := payload.(type) {
|
||||
case []byte:
|
||||
result = value
|
||||
case error:
|
||||
// main key is "error"
|
||||
errWithStack := errors.WithStack(value)
|
||||
if config.Logger != nil {
|
||||
config.Logger(errWithStack)
|
||||
}
|
||||
result, err = encode(errorResponse(errWithStack))
|
||||
case successMessage:
|
||||
// main key is "success"
|
||||
result, err = encode(value)
|
||||
default:
|
||||
// main key is "response"
|
||||
result, err = encode(struct {
|
||||
Response interface{} `json:"response"`
|
||||
}{value})
|
||||
}
|
||||
if err != nil {
|
||||
result, _ = encode(errorResponse(errors.WithStack(err)))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(result)
|
||||
}
|
||||
|
||||
for _, response := range responses {
|
||||
switch value := response.(type) {
|
||||
case nil:
|
||||
// this will match a nil error
|
||||
continue
|
||||
case func() ([]byte, error):
|
||||
result, err := value()
|
||||
JSON(w, err, result)
|
||||
case func() (interface{}, error):
|
||||
result, err := value()
|
||||
JSON(w, err, result)
|
||||
case func() error:
|
||||
err := value()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
respond(err)
|
||||
case *error:
|
||||
if *value == nil {
|
||||
continue
|
||||
}
|
||||
respond(*value)
|
||||
case error:
|
||||
respond(value)
|
||||
case string:
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
respond(value)
|
||||
case bool:
|
||||
if !value {
|
||||
continue
|
||||
}
|
||||
respond(value)
|
||||
case successMessage:
|
||||
respond(value)
|
||||
default:
|
||||
respond(value)
|
||||
}
|
||||
// Exit on the first output...
|
||||
return
|
||||
}
|
||||
respond(false)
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Semaphore provides a run-once type of flag (atomic)
|
||||
type Semaphore struct {
|
||||
semaphore int32
|
||||
}
|
||||
|
||||
// CanRun will allow a caller to proceed
|
||||
func (l *Semaphore) CanRun() bool {
|
||||
return atomic.CompareAndSwapInt32(&l.semaphore, 0, 1)
|
||||
}
|
||||
|
||||
// Done finishes the run
|
||||
func (l *Semaphore) Done() {
|
||||
atomic.CompareAndSwapInt32(&l.semaphore, 1, 0)
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"github.com/sony/sonyflake"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SonyflakeFactory is a configuration struct
|
||||
type SonyflakeFactory struct {
|
||||
*sonyflake.Sonyflake
|
||||
}
|
||||
|
||||
// Sonyflake is the active ID generator instance
|
||||
var Sonyflake *SonyflakeFactory
|
||||
|
||||
func init() {
|
||||
Sonyflake = &SonyflakeFactory{
|
||||
sonyflake.NewSonyflake(sonyflake.Settings{
|
||||
StartTime: time.Unix(1503550784, 0),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// NextID returns uint64 ID, escalates possible error to a panic
|
||||
func (s *SonyflakeFactory) NextID() uint64 {
|
||||
// sonyflake errors out only when the time overflows, that will
|
||||
// occur in approximately 174 years after the custom epoch.
|
||||
// If the 10ms keyspace is exhausted, NextID will sleep and return
|
||||
// an ID from the next interval. It can't fail, because the generator
|
||||
// is protected by a Mutex.
|
||||
id, err := s.Sonyflake.NextID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
Vendored
-4
@@ -168,10 +168,6 @@ github.com/steinfletcher/apitest-jsonpath
|
||||
# github.com/stretchr/testify v1.6.1
|
||||
github.com/stretchr/testify/assert
|
||||
github.com/stretchr/testify/require
|
||||
# github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f
|
||||
github.com/titpetric/factory
|
||||
github.com/titpetric/factory/logger
|
||||
github.com/titpetric/factory/resputil
|
||||
# go.uber.org/atomic v1.6.0
|
||||
go.uber.org/atomic
|
||||
# go.uber.org/multierr v1.5.0
|
||||
|
||||
Reference in New Issue
Block a user