├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── project │ ├── internal │ ├── debug.go │ ├── flags.go │ ├── project.go │ └── repositories.go │ └── main.go ├── database ├── database.yml ├── schema │ ├── notification │ │ ├── 20190613042222_sms_history.down.sql │ │ └── 20190613042222_sms_history.up.sql │ └── user │ │ ├── 20191017070146_users.down.sql │ │ └── 20191017070146_users.up.sql └── setup.sh ├── debug ├── bucket │ └── private-image │ │ └── .keep └── user │ └── user.go ├── docker-compose.yml ├── docs ├── images │ ├── design.png │ ├── import_cyclic.png │ ├── microservice_cylic.png │ ├── microservice_interaction.png │ ├── microservice_user.png │ ├── microservice_user_order.png │ ├── nsq_throttle_design.png │ ├── service_boundaries1.png │ ├── service_package.png │ ├── service_package_cyclic.png │ └── service_package_dependencies.png ├── project_design.md └── service_boundaries.md ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── config.test.toml │ └── test_generator │ │ └── main.go ├── entity │ ├── amenities │ │ ├── amenities.go │ │ └── const.go │ ├── authentication │ │ ├── authentication.go │ │ └── const.go │ ├── booking │ │ ├── booking.go │ │ ├── const.go │ │ └── error.go │ ├── device │ │ └── device.go │ ├── image │ │ ├── const.go │ │ ├── error.go │ │ ├── image.go │ │ └── image_test.go │ ├── invoice │ │ ├── const.go │ │ └── invoice.go │ ├── notification │ │ ├── const.go │ │ └── notification.go │ ├── oauth2 │ │ └── oauth2.go │ ├── order │ │ ├── const.go │ │ └── order.go │ ├── otp │ │ ├── const.go │ │ ├── errors.go │ │ └── otp.go │ ├── property │ │ ├── const.go │ │ └── property.go │ ├── pushmessage │ │ └── pushmessage.go │ ├── room │ │ ├── const.go │ │ └── room.go │ ├── secret │ │ ├── const.go │ │ └── secret.go │ ├── session │ │ ├── const.go │ │ ├── context.go │ │ ├── error.go │ │ └── session.go │ ├── sms │ │ └── sms.go │ ├── state │ │ ├── const.go │ │ ├── errors.go │ │ └── state.go │ └── user │ │ ├── const.go │ │ ├── error.go │ │ ├── register.go │ │ └── user.go ├── featureflag │ ├── consul │ │ └── consul.go │ ├── etcd │ │ ├── docker-compose.yaml │ │ └── etcd.go │ └── featureflag.go ├── kothak │ ├── README.md │ ├── kothak.go │ ├── object_storage.go │ ├── redis.go │ └── sql.go ├── objstoragepath │ └── objstoragepath.go ├── pkg │ ├── README.md │ ├── context │ │ └── context.go │ ├── conv │ │ ├── conv.go │ │ └── conv_test.go │ ├── cucumber │ │ ├── README.md │ │ ├── api.go │ │ ├── api_test.go │ │ ├── cucumber.go │ │ ├── cucumber_test.go │ │ ├── features │ │ │ └── api.feature │ │ ├── file.go │ │ ├── file_test.go │ │ └── test.log │ ├── defaults │ │ ├── README.md │ │ ├── defaults.go │ │ └── defaults_test.go │ ├── envfile │ │ ├── README.md │ │ ├── envfile.go │ │ └── testfile │ │ │ └── envfile.yaml │ ├── http │ │ ├── client │ │ │ └── client.go │ │ ├── misc │ │ │ └── misc.go │ │ ├── monitoring │ │ │ └── monitoring.go │ │ ├── request │ │ │ ├── README.md │ │ │ ├── header.go │ │ │ ├── request.go │ │ │ └── request_test.go │ │ └── response │ │ │ ├── response.go │ │ │ └── response_test.go │ ├── log │ │ ├── log.go │ │ └── logger │ │ │ ├── logger.go │ │ │ ├── logrus │ │ │ └── logrus.go │ │ │ ├── std │ │ │ ├── README.md │ │ │ └── std.go │ │ │ ├── zap │ │ │ └── zap.go │ │ │ └── zerolog │ │ │ └── zerolog.go │ ├── nsq │ │ ├── README.md │ │ ├── fakensq │ │ │ ├── README.md │ │ │ └── fakensq.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── middleware.go │ │ ├── middleware_test.go │ │ ├── nsq.go │ │ ├── nsq_test.go │ │ └── nsqio │ │ │ └── nsqio.go │ ├── objectstorage │ │ ├── README.md │ │ ├── gcs │ │ │ └── gcs.go │ │ ├── local │ │ │ ├── local.go │ │ │ ├── local_test.go │ │ │ └── testfile │ │ │ │ └── copy1.txt │ │ ├── objectstorage.go │ │ ├── objectstorage_test.go │ │ ├── s3 │ │ │ ├── s3.go │ │ │ └── s3_test.go │ │ └── testbucket │ │ │ ├── haloha.txt.attrs │ │ │ ├── objectstorage.go.attrs │ │ │ └── testdownload.txt │ ├── randgen │ │ └── randgen.go │ ├── redis │ │ ├── mock │ │ │ └── redis_mock.go │ │ ├── redigo │ │ │ ├── key.go │ │ │ ├── list.go │ │ │ ├── pipeline.go │ │ │ └── redigo.go │ │ └── redis.go │ ├── router │ │ ├── README.md │ │ └── router.go │ ├── sqldb │ │ ├── slqdb.go │ │ └── sqldb_context.go │ ├── tempe │ │ ├── tempe.go │ │ └── tempe_test.go │ ├── time │ │ └── time.go │ └── ulid │ │ ├── ulid.go │ │ └── ulid_test.go ├── repository │ ├── amenities │ │ └── amenities.go │ ├── image │ │ ├── image.go │ │ └── image_test.go │ ├── invoice │ │ └── invoice.go │ ├── otp │ │ └── otp.go │ ├── property │ │ └── property.go │ ├── repository.go │ ├── secret │ │ └── secret.go │ ├── session │ │ └── session.go │ ├── state │ │ └── state.go │ └── user │ │ ├── session.go │ │ └── user.go ├── server │ ├── admin.go │ ├── debug │ │ ├── handler.go │ │ ├── image │ │ │ └── .keep │ │ ├── server.go │ │ └── user │ │ │ └── user.go │ ├── main │ │ ├── authentication │ │ │ └── README.md │ │ ├── booking │ │ │ └── booking.go │ │ ├── handler.go │ │ ├── oauth2 │ │ │ ├── README.md │ │ │ └── oauth2.go │ │ ├── server.go │ │ └── user │ │ │ └── README.md │ └── server.go ├── third-party │ ├── facebook │ │ └── oauth │ │ │ └── oauth.go │ ├── firebase │ │ └── pushmessage │ │ │ ├── const.go │ │ │ └── pushmessage.go │ ├── google │ │ └── oauth │ │ │ └── oauth.go │ └── nexmo │ │ └── sms │ │ └── sms.go ├── usecase │ ├── authentication │ │ ├── README.md │ │ └── authentication.go │ ├── booking │ │ └── booking.go │ ├── image │ │ ├── README.md │ │ ├── image.go │ │ ├── image_test.go │ │ └── mock │ │ │ └── image_mock.go │ ├── notification │ │ ├── notification.go │ │ ├── pushtemplate.go │ │ ├── smstemplate.go │ │ └── template.go │ ├── oauth2 │ │ ├── oauth2.go │ │ └── scope.go │ ├── order │ │ └── order.go │ ├── otp │ │ └── otp.go │ ├── property │ │ └── property.go │ ├── room │ │ └── room.go │ ├── secret │ │ └── secret.go │ ├── session │ │ ├── README.md │ │ └── session.go │ ├── state │ │ ├── README.md │ │ └── state.go │ ├── usecase.go │ └── user │ │ └── user.go └── xerrors │ ├── README.md │ ├── example │ └── example.go │ └── xerrors.go ├── project.config.toml ├── project.env.toml └── scripts └── install_dependencies.sh /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | # services: 11 | # postgres: 12 | # image: postgres:10.8 13 | # env: 14 | # POSTGRES_USER: projectdb 15 | # POSTGRES_PASSWORD: projectdb 16 | # ports: 17 | # # will assign a random free host port 18 | # - 5444/tcp 19 | # # needed because the postgres container does not provide a healthcheck 20 | # options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 21 | 22 | # redis: 23 | # image: redis 24 | # ports: 25 | # - 6379/tcp 26 | # options: --entrypoint redis-server 27 | 28 | - name: Set up Go 1.13 29 | uses: actions/setup-go@v1 30 | with: 31 | go-version: 1.13 32 | id: go 33 | 34 | - name: Check out code into the Go module directory 35 | uses: actions/checkout@v1 36 | 37 | - name: Get dependencies 38 | run: | 39 | go get -v -t -d ./... 40 | if [ -f Gopkg.toml ]; then 41 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 42 | dep ensure 43 | fi 44 | 45 | - name: Test 46 | run: go test -v ./... 47 | 48 | - name: Build 49 | run: go build -v . 50 | 51 | - name: Testconfig 52 | run: make testconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ide 2 | .vscode 3 | 4 | # sql 5 | schema.sql 6 | 7 | # experiment 8 | experiment 9 | tmp 10 | 11 | # bin 12 | \projectbackend 13 | 14 | # log 15 | projectlog -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | mainprogram=projectbackend 2 | build_commit=$(shell git rev-parse HEAD) 3 | build_version=$(shell git describe --tags 2> /dev/null || echo "dev-$(shell git rev-parse HEAD)") 4 | 5 | .PHONY: install-deps 6 | install-deps: 7 | @./scripts/install_dependencies.sh 8 | 9 | .PHONY: version 10 | version: 11 | echo $(build_version) 12 | echo $(build_commit) 13 | 14 | .PHONY: build 15 | build: 16 | @go build -v \ 17 | -ldflags "-X main.buildVersion=$(build_version) \ 18 | -X main.buildCommit=$(build_commit)" \ 19 | -race \ 20 | -o $(mainprogram) cmd/project/*.go 21 | 22 | .PHONY: run 23 | run: 24 | make build 25 | @./$(mainprogram) \ 26 | -config_file="./project.config.toml" \ 27 | -env_file="./project.env.toml" \ 28 | -tz="Asia/Jakarta" 29 | 30 | .PHONY: test 31 | test: 32 | @go generate -v ./... 33 | @go test -race -v ./... 34 | 35 | .PHONY: testconfig 36 | testconfig: 37 | make build 38 | @./$(mainprogram) \ 39 | -config_file=./project.config.toml \ 40 | -env_file=./project.env.toml \ 41 | -debug=-testconfig=1 \ 42 | -devserver=1 \ 43 | -tz=Asia/Jakarta 44 | 45 | .PHONY: dbup 46 | dbup: 47 | @cd database && ./setup.sh create database.yml 48 | 49 | .PHONY: dbdown 50 | dbdown: 51 | @cd database && ./setup.sh drop database.yml -------------------------------------------------------------------------------- /cmd/project/internal/debug.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | debugserver "github.com/albertwidi/go-project-example/internal/server/debug" 5 | ) 6 | 7 | func newDebugServer(address string, r *Repositories) (*debugserver.Server, error) { 8 | usecases := debugserver.Usecases{} 9 | s, err := debugserver.New(address, usecases) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | return s, nil 15 | } 16 | -------------------------------------------------------------------------------- /cmd/project/internal/flags.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "flag" 5 | "regexp" 6 | ) 7 | 8 | // debugFlag holds the debug flag structure 9 | // spec: 10 | // debug flag is a flag.Parse value 11 | // for example: -debug=-server=1-testconfig=1 12 | type debugFlag struct { 13 | flag string 14 | DevServer bool 15 | TestConfig bool 16 | } 17 | 18 | // String return the value of the flag 19 | func (df *debugFlag) String() string { 20 | return df.flag 21 | } 22 | 23 | // Set string value to debug flag 24 | func (df *debugFlag) Set(value string) error { 25 | if value == "" { 26 | return nil 27 | } 28 | 29 | // find pattern -{flag_name}={flag_value} 30 | regex, err := regexp.Compile("-[a-zA-Z0-9]+=[a-zA-Z0-9]+") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | df.flag = value 36 | fs := flag.CommandLine 37 | fs.BoolVar(&df.DevServer, "devserver", false, "for activating dev server") 38 | fs.BoolVar(&df.TestConfig, "testconfig", false, "for testing the project configuration") 39 | return fs.Parse(regex.FindAllString(value, -1)) 40 | } 41 | 42 | type envFileFlag struct { 43 | flag string 44 | envFiles []string 45 | } 46 | 47 | func (vf *envFileFlag) String() string { 48 | return vf.flag 49 | } 50 | 51 | func (vf *envFileFlag) Set(value string) error { 52 | vf.envFiles = append(vf.envFiles, value) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /cmd/project/internal/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/albertwidi/go-project-example/internal/config" 13 | "github.com/albertwidi/go-project-example/internal/kothak" 14 | lg "github.com/albertwidi/go-project-example/internal/pkg/log/logger" 15 | "github.com/albertwidi/go-project-example/internal/pkg/log/logger/zap" 16 | "github.com/albertwidi/go-project-example/internal/server" 17 | ) 18 | 19 | // Flags of project 20 | type Flags struct { 21 | Debug debugFlag 22 | EnvironmentFile envFileFlag 23 | TimeZone string 24 | ConfigurationFile string 25 | LogFile string 26 | Version bool 27 | } 28 | 29 | // Config of project 30 | type Config struct { 31 | config.DefaultConfig 32 | } 33 | 34 | // Run the project 35 | func Run(f Flags) error { 36 | // set default timezone 37 | os.Setenv("TZ", f.TimeZone) 38 | 39 | // load project configuration 40 | projectConfig := Config{} 41 | if err := config.ParseFile(f.ConfigurationFile, &projectConfig, f.EnvironmentFile.envFiles...); err != nil { 42 | return err 43 | } 44 | 45 | // initiate project logger 46 | logger, err := zap.New(&lg.Config{ 47 | Level: lg.StringToLevel(projectConfig.Log.Level), 48 | LogFile: projectConfig.Log.File, 49 | UseColor: projectConfig.Log.Color, 50 | }) 51 | if err != nil { 52 | return fmt.Errorf("run: error when initiating logger: %w", err) 53 | } 54 | 55 | if f.Debug.TestConfig { 56 | logger.Infof("testing config with flags and configurations:") 57 | logger.Infof("flags:\n%+v", f) 58 | logger.Infof("config:\n%+v", projectConfig) 59 | } 60 | 61 | resources, err := kothak.New(context.TODO(), projectConfig.Resources, logger) 62 | if err != nil { 63 | return err 64 | } 65 | // close all connections when program exiting 66 | defer resources.CloseAll() 67 | 68 | // repositories 69 | repo, err := newRepositories(resources) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // initiate new servers 75 | newMainServer() 76 | debugServerAddr := projectConfig.Servers.Debug.Address 77 | debugServer, err := newDebugServer(debugServerAddr, repo) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | s, err := server.New(projectConfig.Servers.Admin.Address, debugServer) 83 | if err != nil { 84 | return err 85 | } 86 | // run the server 87 | errChan := s.Run() 88 | sigChan := make(chan os.Signal, 1) 89 | signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) 90 | // exit early if we only test config 91 | testChan := make(chan struct{}, 1) 92 | if f.Debug.TestConfig { 93 | logger.Infoln("testing: giving time for server to run") 94 | go func() { 95 | time.Sleep(time.Second * 5) 96 | testChan <- struct{}{} 97 | }() 98 | } 99 | 100 | select { 101 | case err := <-errChan: 102 | return err 103 | case sig := <-sigChan: 104 | switch sig { 105 | case syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT: 106 | return errors.New("project: receive signal to terminate program") 107 | } 108 | case <-testChan: 109 | logger.Infoln("testing: test completed successfully") 110 | return nil 111 | } 112 | return nil 113 | } 114 | 115 | func newMainServer() { 116 | 117 | } 118 | -------------------------------------------------------------------------------- /cmd/project/internal/repositories.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/albertwidi/go-project-example/internal/kothak" 5 | "github.com/albertwidi/go-project-example/internal/repository/image" 6 | ) 7 | 8 | // Repositories list 9 | type Repositories struct { 10 | Image *image.Repository 11 | } 12 | 13 | func newRepositories(resources *kothak.Kothak) (*Repositories, error) { 14 | r := Repositories{} 15 | 16 | // iamge repository 17 | imageRepo := image.New(resources.MustGetRedis("image")) 18 | r.Image = imageRepo 19 | return &r, nil 20 | } 21 | -------------------------------------------------------------------------------- /cmd/project/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | project "github.com/albertwidi/go-project-example/cmd/project/internal" 9 | ) 10 | 11 | var ( 12 | buildVersion string 13 | buildCommit string 14 | ) 15 | 16 | const ( 17 | usage = `Usage: 18 | backend -config_file=./project.config.toml \ 19 | -env_file=./project.env.toml 20 | ` 21 | ) 22 | 23 | func main() { 24 | exitCode := 0 25 | f := project.Flags{} 26 | flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } 27 | flag.StringVar(&f.ConfigurationFile, "config_file", "./aha.config.toml", "configuration file of the project") 28 | flag.Var(&f.EnvironmentFile, "env_file", "helper file for environment variable configuration") 29 | flag.StringVar(&f.TimeZone, "tz", "", "time zone of the project") 30 | flag.BoolVar(&f.Version, "version", false, "to print version of the prgoram") 31 | flag.Var(&f.Debug, "debug", "turn on debug mode, this will set log level to debug") 32 | flag.Parse() 33 | 34 | if f.Version { 35 | fmt.Fprintf(os.Stderr, "version: %s\ncommit: %s\n", buildVersion, buildCommit) 36 | return 37 | } 38 | if err := project.Run(f); err != nil { 39 | exitCode = 1 40 | fmt.Printf("%v", err) 41 | } 42 | os.Exit(exitCode) 43 | } 44 | -------------------------------------------------------------------------------- /database/database.yml: -------------------------------------------------------------------------------- 1 | users: 2 | dialect: postgres 3 | database: users 4 | host: localhost 5 | port: 5444 6 | user: projectdb 7 | password: projectdb 8 | 9 | notifications: 10 | dialect: postgres 11 | database: notifications 12 | host: localhost 13 | port: 5444 14 | user: projectdb 15 | password: projectdb -------------------------------------------------------------------------------- /database/schema/notification/20190613042222_sms_history.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS notification_types; 2 | DROP TABLE IF EXISTS notification_providers; 3 | DROP TABLE IF EXISTS notification_purpose; 4 | DROP TABLE IF EXISTS sms_history; 5 | DROP TABLE IF EXISTS email_history; 6 | DROP TABLE IF EXISTS user_notifications; 7 | DROP TABLE IF EXISTS user_notifications_detail; -------------------------------------------------------------------------------- /database/schema/user/20191017070146_users.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | CREATE TABLE users( 3 | id uuid PRIMARY KEY, 4 | hash_id varchar(6) NOT NULL, 5 | user_type smallint NOT NULL, 6 | user_status smallint NOT NULL, 7 | phone_number varchar(20) NOT NULL, 8 | email varchar(255) NOT NULL, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp, 11 | is_test boolean NOT NULL, 12 | UNIQUE(phone_number) 13 | ); -------------------------------------------------------------------------------- /database/schema/user/20191017070146_users.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS users; 2 | CREATE TABLE users( 3 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 4 | hash_id varchar(8) NOT NULL, 5 | user_type smallint NOT NULL, 6 | user_status smallint NOT NULL, 7 | phone_number varchar(20) NOT NULL, 8 | email varchar(255) NOT NULL, 9 | created_at timestamp NOT NULL, 10 | updated_at timestamp, 11 | is_test boolean NOT NULL, 12 | UNIQUE(phone_number) 13 | ); 14 | 15 | DROP INDEX IF EXISTS idx_users_hash; 16 | CREATE INDEX idx_users_hash ON users(hash_id); 17 | 18 | DROP TABLE IF EXISTS users_bio; 19 | CREATE TABLE users_bio( 20 | user_id bigint PRIMARY KEY, 21 | full_name varchar(60) NOT NULL, 22 | occupation varchar(30) NOT NULL, 23 | gender smallint NOT NULL, 24 | birthday date NOT NULL, 25 | avatar TEXT, 26 | created_at timestamp NOT NULL, 27 | updated_at timestamp, 28 | updated_by varchar(36), 29 | is_test boolean NOT NULL 30 | ); 31 | 32 | DROP TABLE IF EXISTS user_secrets; 33 | CREATE TABLE user_secrets( 34 | id uuid PRIMARY KEY, 35 | user_id bigint NOT NULL, 36 | secret_key varchar(30) NOT NULL, -- secret key is unique per user_id 37 | secret_value varchar(100) NOT NULL, 38 | created_at timestamp NOT NULL, 39 | created_by BIGINT NOT NULL, 40 | updated_at timestamp, 41 | updated_by varchar(36), 42 | is_test boolean NOT NULL, 43 | UNIQUE(user_id, secret_key) 44 | ); 45 | 46 | DROP TABLE IF EXISTS registrations; 47 | CREATE TABLE registrations( 48 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 49 | user_id uuid, 50 | user_type int NOT NULL, 51 | user_status int NOT NULL, 52 | ktp_id bigint NOT NULL, 53 | full_name varchar(60), 54 | birthdate date NOT NULL, 55 | email varchar(255) NOT NULL, 56 | phone_number varchar(20) NOT NULL, 57 | gender smallint NOT NULL, 58 | channel smallint, 59 | device smallint, 60 | latitude varchar(20), 61 | longitude varchar(20), 62 | device_token varchar(200), 63 | created_at timestamp NOT NULL, 64 | updated_at timestamp, 65 | is_test boolean NOT NULL, 66 | ); 67 | 68 | DROP TABLE IF EXISTS scopes; 69 | CREATE TABLE scopes( 70 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 71 | name varchar(60) NOT NULL, 72 | description text NOT NULL, 73 | created_at timestamp NOT NULL, 74 | created_by string NOT NULL, 75 | updated_by string, 76 | updated_at timestamp 77 | ); 78 | 79 | -- insert default scopes 80 | INSERT INTO scopes("kos:search", "search for kos", NOW(), 1) 81 | 82 | DROP TABLE IF EXISTS user_credentials; 83 | CREATE TABLE users_credentials( 84 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 85 | user_id uuid, 86 | token_name varchar(60), 87 | scopes []string, 88 | created_at timestamp NOT NULL, 89 | created_by uuid, 90 | UNIQUE(user_id, token_name) 91 | ) 92 | 93 | DROP TABLE IF EXISTS user_token; 94 | CREATE TABLE users_token( 95 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 96 | user_id uuid, 97 | credential_id uuid, 98 | client_id varchar(60), 99 | client_secret varchar(60), 100 | refresh_token varchar(60), 101 | created_at timestamp NOT NULL 102 | created_by uuid 103 | ) 104 | 105 | DROP INDEX IF EXISTS idx_credential_id; 106 | CREATE INDEX idx_credential_id ON users_token(credential_id); 107 | 108 | DROP INDEX IF EXISTS idx_client_id; 109 | CREATE INDEX idx_client_id ON users_token(client_id); 110 | 111 | DROP INDEX IF EXISTS idx_refresh_token; 112 | CREATE INDEX idx_refresh_token ON users_token(refresh_token); -------------------------------------------------------------------------------- /database/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | export DATABASE_SCHEMA_DIR=./schema 5 | 6 | # we need to specify the database configuration for pop by sending additional parameter 7 | # example: ./setup.sh create database.yml 8 | create() { 9 | if [ -z $1 ]; then 10 | echo "database configuration cannot be empty" 11 | exit 1; 12 | fi 13 | 14 | soda create -a --config $1 15 | if [ $? -ne 0 ]; then 16 | echo "failed to create database with config $1"; 17 | exit 1; 18 | fi 19 | } 20 | 21 | # example: ./setup.sh drop database.yml 22 | drop() { 23 | if [ -z $1 ]; then 24 | echo "database configuration cannot be empty" 25 | exit 1; 26 | fi 27 | 28 | soda drop -a --config $1 29 | if [ $? -ne 0 ]; then 30 | echo "failed to drop database with config $1"; 31 | fi 32 | } 33 | 34 | # example: ./setup.sh generate user users 35 | generate() { 36 | if [ -z $1 ]; then 37 | echo "database configuration cannot be empty" 38 | exit 1; 39 | fi 40 | 41 | if [ -z $2 ]; then 42 | echo "database name cannot be empty" 43 | exit 1; 44 | fi 45 | 46 | if [ -z $3 ]; then 47 | echo "migration name cannot be empty" 48 | exit 1; 49 | fi 50 | 51 | cd ${SCRIPT_DIR} 52 | soda generate sql -c $1 -e $2 -p ${DATABASE_SCHEMA_DIR}/$2 $3 53 | if [ $? -ne 0 ]; then 54 | echo "failed to generate new sql migration schema" 55 | cd - 56 | exit 1; 57 | fi 58 | cd - 59 | } 60 | 61 | migrate() { 62 | if [ -z $1 ]; then 63 | echo "database configuration cannot be empty" 64 | exit 1; 65 | fi 66 | 67 | if [ -z $2 ]; then 68 | echo "database name cannot be empty" 69 | exit 1; 70 | fi 71 | 72 | if [ -z $3 ]; then 73 | echo "migration type cannot be empty. must be up/down" 74 | exit 1; 75 | fi 76 | 77 | cd ${SCRIPT_DIR} 78 | soda migrate $3 -d -c $1 -e $2 -p ${DATABASE_SCHEMA_DIR} 79 | if [ $? -ne 0 ]; then 80 | echo "failed to generate new sql migration schema" 81 | cd - 82 | exit 1; 83 | fi 84 | cd - 85 | } 86 | 87 | # command for database setup 88 | case $1 in 89 | create) 90 | create $2 91 | ;; 92 | drop) 93 | drop $2 94 | ;; 95 | generate) 96 | generate $2 $3 $4 97 | ;; 98 | migrate) 99 | migrate $2 $3 $4 100 | ;; 101 | esac -------------------------------------------------------------------------------- /debug/bucket/private-image/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/debug/bucket/private-image/.keep -------------------------------------------------------------------------------- /debug/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "context" 4 | 5 | // DebugUsecase for user 6 | type DebugUsecase struct { 7 | } 8 | 9 | // New user debug usecase 10 | func New() *DebugUsecase { 11 | du := DebugUsecase{} 12 | return &du 13 | } 14 | 15 | // BypassLogin for bypassing log in to the project 16 | func (du *DebugUsecase) BypassLogin(ctx context.Context) error { 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: library/postgres:12.1-alpine 5 | restart: unless-stopped 6 | ports: 7 | - "5444:5432" 8 | environment: 9 | LC_ALL: C.UTF-8 10 | POSTGRES_USER: projectdb 11 | POSTGRES_PASSWORD: projectdb 12 | 13 | redis: 14 | image: library/redis:5.0.6-alpine 15 | restart: unless-stopped 16 | ports: 17 | - "6379:6379" -------------------------------------------------------------------------------- /docs/images/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/design.png -------------------------------------------------------------------------------- /docs/images/import_cyclic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/import_cyclic.png -------------------------------------------------------------------------------- /docs/images/microservice_cylic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/microservice_cylic.png -------------------------------------------------------------------------------- /docs/images/microservice_interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/microservice_interaction.png -------------------------------------------------------------------------------- /docs/images/microservice_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/microservice_user.png -------------------------------------------------------------------------------- /docs/images/microservice_user_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/microservice_user_order.png -------------------------------------------------------------------------------- /docs/images/nsq_throttle_design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/nsq_throttle_design.png -------------------------------------------------------------------------------- /docs/images/service_boundaries1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/service_boundaries1.png -------------------------------------------------------------------------------- /docs/images/service_package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/service_package.png -------------------------------------------------------------------------------- /docs/images/service_package_cyclic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/service_package_cyclic.png -------------------------------------------------------------------------------- /docs/images/service_package_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/docs/images/service_package_dependencies.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/albertwidi/go-project-example 2 | 3 | go 1.13 4 | 5 | require ( 6 | firebase.google.com/go v3.9.0+incompatible 7 | github.com/BurntSushi/toml v0.3.1 8 | github.com/aws/aws-sdk-go v1.25.21 9 | github.com/coreos/bbolt v1.3.3 // indirect 10 | github.com/coreos/etcd v3.3.18+incompatible // indirect 11 | github.com/coreos/go-semver v0.3.0 // indirect 12 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect 13 | github.com/cucumber/godog v0.9.0 14 | github.com/go-sql-driver/mysql v1.4.1 15 | github.com/golang/mock v1.3.1 16 | github.com/gomodule/redigo v2.0.0+incompatible 17 | github.com/google/go-cmp v0.3.0 18 | github.com/google/uuid v1.1.1 19 | github.com/gorilla/mux v1.7.3 20 | github.com/gorilla/websocket v1.4.1 // indirect 21 | github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 // indirect 22 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 23 | github.com/jmoiron/sqlx v1.2.0 24 | github.com/jonboulle/clockwork v0.1.0 // indirect 25 | github.com/lib/pq v1.1.1 26 | github.com/nsqio/go-nsq v1.0.8 27 | github.com/oklog/ulid v1.3.1 28 | github.com/prometheus/client_golang v1.0.0 29 | github.com/prometheus/common v0.7.0 // indirect 30 | github.com/rs/zerolog v1.16.0 31 | github.com/sirupsen/logrus v1.4.2 32 | github.com/soheilhy/cmux v0.1.4 // indirect 33 | github.com/stretchr/testify v1.5.1 34 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect 35 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 36 | go.etcd.io/bbolt v1.3.3 // indirect 37 | go.etcd.io/etcd v3.3.18+incompatible 38 | go.opencensus.io v0.22.1 39 | go.uber.org/multierr v1.2.0 // indirect 40 | go.uber.org/zap v1.11.0 41 | gocloud.dev v0.17.0 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 43 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d 44 | golang.org/x/tools/gopls v0.2.2 // indirect 45 | google.golang.org/api v0.13.0 46 | google.golang.org/grpc v1.24.0 // indirect 47 | gopkg.in/yaml.v2 v2.2.8 48 | sigs.k8s.io/yaml v1.1.0 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path/filepath" 7 | 8 | "github.com/BurntSushi/toml" 9 | "github.com/albertwidi/go-project-example/internal/kothak" 10 | "github.com/albertwidi/go-project-example/internal/pkg/envfile" 11 | "github.com/albertwidi/go-project-example/internal/pkg/tempe" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // DefaultConfig for the project 16 | type DefaultConfig struct { 17 | Servers DefaultServers `json:"servers" yaml:"servers" toml:"servers"` 18 | Log DefaultLog `json:"log" yaml:"log" toml:"log"` 19 | Resources kothak.Config `json:"resources" yaml:"resources" toml:"resources"` 20 | } 21 | 22 | // DefaultLog config for the project 23 | type DefaultLog struct { 24 | Level string `json:"level" yaml:"level" toml:"level"` 25 | File string `json:"file" yaml:"file" toml:"file"` 26 | Color bool `json:"use_color" yaml:"use_color" toml:"use_color"` 27 | } 28 | 29 | // DefaultServers struct 30 | type DefaultServers struct { 31 | Main ServerConfig `json:"main" yaml:"main" toml:"main"` 32 | Debug ServerConfig `json:"debug" yaml:"debug" toml:"debug"` 33 | Admin ServerConfig `json:"admin" yaml:"admin" toml:"admin"` 34 | } 35 | 36 | // ServerConfig struct 37 | type ServerConfig struct { 38 | Address string `yaml:"address" toml:"address"` 39 | } 40 | 41 | // ParseFile for parsing config file and return DefaultConfig struct 42 | func ParseFile(configFile string, dest interface{}, envFiles ...string) error { 43 | // prepare to replace ${ENV_VAR_NAME} with environment variable 44 | t, err := tempe.New(tempe.EnvVarPattern, tempe.EnvVarReplacerFunc) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | if err := envfile.Load(envFiles...); err != nil { 50 | return err 51 | } 52 | 53 | out, err := ioutil.ReadFile(configFile) 54 | if err != nil { 55 | return err 56 | } 57 | // replacing with environment variables 58 | out, err = t.ReplaceBytes(out) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | ext := filepath.Ext(configFile) 64 | switch ext { 65 | case ".toml": 66 | err = toml.Unmarshal(out, dest) 67 | case ".yaml": 68 | err = yaml.Unmarshal(out, dest) 69 | default: 70 | err = fmt.Errorf("config: file ext is not valid. got %s", ext) 71 | } 72 | 73 | if err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | // Print configuration in json schema 80 | // all config with tag=protected:1 will be hidden from the print 81 | func Print(v interface{}) { 82 | 83 | } 84 | -------------------------------------------------------------------------------- /internal/config/config.test.toml: -------------------------------------------------------------------------------- 1 | # project configuration for unit test 2 | 3 | [servers] 4 | # only open main port to public 5 | [servers.main] 6 | address = ":8000" 7 | [servers.debug] 8 | address = ":9000" 9 | [servers.admin] 10 | address = ":5276" 11 | 12 | [log] 13 | level = "info" 14 | file = "test" 15 | use_color = true 16 | 17 | [resources] 18 | # object storage 19 | [[resources.object_storage]] 20 | name = "image-private" 21 | bucket = "${STORAGE_IMAGE_PRIVATE_BUCKET}" 22 | provider = "${STORAGE_IMAGE_PRIVATE_PROVIDER}" 23 | region = "${STORAGE_IMAGE_PRIVATE_REGION}" 24 | endpoint = "${STORAGE_IMAGE_PRIVATE_ENDPOINT}" 25 | [resources.object_storage.s3] 26 | client_id = "${STORAGE_IMAGE_CLIENT_ID}" 27 | client_secret = "${STORAGE_IMAGE_CLIENT_SECRET}" 28 | disable_ssl = ${STORAGE_IMAGE_DISABLE_SSL} 29 | force_path_style = ${STORAGE_IMAGE_FOCE_PATH_STYLE} 30 | 31 | # database 32 | [resources.database] 33 | # default options 34 | max_open_conns = 20 35 | max_retry = 5 36 | [[resources.database.connect]] 37 | name = "users" 38 | driver = "postgres" 39 | [resources.database.connect.leader] 40 | dsn = "${DB_USER_LEADER_DSN}" 41 | [resources.database.connect.replica] 42 | dsn = "${DB_USER_REPLICA_DSN}" 43 | 44 | [[resources.database.connect]] 45 | name = "notifications" 46 | driver = "postgres" 47 | [resources.database.connect.leader] 48 | dsn = "${DB_NOTIFICATION_LEADER_DSN}" 49 | [resources.database.connect.replica] 50 | dsn = "${DB_NOTIFICATION_REPLICA_DSN}" 51 | 52 | # redis 53 | [resources.redis] 54 | max_active_conn = 100 55 | [[resources.redis.connect]] 56 | name = "session" 57 | address = "${REDIS_SESSION_ADDRESS}" 58 | [[resources.redis.connect]] 59 | name = "image" 60 | address = "${REDIS_IMAGE_ADDRESS}" -------------------------------------------------------------------------------- /internal/config/test_generator/main.go: -------------------------------------------------------------------------------- 1 | // expect this program to run from project root 2 | // go run internal/config/test_generator/main.go 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "log" 9 | "os" 10 | 11 | "github.com/albertwidi/go-project-example/internal/config" 12 | ) 13 | 14 | func main() { 15 | // the path is not relative, run from project root 16 | configFile := "project.config.toml" 17 | envFile := "project.env.toml" 18 | 19 | defaultConfig := config.DefaultConfig{} 20 | if err := config.ParseFile(configFile, &defaultConfig, envFile); err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | // json 25 | out, err := json.MarshalIndent(defaultConfig, "", " ") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | f, err := os.Create("config.test.json") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | if _, err := f.Write(out); err != nil { 36 | log.Fatal(err) 37 | } 38 | // yaml 39 | // toml 40 | } 41 | -------------------------------------------------------------------------------- /internal/entity/amenities/amenities.go: -------------------------------------------------------------------------------- 1 | package amenities 2 | 3 | import "time" 4 | 5 | // Type of amenities 6 | type Type int 7 | 8 | // Amenities struct 9 | type Amenities struct { 10 | ID string 11 | Name string 12 | Type Type 13 | ImagePath string 14 | CreatedAt time.Time 15 | UpdatedAt time.Time 16 | UpdatedBy int64 17 | IsTest bool 18 | IsDeleted bool 19 | } 20 | -------------------------------------------------------------------------------- /internal/entity/amenities/const.go: -------------------------------------------------------------------------------- 1 | package amenities 2 | 3 | // list of amenities type 4 | const ( 5 | TypeGeneral Type = 1 6 | TypeKos Type = 2 7 | TypeHotel Type = 3 8 | TypeRoom Type = 4 9 | TypeHotelRoom Type = 5 10 | TypeKosRoom Type = 6 11 | ) 12 | -------------------------------------------------------------------------------- /internal/entity/authentication/authentication.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | // Action of authentication 4 | type Action int 5 | 6 | // Provider of authentication 7 | type Provider string 8 | 9 | // Authentication entity 10 | type Authentication struct { 11 | Action Action 12 | Provider Provider 13 | Username string 14 | } 15 | -------------------------------------------------------------------------------- /internal/entity/authentication/const.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | // list of authentication action 4 | const ( 5 | ActionRegister string = "register" 6 | ActionLogin string = "login" 7 | ActionVerifyPayment string = "verify-payment" 8 | ActionVerifyChangePin string = "verify-changepin" 9 | ) 10 | 11 | // list of authentication provider 12 | const ( 13 | ProviderOTP Provider = "otp" 14 | ProviderPassword Provider = "password" 15 | ProviderPin Provider = "pin" 16 | ) 17 | -------------------------------------------------------------------------------- /internal/entity/booking/booking.go: -------------------------------------------------------------------------------- 1 | package booking 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Type of booking 8 | type Type int 9 | 10 | // Validate booking type 11 | func (t Type) Validate() error { 12 | switch t { 13 | case TypeMonthly: 14 | case TypeDaily: 15 | default: 16 | return ErrInvalidType 17 | } 18 | 19 | return nil 20 | } 21 | 22 | // Status of booking 23 | type Status int 24 | 25 | // Booking record attempt of booking 26 | // data in attempt is dirty 27 | // because its mixed from attempt to booking 28 | type Booking struct { 29 | ID string 30 | PropertyID string 31 | BookingType Type 32 | Price int64 33 | Deposit int64 34 | Status Status 35 | // mark that booking really happens 36 | IsPaid bool 37 | // a hack, so owner can see booking attempt 38 | IsCreatedByOwner bool 39 | CreatedAt time.Time 40 | UpdatedAt time.Time 41 | CreatedBy int64 42 | IsTest bool 43 | } 44 | 45 | // Detail of booking 46 | type Detail struct { 47 | } 48 | 49 | // PaidBooking data 50 | // booking is different attempt 51 | // this is where data after booking confirmation stored 52 | type PaidBooking struct { 53 | // is an id from attempt 54 | ID string 55 | ItemID string 56 | Price int32 57 | Status int 58 | CreatedAt time.Time 59 | UpdatedAt time.Time 60 | UpdatedBy int64 61 | IsTest bool 62 | } 63 | -------------------------------------------------------------------------------- /internal/entity/booking/const.go: -------------------------------------------------------------------------------- 1 | package booking 2 | 3 | // booking status list 4 | const ( 5 | StatusCreated = 10 6 | StatusWaitingForPayment = 100 7 | StatusWaitingForPaymentConfirmation = 105 8 | StatusWaitingForOwnerConfirmation = 110 9 | StatusConfirmed = 120 10 | StatusActive = 150 11 | StatusFinished = 200 12 | StatusCancelled = 500 13 | StatusCancelledBySystem = 505 14 | StatusRejected = 510 15 | ) 16 | 17 | // booking type list 18 | const ( 19 | TypeDaily Type = 1 20 | TypeMonthly Type = 2 21 | ) 22 | -------------------------------------------------------------------------------- /internal/entity/booking/error.go: -------------------------------------------------------------------------------- 1 | package booking 2 | 3 | import "errors" 4 | 5 | // list of booking errors 6 | var ( 7 | ErrInvalidType = errors.New("booking: type is not valid") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/entity/device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | // Type of device 4 | type Type int 5 | 6 | // type of device 7 | const ( 8 | TypeAndroid Type = 1 9 | TypeIOS Type = 2 10 | 11 | TypeAndroidString = "android" 12 | TypeIOSString = "ios" 13 | ) 14 | 15 | // Device struct 16 | type Device struct { 17 | // FCMToken for firebase cloud messaging 18 | // changed when applicaion uninstalled 19 | FCMToken string 20 | // ACMToken for apple cloud messaging 21 | // changed when application uninstalled 22 | ACMTOken string 23 | // ID of device 24 | // only get changed when phone got formatted 25 | DeviceID string 26 | } 27 | -------------------------------------------------------------------------------- /internal/entity/image/const.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | // list of image mode 4 | const ( 5 | ModePublic Mode = "public" 6 | ModePrivate Mode = "private" 7 | ModeSigned Mode = "signed" 8 | ) 9 | 10 | // list of group of image 11 | const ( 12 | GroupEmpty = "" 13 | GroupMixed = "mixed" 14 | GroupPropertyKos = "property/kos" 15 | GroupPropertyHotel = "property/hotel" 16 | GroupPropertyHostel = "property/hostel" 17 | GroupPropertyHouse = "property/house" 18 | GroupPropertyRoom = "property/room" 19 | GroupAmenities = "amenities" 20 | GroupUserKTP = "user/ktp" 21 | GroupUserAvatar = "user/avatar" 22 | GroupPaymentProof = "payment/proof" 23 | ) 24 | -------------------------------------------------------------------------------- /internal/entity/image/error.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import "errors" 4 | 5 | // list of errors 6 | var ( 7 | ErrTooManyTags = errors.New("image: cannot have more than 5 tags") 8 | ErrTempPathNotFound = errors.New("image: temporary path not found") 9 | ErrInvalidAccessAttribute = errors.New("image: invalid access attribute") 10 | ) 11 | -------------------------------------------------------------------------------- /internal/entity/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "fmt" 5 | "net/textproto" 6 | "strings" 7 | 8 | userentity "github.com/albertwidi/go-project-example/internal/entity/user" 9 | ) 10 | 11 | // Mode of image 12 | type Mode string 13 | 14 | // Validate mode 15 | func (m Mode) Validate() error { 16 | switch m { 17 | case ModePrivate: 18 | break 19 | case ModePublic: 20 | break 21 | case ModeSigned: 22 | break 23 | default: 24 | return fmt.Errorf("image: invalid mode, got %s", m) 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // Group of image 31 | type Group string 32 | 33 | // Validate group 34 | func (g Group) Validate() error { 35 | switch g { 36 | case GroupAmenities, 37 | GroupPropertyKos, 38 | GroupPropertyRoom, 39 | GroupPropertyHotel, 40 | GroupPropertyHostel, 41 | GroupPropertyHouse, 42 | GroupPaymentProof, 43 | GroupUserKTP, 44 | GroupUserAvatar: 45 | default: 46 | return fmt.Errorf("image: invalid group, got %s", g) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // FileInfo struct 53 | type FileInfo struct { 54 | FileName string 55 | Size int64 56 | Header textproto.MIMEHeader 57 | UserHash userentity.Hash 58 | Mode Mode 59 | Group Group 60 | Tags string 61 | } 62 | 63 | // Options struct 64 | type Options struct { 65 | Manipulation *Manipulation 66 | } 67 | 68 | // Manipulation struct 69 | type Manipulation struct { 70 | Resize Resize 71 | } 72 | 73 | // Resize struct 74 | type Resize struct { 75 | Height int 76 | Width int 77 | } 78 | 79 | // Access of image 80 | type Access string 81 | 82 | // CreateAccess return the access of image 83 | // format is: allowed:[]users;priviledge:[]Permission; 84 | func CreateAccess(allowed []string, permission []string) Access { 85 | a := strings.Join(allowed, ",") 86 | p := strings.Join(permission, ",") 87 | 88 | acc := fmt.Sprintf("allowed:%s;priviledge:%s", a, p) 89 | return Access(acc) 90 | } 91 | -------------------------------------------------------------------------------- /internal/entity/image/image_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/albertwidi/go-project-example/internal/entity/image" 7 | ) 8 | 9 | func TestValidateMode(t *testing.T) { 10 | cases := []struct { 11 | mode image.Mode 12 | expectError bool 13 | }{ 14 | { 15 | mode: image.Mode("invalid"), 16 | expectError: true, 17 | }, 18 | { 19 | mode: image.ModePrivate, 20 | expectError: false, 21 | }, 22 | } 23 | 24 | for _, c := range cases { 25 | err := c.mode.Validate() 26 | if err != nil && !c.expectError { 27 | t.Errorf("not expecting error but got %v", err) 28 | return 29 | } 30 | 31 | if err == nil && c.expectError { 32 | t.Error("expecting error but got nil") 33 | return 34 | } 35 | } 36 | } 37 | 38 | func TestValidateGroup(t *testing.T) { 39 | cases := []struct { 40 | group image.Group 41 | expectError bool 42 | }{ 43 | { 44 | group: image.Group("invalid"), 45 | expectError: true, 46 | }, 47 | { 48 | group: image.GroupAmenities, 49 | expectError: false, 50 | }, 51 | } 52 | 53 | for _, c := range cases { 54 | err := c.group.Validate() 55 | if err != nil && !c.expectError { 56 | t.Errorf("not expecting error but got %v", err) 57 | return 58 | } 59 | 60 | if err == nil && c.expectError { 61 | t.Error("expecting error but got nil") 62 | return 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/entity/invoice/const.go: -------------------------------------------------------------------------------- 1 | package invoice 2 | 3 | // list of invoice status 4 | const ( 5 | StatusCreated Status = 10 6 | StatusWaitingForPayment Status = 100 7 | StatusWaitingForPaymentConfirmation Status = 105 8 | StatusPaid Status = 200 9 | StatusCancelled Status = 500 10 | StatusCancelledBySystem Status = 505 11 | ) 12 | -------------------------------------------------------------------------------- /internal/entity/invoice/invoice.go: -------------------------------------------------------------------------------- 1 | package invoice 2 | 3 | import "time" 4 | 5 | // Status of invoice 6 | type Status int 7 | 8 | // Invoice struct 9 | type Invoice struct { 10 | ID string 11 | Number string 12 | OrderID string 13 | InvoiceFrom string 14 | InvoiceTo string 15 | Type int 16 | Total int64 17 | DiscountTotal int64 18 | GrandTotal int64 19 | Details []Detail 20 | Status Status 21 | Description string 22 | CreatedBy int64 23 | DueDate time.Time 24 | PaidAt time.Time 25 | CreatedAt time.Time 26 | UpdatedAt time.Time 27 | IsTest bool 28 | IsDeleted bool 29 | } 30 | 31 | // Detail for invoice detail 32 | type Detail struct { 33 | ID string 34 | InvoiceID string 35 | Amount int64 36 | Discount int64 37 | ItemName string 38 | ItemQuantity int64 39 | Description string 40 | CreatedAt time.Time 41 | CreatedBy string 42 | UpdatedAt time.Time 43 | IsTest bool 44 | IsDeleted bool 45 | } 46 | 47 | // PaidInvoice struct 48 | // store all data of paid invoice 49 | type PaidInvoice struct { 50 | InvoiceID string 51 | InvoiceNumber string 52 | PaymentID string 53 | OrderID string 54 | InvoiceFrom int64 55 | InvoiceTo int64 56 | CreatedAt time.Time 57 | CreatedBy int64 58 | PaidAt time.Time 59 | IsTest bool 60 | IsDeleted bool 61 | } 62 | -------------------------------------------------------------------------------- /internal/entity/notification/const.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | // type of notification 4 | var ( 5 | TypeSMS = 1 6 | TypeEmail = 2 7 | TypePushMessage = 3 8 | 9 | TemplateTypePushMessage = "pushmessage" 10 | TemplateTypeSMS = "sms" 11 | TemplateTypeEmail = "email" 12 | ) 13 | 14 | // notification providers 15 | var ( 16 | ProviderNexmo = 1 17 | ) 18 | 19 | // Purpose of notification 20 | type Purpose int 21 | 22 | // purpose of notification 23 | var ( 24 | PurposeEmpty Purpose = 0 25 | PurposeSystemUpdate Purpose = 1 26 | PurposeAuthenticationOTP Purpose = 2 27 | PurposeAuthenticationPayment Purpose = 3 28 | PurposeAuthenticationWithdraw Purpose = 4 29 | PurposePromotion Purpose = 5 30 | PurposeReminder Purpose = 6 31 | ) 32 | -------------------------------------------------------------------------------- /internal/entity/notification/notification.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/albertwidi/go-project-example/internal/entity/device" 7 | ) 8 | 9 | // Client interface 10 | type Client interface { 11 | Send(templateName string) error 12 | } 13 | 14 | // SendOptions struct 15 | type SendOptions struct { 16 | DryRun bool 17 | } 18 | 19 | // UserNotification struct 20 | type UserNotification struct { 21 | ID string 22 | UserID int64 23 | ProviderType int 24 | ProviderID int 25 | ProviderSendID string 26 | Purpose int 27 | IsWebpage bool 28 | Status int 29 | Title string 30 | Message string 31 | Show bool 32 | HasDetail bool 33 | Read bool 34 | CreatedAt time.Time 35 | UpdatedAt time.Time 36 | IsDeleted bool 37 | IsTest bool 38 | Detail UserNotificationDetail 39 | } 40 | 41 | // UserNotificationDetail struct 42 | type UserNotificationDetail struct { 43 | NotificationID string 44 | Body string 45 | WebLink string 46 | CreatedAt time.Time 47 | IsDeleted bool 48 | IsTest bool 49 | } 50 | 51 | // Notification struct 52 | type Notification struct { 53 | UserID int64 54 | Title string 55 | Message string 56 | DetailBody string 57 | Image string 58 | WebLink string 59 | Purpose int 60 | DeviceID device.Device 61 | // either push, sms or email 62 | NotifData interface{} 63 | } 64 | 65 | // Validate notification param 66 | func (n Notification) Validate() error { 67 | return nil 68 | } 69 | 70 | // Options of notification 71 | type Options struct { 72 | InboxSave bool 73 | Fake bool 74 | } 75 | 76 | // PushNotification struct 77 | type PushNotification struct { 78 | // if device token is present, then the device token is used rather than seeking in session 79 | // this is useful in user register flow 80 | DeviceToken string 81 | } 82 | 83 | // SMSNotification struct 84 | type SMSNotification struct { 85 | From string 86 | ToMSISDN string 87 | } 88 | 89 | // EmailNotification struct 90 | type EmailNotification struct { 91 | To string 92 | Cc string 93 | } 94 | -------------------------------------------------------------------------------- /internal/entity/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | // Scope type for oauth2 4 | type Scope int 5 | 6 | // list of scope 7 | const ( 8 | ScopeRead Scope = iota + 1 9 | ScopeWrite 10 | ) 11 | -------------------------------------------------------------------------------- /internal/entity/order/const.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | // status list of order 4 | const ( 5 | // order is updateable 6 | StatusCreated Status = 10 7 | StatusWaitingForPayment Status = 100 8 | StatusWaitingForPaymentConfirmation Status = 105 9 | StatusPaid Status = 150 10 | StatusWaitingForThirdParty Status = 180 11 | StatusComplete Status = 200 12 | StatusRefundInProcess Status = 400 13 | // order is not updateable 14 | StatusOrderRefunded Status = 450 15 | StatusCancelled Status = 500 16 | StatusCancelledBySystem Status = 505 17 | ) 18 | 19 | // type list of order 20 | const ( 21 | TypeBooking Type = 1 22 | TypeDigitalGoods Type = 2 23 | ) 24 | 25 | // refund status list 26 | const ( 27 | RefundStatusCreated RefundStatus = 100 28 | RefundStatusComplete RefundStatus = 200 29 | ) 30 | -------------------------------------------------------------------------------- /internal/entity/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "time" 4 | 5 | // Status of order 6 | type Status int 7 | 8 | // Type of order 9 | type Type int 10 | 11 | // Order struct 12 | type Order struct { 13 | ID string 14 | UserID int64 15 | OrderType int32 16 | Total int64 17 | DiscountTotal int64 18 | RefundTotal int64 19 | CashbackTotal int64 20 | // grand total = total - discount total - refund total 21 | GrandTotal int64 22 | Status Status 23 | Details []Detail 24 | IsRefundable bool 25 | RefundableBefore time.Time 26 | CreatedAt time.Time 27 | UpdatedAt time.Time 28 | UpdatedBy int64 29 | IsTest bool 30 | IsDeleted bool 31 | } 32 | 33 | // Detail of order 34 | type Detail struct { 35 | ID string 36 | OrderID string 37 | ItemID string 38 | ItemName string 39 | ItemPrice int64 40 | ItemQuantity int64 41 | ItemPriceTotal int64 42 | DiscountAmount int64 43 | RefundAmount int64 44 | CashbackAmount int64 45 | // total = item price total - discount amount - refund amount 46 | Total int64 47 | CreatedAt time.Time 48 | UpdatedAt time.Time 49 | UpdatedBy int64 50 | IsTest bool 51 | IsDeleted bool 52 | } 53 | 54 | // Coupons of order, to track the usage of coupons 55 | type Coupons struct { 56 | OrderID string 57 | UserID int64 58 | CouponCode string 59 | DiscountAmount int64 60 | // to track whether the coupon is really applied or not 61 | IsApplied bool 62 | CreatedAt time.Time 63 | UpdatedAt time.Time 64 | UpdatedBy int64 65 | IsTest bool 66 | IsDeleted bool 67 | } 68 | 69 | // RefundStatus of order 70 | type RefundStatus int 71 | 72 | // Refund of order 73 | type Refund struct { 74 | ID string 75 | OrderID string 76 | Status RefundStatus 77 | TotalAmount int64 78 | Details []RefundDetail 79 | Description string 80 | CreatedAt time.Time 81 | UpdatedAt time.Time 82 | UpdatedBy int64 83 | IsTest bool 84 | IsDeleted bool 85 | } 86 | 87 | // RefundDetail struct 88 | type RefundDetail struct { 89 | RefundID string 90 | OrderDetailID string 91 | Amount int64 92 | CreatedAt time.Time 93 | IsTest bool 94 | IsDeleted bool 95 | } 96 | -------------------------------------------------------------------------------- /internal/entity/otp/const.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import "time" 4 | 5 | // CodeLength type 6 | type CodeLength int 7 | 8 | // Validate code length 9 | func (clength CodeLength) Validate() error { 10 | if clength < CodeLength4 || clength > CodeLength6 { 11 | return ErrCodeLengthInvalid 12 | } 13 | return nil 14 | } 15 | 16 | // legnth of code for otp 17 | const ( 18 | CodeLength4 = 4 19 | CodeLength6 = 6 20 | ) 21 | 22 | // list of otp threshold 23 | const ( 24 | ThresholdOTPResend int = 3 25 | ThresholdOTPValidate int = 10 26 | ) 27 | 28 | // list of expiry time 29 | const ( 30 | OTPKeyExpiry = time.Hour * 24 31 | 32 | // expire time for otp 33 | ExpiryTimeDefault = time.Minute * 4 34 | 35 | // the default time for expiring otp is 90 seconds 36 | ResendTimeDefault = time.Second * 90 37 | ResendTimeAfterThreshold = time.Minute * 30 38 | // the maximum time for otp exiry is 1 day 39 | ResendTimeMax = time.Hour * 24 40 | ) 41 | -------------------------------------------------------------------------------- /internal/entity/otp/errors.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // list of otp errors 8 | var ( 9 | ErrCodeLengthInvalid = errors.New("otp: code length is invalid") 10 | ErrOTPInvalid = errors.New("otp: password invalid") 11 | ErrOTPExpired = errors.New("otp: otp already expired") 12 | ErrOTPReachResendMaxAttempt = errors.New("otp: reach maximum resend attempt") 13 | ErrOTPReachValidateMaxAttempt = errors.New("otp: reach maximum validate attempt, too many wrong password") 14 | ErrOTPNotResendable = errors.New("otp: not resendable") 15 | ) 16 | -------------------------------------------------------------------------------- /internal/entity/otp/otp.go: -------------------------------------------------------------------------------- 1 | package otp 2 | 3 | import ( 4 | "time" 5 | 6 | authentity "github.com/albertwidi/go-project-example/internal/entity/authentication" 7 | ) 8 | 9 | // OTP struct 10 | type OTP struct { 11 | UniqueID string `json:"unique_id"` 12 | Action authentity.Action `json:"action"` 13 | Code string `json:"code"` 14 | CreatedAt time.Time `json:"created_at"` 15 | ExpiryTime time.Duration `json:"expiry_time"` 16 | ExpiredAt time.Time `json:"expired_at"` 17 | ResendTime time.Duration `json:"resend_time"` 18 | ResendableAt time.Time `json:"resendable_at"` 19 | } 20 | 21 | // IsResendable return whether otp is resendable or not 22 | func (otp OTP) IsResendable() (bool, error) { 23 | if otp.Code == "" { 24 | return true, nil 25 | } 26 | 27 | now := time.Now() 28 | if now.Before(otp.ResendableAt) { 29 | return false, nil 30 | } 31 | 32 | return true, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/entity/property/const.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | // staus of property 4 | const ( 5 | StatusCreated = 10 6 | StatusInactive = 50 7 | StatusPermanentlyClosed = 100 8 | StatusClosed = 150 9 | StatusActive = 200 10 | ) 11 | 12 | // type list of property 13 | const ( 14 | TypeKos Type = 1 15 | TypeHostel Type = 2 16 | TypeHotel Type = 3 17 | TypeHouse Type = 4 18 | TypeApartment Type = 5 19 | TypeFlat Type = 6 20 | TypePrivateRoom Type = 7 21 | ) 22 | 23 | // segment list of property 24 | const ( 25 | SegmentStay Segment = 1 26 | ) 27 | -------------------------------------------------------------------------------- /internal/entity/property/property.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import "time" 4 | 5 | // Status of property 6 | type Status int 7 | 8 | // Type of property 9 | type Type int 10 | 11 | // Segment of the property 12 | type Segment int 13 | 14 | // Property data 15 | type Property struct { 16 | ID string 17 | Owner int64 18 | Name string 19 | Type Type 20 | Segment Segment 21 | // it means we can book the room instead of property 22 | HasRooms bool 23 | Status Status 24 | Detail Detail 25 | CreatedAt time.Time 26 | UpdatedAt time.Time 27 | IsTest bool 28 | IsDeleted bool 29 | } 30 | 31 | // Detail of property data 32 | type Detail struct { 33 | PropertyID string 34 | Address string 35 | Amenities []string 36 | CreatedAt time.Time 37 | UpdatedAt time.Time 38 | IsTest bool 39 | IsDeleted bool 40 | } 41 | 42 | // Address of property 43 | type Address struct { 44 | Address string 45 | City string 46 | State string 47 | CreatedAt time.Time 48 | UpdatedAt time.Time 49 | IsTest bool 50 | IsDeleted bool 51 | } 52 | 53 | // AddressMap of the property 54 | type AddressMap struct { 55 | PropertyID string 56 | Radius int64 57 | Lat float64 58 | Long float64 59 | CreatedAt time.Time 60 | IsTest bool 61 | IsDeleted bool 62 | } 63 | 64 | // Pricing of property 65 | type Pricing struct { 66 | PropertyID string 67 | CreatedAt time.Time 68 | IsTest bool 69 | IsDeleted bool 70 | } 71 | 72 | // Amenities of property 73 | type Amenities struct { 74 | PropertyID string 75 | AmenitiesID string 76 | CreatedAt time.Time 77 | UpdatedAt time.Time 78 | IsTest bool 79 | IsDeleted bool 80 | } 81 | -------------------------------------------------------------------------------- /internal/entity/pushmessage/pushmessage.go: -------------------------------------------------------------------------------- 1 | package pushmessage 2 | 3 | // Message for push message 4 | type Message struct { 5 | Token string 6 | Title string 7 | Body string 8 | Data map[string]string 9 | Android *AndroidConfig 10 | IOS *IOSConfig 11 | } 12 | 13 | // AndroidConfig push message specific 14 | // TODO: add more things that is available in the SDK 15 | type AndroidConfig struct { 16 | Icon string 17 | // in hex format 18 | Color string 19 | Sound string 20 | Tag string 21 | ClickAction string 22 | BodyLockKey string 23 | BodyLockArgs string 24 | TitleLockKey string 25 | TItleLockArgs string 26 | } 27 | 28 | // IOSConfig push message specific 29 | // TODO: add more things that is available in the SDK 30 | type IOSConfig struct { 31 | MutableContent bool 32 | ContentAvailable bool 33 | Category string 34 | ThreadID string 35 | Badge *int 36 | CustomData map[string]interface{} 37 | } 38 | -------------------------------------------------------------------------------- /internal/entity/room/const.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | // pricing type list of room 4 | const ( 5 | PricingTypeDaily = 1 6 | PricingTypeWeekly = 2 7 | PricingTypeMonthly = 3 8 | ) 9 | -------------------------------------------------------------------------------- /internal/entity/room/room.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | // Room struct 4 | type Room struct { 5 | ID string 6 | Tags []string 7 | Name string 8 | } 9 | 10 | // Detail of the room 11 | type Detail struct { 12 | RoomID string 13 | Amenities []string 14 | } 15 | 16 | // PricingType of room 17 | type PricingType int 18 | 19 | // Pricing of room 20 | type Pricing struct { 21 | Name string 22 | Amount string 23 | } 24 | 25 | // Amenities of room 26 | type Amenities struct { 27 | RoomID string 28 | AmenitiesID string 29 | } 30 | -------------------------------------------------------------------------------- /internal/entity/secret/const.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | // secret list 4 | const ( 5 | KeyPassword = "PASSWORD" 6 | KeyPIN = "PIN" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/entity/secret/secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Key of secret 8 | type Key string 9 | 10 | // Secret of user 11 | type Secret struct { 12 | ID string 13 | UserID string 14 | SecretKey Key 15 | SecretValue string 16 | CreatedAt time.Time 17 | CreatedBy int64 18 | UpdatedAt time.Time 19 | UpdatedBy int64 20 | IsTest bool 21 | } 22 | -------------------------------------------------------------------------------- /internal/entity/session/const.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "time" 4 | 5 | // expiry time list 6 | const ( 7 | DefaultSessionExpiryTime = time.Hour * 24 * 15 8 | ) 9 | -------------------------------------------------------------------------------- /internal/entity/session/context.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "context" 4 | 5 | type contextKey string 6 | 7 | const sessionContextKey contextKey = "session:context:key" 8 | 9 | // WithSession return a context with appended session data 10 | func WithSession(ctx context.Context, sess *Session) context.Context { 11 | return context.WithValue(ctx, sessionContextKey, sess) 12 | } 13 | 14 | // FromContext return a session instance from requestContext 15 | func FromContext(ctx context.Context) *Session { 16 | sess, ok := ctx.Value(sessionContextKey).(*Session) 17 | if !ok { 18 | // do something when session not found 19 | } 20 | return sess 21 | } 22 | -------------------------------------------------------------------------------- /internal/entity/session/error.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "errors" 4 | 5 | // list of error 6 | var ( 7 | ErrSessionNotFound error = errors.New("session: not found") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/entity/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "time" 5 | 6 | authentity "github.com/albertwidi/go-project-example/internal/entity/authentication" 7 | userentity "github.com/albertwidi/go-project-example/internal/entity/user" 8 | ) 9 | 10 | // Session struct 11 | type Session struct { 12 | ID string `json:"id"` 13 | HashID string `json:"hash_id"` 14 | AuthData AuthData `json:"auth_data"` 15 | Authenticated bool `json:"authenticated"` 16 | ExpiryTime time.Duration `json:"expiry_time"` 17 | ExpiredAt time.Time `json:"expired_at"` 18 | CreatedAt time.Time `json:"created_at"` 19 | } 20 | 21 | // UserData session is a user data cache that assosiated to session of user 22 | type UserData struct { 23 | User userentity.User `json:"user"` 24 | Bio userentity.Bio `json:"bio"` 25 | } 26 | 27 | // AuthData struct 28 | type AuthData struct { 29 | Provider authentity.Provider `json:"provider"` 30 | Action authentity.Action `json:"action"` 31 | } 32 | -------------------------------------------------------------------------------- /internal/entity/sms/sms.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "errors" 5 | 6 | notifentity "github.com/albertwidi/go-project-example/internal/entity/notification" 7 | ) 8 | 9 | // Payload struct 10 | type Payload struct { 11 | From string 12 | To string 13 | Message string 14 | Purpose notifentity.Purpose 15 | } 16 | 17 | // Validate sms payload 18 | func (p Payload) Validate() error { 19 | if p.Purpose == notifentity.PurposeEmpty { 20 | return errors.New("sms: purpose payload cannot be empty") 21 | } 22 | 23 | return nil 24 | } 25 | 26 | // NexmoCallback struct 27 | type NexmoCallback struct { 28 | } 29 | -------------------------------------------------------------------------------- /internal/entity/state/const.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import "time" 4 | 5 | // default state expiry time 6 | const ( 7 | DefaultStateExpiryTime = time.Minute * 5 8 | MinStateExpiryTime = time.Minute * 1 9 | MaxStateExpiryTime = time.Minute * 30 10 | ) 11 | -------------------------------------------------------------------------------- /internal/entity/state/errors.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import "errors" 4 | 5 | // error list of state 6 | var ( 7 | ErrStateNotFound = errors.New("state: not found") 8 | ErrCreatedByEmpty = errors.New("state: created by is empty") 9 | ErrExpiryTimeMoreThanMax = errors.New("state: expiry time is more than max expiry time allowed") 10 | ErrExpiryTimeLessThanMin = errors.New("state: expiry time is less than min expiry time allowed") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/entity/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "time" 5 | 6 | authentity "github.com/albertwidi/go-project-example/internal/entity/authentication" 7 | ) 8 | 9 | // New to create a new state 10 | func New() State { 11 | s := State{ 12 | MetaData: make(map[string]string), 13 | } 14 | 15 | return s 16 | } 17 | 18 | // State data 19 | type State struct { 20 | // CreatedBy define by whom the state is created 21 | CreatedBy string 22 | // CreatedByHashID is a hash of user id used externally 23 | CreatedByHashID string 24 | // Authentication data of state 25 | Authentication authentity.Authentication 26 | // metadata that want to be stored in the creation of state 27 | MetaData map[string]string 28 | ExpiryTime time.Duration 29 | ExpiredAt time.Time 30 | CreatedAt time.Time 31 | } 32 | 33 | // Validate state 34 | func (s State) Validate() error { 35 | if s.CreatedBy == "" { 36 | return ErrCreatedByEmpty 37 | } 38 | 39 | if s.ExpiryTime > MaxStateExpiryTime { 40 | return ErrExpiryTimeMoreThanMax 41 | } else if s.ExpiryTime < MinStateExpiryTime { 42 | return ErrExpiryTimeLessThanMin 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // IsExpired to check whether the state is expired or not 49 | func (s State) IsExpired() (bool, error) { 50 | now := time.Now() 51 | 52 | if now.After(s.ExpiredAt) { 53 | return true, nil 54 | } 55 | 56 | return false, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/entity/user/const.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | // list of user type 4 | const ( 5 | TypeOwner Type = 1 6 | TypeRenter Type = 2 7 | 8 | TypeOwnerString = "owner" 9 | TypeRenterString = "renter" 10 | ) 11 | 12 | // list of gender 13 | const ( 14 | // GenderInvalid Gender = 0 15 | // GenderMale Gender = 1 16 | // GenderFemale Gender = 2 17 | 18 | GenderInvalidString = "invalid" 19 | GenderMaleString = "male" 20 | GenderFemaleString = "female" 21 | ) 22 | 23 | // list of country in this project 24 | const ( 25 | CountryID Country = "ID" 26 | ) 27 | -------------------------------------------------------------------------------- /internal/entity/user/error.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "errors" 4 | 5 | // list of user errors 6 | var ( 7 | ErrUserNotFound = errors.New("user not found") 8 | ) 9 | -------------------------------------------------------------------------------- /internal/entity/user/register.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "time" 4 | 5 | // Registrations struct 6 | type Registrations struct { 7 | ID string 8 | UserID string 9 | UserType int 10 | UserStatus int 11 | HashID string 12 | FullName string 13 | Email string 14 | PhoneNumber string 15 | BirthDate time.Time 16 | Channel int 17 | Device int 18 | Lat string 19 | Long string 20 | DeviceToken string 21 | CreatedAt time.Time 22 | UpdatedAt time.Time 23 | IsTest bool 24 | } 25 | -------------------------------------------------------------------------------- /internal/entity/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // list of user types 10 | type ( 11 | // Type of user 12 | Type int 13 | // Hash of user 14 | Hash string 15 | // Country of user 16 | Country string 17 | // CountryCode of user 18 | CountryCode string 19 | // PhoneNumber of user 20 | PhoneNumber string 21 | ) 22 | 23 | // Validate user hash 24 | func (h Hash) Validate() error { 25 | if h == "" { 26 | return errors.New("user hash cannot be empty") 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // Validate user country 33 | func (c Country) Validate() error { 34 | switch c { 35 | case CountryID: 36 | return nil 37 | } 38 | return fmt.Errorf("country: country with name %s is not valid", c) 39 | } 40 | 41 | // User struct 42 | type User struct { 43 | ID string 44 | HashID Hash 45 | UserStatus int 46 | UserType Type 47 | PhoneNumber string 48 | Email string 49 | CreatedAt time.Time 50 | UpdatedAt time.Time 51 | IsTest bool 52 | } 53 | 54 | // Bio of user 55 | type Bio struct { 56 | UserID string 57 | FullName string 58 | // Gender Gender 59 | Avatar string 60 | Birthday time.Time 61 | CreatedAt time.Time 62 | UpdatedAt time.Time 63 | IsTest bool 64 | } 65 | -------------------------------------------------------------------------------- /internal/featureflag/consul/consul.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | // New consul feature-flag backend 4 | func New() { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /internal/featureflag/etcd/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | etcd-1: 5 | image: quay.io/coreos/etcd:v3.4.0 6 | container_name: ff_etcd1 7 | entrypoint: /usr/local/bin/etcd 8 | command: 9 | - '--name=etcd-1' 10 | - '--initial-advertise-peer-urls=http://etcd-1:2380' 11 | - '--listen-peer-urls=http://etcd-1:2380' 12 | - '--listen-client-urls=http://etcd-1:2379,http://localhost:2379' 13 | - '--advertise-client-urls=http://etcd-1:2379' 14 | - '--initial-cluster-token=mys3cr3ttok3n' 15 | - '--heartbeat-interval=250' 16 | - '--election-timeout=1250' 17 | - '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380' 18 | - '--initial-cluster-state=new' 19 | ports: 20 | - "2379:2379" 21 | 22 | etcd-2: 23 | image: quay.io/coreos/etcd:v3.4.0 24 | container_name: ff_etcd2 25 | entrypoint: /usr/local/bin/etcd 26 | command: 27 | - '--name=etcd-2' 28 | - '--initial-advertise-peer-urls=http://etcd-2:2380' 29 | - '--listen-peer-urls=http://etcd-2:2380' 30 | - '--listen-client-urls=http://etcd-2:2379,http://localhost:2379' 31 | - '--advertise-client-urls=http://etcd-2:2379' 32 | - '--initial-cluster-token=mys3cr3ttok3n' 33 | - '--heartbeat-interval=250' 34 | - '--election-timeout=1250' 35 | - '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380' 36 | - '--initial-cluster-state=new' 37 | ports: 38 | - "3379:2379" 39 | 40 | etcd-3: 41 | image: quay.io/coreos/etcd:v3.4.0 42 | container_name: ff_etcd3 43 | entrypoint: /usr/local/bin/etcd 44 | command: 45 | - '--name=etcd-3' 46 | - '--initial-advertise-peer-urls=http://etcd-3:2380' 47 | - '--listen-peer-urls=http://etcd-3:2380' 48 | - '--listen-client-urls=http://etcd-3:2379,http://localhost:2379' 49 | - '--advertise-client-urls=http://etcd-3:2379' 50 | - '--initial-cluster-token=mys3cr3ttok3n' 51 | - '--heartbeat-interval=250' 52 | - '--election-timeout=1250' 53 | - '--initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380' 54 | - '--initial-cluster-state=new' 55 | ports: 56 | - "4379:2379" -------------------------------------------------------------------------------- /internal/featureflag/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "go.etcd.io/etcd/client" 9 | ) 10 | 11 | // Etcd backend for feature flag 12 | type Etcd struct { 13 | c client.Client 14 | keysAPI client.KeysAPI 15 | } 16 | 17 | // New etcd feature-flag backend 18 | func New(endpoints []string, timeout time.Duration) (*Etcd, error) { 19 | c, err := client.New(client.Config{ 20 | Endpoints: endpoints, 21 | Transport: client.DefaultTransport, 22 | HeaderTimeoutPerRequest: timeout, 23 | }) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | etcd := Etcd{ 29 | c: c, 30 | keysAPI: client.NewKeysAPI(c), 31 | } 32 | return &etcd, nil 33 | } 34 | 35 | // Set key to etcd 36 | func (etcd *Etcd) Set(ctx context.Context, key, value string) error { 37 | resp, err := etcd.keysAPI.Set(ctx, key, value, nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if resp.Index == 0 { 43 | return errors.New("etcd: set failed, have no index") 44 | } 45 | return nil 46 | } 47 | 48 | // Get key from etcd 49 | func (etcd *Etcd) Get(ctx context.Context, key string) (string, error) { 50 | resp, err := etcd.keysAPI.Get(ctx, key, nil) 51 | if err != nil { 52 | return "", err 53 | } 54 | return resp.Node.Value, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/featureflag/featureflag.go: -------------------------------------------------------------------------------- 1 | package featureflag 2 | 3 | var _ff = FeatureFlag{} 4 | 5 | // FeatureFlag struct 6 | type FeatureFlag struct { 7 | backend Backend 8 | } 9 | 10 | // Backend of feature flag 11 | type Backend interface { 12 | GetString(key string) (string, error) 13 | GetBoolean(key string) (bool, error) 14 | GetInt(key string) (int, error) 15 | } 16 | 17 | // SetBackend for feature flag 18 | // so it can be used globally 19 | func SetBackend(backend Backend) { 20 | _ff.backend = backend 21 | _ff.work() 22 | } 23 | 24 | // StopUpdate the feature flag 25 | // means the checking to backend will be stopped 26 | func StopUpdate() { 27 | _ff.stop() 28 | } 29 | 30 | // regularly check 31 | func (ff *FeatureFlag) work() { 32 | 33 | } 34 | 35 | func (ff *FeatureFlag) stop() { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /internal/kothak/README.md: -------------------------------------------------------------------------------- 1 | # Kothak 2 | 3 | Kothak is a `resource` holder. This library holds resource connection and object. -------------------------------------------------------------------------------- /internal/kothak/object_storage.go: -------------------------------------------------------------------------------- 1 | package kothak 2 | 3 | // ObjectStorageConfig struct 4 | type ObjectStorageConfig struct { 5 | Name string `json:"name" yaml:"name" toml:"name"` 6 | Provider string `json:"provider" yaml:"provider" toml:"provider"` 7 | Region string `json:"region" yaml:"region" toml:"region"` 8 | Endpoint string `json:"endpoint" yaml:"endpoint" toml:"endpoint"` 9 | Bucket string `json:"bucket" yaml:"bucket" toml:"bucket"` 10 | BucketProto string `json:"bucket_proto" yaml:"bucket_proto" toml:"bucket_proto"` 11 | BucketURL string `json:"bucket_url" yaml:"bucket_url" toml:"bucket_url"` 12 | S3 S3Config `json:"s3" yaml:"s3" toml:"s3"` 13 | GCS GCSConfig `json:"gcs" yaml:"gcs" toml:"gcs"` 14 | } 15 | 16 | // S3Config for s3 storage 17 | type S3Config struct { 18 | ClientID string `json:"client_id" yaml:"client_id" toml:"client_id"` 19 | ClientSecret string `json:"client_secret" yaml:"client_secret" toml:"client_secret"` 20 | DisableSSL bool `json:"disable_ssl" yaml:"disable_ssl" toml:"disable_ssl"` 21 | ForcePathStyle bool `json:"force_path_style" yaml:"force_path_style" toml:"force_path_style"` 22 | } 23 | 24 | // GCSConfig for google cloud storage 25 | type GCSConfig struct { 26 | ClientID string `json:"client_id" yaml:"client_id" toml:"client_id"` 27 | ClientSecret string `json:"client_secret" yaml:"client_secret" toml:"client_secret"` 28 | JSONKey string `json:"json_key" yaml:"json_key" toml:"json_key"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/kothak/redis.go: -------------------------------------------------------------------------------- 1 | package kothak 2 | 3 | // Redis interface for infra 4 | type Redis interface { 5 | } 6 | 7 | // RedisConfig of kothak 8 | type RedisConfig struct { 9 | MaxIdle int `json:"max_idle_conn" yaml:"max_idle_conn" toml:"max_idle_conn"` 10 | MaxActive int `json:"max_active_conn" yaml:"max_active_conn" toml:"max_active_conn"` 11 | Timeout int `json:"timeout" yaml:"timeout" toml:"timeout"` 12 | Rds []RedisConnConfig `json:"connect" yaml:"connect" toml:"connect"` 13 | } 14 | 15 | // RedisConnConfig struct 16 | type RedisConnConfig struct { 17 | Name string `json:"name" yaml:"name" toml:"name"` 18 | Address string `json:"address" yaml:"address" toml:"address"` 19 | MaxIdle int `json:"max_idle_conn" yaml:"max_idle_conn" toml:"max_idle_conn"` 20 | MaxActive int `json:"max_active_conn" yaml:"max_active_conn" toml:"max_active_conn"` 21 | Timeout int `json:"timeout" yaml:"timeout" toml:"timeout"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/kothak/sql.go: -------------------------------------------------------------------------------- 1 | package kothak 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/albertwidi/go-project-example/internal/pkg/defaults" 7 | ) 8 | 9 | // DBConfig define sql databases configuration 10 | type DBConfig struct { 11 | MaxRetry int `json:"max_retry" yaml:"max_retry" toml:"max_retry" default:"1"` 12 | MaxOpenConnections int `json:"max_open_conns" yaml:"max_open_conns" toml:"max_open_conns" default:"10"` 13 | MaxIdleConnections int `json:"max_idle_conns" yaml:"max_idle_conns" toml:"max_idle_conns" default:"2"` 14 | ConnectionMaxLifetime string `json:"conn_max_lifetime" yaml:"conn_max_lifetime" toml:"conn_max_lifetime" default:"30s"` 15 | SQLDBs []SQLDBConfig `json:"connect" yaml:"connect" toml:"connect"` 16 | connMaxLifetime time.Duration 17 | } 18 | 19 | // SetDefault configuration 20 | func (dbconf *DBConfig) SetDefault() error { 21 | if err := defaults.SetDefault(dbconf); err != nil { 22 | return err 23 | } 24 | 25 | if dbconf.ConnectionMaxLifetime != "" { 26 | dur, err := time.ParseDuration(dbconf.ConnectionMaxLifetime) 27 | if err != nil { 28 | return err 29 | } 30 | dbconf.connMaxLifetime = dur 31 | } 32 | return nil 33 | } 34 | 35 | // SQLDBConfig of kothak 36 | type SQLDBConfig struct { 37 | Name string `yaml:"name" toml:"name"` 38 | Driver string `yaml:"driver" toml:"driver"` 39 | LeaderConnConfig SQLDBConnectionConfig `yaml:"leader" toml:"leader"` 40 | ReplicaConnConfig SQLDBConnectionConfig `yaml:"replica" toml:"replica"` 41 | } 42 | 43 | // SQLDBConnectionConfig struct 44 | type SQLDBConnectionConfig struct { 45 | DSN string `json:"dsn" yaml:"dsn" toml:"dsn" protected:"1"` 46 | MaxOpenConnections int `json:"max_open_conns" yaml:"max_open_conns" toml:"max_open_conns"` 47 | MaxIdleConnections int `json:"max_idle_conns" yaml:"max_idle_conns" toml:"max_idle_conns"` 48 | ConnectionMaxLifetime string `json:"conn_max_lifetime" yaml:"conn_max_lifetime" toml:"conn_max_lifetime"` 49 | MaxRetry int `json:"max_retry" yaml:"max_retry" toml:"max_retry"` 50 | connMaxLifeTime time.Duration 51 | } 52 | 53 | // SetDefault configuration 54 | func (connConfig *SQLDBConnectionConfig) SetDefault(dbconfig DBConfig) error { 55 | if connConfig.MaxRetry == 0 { 56 | connConfig.MaxRetry = dbconfig.MaxRetry 57 | } 58 | 59 | if connConfig.MaxOpenConnections == 0 { 60 | connConfig.MaxOpenConnections = dbconfig.MaxOpenConnections 61 | } 62 | 63 | if connConfig.MaxIdleConnections == 0 { 64 | connConfig.MaxIdleConnections = dbconfig.MaxIdleConnections 65 | } 66 | 67 | if connConfig.ConnectionMaxLifetime == "" { 68 | connConfig.ConnectionMaxLifetime = dbconfig.ConnectionMaxLifetime 69 | } 70 | 71 | connConfig.connMaxLifeTime = dbconfig.connMaxLifetime 72 | if connConfig.ConnectionMaxLifetime != "" { 73 | dur, err := time.ParseDuration(connConfig.ConnectionMaxLifetime) 74 | if err != nil { 75 | return err 76 | } 77 | connConfig.connMaxLifeTime = dur 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/objstoragepath/objstoragepath.go: -------------------------------------------------------------------------------- 1 | package objstoragepath 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "path" 9 | "strings" 10 | "time" 11 | 12 | imageentity "github.com/albertwidi/go-project-example/internal/entity/image" 13 | ) 14 | 15 | // Config of image path library 16 | type Config struct { 17 | Public DownloadConfig 18 | Private DownloadConfig 19 | } 20 | 21 | // DownloadConfig struct 22 | type DownloadConfig struct { 23 | DownloadProto string 24 | DownloadHost string 25 | DownloadPort string 26 | DownloadPath string 27 | } 28 | 29 | // FilePath struct 30 | type FilePath struct { 31 | Proto string 32 | Host string 33 | FilePath string 34 | DownloadPath string 35 | DownloadLink string 36 | Signed bool 37 | } 38 | 39 | // ObjectStoragePath generator 40 | type ObjectStoragePath struct { 41 | config *Config 42 | } 43 | 44 | // New object storage path 45 | func New(c *Config, local bool) (*ObjectStoragePath, error) { 46 | if c == nil { 47 | return nil, errors.New("imagepath: config cannot be nil") 48 | } 49 | 50 | if local { 51 | dialer := net.Dialer{Timeout: time.Second * 3} 52 | // TODO: check when internet connection is down 53 | conn, err := dialer.Dial("udp", "8.8.8.8:80") 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | addr := conn.LocalAddr().(*net.UDPAddr) 59 | // split the port 60 | s := strings.Split(addr.String(), ":") 61 | c.Public.DownloadHost = s[0] 62 | c.Private.DownloadHost = s[0] 63 | } 64 | return &ObjectStoragePath{config: c}, nil 65 | } 66 | 67 | // GetDownloadPath return needed download path 68 | func (o *ObjectStoragePath) GetDownloadPath(mode imageentity.Mode) (string, error) { 69 | switch mode { 70 | case imageentity.ModePublic: 71 | return o.config.Public.DownloadPath, nil 72 | case imageentity.ModePrivate: 73 | return o.config.Private.DownloadPath, nil 74 | default: 75 | return "", fmt.Errorf("objectstoragepath: invalid mode, got %s", mode) 76 | } 77 | } 78 | 79 | // Generate return file path struct with all download information needed 80 | func (o *ObjectStoragePath) Generate(mode imageentity.Mode, filePath string) (*FilePath, error) { 81 | var file FilePath 82 | if err := mode.Validate(); err != nil { 83 | return nil, err 84 | } 85 | 86 | switch mode { 87 | case imageentity.ModePublic: 88 | u, err := url.Parse(o.config.Public.DownloadProto + o.config.Public.DownloadHost + o.config.Public.DownloadPort) 89 | if err != nil { 90 | return nil, err 91 | } 92 | u.Path = path.Join(u.Path, o.config.Public.DownloadPath, filePath) 93 | 94 | file = FilePath{ 95 | Proto: o.config.Public.DownloadProto, 96 | Host: o.config.Public.DownloadHost, 97 | FilePath: filePath, 98 | DownloadPath: path.Join(o.config.Public.DownloadPath, filePath), 99 | DownloadLink: u.String(), 100 | } 101 | 102 | case imageentity.ModePrivate: 103 | u, err := url.Parse(o.config.Private.DownloadProto + o.config.Private.DownloadHost + o.config.Private.DownloadPort) 104 | if err != nil { 105 | return nil, err 106 | } 107 | u.Path = path.Join(u.Path, o.config.Private.DownloadPath) 108 | 109 | v := u.Query() 110 | v.Add("image_path", filePath) 111 | u.RawQuery = v.Encode() 112 | 113 | file = FilePath{ 114 | Proto: o.config.Private.DownloadProto, 115 | Host: o.config.Private.DownloadHost, 116 | FilePath: filePath, 117 | DownloadPath: path.Join(o.config.Private.DownloadPath, filePath), 118 | DownloadLink: u.String(), 119 | } 120 | 121 | case imageentity.ModeSigned: 122 | file = FilePath{ 123 | Proto: "", 124 | Host: "", 125 | FilePath: "", 126 | DownloadPath: "", 127 | DownloadLink: filePath, 128 | Signed: true, 129 | } 130 | } 131 | return &file, nil 132 | } 133 | -------------------------------------------------------------------------------- /internal/pkg/README.md: -------------------------------------------------------------------------------- 1 | # Lib 2 | 3 | Contains package/library used for this project. 4 | 5 | Some package might be very speficic for this project usage. For example: 6 | 7 | - Imagepath 8 | - Context 9 | - Router 10 | - Http 11 | -------------------------------------------------------------------------------- /internal/pkg/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/albertwidi/go-project-example/internal/pkg/http/response" 10 | ) 11 | 12 | // RequestContext struct 13 | type RequestContext struct { 14 | httpResponseWriter http.ResponseWriter 15 | httpRequest *http.Request 16 | address string 17 | path string 18 | method string 19 | } 20 | 21 | // Constructor of context 22 | type Constructor struct { 23 | HTTPResponseWriter http.ResponseWriter 24 | HTTPRequest *http.Request 25 | Address string 26 | Path string 27 | Method string 28 | } 29 | 30 | // New context 31 | func New(constructor Constructor) *RequestContext { 32 | rc := RequestContext{ 33 | httpResponseWriter: constructor.HTTPResponseWriter, 34 | httpRequest: constructor.HTTPRequest, 35 | address: constructor.Address, 36 | path: constructor.Path, 37 | method: constructor.Method, 38 | } 39 | return &rc 40 | } 41 | 42 | // Address return the address where request arrived to 43 | func (rc *RequestContext) Address() string { 44 | return rc.address 45 | } 46 | 47 | // Request return http request from request context 48 | func (rc *RequestContext) Request() *http.Request { 49 | return rc.httpRequest 50 | } 51 | 52 | // RequestHeader return http.Request.Header 53 | func (rc *RequestContext) RequestHeader() http.Header { 54 | return rc.httpRequest.Header 55 | } 56 | 57 | // RequestHandler return handler name of the request 58 | func (rc *RequestContext) RequestHandler() string { 59 | return rc.path 60 | } 61 | 62 | // Context return the http.Request.Context 63 | func (rc *RequestContext) Context() context.Context { 64 | return rc.httpRequest.Context() 65 | } 66 | 67 | // ResponseWriter return http response writer from request context 68 | func (rc *RequestContext) ResponseWriter() http.ResponseWriter { 69 | return rc.httpResponseWriter 70 | } 71 | 72 | // JSON to create a json response via http response lib 73 | func (rc *RequestContext) JSON() *response.JSONResponse { 74 | j := response.JSON(rc.httpResponseWriter) 75 | return j 76 | } 77 | 78 | // DecodeJSON from request body 79 | func (rc *RequestContext) DecodeJSON(out interface{}) error { 80 | in, err := ioutil.ReadAll(rc.httpRequest.Body) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if err := json.Unmarshal(in, out); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/pkg/conv/conv.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import "strconv" 4 | 5 | //Float64ToString return string form of float64 6 | func Float64ToString(f float64, precission int) string { 7 | return strconv.FormatFloat(f, 'f', precission, 64) 8 | } 9 | 10 | //Int64ToString return string form of int64 11 | func Int64ToString(i int64) string { 12 | return strconv.FormatInt(i, 10) 13 | } 14 | 15 | // StringToInt64 convert string to int64 16 | func StringToInt64(i string) (int64, error) { 17 | n, err := strconv.ParseInt(i, 10, 64) 18 | if err != nil { 19 | return 0, err 20 | } 21 | 22 | return n, nil 23 | } 24 | 25 | // AnyToString convert type (int, int64, float32, float64, byte, and []bytes) to string 26 | // BEWARE: do not use this function for a very spesific usecase 27 | func AnyToString(n interface{}, p ...int) string { 28 | var t string 29 | switch n.(type) { 30 | case int: 31 | t = strconv.Itoa(n.(int)) 32 | case int64: 33 | t = strconv.FormatInt(n.(int64), 10) 34 | case float32: 35 | if len(p) > 0 { 36 | t = strconv.FormatFloat(float64(n.(float32)), 'f', p[0], 64) 37 | } else { 38 | t = strconv.FormatFloat(float64(n.(float32)), 'f', -1, 64) 39 | } 40 | case float64: 41 | if len(p) > 0 { 42 | t = strconv.FormatFloat(n.(float64), 'f', p[0], 64) 43 | } else { 44 | t = strconv.FormatFloat(n.(float64), 'f', -1, 64) 45 | } 46 | case byte: 47 | t = string(n.(byte)) 48 | case []byte: 49 | t = string(n.([]byte)) 50 | case string: 51 | t = n.(string) 52 | case bool: 53 | t = strconv.FormatBool(n.(bool)) 54 | } 55 | 56 | return t 57 | } 58 | -------------------------------------------------------------------------------- /internal/pkg/conv/conv_test.go: -------------------------------------------------------------------------------- 1 | package conv_test 2 | -------------------------------------------------------------------------------- /internal/pkg/cucumber/cucumber.go: -------------------------------------------------------------------------------- 1 | package cucumber 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/cucumber/godog" 10 | ) 11 | 12 | // Feature interface 13 | type Feature interface { 14 | BeforeRegister() error 15 | SetLogger(logger *log.Logger) 16 | FeatureContext(s *godog.Suite) 17 | } 18 | 19 | // Cucumber object 20 | type Cucumber struct { 21 | features []Feature 22 | options Options 23 | // logger 24 | logger *log.Logger 25 | } 26 | 27 | // Options struct 28 | type Options struct { 29 | Debug Debug 30 | } 31 | 32 | // Debug options 33 | type Debug struct { 34 | LogFile string 35 | } 36 | 37 | // New cucumber instance 38 | func New(opts *Options) (*Cucumber, error) { 39 | var ( 40 | options Options 41 | f *os.File 42 | err error 43 | ) 44 | if opts != nil { 45 | options = *opts 46 | } 47 | 48 | // set the logger 49 | if options.Debug.LogFile != "" { 50 | _, err := os.Stat(options.Debug.LogFile) 51 | if err != nil && !os.IsNotExist(err) { 52 | return nil, err 53 | } 54 | 55 | if os.IsNotExist(err) { 56 | err := os.MkdirAll(filepath.Dir(options.Debug.LogFile), 0744) 57 | if err != nil && err != os.ErrExist { 58 | return nil, err 59 | } 60 | 61 | f, err = os.OpenFile(options.Debug.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | if err == nil { 67 | f, err = os.OpenFile(options.Debug.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 68 | if err != nil { 69 | return nil, err 70 | } 71 | } 72 | } else { 73 | // throw logger to dev null 74 | f, err = os.Open("/dev/null") 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | 80 | // write to multi writer 81 | multi := io.MultiWriter(os.Stdout, f) 82 | // set logger 83 | logger := &log.Logger{} 84 | logger.SetOutput(multi) 85 | c := Cucumber{ 86 | features: make([]Feature, 0), 87 | options: options, 88 | logger: logger, 89 | } 90 | return &c, nil 91 | } 92 | 93 | // Logger return the cucumber logger 94 | func (c *Cucumber) Logger() *log.Logger { 95 | return c.logger 96 | } 97 | 98 | // RegisterFeatures func 99 | func (c *Cucumber) RegisterFeatures(features ...Feature) error { 100 | for _, f := range features { 101 | f.SetLogger(c.Logger()) 102 | if err := f.BeforeRegister(); err != nil { 103 | return err 104 | } 105 | } 106 | c.features = append(c.features, features...) 107 | return nil 108 | } 109 | 110 | // FeatureContext for triggering godog 111 | func (c *Cucumber) FeatureContext(s *godog.Suite) { 112 | for _, f := range c.features { 113 | f.FeatureContext(s) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /internal/pkg/cucumber/cucumber_test.go: -------------------------------------------------------------------------------- 1 | // this test is intended for testing the godog itself and working as integration test 2 | 3 | package cucumber_test 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "testing" 15 | "time" 16 | 17 | "github.com/albertwidi/go-project-example/internal/pkg/cucumber" 18 | "github.com/cucumber/godog" 19 | ) 20 | 21 | func runWebServer() error { 22 | type BookRequest struct { 23 | BookID int64 `json:"book_id"` 24 | } 25 | type BookResponse struct { 26 | BookID int64 `json:"book_id"` 27 | Name string `json:"name"` 28 | Author string `json:"author"` 29 | } 30 | http.HandleFunc("/v1/book/detail", func(w http.ResponseWriter, r *http.Request) { 31 | b := BookRequest{} 32 | out, err := ioutil.ReadAll(r.Body) 33 | if err != nil { 34 | w.WriteHeader(http.StatusInternalServerError) 35 | w.Write([]byte(err.Error())) 36 | return 37 | } 38 | 39 | if err := json.Unmarshal(out, &b); err != nil { 40 | err = fmt.Errorf("BookDetailEndpoint: %w", err) 41 | w.WriteHeader(http.StatusInternalServerError) 42 | w.Write([]byte(err.Error())) 43 | return 44 | } 45 | 46 | resp := BookResponse{ 47 | BookID: b.BookID, 48 | Name: "testing", 49 | Author: "what?", 50 | } 51 | out, err = json.Marshal(resp) 52 | if err != nil { 53 | err = fmt.Errorf("BookDetailEndpoint: %w", err) 54 | w.WriteHeader(http.StatusInternalServerError) 55 | w.Write([]byte(err.Error())) 56 | return 57 | } 58 | 59 | w.WriteHeader(http.StatusOK) 60 | w.Write(out) 61 | return 62 | }) 63 | if err := http.ListenAndServe(":9863", nil); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | func TestMain(m *testing.M) { 70 | var ( 71 | options = godog.Options{ 72 | Output: os.Stdout, 73 | Format: "pretty", 74 | } 75 | errChan = make(chan error) 76 | ) 77 | 78 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 79 | defer cancel() 80 | 81 | go func() { 82 | errChan <- runWebServer() 83 | }() 84 | 85 | select { 86 | case err := <-errChan: 87 | log.Fatal(err) 88 | case <-ctx.Done(): 89 | // this means the webserver is alive 90 | break 91 | } 92 | 93 | flag.Parse() 94 | options.Paths = flag.Args() 95 | 96 | c, err := cucumber.New(&cucumber.Options{ 97 | Debug: cucumber.Debug{ 98 | LogFile: "test.log", 99 | }, 100 | }) 101 | if err != nil { 102 | log.Fatal(err) 103 | } 104 | 105 | apiFeature := &cucumber.APIFeature{ 106 | Options: cucumber.APIFeatureOptions{ 107 | EndpointsMapping: map[string]string{ 108 | "book": "http://127.0.0.1:9863", 109 | }, 110 | }, 111 | } 112 | c.RegisterFeatures(apiFeature) 113 | 114 | status := godog.RunWithOptions("godogs", func(s *godog.Suite) { 115 | c.FeatureContext(s) 116 | }, options) 117 | 118 | if st := m.Run(); st > status { 119 | status = st 120 | } 121 | os.Exit(status) 122 | } 123 | -------------------------------------------------------------------------------- /internal/pkg/cucumber/features/api.feature: -------------------------------------------------------------------------------- 1 | Feature: get book detail 2 | In order eto get book 3 | As an API user 4 | I need to be able to request book detail 5 | 6 | Scenario: get book detail 7 | Given set request header "Content-Type: application/json" 8 | And set request body: 9 | """ 10 | { 11 | "book_id": 10 12 | } 13 | """ 14 | When I send "POST" request to "http://127.0.0.1:9863/v1/book/detail" 15 | Then the response code should be 200 16 | And the response header "Content-Type" should be "text/plain; charset=utf-8" 17 | And the response should match json: 18 | """ 19 | { 20 | "book_id": 10, 21 | "name": "testing", 22 | "author": "what?" 23 | } 24 | """ 25 | 26 | Scenario: get book detail with endpoint mapping and path 27 | Given set request header "Content-Type: application/json" 28 | And set request body: 29 | """ 30 | { 31 | "book_id": 10 32 | } 33 | """ 34 | When I send "POST" request to "book service" with path "/v1/book/detail" 35 | Then the response code should be 200 36 | And the response header "Content-Type" should be "text/plain; charset=utf-8" 37 | And the response should match json: 38 | """ 39 | { 40 | "book_id": 10, 41 | "name": "testing", 42 | "author": "what?" 43 | } 44 | """ 45 | 46 | Scenario Outline: this is an example of scenario outlines 47 | Given set request header "" 48 | And set request body: 49 | """ 50 | 51 | """ 52 | When I send "" request to " service" with path "" 53 | Then the response code should be 54 | And the response header "" should be "" 55 | And the response should match json: 56 | """ 57 | 58 | """ 59 | Examples: 60 | | request_content_type | request_body | method | service | path | response_code | response_header_key | response_header_value | response_body | 61 | | Content-Type: application/json | {"book_id": 10} | POST | book | /v1/book/detail | 200 | Content-Type | text/plain; charset=utf-8 | {"book_id": 10, "name": "testing", "author": "what?"} | 62 | | Content-Type: application/json | {"book_id": 20} | POST | book | /v1/book/detail | 200 | Content-Type | text/plain; charset=utf-8 | {"book_id": 20, "name": "testing", "author": "what?"} | -------------------------------------------------------------------------------- /internal/pkg/cucumber/file.go: -------------------------------------------------------------------------------- 1 | package cucumber 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/cucumber/godog" 8 | ) 9 | 10 | // FileFeature for cucumber 11 | type FileFeature struct { 12 | } 13 | 14 | func (ff *FileFeature) fileShouldExist(filepath string) error { 15 | _, err := os.Stat(filepath) 16 | if err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | 22 | func (ff *FileFeature) fileShouldNotExist(filepath string) error { 23 | _, err := os.Stat(filepath) 24 | if os.IsNotExist(err) { 25 | return nil 26 | } 27 | if err == nil { 28 | return fmt.Errorf("file: file with filepath: %s still exists", filepath) 29 | } 30 | return err 31 | } 32 | 33 | func (ff *FileFeature) deleteFile(filepath string) error { 34 | return os.Remove(filepath) 35 | } 36 | 37 | // FeatureContext for file 38 | func (ff *FileFeature) FeatureContext(s *godog.Suite) { 39 | s.Step(`^I delete file "([^"]*)"$`, ff.deleteFile) 40 | s.Step(`^the file "([^"]*)" should exist`, ff.fileShouldExist) 41 | s.Step(`^the file "([^"]*)" should not exist`, ff.fileShouldNotExist) 42 | } 43 | -------------------------------------------------------------------------------- /internal/pkg/cucumber/file_test.go: -------------------------------------------------------------------------------- 1 | package cucumber 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestFileShouldExist(t *testing.T) { 9 | file := "file_test.go" 10 | ff := FileFeature{} 11 | if err := ff.fileShouldExist(file); err != nil { 12 | t.Error(err) 13 | return 14 | } 15 | } 16 | 17 | func TestFileShouldNotExist(t *testing.T) { 18 | file := "file_should_never_exist" 19 | ff := FileFeature{} 20 | if err := ff.fileShouldNotExist(file); err != nil { 21 | t.Error(err) 22 | return 23 | } 24 | } 25 | 26 | func TestDeleteFile(t *testing.T) { 27 | fileName := "test.txt" 28 | if _, err := os.Create(fileName); err != nil { 29 | t.Error(err) 30 | return 31 | } 32 | 33 | ff := FileFeature{} 34 | if err := ff.fileShouldExist(fileName); err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | 39 | if err := ff.deleteFile(fileName); err != nil { 40 | t.Error(err) 41 | return 42 | } 43 | 44 | if err := ff.fileShouldNotExist(fileName); err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/pkg/cucumber/test.log: -------------------------------------------------------------------------------- 1 | ApiFeature: invoking feature context 2 | request body: { 3 | "book_id": 10 4 | } 5 | request body: { 6 | "book_id": 10 7 | } 8 | request body: {"book_id": 10} 9 | request body: {"book_id": 20} 10 | ApiFeature: invoking feature context 11 | ApiFeature: invoking feature context 12 | ApiFeature: invoking feature context 13 | ApiFeature: invoking feature context 14 | ApiFeature: invoking feature context 15 | ApiFeature: invoking feature context 16 | ApiFeature: invoking feature context 17 | ApiFeature: invoking feature context 18 | -------------------------------------------------------------------------------- /internal/pkg/defaults/README.md: -------------------------------------------------------------------------------- 1 | # Defaults 2 | 3 | Defaults is a library to set default value to a struct using a struct `tag`. Struct tag `default:"value"` is used to replace struct field value. 4 | The field only set with default value if the field value is empty or zero, which means: 5 | - `0` for number 6 | - `""` for string 7 | 8 | ## Boolean is not supported 9 | 10 | Boolean is not supported because when the default is `true`, it will always be set to `true`. 11 | Because `false` is similar to `0` for number. 12 | 13 | The workaround for this problem is to make your `boolean` field to always has `false` as default value. 14 | 15 | ## Example 16 | 17 | ```go 18 | type A struct { 19 | Str string `default:"asd"` 20 | I int `default:"123"` 21 | } 22 | 23 | func main() { 24 | a := A{ 25 | I:10, 26 | } 27 | if err := defaults.SetDefault(&a); err != nil { 28 | // do something to error 29 | } 30 | } 31 | ``` 32 | 33 | The `Str` field in struct `A` will be replaced by value in the `default` tag. 34 | 35 | ## Default Value Replacer (Experimental) 36 | 37 | Replacing a struct default value with another struct value. This is useful for overriding some values in a config. 38 | 39 | More to be added here. 40 | 41 | ## Known Issues 42 | 43 | The integer and float value is vurnerable to `incompability` of its type. For example, a very large `int` value is being set for `int8` value. 44 | 45 | ```go 46 | type A struct { 47 | I8 int8 `default:"100000"` 48 | } 49 | ``` 50 | 51 | Will make `I8` value to `0` -------------------------------------------------------------------------------- /internal/pkg/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | var ( 11 | timeDurationType = reflect.TypeOf(time.Second) 12 | ) 13 | 14 | // error list 15 | var ( 16 | // ErrPassValue is returned if the caller pass a value instead of a pointer 17 | ErrPassValue = errors.New("must pass a pointer, not a value") 18 | // ErrNotStruct is returned if the caller pass a pointer of non struct 19 | ErrNotStruct = errors.New("must pass a pointer of struct") 20 | ) 21 | 22 | // SetDefault set default value from struct tag: default 23 | // for example: 24 | // type A struct { 25 | // S string `default:"this is default"` 26 | // } 27 | func SetDefault(v interface{}) error { 28 | val := reflect.ValueOf(v) 29 | // prevent silent error, if a value is sent, the original value won't change 30 | if val.Kind() != reflect.Ptr { 31 | return ErrPassValue 32 | } 33 | indirect := reflect.Indirect(val) 34 | // prevent panic when call NumField() 35 | if indirect.Kind() != reflect.Struct { 36 | return ErrNotStruct 37 | } 38 | 39 | numfield := indirect.NumField() 40 | for i := 0; i < numfield; i++ { 41 | fi := indirect.Field(i) 42 | if !fi.CanSet() { 43 | continue 44 | } 45 | 46 | // continue if it is not empty value 47 | if !reflect.DeepEqual(reflect.Zero(fi.Type()).Interface(), fi.Interface()) { 48 | continue 49 | } 50 | 51 | f := indirect.Type().Field(i) 52 | t := f.Tag.Get("default") 53 | // continue if default tag is not available 54 | if t == "" { 55 | continue 56 | } 57 | 58 | // for special types which have their own parser 59 | switch f.Type { 60 | case timeDurationType: 61 | n, err := time.ParseDuration(t) 62 | if err != nil { 63 | return err 64 | } 65 | fi.Set(reflect.ValueOf(n)) 66 | continue 67 | } 68 | 69 | // for primitive types 70 | switch f.Type.Kind() { 71 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 72 | n, err := strconv.ParseInt(t, 10, 64) 73 | if err != nil { 74 | return err 75 | } 76 | fi.Set(reflect.ValueOf(n).Convert(f.Type)) 77 | case reflect.Float32, reflect.Float64: 78 | n, err := strconv.ParseFloat(t, 64) 79 | if err != nil { 80 | return err 81 | } 82 | fi.Set(reflect.ValueOf(n).Convert(f.Type)) 83 | case reflect.String: 84 | fi.Set(reflect.ValueOf(t).Convert(f.Type)) 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | // ReplaceDefaultFrom will replace value of a struct with another value in a struct 91 | // to be replaced, field name and type between two struct must be the same 92 | // for example: 93 | // type A struct { 94 | // Field1 string 95 | // } 96 | // 97 | // type B struct { 98 | // Field1 string 99 | //} 100 | func ReplaceDefaultFrom(source interface{}, replacer interface{}) error { 101 | val := reflect.ValueOf(source) 102 | // prevent silent error, if a value is sent, the original value won't change 103 | if val.Kind() != reflect.Ptr { 104 | return ErrPassValue 105 | } 106 | 107 | inreplacer := reflect.Indirect(reflect.ValueOf(replacer)) 108 | insource := reflect.Indirect(val) 109 | // prevent panic when call NumField() and FieldByName() 110 | if insource.Kind() != reflect.Struct || inreplacer.Kind() != reflect.Struct { 111 | return ErrNotStruct 112 | } 113 | 114 | numfield := insource.NumField() 115 | for i := 0; i < numfield; i++ { 116 | field := insource.Field(i) 117 | fieldName := insource.Type().Field(i).Name 118 | replacerfield := inreplacer.FieldByName(fieldName) 119 | 120 | // skip if cannot set field or field not found in replacer or have different kind 121 | if !field.CanSet() || !replacerfield.IsValid() || field.Kind() != replacerfield.Kind() { 122 | continue 123 | } 124 | 125 | // continue if it is not empty value 126 | if !reflect.DeepEqual(reflect.Zero(field.Type()).Interface(), field.Interface()) { 127 | continue 128 | } 129 | 130 | field.Set(replacerfield) 131 | } 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /internal/pkg/defaults/defaults_test.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type ( 11 | Def struct { 12 | Str string `default:"abcde"` 13 | I64 int64 `default:"123"` 14 | I32 int32 `default:"123"` 15 | I16 int16 `default:"123"` 16 | I8 int8 `default:"123"` 17 | I int `default:"123"` 18 | F32 float32 `default:"123.32"` 19 | F64 float64 `default:"123.32"` 20 | Dur time.Duration `default:"1m23s"` 21 | } 22 | 23 | Def2 struct { 24 | I16 int16 25 | F32 float32 26 | Str string 27 | I64 int32 28 | } 29 | ) 30 | 31 | var ( 32 | allSet = Def{ 33 | Str: "abcde", 34 | I64: 123, 35 | I32: 123, 36 | I16: 123, 37 | I8: 123, 38 | I: 123, 39 | F32: 123.32, 40 | F64: 123.32, 41 | Dur: 1*time.Minute + 23*time.Second, 42 | } 43 | allSet2 = allSet 44 | notStruct = "not struct" 45 | ) 46 | 47 | func TestSetDefault(t *testing.T) { 48 | allSet2.Str = "abc" 49 | notStruct := "not struct" 50 | 51 | testCases := []struct { 52 | name string 53 | inp interface{} 54 | out interface{} 55 | err error 56 | }{ 57 | { 58 | name: "from all empty", 59 | inp: &Def{}, 60 | out: &allSet, 61 | err: nil, 62 | }, 63 | { 64 | name: "all set", 65 | inp: &allSet, 66 | out: &allSet, 67 | err: nil, 68 | }, 69 | { 70 | name: "pass value", 71 | inp: Def{}, 72 | out: Def{}, 73 | err: ErrPassValue, 74 | }, 75 | { 76 | name: "pass nil", 77 | inp: nil, 78 | out: nil, 79 | err: ErrPassValue, 80 | }, 81 | { 82 | name: "partially set", 83 | inp: &Def{Str: "abc"}, 84 | out: &allSet2, 85 | err: nil, 86 | }, 87 | { 88 | name: "not struct", 89 | inp: ¬Struct, 90 | out: ¬Struct, 91 | err: ErrNotStruct, 92 | }, 93 | } 94 | 95 | for _, tc := range testCases { 96 | t.Run(tc.name, func(t *testing.T) { 97 | err := SetDefault(tc.inp) 98 | require.Equal(t, tc.err, err) 99 | 100 | require.Equal(t, tc.out, tc.inp) 101 | }) 102 | } 103 | } 104 | 105 | func TestSetDefaultFrom(t *testing.T) { 106 | allSet2.Str = "abc" 107 | 108 | testCases := []struct { 109 | name string 110 | src interface{} 111 | rep interface{} 112 | exp interface{} 113 | err error 114 | }{ 115 | { 116 | name: "from all empty", 117 | src: &Def{}, 118 | rep: allSet, 119 | exp: &allSet, 120 | err: nil, 121 | }, 122 | { 123 | name: "pass value", 124 | src: Def{}, 125 | rep: allSet, 126 | exp: Def{}, 127 | err: ErrPassValue, 128 | }, 129 | { 130 | name: "pass nil", 131 | src: nil, 132 | rep: allSet, 133 | exp: nil, 134 | err: ErrPassValue, 135 | }, 136 | { 137 | name: "not struct", 138 | src: ¬Struct, 139 | rep: notStruct, 140 | exp: ¬Struct, 141 | err: ErrNotStruct, 142 | }, 143 | { 144 | name: "partially set", 145 | src: &Def{Str: "abc"}, 146 | rep: allSet, 147 | exp: &allSet2, 148 | err: nil, 149 | }, 150 | { 151 | name: "different type", 152 | src: &Def{}, 153 | rep: Def2{ 154 | Str: "abcde", 155 | I16: 123, 156 | F32: 123.32, 157 | I64: 123, 158 | }, 159 | exp: &Def{ 160 | Str: "abcde", 161 | I16: 123, 162 | F32: 123.32, 163 | I64: 0, // not set due to mismatch type 164 | }, 165 | err: nil, 166 | }, 167 | } 168 | 169 | for _, tc := range testCases { 170 | t.Run(tc.name, func(t *testing.T) { 171 | err := ReplaceDefaultFrom(tc.src, tc.rep) 172 | require.Equal(t, tc.err, err) 173 | 174 | require.Equal(t, tc.exp, tc.src) 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /internal/pkg/envfile/README.md: -------------------------------------------------------------------------------- 1 | # Envfile 2 | 3 | Helper file for environment variable loader 4 | 5 | This is useful for local environment -------------------------------------------------------------------------------- /internal/pkg/envfile/envfile.go: -------------------------------------------------------------------------------- 1 | package envfile 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/BurntSushi/toml" 11 | "github.com/albertwidi/go-project-example/internal/pkg/conv" 12 | yaml "gopkg.in/yaml.v2" 13 | ) 14 | 15 | // EnvConfigYAML struct 16 | type EnvConfigYAML struct { 17 | Envs []EnvYAML `yaml:"envs"` 18 | } 19 | 20 | // EnvYAML file struct 21 | type EnvYAML struct { 22 | Name string `yaml:"name"` 23 | Value string `yaml:"value"` 24 | } 25 | 26 | // Load envfile 27 | // capable of reading multiple files 28 | func Load(files ...string) error { 29 | for _, envFile := range files { 30 | if envFile == "" { 31 | continue 32 | } 33 | 34 | ext := filepath.Ext(envFile) 35 | var kv map[string]interface{} 36 | var err error 37 | 38 | switch ext { 39 | case ".toml": 40 | kv, err = loadToml(envFile) 41 | case ".yaml", ".yml": 42 | kv, err = loadYaml(envFile) 43 | default: 44 | err = fmt.Errorf("cannot process file with format %s", ext) 45 | } 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // insert all value in the yaml file into env variable 52 | for k, v := range kv { 53 | if err := os.Setenv(strings.ToUpper(k), conv.AnyToString(v)); err != nil { 54 | return err 55 | } 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func loadToml(file string) (map[string]interface{}, error) { 62 | kv := make(map[string]interface{}) 63 | _, err := toml.DecodeFile(file, &kv) 64 | 65 | return kv, err 66 | } 67 | 68 | func loadYaml(file string) (map[string]interface{}, error) { 69 | envs := EnvConfigYAML{} 70 | out, err := ioutil.ReadFile(file) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if err := yaml.Unmarshal(out, &envs); err != nil { 76 | return nil, err 77 | } 78 | 79 | kv := make(map[string]interface{}) 80 | 81 | for _, e := range envs.Envs { 82 | kv[e.Name] = e.Value 83 | } 84 | 85 | return kv, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/pkg/envfile/testfile/envfile.yaml: -------------------------------------------------------------------------------- 1 | envs: 2 | - name: "TEST1" 3 | value: "TEST1" 4 | - name: "TEST2" 5 | value: "TEST2" -------------------------------------------------------------------------------- /internal/pkg/http/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "net/http" 4 | 5 | // Wrapper for http client 6 | type Wrapper struct { 7 | c *http.Client 8 | } 9 | 10 | // Wrap htpp client 11 | func Wrap(client *http.Client) *Wrapper { 12 | w := Wrapper{client} 13 | return &w 14 | } 15 | 16 | // Options for http client 17 | type Options struct { 18 | } 19 | 20 | // New http client 21 | func New(options Options) *Wrapper { 22 | w := Wrapper{ 23 | c: &http.Client{}, 24 | } 25 | return &w 26 | } 27 | 28 | // Get wrap the http client get request 29 | func (w *Wrapper) Get() { 30 | 31 | } 32 | -------------------------------------------------------------------------------- /internal/pkg/http/misc/misc.go: -------------------------------------------------------------------------------- 1 | // misc contains function from prometheus promhttp package 2 | 3 | package misc 4 | 5 | import ( 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // SanitizeMethod for sanitizing http request method 12 | // function from prometheus promhttp package 13 | func SanitizeMethod(m string) string { 14 | switch m { 15 | case "GET", "get": 16 | return "get" 17 | case "PUT", "put": 18 | return "put" 19 | case "HEAD", "head": 20 | return "head" 21 | case "POST", "post": 22 | return "post" 23 | case "DELETE", "delete": 24 | return "delete" 25 | case "CONNECT", "connect": 26 | return "connect" 27 | case "OPTIONS", "options": 28 | return "options" 29 | case "NOTIFY", "notify": 30 | return "notify" 31 | default: 32 | return strings.ToLower(m) 33 | } 34 | } 35 | 36 | // SanitizeCode for sanitizing http code 37 | // If the wrapped http.Handler has not set a status code, i.e. the value is 38 | // currently 0, santizeCode will return 200, for consistency with behavior in 39 | // the stdlib. 40 | func SanitizeCode(s int) string { 41 | switch s { 42 | case 100: 43 | return "100" 44 | case 101: 45 | return "101" 46 | 47 | case 200, 0: 48 | return "200" 49 | case 201: 50 | return "201" 51 | case 202: 52 | return "202" 53 | case 203: 54 | return "203" 55 | case 204: 56 | return "204" 57 | case 205: 58 | return "205" 59 | case 206: 60 | return "206" 61 | 62 | case 300: 63 | return "300" 64 | case 301: 65 | return "301" 66 | case 302: 67 | return "302" 68 | case 304: 69 | return "304" 70 | case 305: 71 | return "305" 72 | case 307: 73 | return "307" 74 | 75 | case 400: 76 | return "400" 77 | case 401: 78 | return "401" 79 | case 402: 80 | return "402" 81 | case 403: 82 | return "403" 83 | case 404: 84 | return "404" 85 | case 405: 86 | return "405" 87 | case 406: 88 | return "406" 89 | case 407: 90 | return "407" 91 | case 408: 92 | return "408" 93 | case 409: 94 | return "409" 95 | case 410: 96 | return "410" 97 | case 411: 98 | return "411" 99 | case 412: 100 | return "412" 101 | case 413: 102 | return "413" 103 | case 414: 104 | return "414" 105 | case 415: 106 | return "415" 107 | case 416: 108 | return "416" 109 | case 417: 110 | return "417" 111 | case 418: 112 | return "418" 113 | 114 | case 500: 115 | return "500" 116 | case 501: 117 | return "501" 118 | case 502: 119 | return "502" 120 | case 503: 121 | return "503" 122 | case 504: 123 | return "504" 124 | case 505: 125 | return "505" 126 | 127 | case 428: 128 | return "428" 129 | case 429: 130 | return "429" 131 | case 431: 132 | return "431" 133 | case 511: 134 | return "511" 135 | 136 | default: 137 | return strconv.Itoa(s) 138 | } 139 | } 140 | 141 | // ComputeApproximateRequestSize for computing the http request size 142 | func ComputeApproximateRequestSize(r *http.Request) int { 143 | s := 0 144 | if r.URL != nil { 145 | s += len(r.URL.String()) 146 | } 147 | 148 | s += len(r.Method) 149 | s += len(r.Proto) 150 | for name, values := range r.Header { 151 | s += len(name) 152 | for _, value := range values { 153 | s += len(value) 154 | } 155 | } 156 | s += len(r.Host) 157 | 158 | // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. 159 | 160 | if r.ContentLength != -1 { 161 | s += int(r.ContentLength) 162 | } 163 | return s 164 | } 165 | -------------------------------------------------------------------------------- /internal/pkg/http/monitoring/monitoring.go: -------------------------------------------------------------------------------- 1 | // this package provide delagation of http.ResponseWriter for monitoring purpose 2 | 3 | package monitoring 4 | 5 | import "net/http" 6 | 7 | // Delegator interface for delegating http.ResponseWriter 8 | type Delegator interface { 9 | http.ResponseWriter 10 | Status() int 11 | Written() int64 12 | } 13 | 14 | // responseWriterDelagator is a http.ResponseWriter delegator 15 | type responseWriterDelegator struct { 16 | http.ResponseWriter 17 | status int 18 | written int64 19 | wroteHeader bool 20 | } 21 | 22 | // Status return the status of WriteHeader 23 | func (rwdg *responseWriterDelegator) Status() int { 24 | return rwdg.status 25 | } 26 | 27 | func (rwdg *responseWriterDelegator) Written() int64 { 28 | return rwdg.written 29 | } 30 | 31 | // WriteHeader via http.ResponseWriter 32 | func (rwdg *responseWriterDelegator) WriteHeader(code int) { 33 | if rwdg.wroteHeader { 34 | return 35 | } 36 | rwdg.status = code 37 | rwdg.wroteHeader = true 38 | rwdg.ResponseWriter.WriteHeader(code) 39 | } 40 | 41 | // Write the byte via http.ResponseWriter 42 | func (rwdg *responseWriterDelegator) Write(b []byte) (int, error) { 43 | if !rwdg.wroteHeader { 44 | rwdg.WriteHeader(http.StatusOK) 45 | } 46 | 47 | n, err := rwdg.ResponseWriter.Write(b) 48 | rwdg.written += int64(n) 49 | return n, err 50 | } 51 | 52 | // NewResponseWriterDelegator is a delegator for http.ResponseWriter 53 | // with some additional function for monitoring purpose 54 | func NewResponseWriterDelegator(w http.ResponseWriter) Delegator { 55 | d := responseWriterDelegator{ 56 | ResponseWriter: w, 57 | } 58 | return &d 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/http/request/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Request Builder 2 | 3 | This package is an HTTP request builder for go 4 | 5 | ## Get Request 6 | 7 | ```go 8 | import "github.com/albertwidi/internal/pkg/http/request" 9 | 10 | func main() { 11 | // req is a *http.Request 12 | req, err := request.New(context.Background()). 13 | Get("https://google.com"). 14 | Compile() 15 | if err != nil { 16 | // do something with error 17 | } 18 | ``` 19 | 20 | ### Get Request With Query 21 | 22 | ```go 23 | import "github.com/albertwidi/internal/pkg/http/request" 24 | 25 | func main() { 26 | // req is a *http.Request 27 | req, err := request.New(context.Background()). 28 | Get("https://google.com"). 29 | Query("key", "value"). 30 | Compile() 31 | if err != nil { 32 | // do something with error 33 | } 34 | ``` 35 | 36 | `Query` receive variadic parameters with type `string` 37 | 38 | ## Post Request 39 | 40 | ```go 41 | import "github.com/albertwidi/internal/pkg/http/request" 42 | 43 | func main() { 44 | // req is a *http.Request 45 | req, err := request.New(context.Background()). 46 | Post("https://google.com"). 47 | Compile() 48 | if err != nil { 49 | // do something with error 50 | } 51 | ``` 52 | 53 | ### Post Form 54 | 55 | ```go 56 | import "github.com/albertwidi/internal/pkg/http/request" 57 | 58 | func main() { 59 | // req is a *http.Request 60 | req, err := request.New(context.Background()). 61 | Post("https://google.com"). 62 | PostForm("key", "value") 63 | Compile() 64 | if err != nil { 65 | // do something with error 66 | } 67 | ``` 68 | 69 | `PostForm` receive variadic parameters with type `string` 70 | 71 | ## Request Body 72 | 73 | ### JSON 74 | 75 | ```go 76 | import "github.com/albertwidi/internal/pkg/http/request" 77 | 78 | func main() { 79 | s := struct { 80 | Asd string `json:"asd"` 81 | Jkl string `json:"jkl"` 82 | }{} 83 | 84 | // req is a *http.Request 85 | req, err := request.New(context.Background()). 86 | Post("https://google.com"). 87 | BodyJSON(s). 88 | Compile() 89 | if err != nil { 90 | // do something with error 91 | } 92 | ``` 93 | 94 | ### Raw/io.Reader 95 | 96 | ```go 97 | func main() { 98 | s := struct { 99 | Asd string `json:"asd"` 100 | Jkl string `json:"jkl"` 101 | }{} 102 | 103 | // req is a *http.Request 104 | req, err := request.New(context.Background()). 105 | Post("https://google.com"). 106 | BodyJSON(s). 107 | Compile() 108 | if err != nil { 109 | // do something with error 110 | } 111 | ``` 112 | 113 | ## Version Selection Header 114 | 115 | To be added -------------------------------------------------------------------------------- /internal/pkg/http/request/header.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "net/http" 4 | 5 | // hTTPHeader for http request 6 | type hTTPHeader struct { 7 | headers []string 8 | HTTPheader http.Header 9 | } 10 | 11 | // Header function 12 | func Header(kv ...string) *hTTPHeader { 13 | return &hTTPHeader{} 14 | } 15 | 16 | // Headers return http.Header 17 | func (h *hTTPHeader) Headers() http.Header { 18 | // process passed header 19 | for idx := range h.headers { 20 | if idx > 0 { 21 | idx++ 22 | if idx == len(h.headers)-1 { 23 | break 24 | } 25 | } 26 | h.HTTPheader.Add(h.headers[idx], h.headers[idx+1]) 27 | } 28 | return h.HTTPheader 29 | } 30 | 31 | type hTTPContentType struct { 32 | header *hTTPHeader 33 | key string 34 | } 35 | 36 | // ContentType for requesting http header content-type 37 | func (h *hTTPHeader) ContentType() *hTTPContentType { 38 | return &hTTPContentType{key: "Content-Type"} 39 | } 40 | 41 | // ApplicationFormWWWURLEncoded return x-www-form-urlencoded for http header 42 | func (ct *hTTPContentType) ApplicationFormWWWURLEncoded() *hTTPHeader { 43 | ct.header.HTTPheader.Add(ct.key, "application/x-www-form-urlencoded") 44 | return ct.header 45 | } 46 | 47 | // ApplicationJSON return content type application json for http header 48 | func (ct *hTTPContentType) ApplicationJSON() *hTTPHeader { 49 | ct.header.HTTPheader.Add(ct.key, "application/json") 50 | return ct.header 51 | } 52 | -------------------------------------------------------------------------------- /internal/pkg/http/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/albertwidi/go-project-example/internal/xerrors" 8 | ) 9 | 10 | // Status of json response 11 | type Status string 12 | 13 | // list of status for http response 14 | const ( 15 | StatusOK Status = "OK" 16 | StatusRetry Status = "RETRY" 17 | StatusBadRequest Status = "BAD_REQUEST" 18 | StatusNotFound Status = "NOT_FOUND" 19 | StatusUnauthorized Status = "UNAUTHORIZED" 20 | StatusInternalError Status = "INTERNAL_ERROR" 21 | ) 22 | 23 | // JSONResponse struct for http json response 24 | type JSONResponse struct { 25 | writer http.ResponseWriter 26 | xerr *xerrors.Errors 27 | headerWritten bool 28 | 29 | // response part 30 | ResponseStatus Status `json:"status"` 31 | ResponseData interface{} `json:"data"` 32 | ResponseRetry *JSONRetryResponse `json:"retry,omitempty"` 33 | ResponseError *JSONError `json:"error,omitempty"` 34 | } 35 | 36 | // JSONRetryResponse struct for retry field in json response 37 | type JSONRetryResponse struct { 38 | RetryMin int `json:"retry_min"` 39 | RetryMax int `json:"retry_max"` 40 | } 41 | 42 | // JSONError struct for error field in json response 43 | type JSONError struct { 44 | Title string `json:"title"` 45 | Message string `json:"message"` 46 | Detail string `json:"detail"` 47 | Errors []string `json:"errors"` 48 | } 49 | 50 | // JSON create a new JSON response 51 | func JSON(w http.ResponseWriter) *JSONResponse { 52 | resp := JSONResponse{ 53 | writer: w, 54 | } 55 | return &resp 56 | } 57 | 58 | // SetHeader used to set header in http.ResponseWriter of JSONResponse 59 | func (jresp *JSONResponse) SetHeader(key, value string) { 60 | jresp.writer.Header().Set(key, value) 61 | } 62 | 63 | // Data for set data to json response 64 | func (jresp *JSONResponse) Data(data interface{}) *JSONResponse { 65 | jresp.ResponseData = data 66 | return jresp 67 | } 68 | 69 | // Error set error to json response 70 | // only use error when the type of error is *xerrors.Error 71 | func (jresp *JSONResponse) Error(err error, errResp *JSONError) *JSONResponse { 72 | xerr, ok := err.(*xerrors.Errors) 73 | if !ok { 74 | return jresp 75 | } 76 | jresp.xerr = xerr 77 | jresp.ResponseError = errResp 78 | return jresp 79 | } 80 | 81 | // WriteHeader set the header of JSONResponse writer 82 | func (jresp *JSONResponse) WriteHeader(statusCode int) *JSONResponse { 83 | if jresp.headerWritten { 84 | return jresp 85 | } 86 | jresp.writer.WriteHeader(statusCode) 87 | jresp.headerWritten = true 88 | return jresp 89 | } 90 | 91 | // Write json response 92 | func (jresp *JSONResponse) Write() (int, error) { 93 | jresp.writer.Header().Set("Content-Type", "application/json") 94 | // process the error internals 95 | if jresp.xerr != nil { 96 | kind := jresp.xerr.Kind() 97 | switch kind { 98 | case xerrors.KindOK: 99 | jresp.ResponseStatus = StatusOK 100 | jresp.WriteHeader(http.StatusOK) 101 | 102 | case xerrors.KindNotFound: 103 | jresp.ResponseStatus = StatusNotFound 104 | jresp.WriteHeader(http.StatusNotFound) 105 | 106 | case xerrors.KindBadRequest: 107 | jresp.ResponseStatus = StatusBadRequest 108 | jresp.writer.WriteHeader(http.StatusBadRequest) 109 | 110 | case xerrors.KindUnauthorized: 111 | jresp.ResponseStatus = StatusUnauthorized 112 | jresp.WriteHeader(http.StatusUnauthorized) 113 | 114 | case xerrors.KindInternalError: 115 | jresp.ResponseStatus = StatusInternalError 116 | jresp.WriteHeader(http.StatusInternalServerError) 117 | } 118 | } 119 | 120 | out, err := json.Marshal(jresp) 121 | if err != nil { 122 | return 0, err 123 | } 124 | return jresp.writer.Write(out) 125 | } 126 | -------------------------------------------------------------------------------- /internal/pkg/http/response/response_test.go: -------------------------------------------------------------------------------- 1 | package response_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/albertwidi/go-project-example/internal/pkg/http/response" 9 | "github.com/albertwidi/go-project-example/internal/xerrors" 10 | ) 11 | 12 | func kindToStatusCode(err *xerrors.Errors) int { 13 | switch err.Kind() { 14 | case xerrors.KindOK: 15 | return http.StatusOK 16 | case xerrors.KindBadRequest: 17 | return http.StatusBadRequest 18 | case xerrors.KindNotFound: 19 | return http.StatusNotFound 20 | case xerrors.KindUnauthorized: 21 | return http.StatusUnauthorized 22 | case xerrors.KindInternalError: 23 | return http.StatusInternalServerError 24 | } 25 | return 0 26 | } 27 | 28 | func TestWrite(t *testing.T) { 29 | cases := []struct { 30 | Name string 31 | Headers map[string]string 32 | HTTPStatus int 33 | XErrors error 34 | }{ 35 | { 36 | Name: "Test Status", 37 | HTTPStatus: http.StatusOK, 38 | }, 39 | { 40 | Name: "Test XErrors Kind", 41 | XErrors: xerrors.New("bad request", xerrors.KindBadRequest), 42 | }, 43 | { 44 | Name: "Test XErrors Kind with Override HTTP Status", 45 | HTTPStatus: http.StatusOK, 46 | XErrors: xerrors.New("bad request", xerrors.KindBadRequest), 47 | }, 48 | { 49 | Name: "Test Headers", 50 | Headers: map[string]string{"asd": "jkl", "sdf": "hjk"}, 51 | HTTPStatus: http.StatusOK, 52 | }, 53 | } 54 | 55 | for _, c := range cases { 56 | t.Logf("test number: %s", c.Name) 57 | handler := func(w http.ResponseWriter, r *http.Request) { 58 | jsonresp := response.JSON(w) 59 | for k, v := range c.Headers { 60 | jsonresp.SetHeader(k, v) 61 | } 62 | if c.XErrors == nil { 63 | jsonresp.WriteHeader(c.HTTPStatus) 64 | } else { 65 | jsonresp.Error(c.XErrors, nil) 66 | } 67 | jsonresp.Write() 68 | } 69 | 70 | req := httptest.NewRequest("GET", "http://example.com", nil) 71 | w := httptest.NewRecorder() 72 | handler(w, req) 73 | 74 | resp := w.Result() 75 | statusCode := c.HTTPStatus 76 | if c.XErrors != nil { 77 | // always expect *xerrors.Errors 78 | statusCode = kindToStatusCode(c.XErrors.(*xerrors.Errors)) 79 | } 80 | // check status code 81 | if statusCode != resp.StatusCode { 82 | t.Errorf("invalid http status, expect %d but got %d", c.HTTPStatus, resp.StatusCode) 83 | return 84 | } 85 | // check header 86 | for key, val := range c.Headers { 87 | hval := resp.Header.Get(key) 88 | if hval != val { 89 | t.Errorf("invalid header value, expect %s but got %s", val, hval) 90 | return 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/pkg/log/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type ( 11 | // KV is a type for logging with more information 12 | // this used by with function 13 | KV map[string]interface{} 14 | 15 | // Logger interface 16 | Logger interface { 17 | // this method is not concurrently safe to acess 18 | // preferable to create a new logger instead 19 | SetConfig(config *Config) error 20 | SetLevel(level Level) error 21 | Debug(args ...interface{}) 22 | Debugf(format string, args ...interface{}) 23 | Debugw(msg string, KV KV) 24 | Info(args ...interface{}) 25 | Infof(format string, args ...interface{}) 26 | Infow(msg string, KV KV) 27 | Warn(args ...interface{}) 28 | Warnf(format string, args ...interface{}) 29 | Warnw(msg string, KV KV) 30 | Error(args ...interface{}) 31 | Errorf(format string, args ...interface{}) 32 | Errorw(msg string, KV KV) 33 | Fatal(args ...interface{}) 34 | Fatalf(format string, args ...interface{}) 35 | Fatalw(msg string, KV KV) 36 | } 37 | 38 | // Level of log 39 | Level int 40 | 41 | // Config of logger 42 | Config struct { 43 | Level Level 44 | LogFile string 45 | TimeFormat string 46 | Caller bool 47 | UseColor bool 48 | UseJSON bool 49 | } 50 | ) 51 | 52 | // list of log level 53 | const ( 54 | DebugLevel Level = iota 55 | InfoLevel 56 | WarnLevel 57 | ErrorLevel 58 | FatalLevel 59 | ) 60 | 61 | // Log level 62 | const ( 63 | DebugLevelString = "debug" 64 | InfoLevelString = "info" 65 | WarnLevelString = "warn" 66 | ErrorLevelString = "error" 67 | FatalLevelString = "fatal" 68 | ) 69 | 70 | // DefaultTimeFormat of logger 71 | const DefaultTimeFormat = time.RFC3339 72 | 73 | // StringToLevel to set string to level 74 | func StringToLevel(level string) Level { 75 | switch strings.ToLower(level) { 76 | case DebugLevelString: 77 | return DebugLevel 78 | case InfoLevelString: 79 | return InfoLevel 80 | case WarnLevelString: 81 | return WarnLevel 82 | case ErrorLevelString: 83 | return ErrorLevel 84 | case FatalLevelString: 85 | return FatalLevel 86 | default: 87 | // TODO: make this more informative when happened 88 | return InfoLevel 89 | } 90 | } 91 | 92 | // LevelToString convert log level to readable string 93 | func LevelToString(l Level) string { 94 | switch l { 95 | case DebugLevel: 96 | return DebugLevelString 97 | case InfoLevel: 98 | return InfoLevelString 99 | case WarnLevel: 100 | return WarnLevelString 101 | case ErrorLevel: 102 | return ErrorLevelString 103 | case FatalLevel: 104 | return FatalLevelString 105 | default: 106 | return InfoLevelString 107 | } 108 | } 109 | 110 | // CreateLogFile create a file and return io.Writer for file manipulation 111 | func CreateLogFile(filename string) (*os.File, error) { 112 | err := os.MkdirAll(filepath.Dir(filename), 0744) 113 | if err != nil && err != os.ErrExist { 114 | return nil, err 115 | } 116 | file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return file, nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/pkg/log/logger/std/README.md: -------------------------------------------------------------------------------- 1 | # Standard Go Logger 2 | 3 | Simple wrapper for wrapper standard go logger implementation 4 | -------------------------------------------------------------------------------- /internal/pkg/nsq/README.md: -------------------------------------------------------------------------------- 1 | # NSQ 2 | 3 | Wrapper of nsqio library 4 | 5 | Please note that this library only wrapped the existing consumers from `nsq/nsqio` backend and mapped them. The `nsq.Handler` also overrided with `HandlerFunc` in this library. 6 | 7 | ## Design 8 | 9 | The library flow control is based on buffered channel for each `topic` and `channel`. This means every consumer for different `topic` and `channel` might has different size of buffered channel and number of concurrency. 10 | 11 | The worker model will replace `ConcurrenHandler` for handling multiple message concurrently. This is because the library want to control the flow of the message by using buffered channel as the main communication channel for worker. 12 | 13 | ![nsq throttling design](../../../docs/images/nsq_throttle_design.png) 14 | 15 | ### Throttling 16 | 17 | By design, the `handler` that registered by this library is not directly exposed to the `consumer handler`. This means the `handler` not directly asking for message from `nsq`. 18 | 19 | The message is being sent into the `handler` from a go channel that is dedicated for specific `topic` and `channel`, and the message can be consumed from that go channel only. By using this mechanism, the rate of message consumed by the `concurrent handler` can be controlled when something wrong happened. 20 | 21 | 22 | **Message Retrieval Throttling** 23 | 24 | This throttling is on by default. 25 | 26 | The message retrieval is throttled when the number of message in the `channel` is more than half of its size. 27 | 28 | For example, if the length of buffer is 10, and the message already exceeding 5. The consumer will pause the message consumption until the number of message in the buffer is going back to less than half of the buffer. 29 | 30 | **Message Processing Throttling** 31 | 32 | This throttling can be enabled by using `Throttling` middleware 33 | 34 | The message processing is throttled when the number of message in the `channel` is mor ethan half of its size. 35 | 36 | For example if the length of buffer is 10, and the message already exceeding 5. The consumer will slow down the message processing, this throttling is being handled by the `Throttling` middleware in this library. If the throttle middleware is set, then the library will seek `throttled` status in the message. 37 | 38 | ## How To Use The Library 39 | 40 | To use this library, the `consumer` must be created using `nsq/nsqio`. 41 | 42 | ## TODO 43 | 44 | - DNS: make it possible to specify a single addresss with host or single/multiple address with IP. If a single host is given, then resolve to host. -------------------------------------------------------------------------------- /internal/pkg/nsq/fakensq/README.md: -------------------------------------------------------------------------------- 1 | # Fake NSQ 2 | 3 | Fake NSQ Consumer and Producer for `backend/internal/pkg/nsq` 4 | 5 | Built to test the correctness of the nsqio wrapper 6 | 7 | Limitations: 8 | 9 | - Consumer must registered first before publishing message. 10 | - Do not expecting message to be stored, all message directly consumed. 11 | - Message published before any active consumer will be lost. 12 | - Message requeue not working 13 | - Message is always finished -------------------------------------------------------------------------------- /internal/pkg/nsq/middleware.go: -------------------------------------------------------------------------------- 1 | package nsq 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Metrics middleware for nsq 9 | // metrics that might be missleading: 10 | // - throttled 11 | // - message_in_buffer 12 | // The metrics might be missleading because the message is not processed in ordered manner. 13 | func Metrics(handler HandlerFunc) HandlerFunc { 14 | return func(ctx context.Context, message *Message) error { 15 | t := time.Now() 16 | e := "0" 17 | err := handler(ctx, message) 18 | if err != nil { 19 | e = "1" 20 | } 21 | _nsqHandleDurationHist.WithLabelValues(message.Topic, message.Channel).Observe(float64(time.Now().Sub(t).Milliseconds())) 22 | _nsqHandleCount.WithLabelValues(message.Topic, message.Channel, e).Add(1) 23 | _nsqWorkerCurrentGauge.WithLabelValues(message.Topic, message.Channel).Set(float64(message.Info.WorkerCurrent)) 24 | _nsqThrottleGauge.WithLabelValues(message.Topic, message.Channel).Set(float64(message.Info.Throttled)) 25 | _nsqMessageInBuffGauge.WithLabelValues(message.Topic, message.Channel).Set(float64(message.Info.MessageInBuffer)) 26 | return err 27 | } 28 | } 29 | 30 | // ThrottleMiddleware implement MiddlewareFunc 31 | type ThrottleMiddleware struct { 32 | // TimeDelay means the duration of time to pause message consumption 33 | TimeDelay time.Duration 34 | } 35 | 36 | // Throttle middleware for nsq. 37 | // This middleware check whether there is some information about throttling in the message. 38 | func (tm *ThrottleMiddleware) Throttle(handler HandlerFunc) HandlerFunc { 39 | return func(ctx context.Context, message *Message) error { 40 | // this means the worker is being throttled 41 | if message.Info.ThrottleFlag == 1 { 42 | time.Sleep(tm.TimeDelay) 43 | // set the status to be throttled because the middleware is active 44 | // this is needed for metrics 45 | message.Info.Throttled = 1 46 | } 47 | return handler(ctx, message) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/README.md: -------------------------------------------------------------------------------- 1 | # Objectstorage 2 | 3 | Wrapper of go cloud blob library 4 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/albertwidi/go-project-example/internal/pkg/objectstorage" 9 | "gocloud.dev/blob" 10 | "gocloud.dev/blob/fileblob" 11 | ) 12 | 13 | // Local storage struct 14 | type Local struct { 15 | path string 16 | localBlobBucket *blob.Bucket 17 | options *Options 18 | } 19 | 20 | // Options of local storage 21 | type Options struct { 22 | // delete bucket when closing the storage 23 | DeleteOnClose bool 24 | } 25 | 26 | // New local storage 27 | func New(ctx context.Context, bucketpath string, opts *Options) (*Local, error) { 28 | err := os.MkdirAll(filepath.Dir(bucketpath), 0744) 29 | if err != nil && err != os.ErrExist { 30 | return nil, err 31 | } 32 | 33 | // if !path.IsAbs(bucketpath) { 34 | // // get current working directory for full path 35 | // currentDir, err := os.Getwd() 36 | // if err != nil { 37 | // return nil, err 38 | // } 39 | 40 | // bucketpath = path.Join(currentDir, bucketpath) 41 | // } 42 | 43 | b, err := fileblob.OpenBucket(bucketpath, nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | l := Local{ 49 | path: bucketpath, 50 | localBlobBucket: b, 51 | options: opts, 52 | } 53 | return &l, nil 54 | } 55 | 56 | // Bucket of local storage 57 | func (l *Local) Bucket() *blob.Bucket { 58 | return l.localBlobBucket 59 | } 60 | 61 | // Name return the name of provider 62 | func (l *Local) Name() string { 63 | return objectstorage.StorageLocal 64 | } 65 | 66 | // BucketName return name of path in local 67 | func (l *Local) BucketName() string { 68 | return l.path 69 | } 70 | 71 | // BucketURL return the url of blob storage bucket 72 | func (l *Local) BucketURL() string { 73 | // return l.path 74 | return "" 75 | } 76 | 77 | // Close will close the local bucket 78 | func (l *Local) Close() error { 79 | return l.localBlobBucket.Close() 80 | } 81 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/local/local_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | // func TestUpload(t *testing.T) { 4 | // l, err := New(NewConfig(".")) 5 | // if err != nil { 6 | // t.Error(err) 7 | // return 8 | // } 9 | 10 | // from := "./testfile" 11 | // to := "copy1.txt" 12 | // if err = l.Upload(context.TODO(), from, to); err != nil { 13 | // t.Error(err) 14 | // return 15 | // } 16 | 17 | // if err := os.Remove(l.bucketPath(to)); err != nil { 18 | // t.Error(err) 19 | // return 20 | // } 21 | // } 22 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/local/testfile/copy1.txt: -------------------------------------------------------------------------------- 1 | this is a text for copy -------------------------------------------------------------------------------- /internal/pkg/objectstorage/s3/s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import "testing" 4 | 5 | func TestUpload(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/testbucket/haloha.txt.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"S9NrXl6U9mxlgVVlfQlV9A=="} 2 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/testbucket/objectstorage.go.attrs: -------------------------------------------------------------------------------- 1 | {"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"vSmA/ibXbQfJm3BxsJOaBQ=="} 2 | -------------------------------------------------------------------------------- /internal/pkg/objectstorage/testbucket/testdownload.txt: -------------------------------------------------------------------------------- 1 | haloha -------------------------------------------------------------------------------- /internal/pkg/randgen/randgen.go: -------------------------------------------------------------------------------- 1 | package randgen 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // Generator for number generation in go is not concurrently safe (for new source). 8 | // In order to generate random number concurrently, a number of workers to generate the random numbers are needed. 9 | // each worker will generate a random number and push it into a buffered channel 10 | // Reference: https://golang.org/pkg/math/rand/ 11 | type Generator struct { 12 | randnum chan int 13 | woker []RandWorker 14 | } 15 | 16 | // RandWorker struct 17 | type RandWorker struct { 18 | randchan chan int 19 | randgen *rand.Rand 20 | seed int64 21 | min int 22 | max int 23 | stopped bool 24 | } 25 | 26 | // Work via goroutines 27 | // the worker will run forever when program runs 28 | // but will get destroyed if the program exit 29 | func (rw *RandWorker) Work() { 30 | go func() { 31 | for { 32 | // the worker will not stopped immediately 33 | // because it will wait the buffered channel to have space 34 | // closing channel might help this case 35 | if rw.stopped { 36 | return 37 | } 38 | randnum := rw.randgen.Intn(rw.max - rw.min) 39 | randnum = rw.min + randnum 40 | rw.randchan <- randnum 41 | } 42 | }() 43 | } 44 | 45 | // New random number generator 46 | func New(workernumber, min, max int, seed int64) *Generator { 47 | randnumchan := make(chan int, workernumber*10) 48 | gen := Generator{ 49 | randnum: randnumchan, 50 | } 51 | 52 | for i := 0; i < workernumber; i++ { 53 | r := rand.New(rand.NewSource(seed)) 54 | w := RandWorker{ 55 | randchan: randnumchan, 56 | randgen: r, 57 | seed: seed, 58 | min: min, 59 | max: max, 60 | } 61 | w.Work() 62 | gen.woker = append(gen.woker, w) 63 | } 64 | return &gen 65 | } 66 | 67 | // Generate a new random number 68 | func (gen *Generator) Generate() int { 69 | return <-gen.randnum 70 | } 71 | 72 | // Stop generator's worker 73 | func (gen *Generator) Stop() { 74 | for i := range gen.woker { 75 | gen.woker[i].stopped = true 76 | } 77 | // close the generator channel 78 | close(gen.randnum) 79 | } 80 | -------------------------------------------------------------------------------- /internal/pkg/redis/redigo/list.go: -------------------------------------------------------------------------------- 1 | package redigo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/albertwidi/go-project-example/internal/pkg/redis" 7 | redigo "github.com/gomodule/redigo/redis" 8 | ) 9 | 10 | // LLen get the length of the list 11 | func (rdg *Redigo) LLen(ctx context.Context, key string) (int, error) { 12 | result, err := redigo.Int(rdg.do(ctx, redis.CommandLLen, key)) 13 | if err != nil && !rdg.IsErrNil(err) { 14 | return 0, err 15 | } 16 | return result, err 17 | } 18 | 19 | // LIndex to get value from a certain list index 20 | func (rdg *Redigo) LIndex(ctx context.Context, key string, index int) (string, error) { 21 | result, err := redigo.String(rdg.do(ctx, redis.CommandLIndex, key, index)) 22 | if err != nil && !rdg.IsErrNil(err) { 23 | return "", err 24 | } 25 | return result, err 26 | } 27 | 28 | // LSet to set value to some index 29 | func (rdg *Redigo) LSet(ctx context.Context, key, value string, index int) (int, error) { 30 | result, err := redigo.Int(rdg.do(ctx, redis.CommandLSET, index, value)) 31 | if err != nil && !rdg.IsErrNil(err) { 32 | return 0, err 33 | } 34 | return result, err 35 | } 36 | 37 | // LPush prepend values to the list 38 | func (rdg *Redigo) LPush(ctx context.Context, key string, values ...interface{}) (int, error) { 39 | args := make([]interface{}, len(values)+1) 40 | args[0] = key 41 | for i, value := range values { 42 | args[i+1] = value 43 | } 44 | 45 | result, err := redigo.Int(rdg.do(ctx, redis.CommandLPush, args...)) 46 | if err != nil && !rdg.IsErrNil(err) { 47 | return 0, err 48 | } 49 | return result, err 50 | } 51 | 52 | // LPushX prepend values to the list 53 | func (rdg *Redigo) LPushX(ctx context.Context, key string, values ...interface{}) (int, error) { 54 | args := make([]interface{}, len(values)+1) 55 | args[0] = key 56 | for i, value := range values { 57 | args[i+1] = value 58 | } 59 | 60 | result, err := redigo.Int(rdg.do(ctx, redis.CommandLPushX, args...)) 61 | if err != nil && !rdg.IsErrNil(err) { 62 | return 0, err 63 | } 64 | return result, err 65 | } 66 | 67 | // LPop removes and get the first element in the list 68 | func (rdg *Redigo) LPop(ctx context.Context, key string) (string, error) { 69 | result, err := redigo.String(rdg.do(ctx, redis.CommandLPop, key)) 70 | if err != nil && !rdg.IsErrNil(err) { 71 | return "", err 72 | } 73 | return result, err 74 | } 75 | 76 | // LRem command 77 | func (rdg *Redigo) LRem(ctx context.Context, key, value string, count int) (int, error) { 78 | result, err := redigo.Int(rdg.do(ctx, redis.CommandLRem, key, count, value)) 79 | if err != nil && !rdg.IsErrNil(err) { 80 | return 0, err 81 | } 82 | return result, err 83 | } 84 | 85 | // LTrim command 86 | func (rdg *Redigo) LTrim(ctx context.Context, key string, start, stop int) (string, error) { 87 | result, err := redigo.String(rdg.do(ctx, redis.CommandLTrim, key, start, stop)) 88 | if err != nil && !rdg.IsErrNil(err) { 89 | return "", err 90 | } 91 | return result, err 92 | } 93 | -------------------------------------------------------------------------------- /internal/pkg/redis/redigo/pipeline.go: -------------------------------------------------------------------------------- 1 | package redigo 2 | 3 | // implement redis pipelining using redigo conn.Send 4 | -------------------------------------------------------------------------------- /internal/pkg/redis/redigo/redigo.go: -------------------------------------------------------------------------------- 1 | package redigo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/albertwidi/go-project-example/internal/pkg/redis" 7 | 8 | redigo "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | // Redigo redis 12 | type Redigo struct { 13 | pool *redigo.Pool 14 | } 15 | 16 | // Config of connection 17 | type Config struct { 18 | MaxActive int 19 | MaxIdle int 20 | Timeout int 21 | } 22 | 23 | // New redis connection using redigo library 24 | func New(ctx context.Context, address string, config *Config) (*Redigo, error) { 25 | pool := &redigo.Pool{ 26 | Dial: func() (redigo.Conn, error) { 27 | return redigo.Dial("tcp", address) 28 | }, 29 | } 30 | 31 | r := Redigo{ 32 | pool: pool, 33 | } 34 | return &r, nil 35 | } 36 | 37 | // getConn return the connection of redigo 38 | func (rdg *Redigo) getConn(ctx context.Context) (redigo.Conn, error) { 39 | return rdg.pool.GetContext(ctx) 40 | } 41 | 42 | func (rdg *Redigo) do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error) { 43 | conn, err := rdg.getConn(ctx) 44 | if err != nil { 45 | return nil, err 46 | } 47 | defer conn.Close() 48 | 49 | resp, err := conn.Do(cmd, args...) 50 | return resp, err 51 | } 52 | 53 | // Ping the redis 54 | func (rdg *Redigo) Ping(ctx context.Context) (string, error) { 55 | val, err := rdg.do(ctx, redis.CommandPing) 56 | return redigo.String(val, err) 57 | } 58 | 59 | // Close all redis connection 60 | func (rdg *Redigo) Close() error { 61 | return rdg.pool.Close() 62 | } 63 | 64 | // IsErrNil return true if error is nil 65 | func (rdg *Redigo) IsErrNil(err error) bool { 66 | if !errors.Is(err, redigo.ErrNil) { 67 | return false 68 | } 69 | return true 70 | } 71 | 72 | // IsResponseOK return true if result value of command is ok 73 | func (rdg *Redigo) IsResponseOK(result string) bool { 74 | if result != "OK" { 75 | return false 76 | } 77 | return true 78 | } 79 | -------------------------------------------------------------------------------- /internal/pkg/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | //go:generate mockgen -source=redis.go -destination=mock/redis_mock.go -package redismock 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | ) 9 | 10 | // error list 11 | var ( 12 | ErrResponseNotOK = errors.New("redis: response is not ok") 13 | ) 14 | 15 | // Redis interface 16 | type Redis interface { 17 | Close() error 18 | IsErrNil(err error) bool 19 | IsResponseOK(result string) bool 20 | Set(ctx context.Context, key string, value interface{}) (string, error) 21 | SetNX(ctx context.Context, key string, value interface{}, expire int) (int, error) 22 | SetEX(ctx context.Context, key string, value interface{}, expire int) (string, error) 23 | Get(ctx context.Context, key string) (string, error) 24 | Delete(ctx context.Context, key string) (int, error) 25 | Increment(ctx context.Context, key string) (int, error) 26 | IncrementBy(ctx context.Context, key string, amount int) (int, error) 27 | Expire(ctx context.Context, key string, duration int) (int, error) 28 | MSet(ctx context.Context, pairs ...interface{}) (string, error) 29 | MGet(ctx context.Context, keys ...string) ([]string, error) 30 | HSet(ctx context.Context, key, field string, value interface{}) (int, error) 31 | HSetEX(ctx context.Context, key, field string, value interface{}, expire int) (int, error) 32 | HGet(ctx context.Context, key, field string) (string, error) 33 | HGetAll(ctx context.Context, key string) (map[string]string, error) 34 | HMSet(ctx context.Context, key string, kv map[string]interface{}) (string, error) 35 | HMGet(ctx context.Context, key string, fields ...string) ([]string, error) 36 | HDel(ctx context.Context, key string, fields ...string) (int, error) 37 | LLen(ctx context.Context, key string) (int, error) 38 | LIndex(ctx context.Context, key string, index int) (string, error) 39 | LSet(ctx context.Context, key, value string, index int) (int, error) 40 | LPush(ctx context.Context, key string, values ...interface{}) (int, error) 41 | LPushX(ctx context.Context, key string, values ...interface{}) (int, error) 42 | LPop(ctx context.Context, key string) (string, error) 43 | LRem(ctx context.Context, key, value string, count int) (int, error) 44 | LTrim(ctd context.Context, key string, start, stop int) (string, error) 45 | } 46 | 47 | // list of redis command 48 | const ( 49 | CommandPing = "PING" 50 | CommandExpire = "EXPIRE" 51 | CommandSet = "SET" 52 | CommandGet = "GET" 53 | CommandDelete = "DEL" 54 | CommandIncrement = "INCR" 55 | CommandIncrementBy = "INCRBY" 56 | CommandSetNX = "SETNX" 57 | CommandSetEX = "SETEX" 58 | CommandMSet = "MSET" 59 | CommandMGet = "MGET" 60 | CommandHSet = "HSET" 61 | CommandHGet = "HGET" 62 | CommandHGetAll = "HGETALL" 63 | CommandHMSet = "HMSET" 64 | CommandHMGet = "HMGET" 65 | CommandHDel = "HDEL" 66 | CommandLLen = "LLEN" 67 | CommandLIndex = "LINDEX" 68 | CommandLSET = "LSET" 69 | CommandLPush = "LPUSH" 70 | CommandLPushX = "LPUSHX" 71 | CommandLPop = "LPOP" 72 | CommandLRem = "LREM" 73 | CommandLTrim = "LTRIM" 74 | ) 75 | -------------------------------------------------------------------------------- /internal/pkg/router/README.md: -------------------------------------------------------------------------------- 1 | # Router 2 | 3 | Is a wrapper for Gorilla mux 4 | -------------------------------------------------------------------------------- /internal/pkg/sqldb/sqldb_context.go: -------------------------------------------------------------------------------- 1 | package sqldb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | // GetContext function 9 | func (db *DB) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { 10 | return db.follower.GetContext(ctx, dest, query, args...) 11 | } 12 | 13 | // SelectContext fuction 14 | func (db *DB) SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error { 15 | return db.follower.SelectContext(ctx, dest, query, args...) 16 | } 17 | 18 | // QueryContext function 19 | func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { 20 | return db.follower.QueryContext(ctx, query, args...) 21 | } 22 | 23 | // QueryRowContext function 24 | func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { 25 | return db.follower.QueryRowContext(ctx, query, args...) 26 | } 27 | 28 | // ExecContext function 29 | func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { 30 | return db.leader.ExecContext(ctx, query, args...) 31 | } 32 | 33 | // NamedExecContext function 34 | func (db *DB) NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) { 35 | return db.leader.NamedExecContext(ctx, query, arg) 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/tempe/tempe.go: -------------------------------------------------------------------------------- 1 | // tempe is template replacer for replacing regex matching string 2 | // it has Replacer function to return a KV map for matching string 3 | 4 | package tempe 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "regexp" 10 | ) 11 | 12 | // ReplaceFunc for tempe 13 | type ReplaceFunc func(matches [][]byte) (map[string]string, error) 14 | 15 | // Tempe struct 16 | type Tempe struct { 17 | regex *regexp.Regexp 18 | replcer ReplaceFunc 19 | } 20 | 21 | // EnvVarPattern define environment variable pattern with ${MY_ENV_VAR} 22 | const EnvVarPattern = "\\${[a-zA-Z0-9/-_--]+}" 23 | 24 | // EnvVarReplacerFunc for replacing environment variable with the regex 25 | var EnvVarReplacerFunc = func(matches [][]byte) (map[string]string, error) { 26 | kv := make(map[string]string) 27 | for _, m := range matches { 28 | k := string(m) 29 | v := os.Getenv(k[2 : len(k)-1]) 30 | kv[k] = v 31 | } 32 | return kv, nil 33 | } 34 | 35 | // New tempe object 36 | func New(regex string, replacer ReplaceFunc) (*Tempe, error) { 37 | t := Tempe{ 38 | replcer: replacer, 39 | } 40 | rxp, err := regexp.Compile(regex) 41 | if err != nil { 42 | return nil, err 43 | } 44 | t.regex = rxp 45 | 46 | return &t, nil 47 | } 48 | 49 | // ReplaceBytes the string 50 | func (t *Tempe) ReplaceBytes(in []byte) ([]byte, error) { 51 | matches := t.regex.FindAll(in, -1) 52 | if len(matches) == 0 { 53 | return in, nil 54 | } 55 | 56 | kv, err := t.replcer(matches) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | for k, v := range kv { 62 | in = bytes.Replace(in, []byte(k), []byte(v), -1) 63 | } 64 | return in, nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/pkg/tempe/tempe_test.go: -------------------------------------------------------------------------------- 1 | package tempe 2 | 3 | import "testing" 4 | 5 | // a simple test 6 | func TestReplace(t *testing.T) { 7 | replacefunc := func(matches [][]byte) (map[string]string, error) { 8 | kv := make(map[string]string) 9 | for _, m := range matches { 10 | k := string(m) 11 | v := "asd" 12 | kv[k] = v 13 | } 14 | return kv, nil 15 | } 16 | 17 | s := "${this} is ${a} string" 18 | sbyte := []byte(s) 19 | 20 | te, err := New("\\${[a-zA-Z0-9/-_--]+}", replacefunc) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | out, err := te.ReplaceBytes(sbyte) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | 30 | sout := string(out) 31 | if sout != "asd is asd string" { 32 | t.Error("string unmatch") 33 | return 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/pkg/time/time.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import "time" 4 | 5 | var ( 6 | _defaultLocation *time.Location 7 | ) 8 | 9 | // SetDefaultLocation will set the default location of timeutil library to the defined location 10 | func SetDefaultLocation(location string) error { 11 | var err error 12 | 13 | _defaultLocation, err = time.LoadLocation(location) 14 | return err 15 | } 16 | 17 | // Now return the current time based on _defaultLocation 18 | func Now() time.Time { 19 | return time.Now().In(_defaultLocation) 20 | } 21 | 22 | // Time return the Go time struct 23 | func Time() time.Time { 24 | return time.Time{} 25 | } 26 | -------------------------------------------------------------------------------- /internal/pkg/ulid/ulid.go: -------------------------------------------------------------------------------- 1 | package ulid 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/oklog/ulid" 8 | ) 9 | 10 | // UlidIface interface, best to use instead of Ulid struct 11 | type UlidIface interface { 12 | Ulid() string 13 | } 14 | 15 | // Ulid struct 16 | type Ulid struct { 17 | ulidchan chan string 18 | } 19 | 20 | // Worker of ulid 21 | type Worker struct { 22 | ulidchan chan string 23 | } 24 | 25 | func (w *Worker) Work(source rand.Source) { 26 | r := rand.New(source) 27 | for { 28 | w.ulidchan <- ulid.MustNew(ulid.Now(), r).String() 29 | } 30 | } 31 | 32 | // New ulid 33 | func New(workernumber int) *Ulid { 34 | ch := make(chan string, workernumber*10) 35 | u := Ulid{ 36 | ulidchan: ch, 37 | } 38 | 39 | for i := 0; i < workernumber; i++ { 40 | s := rand.NewSource(time.Now().UnixNano()) 41 | w := Worker{ 42 | ulidchan: ch, 43 | } 44 | go w.Work(s) 45 | } 46 | return &u 47 | } 48 | 49 | // Ulid return ulid string 50 | func (u *Ulid) Ulid() string { 51 | return <-u.ulidchan 52 | } 53 | 54 | // UlidMock for mocking ulid 55 | type UlidMock struct { 56 | ulidID []string 57 | defaultVal string 58 | } 59 | 60 | // NewMock return new mock object 61 | func NewMock(ulid ...string) *UlidMock { 62 | return &UlidMock{ 63 | ulidID: ulid, 64 | defaultVal: "loremipsumdolorsitamet", 65 | } 66 | } 67 | 68 | // DefaultValue return default value of mock 69 | func (u *UlidMock) DefaultValue() string { 70 | return u.defaultVal 71 | } 72 | 73 | // Ulid return mock ulid value 74 | func (u *UlidMock) Ulid() string { 75 | if u == nil { 76 | return "" 77 | } 78 | 79 | if len(u.ulidID) == 0 { 80 | return u.defaultVal 81 | } 82 | 83 | temp := u.ulidID[0] 84 | u.ulidID = u.ulidID[1:] 85 | 86 | return temp 87 | } 88 | -------------------------------------------------------------------------------- /internal/pkg/ulid/ulid_test.go: -------------------------------------------------------------------------------- 1 | package ulid 2 | 3 | import "testing" 4 | 5 | func TestGenUlid(t *testing.T) { 6 | u := New(3) 7 | res := u.Ulid() 8 | if res == "" { 9 | t.Error("result from ulid is empty") 10 | } 11 | } 12 | 13 | func TestMock(t *testing.T) { 14 | ulidTest := "111122223333" 15 | um := NewMock(ulidTest) 16 | 17 | if res := um.Ulid(); res != ulidTest { 18 | t.Error("ulid doesn't match") 19 | } 20 | } 21 | 22 | func TestEmptyMock(t *testing.T) { 23 | um := NewMock() 24 | 25 | if res := um.Ulid(); res != um.DefaultValue() { 26 | t.Error("default ulid doesn't exist") 27 | } 28 | } 29 | 30 | func TestMultipleMock(t *testing.T) { 31 | ulidTest := "111122223333" 32 | ulidTest2 := "111122224444" 33 | 34 | um := NewMock(ulidTest, ulidTest2) 35 | 36 | if res1 := um.Ulid(); res1 != ulidTest { 37 | t.Errorf("expect %v, got %v\n", ulidTest, res1) 38 | } 39 | 40 | if res2 := um.Ulid(); res2 != ulidTest2 { 41 | t.Errorf("expect %v, got %v\n", ulidTest2, res2) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/repository/amenities/amenities.go: -------------------------------------------------------------------------------- 1 | package amenities 2 | 3 | import ( 4 | "context" 5 | "github.com/lib/pq" 6 | "time" 7 | 8 | entity "github.com/albertwidi/go-project-example/internal/entity/amenities" 9 | "github.com/albertwidi/go-project-example/internal/pkg/sqldb" 10 | ) 11 | 12 | // Repository of amenities 13 | type Repository struct { 14 | db *sqldb.DB 15 | } 16 | 17 | // Amenities struct 18 | // @database: project, table: amenities 19 | type Amenities struct { 20 | ID string `db:"id"` 21 | Name string `db:"name"` 22 | Type int `db:"type"` 23 | ImagePath string `db:"image_path"` 24 | CreatedAt time.Time `db:"created_at"` 25 | UpdatedAt pq.NullTime `db:"updated_at"` 26 | IsDeleted bool `db:"is_deleted"` 27 | IsTest bool `db:"is_test"` 28 | } 29 | 30 | // New amenities repo 31 | func New() *Repository { 32 | r := Repository{} 33 | return &r 34 | } 35 | 36 | // Create a new amenities 37 | func (r Repository) Create(ctx context.Context, amenities entity.Amenities) error { 38 | return nil 39 | } 40 | 41 | // Get amenities 42 | func (r Repository) Get(ctx context.Context, amenitiesID ...string) ([]entity.Amenities, error) { 43 | a := []entity.Amenities{} 44 | return a, nil 45 | } 46 | 47 | // Update amenities 48 | func (r Repository) Update(ctx context.Context, amenities entity.Amenities) error { 49 | return nil 50 | } 51 | 52 | // Delete the amenities 53 | func (r Repository) Delete(ctx context.Context, amenitiesID string) error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/repository/image/image.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | imageentity "github.com/albertwidi/go-project-example/internal/entity/image" 9 | "github.com/albertwidi/go-project-example/internal/pkg/redis" 10 | ) 11 | 12 | // Repository of image 13 | type Repository struct { 14 | redis redis.Redis 15 | } 16 | 17 | // New repository for image 18 | func New(redis redis.Redis) *Repository { 19 | r := Repository{ 20 | redis: redis, 21 | } 22 | return &r 23 | } 24 | 25 | func createImageKey(id string) string { 26 | return strings.Join([]string{"image_temp", id}, ":") 27 | } 28 | 29 | // SaveTempPath image path 30 | func (r Repository) SaveTempPath(ctx context.Context, id, originalPath string, expiryTime time.Duration) error { 31 | key := createImageKey(id) 32 | _, err := r.redis.SetEX(ctx, key, originalPath, int(expiryTime.Seconds())) 33 | return err 34 | } 35 | 36 | // GetTempPath will return the original path from a temporary id 37 | func (r Repository) GetTempPath(ctx context.Context, id string) (string, error) { 38 | key := createImageKey(id) 39 | out, err := r.redis.Get(ctx, key) 40 | if err != nil { 41 | if r.redis.IsErrNil(err) { 42 | return "", imageentity.ErrTempPathNotFound 43 | } 44 | return "", err 45 | } 46 | return out, err 47 | } 48 | -------------------------------------------------------------------------------- /internal/repository/image/image_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | redismock "github.com/albertwidi/go-project-example/internal/pkg/redis/mock" 9 | "github.com/albertwidi/go-project-example/internal/repository/image" 10 | "github.com/golang/mock/gomock" 11 | ) 12 | 13 | func TestSaveTempPath(t *testing.T) { 14 | t.Parallel() 15 | 16 | redisMock := redismock.NewMockRedis(gomock.NewController(t)) 17 | redisMock.EXPECT(). 18 | SetEX(context.Background(), gomock.Eq("image_temp:abcd"), gomock.Eq("jklf"), gomock.Eq(int(time.Minute.Seconds()))). 19 | Return("OK", nil) 20 | 21 | repo := image.New(redisMock) 22 | cases := []struct { 23 | key string 24 | value string 25 | expiryTime time.Duration 26 | expectResult string 27 | expectError error 28 | }{ 29 | { 30 | key: "abcd", 31 | value: "jklf", 32 | expiryTime: time.Minute, 33 | expectError: nil, 34 | }, 35 | } 36 | 37 | for _, c := range cases { 38 | err := repo.SaveTempPath(context.Background(), c.key, c.value, c.expiryTime) 39 | if err != c.expectError { 40 | t.Errorf("saveTempPath: expect error %v but got %v", c.expectError, err) 41 | return 42 | } 43 | } 44 | } 45 | 46 | func TestGetTempPath(t *testing.T) { 47 | 48 | } 49 | -------------------------------------------------------------------------------- /internal/repository/property/property.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | import ( 4 | "context" 5 | 6 | entity "github.com/albertwidi/go-project-example/internal/entity/property" 7 | "github.com/albertwidi/go-project-example/internal/pkg/sqldb" 8 | ) 9 | 10 | // Repository of property 11 | type Repository struct { 12 | db *sqldb.DB 13 | } 14 | 15 | // New property repository 16 | func New(db *sqldb.DB) *Repository { 17 | r := Repository{ 18 | db: db, 19 | } 20 | return &r 21 | } 22 | 23 | // Create new property 24 | func (r Repository) Create(ctx context.Context, property entity.Property, detail entity.Detail, addressMap entity.AddressMap, pricings []entity.Pricing) error { 25 | return nil 26 | } 27 | 28 | // Update property 29 | func (r Repository) Update(ctx context.Context) error { 30 | return nil 31 | } 32 | 33 | // UpdateDetail of property 34 | func (r Repository) UpdateDetail(ctx context.Context) error { 35 | return nil 36 | } 37 | 38 | // UpdateAddress update property address 39 | func (r Repository) UpdateAddress(ctx context.Context) error { 40 | return nil 41 | } 42 | 43 | // Delete property 44 | func (r Repository) Delete(ctx context.Context, id string) error { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | // Repositories contains list of repository that can be used 4 | type Repositories struct { 5 | } 6 | 7 | // New repositories 8 | func New() *Repositories { 9 | r := Repositories{} 10 | return &r 11 | } 12 | -------------------------------------------------------------------------------- /internal/repository/secret/secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | secretentity "github.com/albertwidi/go-project-example/internal/entity/secret" 8 | "github.com/albertwidi/go-project-example/internal/pkg/sqldb" 9 | "github.com/lib/pq" 10 | ) 11 | 12 | // Repository of secret 13 | type Repository struct { 14 | db *sqldb.DB 15 | } 16 | 17 | // New secret 18 | func New(db *sqldb.DB) *Repository { 19 | r := Repository{ 20 | db: db, 21 | } 22 | return &r 23 | } 24 | 25 | // Secret of user 26 | type Secret struct { 27 | ID string `db:"id"` 28 | UserID string `db:"user_id"` 29 | SecretKey secretentity.Key `db:"secret_key"` 30 | SecretValue string `db:'secret_value"` 31 | CreatedAt time.Time `db:"created_at"` 32 | CreatedBy int64 `db:"created_by"` 33 | UpdatedAt pq.NullTime `db:"updated_at"` 34 | UpdatedBy int64 `db:"updated_by"` 35 | IsTest bool `db:"is_test"` 36 | } 37 | 38 | const ( 39 | createSecretQuery = ` 40 | INSERT INTO user_secrets( 41 | user_id, 42 | secret_key, 43 | secret_value, 44 | created_at, 45 | created_by, 46 | is_test 47 | ) 48 | VALUES(?, ?, ?, ?, ?, ?) 49 | ` 50 | ) 51 | 52 | // Create secret based on secret data 53 | func (r Repository) Create(ctx context.Context, secret secretentity.Secret) error { 54 | q := r.db.Rebind(createSecretQuery) 55 | 56 | createdAt := time.Now() 57 | _, err := r.db.ExecContext(ctx, q, secret.UserID, secret.SecretKey, secret.SecretValue, secret.CreatedAt, createdAt) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | const ( 66 | getSecretQuery = ` 67 | SELECT id, user_id, secret_key, secret_value, created_at, created_by, updated_at, updated_by, is_test 68 | FROM user_secrets 69 | WHERE user_id = $1 70 | AND secret_key = $2 71 | ` 72 | ) 73 | 74 | // GetSecret for getting a secret 75 | func (r Repository) GetSecret(ctx context.Context, userID int64, secretKey secretentity.Key) (*secretentity.Secret, error) { 76 | s := Secret{} 77 | err := r.db.Get(&s, getSecretQuery, userID, secretKey) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return &secretentity.Secret{ 83 | ID: s.ID, 84 | UserID: s.UserID, 85 | SecretKey: s.SecretKey, 86 | CreatedAt: s.CreatedAt, 87 | CreatedBy: s.CreatedBy, 88 | UpdatedAt: s.UpdatedAt.Time, 89 | UpdatedBy: s.UpdatedBy, 90 | IsTest: s.IsTest, 91 | }, nil 92 | } 93 | 94 | const ( 95 | updateSecretQuery = ` 96 | UPDATE user_secrets 97 | SET secret_value = :secret_value, 98 | updated_at = :updated_at, 99 | updated_by = :updated_by 100 | WHERE id = :id 101 | AND secret_key = :secret_key 102 | ` 103 | ) 104 | 105 | // UpdateSecret for updating current secret 106 | func (r Repository) UpdateSecret(ctx context.Context, secret secretentity.Secret) error { 107 | _, err := r.db.NamedExecContext(ctx, updateSecretQuery, secret) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /internal/repository/session/session.go: -------------------------------------------------------------------------------- 1 | // session is stored in a single hash key inside of redis k/v 2 | // for example we have hash of user:{hashed_value} 3 | // key 'data' is reserved for user data information 4 | // and the other key is {session_id} 5 | 6 | package session 7 | 8 | import "context" 9 | 10 | // Repository struct 11 | type Repository struct { 12 | } 13 | 14 | // New session repository 15 | func New() { 16 | 17 | } 18 | 19 | // Create a new session 20 | func (r *Repository) Create(ctx context.Context) error { 21 | return nil 22 | } 23 | 24 | // SaveUserInfo is a special event 25 | // whether user is a new user login, or user is updating the user information 26 | func (r *Repository) SaveUserInfo(ctx context.Context) error { 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/repository/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | stateentity "github.com/albertwidi/go-project-example/internal/entity/state" 10 | "github.com/albertwidi/go-project-example/internal/pkg/redis" 11 | "github.com/albertwidi/go-project-example/internal/pkg/ulid" 12 | ) 13 | 14 | // Repository struct 15 | type Repository struct { 16 | Redis redis.Redis 17 | // ulid generator 18 | ulidgen *ulid.Ulid 19 | } 20 | 21 | // New repository 22 | func New(redis redis.Redis) *Repository { 23 | r := Repository{ 24 | Redis: redis, 25 | ulidgen: ulid.New(3), 26 | } 27 | 28 | return &r 29 | } 30 | 31 | func formatStateIDKey(id string) string { 32 | return fmt.Sprintf("state:%s", id) 33 | } 34 | 35 | // Save state 36 | func (r *Repository) Save(ctx context.Context, id string, data stateentity.State) error { 37 | stateID := formatStateIDKey(id) 38 | 39 | out, err := json.Marshal(data) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | _, err = r.Redis.SetEX(ctx, stateID, string(out), int(data.ExpiryTime.Seconds())) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // Get return available state 53 | func (r *Repository) Get(ctx context.Context, stateID string) (stateentity.State, error) { 54 | stt := stateentity.State{} 55 | 56 | stateID = formatStateIDKey(stateID) 57 | val, err := r.Redis.Get(stateID) 58 | if err != nil { 59 | if redis.IsErrNil(err) { 60 | return stt, stateentity.ErrStateNotFound 61 | } 62 | 63 | return stt, err 64 | } 65 | 66 | if val == "" { 67 | return stt, stateentity.ErrStateNotFound 68 | } 69 | 70 | err = json.Unmarshal([]byte(val), &stt) 71 | if err != nil { 72 | return stt, err 73 | } 74 | 75 | return stt, nil 76 | } 77 | 78 | // SetExpire to set expire for state 79 | func (r *Repository) SetExpire(ctx context.Context, stateID string, expiryTime time.Duration) error { 80 | // op := xerrors.EOp("stateResource/setExpire") 81 | 82 | stateID = formatStateIDKey(stateID) 83 | _, err := r.Redis.Expire(stateID, int(expiryTime.Seconds())) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return err 89 | } 90 | 91 | // Delete state 92 | func (r *Repository) Delete(ctx context.Context, stateID string) error { 93 | stateID = formatStateIDKey(stateID) 94 | _, err := r.Redis.Delete(stateID) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /internal/repository/user/session.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | -------------------------------------------------------------------------------- /internal/repository/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | 6 | userentity "github.com/albertwidi/go-project-example/internal/entity/user" 7 | "github.com/albertwidi/go-project-example/internal/pkg/redis" 8 | "github.com/albertwidi/go-project-example/internal/pkg/sqldb" 9 | ) 10 | 11 | // Repository of user 12 | type Repository struct { 13 | db *sqldb.DB 14 | redis redis.Redis 15 | } 16 | 17 | // New repository of user 18 | func New(db *sqldb.DB, redis redis.Redis) *Repository { 19 | r := Repository{ 20 | db: db, 21 | redis: redis, 22 | } 23 | return &r 24 | } 25 | 26 | // Create user 27 | func (r *Repository) Create(ctx context.Context, user userentity.User) (string, error) { 28 | return "", nil 29 | } 30 | 31 | // Update user 32 | func (r *Repository) Update(ctx context.Context) error { 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/server/admin.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/albertwidi/go-project-example/internal/pkg/router" 9 | "github.com/prometheus/client_golang/prometheus/promhttp" 10 | ) 11 | 12 | type adminServer struct { 13 | address string 14 | httpServer *http.Server 15 | listener net.Listener 16 | } 17 | 18 | func (s *Server) newAdminServer(address string) (*adminServer, error) { 19 | listener, err := net.Listen("tcp", address) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | adm := adminServer{ 25 | address: address, 26 | listener: listener, 27 | httpServer: &http.Server{}, 28 | } 29 | return &adm, nil 30 | } 31 | 32 | // Run admin server 33 | func (adm *adminServer) Run(middlewares ...router.MiddlewareFunc) error { 34 | r := router.New(adm.address, nil) 35 | r.Use(middlewares...) 36 | adm.registerHandler(r) 37 | adm.httpServer.Handler = r 38 | return adm.httpServer.Serve(adm.listener) 39 | } 40 | 41 | // Shutdown admin server 42 | func (adm *adminServer) Shutdown(ctx context.Context) error { 43 | return adm.httpServer.Shutdown(ctx) 44 | } 45 | 46 | func (adm *adminServer) registerHandler(r *router.Router) { 47 | r.Handle("/metrics", promhttp.Handler()) 48 | } 49 | -------------------------------------------------------------------------------- /internal/server/debug/handler.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | // go:generate swagger 4 | 5 | import ( 6 | "github.com/albertwidi/go-project-example/internal/pkg/router" 7 | "github.com/albertwidi/go-project-example/internal/server/debug/user" 8 | ) 9 | 10 | // Handlers of debug server 11 | type Handlers struct { 12 | user *user.Handler 13 | } 14 | 15 | func (s *Server) registerHandlers(r *router.Router) { 16 | // swagger:route GET /user/login/bypass bypass user login 17 | // Bypassing user login 18 | // This will bypass user login 19 | // Only ued in development 20 | // Consumes: 21 | // - application/json 22 | // Produces: 23 | // - application/json 24 | // Schemes: http 25 | r.Get("/user/login/bypass", s.handlers.user.BypassLogin) 26 | } 27 | -------------------------------------------------------------------------------- /internal/server/debug/image/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/albertwidi/go-project-example/d22e7c36ff52653442b1f3c7ba4f4e01a0c3c355/internal/server/debug/image/.keep -------------------------------------------------------------------------------- /internal/server/debug/server.go: -------------------------------------------------------------------------------- 1 | // debug server is for debug/development purpose only 2 | // to provide some debug functionality 3 | // for example login, file server, etc 4 | 5 | package debug 6 | 7 | import ( 8 | "context" 9 | "net" 10 | "net/http" 11 | 12 | "github.com/albertwidi/go-project-example/debug/user" 13 | "github.com/albertwidi/go-project-example/internal/pkg/router" 14 | userhandler "github.com/albertwidi/go-project-example/internal/server/debug/user" 15 | ) 16 | 17 | // Server struct 18 | type Server struct { 19 | address string 20 | httpServer *http.Server 21 | listener net.Listener 22 | // handlers 23 | handlers Handlers 24 | } 25 | 26 | // Usecases of debug server 27 | type Usecases struct { 28 | user *user.DebugUsecase 29 | } 30 | 31 | // New server 32 | func New(address string, usecases Usecases) (*Server, error) { 33 | listener, err := net.Listen("tcp", address) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // init all handlers 39 | userHandler := userhandler.New(usecases.user) 40 | handlers := Handlers{ 41 | user: userHandler, 42 | } 43 | s := Server{ 44 | address: address, 45 | listener: listener, 46 | httpServer: &http.Server{}, 47 | handlers: handlers, 48 | } 49 | return &s, nil 50 | } 51 | 52 | // Run debug server 53 | func (s *Server) Run(middlewares ...router.MiddlewareFunc) error { 54 | // initiate httpserver handler 55 | r := router.New(s.address, nil) 56 | r.Use(middlewares...) 57 | s.registerHandlers(r) 58 | s.httpServer.Handler = r 59 | return s.httpServer.Serve(s.listener) 60 | } 61 | 62 | // Shutdown debug server 63 | func (s *Server) Shutdown(ctx context.Context) error { 64 | return s.httpServer.Shutdown(ctx) 65 | } 66 | -------------------------------------------------------------------------------- /internal/server/debug/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | //go:generate swagger generate spec 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/albertwidi/go-project-example/debug/user" 9 | "github.com/albertwidi/go-project-example/internal/pkg/context" 10 | ) 11 | 12 | // Handler for user debug 13 | type Handler struct { 14 | } 15 | 16 | // New handler for user debug 17 | func New(userdebug *user.DebugUsecase) *Handler { 18 | h := Handler{} 19 | return &h 20 | } 21 | 22 | // BypassLogin handler for bypassing user login function 23 | func (h *Handler) BypassLogin(rctx *context.RequestContext) error { 24 | rctx.ResponseWriter().WriteHeader(http.StatusOK) 25 | rctx.ResponseWriter().Write([]byte("OK")) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/server/main/authentication/README.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | List of Authentication APIs 4 | 5 | ## **[GET]** /v1/authenticate 6 | 7 | ### Request 8 | 9 | header: 10 | 11 | - sid: `my_session_id` 12 | - uid: `my_user_id` 13 | 14 | url value: 15 | 16 | - type: `otp|pin` 17 | - username: `your_username` | username is a `phone_number` 18 | - state: `your_state` 19 | - action: `register|pay` 20 | 21 | below url value only valid if authentication `type` is `otp` 22 | 23 | - country: `ID` 24 | - code_length: `6` 25 | - code_expiry_time_seconds: `150` 26 | 27 | example: `/v1/authenticate?type=otp&username=628XXXXX&country=ID&state=thisismystate` 28 | 29 | ### Response 30 | 31 | ```json 32 | { 33 | "status": "OK", 34 | "data": { 35 | "authentication_id": "xxxxxxxx" 36 | } 37 | } 38 | ``` 39 | 40 | ## **[POST]** /v1/authenticate 41 | 42 | ### Request 43 | 44 | params: 45 | 46 | - state: `your_state` 47 | - authentication_id: `authentication_id` 48 | - username: `your_username` [only required if authentication type is password] 49 | - password: `your_password` 50 | 51 | ### Response 52 | 53 | ```json 54 | { 55 | "status": "OK", 56 | "data": { 57 | "authentication_id": "xxxxxxxx", 58 | "authentication_status": "authenticated" 59 | } 60 | } 61 | ``` -------------------------------------------------------------------------------- /internal/server/main/booking/booking.go: -------------------------------------------------------------------------------- 1 | package booking 2 | 3 | import ( 4 | "github.com/albertwidi/go-project-example/internal/pkg/context" 5 | ) 6 | 7 | func Something(context.RequestContext) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /internal/server/main/handler.go: -------------------------------------------------------------------------------- 1 | package mainserver 2 | -------------------------------------------------------------------------------- /internal/server/main/oauth2/README.md: -------------------------------------------------------------------------------- 1 | # Oauth2 2 | 3 | List of Oauth2 http apis 4 | 5 | ## [GET] **/v1/login/oauth/authorize** 6 | 7 | ### Request 8 | 9 | Query: 10 | 11 | - client_id: `user client_id, used for client_credentials grant` 12 | - redirect_uri: `user authorization callback URL` 13 | - state: `random string to protect againts forgery attacks` 14 | - login: `specific account to use for signing in` 15 | 16 | ### Example 17 | 18 | Header: 19 | 20 | - grant: `client_credentials` 21 | 22 | `/v1/oauth2/auth?client_id=xxx&client_secret=xxxx` 23 | 24 | Header: 25 | 26 | - grant: `password` 27 | 28 | `/v1/oauth2/auth?username=xxxx&password=xxx` 29 | 30 | ## [POST] **/v1/login/oauth/authenticate 31 | 32 | Header: 33 | 34 | - grant_type: `password|client_credentials` 35 | - otp: `required` -------------------------------------------------------------------------------- /internal/server/main/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | // Handler for oauth2 4 | type Handler struct{} 5 | -------------------------------------------------------------------------------- /internal/server/main/server.go: -------------------------------------------------------------------------------- 1 | package mainserver 2 | 3 | import "context" 4 | 5 | // Server struct 6 | type Server struct { 7 | address string 8 | } 9 | 10 | // New http server 11 | func New(address string) { 12 | 13 | } 14 | 15 | // Run the http server 16 | func (s *Server) Run() error { 17 | return nil 18 | } 19 | 20 | // Shutdown the main server 21 | func (s *Server) Shutdown(ctx context.Context) error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/server/main/user/README.md: -------------------------------------------------------------------------------- 1 | # User 2 | 3 | List of user APIs 4 | 5 | ## [GET] /v1/user/login 6 | 7 | ### Request 8 | 9 | url value: 10 | 11 | - username: `my_username` 12 | - type: `otp|password` 13 | - metadata: `redirect_url=https://redirect_url,foo=bar` 14 | 15 | example: `/v1/login?username=my_username&type=otp&metadata=redirect_url=https://redirect_url,foo=bar` 16 | 17 | ### Response 18 | 19 | ```json 20 | { 21 | "status": "OK", 22 | "data": { 23 | "authentication_url": "/v1/login", 24 | "authentication_method": "post", 25 | "authentication_state": "thisismystate" 26 | } 27 | } 28 | ``` 29 | 30 | ## [POST] /v1/user/login 31 | 32 | ### Request 33 | 34 | params: 35 | 36 | - password: `my_password` 37 | - state: `thisismystate` 38 | 39 | ### Response 40 | 41 | ```json 42 | { 43 | "status": "OK", 44 | "data": { 45 | "session": "my_session", 46 | "user_id": "my_user_id", 47 | "metadata": { 48 | "redirect_url": "https://redirect_url" 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## [POST] /v1/user/register 55 | 56 | ### Request 57 | 58 | header: 59 | 60 | - sid : `session_id` 61 | 62 | params: 63 | 64 | - username: `my_phone_number` 65 | - full_name: `my_full_name` 66 | - email: `my_email` 67 | 68 | ## Response 69 | 70 | ```json 71 | { 72 | "status": "OK", 73 | "data": { 74 | "authentication_state": "your_state", 75 | "authentication_id": "xxxx" 76 | } 77 | } 78 | ``` 79 | 80 | ## [POST] /v1/user/register/confirm 81 | 82 | ### Request 83 | 84 | header: 85 | 86 | - sid: `session_id` 87 | 88 | params: 89 | 90 | - password: `my_password` 91 | - authentication_id: `authentication_id` 92 | - state: `my_state` 93 | 94 | ### Response 95 | 96 | ```json 97 | { 98 | "status": "OK", 99 | "data": { 100 | "user_id": "my_user_id", 101 | "session": "my_session" 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /internal/third-party/facebook/oauth/oauth.go: -------------------------------------------------------------------------------- 1 | // Package oauth implements facebook oauth package 2 | package oauth 3 | 4 | import ( 5 | "golang.org/x/oauth2" 6 | "golang.org/x/oauth2/facebook" 7 | ) 8 | 9 | // scope list 10 | const ( 11 | ScopeEmail = "email" 12 | ScopeProfile = "profile" 13 | ) 14 | 15 | // OAuth struct 16 | type OAuth struct { 17 | config *oauth2.Config 18 | } 19 | 20 | // Config struct 21 | type Config struct { 22 | ClientID string 23 | ClientSecret string 24 | RedirectURL string 25 | Scopes []string 26 | } 27 | 28 | // New oauth 29 | func New(config Config) *OAuth { 30 | oauth := OAuth{ 31 | config: &oauth2.Config{ 32 | ClientID: config.ClientID, 33 | ClientSecret: config.ClientSecret, 34 | RedirectURL: config.RedirectURL, 35 | Scopes: config.Scopes, 36 | Endpoint: facebook.Endpoint, 37 | }, 38 | } 39 | return &oauth 40 | } 41 | 42 | // Config return google oauth2 configuration 43 | func (oauth *OAuth) Config() *oauth2.Config { 44 | return oauth.config 45 | } 46 | -------------------------------------------------------------------------------- /internal/third-party/firebase/pushmessage/const.go: -------------------------------------------------------------------------------- 1 | package pushmessage 2 | 3 | const ( 4 | dummyToken = "fzA7f5yE774:APA91bFNWhZYgfmJpWFi-R3xQ9BJuSCA6aHDKxUAO2TdjjrqvxOPnmOcEVIWv5bZ2ZNyed-rVKoiV32lVEJfGLaG-R73kt7F3Hy-n0pg73aYnmY0SkKmI4EP_RdcF4cADQDKx562LROQ" 5 | ) 6 | -------------------------------------------------------------------------------- /internal/third-party/firebase/pushmessage/pushmessage.go: -------------------------------------------------------------------------------- 1 | package pushmessage 2 | 3 | import ( 4 | "context" 5 | 6 | firebase "firebase.google.com/go" 7 | "firebase.google.com/go/messaging" 8 | "google.golang.org/api/option" 9 | ) 10 | 11 | // Firebase backend for pushmessage 12 | type Firebase struct { 13 | msgclient *messaging.Client 14 | } 15 | 16 | // Config of firebase 17 | type Config struct { 18 | ProjectID string 19 | ServiceAccountID string 20 | Bucket string 21 | ServiceAccountFile string 22 | DryRun bool 23 | } 24 | 25 | // SendOptions for firebase client 26 | type SendOptions struct { 27 | DryRun bool 28 | } 29 | 30 | // New firebase push message package 31 | func New(ctx context.Context, config *Config) (*Firebase, error) { 32 | var opts option.ClientOption 33 | cfg := new(firebase.Config) 34 | if config != nil { 35 | cfg.ProjectID = config.ProjectID 36 | cfg.ServiceAccountID = config.ServiceAccountID 37 | 38 | if config.ServiceAccountFile != "" { 39 | opts = option.WithCredentialsFile(config.ServiceAccountFile) 40 | } 41 | } 42 | 43 | app, err := firebase.NewApp(ctx, cfg, opts) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | msgclient, err := app.Messaging(ctx) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | f := Firebase{ 54 | msgclient: msgclient, 55 | } 56 | 57 | return &f, nil 58 | } 59 | 60 | // Send notification 61 | func (f *Firebase) Send(ctx context.Context, message *messaging.Message, options *SendOptions) (string, error) { 62 | var opts SendOptions 63 | if options != nil { 64 | opts = *options 65 | } 66 | 67 | var ( 68 | id string 69 | err error 70 | ) 71 | 72 | if !opts.DryRun { 73 | id, err = f.msgclient.Send(ctx, message) 74 | } else { 75 | // use default token if token not exists 76 | if message.Token == "" { 77 | message.Token = dummyToken 78 | } 79 | id, err = f.msgclient.SendDryRun(ctx, message) 80 | } 81 | return id, err 82 | } 83 | -------------------------------------------------------------------------------- /internal/third-party/google/oauth/oauth.go: -------------------------------------------------------------------------------- 1 | // the oauth package implements google oauth package 2 | 3 | package oauth 4 | 5 | import ( 6 | "golang.org/x/oauth2" 7 | "golang.org/x/oauth2/google" 8 | ) 9 | 10 | // scope list 11 | const ( 12 | ScopeEmail = "email" 13 | ScopeProfile = "profile" 14 | ) 15 | 16 | // OAuth struct 17 | type OAuth struct { 18 | config *oauth2.Config 19 | } 20 | 21 | // Config struct 22 | type Config struct { 23 | ClientID string 24 | ClientSecret string 25 | RedirectURL string 26 | Scopes []string 27 | } 28 | 29 | // New oauth 30 | func New(config Config) *OAuth { 31 | oauth := OAuth{ 32 | config: &oauth2.Config{ 33 | ClientID: config.ClientID, 34 | ClientSecret: config.ClientSecret, 35 | RedirectURL: config.RedirectURL, 36 | Scopes: config.Scopes, 37 | Endpoint: google.Endpoint, 38 | }, 39 | } 40 | return &oauth 41 | } 42 | 43 | // Config return google oauth2 configuration 44 | func (oauth *OAuth) Config() *oauth2.Config { 45 | return oauth.config 46 | } 47 | -------------------------------------------------------------------------------- /internal/usecase/authentication/README.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Authentication usecase handle all authentication request for the project. 4 | -------------------------------------------------------------------------------- /internal/usecase/authentication/authentication.go: -------------------------------------------------------------------------------- 1 | package authentication 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | authentity "github.com/albertwidi/go-project-example/internal/entity/authentication" 9 | otpentity "github.com/albertwidi/go-project-example/internal/entity/otp" 10 | stateentity "github.com/albertwidi/go-project-example/internal/entity/state" 11 | "github.com/albertwidi/go-project-example/internal/xerrors" 12 | ) 13 | 14 | // Usecase of authentication 15 | type Usecase struct { 16 | stateUsecase stateUsecase 17 | otpUsecase otpUsecase 18 | } 19 | 20 | type stateUsecase interface { 21 | Create(ctx context.Context, state stateentity.State) (string, error) 22 | Get(ctx context.Context, id string) (stateentity.State, error) 23 | } 24 | 25 | type secretUsecase interface { 26 | Get(ctx context.Context, username string) error 27 | } 28 | 29 | type otpUsecase interface { 30 | Create(ctx context.Context, uniqueID string, codeLength otpentity.CodeLength, expire time.Duration) (*otpentity.OTP, error) 31 | Get(ctx context.Context, uniqueID string) error 32 | } 33 | 34 | // New authentication usecase 35 | func New(stateUsecase stateUsecase, otpUsecase otpUsecase) *Usecase { 36 | u := Usecase{ 37 | stateUsecase: stateUsecase, 38 | otpUsecase: otpUsecase, 39 | } 40 | return &u 41 | } 42 | 43 | // Authenticate for trying to authenticate 44 | func (u *Usecase) Authenticate(ctx context.Context, username string, action authentity.Action, provider authentity.Provider, metadata map[string]string) (string, error) { 45 | state := stateentity.State{ 46 | CreatedBy: username, 47 | Authentication: authentity.Authentication{ 48 | Action: action, 49 | Provider: provider, 50 | Username: username, 51 | }, 52 | MetaData: metadata, 53 | CreatedAt: time.Now(), 54 | } 55 | 56 | id, err := u.stateUsecase.Create(ctx, state) 57 | if err != nil { 58 | return "", err 59 | } 60 | switch provider { 61 | case authentity.ProviderOTP: 62 | otpUniqueID := strings.Join([]string{username, string(action)}, ",") 63 | _, err := u.otpUsecase.Create(ctx, otpUniqueID, otpentity.CodeLength6, time.Minute*5) 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | } 69 | return id, nil 70 | } 71 | 72 | // Confirm for confirming the authenticating user request 73 | func (u *Usecase) Confirm(ctx context.Context, username, password, stateID string) error { 74 | op := xerrors.Op("authentication/authenticate") 75 | _, err := u.stateUsecase.Get(ctx, stateID) 76 | if err != nil { 77 | return xerrors.New(op, err) 78 | } 79 | return nil 80 | } 81 | 82 | // ResendCode for resend the code that used for authentication 83 | // for example the OTP code 84 | func (u *Usecase) ResendCode(ctx context.Context, stateID string) error { 85 | return nil 86 | } 87 | 88 | // IsAuthenticated to check whether state is used and authenticated for particular username 89 | func (u *Usecase) IsAuthenticated(ctx context.Context, username, stateID string, action authentity.Action) (bool, error) { 90 | return false, nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/usecase/booking/booking.go: -------------------------------------------------------------------------------- 1 | package booking 2 | 3 | // Usecase for booking 4 | type Usecase struct { 5 | } 6 | 7 | // New booking usecase 8 | func New() *Usecase { 9 | u := Usecase{} 10 | return &u 11 | } 12 | 13 | // Create booking 14 | func (u *Usecase) Create() error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/usecase/image/README.md: -------------------------------------------------------------------------------- 1 | # Image Usecase 2 | 3 | The usecase for image is only 2(at least for now): 4 | 5 | 1. Upload Image 6 | 2. Download Image 7 | 8 | ## Upload Image 9 | 10 | There are two `mode` for uploading image: 11 | 12 | 1. Public 13 | 2. Private 14 | 15 | We want to have different mode, because not all image is public. We might want to store a sensitive image related to customer. 16 | 17 | ### Private Image 18 | 19 | When uploading the `private` image, `metadata` attribute is saved alongside the `image`. In this `metadata`, contains the access and priviledge that belong to spesific user. This to make sure that `private` image can only be accessed by the rightful users. 20 | 21 | ## Download Image 22 | 23 | Download image usecase exists to serve `private` image. Image that belong to one user and is `private` should not visible to other users. So we need to check whether the downloader is the rightful user. 24 | 25 | ### Temporary Path 26 | 27 | To be added 28 | 29 | ### Object Storage Signed URL 30 | 31 | To be added 32 | 33 | ## TODO 34 | 35 | - Image manipulation(resize) 36 | - Image compression 37 | -------------------------------------------------------------------------------- /internal/usecase/image/image_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "testing" 8 | 9 | imageentity "github.com/albertwidi/go-project-example/internal/entity/image" 10 | "github.com/albertwidi/go-project-example/internal/pkg/objectstorage" 11 | "github.com/albertwidi/go-project-example/internal/pkg/objectstorage/local" 12 | imageusecase "github.com/albertwidi/go-project-example/internal/usecase/image" 13 | imagemock "github.com/albertwidi/go-project-example/internal/usecase/image/mock" 14 | "github.com/golang/mock/gomock" 15 | "github.com/google/go-cmp/cmp" 16 | ) 17 | 18 | func newLocalStorage(bucketName string) (*objectstorage.Storage, error) { 19 | storage, err := local.New(context.Background(), bucketName, &local.Options{DeleteOnClose: true}) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return objectstorage.New(storage), nil 24 | } 25 | 26 | func TestUpload(t *testing.T) { 27 | t.Parallel() 28 | 29 | repo := imagemock.NewMockimageRepository(gomock.NewController(t)) 30 | repo.EXPECT(). 31 | GetTempPath(context.Background(), "abcd"). 32 | Return("jkl", nil) 33 | repo.EXPECT(). 34 | GetTempPath(context.Background(), "jkl"). 35 | Return("", errors.New("fuck you")) 36 | 37 | storage, err := newLocalStorage("./testUpload") 38 | if err != nil { 39 | t.Error(err) 40 | return 41 | } 42 | defer storage.Close() 43 | 44 | usecase, err := imageusecase.New(storage, repo, nil) 45 | if err != nil { 46 | t.Error(err) 47 | return 48 | } 49 | 50 | cases := []struct { 51 | reader io.Reader 52 | info imageentity.FileInfo 53 | expectImage imageusecase.Image 54 | expectError error 55 | }{ 56 | { 57 | info: imageentity.FileInfo{ 58 | FileName: "testing.go", 59 | Size: 1000, 60 | UserHash: "eUjks", 61 | Mode: imageentity.ModePrivate, 62 | Group: imageentity.GroupUserAvatar, 63 | Tags: "asd,jkl,abcd", 64 | }, 65 | }, 66 | } 67 | 68 | for _, c := range cases { 69 | img, err := usecase.Upload(context.Background(), c.reader, c.info) 70 | if err != c.expectError { 71 | t.Errorf("testUpload: expecting error %v but got %v", c.expectError, err) 72 | return 73 | } 74 | 75 | if !cmp.Equal(img, c.expectImage) { 76 | t.Errorf("testUpload: expecting %+v but got %+v", c.expectImage, img) 77 | return 78 | } 79 | } 80 | } 81 | 82 | func TestDownload(t *testing.T) { 83 | } 84 | -------------------------------------------------------------------------------- /internal/usecase/image/mock/image_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: image.go 3 | 4 | // Package image is a generated GoMock package. 5 | package image 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | reflect "reflect" 11 | time "time" 12 | ) 13 | 14 | // MockimageRepository is a mock of imageRepository interface 15 | type MockimageRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockimageRepositoryMockRecorder 18 | } 19 | 20 | // MockimageRepositoryMockRecorder is the mock recorder for MockimageRepository 21 | type MockimageRepositoryMockRecorder struct { 22 | mock *MockimageRepository 23 | } 24 | 25 | // NewMockimageRepository creates a new mock instance 26 | func NewMockimageRepository(ctrl *gomock.Controller) *MockimageRepository { 27 | mock := &MockimageRepository{ctrl: ctrl} 28 | mock.recorder = &MockimageRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockimageRepository) EXPECT() *MockimageRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // SaveTempPath mocks base method 38 | func (m *MockimageRepository) SaveTempPath(ctx context.Context, id, originalPath string, expiryTime time.Duration) error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "SaveTempPath", ctx, id, originalPath, expiryTime) 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // SaveTempPath indicates an expected call of SaveTempPath 46 | func (mr *MockimageRepositoryMockRecorder) SaveTempPath(ctx, id, originalPath, expiryTime interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTempPath", reflect.TypeOf((*MockimageRepository)(nil).SaveTempPath), ctx, id, originalPath, expiryTime) 49 | } 50 | 51 | // GetTempPath mocks base method 52 | func (m *MockimageRepository) GetTempPath(ctx context.Context, id string) (string, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "GetTempPath", ctx, id) 55 | ret0, _ := ret[0].(string) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // GetTempPath indicates an expected call of GetTempPath 61 | func (mr *MockimageRepositoryMockRecorder) GetTempPath(ctx, id interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTempPath", reflect.TypeOf((*MockimageRepository)(nil).GetTempPath), ctx, id) 64 | } 65 | -------------------------------------------------------------------------------- /internal/usecase/notification/pushtemplate.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "html/template" 7 | ) 8 | 9 | // PushMessageTemplateFile to store template in a file 10 | type PushMessageTemplateFile struct { 11 | Title string `json:"title"` 12 | Message string `json:"message"` 13 | MessageDetail string `json:"message_detail"` 14 | Image string `json:"image"` 15 | WebViewLink string `json:"webview_link"` 16 | } 17 | 18 | // PushMessageGoTemplate for storing information about push message template as go template 19 | type PushMessageGoTemplate struct { 20 | Title *template.Template 21 | Message *template.Template 22 | MessageDetail *template.Template 23 | Image string 24 | WebViewLink string 25 | } 26 | 27 | // PushMessageTemplate for notification 28 | type PushMessageTemplate struct { 29 | templates map[string]PushMessageGoTemplate 30 | } 31 | 32 | // NewPushMessageTemplate for push notification template 33 | func NewPushMessageTemplate(ctx context.Context, templates map[string]PushMessageTemplateFile) (*PushMessageTemplate, error) { 34 | goTemplates := make(map[string]PushMessageGoTemplate) 35 | 36 | // TODO: make parsing concurrent with goroutines 37 | for name, file := range templates { 38 | var err error 39 | title := template.New(fmt.Sprintf("%s%s", "title", name)) 40 | title, err = title.Parse(file.Title) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | message := template.New(fmt.Sprintf("%s%s", "message", name)) 46 | message, err = message.Parse(file.Message) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | messageDetail := template.New(fmt.Sprintf("%s%s", "message_detail", name)) 52 | messageDetail, err = messageDetail.Parse(file.MessageDetail) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | goTemplates[name] = PushMessageGoTemplate{ 58 | Title: title, 59 | Message: message, 60 | MessageDetail: messageDetail, 61 | Image: file.Image, 62 | WebViewLink: file.WebViewLink, 63 | } 64 | } 65 | 66 | t := PushMessageTemplate{ 67 | templates: goTemplates, 68 | } 69 | return &t, nil 70 | } 71 | 72 | // Execute push message template 73 | func (pmt *PushMessageTemplate) Execute(name string, data interface{}) error { 74 | _, ok := pmt.templates[name] 75 | if !ok { 76 | return fmt.Errorf("push_message_tempalte: template with name %s not found", name) 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /internal/usecase/notification/smstemplate.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | // SMSTemplate for notification 4 | type SMSTemplate struct { 5 | Message string `json:"message"` 6 | } 7 | 8 | // NewSMSTemplate function 9 | func NewSMSTemplate() { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /internal/usecase/notification/template.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "html/template" 9 | "io/ioutil" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | 14 | entity "github.com/albertwidi/go-project-example/internal/entity/notification" 15 | ) 16 | // notificationTemplate struct 17 | type notificationTemplate struct { 18 | pushMessageTemplate map[string]*template.Template 19 | smsTemplate map[string]*template.Template 20 | emailTemplate map[string]*template.Template 21 | } 22 | 23 | // newTemplateGen for notification template 24 | func newTemplateGen(basePath string) (*notificationTemplate, error) { 25 | templatesList := []string{"sms", "pushmessage"} 26 | ntm := notificationTemplate{ 27 | pushMessageTemplate: make(map[string]*template.Template), 28 | smsTemplate: make(map[string]*template.Template), 29 | emailTemplate: make(map[string]*template.Template), 30 | } 31 | 32 | // load the template 33 | for _, l := range templatesList { 34 | var err error 35 | templatePath := path.Join(basePath, l) 36 | 37 | switch l { 38 | case "sms": 39 | err = filepath.Walk(templatePath, ntm.loadSMS) 40 | } 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | } 46 | 47 | return &ntm, nil 48 | } 49 | 50 | // Render template 51 | func (ntm notificationTemplate) Execute(templateType, templateName string, data interface{}) (string, error) { 52 | var ( 53 | t *template.Template 54 | ok bool 55 | ) 56 | switch templateType { 57 | case entity.TemplateTypePushMessage: 58 | t, ok = ntm.pushMessageTemplate[templateName] 59 | case entity.TemplateTypeSMS: 60 | t, ok = ntm.smsTemplate[templateName] 61 | case entity.TemplateTypeEmail: 62 | t, ok = ntm.emailTemplate[templateName] 63 | } 64 | 65 | if !ok { 66 | return "", fmt.Errorf("templategen: template with type %s and name %s not found", templateType, templateName) 67 | } 68 | 69 | b := []byte{} 70 | buffer := bytes.NewBuffer(b) 71 | if err := t.Execute(buffer, data); err != nil { 72 | return "", err 73 | } 74 | 75 | return buffer.String(), nil 76 | } 77 | 78 | // isExtSupported to check whether the file extension is valid for notification template 79 | func (ntm notificationTemplate) isExtSupported(ext string) error { 80 | switch ext { 81 | case ".json": 82 | default: 83 | return errors.New("templategen: template format not valid, expecting json") 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // TemplateSMS struct for loading sms notification template 90 | type TemplateSMS struct { 91 | Message string `json:"message"` 92 | } 93 | 94 | func (ntm *notificationTemplate) loadSMS(templatePath string, info os.FileInfo, err error) error { 95 | if err != nil { 96 | return err 97 | } 98 | 99 | // skip if is dir 100 | if info.IsDir() { 101 | return nil 102 | } 103 | 104 | // now only support json 105 | if err := ntm.isExtSupported(info.Name()); err != nil { 106 | return err 107 | } 108 | 109 | out, err := ioutil.ReadFile(templatePath) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | smsTemplates := make(map[string]TemplateSMS) 115 | if err := json.Unmarshal(out, &smsTemplates); err != nil { 116 | return err 117 | } 118 | 119 | for templateName, smsTemplate := range smsTemplates { 120 | var err error 121 | 122 | t := template.New(templateName) 123 | t, err = t.Parse(smsTemplate.Message) 124 | if err != nil { 125 | return err 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/usecase/oauth2/oauth2.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import "errors" 4 | 5 | var ( 6 | allowedGrantType = map[string]bool{ 7 | "password": true, 8 | "client_credentials": true, 9 | } 10 | ) 11 | 12 | // Usecase for oauth2 13 | type Usecase struct { 14 | } 15 | 16 | // New oauth2 usecase 17 | func New() *Usecase { 18 | u := Usecase{} 19 | return &u 20 | } 21 | 22 | // IsGrantTypeAllowed to check whether the grant type is allowed or not 23 | func (u *Usecase) IsGrantTypeAllowed(grantType string) error { 24 | _, ok := allowedGrantType[grantType] 25 | if !ok { 26 | return errors.New("oauth2: grant type is not allowed") 27 | } 28 | return nil 29 | } 30 | 31 | // Create new oauth2 access 32 | func (u *Usecase) Create(userID string, scopes []string) error { 33 | return nil 34 | } 35 | 36 | // Authorize user via oauth2 37 | func (u *Usecase) Authorize(grantType string, authParam interface{}) error { 38 | return nil 39 | } 40 | 41 | // AuthGrantPassword to authorize user via 42 | type AuthGrantPassword struct { 43 | Username string 44 | Password string 45 | } 46 | 47 | // authorizeGrantPassword for authorizing user via password 48 | // authorize with password grant always return the primary oauth2 token that user has 49 | // this grant type is used in user login via password 50 | // password can be user encrypted password in database 51 | // or an otp via sms 52 | func (u *Usecase) authorizeGrantPassword(username, password string) error { 53 | return nil 54 | } 55 | 56 | // authorizeGrantClientCredentials for athorizing user via client credentials(client_id/client_secret) 57 | func (u *Usecase) authorizeGrantClientCredentials(clientID, clientSecret string) error { 58 | return nil 59 | } -------------------------------------------------------------------------------- /internal/usecase/oauth2/scope.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | -------------------------------------------------------------------------------- /internal/usecase/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | // Usecase order 4 | type Usecase struct { 5 | } 6 | 7 | // New order usecase 8 | func New() *Usecase { 9 | u := Usecase{} 10 | return &u 11 | } 12 | -------------------------------------------------------------------------------- /internal/usecase/property/property.go: -------------------------------------------------------------------------------- 1 | package property 2 | 3 | // Usecase of property 4 | type Usecase struct { 5 | } 6 | 7 | // New property usecase 8 | func New() *Usecase { 9 | u := Usecase{} 10 | return &u 11 | } 12 | -------------------------------------------------------------------------------- /internal/usecase/room/room.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | // Usecase room 4 | type Usecase struct { 5 | } 6 | 7 | // New room usecase 8 | func New() *Usecase { 9 | u := Usecase{} 10 | return &u 11 | } 12 | -------------------------------------------------------------------------------- /internal/usecase/secret/secret.go: -------------------------------------------------------------------------------- 1 | package secret 2 | 3 | // New secret usecase 4 | func New() { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /internal/usecase/session/README.md: -------------------------------------------------------------------------------- 1 | # Session 2 | 3 | Allowed multiple session running on multiple device 4 | 5 | For example user `A` might have `android` and `ios` device, the user might use the same account to login into two different device and the session will be handled separately. 6 | 7 | ## Session Storage 8 | 9 | Session storage is using `redis` 10 | 11 | ## Multiple Session 12 | 13 | To ahchieve multiple session, `HASH` is used in redis, and the session is formed in this fashion: 14 | 15 | `usersession:{userid} {hash_sessionid1} {hash_sessionid2}` 16 | 17 | So it is possible to track how many session a user had and allowed us to delete all the session and force user to logout from all device. 18 | 19 | ## Tracking Expiry 20 | 21 | To track the expiry and allow each single `HASHFIELD` to have its own `expiry_time`, `expired_at` field must be added to the session. 22 | 23 | When a key is `retrieved`, program will check whether the `session` is expired or not. If the `session` expired, then the program will force the `user` to re-login and dispatch a job to delete the `session` from `HASHKEY` 24 | 25 | ## Downside 26 | 27 | ### State Sync 28 | 29 | The state between multiple session instances needs to be synced, for example if a user status is changed. 30 | 31 | ### Session Expiry 32 | 33 | User will only have 1 expire time. Once 1 key is expired, the other key will get expired too. The management of the `HASHFIELD` is become more complex than simple `session`. 34 | 35 | Many keys might be dangling when user is no longer active. Because we only have one time to expire, the expire time will be far longer than the usual `session` key, or maybe expire will never happens. All key `deletion` and `renewal` is based on user action. 36 | 37 | To delete the remaining `dangling` keys we might need some activity tracker, and by using cron to delete the `dangling` user session one by one. 38 | -------------------------------------------------------------------------------- /internal/usecase/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "strings" 7 | "time" 8 | 9 | sessionentity "github.com/albertwidi/go-project-example/internal/entity/session" 10 | userentity "github.com/albertwidi/go-project-example/internal/entity/user" 11 | guuid "github.com/google/uuid" 12 | ) 13 | 14 | // UseCase struct 15 | type UseCase struct { 16 | repo sessionRepo 17 | } 18 | 19 | // sessionRepo interface 20 | type sessionRepo interface { 21 | Save(ctx context.Context, userhash userentity.Hash, sessionid string, data sessionentity.Session) error 22 | SaveUserData(ctx context.Context, userhash userentity.Hash, userData sessionentity.UserData) error 23 | Get(ctx context.Context, userhash userentity.Hash, sessionid string) (sessionentity.Session, error) 24 | Delete(ctx context.Context, userhash userentity.Hash, sessionid string) error 25 | DeleteAll(ctx context.Context, userhash userentity.Hash) error 26 | } 27 | 28 | // New session usecase 29 | func New(repo sessionRepo) *UseCase { 30 | u := UseCase{ 31 | repo: repo, 32 | } 33 | 34 | return &u 35 | } 36 | 37 | // createID of session 38 | func (u UseCase) createID(userhash userentity.Hash) string { 39 | keys := []string{time.Now().String(), string(userhash), guuid.New().String()} 40 | id := strings.Join(keys, ":") 41 | id = base64.RawStdEncoding.EncodeToString([]byte(id)) 42 | return id 43 | } 44 | 45 | // Create function 46 | func (u UseCase) Create(ctx context.Context, userhash userentity.Hash, sessionData sessionentity.Session) (string, error) { 47 | if err := userhash.Validate(); err != nil { 48 | return "", err 49 | } 50 | 51 | id := u.createID(userhash) 52 | sessionData.ID = id 53 | err := u.repo.Save(ctx, userhash, id, sessionData) 54 | if err != nil { 55 | return "", sessionentity.ErrSessionNotFound 56 | } 57 | 58 | return id, nil 59 | } 60 | 61 | // SetUserInfo to set/change user information in session 62 | func (u UseCase) SetUserInfo(ctx context.Context, userhash userentity.Hash, sessionid string, userData sessionentity.UserData) error { 63 | sess, err := u.Get(ctx, userhash, sessionid) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | user := userentity.User{} 69 | bio := userentity.Bio{} 70 | 71 | if userData.User != user { 72 | sess.UserData.User = userData.User 73 | } 74 | 75 | if userData.Bio != bio { 76 | sess.UserData.Bio = userData.Bio 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // Get session 83 | // sessionkey is the user hash-id 84 | // sessionid is the sessionid returned when logged in or creating session 85 | func (u UseCase) Get(ctx context.Context, userhash userentity.Hash, sessionid string) (sessionentity.Session, error) { 86 | var err error 87 | s := sessionentity.Session{} 88 | 89 | if userhash == "" { 90 | return s, sessionentity.Err 91 | } 92 | 93 | if err := userhash.Validate(); err != nil { 94 | return s, err 95 | } 96 | 97 | s, err = u.repo.Get(ctx, userhash, sessionid) 98 | if err != nil { 99 | return s, err 100 | } 101 | return s, nil 102 | } 103 | 104 | // Remove a specific session in a user 105 | func (u UseCase) Remove(ctx context.Context, userhash userentity.Hash, sessionid string) error { 106 | if err := userhash.Validate(); err != nil { 107 | return err 108 | } 109 | 110 | err := u.repo.Delete(ctx, userhash, sessionid) 111 | if err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | // RemoveAll the session in user 118 | func (u UseCase) RemoveAll(ctx context.Context, userhash userentity.Hash) error { 119 | if err := userhash.Validate(); err != nil { 120 | return err 121 | } 122 | 123 | err := u.repo.DeleteAll(ctx, userhash) 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/usecase/state/README.md: -------------------------------------------------------------------------------- 1 | # State Usecase 2 | 3 | State is useful to ensure that request is coming from the right person, to prevent a CFRS(Cross-site Request Forgery) -------------------------------------------------------------------------------- /internal/usecase/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "errors" 7 | "strings" 8 | "time" 9 | 10 | entity "github.com/albertwidi/go-project-example/internal/entity/state" 11 | "github.com/albertwidi/go-project-example/internal/pkg/ulid" 12 | ) 13 | 14 | // Usecase of state 15 | type Usecase struct { 16 | repo stateRepository 17 | // ulid generator 18 | ulidgen *ulid.Ulid 19 | } 20 | 21 | type stateRepository interface { 22 | Save(ctx context.Context, stateid string, data entity.State) error 23 | Get(ctx context.Context, stateid string) (entity.State, error) 24 | Delete(ctx context.Context, stateid string) error 25 | SetExpire(ctx context.Context, stateid string, duration time.Duration) error 26 | } 27 | 28 | // New state Usecase 29 | func New(stateRepo stateRepository) *Usecase { 30 | u := Usecase{ 31 | repo: stateRepo, 32 | ulidgen: ulid.New(3), 33 | } 34 | return &u 35 | } 36 | 37 | // create id for state 38 | func (u Usecase) createID(data entity.State) (string, error) { 39 | now := time.Now() 40 | 41 | // key for state_id generation conains ulid:identifier:timeunix 42 | keyList := []string{u.ulidgen.Ulid(), data.Identifier, now.String()} 43 | key := strings.Join(keyList, ":") 44 | // encode string to create a stateID 45 | stateID := base64.RawStdEncoding.EncodeToString([]byte(key)) 46 | 47 | return stateID, nil 48 | } 49 | 50 | // Create state 51 | // returning state_id and error 52 | func (u Usecase) Create(ctx context.Context, data entity.State) (string, error) { 53 | if err := data.Validate(); err != nil { 54 | return "", err 55 | } 56 | 57 | id, err := u.createID(data) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | if id == "" { 63 | return "", errors.New("state id is empty") 64 | } 65 | 66 | now := time.Now() 67 | if data.ExpiryTime == 0 { 68 | data.ExpiryTime = entity.DefaultStateExpiryTime 69 | } 70 | 71 | // set the expire at of the data 72 | data.ExpiredAt = now.Add(data.ExpiryTime) 73 | if err := u.repo.Save(ctx, id, data); err != nil { 74 | return "", err 75 | } 76 | 77 | return id, nil 78 | } 79 | 80 | // Get state 81 | func (u Usecase) Get(ctx context.Context, stateid string) (entity.State, error) { 82 | state, err := u.repo.Get(ctx, stateid) 83 | return state, err 84 | } 85 | 86 | // Delete state 87 | func (u Usecase) Delete(ctx context.Context, stateid string) error { 88 | err := u.repo.Delete(ctx, stateid) 89 | return err 90 | } 91 | 92 | // SetExpire for state 93 | func (u Usecase) SetExpire(ctx context.Context, stateid string, duration time.Duration) error { 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/usecase/usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | // Usecases contains list of usecase available to use 4 | type Usecases struct { 5 | } 6 | -------------------------------------------------------------------------------- /internal/usecase/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | 6 | userentity "github.com/albertwidi/go-project-example/internal/entity/user" 7 | ) 8 | 9 | // Usecase of user 10 | type Usecase struct { 11 | authUsecase authUsecase 12 | } 13 | 14 | type authUsecase interface { 15 | } 16 | 17 | type userRepository interface { 18 | Create() error 19 | Update() error 20 | UpdateStatus() error 21 | Remove() error 22 | } 23 | 24 | // New user usecase 25 | func New(authUsecase authUsecase) *Usecase { 26 | u := Usecase{ 27 | authUsecase: authUsecase, 28 | } 29 | return &u 30 | } 31 | 32 | // RegisterData for registration data parameter 33 | type RegisterData struct { 34 | Country userentity.Country 35 | PhoneNumber string 36 | FullName string 37 | } 38 | 39 | // Validate register data 40 | func (rgd *RegisterData) Validate() error { 41 | if err := rgd.Country.Validate(); err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | 47 | // Register usecase 48 | func (u *Usecase) Register(ctx context.Context, data RegisterData) error { 49 | if err := data.Validate(); err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | 55 | // RegisterConfirm usecase 56 | func (u *Usecase) RegisterConfirm(ctx context.Context) error { 57 | return nil 58 | } 59 | 60 | // Login usecase 61 | func (u *Usecase) Login(ctx context.Context) error { 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/xerrors/README.md: -------------------------------------------------------------------------------- 1 | # XErrors 2 | 3 | XErrors package wrap Go 1.13 errors package 4 | 5 | This package has `upspin` style error. 6 | 7 | ## Creating Error 8 | 9 | Creating error in `xerrors` is as simple as in `errors` package 10 | 11 | ```go 12 | xerrors.New("this is an error) 13 | ``` 14 | 15 | ## Error With Kind 16 | 17 | Error can be ambigous and hard to categorized, kind is a constant that means to categorized error. 18 | 19 | For example: 20 | 21 | - Not Found 22 | - Internal Error 23 | - OK 24 | - Bad Request 25 | 26 | To create an error with `kind`: 27 | 28 | ```go 29 | xerrors.New("this is an error", xerrors.KindOK) 30 | ``` 31 | 32 | ## Error With Op/Operation 33 | 34 | Sometimes function need to be tagged, especially for tracing. With `op` we can tag our error to trace our error more easily. 35 | 36 | To create an error with `op`: 37 | 38 | ```go 39 | xerrors.New(xerrors.Op("doing_something), "this is an error") 40 | ``` 41 | -------------------------------------------------------------------------------- /internal/xerrors/example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/albertwidi/go-project-example/internal/xerrors" 8 | ) 9 | 10 | var ( 11 | errSomething = errors.New("something is error bruh") 12 | ) 13 | 14 | func main() { 15 | // xerrors.SetCaller(true) 16 | err := a() 17 | log.Printf("%v", err) 18 | 19 | if xerrors.Is(err, errSomething) { 20 | log.Println("WAIKI") 21 | } else { 22 | log.Println("WAHH") 23 | } 24 | 25 | e := xerrors.XUnwrap(err) 26 | log.Println(e.Kind()) 27 | } 28 | 29 | func a() error { 30 | err := b() 31 | return xerrors.New(xerrors.Op("function/a"), err) 32 | } 33 | 34 | func b() error { 35 | err := c() 36 | return xerrors.New(xerrors.Op("function/b"), err) 37 | } 38 | 39 | func c() error { 40 | err := d() 41 | return xerrors.New(xerrors.Op("function/c"), err) 42 | } 43 | 44 | func d() error { 45 | return xerrors.New(xerrors.Op("function/d"), errSomething, xerrors.KindBadRequest) 46 | } 47 | -------------------------------------------------------------------------------- /internal/xerrors/xerrors.go: -------------------------------------------------------------------------------- 1 | package xerrors 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime" 7 | ) 8 | 9 | // xerrors global var 10 | var ( 11 | _caller bool 12 | ) 13 | 14 | // Kind of errors 15 | type Kind int16 16 | 17 | // kind of errors 18 | const ( 19 | KindOK Kind = iota 20 | KindNotFound 21 | KindBadRequest 22 | KindUnauthorized 23 | KindInternalError 24 | ) 25 | 26 | // Op is the operation when error happens 27 | type Op string 28 | 29 | // String value of Op 30 | func (op Op) String() string { 31 | return string(op) 32 | } 33 | 34 | // Fields of errors 35 | type Fields map[string]interface{} 36 | 37 | // Errors of xerrors 38 | type Errors struct { 39 | Err error 40 | InnerErr error 41 | kind Kind 42 | op Op 43 | } 44 | 45 | // New errors 46 | func New(v ...interface{}) error { 47 | var ( 48 | xerr = &Errors{} 49 | file string 50 | line int 51 | ) 52 | 53 | // only cal _caller when xerrors _caller is true 54 | if _caller { 55 | _, file, line, _ = runtime.Caller(1) 56 | } 57 | 58 | for _, arg := range v { 59 | switch val := arg.(type) { 60 | case Op: 61 | xerr.op = val 62 | 63 | case string: 64 | if _caller { 65 | xerr.Err = fmt.Errorf("%s: %s: [file=%s, line=%d]", val, xerr.op, file, line) 66 | continue 67 | } 68 | xerr.Err = fmt.Errorf("%s: %s", val, xerr.op) 69 | 70 | case Kind: 71 | xerr.kind = val 72 | 73 | case *Errors: 74 | val.op = xerr.op 75 | // copy the errors 76 | xerr = val 77 | 78 | if _caller { 79 | xerr.Err = fmt.Errorf("error executing %s: [file=%s, line=%d] \n%w", xerr.op, file, line, val.Err) 80 | continue 81 | } 82 | xerr.Err = fmt.Errorf("error executing %s: %w", xerr.op, val.Err) 83 | 84 | case error: 85 | if _caller { 86 | xerr.Err = fmt.Errorf("%w: %s: [file=%s, line=%d]", val, xerr.op, file, line) 87 | continue 88 | } 89 | xerr.Err = fmt.Errorf("%w: %s", val, xerr.op) 90 | 91 | default: 92 | continue 93 | } 94 | } 95 | return xerr 96 | } 97 | 98 | // Error return string of error 99 | func (e *Errors) Error() string { 100 | return e.Err.Error() 101 | } 102 | 103 | // Unwrap errors 104 | func (e *Errors) Unwrap() error { 105 | return e.Err 106 | } 107 | 108 | // Kind of errors 109 | func (e *Errors) Kind() Kind { 110 | return e.kind 111 | } 112 | 113 | // Is wrap the errors is 114 | func Is(err, target error) bool { 115 | return errors.Is(err, target) 116 | } 117 | 118 | // As wrap the error as 119 | func As(err error, target interface{}) bool { 120 | return errors.As(err, target) 121 | } 122 | 123 | // Unwrap error 124 | func Unwrap(err error) error { 125 | return errors.Unwrap(err) 126 | } 127 | 128 | // XUnwrap return errors with xerror package type 129 | func XUnwrap(err error) *Errors { 130 | xerr, ok := err.(*Errors) 131 | if ok { 132 | return xerr 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // SetCaller to print the stack-trace of the error 139 | func SetCaller(c bool) { 140 | _caller = c 141 | } 142 | -------------------------------------------------------------------------------- /project.config.toml: -------------------------------------------------------------------------------- 1 | # project configuration 2 | # ${} variable will be replaced with environment variables in runtime 3 | 4 | [servers] 5 | # only open main port to public 6 | [servers.main] 7 | address = "${MAIN_SERVER_ADDRESS}" 8 | [servers.debug] 9 | address = "${DEBUG_SERVER_ADDRESS}" 10 | [servers.admin] 11 | address = "${ADMIN_SERVER_ADDRESS}" 12 | 13 | [log] 14 | level = "${LOG_LEVEL}" 15 | file = "${LOG_FILE}" 16 | use_color = ${LOG_USE_COLOR} 17 | 18 | [resources] 19 | # object storage 20 | [[resources.object_storage]] 21 | name = "image-private" 22 | bucket = "${STORAGE_IMAGE_PRIVATE_BUCKET}" 23 | provider = "${STORAGE_IMAGE_PRIVATE_PROVIDER}" 24 | region = "${STORAGE_IMAGE_PRIVATE_REGION}" 25 | endpoint = "${STORAGE_IMAGE_PRIVATE_ENDPOINT}" 26 | [resources.object_storage.s3] 27 | client_id = "${STORAGE_IMAGE_CLIENT_ID}" 28 | client_secret = "${STORAGE_IMAGE_CLIENT_SECRET}" 29 | disable_ssl = ${STORAGE_IMAGE_DISABLE_SSL} 30 | force_path_style = ${STORAGE_IMAGE_FOCE_PATH_STYLE} 31 | 32 | # database 33 | [resources.database] 34 | # default options 35 | max_open_conns = 20 36 | max_retry = 5 37 | [[resources.database.connect]] 38 | name = "users" 39 | driver = "postgres" 40 | [resources.database.connect.leader] 41 | dsn = "${DB_USER_LEADER_DSN}" 42 | [resources.database.connect.replica] 43 | dsn = "${DB_USER_REPLICA_DSN}" 44 | 45 | [[resources.database.connect]] 46 | name = "notifications" 47 | driver = "postgres" 48 | [resources.database.connect.leader] 49 | dsn = "${DB_NOTIFICATION_LEADER_DSN}" 50 | [resources.database.connect.replica] 51 | dsn = "${DB_NOTIFICATION_REPLICA_DSN}" 52 | 53 | # redis 54 | [resources.redis] 55 | max_active_conn = 100 56 | [[resources.redis.connect]] 57 | name = "session" 58 | address = "${REDIS_SESSION_ADDRESS}" 59 | [[resources.redis.connect]] 60 | name = "image" 61 | address = "${REDIS_IMAGE_ADDRESS}" -------------------------------------------------------------------------------- /project.env.toml: -------------------------------------------------------------------------------- 1 | # env file is a helper file for the project to set environment variable 2 | # similar to .env file 3 | 4 | # server 5 | main_server_address = ":8000" 6 | debug_server_address = ":9000" 7 | admin_server_address = ":5726" 8 | 9 | # log 10 | log_level = "info" 11 | log_file = "./projectlog/project.log" 12 | log_use_color = true 13 | 14 | # storage image options 15 | storage_image_client_id = "" 16 | storage_image_client_secret = "" 17 | storage_image_disable_ssl = true 18 | storage_image_foce_path_style = true 19 | 20 | # storage image private 21 | storage_image_private_bucket = "./debug/bucket/private-image" 22 | storage_image_private_provider = "local" 23 | storage_image_private_region = "" 24 | storage_image_private_endpoint = "" 25 | 26 | # image download options 27 | image_private_download_proto = "http://" 28 | image_private_download_host = "" 29 | image_private_download_port = ":9000" 30 | image_private_download_path = "/v1/image/view" 31 | 32 | # postgres database 33 | db_user_leader_dsn = "postgres://projectdb:projectdb@localhost:5444/users?sslmode=disable" 34 | db_user_replica_dsn = "postgres://projectdb:projectdb@localhost:5444/users?sslmode=disable" 35 | db_notification_leader_dsn = "postgres://projectdb:projectdb@localhost:5444/notifications?sslmode=disable" 36 | db_notification_replica_dsn = "postgres://projectdb:projectdb@localhost:5444/notifications?sslmode=disable" 37 | 38 | # redis database 39 | redis_session_address = "localhost:6379" 40 | redis_image_address = "localhost:6379" -------------------------------------------------------------------------------- /scripts/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # installing soda 4 | if ! soda -v 2>/dev/null; then 5 | go get -v github.com/gobuffalo/pop/... 6 | go install github.com/gobuffalo/pop/soda 7 | fi 8 | 9 | # installing mockgen 10 | if ! mockgen -v 2>/dev/null; then 11 | go get github.com/golang/mock/mockgen 12 | fi --------------------------------------------------------------------------------