├── db ├── utils.go ├── builder.go ├── advanced.go ├── crud.go └── database.go ├── queue ├── queue.go └── jobs.go ├── go.mod ├── request └── request.go ├── LICENSE ├── go.sum ├── router └── router.go ├── mail └── mail.go ├── config └── config.go ├── response └── response.go ├── pulsar.go └── README.md /db/utils.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "strings" 4 | 5 | // IsNew checks if the model was already saved 6 | func (b *DB) IsNew(model interface{}) bool { 7 | return b.NewRecord(model) 8 | } 9 | 10 | // enhanceQuery params 11 | func enhanceQuery(query interface{}) interface{} { 12 | if s, ok := query.(string); ok && !strings.Contains(s, "?") { 13 | return query.(string) + " = ?" 14 | } 15 | 16 | return query 17 | } 18 | -------------------------------------------------------------------------------- /queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "github.com/panjf2000/ants" 4 | 5 | // Pool defines the default queue pool 6 | var Pool *ants.Pool 7 | 8 | // NewPool creates the default queue pool 9 | func NewPool(number int) { 10 | // Create that pool 11 | pool, _ := ants.NewPool(number) 12 | Pool = pool 13 | } 14 | 15 | // Dispatch runs the handler in a goroutine 16 | func Dispatch(handler func()) error { 17 | return Pool.Submit(handler) 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pulsar-go/pulsar 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/go-sql-driver/mysql v1.4.1 // indirect 8 | github.com/jinzhu/gorm v1.9.2 9 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect 10 | github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8 11 | github.com/julienschmidt/httprouter v1.2.0 12 | github.com/kabukky/httpscerts v0.0.0-20150320125433-617593d7dcb3 13 | github.com/lib/pq v1.0.0 // indirect 14 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 15 | github.com/panjf2000/ants v1.0.0 16 | github.com/rs/cors v1.3.0 17 | ) 18 | -------------------------------------------------------------------------------- /queue/jobs.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | /* 4 | newJob(...).dispatch() 5 | */ 6 | type Job struct { 7 | Handler func() 8 | ShouldQueue bool 9 | } 10 | 11 | // NewJob creates a new Job struct 12 | func NewJob(handler func()) *Job { 13 | return &Job{ 14 | Handler: handler, 15 | ShouldQueue: false, 16 | } 17 | } 18 | 19 | // Queue tells the job to run on a goroutine or not. 20 | func (j *Job) Queue(shouldQueue bool) *Job { 21 | j.ShouldQueue = shouldQueue 22 | return j 23 | } 24 | 25 | // Dispatch runs the handler in the queue or synchronously 26 | func (j *Job) Dispatch() error { 27 | if j.ShouldQueue { 28 | return Dispatch(j.Handler) 29 | } 30 | j.Handler() 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | ) 11 | 12 | // ErrorNoJSONHeader determines that the current request have no JSON headers. 13 | var ErrorNoJSONHeader = errors.New("a") 14 | 15 | // Type is the name of the response type. 16 | type Type uint 17 | 18 | // Indicate the available response types. 19 | const ( 20 | GetRequest Type = iota 21 | HeadRequest 22 | PostRequest 23 | PutRequest 24 | PatchRequest 25 | DeleteRequest 26 | ) 27 | 28 | // HTTP represents the web server request. 29 | type HTTP struct { 30 | Request *http.Request 31 | Body string 32 | Writer http.ResponseWriter 33 | Params httprouter.Params 34 | Additionals map[string]interface{} 35 | } 36 | 37 | // JSON transforms the input body that's formatted in 38 | func (req *HTTP) JSON(data interface{}) error { 39 | return json.NewDecoder(bytes.NewBufferString(req.Body)).Decode(data) 40 | } 41 | -------------------------------------------------------------------------------- /db/builder.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // First finds the first record with the given condition 4 | func (b *DB) First(out interface{}, where ...interface{}) *DB { 5 | return b.clone(b.DB.First(out, where...)) 6 | } 7 | 8 | // Last finds the last record with the given condition 9 | func (b *DB) Last(out interface{}, where ...interface{}) *DB { 10 | return b.clone(b.DB.Last(out, where...)) 11 | } 12 | 13 | // All finds all records of a given model 14 | func (b *DB) All(models interface{}, where ...interface{}) *DB { 15 | return b.clone(b.DB.Find(models, where...)) 16 | } 17 | 18 | // Where adds a condition to the query statement 19 | func (b *DB) Where(query interface{}, args ...interface{}) *DB { 20 | query = enhanceQuery(query) 21 | return b.clone(b.DB.Where(query, args...)) 22 | } 23 | 24 | // WhereNot adds a condition to the query statement 25 | func (b *DB) WhereNot(query interface{}, args ...interface{}) *DB { 26 | return b.clone(b.Not(query, args...)) 27 | } 28 | 29 | // OrWhere adds an or filter to the query 30 | func (b *DB) OrWhere(query interface{}, args ...interface{}) *DB { 31 | return b.clone(b.Or(query, args...)) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Èrik Campobadal Forés & Krishan König 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /db/advanced.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // Limit adds a limit to the query 4 | func (b *DB) Limit(limit interface{}) *DB { 5 | return b.clone(b.DB.Limit(limit)) 6 | } 7 | 8 | // Skip adds an offset to the query 9 | func (b *DB) Skip(skip interface{}) *DB { 10 | return b.clone(b.Offset(skip)) 11 | } 12 | 13 | // Count 's the database records in the given table 14 | func (b *DB) Count() interface{} { 15 | var count interface{} 16 | b.DB.Count(&count) 17 | 18 | return count 19 | } 20 | 21 | // Table specify the table you would like to run db operations 22 | func (b *DB) Table(name string) *DB { 23 | return b.clone(b.DB.Table(name)) 24 | } 25 | 26 | // Select specifies field you want to query from the database 27 | func (b *DB) Select(query interface{}, args ...interface{}) *DB { 28 | return b.clone(b.DB.Select(query, args...)) 29 | } 30 | 31 | // Group specify the group method on the find 32 | func (b *DB) Group(query string) *DB { 33 | return b.clone(b.DB.Group(query)) 34 | } 35 | 36 | // Having specify HAVING conditions for GROUP BY 37 | func (b *DB) Having(query string) *DB { 38 | return b.clone(b.DB.Having(query)) 39 | } 40 | 41 | // Scan scan value to a struct 42 | func (b *DB) Scan(dest interface{}) *DB { 43 | return b.clone(b.DB.Scan(dest)) 44 | } 45 | -------------------------------------------------------------------------------- /db/crud.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // Model specify the model you would like to run db operations 4 | func (b *DB) Model(value interface{}) *DB { 5 | return b.clone(b.DB.Model(value)) 6 | } 7 | 8 | // Create insert the value into database 9 | func (b *DB) Create(value interface{}) *DB { 10 | return b.clone(b.DB.Create(value)) 11 | } 12 | 13 | // Save update value in database, if the value doesn't have primary key, will insert it 14 | func (b *DB) Save(value interface{}) *DB { 15 | return b.clone(b.DB.Save(value)) 16 | } 17 | 18 | // Update update attributes with callbacks 19 | func (b *DB) Update(attrs ...interface{}) *DB { 20 | return b.clone(b.DB.Update(attrs...)) 21 | } 22 | 23 | // Updates update attributes with callbacks 24 | func (b *DB) Updates(values interface{}, ignoreProtectedAttrs ...bool) *DB { 25 | return b.clone(b.DB.Updates(values, ignoreProtectedAttrs...)) 26 | } 27 | 28 | // Delete delete value match given conditions, if the value has primary key, then will including the primary key as condition 29 | func (b *DB) Delete(value interface{}, where ...interface{}) *DB { 30 | return b.clone(b.DB.Delete(value, where...)) 31 | } 32 | 33 | // Unscoped return all record including deleted record 34 | func (b *DB) Unscoped() *DB { 35 | return b.clone(b.DB.Unscoped()) 36 | } 37 | -------------------------------------------------------------------------------- /db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/jinzhu/gorm" 11 | "github.com/pulsar-go/pulsar/config" 12 | 13 | // The following imports are for the database drivers. 14 | _ "github.com/jinzhu/gorm/dialects/mysql" 15 | _ "github.com/jinzhu/gorm/dialects/postgres" 16 | _ "github.com/jinzhu/gorm/dialects/sqlite" 17 | ) 18 | 19 | // Model represents the base database model. 20 | type Model struct { 21 | ID uint `json:"id,omitempty" gorm:"primary_key"` 22 | CreatedAt time.Time `json:"created_at,omitempty"` 23 | UpdatedAt time.Time `json:"updated_at,omitempty"` 24 | DeletedAt *time.Time `json:"deleted_at,omitempty" sql:"index"` 25 | } 26 | 27 | // DB represents the database structure used 28 | type DB struct { 29 | *gorm.DB 30 | } 31 | 32 | // Builder represents the current database used. 33 | var Builder *DB 34 | 35 | // Models stores the current set of application models. 36 | var Models []interface{} 37 | 38 | // AddModels add the given models to the model list. 39 | func AddModels(models ...interface{}) { 40 | Models = append(Models, models...) 41 | } 42 | 43 | // Open opens a new database connection. 44 | func Open() { 45 | // Create the arguments 46 | var args string 47 | // Copy to reduce code size. 48 | s := &config.Settings.Database 49 | switch s.Driver { 50 | case "mysql": 51 | args = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", s.User, s.Password, s.Host, s.Port, s.Database) 52 | case "postgres": 53 | args = fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s", s.Host, s.Port, s.User, s.Database, s.Password) 54 | case "sqlite3": 55 | f, err := filepath.Abs(filepath.Dir(os.Args[0]) + "/" + s.Database) 56 | if err != nil { 57 | log.Fatalf("Unable to get path of database %s\n", s.Database) 58 | } 59 | args = f 60 | default: 61 | log.Fatalf("Database driver '%s' is not supported.\n", s.Driver) 62 | } 63 | // Open the database 64 | dbOpened, err := gorm.Open(s.Driver, args) 65 | if err != nil { 66 | log.Fatalln(err) 67 | } 68 | 69 | Builder = &DB{dbOpened} 70 | } 71 | 72 | // clone creates a new instance of the DB 73 | func (b *DB) clone(lib *gorm.DB) *DB { 74 | return &DB{ 75 | lib, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 4 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 5 | github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw= 6 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 7 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 8 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 9 | github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8 h1:XMe1IsRiRx3E3M50BhP7327VYF4A9RpCFfhHUFW+IeE= 10 | github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= 11 | github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= 12 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 13 | github.com/kabukky/httpscerts v0.0.0-20150320125433-617593d7dcb3 h1:Iy7Ifq2ysilWU4QlCx/97OoI4xT1IV7i8byT/EyIT/M= 14 | github.com/kabukky/httpscerts v0.0.0-20150320125433-617593d7dcb3/go.mod h1:BYpt4ufZiIGv2nXn4gMxnfKV306n3mWXgNu/d2TqdTU= 15 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 16 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 17 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 18 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 19 | github.com/panjf2000/ants v1.0.0 h1:MZBsUt8W6ktQfhIswUZpw17IJlXY6ly2+U5b9jxwad4= 20 | github.com/panjf2000/ants v1.0.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY= 21 | github.com/panjf2000/ants v4.0.2+incompatible h1:6K9b4YyUmbTvfxGm2YZgmDgIiCEyP1x+gewN+tBr/8U= 22 | github.com/panjf2000/ants v4.0.2+incompatible/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY= 23 | github.com/rs/cors v1.3.0 h1:R0sy4XekGcOFoby9D76NXXg2birJ3WFkzGvXF9Kn3xE= 24 | github.com/rs/cors v1.3.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 25 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/pulsar-go/pulsar/request" 5 | "github.com/pulsar-go/pulsar/response" 6 | ) 7 | 8 | // Handler represents a route handler. 9 | type Handler func(req *request.HTTP) response.HTTP 10 | 11 | // Middleware represents a route middleware. 12 | type Middleware func(next Handler) Handler 13 | 14 | // Options represents the route options. 15 | type Options struct { 16 | Prefix string 17 | Middleware Middleware 18 | } 19 | 20 | // Route is the definition of a route. 21 | type Route struct { 22 | URI string 23 | Method request.Type 24 | Handler Handler 25 | } 26 | 27 | // Router determines how a route is. 28 | type Router struct { 29 | Routes []Route 30 | options Options 31 | Childs []*Router 32 | } 33 | 34 | // Routes representrs the global application routes. 35 | var Routes Router 36 | 37 | // Adds the route to the given router. 38 | func addRoute(r *Router, uri string, handler Handler, method request.Type) *Router { 39 | h := handler 40 | if r.options.Middleware != nil { 41 | h = r.options.Middleware(handler) 42 | } 43 | // Append the route to the list. 44 | r.Routes = append( 45 | r.Routes, 46 | Route{URI: r.options.Prefix + uri, Method: method, Handler: h}, 47 | ) 48 | return r 49 | } 50 | 51 | // Get creates a GET route. 52 | func (r *Router) Get(uri string, handler Handler) *Router { 53 | return addRoute(r, uri, handler, request.GetRequest) 54 | } 55 | 56 | // Head creates a HEAD route. 57 | func (r *Router) Head(uri string, handler Handler) *Router { 58 | return addRoute(r, uri, handler, request.HeadRequest) 59 | } 60 | 61 | // Post creates a POST route. 62 | func (r *Router) Post(uri string, handler Handler) *Router { 63 | return addRoute(r, uri, handler, request.PostRequest) 64 | } 65 | 66 | // Put creates a PUT route. 67 | func (r *Router) Put(uri string, handler Handler) *Router { 68 | return addRoute(r, uri, handler, request.PutRequest) 69 | } 70 | 71 | // Patch creates a PATCH route. 72 | func (r *Router) Patch(uri string, handler Handler) *Router { 73 | return addRoute(r, uri, handler, request.PatchRequest) 74 | } 75 | 76 | // Delete creates a DELETE route. 77 | func (r *Router) Delete(uri string, handler Handler) *Router { 78 | return addRoute(r, uri, handler, request.DeleteRequest) 79 | } 80 | 81 | // Group certain routes uner certain options. 82 | func (r *Router) Group(options *Options, routes func(r *Router)) *Router { 83 | router := &Router{options: *options} 84 | routes(router) 85 | r.Childs = append(r.Childs, router) 86 | return r 87 | } 88 | -------------------------------------------------------------------------------- /mail/mail.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "log" 5 | "net/smtp" 6 | "net/textproto" 7 | 8 | "github.com/pulsar-go/pulsar/config" 9 | "github.com/pulsar-go/pulsar/queue" 10 | 11 | "github.com/jordan-wright/email" 12 | ) 13 | 14 | // Mail exposes the Mail type. 15 | type Mail struct { 16 | Message *email.Email 17 | // Reserved for future things. 18 | } 19 | 20 | // Create creates a new empty mail. 21 | func Create() *Mail { 22 | mail := &Mail{Message: email.NewEmail()} 23 | return mail.From(config.Settings.Mail.From) 24 | } 25 | 26 | // ReplyTo determines where the message will get replied to. 27 | func (m *Mail) ReplyTo(replyTo []string) *Mail { 28 | m.Message.ReplyTo = replyTo 29 | return m 30 | } 31 | 32 | // From determines who the message is from (There is already a default value provided in the config here). 33 | func (m *Mail) From(from string) *Mail { 34 | m.Message.From = from 35 | return m 36 | } 37 | 38 | // To determines who the message is for. 39 | func (m *Mail) To(to ...string) *Mail { 40 | m.Message.To = to 41 | return m 42 | } 43 | 44 | // Bcc determines the Blind carbon copy. 45 | func (m *Mail) Bcc(bcc ...string) *Mail { 46 | m.Message.Bcc = bcc 47 | return m 48 | } 49 | 50 | // Cc determines the Carbon copy. 51 | func (m *Mail) Cc(cc ...string) *Mail { 52 | m.Message.Cc = cc 53 | return m 54 | } 55 | 56 | // Subject determines the Carbon copy. 57 | func (m *Mail) Subject(subject string) *Mail { 58 | m.Message.Subject = subject 59 | return m 60 | } 61 | 62 | // Text determines the message plaintext (optional). 63 | func (m *Mail) Text(text string) *Mail { 64 | m.Message.Text = []byte(text) 65 | return m 66 | } 67 | 68 | // HTML determines the message HTML (optional). 69 | func (m *Mail) HTML(html string) *Mail { 70 | m.Message.HTML = []byte(html) 71 | return m 72 | } 73 | 74 | // Sender override From as SMTP envelope sender (optional). 75 | func (m *Mail) Sender(sender string) *Mail { 76 | m.Message.Sender = sender 77 | return m 78 | } 79 | 80 | // Headers determine the message headers. 81 | func (m *Mail) Headers(headers textproto.MIMEHeader) *Mail { 82 | m.Message.Headers = headers 83 | return m 84 | } 85 | 86 | // AttachFile attaches a file to the mail. 87 | func (m *Mail) AttachFile(filename string) *Mail { 88 | _, err := m.Message.AttachFile(filename) 89 | if err != nil { 90 | log.Println(err) 91 | } 92 | return m 93 | } 94 | 95 | // SendNow sends the mail. 96 | func (m *Mail) SendNow() error { 97 | // Copy to keep the code length considerable. 98 | s := &config.Settings.Mail 99 | return m.Message.Send(s.Host+":"+s.Port, smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)) 100 | } 101 | 102 | // Send sends the mail. 103 | func (m *Mail) Send() error { 104 | // Copy to keep the code length considerable. 105 | s := &config.Settings.Mail 106 | return queue.Dispatch(func() { 107 | err := m.Message.Send(s.Host+":"+s.Port, smtp.PlainAuth(s.Identity, s.Username, s.Password, s.Host)) 108 | if err != nil { 109 | log.Println(err) 110 | } 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/BurntSushi/toml" 9 | ) 10 | 11 | // ServerConfig specifies the configuration for the server file. 12 | type ServerConfig struct { 13 | Host string `toml:"host"` 14 | Port string `toml:"port"` 15 | Development bool `toml:"development"` 16 | AllowedOrigins []string `toml:"allowed_origins"` 17 | AllowedHeaders []string `toml:"allowed_headers"` 18 | AllowedMethods []string `toml:"allowed_methods"` 19 | ExposedHeaders []string `toml:"exposed_headers"` 20 | AllowCredentials bool `toml:"allow_credentials"` 21 | } 22 | 23 | // CertificateConfig specifies the configuration for the certificate file. 24 | type CertificateConfig struct { 25 | Enabled bool `toml:"enabled"` 26 | CertFile string `toml:"cert_file"` 27 | KeyFile string `toml:"key_file"` 28 | } 29 | 30 | // ViewsConfig specifies the configuration for the view file. 31 | type ViewsConfig struct { 32 | Path string `toml:"path"` 33 | } 34 | 35 | // DatabaseConfig specifies the configuration for the database file. 36 | type DatabaseConfig struct { 37 | Driver string `toml:"driver"` 38 | Database string `toml:"database"` 39 | Host string `toml:"host"` 40 | Port string `toml:"port"` 41 | User string `toml:"user"` 42 | Password string `toml:"password"` 43 | AutoMigrate bool `toml:"auto_migrate"` 44 | } 45 | 46 | // MailConfig specifies the configuration for the mail file. 47 | type MailConfig struct { 48 | Host string `toml:"host"` 49 | Port string `toml:"port"` 50 | Identity string `toml:"identity"` 51 | Username string `toml:"username"` 52 | Password string `toml:"password"` 53 | From string `toml:"from"` 54 | } 55 | 56 | // QueueConfig specifies the configuration for the queue file. 57 | type QueueConfig struct { 58 | Routines string `toml:"routines"` 59 | } 60 | 61 | // Config represents the pulsar server settings structure. 62 | type Config struct { 63 | Server ServerConfig 64 | Certificate CertificateConfig 65 | Views ViewsConfig 66 | Database DatabaseConfig 67 | Mail MailConfig 68 | Queue QueueConfig 69 | } 70 | 71 | // Settings define the global settings for pulsar. 72 | var Settings Config 73 | 74 | // @todo revisit with map[string]interface{} to make it dynamic 75 | func setConfigOf(file string, v interface{}) { 76 | absPath, _ := filepath.Abs(filepath.Clean(filepath.Dir(os.Args[0]) + "/config/" + file + ".toml")) 77 | if _, err := toml.DecodeFile(absPath, v); err != nil { 78 | log.Fatalln("There was an error decoding file " + absPath + ", Error: " + err.Error()) 79 | } 80 | } 81 | 82 | // Set sets the configuration from a configuration file. 83 | func init() { 84 | // Server config 85 | setConfigOf("server", &Settings.Server) 86 | // Certificate config 87 | setConfigOf("certificate", &Settings.Certificate) 88 | // Views config 89 | setConfigOf("views", &Settings.Views) 90 | // Database config 91 | setConfigOf("database", &Settings.Database) 92 | // Mail config 93 | setConfigOf("mail", &Settings.Mail) 94 | // Queue config 95 | setConfigOf("queue", &Settings.Queue) 96 | // Transform the relative paths into absolute. 97 | Settings.Certificate.CertFile, _ = filepath.Abs(filepath.Dir(filepath.Dir(os.Args[0])+"/config") + "/" + filepath.Clean(Settings.Certificate.CertFile)) 98 | Settings.Certificate.KeyFile, _ = filepath.Abs(filepath.Dir(filepath.Dir(os.Args[0])+"/config") + "/" + filepath.Clean(Settings.Certificate.KeyFile)) 99 | } 100 | -------------------------------------------------------------------------------- /response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "log" 9 | "mime" 10 | "net/http" 11 | "path/filepath" 12 | 13 | "github.com/pulsar-go/pulsar/config" 14 | "github.com/pulsar-go/pulsar/request" 15 | ) 16 | 17 | // Type is the name of the response type. 18 | type Type uint 19 | 20 | // Indicate the available response types. 21 | const ( 22 | TextResponse Type = iota 23 | JSONResponse 24 | StaticResponse 25 | AssetResponse 26 | ViewResponse 27 | ) 28 | 29 | // HTTP is the web server response. 30 | type HTTP struct { 31 | StatusCode int 32 | Type Type 33 | TextData string 34 | JSONData interface{} 35 | } 36 | 37 | // Text returns a HTTP response with plain text. 38 | func Text(text string) HTTP { 39 | return HTTP{StatusCode: http.StatusOK, Type: TextResponse, TextData: text} 40 | } 41 | 42 | // TextWithCode is a Text response with additional status code. 43 | func TextWithCode(text string, code int) HTTP { 44 | res := Text(text) 45 | res.StatusCode = code 46 | return res 47 | } 48 | 49 | // JSON returns a HTTP response with the JSON headers. 50 | func JSON(data interface{}) HTTP { 51 | return HTTP{StatusCode: http.StatusOK, Type: JSONResponse, JSONData: data} 52 | } 53 | 54 | // JSONWithCode is a JSON response with additional status code. 55 | func JSONWithCode(data interface{}, code int) HTTP { 56 | res := JSON(data) 57 | res.StatusCode = code 58 | return res 59 | } 60 | 61 | // Static return a View response without templating data. 62 | func Static(name string) HTTP { 63 | path, err := filepath.Abs(filepath.Clean(config.Settings.Views.Path) + "/" + filepath.Clean(name+".html")) 64 | if err != nil { 65 | log.Println(err) 66 | } 67 | return HTTP{StatusCode: http.StatusOK, Type: StaticResponse, TextData: path} 68 | } 69 | 70 | // StaticWithCode is a Static response with additional status code. 71 | func StaticWithCode(name string, code int) HTTP { 72 | res := Static(name) 73 | res.StatusCode = code 74 | return res 75 | } 76 | 77 | // Asset return an asset response (css files, js files, images, etc.). 78 | func Asset(name string) HTTP { 79 | path, err := filepath.Abs(filepath.Clean(config.Settings.Views.Path) + "/" + filepath.Clean(name)) 80 | if err != nil { 81 | log.Println(err) 82 | } 83 | return HTTP{StatusCode: http.StatusOK, Type: AssetResponse, TextData: path} 84 | } 85 | 86 | // View return a View response with templating data. 87 | func View(name string, data interface{}) HTTP { 88 | path, err := filepath.Abs(filepath.Clean(config.Settings.Views.Path) + "/" + filepath.Clean(name+".gohtml")) 89 | if err != nil { 90 | log.Println(err) 91 | } 92 | return HTTP{StatusCode: http.StatusOK, Type: ViewResponse, TextData: path, JSONData: data} 93 | } 94 | 95 | // ViewWithCode is a View response with additional code. 96 | func ViewWithCode(name string, data interface{}, code int) HTTP { 97 | res := View(name, data) 98 | res.StatusCode = code 99 | return res 100 | } 101 | 102 | // Handle handles the HTTP request using a response writter. 103 | func (response *HTTP) Handle(req *request.HTTP) { 104 | writer := req.Writer 105 | switch response.Type { 106 | case TextResponse: 107 | writer.WriteHeader(response.StatusCode) 108 | fmt.Fprint(writer, response.TextData) 109 | case JSONResponse: 110 | result, err := json.Marshal(response.JSONData) 111 | if err != nil { 112 | fmt.Fprint(writer, "Error while marshaling JSON.") 113 | } 114 | writer.Header().Set("Content-Type", "application/json") 115 | writer.WriteHeader(response.StatusCode) 116 | fmt.Fprint(writer, string(result)) 117 | case StaticResponse, AssetResponse: 118 | content, err := ioutil.ReadFile(response.TextData) 119 | if err != nil { 120 | log.Println("File " + response.TextData + " not found.") 121 | } 122 | writer.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(response.TextData))) 123 | writer.WriteHeader(response.StatusCode) 124 | fmt.Fprint(writer, string(content)) 125 | case ViewResponse: 126 | writer.WriteHeader(response.StatusCode) 127 | template.Must(template.ParseFiles(response.TextData)).Execute(writer, response.JSONData) 128 | default: 129 | writer.WriteHeader(response.StatusCode) 130 | fmt.Fprint(writer, "Invalid HTTP response type.") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pulsar.go: -------------------------------------------------------------------------------- 1 | package pulsar 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | 12 | "github.com/julienschmidt/httprouter" 13 | "github.com/kabukky/httpscerts" 14 | "github.com/pulsar-go/pulsar/config" 15 | "github.com/pulsar-go/pulsar/db" 16 | "github.com/pulsar-go/pulsar/queue" 17 | "github.com/pulsar-go/pulsar/request" 18 | "github.com/pulsar-go/pulsar/router" 19 | "github.com/rs/cors" 20 | ) 21 | 22 | // fileExists determines if a file exists in a given path. 23 | func fileExists(path string) bool { 24 | _, err := os.Stat(path) 25 | return !os.IsNotExist(err) 26 | } 27 | 28 | // debugHandler is responsible for each http handler in debug mode. 29 | func developmentHandler(route *router.Route) func(http.ResponseWriter, *http.Request, httprouter.Params) { 30 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 31 | log.Printf("[PULSAR] Request %s\n", r.URL) 32 | req := &request.HTTP{Request: r, Writer: w, Params: ps} 33 | buff, err := ioutil.ReadAll(req.Request.Body) 34 | if err != nil { 35 | log.Printf("[PULSAR] Failed to read the request body\n") 36 | } 37 | req.Body = string(buff) 38 | req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buff)) 39 | res := route.Handler(req) 40 | res.Handle(req) 41 | } 42 | } 43 | 44 | // productionHandler is responsible for each http handler in debug mode. 45 | func productionHandler(route *router.Route) func(http.ResponseWriter, *http.Request, httprouter.Params) { 46 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 47 | req := &request.HTTP{Request: r, Writer: w, Params: ps} 48 | buff, _ := ioutil.ReadAll(req.Request.Body) 49 | req.Body = string(buff) 50 | req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buff)) 51 | res := route.Handler(req) 52 | res.Handle(req) 53 | } 54 | } 55 | 56 | // RegisterRoutes registers the routes. 57 | func RegisterRoutes(mux *httprouter.Router, r *router.Router) { 58 | // Register the routes. 59 | var handler func(*router.Route) func(http.ResponseWriter, *http.Request, httprouter.Params) 60 | if config.Settings.Server.Development { 61 | handler = developmentHandler 62 | } else { 63 | handler = productionHandler 64 | } 65 | for _, element := range r.Routes { 66 | route := element 67 | switch route.Method { 68 | case request.GetRequest: 69 | mux.GET(route.URI, handler(&route)) 70 | case request.HeadRequest: 71 | mux.HEAD(route.URI, handler(&route)) 72 | case request.PostRequest: 73 | mux.POST(route.URI, handler(&route)) 74 | case request.PutRequest: 75 | mux.PUT(route.URI, handler(&route)) 76 | case request.PatchRequest: 77 | mux.PATCH(route.URI, handler(&route)) 78 | case request.DeleteRequest: 79 | mux.DELETE(route.URI, handler(&route)) 80 | } 81 | } 82 | // Register his childs. 83 | for _, element := range r.Childs { 84 | RegisterRoutes(mux, element) 85 | } 86 | } 87 | 88 | // Serve starts the server. 89 | func Serve() error { 90 | router := &router.Routes 91 | mux := httprouter.New() 92 | // Register the application routes. 93 | RegisterRoutes(mux, router) 94 | // Register the CORS 95 | handler := cors.New(cors.Options{ 96 | AllowedOrigins: config.Settings.Server.AllowedOrigins, 97 | AllowedHeaders: config.Settings.Server.AllowedHeaders, 98 | AllowedMethods: config.Settings.Server.AllowedMethods, 99 | AllowCredentials: config.Settings.Server.AllowCredentials, 100 | ExposedHeaders: config.Settings.Server.ExposedHeaders, 101 | Debug: config.Settings.Server.Development, 102 | OptionsPassthrough: true, 103 | }).Handler(mux) 104 | // Set the address of the server. 105 | address := config.Settings.Server.Host + ":" + config.Settings.Server.Port 106 | // Generate SSL. 107 | generateSSLCertificate(address) 108 | // Set the database configuration 109 | db.Open() 110 | defer db.Builder.Close() 111 | // Migrate if nessesary 112 | if config.Settings.Database.AutoMigrate { 113 | db.Builder.AutoMigrate(db.Models...) 114 | } 115 | // Configure the queue system. 116 | routines, err := strconv.ParseInt(config.Settings.Queue.Routines, 10, 32) 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | queue.NewPool(int(routines)) 121 | defer queue.Pool.Release() 122 | if config.Settings.Server.Development { 123 | fmt.Println("-----------------------------------------------------") 124 | fmt.Println("| |") 125 | fmt.Println("| P U L S A R |") 126 | fmt.Println("| Go Web framework |") 127 | fmt.Println("| |") 128 | fmt.Println("| Erik Campobadal |") 129 | fmt.Println("| Krishan König |") 130 | fmt.Println("| |") 131 | fmt.Println("-----------------------------------------------------") 132 | fmt.Println() 133 | } 134 | if config.Settings.Certificate.Enabled { 135 | if config.Settings.Server.Development { 136 | fmt.Printf("Creating a HTTP/2 server with TLS on %s\n", address) 137 | fmt.Printf("Certificate: %s\nKey: %s\n\n", config.Settings.Certificate.CertFile, config.Settings.Certificate.KeyFile) 138 | } 139 | return http.ListenAndServeTLS(address, config.Settings.Certificate.CertFile, config.Settings.Certificate.KeyFile, handler) 140 | } 141 | if config.Settings.Server.Development { 142 | fmt.Printf("Creating a HTTP/1.1 server on %s\n\n", address) 143 | } 144 | return http.ListenAndServe(address, handler) 145 | } 146 | 147 | // generateSSLCertificate creates an ssl certificate if https is enabled 148 | func generateSSLCertificate(address string) { 149 | // Generate a SSL certificate if needed. 150 | if !config.Settings.Certificate.Enabled { 151 | return 152 | } 153 | 154 | err := httpscerts.Check(config.Settings.Certificate.CertFile, config.Settings.Certificate.KeyFile) 155 | if err == nil { 156 | return 157 | } 158 | 159 | // If they are not available, generate new ones. 160 | err = httpscerts.Generate(config.Settings.Certificate.CertFile, config.Settings.Certificate.KeyFile, address) 161 | if err != nil { 162 | log.Fatal("Unable to create HTTP certificates.") 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pulsar - A Go Web framework for web artisans 2 | 3 | This is a go web microframework for web artisans willing to create amazing go web applications with ease. 4 | 5 | ## Features 6 | 7 | - Server configuration in a single file 8 | - A blazing fast router ([github.com/julienschmidt/httprouter](https://github.com/julienschmidt/httprouter)) 9 | - Simplified request / response API 10 | - MVC architecture 11 | - Middlewares 12 | - Automatic headers for different responses 13 | - Automatic TLS (SSL) certificate using openssl cli 14 | - Automatic server creation using HTTP/1.1 or HTTP/2 15 | - Database Configuration + ORM 16 | - Emails 17 | 18 | ## How to use 19 | 20 | You first need to get the package: 21 | 22 | ``` 23 | go get github.com/pulsar-go/pulsar 24 | ``` 25 | 26 | **IMPORTANT**: Check the official example here: 27 | 28 | Then you'll need to create some server configuration (`server.toml` for example): 29 | 30 | ```toml 31 | # Server stores all the settings releated 32 | # to the HTTP / HTTP server itself. 33 | [server] 34 | # Host determines the IP or name of the 35 | # HTTP server. 36 | host = "" 37 | # Port specifies the port that is going to 38 | # be used to run the HTTP server. 39 | port = "8080" 40 | # Development specifies if the server is going 41 | # to run in development mode, allowing some output 42 | # in the terminal while the application runs giving 43 | # help and insight of what's going on. 44 | development = true 45 | 46 | # HTTPS stores all the settings releated 47 | # to the TLS (SSL) settings used to ensure 48 | # encrypted connections. 49 | [https] 50 | # Enables or disables the HTTPs server. 51 | # Please notice that this should always be 52 | # enabled. HTTPs should always be used. 53 | # Only a HTTP or HTTPs server can be fired 54 | # so if enabled, no HTTP server will be provided. 55 | enabled = true 56 | # Certificate file that the HTTPs 57 | # server will use to encrypt connections. 58 | # Auto generated if no cert_file and key_file 59 | # exists in the path provided. 60 | cert_file = "./server.cert" 61 | # Key file that the HTTPs 62 | # server will use to encrypt connections. 63 | # Auto generated if no cert_file and key_file 64 | # exists in the path provided. 65 | key_file = "./server.key" 66 | 67 | # Views stores all the settings releated 68 | # to the views stored in the application 69 | # used when delivering responses from 70 | # the web server. 71 | [views] 72 | # Path represents the relative or absolute 73 | # path where the views will come from. 74 | # It acts as a path prefix when returning views 75 | path = "./views" 76 | 77 | # Database stores all the settings releated 78 | # to the database connection that the ORM 79 | # will use in order to provide it's functionality 80 | [database] 81 | # Determines the driver to use 82 | # Possible options are: 83 | # 'mysql', 'postgres', 'sqlite3'. 84 | driver = "mysql" 85 | # Database represents the database name 86 | # to use or the database path in case of 87 | # using sqlite3 driver. If driver is sqlire3 88 | # the value ':memory:' can also be used to create 89 | # a temp database stored in memory. 90 | database = "sample" 91 | # Host represents the database host where 92 | # it will connect to. Unused if using 93 | # the sqlite3 driver. 94 | host = "localhost" 95 | # Port represents the database port where 96 | # it will connect to. Unused if using 97 | # the sqlite3 driver. 98 | port = "3306" 99 | # User represents the user used to establish 100 | # the database connection. Unused if using 101 | # the sqlite3 driver. 102 | user = "sample" 103 | # Password represents the password used to establish 104 | # the database connection. Unused if using 105 | # the sqlite3 driver. 106 | password = "secret" 107 | # Auto migrate is used to migrate the database shema 108 | # using the provided models. It will ONLY create tables, 109 | # missing columns and missing indexes, and WON’T change 110 | # existing column’s type or delete unused columns 111 | # to protect your data. 112 | auto_migrate = true 113 | 114 | # Mail stores all the information about 115 | # SMTP mailing to send any form of email. 116 | [mail] 117 | # Host determines the SMTP host that 118 | # is going to be used. 119 | host = "smtp.mailtrap.io" 120 | # Port determines the SMTP port that 121 | # is going to be used. 122 | port = "465" 123 | # Identity determines how the auth is 124 | # pretended to act as. Usually this 125 | # should be an empty string. 126 | identity = "" 127 | # Username used to authenticate when connecting 128 | # to the host. Part of the credentials. 129 | username = "" 130 | # Password used to authenticate when connecting 131 | # to the host. Part of the credentials. 132 | password = "" 133 | # From determines who the mail is going to be sent 134 | # from. This setting is the default from address used. 135 | from = "mail@example.com" 136 | ``` 137 | 138 | Then create a main file (`server.go` for example): 139 | ```go 140 | package main 141 | 142 | import ( 143 | "log" 144 | 145 | "github.com/pulsar-go/pulsar" 146 | "github.com/pulsar-go/pulsar/config" 147 | "github.com/pulsar-go/pulsar/router" 148 | "github.com/pulsar-go/pulsar/request" 149 | "github.com/pulsar-go/pulsar/response" 150 | ) 151 | 152 | type sample struct { 153 | Name string `json:"name"` 154 | Age int `json:"age"` 155 | } 156 | 157 | func index(req *request.HTTP) response.HTTP { 158 | return response.Text("Sample index response") 159 | } 160 | 161 | func user(req *request.HTTP) response.HTTP { 162 | return response.Text("User id: " + req.Params.ByName("id")) 163 | } 164 | 165 | func about(req *request.HTTP) response.HTTP { 166 | return response.JSON(sample{Name: "Erik", Age: 22}) 167 | } 168 | 169 | func sampleMiddleware(next router.Handler) router.Handler { 170 | return router.Handler(func(req *request.HTTP) response.HTTP { 171 | log.Println("Before route middleware") 172 | r := next(req) 173 | log.Println("After route middleware") 174 | return r 175 | }) 176 | } 177 | 178 | func main() { 179 | // Get the settings from the configuration files. 180 | config.Set("./server.toml") 181 | // Set the application routes. 182 | router.Routes. 183 | Get("/", index). 184 | Get("/user/:id", user). 185 | Group(&router.Options{Prefix: "/sample", Middleware: sampleMiddleware}, func(routes *router.Router) { 186 | routes.Get("/about", about) 187 | }) 188 | // Serve the HTTP server. 189 | log.Fatalln(pulsar.Serve()) 190 | } 191 | ``` 192 | 193 | ## Documentation 194 | 195 | - Pulsar: 196 | - Config: 197 | - Router: 198 | - Request: 199 | - Response: 200 | --------------------------------------------------------------------------------