├── README.md ├── config-dev.json ├── config └── config.go ├── create_user.sh ├── domain └── user.go ├── get_users.sh ├── infrastructure └── mongo.go ├── interfaces ├── repositories │ ├── mongo.go │ └── mongo │ │ └── mongo.go └── webcontrollers │ ├── middleware │ └── middleware.go │ └── user.go ├── main.go └── usecases └── user.go /README.md: -------------------------------------------------------------------------------- 1 | handybid 2 | ======== 3 | 4 | Sample social app written in go and mongo 5 | 6 | - server design based on uncle bob clean architecture 7 | - http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html 8 | 9 | - package dependencies from inner circle going outwards : 10 | - domain 11 | - data model definitions and interfaces 12 | - interfaces.repositories 13 | - implementation of domain interfaces. DB is mongo 14 | - usecases 15 | - application use case implementations. Also defines data types used across usecases and webcontrollers 16 | - infrastructure (same level as interfaces.webcontrollers) 17 | - DB connection specifics to mongo and other infra related code 18 | - interfaces.webcontrollers (same level as infrastructure) 19 | - REST interfaces. Defines interfaces implemented by usecases 20 | 21 | - main.go : usecases module is injected into webcontroller. user repository (interfaces.repositories.mongo implementation) is injected into usecases. This way each of the outer modules only provides implementations for interfaces provided by lower layer. This makes modules pluggable, easily mocked for testing and loosely coupled. 22 | -------------------------------------------------------------------------------- /config-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "db" : { 3 | "host" : "localhost:27000", 4 | "database" : "handybid", 5 | "user" : "handybid", 6 | "password" : "handybid" 7 | } 8 | } -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | type DB struct { 11 | Host string `json:"host"` 12 | Database string `json:"database"` 13 | User string `json:"user"` 14 | Password string `json:"password"` 15 | } 16 | 17 | // Config represents the configuration information. 18 | type Config struct { 19 | Db DB `json:"db"` 20 | } 21 | 22 | func (c *Config) String() string { 23 | return fmt.Sprintf("%s:%s@(%s)/%s", c.Db.User, c.Db.Password, c.Db.Host, c.Db.Database) 24 | } 25 | 26 | var Conf Config 27 | 28 | func init() { 29 | // default to development 30 | configFile := "./config-dev.json" 31 | 32 | // Get the config file name if passed in. For prd, pass it in 33 | if len(os.Args) > 1 { 34 | configFile = os.Args[1] 35 | } 36 | 37 | config_file, err := ioutil.ReadFile(configFile) 38 | if err != nil { 39 | fmt.Printf("File error: %v\n", err) 40 | } 41 | json.Unmarshal(config_file, &Conf) 42 | } 43 | -------------------------------------------------------------------------------- /create_user.sh: -------------------------------------------------------------------------------- 1 | curl -v -H "Content-Type: application/json" -X POST -d '{"disp_name":"test display", "name":"test","phone":"1650-484-4848","email_id":"test@gmail.com","password":"testpwd","address":"1653 hollenbeck ave, sunnyvale"}' http://127.0.0.1:8090/users 2 | -------------------------------------------------------------------------------- /domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | "time" 6 | ) 7 | 8 | // Implementation in repositories package 9 | type UserRepository interface { 10 | StoreUser(user User) 11 | FindUserById(id string) User 12 | ListUsers() ([]User, error) 13 | } 14 | 15 | type User struct { 16 | // FIXME: domain types should have no dependency on any mongo bson data types 17 | // Can only be done by having repository specific data types that maps to types in here 18 | ID bson.ObjectId `json:"id" bson:"_id,omitempty"` 19 | DispName string 20 | Name string 21 | Phone string 22 | EmailId string 23 | Password string 24 | Address string 25 | Lat float64 26 | Long float64 27 | IsProvider bool 28 | ProviderInfo []Provider 29 | CreateTs time.Time 30 | UpdateTs time.Time 31 | } 32 | 33 | type Provider struct { 34 | Type string // "cleaner", "handyman" 35 | Rate float64 // example $22.50 / hour 36 | DiscountPct float64 // discount percentage - example 10% - applied to Rate 37 | AutoReply bool // If on and available, then system auto replies to jobs 38 | Available bool 39 | } 40 | -------------------------------------------------------------------------------- /get_users.sh: -------------------------------------------------------------------------------- 1 | curl http://127.0.0.1:8090/users 2 | -------------------------------------------------------------------------------- /infrastructure/mongo.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "github.com/jaijiv/handybid/config" 5 | "gopkg.in/mgo.v2" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type MgoSession struct { 11 | Session *mgo.Session 12 | } 13 | 14 | var mongoSession *mgo.Session 15 | 16 | func ConnectMongo() error { 17 | var err error 18 | /* 19 | * Create the connection 20 | */ 21 | log.Println("Connecting to mongo database...") 22 | 23 | // We need this object to establish a session to our MongoDB. 24 | mongoDBDialInfo := &mgo.DialInfo{ 25 | Addrs: []string{config.Conf.Db.Host}, 26 | Timeout: 60 * time.Second, 27 | Database: config.Conf.Db.Database, 28 | Username: config.Conf.Db.User, 29 | Password: config.Conf.Db.Password, 30 | } 31 | 32 | // Create a session which maintains a pool of socket connections 33 | // to our MongoDB. 34 | mongoSession, err = mgo.DialWithInfo(mongoDBDialInfo) 35 | if err != nil { 36 | log.Fatalf("CreateSession: %s\n", err) 37 | } 38 | 39 | mongoSession.SetMode(mgo.Monotonic, true) 40 | 41 | return nil 42 | } 43 | 44 | func MongoSession() *MgoSession { 45 | return &MgoSession{mongoSession.Copy()} 46 | } 47 | 48 | func (ms *MgoSession) UserCol() *mgo.Collection { 49 | return ms.Session.DB(config.Conf.Db.Database).C("user") 50 | } 51 | -------------------------------------------------------------------------------- /interfaces/repositories/mongo.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/jaijiv/handybid/domain" 5 | ) 6 | 7 | type MongoRepo struct{} 8 | 9 | func (MongoRepo) StoreUser(user domain.User) { 10 | } 11 | 12 | func (MongoRepo) FindUserById(id string) domain.User { 13 | return domain.User{} 14 | } 15 | -------------------------------------------------------------------------------- /interfaces/repositories/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "github.com/jaijiv/handybid/domain" 5 | "github.com/jaijiv/handybid/infrastructure" 6 | "log" 7 | ) 8 | 9 | type MongoRepo struct{} 10 | 11 | func (MongoRepo) StoreUser(user domain.User) { 12 | ms := infrastructure.MongoSession() 13 | defer ms.Session.Close() 14 | 15 | ms.UserCol().Insert(&user) 16 | log.Println("Inserted user into db") 17 | } 18 | 19 | func (MongoRepo) ListUsers() ([]domain.User, error) { 20 | ms := infrastructure.MongoSession() 21 | defer ms.Session.Close() 22 | 23 | var dusers []domain.User 24 | err := ms.UserCol().Find(nil).All(&dusers) 25 | if err != nil { 26 | return nil, err 27 | } 28 | log.Println("Listed all users from db") 29 | return dusers, nil 30 | } 31 | 32 | func (MongoRepo) FindUserById(id string) domain.User { 33 | return domain.User{} 34 | } 35 | -------------------------------------------------------------------------------- /interfaces/webcontrollers/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | func Auth(h http.Handler) http.Handler { 10 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 11 | log.Println("Authorizing...") 12 | h.ServeHTTP(writer, request) 13 | }) 14 | } 15 | 16 | func Logger(h http.Handler) http.Handler { 17 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 18 | startTime := time.Now() 19 | h.ServeHTTP(writer, request) 20 | log.Printf("%s - %s (%v)\n", request.Method, request.URL.Path, time.Since(startTime)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /interfaces/webcontrollers/user.go: -------------------------------------------------------------------------------- 1 | package webcontrollers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/jaijiv/handybid/usecases" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | // Implementation in use cases package 15 | type UserInteractor interface { 16 | RegisterUser(user usecases.User) error 17 | ListUsers() ([]usecases.User, error) 18 | } 19 | 20 | type WebserviceHandler struct { 21 | UserInteractor UserInteractor 22 | } 23 | 24 | func (handler WebserviceHandler) RegisterUser(w http.ResponseWriter, r *http.Request) { 25 | log.Println("RegisterUser called...") 26 | // Parse json request 27 | body, err := ioutil.ReadAll(r.Body) 28 | if err != nil { 29 | serveError(w, err) 30 | return 31 | } 32 | log.Println(string(body)) 33 | var user usecases.User 34 | err = json.Unmarshal(body, &user) 35 | if err != nil { 36 | serveError(w, err) 37 | return 38 | } 39 | log.Println(user) 40 | handler.UserInteractor.RegisterUser(user) 41 | } 42 | 43 | func (handler WebserviceHandler) ListUsers(w http.ResponseWriter, r *http.Request) { 44 | log.Println("ListUsers called...") 45 | users, err := handler.UserInteractor.ListUsers() 46 | if err != nil { 47 | serveError(w, err) 48 | return 49 | } 50 | jsonBytes, _ := json.Marshal(users) 51 | content := strings.Replace(string(jsonBytes), "%", "%%", -1) 52 | 53 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 54 | w.WriteHeader(200) 55 | fmt.Fprintf(w, content) 56 | 57 | } 58 | 59 | func serve404(w http.ResponseWriter) { 60 | w.WriteHeader(http.StatusNotFound) 61 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 62 | io.WriteString(w, "404 error") 63 | } 64 | 65 | func serveError(w http.ResponseWriter, err error) { 66 | log.Println(err) 67 | w.WriteHeader(http.StatusInternalServerError) 68 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 69 | } 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | 11 | "github.com/adampresley/sigint" 12 | "github.com/gorilla/mux" 13 | "github.com/jaijiv/handybid/infrastructure" 14 | "github.com/jaijiv/handybid/interfaces/repositories/mongo" 15 | "github.com/jaijiv/handybid/interfaces/webcontrollers" 16 | "github.com/jaijiv/handybid/interfaces/webcontrollers/middleware" 17 | "github.com/jaijiv/handybid/usecases" 18 | "github.com/justinas/alice" 19 | ) 20 | 21 | func init() { 22 | var err error 23 | err = infrastructure.ConnectMongo() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | } 28 | 29 | var ( 30 | port = flag.Int("port", 8090, "Port for web server") 31 | host = flag.String("host", "localhost", "Address for web server") 32 | webserviceHandler = new(webcontrollers.WebserviceHandler) 33 | ) 34 | 35 | func main() { 36 | runtime.GOMAXPROCS(runtime.NumCPU()) 37 | flag.Parse() 38 | 39 | /* 40 | * Handle SIGINT (CTRL+C) 41 | */ 42 | sigint.ListenForSIGINT(func() { 43 | log.Println("Shutting down handybid server...") 44 | os.Exit(0) 45 | }) 46 | 47 | // Instantiate UserInteractor from usecases 48 | userInteractor := new(usecases.UserInteractor) 49 | // Inject mongo repository that implements domain methods into userInteractor 50 | userInteractor.UserRepository = mongo.MongoRepo{} 51 | // Inject userInteractor from usecases into web service handler 52 | webserviceHandler.UserInteractor = userInteractor 53 | 54 | router := mux.NewRouter() 55 | router.HandleFunc("/users", RegisterUser).Methods("POST") 56 | router.HandleFunc("/users", ListUsers).Methods("GET") 57 | middleware := alice.New(middleware.Logger).Then(router) 58 | 59 | log.Printf("handybid services started on %s:%d\n\n", *host, *port) 60 | http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), middleware) 61 | } 62 | 63 | func RegisterUser(w http.ResponseWriter, r *http.Request) { 64 | webserviceHandler.RegisterUser(w, r) 65 | } 66 | 67 | func ListUsers(w http.ResponseWriter, r *http.Request) { 68 | webserviceHandler.ListUsers(w, r) 69 | } 70 | -------------------------------------------------------------------------------- /usecases/user.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "github.com/jaijiv/handybid/domain" 5 | "time" 6 | ) 7 | 8 | // These data types are exposed to interfaces layer. 9 | // These are data types that satisfies usecases for this application. 10 | type User struct { 11 | Id string `json:"id"` 12 | DispName string `json:"disp_name"` 13 | Name string `json:"name"` 14 | Phone string `json:"phone"` 15 | EmailId string `json:"email_id"` 16 | Password string `json:"password"` 17 | Address string `json:"address"` 18 | IsProvider bool `json:"is_provider"` 19 | } 20 | 21 | type UserInteractor struct { 22 | UserRepository domain.UserRepository 23 | } 24 | 25 | func (interactor *UserInteractor) RegisterUser(user User) error { 26 | // map user to dUser 27 | interactor.UserRepository.StoreUser(mapToDomainUser(user)) 28 | return nil 29 | } 30 | 31 | func (interactor *UserInteractor) ListUsers() ([]User, error) { 32 | users, err := interactor.UserRepository.ListUsers() 33 | if err != nil { 34 | return nil, err 35 | } 36 | // map domain users to usecases user 37 | dusers := make([]User, len(users), len(users)) 38 | for i := range users { 39 | dusers[i] = mapFromDomainUser(users[i]) 40 | } 41 | return dusers, nil 42 | } 43 | 44 | func mapFromDomainUser(dUser domain.User) (user User) { 45 | user.Id = dUser.ID.Hex() 46 | user.DispName = dUser.DispName 47 | user.Name = dUser.Name 48 | user.Phone = dUser.Phone 49 | user.EmailId = dUser.EmailId 50 | user.Password = "******" 51 | user.Address = dUser.Address 52 | user.IsProvider = dUser.IsProvider 53 | return 54 | } 55 | 56 | func mapToDomainUser(user User) (dUser domain.User) { 57 | dUser.DispName = user.DispName 58 | dUser.Name = user.Name 59 | dUser.Phone = user.Phone 60 | dUser.EmailId = user.EmailId 61 | dUser.Password = user.Password 62 | dUser.Address = user.Address 63 | dUser.IsProvider = user.IsProvider 64 | dUser.CreateTs = time.Now().UTC() 65 | return 66 | } 67 | --------------------------------------------------------------------------------